スクレイピング

大規模な変更検知:ノイズに埋もれずにWebページの差分を取る方法

広告、タイムスタンプ、トークンなど、Webページは読み込むたびに変化する。正規化、フィールド単位の差分、simhashを使って本当の変更をノイズと区別する方法を、テスト済みコードとともに解説する。

Chris Collins

Chris Collins

2026年9月27日 · 3 分で読める

毎回の監視ジョブは、最終的に同じ問いに行き着く。このページは変わったのか。ハッシュ比較のように聞こえるが、そうではない。現代のページはロード毎に変化する。ローテーションする広告、タイムスタンプ、レコメンドブロック、セッショントークン、ライブカウンターなどがあるため、単純な比較では毎回「変化あり」と報告され、アラートチャンネルはノイズで埋め尽くされ、やがて誰もがミュートするようになる。逆の失敗はより静かで、より悪質だ。ノイズを無視するように調整しすぎたパイプラインが、本来捕捉すべき値下げや証明書の削除を見逃してしまうのだ。

このガイドでは、大規模に実際の変化を検出する方法を扱う。ページ全体ではなくフィールドを比較すること、避けられない比較対象を正規化すること、変化したかどうかではなくどれだけ変化したかを測定すること、そして各種の変化を適切な場所にルーティングすることだ。

要点

  • ほとんどのサイトでは、生のバイト比較は役に立たない。あるニュースのホームページを45秒間隔で2回取得したところ、バイトは異なり、クリーンアップ後のテキストすら異なっていた。
  • 可能な限り、ページではなくフィールドを比較する。価格、タイトル、在庫状況は、変わったか変わらなかったかのどちらかだ。
  • ページ全体を比較しなければならない場合は、まず正規化してから類似度を測定する。Simhashは、Googleが80億ページに及ぶ近似重複検出のために説明したフィンガープリント手法であり、「変わったか?」を「どれだけ変わったか?」という問いに変える。
  • あらゆる変化を分類する。フィールド変化、コンテンツ変化、ノイズだ。それぞれに異なる対応が必要だ。
  • サイトごと、ページタイプごとに調整する。商品ページに適した閾値は、ニュースのホームページには適さない。

単純な差分比較が失敗する理由

問題を具体的に見るために、あるメジャーなニュースサイトの国際版トップページを45秒間隔で2回取得した。生のHTMLは予想通り異なっていた。より興味深いのは、スクリプト、スタイル、マークアップを除去し、タイムスタンプやトークンを削除した後も、目に見えるテキストがなお異なっていたことだ。ライブページは絶えず更新される。差異があれば毎回アラートを出す監視システムは、このページを実行するたびにアラートを出していただろう。

ノイズの発生源は予測可能だ。

ノイズ例典型的な対処法
埋め込みスクリプトとデータアナリティクスのペイロード、JSON状態、CSRFトークン比較前にscriptとstyleブロックを除去する
時間ベースのテキスト「3分前に更新」、時刻、日付パターンで除去するか、それらを除いたフィールドを比較する
ローテーションするモジュール広告、「トレンド」、レコメンド関心のある領域やフィールドのみを比較する
パーソナライゼーションと実験A/Bテストのバリアント、地域固有のブロック一貫した設定で固定の観測地点から収集する
順序ロード毎にシャッフルされるリスト比較前にソートする

一部のノイズは実際には「誰が尋ねたか」の違いに過ぎない。ドイツから読み込んだページは、米国から読み込んだ同じページと、変化とは無関係の理由で異なる場合がある。監視対象の各ページについて観測地点を固定し、実行間で変わるのは時間だけになるようにする。レジデンシャルゲートウェイを使う場合、そのジョブのすべての実行で国を固定することを意味する。例えばcustomer-USERNAME-country-deのようにだ。

原則1: ページではなくフィールドを比較する

最も信頼性の高い変化検出器は、ページの差分比較を一切行わない。価格、在庫状況、タイトル、証明書リスト、住所といった重要なフィールドを抽出し、それらを比較する。フィールドは変わったか変わらなかったかのどちらかであり、ページの他の部分のノイズは無関係だ。

