サイトが通常のクロール応答から完全なブロックへ一気に移行することはほとんどない。最初に起きるのはもっと静かな変化だ。チャレンジページに当たるリクエストが少し増える。いくつかのレスポンスは200 OKを返すが中身に使えるものがない。レイテンシがじわじわ上がる。一部のレコードが検証に失敗する。それぞれの信号は単体ではノイズに見え、それぞれが別のダッシュボードに存在するため、データセットに穴が開くまで誰もそれらを結びつけない。
ターゲットヘルススコアはその解決策だ。サイトごとに1つの数値を持ち、そのサイトがまだ自分自身の通常の振る舞いをしているかどうかを、理由付きで示す。この記事では、どの信号がそれに入力されるか、自分を欺かずにどう組み合わせるか、そしてその数値が下がったときにクローラーが何をすべきかを扱う。
ターゲットヘルスはプロキシヘルスではない
何を測っているのかを正確にしておく価値がある。この二つは混同されやすい。
プロキシヘルスは、あるルートが機能しているかを問う。ゲートウェイ、国、セッション、出口といったものだ。ターゲットヘルスは、特定のサイトがまだあなたにサービスを提供する意志と能力を持っているかを問う。死んだプロキシルートはすべてのサイトに対して失敗する。あなたに敵対的になったサイトはすべてのルートにわたって失敗する。
この違いが最初の診断になる。失敗が増えたら、それをルートごとに分けてみる。特定の国、特定のプール、特定のセッションパターンに集中しているなら、それはルートの問題であり、大規模なレジデンシャルプロキシヘルスの監視で述べたアプローチが当てはまる。そのサイトに送るすべてのルートで均等に失敗が増えているなら、サイトがあなたに対する態度を変えたということであり、それこそこのスコアが対象とするものだ。
信号
信号は、それが明らかにするものによってグループ化する。あるものは大声で、あるものはほとんど静かで、静かなものほど見逃すコストが高い。
| 信号 | どう見えるか | なぜ重要か |
|---|---|---|
| ハードブロック | 403、429、接続リセット | 明示的な拒否。数えるのは容易 |
| チャレンジ | CAPTCHAやインタースティシャルページ | サイトが自動化を疑い、テストしている |
| ソフトブロック | ブロックページ、空の結果、簡略化されたレイアウトを伴う200 OK | 成功に偽装された拒否 |
| レイテンシの変動 | そのサイト自身の通常値に対してp95応答時間が上昇 | しばしば意図的な遅延、時には単なる負荷 |
| 抽出失敗 | 必須フィールドが欠落、スキーマが一致しなくなる | ページが変更された、あるいは違うものを提供されている |
| コストの変動 | 有効なレコード1件あたりのリトライ、バイト数、クレジットの増加 | 上記すべてが金額として表れたもの |
ソフトブロックは特に注意が必要だ。ステータスコードは嘘をつく。キューバからのジオブロッキングに関する2023年の測定では、32のドメインが200 OKステータスでブロックページを提供していた。詳細はどの国が最もジオブロックされているかに記載されている。ステータスコードを信用するクローラーは、それらを成功として記録してしまう。ソフトブロックはコンテンツで検出する。既知のブロックページのマーカー、そのページタイプの通常サイズより大幅に小さいレスポンスサイズ、常に何かを返していた抽出処理が何も返さなくなった場合などだ。
二人の所有者、二つのスコア
一つの区別が多くの混乱を防ぐ。「サイトが変わった」ことと「サイトがあなたに敵対的になった」ことを分けることだ。
リデザインはパーサーを壊す。すべてのページは正常にロードされるが、必須フィールドが消える。これは抽出の問題であり、コードの修正で解決するもので、パーサーを保守する担当者が所有する。チャレンジやソフトブロックを始めたサイトは関係性の問題であり、修正は収集の方法、量、あるいは収集自体を行うかどうかの変更になる。所有者が異なれば、対応も異なる。
そのため、敵対度、つまりブロック、チャレンジ、ソフトブロック、レイテンシをヘルススコアとして計算し、抽出の妥当性はそれとは別のパーサーヘルス信号として並行して追跡する。両方が同時に下がったときは、まず敵対度を見る。チャレンジページを提供しているサイトは、すべての抽出処理も壊すからだ。
サイト自身の通常値に対してスコア化する
最も一般的な間違いは、グローバルな閾値を使うことだ。これまで一度もチャレンジしてこなかったサイトでの5%のチャレンジ率は警戒すべきものであり、初回訪問で誰にでもチャレンジするサイトでは完全に正常だ。すべての信号は、そのサイト自身のベースラインと比較する必要がある。それは十分に安定した長さの期間、たとえば過去2週間にわたるもので、直近1日は除外する。悪い1日が新しい通常値になってしまわないようにするためだ。
そして重み付けし、上限を設け、合計する。
from dataclasses import dataclass
@dataclass
class Window:
requests: int
hard_blocks: int # 403, 429, connection resets
challenges: int # CAPTCHA or interstitial pages
soft_blocks: int # 200 OK carrying a block page or an empty result
p95_latency_ms: float
MIN_REQUESTS = 50
WEIGHTS = {"hard_block": 35, "challenge": 25, "soft_block": 25, "latency": 15}
def signals(w):
n = max(1, w.requests)
return {
"hard_block": w.hard_blocks / n,
"challenge": w.challenges / n,
"soft_block": w.soft_blocks / n,
"latency": w.p95_latency_ms,
}
def badness(name, now, base):
if name == "latency":
# Full penalty at three times the site's own normal latency.
return min(1.0, max(0.0, (now / max(base, 1.0) - 1) / 2))
# Full penalty at 20 percentage points above the site's own normal rate.
return min(1.0, max(0.0, (now - base) / 0.20))
def health_score(current, baseline):
if current.requests < MIN_REQUESTS:
return None, ["not enough requests to judge"]
now, base = signals(current), signals(baseline)
penalty = {k: w * badness(k, now[k], base[k]) for k, w in WEIGHTS.items()}
score = round(100 - sum(penalty.values()))
reasons = [k for k, p in sorted(penalty.items(), key=lambda kv: -kv[1]) if p >= 1]
return score, reasons
ここでいくつかの設計上の選択は意図的なものだ。
- ベースラインを超えた分だけがカウントされる。 常に3%のチャレンジ率だったサイトは、今日それをやっても点を失わない。
- 各信号には上限がある。 一つの暴走した信号がスコアを0以下に押し下げたり、他の信号をかき消したりすることはできず、理由のリストがどれが支配的だったかを示す。
- 小さなウィンドウはスコアを返さない。 1件のブロックを含む5件のリクエストは20%のブロック率ではなく、単にデータが足りないということだ。スコアが存在しないことは、自信満々の誤ったスコアより誠実だ。
- 重みは意見である。 このようなものから始め、いくつかの実際のインシデントを検討した後に調整する。ハードブロックとソフトブロックには最も大きな重みを与えるべきだ。それらは得られなかったデータを意味するからだ。
スコアは時間的にも平滑化する。たとえば指数加重平均を使い、1分間の悪化がアラートをフラップさせないようにしつつ、着実な低下は1時間以内に現れるようにする。
カナリア: 自分で制御できる真実
上記のすべての信号は推測されたものだ。カナリアは真実に近いものを与えてくれる。サイトごとに、正しい答えがどう見えるか分かっている、いくつかの安定したページを選ぶ。価格を確認できる商品、件数を知っているリスト、数か月間構造が変わっていないページなどだ。これらを本番トラフィックと同じ経路を通じて、決まったスケジュールで取得する。
カナリアが正常に見えるページを返すが、間違った値を持っている場合、それは検出が最も難しい種類の失敗を見つけたということだ。つまり、完全に解析できて、単に実際の訪問者が見るものとは違うコンテンツだ。ステータスコードやレイテンシグラフはそれを示さない。カナリアはまた、ベースラインを信頼できるものにする。クローラーとは独立にその正しい振る舞いを知っているからだ。
アクションをバンドに紐付け、人間をループに保つ
誰も行動しないスコアはただのダッシュボードだ。バンドを設け、各バンドにシステムが自ら取るアクションを与える。
| バンド | 意味 | 自動アクション |
|---|---|---|
| 80から100 | 自身の通常の振る舞いをしている | なし |
| 50から79 | 悪化中 | このサイトの並行数を減らす、再訪問間隔を延ばす、失敗がサイト全体かルート固有かを確認する |
| 50未満 | サイトが明らかに反発している | サイトを一時停止し、カナリアだけを稼働させ続け、担当者にアラートを送る |
悪化中バンドはクローラーの他の部分に直結する。並行数を下げるのは、バックプレッシャーとフローコントロールにあるホストごとのリミッターの役割であり、スコアの低下はコストを意識したスケジューラーにおいてそのサイトの実効コストを上げるべきだ。そうすれば予算は、まだデータが得られるサイトへと移動する。
最下位のバンドは、一時停止を超えて自動化しないことが意図的だ。強く反発しているサイトは何かを伝えている。正しい対応は人間の判断だ。さらに速度を落とす、収集がまだそのサイトの利用規約に適合しているか確認する、公式のAPIやデータフィードを探す、あるいは停止する。スコアが下がるたびに、より重い収集手法に自動的にエスカレーションすることは、信号を軍拡競争に変えるだけであり、それこそヘルススコアが避けるべきものだ。レート制限とリクエストスロットリングは、サイトが何を求めているかをどう読み取るかを扱っている。
他のアラートと同様にレビューする
このスコアを、偽陽性率を持つアラートとして扱い、レビューする。各インシデントの後、スコアが十分早く動いたか、理由が本当の原因を指し示していたか、自動アクションが役に立ったかを問う。ほとんどのチューニングは、事前に重みを設計することからではなく、2、3件の実際のインシデントから得られる。履歴は保持しておく。数か月にわたるスコアは、各サイトの自動化されたトラフィックに対する態度がどう変化しているかを示す最良の記録だ。
結論
サイトはクローラーに対して、完全なブロックのずっと前から、チャレンジ、偽装された拒否、遅い応答を通じて、徐々に静かに敵対的になっていく。それぞれの信号は単体では曖昧だ。そのサイト自身の通常値と比較し、重み付けし、上限を設け、組み合わせることで、早期に動く理由付きの一つの数値が得られる。
このスコアが最も価値を発揮するのは、クローラーが冷静にできることにある。ブロックされる前に手を緩め、予算を他所に移し、そして難しい判断を、証拠を目の前にした人間に委ねることだ。それを取り巻くより広範な指標については、ウェブスクレイピングパイプラインの監視で扱っている。