클라이언트 목록을 위한 대량 DR, 40줄짜리 Python 작업으로
도메인 스프레드시트와 각 행당 하나의 숫자가 필요한 대시보드가 있다면, 해당 작업을 위한 백링크 도메인 체커는 대부분 배관 공사와 같으며 이 스크립트가 바로 그 배관 공사에 해당합니다.

스크립트
domains.txt를 읽고 요청당 200개의 도메인을 전송하며, 각 원본 응답을 SQLite에 저장하고 실행 비용을 출력합니다. 필드명은 블로그 게시물이 아닌 실제 응답에서 복사하는 것이 가장 정확하므로 전체 페이로드를 저장합니다.
import os, sqlite3, time, json, requests
BASE = "https://seomasters.dev/api.php"
KEY = os.environ["SEO_API_KEY"]
BATCH = 200
db = sqlite3.connect("dr.db")
db.execute("create table if not exists raw (ts int, batch text, body text)")
domains = [d.strip() for d in open("domains.txt") if d.strip()]
spent = 0
for i in range(0, len(domains), BATCH):
chunk = domains[i:i + BATCH]
for attempt in range(4):
try:
r = requests.get(BASE, timeout=30,
params={"action": "bulk_dr", "domains": ",".join(chunk)},
headers={"Authorization": "Bearer " + KEY})
break
except requests.RequestException:
time.sleep(2 ** attempt)
else:
raise SystemExit(f"network gave up at batch {i}")
if r.status_code in (401, 402):
raise SystemExit(f"HTTP {r.status_code}: fix the key or top up, do not retry")
body = r.json()
if not body.get("ok"):
raise SystemExit(f"ok=false at batch {i}: {json.dumps(body)[:300]}")
db.execute("insert into raw values (?,?,?)", (int(time.time()), str(i), r.text))
db.commit()
print(body.get("_credits"))각 부분의 역할
action=bulk_dr
호출당 1 크레딧이 소모됩니다. 문서화된 상한선은 호출당 2,000개 도메인입니다.
domains=
GET 파라미터입니다. 엔드포인트는 POST 본문도 허용하며, 목록이 길어질수록 POST가 더 나은 선택입니다. 전체 실행 전에 두 도메인으로 구분자를 테스트하세요.
BATCH = 200
우리의 관행일 뿐 제한 사항은 아닙니다. 짧은 도메인 200개는 URL을 일반적인 프록시 제한보다 훨씬 아래로 유지합니다.
_credits
인증된 응답에 표시됩니다: 호출 비용과 잔여량입니다. 각 배치마다 이를 출력하는 것이 가장 저렴한 감사 추적 방법입니다.
원본 body 열
저장된 샘플을 기준으로 나중에 파싱합니다. 서버 측 스키마 변경 시 재실행이 아닌 쿼리 한 번으로 해결할 수 있습니다.