構造化データはこれを見た目以上に容易にする。多くのページは主要フィールドをJSON-LDとして公開しており、これは周囲のレイアウトよりもはるかに安定している。その抽出方法はHTMLのパースをやめるで扱っている。フィールドがセレクタから取得される場合は、抽出された値を比較し、周囲のHTMLは決して比較しない。

フィールド比較はアラートを実用的なものにもする。「価格が89.00から79.00に変わった」は行動につながる。「ページが変わった」はそうではない。

原則2: 比較せざるを得ないものは正規化する

一部の監視は本当にページ全体に関するものだ。利用規約、サプライヤーの「会社概要」ページ、サステナビリティ声明、ポリシーページなどだ。それらについては、比較前に重要でないものを除去する。

  • 非コンテンツブロックを除去する: script、style、テンプレート、インラインSVG。
  • 表示テキストのみを保持する。 空白を圧縮し、大文字小文字を統一する。
  • 揮発性の断片を除去する: 時刻、ISO日付、相対時刻、トークン、長い16進数の識別子。
  • ページに安定したメインコンテンツ領域がある場合は、その領域に限定する。 記事本文やmain要素などだ。

正規化はノイズの大部分を除去する。すべてを除去するわけではないため、次のステップが重要になる。

原則3: 変わったかどうかではなく、どれだけ変わったかを測定する

正規化されたテキストが同一かどうかを尋ねる代わりに、どれだけ似ているかを尋ねる。Simhashはこれに適したツールだ。文書を64ビットのフィンガープリントに縮約し、有用な性質を持つ。類似した文書は、ビット位置がわずかしか異ならないフィンガープリントを得る。Googleは「Detecting Near-Duplicates for Web Crawling」(WWW 2007)でこれを近似重複検出に使うことを説明しており、著者らは「80億ウェブページのリポジトリに対して、64ビットのsimhashフィンガープリントとk = 3は妥当である」と検証している。これは、フィンガープリントが最大3ビットしか異ならないページは近似重複として扱えることを意味する。

これにより、変化検出は距離の問題になる。今回のテストでは、45秒間隔で取得したニュースのホームページの2回分は2ビットの差だった。閾値以下であり、つまりノイズだ。同じホームページを無関係の記事と比較すると34ビットの差になった。明らかに異なるコンテンツだ。

import hashlib
import re

VOLATILE = [
    r"\b\d{1,2}:\d{2}(?::\d{2})?\s?(?:am|pm|AM|PM)?\b",          # clock times
    r"\b\d{4}-\d{2}-\d{2}(?:T[\d:.]+Z?)?\b",                      # ISO dates
    r"\b\d+\s+(?:second|minute|hour|day)s?\s+ago\b",              # relative times
    r"\b(?:csrf|nonce|token|session|sid)[\w-]*[=:]\s*[\w-]+",      # tokens
    r"\b[0-9a-f]{24,}\b",                                         # long hex ids
]


def normalise(html):
    """Visible text only, with volatile fragments removed."""
    html = re.sub(r"(?is)<(script|style|noscript|svg|template)\b.*?</\1>", " ", html)
    text = re.sub(r"(?s)<[^>]+>", " ", html)
    for pattern in VOLATILE:
        text = re.sub(pattern, " ", text, flags=re.I)
    return re.sub(r"\s+", " ", text).strip().lower()


def simhash(text, bits=64):
    """Charikar-style simhash over word 3-grams."""
    words = text.split()
    grams = [" ".join(words[i:i + 3]) for i in range(max(1, len(words) - 2))]
    weights = [0] * bits
    for gram in grams:
        h = int.from_bytes(hashlib.blake2b(gram.encode(), digest_size=8).digest(), "big")
        for i in range(bits):
            weights[i] += 1 if h >> i & 1 else -1
    return sum(1 << i for i in range(bits) if weights[i] > 0)


def distance(a, b):
    return bin(a ^ b).count("1")


