🎯 カスタマイズ可能で検出回避型のクラウドブラウザ。自社開発のChromiumを搭載し、ウェブクローラーAIエージェント向けに設計されています。👉今すぐ試す
ブログに戻ります

自己修復型ウェブスクレイパーの構築方法

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

05-Aug-2026

TL;DR:

  • 現在機能しているCSSセレクタが、サイトがクラス名を変更すると壊れ、通常のスクレイパーは何も返さずに静かに失敗します。
  • 自己修復型スクレイパーは、ランタイムでセレクタを修復します:ページを取得し、保存されたセレクタを試し、失敗した場合は言語モデルにライブDOMから読み取った新しいものを尋ねます。
  • このパイプラインは、Scrapeless Universal Scraping APIを通じてレンダリングされたページを取得するため、モデルは常に空のシェルではなく、実際のスクリプト実行済みのマークアップに対して修復します。
  • ライブ実行では、保存されたセレクタ.author-nameがゼロ要素にマッチし、その後deepseek-v4-flashsmall.authorを提案し、全十名の著者を抽出します。
  • 修復されたセレクタは、信頼される前にDOMに対して検証されます ― 間違った推測は何にもマッチせず却下されるため、不適切な提案がデータを静かに破損させることはありません。
  • 無料プランでScrapeless APIキーを取得し、全ループをエンドツーエンドで実行します。

パイプラインの概要

スクレイパーが壊れるのは、コードが間違っているからではなく、ページが移動するからです。解決策は、セレクタを再生成可能なデータとして扱うことであり、ソースに焼き付けられた定数ではありません。このパイプラインは、五つの段階でそれを実現します:

レンダリングされたページを取得 (Scrapeless) → 保存されたセレクタを試す → 失敗を検出 → 言語モデルで修復 → 検証して抽出 → 動作しているセレクタを永続化

以下の各段階は、ライブページに対して実行されます。取得と修復は、あなたのマシンから出る二つのコールです;それ以外は、あなたが検査できるローカル解析です。

ステージ1: Scrapelessでレンダリングされたページを取得

修復は、モデルに見せるHTMLの質によって左右されるため、取得はブラウザが構築するページを返さなければなりません。クライアントレンダリングされたサイトが最初に送信する空のコンテナではありません。そのレンダリングされた取得は、Scrapeless Universal Scraping APIの仕事です。Universal Scraping APIは、サーバーサイドでページをjs_renderしてレンダリングし、その完成したHTMLをdataフィールドに返します。パイプラインが使用する二つのライブラリをインストールします:

bash Copy
pip install requests beautifulsoup4

環境変数に双方のキーを入れます — 取得のためのScrapelessキーと、修復のためのモデルキー:

bash Copy
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
export OPENROUTER_API_KEY="your_openrouter_api_key"

ステージ2: 保存されたセレクタを試して壊れを検出

先月のスクレイパーを今日のページに対して実行すると、失敗は静かです:セレクタは何にもマッチせず、抽出は空のリストを返すため、素朴なジョブはゼロ行を書き込み、成功を報告します。以下のデモページはJavaScriptで引用を構築しており、保存されたセレクタ.author-nameは、マークアップがもはや使用していないクラスであり、リデザインによって名前が変更された任意のセレクタの代わりを務めています:

python Copy
import os, requests
from bs4 import BeautifulSoup

