クライアントリスト向けのバッチDR、40行のPythonジョブとして
ドメインのスプレッドシートと、各行に1つの数値を必要とするダッシュボードがあります。その作業用のバックリンクドメインチェッカーは主に配管作業であり、このスクリプトがその配管です。

スクリプト
domains.txtを読み取り、1リクエストあたり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クレジット。文書化された上限は1回の呼び出しあたり2,000ドメインです。
domains=
ここではGETパラメータです。エンドポイントはPOSTボディも受け付けますが、リストが長くなると後者がより良い選択です。フル実行前に、2つのドメインで区切り文字をテストしてください。
BATCH = 200
私たちの習慣であり、制限ではありません。200の短いドメインは、URLを一般的なプロキシの制限をはるかに下回るように保ちます。
_credits
認証済みレスポンスに含まれます:呼び出しのコストと残高。各バッチでこれを出力するのは、最も安価な監査証跡です。
生bodyカラム
後で、保存されたサンプルに対して1回だけ解析します。こちら側のスキーマ変更は、再実行ではなくクエリのコストで済みます。

仮想的な実行と算術
これは例であり、顧客事例ではありません。Googleシートで1,800のクライアントおよび見込みドメインを追跡し、毎週月曜日にDRを更新したいエージェントを想像してください。
バッチあたり200ドメインの場合、9回のリクエストになります。1クレジットずつの9回のリクエストは9クレジット、つまり1回の実行あたり$0.0009、52回の月曜日で合計468クレジット、5セント未満に丸められます。
コストが制約ではありません。新鮮さが制約です。インデックスは約月1回で再構築されるため、週次実行では4週間のうち3週間で同じ値が表示され、月次実行は約4分の1の呼び出しで同じ変化を捉えます。
したがって月次で実行し、週次シートは人間の編集用に保持してください。スコアが静止する可能性がある理由の詳細はDRリファレンスにあります。
このスクリプトで発生する問題
| 症状 | 原因 | 修正 |
|---|---|---|
SystemExit: HTTP 402 | 残高がゼロ | アカウントページからチャージして再実行してください。完了したバッチはすでにSQLiteに保存されています |
SystemExit: HTTP 401 | 環境変数が空、またはキーのタイプミス | len(KEY)を出力し、プレフィックスsk_を確認してください |
バッチ3でok=false | リスト内の無効な値(完全なURLなど) | 送信前にスキームとパスを削除してください。エラーテーブルを参照 |
| 先週と同じDR | インデックスはまだ再構築されていない | 想定内です。月次で比較してください |
| 長いURLが拒否された | バッチがGETには大きすぎる | BATCHを下げるか、POSTボディに移動 |
バッチサイズ、GETとPOST、長いリスト用
上限は1回の呼び出しあたり2,000ドメインですが、スクリプトはその10分の1を送信します。各呼び出しは同じ固定クレジットコストがかかるため、バッチサイズは価格の問題ではなく、失敗とペイロードの問題です。
なぜ200で、2,000ではないのか
小さなバッチは安価に失敗します。実行が途中で停止した場合、再実行が必要な末尾はリスト全体ではなく、1バッチの長さになります。
短いリストは、保存された各ボディを目で追えるほど小さく保ちます。これは、数値が間違っている場合に重要であり、数回の保存呼び出しよりも重要です。
後でバッチサイズを上げる場合は、1回、1つの定数で行い、2ドメインテストを再実行してください。大きな数字はテストをスキップする理由にはなりません。
バッチを上限ではなく、許容できる実行時間に合わせて計画してください。朝のレポートの前に完了する必要がある夜間ジョブは、1回限りのバックフィルよりも小さく、安全なバッチを許容できます。
短いリストにはGET、長いリストにはPOST
GET形式では、すべてのドメインがURLに入ります。プロキシやログ行がクエリを切り捨てるまで機能しますが、それが上記のテーブルでまずバッチを下げるよう指示する理由です。リストが成長したら、同じdomains値をPOSTボディで送信してください。
フル実行前に、2つのドメインで区切り文字とエンコーディングをテストしてください。余分なコンマは最初のバッチで表示され、そこで確認する方が安価です。
POSTボディは、多くのホストがデフォルトで保持するアクセスログからリストを除外します。これはクライアントリストにとって小さなプライバシーの向上であり、スクリプトに1行のヘッダーを追加するコストで済みます。
POSTに切り替えるときは、短いリスト用のフォールバックとしてGETバージョンのスクリプトを保持してください。2つのコードパスは重く聞こえますが、長さによって1つを選択する小さな関数1つであれば、読みやすく、テストしやすいです。
最初のリクエストの前にトリムと重複排除
空白を削除し、ホストを小文字にし、重複を削除してから、データがマシンから離れるようにします。重複は2,000ドメインの上限内でスペースを占有し、シート内のカウントとレスポンス内のカウントの不一致を引き起こします。
www.のルールを1つ選び、1つの関数で適用してください。同じサイトが2つのスペルで検索されないようにします。数百の名前を超えてスケーリングするバックリンクドメインチェッカーには、その規律が必要です。
実行ログに記録すべき内容
各バッチの後に_credits値を出力し、バッチ番号、ドメイン数、時刻をその横に書き出します。これらの3つの数字は、財務担当者が実行について尋れるほぼすべての質問に答えます。
最後まで何も出力しない実行は、停止したときに手がかりを与えません。バッチごとの行は安価であり、サイレントハングを名前を付けられるバッチ番号に変えます。
ログをSQLiteファイルと同じフォルダに保持し、1つのバックアップで両方をコピーできるようにしてください。別の場所にあるログは、インシデント中に紛失するものです。