def compare(old_html, new_html, old_fields=None, new_fields=None, threshold=3):
    """Classify a change as none, noise, content or field-level."""
    old_fields, new_fields = old_fields or {}, new_fields or {}
    field_changes = {k: (old_fields.get(k), new_fields.get(k))
                     for k in set(old_fields) | set(new_fields)
                     if old_fields.get(k) != new_fields.get(k)}
    if field_changes:
        return {"kind": "field", "changes": field_changes}
    if old_html == new_html:
        return {"kind": "none"}
    d = distance(simhash(normalise(old_html)), simhash(normalise(new_html)))
    return {"kind": "content" if d > threshold else "noise", "distance": d}

この閾値は定数ではなく出発点として扱うこと。WWW 2007の数値は、1ページを継続的に監視するためではなく、数十億ページの中から重複を見つけるために選ばれたものだ。ページタイプごとに較正する。何も意味のある変化が起きないはずの短時間で、監視対象のページを複数回取得し、観測された距離のわずかに上に閾値を設定する。短いページはより注意が必要だ。なぜなら、少数の変化した単語が、小さな文書のフィンガープリントを、長い文書よりも大きく動かすからだ。

原則4: 分類してからルーティングする

「変化あり」としか返さない検出器は、実際の作業をアラートを読む人に押し付けてしまう。種類を返し、それに応じてルーティングする。

種類意味送り先
field追跡している値が変化したデータセットに直接投入し、重要であればアラートも
contentノイズを超えてページの実質的内容が変化したテキスト差分を添付した人的レビューキュー
noiseページは動いたが、通常の変動範囲内較正のためにログに記録し、アラートは出さない
noneバイト単位で同一何もしない

2つの改良は投資に見合う。すべてのcontent変化について、直前のスナップショットと読みやすいテキスト差分を保存する。そうすればレビュー担当者は数秒で判断できる。そして、サイトごとに各種類の発生率を記録する。数週間かけてノイズの距離が徐々に大きくなるサイトは、テンプレートを変更している最中である。突然一切変化を生まなくなるサイトは、古い、あるいはブロックされたページを提供している可能性があり、これはサイレント失敗率で扱った失敗モードだ。

規模の拡大

大規模な変化検出は、主にストレージとスケジューリングの問題だ。

  • ほとんどの実行では、ページ全体ではなくフィンガープリントとフィールドを保存する。 64ビットのフィンガープリントといくつかのフィールドはごく小さい。フルスナップショットは、変化が検出されたときだけ、あるいは監査用のより遅いスケジュールでのみ保持する。
  • 期待される変化に応じて再訪する。 めったに変化しないページは毎時間チェックする必要はない。コストを意識したクロールスケジューリングは、変化が起こりそうな場所に取得予算を使う方法を示しており、noneやnoiseという結果はすべて、そのモデルの根拠となる。
  • まず安価なシグナルを使う。 サイトが信頼できるETagやLast-Modifiedヘッダーを送る場合、304 Not Modifiedを返す条件付きリクエストは、ほぼ帯域幅を使わずに答えを得られる。
  • 監視役を監視する。 既知の変化パターンを持つカナリアページは、検出器自体がまだ機能しているかどうかを教えてくれる。

使用例

同じ仕組みが、まったく異なる用途の基盤になっている。価格・在庫監視では、フィールド変化がすべての要点であり、リアルタイムの競合価格フィード構築がその例だ。サプライヤー・コンプライアンス監視では、ページ全体のコンテンツ変化が重要であり、サプライヤーの公開足跡の監視とESG主張の検証がその例だ。そして、変化が法的に重要な場合には、証拠の保全にも使われる。

結論

「このページは変わったか?」は、現代のウェブに対しては誤った問いだ。答えはほぼ常にイエスだからだ。有用な問いは「関心のあるフィールドが変わったか?」であり、ページ全体を比較しなければならない場合は「このページの通常の変動を超えて、実質的内容が変わったか?」だ。

可能な限りフィールドを比較する。避けられない比較対象は正規化し、同一性ではなく類似度を測定し、ページタイプごとに閾値を較正し、各種の変化を対応できる場所にルーティングする。その結果得られるのは、人々が信頼する変化フィードであり、それこそが読まれる唯一の種類のものだ。

出典と参考文献

始める準備はできていますか?

Shifterのレジデンシャルプロキシをお試しください。IP 205M+件、195+カ国、$0.10/GBから。

始める