def fetch(url):
    r = requests.post(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": True}},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["data"]

soup = BeautifulSoup(fetch("https://quotes.toscrape.com/js/"), "html.parser")

SAVED_SELECTOR = ".author-name"          # 先月は機能していた;サイトがクラス名を変更した
authors = [e.get_text(strip=True) for e in soup.select(SAVED_SELECTOR)]
print(f"保存されたセレクタ {SAVED_SELECTOR!r} がマッチした要素数: {len(authors)}",
      "-> 壊れている、何も抽出できなかった" if not authors else "-> ok")

実行すると、静かな失敗が大きな失敗になります:

text Copy
保存されたセレクタ '.author-name' がマッチした要素数: 0 -> 壊れている、何も抽出できなかった

ステージ3: 言語モデルで修復

保存されたセレクタが何にもマッチしないとき、ライブDOMをモデルに渡し、代替を求めます。モデルは実際の構造 ― タグ、クラス、CSSセレクタ仕様によって定義されたネストを読み取り、目の前のページに合わせたセレクタを返します。トレーニング中に見たページに基づくものではありません。JSONオブジェクトで応答を制限することで、答えは説明ではなく単一のセレクタ文字列になります:

python Copy
import os, json, requests

def propose_selector(html_fragment, target):
    prompt = (f'ウェブスクレイピングCSSセレクタが壊れました。このHTMLフラグメントから、{target}を保持する要素のためのJSON '
              f'{{"selector": "<css>"}}を返してください。JSONのみを返します。\n\n'
              f"HTML:\n{html_fragment}")
    r = requests.post(
        "https://openrouter.ai/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}", "Content-Type": "application/json"},
        json={"model": "deepseek/deepseek-v4-flash",
              "messages": [{"role": "user", "content": prompt}],
              "response_format": {"type": "json_object"}, "temperature": 0},
        timeout=120,
    )
    r.raise_for_status()
    return json.loads(r.json()["choices"][0]["message"]["content"])["selector"]

1つのレンダリングされた.quoteブロックを送信するだけで十分なコンテキストとなり、トークン数とコストを抑えることができます。

無料プランでAPIキーを取得する: app.scrapeless.com

ステージ4: 検証と抽出

提案されたセレクタは、DOM が確認するまでの提案に過ぎません。癒されたセレクタを同じスープに対して実行し、一致するものの数を数えます。実際の修正は行を返し、誤った推測はゼロを返し、空のデータを書き込む前に破棄されます。全体のループは3つの呼び出しを結びつけます。

python Copy
import os, json, requests
from bs4 import BeautifulSoup

def fetch(url):
    r = requests.post(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": True}},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["data"]

def propose_selector(html_fragment, target):
    prompt = (f'ウェブスクレイピングのCSSセレクタが壊れました。このHTMLフラグメントから、{target}を保持している要素のためのJSON '
              f'{{"selector": "<css>"}}を返してください。JSONのみを返してください。\n\n'
              f"HTML:\n{html_fragment}")
    r = requests.post(
        "https://openrouter.ai/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}", "Content-Type": "application/json"},
        json={"model": "deepseek/deepseek-v4-flash",
              "messages": [{"role": "user", "content": prompt}],
              "response_format": {"type": "json_object"}, "temperature": 0},
        timeout=120,
    )
    r.raise_for_status()
    return json.loads(r.json()["choices"][0]["message"]["content"])["selector"]

def extract(soup, selector):
    return [e.get_text(strip=True) for e in soup.select(selector)]

soup = BeautifulSoup(fetch("https://quotes.toscrape.com/js/"), "html.parser")
authors = extract(soup, ".author-name")

if not authors:
    print("保存されたセレクタ'.author-name'に一致: 0 -> 修復中")
    sample = str(soup.select_one(".quote") or soup.body)[:1500]
    healed = propose_selector(sample, "引用の著者名")
    print("モデルが提案したセレクタ:", repr(healed))
    authors = extract(soup, healed)
    print("修復されたセレクタに一致した数:", len(authors))

print("著者:", ", ".join(authors[:3]), "...")

壊れたセレクタが癒され、抽出が再び行われます:

text Copy
保存されたセレクタ'.author-name'に一致: 0 -> 修復中
モデルが提案したセレクタ: 'small.author'
修復されたセレクタに一致した数: 10
著者: アルバート・アインシュタイン, J.K. ローリング, ジェーン・オースティン ...

