ランク追跡の自動化は、API呼び出しをcronジョブで包んだだけのように見え、最初の1週間はその通りだ。問題は後になって現れる。異なる時刻に実行されたために比較できない2つの実行結果、チャートがおかしく見えるまで誰も気づかないシリーズ中の欠落日、ノイズに反応して発火するアラート、そしてクライアントから「なぜあなたの数字は私たちのものと一致しないのか」と聞かれること。
これらはいずれも取得(フェッチ)の問題ではない。パイプラインの問題であり、穴の空いた6か月分のデータを抱える前に、あらかじめ設計しておく価値がある。以下がその構築方法だ。
Decide the measurement contract first
コードを書く前に、2つの測定を比較可能にする変数を固定する必要がある。ランクはこれらなしでは意味を持たない。すなわち、位置、デバイス、言語、そして検索がパーソナライズされているかどうかだ。これらの選択はジョブ内で実行ごとのオプションではなく、定数となる。
理由は、これらのわずかなずれが、実際には起きていないランキング変動としてデータに現れてしまうからだ。月曜日はある都市から、火曜日は別の都市から測定されたキーワードは、動いたように見えてしまう。詳しい理由はmeasuring accurate keyword rankingsにあるが、要点を言えば、絶対的な精度よりも一貫性の方が重要だということだ。
契約をスキーマとして書き出しておこう。それはクライアントから数字の意味を聞かれたときに渡すものにもなる。
# one row per keyword per market: this is the unit of measurement
TARGET = {
"keyword": "residential proxies",
"country": "de",
"city": "berlin", # optional, only where local intent matters
"language": "de",
"device": "desktop",
}
Fetch through a SERP API
収集レイヤーをプロキシ上に自分で構築することもできるし、解析済みの結果を返すAPIを呼び出すこともできる。日次のモニタリングジョブにおいては、APIという選択肢が、実際に時間を消費する2つの保守負担、すなわち結果ページのレイアウト変更への追従と収集用インフラの運用を取り除いてくれる。
SERP APIは上記のパラメータを受け取り、構造化された結果を返す。したがってあなたのジョブは、フェッチとパーサーではなく、リクエストと書き込みだけになる。
import requests, datetime as dt
def fetch_serp(t):
r = requests.get("https://serp.shifter.io/v1", timeout=45, params={
"api_key": API_KEY,
"q": t["keyword"],
"gl": t["country"], # market
"hl": t["language"], # interface language
"location": t.get("city"),
"device": t["device"],
})
r.raise_for_status()
return r.json()
パラメータ名は変化していくため、ブログ記事を信用するのではなく、必ずAPIドキュメントで現在の名称を確認すること。レジデンシャルの出口を経由して自分で収集を行う選択肢については、why accurate rank tracking requires residential proxiesで取り上げている。そのトレードオフは制御と保守のどちらを取るかだ。
Store the whole result, not just your position
最もよくある設計上の誤りであり、後から修正するのに高いコストがかかるのが、キーワードごと・日ごとに単一の数値だけを保存することだ。
URLのランク付きリスト全体、結果機能の有無と位置、そして生のペイロードを保存すべきだ。理由は3つある。競合の動きは、自分自身の動きを解釈可能にする文脈になる。AIオーバービューがファーストビューに表示されるといった機能の変化は、数値自体を変えずにその順位の価値を変えてしまう。そして先週の火曜日のSERPを後から遡って収集することはできないため、保存しなかったものは失われる。
CREATE TABLE serp_snapshot (
id BIGSERIAL PRIMARY KEY,
keyword TEXT NOT NULL,
country TEXT NOT NULL,
city TEXT,
device TEXT NOT NULL,
captured_at TIMESTAMPTZ NOT NULL,
results JSONB NOT NULL, -- full ranked list
features JSONB NOT NULL, -- ai overview, local pack, shopping
raw JSONB -- keep it, storage is cheaper than regret
);
CREATE INDEX ON serp_snapshot (keyword, country, device, captured_at DESC);
順位はプライマリレコードとして保存するのではなく、スナップショットから導出する。これにより、パース処理の修正を今後の実行だけでなく履歴全体にも再適用できる。
Schedule for comparability
結果は一日の中でも変動するため、時間帯を変えて収集されたシリーズには帰属できないばらつきが生じる。だからこそ、毎日同じ時刻に、安定したウィンドウ内で実行すること。
すべてのキーワードを一度に発火させるのではなく、そのウィンドウ内で作業を分散させる。バースト的なアクセスは対象側への負荷が大きく、スロットルされやすい。締め切りが数時間先であるなら、分散させることに何のコストもかからない。リクエスト間にジッターを加え、同時実行数を制限し、実行全体を4分ではなく1時間かけて行おう。
import random, time
def run_daily(targets, window_seconds=3600):
gap = window_seconds / max(len(targets), 1)
for t in targets:
snapshot = fetch_with_retries(t)
store(t, snapshot)
time.sleep(gap * random.uniform(0.7, 1.3)) # spread, with jitter
Distinguish “no change” from “no data”
これは信頼できるシリーズと誤解を招くシリーズを分ける細部だ。フェッチが失敗して何も書き込まなければ、後で見る人には何も動かなかった日と同じように見える欠落が生じる。すると「前日」との比較が知らないうちに2日分をまたいでしまい、実際には起きていない変動を報告することになる。
すべての試行を明示的な結果とともに記録し、後続の分析が欠落をまたいだ比較を拒否するようにする。
def fetch_with_retries(t, attempts=3):
for i in range(attempts):
try:
data = fetch_serp(t)
if is_valid(data): # sanity-check the payload
return {"status": "ok", "data": data}
record_status(t, "invalid") # parsed, but not a real SERP
except requests.RequestException:
record_status(t, "error")
time.sleep(2 ** i * random.uniform(0.5, 1.5))
return {"status": "failed", "data": None} # written as a failure, not skipped
ペイロードの検証は、例外を捕捉することと同じくらい重要だ。レスポンスが正常に届いても、それが使える結果ページではない場合があるからだ。これはdetecting blocked or fake contentで取り上げている一般的な問題だ。
Alert on movement that means something
順位の変化すべてに単純にアラートを出すと、常時発火してしまう。誰も信用しないアラートシステムは、無いよりも悪い。
アラートを有用にする3つの規則がある。順位に比例したしきい値を要求すること。3位から6位への移動は、47位から50位への移動よりもはるかに重要だからだ。持続性、つまり2〜3回連続した実行での確認を要求すること。1日だけの揺れは通常の現象だからだ。そして1ページ目への出入りは別扱いでアラートを出すこと。その境界には実際のトラフィックへの影響があるからだ。
そして、人々が忘れがちなもう一つのアラートを追加する。収集自体の健全性だ。ある市場が3日前から静かにデータを返さなくなっていた場合、それはどのランキング変動よりも緊急性が高い。そしてそれは、実行結果をmonitoring a scraping pipelineにあるように、第一級のデータとして追跡している場合にのみ見える。
Make the data explain itself
2つの追加要素によって、順位のテーブルはクライアントに提示できるものになる。
自分自身のタイムラインに注釈を付ける。デプロイ、コンテンツの公開、移行などだ。「なぜ下落したのか」という調査の半分は、その週にリリースされたものが原因で終わる。注釈付きのチャートならすぐにそれを見つけられる。
キーワードごとの順位だけでなく、ポートフォリオ全体の変動も計算する。多数の無関係なキーワードにわたる広範な変化は、1つのページが順位を落とすこととは異なる事象だからだ。これはdetecting SERP volatility and algorithm updatesにあるボラティリティ分析であり、自分の順位だけでなく結果セット全体を保存していたからこそ可能になる。
Collect responsibly
公開されている検索結果のみを対象とし、各検索エンジンの利用規約を尊重し、レート制限を障害として扱うのではなく、丁寧なペースを保つこと。日次のモニタリングジョブが攻撃的である理由はない。締め切りは翌朝であり、作業を分散させることに何のコストもかからない。
The bottom line
cronジョブの部分は簡単だ。まず測定契約を固定し、位置、デバイス、言語、パーソナライズが偶発的な変数ではなく定数になるようにする。自分の順位だけでなく、結果セット全体と生のペイロードを保存する。競合の文脈と結果機能があるからこそ数値が解釈可能になるのであり、後から遡って取得することはできない。一定の時刻に実行し、ジッターを加えてウィンドウ内に分散させる。欠落が安定と誤解されないよう、失敗を明示的に記録する。持続的で順位に応じた重みづけをした変動と、収集の健全性の両方に対してアラートを出す。そして自分自身の変更に注釈を付け、チャートが人々が実際に尋ねる質問に答えられるようにする。
収集を保守せずに解析済みの結果を得たいなら、取得レイヤーはSERP APIになる。自分で運用したいなら、国と都市を指定できるresidential proxiesを使う方法があり、小規模で頻繁なチェックに適したper-GB pricingも用意されている。