가상의 실행 시나리오와 산술
이是客户故事,而是一个示例。想象一家机构在 Google Sheet 中跟踪 1,800 个客户和潜在客户域名,并希望每周一刷新 DR。
배치당 200개 도메인 기준 9번의 요청이 필요합니다. 요청당 1 크레딧씩 총 9 크레딧, 즉 실행당 $0.0009이며, 52번의 월요일을 기준으로 총 468 크레딧, 5센트 미만입니다.
비용이 제약 조건이 아닙니다. 신선도가 핵심입니다. 색인은 약 월간으로 재빌드되므로 주간 실행은 4주 중 3주 동안 동일한 값을 보여주며, 월간 실행은 호출량의 약 4분의 1로 동일한 변화를 포착합니다.
따라서 월간으로 실행하고 주간 스프레드시트는 수동 편집용으로 유지하세요. 점수가 제자리에 머무는 이유에 대한 자세한 내용은 DR 레퍼런스를 참조하세요.
스크립트에서 발생할 수 있는 문제들
| 증상 | 원인 | 해결책 |
|---|---|---|
SystemExit: HTTP 402 | 잔액이 0 | 계정 페이지에서 충전 후 재실행; 완료된 배치는 이미 SQLite에 저장됨 |
SystemExit: HTTP 401 | 환경 변수 비어 있음 또는 키 오타 | len(KEY)를 출력하고 접두사 sk_를 확인하세요 |
배치 3에서 ok=false | 목록 내 잘못된 값 (예: 전체 URL) | 전송 전 스키마와 경로를 제거하세요. 오류 테이블 참조 |
| 지난주와 동일한 DR | 색인이 아직 재빌드되지 않음 | 예상됨; 월간으로 비교하세요 |
| 긴 URL 거부됨 | GET 요청에 배치 크기가 너무 큼 | BATCH를 낮추거나 POST 본문으로 이동 |
긴 목록을 위한 배치 크기, GET 및 POST
상한선은 호출당 2,000개 도메인이며, 스크립트는 그 10분의 1만큼 전송합니다. 호출당 고정 크레딧 비용이 동일하므로 배치 크기는 가격 문제가 아니라 실패율과 페이로드의 문제입니다.
왜 200이고 2,000이 아닌가
작은 배치는 저렴하게 실패합니다. 실행이 중간에 중단되면 다시 실행해야 하는 남은 분량은 전체 목록이 아닌 한 배치 분량입니다.
짧은 목록은 저장된 본문이 눈으로 스캔할 수 있을 만큼 작게 유지되므로, 몇 번의 저장된 호출보다 훨씬 중요합니다.
나중에 배치 크기를 높이려면 상수 하나에서 한 번에 변경하고 두 도메인 테스트를 다시 실행하세요. 큰 숫자가 테스트를 건너뛰는 이유가 될 수는 없습니다.
상한선이 아닌 허용 가능한 실행 길이에 맞춰 배치를 계획하세요. 아침 리포트 전에 완료해야 하는 야간 작업은 일회성 백필보다 작고 안전한 배치를 감당할 수 있습니다.
짧은 목록은 GET, 긴 목록은 POST
GET 형태는 모든 도메인을 URL에 넣습니다. 프록시나 로그 줄이 쿼리를 잘라낼 때까지 작동하지만, 목록이 커지면 동일한 domains 값을 POST 본문에 보냅니다.
전체 실행 전에 두 도메인으로 구분자와 인코딩을 테스트하세요. 여분의 콤마는 첫 번째 배치에서 나타나며, 그곳에서 보는 것이 더 저렴합니다.
POST 본문은 목록을 많은 호스트가 기본적으로 유지하는 액세스 로그에서 제외합니다. 이는 클라이언트 목록에 대한 작은 프라이버시 이득이며, 스크립트에 한 줄의 헤더만 추가로 필요합니다.
POST로 전환할 때는 짧은 목록을 위한 GET 버전 스크립트를 대체용으로 유지하세요. 두 가지 코드 경로가 무겁게 보일 수 있지만, 길이에 따라 하나를 선택하는 작은 함수는 읽기 쉽고 테스트하기 쉽습니다.
첫 요청 전에 공백 제거 및 중복 제거
호스트를 소문자로 변환하고 중복을 제거한 후 데이터가 머신에서 나가야 합니다. 중복은 2,000개 도메인 상한선 아래 공간을 차지하며, 스프레드시트의 카운트와 응답의 카운트가 불일치하게 만듭니다.
www.에 대한 규칙을 하나 정하고 단일 함수에서 적용하여 동일한 사이트가 두 가지 철자로 조회되지 않도록 하세요. 수백 개 이상의 이름을 확장하는 백링크 도메인 체커라면 그 규율이 필요합니다.
실행 로그는 이렇게 표시되어야 합니다
각 배치 후 _credits 값을 출력하고 배치 번호, 도메인 수, 시간을 함께 기록하세요. 이 세 숫자는 재무 담당자가 실행에 대해 묻는 거의 모든 질문에 답합니다.
끝까지 아무것도 출력하지 않는 실행은 중단되었을 때 단서를 주지 못합니다. 배치당 한 줄은 저렴하며, 침묵하는 멈춤을 이름 붙일 수 있는 배치 번호로 바꿉니다.
SQLite 파일과 같은 폴더에 로그를 유지하세요. 하나의 백업으로 둘을 복사할 수 있습니다. 다른 곳에 있는 로그는 사고 발생 시 가장 먼저 사라지는 것입니다.