午後の時間をかける価値のある3つの拡張機能
タイムスタンプの横にバッチ番号を保存します。それは上記のテーブルにもあり、実行中の402後に失敗したバッチのみを再実行できます。
実際のレスポンスを検査した後、生テーブルの上に小さなビューを書いてください:ドメイン、スコア、フェッチ日時。ダッシュボードはビューを読み取り、生カラムは数値が疑問視された場合の証拠として残ります。
GitHub Actionsまたは小型VPSのプレーンcronから実行してください。どちらでも構いません。重要なのは、ジョブが失敗時に非ゼロで終了することです。サイレントな失敗は、3週間後に古くなったチャートのように見えます。
出力のソートはフロントエンドの問題です。ポートフォリオをスコアでランク付け、バケット化、またはdr_distribution(1クレジット)と比較して、各クライアントが181,132,097ドメイン全体の中でどこに位置するかを確認します。
フィールドのドリフトと生テーブル
長期間実行されるジョブは、フィールドの形状が変更されると静かに失敗します。生カラムは、その失敗が顕著で修正が安価であることを保証するためにあります。
生ボディを最初に保存
生カラムはセーフティネットです。フィールドがリネームされたか、新しいものが追加された場合、リスト全体に料金を支払うのではなく、保存されたレスポンスに対してクエリを再実行します。後での解析は安価ですが、再購入はそうではありません。
週ごとに1つの保存されたサンプルを独自のファイルにも保持してください。解析が失敗した場合、今週のサンプルを最後に機能したものと比較します。
受信どおりのボディを、整形やトリミングなしで正確に保存します。保存前にテキストに加えた変更は、後で元に戻せない変更です。
実際のドリフトの例
ダッシュボードが突然1つのバッチで空のスコアを表示するとします。生行はまだそこにあります。開き、キーを先週のサンプルと比較し、数分で違いを見つけます。
修正はリスト全体の再実行ではなく、ビュー内の1行です。それこそが生ボディを保存する理由です。
ビューの横に、どのフィールドを読み取り、最後に実際のレスポンスでいつ確認したかを示す短いメモを保持してください。このメモを書くのは1分ですが、後の長い検索を節約します。
データとともに移行を順序付ける
生テーブルを作成し、次にビューを作成し、その後でダッシュボードクエリを作成します。作成ステートメントを1つのファイルに保持し、順序通りに実行し、各行にバッチ番号を記録します。
クライアントごとの分離データベースは、共有データベースと同じ方法でドリフトします。ルールは同じです:呼び出しごとに1つの保存ボディ、そしてそれを解析する1つの場所。
パーサーをテストの隣に保持
保存されたボディの1つを読み取るテストと同じリポジトリにパーサー関数を配置します。テストは、チャートよりもずっと前にドリフトしたフィールドが最初に現れる場所です。
パーサーが期待するフィールド名をファイルの上部近くの1つの定数に名前を付けてください。レビュアーは、レスポンスの形状に対するすべての依存関係を1画面で確認できます。
最初のラン、順序通り
- 1
1ドメインをテスト
既知の単一ドメインで
bulk_drを呼び出し、レスポンス全体を読み取ります。解析コードを書く前にキーを書き留めてください。 - 2
2バッチを実行
スクリプトに400ドメインのリストを与え、ちょうど2バッチが実行されるようにします。
_creditsの減少が、1クレジットずつの2回の呼び出しと一致することを確認してください。 - 3
保存された行数をカウント
SQLiteファイルを開き、行数をバッチ数と比較します。不一致は、バッチがスキップされたか、2回保存されたことを意味し、ダッシュボードよりも前にそれを見つけます。
- 4
フルリストをスケジュール
テストランがクリーンな場合にのみ、フルリストとスケジューラを追加します。非ゼロの終了を保持し、最初の夜に失敗が顕明になるようにします。
スケジュールする前に
- まずリストの重複排除と小文字化
www.を残すか削除するかを、1つの関数で1回決定- キーをcrontabではなくシークレットストアに保持
- 非ゼロの終了コードでアラート
- 自分のボリュームに対してクレジット予算の数値を実行