癒されたセレクタがすでに使用されているパーサーが利用するCSSセレクタエンジンに対して検証されるため、検証は正確です: 一致する要素があるか、ないかだけであり、一致が受け入れられます。

ステージ5: 動作しているセレクタを保存

一度の修正は修復ですが、毎回の修正は無駄です。セレクタが検証された後、それをフィールドにキー付けされた小さなキャッシュに書き戻し、次回の実行では動作するセレクタをディスクから直接読み込み、壊れた場合にのみモデルを呼び出します:

python Copy
import json
from pathlib import Path

CACHE = Path("selectors.json")

def remember(field, selector):
    cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
    cache[field] = selector
    CACHE.write_text(json.dumps(cache, indent=2))

remember("quote_author", "small.author")   # 次回の実行は修復されたセレクタから始まります

これにより、スクレイパーは優雅に劣化します: 動作する間はキャッシュされたセレクタを使用し、ページが変更されるとすぐに修正します。誰かが気づいてコードを編集するまで失敗することはありません。

結論

自己修復型スクレイパーは、停止からの壊れたセレクターをランタイムイベントに変えます。Scrapeless Universal Scraping APIは、モデルが推論するために必要なレンダリングされたHTMLを提供し、モデルはそのライブDOMを読み取ってセレクターを提案し、パーサーに対するマッチカウントが修正が本物であるかどうかを決定します。上記の実行 — .author-name がゼロと一致し、small.author が十と一致する — は、ミニチュアのループであり、取得、検出、修復、検証、永続化です。スケールする前に予想されるリクエスト量を料金ページに対してサイズ設定し、レンダリングされた取得が重要である理由を理解したい場合は、ブラウザがHTMLをDOMに解析する方法に関する入門書をお読みください。抽出における言語モデルの広い視点については、LLMスクレイパーとは何かに関する説明がコンテキストを設定します。

私たちのコミュニティに参加して無料プランを取得し、耐障害性のあるスクレイパーを構築している他の開発者とノートを比較してください: Discord · Telegram

よくある質問

Q: スクレイパーが「壊れる」とは実際に何が壊れるのでしょうか?

通常はセレクターです。サイトがデザインをリニューアルし、クラスの名前を変更またはマークアップを再構築すると、保存したCSSセレクターが一致しなくなり、抽出が空のリストを返します。リクエストはまだ成功するため、失敗は見逃しやすいです。

Q: プレーンリクエストの代わりにScrapeless Universal Scraping APIを通じて取得する理由は何ですか?

モデルは示されたHTMLに対してのみ修復が可能だからです。クライアントレンダリングされたページは、ベアHTTPクライアントに空のコンテナを返すため、モデルはデータが含まれていないマークアップについて推論することになります。Universal Scraping APIはページをサーバー側でレンダリングし、完成したDOMを返すことで、モデルに実際の構造を提供します。

Q: 一致しないセレクターをモデルが発明するのをどのように防ぎますか?

信頼する前に検証します。修復されたセレクターは解析されたDOMに対して実行され、一致数がカウントされます。ゼロ要素を返すセレクターは使用されるのではなく破棄されます。モデルが提案しますが、一致数が決定します。

Q: この処理は毎回モデルを呼び出しますか?

いいえ。一度セレクターが検証されると、フィールドごとにキー付けされたディスクにキャッシュされ、後の実行は直接動作するセレクターを読み取ります。キャッシュされたセレクターが一致しなくなるときにのみモデルが呼び出されるため、レイテンシとコストが実際の故障に関連付けられ、すべてのスクレイプに結び付けられることはありません。

Q: この方法でスクレイピングすることは合法ですか?

修復ループは、収集を許可されている内容を変更しません。公共のページをスクレイピングし、サイトの利用規約とロボット指示を尊重し、処理する根拠のない個人データを避け、リクエストレートを控えめに保ってください。

Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。

最も人気のある記事

カタログ