오후 시간을 투자할 가치가 있는 세 가지 확장
타임스탬프 옆에 배치 번호를 저장하세요. 이미 위의 표에 있으며, 실행 중 402 오류 발생 후 실패한 배치만 다시 실행할 수 있게 해줍니다.
실제 응답을 조사한 후 원본 테이블 위에 작은 뷰를 작성하세요: 도메인, 점수, 조회 시간. 대시보드는 뷰를 읽으며, 원본 열은 숫자가 의심될 때 증거로 남습니다.
GitHub Actions 또는 작은 VPS의 일반 cron에서 실행하세요. 둘 다 괜찮습니다. 중요한 것은 실패 시 비-zero 종료 코드여야 한다는 점입니다. 그렇지 않으면 3주 후에 정체가 없는 차트처럼 보입니다.
출력 정렬은 프론트엔드 문제입니다. 포트폴리오를 점수로 순위 매기기, 범주화 또는 dr_distribution(1 크레딧)과 비교하여 각 클라이언트가 181,132,097개 전체 도메인 중 어디에 위치하는지 확인하세요.
필드 드리프트와 원본 테이블
장기 실행 작업은 필드 형태가 변경될 때 조용히 실패합니다. 원본 열은 실패가 크고 저렴하게 수정되도록 존재합니다.
원본 본문 먼저 저장
원본 열은 안전망입니다. 필드가 이름 변경되거나 새 필드가 나타나면 전체 목록에 다시 지불하는 대신 저장된 응답에 대해 쿼리를 다시 실행합니다. 나중에 파싱하는 것은 저렴하지만 다시 구매하는 것은 그렇지 않습니다.
매주 저장된 샘플을 별도의 파일로 하나 유지하세요. 파싱이 깨지면 이번 주 샘플과 마지막으로 작동했던 샘플을 비교합니다.
수정 없이 그대로 저장하세요. 저장 전에 텍스트에 가한 모든 변경은 나중에 되돌릴 수 없습니다.
실제 드리프트의 모습
대시보드가 갑자기 한 배치에 대해 빈 점수를 표시한다고 가정하세요. 원본 행은 여전히 있습니다. 열기를 열고 마지막 주 샘플과 키를 비교하면 몇 분 만에 차이를 찾을 수 있습니다.
해결책은 전체 목록 재실행이 아닌 뷰의 한 줄입니다. 이것이 본문을 저장하는 전부가 있습니다.
뷰 옆에 해당 필드를 읽고 마지막 실제 응답 확인 시기를 기록한 짧은 메모를 유지하세요. 작성하는 데 1분이 걸리지만 나중에 긴 검색을 절약합니다.
데이터와 함께 마이그레이션 순서 정하기
원본 테이블을 먼저 생성하고, 뷰를 그 다음 생성한 후 대시보드 쿼리를 만드세요. 생성문을 하나의 파일에 유지하고 순서대로 실행하며 각 행의 배치 번호를 기록하세요.
클라이언트별 분리된 데이터베이스도 공유 데이터베이스와 같은 방식으로 드리프트합니다. 규칙은 동일합니다: 호출당 하나의 저장된 본문, 그리고 파싱할 하나의 장소.
파서를 테스트 옆에 유지
저장된 본문 하나를 읽는 테스트와 함께 파스 함수를 같은 저장소에 두세요. 테스트는 차트보다 훨씬 먼저 드리프트된 필드가 나타나는 곳입니다.
파서가 기대하는 필명들을 파일 상단 근처의 상수로 명명하세요. 검토자는 응답 형태에 대한 모든 의존성을 한 화면에서 확인할 수 있습니다.
순서대로 첫 실행
- 1
도메인 하나 테스트
알고 있는 단일 도메인으로
bulk_dr를 호출하고 전체 응답을 읽으세요. 파싱 코드 작성 전에 키를 적어두세요. - 2
두 배치 실행
스크립트에 400개 도메인 목록을 제공하여 정확히 두 배치의 실행을 확인하세요.
_credits감소가 호출당 1 크레딧 두 호출과 일치하는지 확인하세요. - 3
저장된 행 세기
SQLite 파일을 열고 행 수를 배치 수와 비교하세요. 불일치는 배치 건너뛰기 또는 중복 저장을 의미하며, 대시보드보다 먼저 발견할 수 있습니다.
- 4
전체 목록 예약
테스트 실행이 깨끗한 후에야 전체 목록과 스케줄러를 추가하세요. 비-zero 종료를 유지하여 첫 밤에 실패가 명확하게 드러나도록 하세요.
예약하기 전
- 목록 먼저 중복 제거 및 소문자 변환
www.유지 여부 결정은 단일 함수에서 한 번에- 키를 crontab이 아닌 시크릿 스토어에 보관
- 비-zero 종료 코드에 알림 설정
- 자신의 사용량에 맞는 크레딧 예산 숫자를 계산하세요