「サイトリニューアルでスクレイパーが全滅」——この障害対応から、もう解放されませんか?
毎日定時にcronで回しているスクレイパーが、ある朝いきなりデータ0件。調べてみたらdiv.product-cardがarticle.item-tileに変わっていた。セレクタを書き直し、テストし、デプロイし直す——この保守コストが、サイトが変わるたびに積み重なります。
Scrapling(GitHub Stars 17,700+)は、要素の「指紋」をSQLiteに保存して、HTML構造が変わっても類似度スコアで自動的に再発見してくれる Adaptive Scraping という機能を持つライブラリです。
ダミーECサイトでタグ名・クラス名を大幅に変えて試したところ、5要素中4要素が正しく復元できました。 BS4で同じセレクタを使うと当然0件。この記事では、検証の全容、誤対応の原因分析、実運用で使えるバリデーションパターン、そしてBS4にはない便利機能を紹介します。
この記事でわかること
- BS4との違い(対応表つき)
- Adaptive機能の仕組みと、類似度マッチングの中身
- 5要素での検証結果と、誤対応が起きた理由の深掘り
-
find_similar()/find_by_text()など、BS4にはない便利機能3つ - 誤対応への実践的な対策コード(バリデーション+フォールバック)
📝 この記事の対話版(先輩×後輩の掛け合い形式)もあります
まず、BS4との違いを確認
Scraplingは「BS4の代替」というより「BS4 + α」のライブラリです。APIの感覚はかなり近いので、乗り換えのハードルは低いと思います。
インストール
pip install scrapling[all]
scrapling だけだとFetcherが動きません。[all] をつけると curl_cffi / playwright / browserforge もまとめて入ります。
また、ブラウザの実体をインストールするために以下のコマンドも実行してください(これがないと DynamicFetcher / StealthyFetcher が動きません)。
playwright install
BS4との対応表
| やりたいこと | BeautifulSoup4 | Scrapling |
|---|---|---|
| HTMLパース | BeautifulSoup(html, "html.parser") |
Selector(html) |
| HTTP取得+パース |
requests.get() + BS4 |
Fetcher.get(url) |
| CSSセレクタ検索 | soup.select(".class") |
page.css(".class") |
| 最初の1件 | soup.select_one(".class") |
page.css(".class").first |
| テキスト取得 | element.text |
element.text |
| 属性取得 | element["href"] |
element.attrib["href"] |
ほぼ同じですね。じゃあ何が違うのか。BS4にはない目玉機能が Adaptive Scraping です。
検証:セレクタを全滅させてAdaptiveで復元できるか
検証条件
Flask製のダミーECサイトで、v1 → v2 の構造変更を再現しました。クラス名だけでなくタグ名まで変更しています。
| 要素 | v1 セレクタ | v2 セレクタ | 変更の度合い |
|---|---|---|---|
| 商品カード | div.product-card |
article.item-tile |
タグ+クラス変更 |
| 商品名 | h2.product-name |
h3.title |
タグ+クラス変更 |
| 価格 | span.product-price |
div.cost |
タグ+クラス変更 |
| 評価 | div.product-rating |
div.stars |
クラスのみ変更 |
| カテゴリ | span.product-category |
span.tag |
クラスのみ変更 |
| 説明 | p.product-desc |
p.desc |
クラスのみ変更 |
上記のうち商品カードを除く 5要素 でAdaptive復元を検証します。この5要素のうち商品名と価格はタグ名まで変わっており、特に価格は span → div という、セレクタベースでは絶対に追跡できない変更です。
Adaptiveの仕組み(2ステップ)
Step 1: v1のHTMLで要素を取得 → 「指紋」をSQLiteに保存
Step 2: v2のHTMLに対して、保存した指紋と照合 → 類似要素を自動発見
「指紋」とは何か
Scraplingが保存する「指紋」は、要素の以下の特徴を組み合わせたものです:
- テキスト内容 — 要素が持つテキスト(「¥12,800」「ワイヤレスイヤホン Pro」など)
-
タグ名 —
span,div,h2など -
属性 —
class,id,data-*などの属性名と値 - 親要素の構造 — どんな要素の中に入っているか
- 兄弟要素の並び — 同じ階層に何があるか
セレクタ(名前)ではなく、要素そのものの特徴を記録しておくイメージです。名前が変わっても「顔」で見つけられる。
この指紋を各要素のペアで比較し、類似度スコアを算出して最も近い要素を返します。完全一致ではなく「どれが一番似ているか」で判定するため、複数の特徴が同時に変わっても対応できるわけです。
Step 1:指紋を保存
from scrapling.parser import Selector
from scrapling.fetchers import Fetcher
from scrapling.core.storage import SQLiteStorageSystem
STORAGE = "data/elements_storage.db" # ※事前に data ディレクトリを作成しておく必要があります
URL = "http://localhost:5001"
# ScraplingのFetcherでHTTP取得(requests不要!)
page = Fetcher.get(f"{URL}?v=v1")
selector = Selector(
page.html_content, # Fetcherが取得したHTML
url=URL,
adaptive=True,
storage=SQLiteStorageSystem,
storage_args={"storage_file": STORAGE, "url": URL},
)
# auto_save=True で指紋をSQLiteに保存
selector.css(".product-name", auto_save=True)
selector.css(".product-price", auto_save=True)
selector.css(".product-rating", auto_save=True)
selector.css(".product-category", auto_save=True)
selector.css(".product-desc", auto_save=True)
auto_save=True をつけるだけで、各要素の指紋がSQLiteに保存されます。保存先はファイル1つで、特別なセットアップは不要です。
Step 2:v2で復元
# v2のHTMLを取得(セレクタが全部変わっている方)
page = Fetcher.get(f"{URL}?v=v2")
selector = Selector(
page.html_content,
url=URL,
adaptive=True,
storage=SQLiteStorageSystem,
storage_args={"storage_file": STORAGE, "url": URL},
)
# adaptive=True で、保存済みの指紋をもとに要素を再発見
found = selector.css(".product-name", adaptive=True)
print(found.first.text) # → "ワイヤレスイヤホン Pro"
v2に .product-name というクラスはもう存在しませんが、Scraplingは h3.title を「これが元の .product-name だ」と判断して返してくれます。
結果
BS4(v1のセレクタでv2を検索):全滅
| セレクタ | ヒット数 |
|---|---|
.product-name |
0件 |
.product-price |
0件 |
.product-rating |
0件 |
.product-category |
0件 |
.product-desc |
0件 |
BS4はCSSセレクタの完全一致で検索するので、クラス名が変わった時点で何も返せません。当然の結果です。
Scrapling Adaptive(v1で保存 → v2で復元):4/5成功
| 元のセレクタ | 復元先 | 取得テキスト | 結果 |
|---|---|---|---|
.product-name |
h3.title |
ワイヤレスイヤホン Pro | ✅ 成功 |
.product-price |
span.review-count |
128件のレビュー | ❌ 誤対応 |
.product-rating |
div.stars |
★ 4.5 | ✅ 成功 |
.product-category |
span.tag |
オーディオ | ✅ 成功 |
.product-desc |
p.desc |
ノイズキャンセリング搭載の... | ✅ 成功 |
タグ名もクラス名も変わった状態で 4/5復元。なかなか実用的な精度です。
ただし1件の誤対応があります。次のセクションで原因を深掘りし、実運用での対策を考えます。
誤対応の分析と対策
なぜ「価格」だけ間違えたのか
「価格」(span.product-price)だけ「レビュー数」(span.review-count)にマッチしました。v1の span.product-price(テキスト: ¥12,800)とv2の候補を比較すると、以下のような状況です。
| v2の候補 | タグ一致 | 数値テキスト | 構造的位置 | 類似度 |
|---|---|---|---|---|
span.review-count(128件のレビュー) |
✅ span | ✅ 数値あり | ✅ 近い | 高 |
div.cost(¥12,800)← 正解 |
❌ div | ✅ 数値あり | ✅ 近い | 中 |
-
タグ名: 正解の
div.costはspan→divに変わっている。span.review-countはタグがそのままspanなので、ここで差がついた - テキスト: どちらも数値を含んでいる。「¥12,800」と「128件のレビュー」は人間なら明らかに違うものだが、テキスト類似度だけでは区別しきれなかった
- 構造: 商品カード内のほぼ同じ位置にある
つまり、タグ名が変わった+テキストの意味的な違いを判別できない という2つの要因が重なった結果です。
Scraplingは内部で要素間の類似度を0.0〜1.0のスコアで算出しています。今回は正解の div.cost よりも、タグ名が一致した span.review-count の方が僅かにスコアの閾値(Threshold)を上回ってしまったことが原因です。
Adaptiveが得意なケース・苦手なケース
今回の検証結果から、以下の傾向が見えます。
| 比較軸 | Adaptive得意 | Adaptive苦手 |
|---|---|---|
| タグ名 | 変わらない or 意味的に近い変更 |
span → div のような汎用タグ間の変更 |
| テキスト | ユニークなテキスト(商品名など) | 数値のみ(価格、件数など似た形式) |
| 構造 | 位置が大きく異なる | 同じ親要素内の似た位置 |
テキストがユニークな要素(商品名「ワイヤレスイヤホン Pro」、カテゴリ「オーディオ」など)は高い精度で復元できました。逆に、数値だけの要素が複数あり、かつタグも変わった場合に誤対応リスクが高まります。
対策:バリデーション+フォールバック
Adaptiveの結果を「信頼しつつ検証する」スタンスが実運用のポイントです。Adaptive Scrapingは魔法ではなく、あくまで「構造的に最も似ている要素」を返す仕組みです。取得した値が正しいかどうかは、正規表現や型チェックなどのビジネスロジックで検証する必要があります。Adaptive + バリデーションの併用が実運用のスタンダードです。
import logging
import re
logger = logging.getLogger(__name__)
def get_price_adaptive(selector):
"""Adaptive復元 + バリデーションで価格を安全に取得"""
found = selector.css(".product-price", adaptive=True)
if not found:
return None
text = found.first.text.strip()
# ¥ + 数値のフォーマットかチェック
if re.match(r"[¥¥][\d,]+", text):
return text
# Adaptiveが誤対応した場合 → find_by_text でフォールバック
logger.warning(f"想定外の値: {text}")
price_els = selector.find_by_text("¥", partial=True)
return price_els.first.text.strip() if price_els else None
ポイントは3つ:
- まずAdaptiveで取得 — 正しければそのまま使う(4/5はこれで済む)
-
バリデーション — 取得した値が期待する形式かチェック(価格なら
¥で始まるか) -
フォールバック — バリデーションに引っかかったら
find_by_text()で代替検索
対策:複数要素のクロスチェック
もう1つのパターンとして、複数要素をまとめて取得してから整合性を検証する方法もあります。
def get_product_data(selector):
"""複数要素をまとめて取得し、整合性をチェック"""
name = selector.css(".product-name", adaptive=True)
price = selector.css(".product-price", adaptive=True)
rating = selector.css(".product-rating", adaptive=True)
result = {
"name": name.first.text.strip() if name else None,
"price": price.first.text.strip() if price else None,
"rating": rating.first.text.strip() if rating else None,
}
# クロスチェック:価格に「件」が入っていたらレビュー数との取り違え
if result["price"] and "件" in result["price"]:
logger.warning(f"価格の値にレビュー数の疑い: {result['price']}")
result["price"] = None # 安全側に倒す
return result
Adaptiveの誤対応パターンは「似た形式の別データを取ってくる」こと。ドメイン知識に基づいたバリデーションを入れるだけで、ほとんど防げます。
結論:Adaptive + バリデーション = 実用レベル
今回の検証ではAdaptive単体の精度は4/5。バリデーションを加えれば、誤対応を検知して安全にフォールバックできる。「完璧な自動化」より「壊れたことに気づける自動化」が実運用では大事です。
BS4にはない便利機能3選
Adaptive以外にも、BS4にはないスクレイピング向け機能が揃っています。日常で使える便利機能を3つ紹介します。
1. find_similar() — 1つ見つけたら残りは自動
first_card = page.css(".product-card").first
# 同じ構造の要素を自動発見
similar = first_card.find_similar()
# → 残り5枚の商品カードが返る
「1つ見つけたら同じ構造のもの全部ちょうだい」ができます。
これが便利な場面: 初見のサイトで商品一覧をスクレイピングしたいとき。DevToolsで1つ目のカードだけ特定すれば、残りはScraplingがDOM構造の類似性から自動で見つけてくれます。BS4だと、共通のクラス名やタグ構造を自分で調べる必要がありました。
2. find_by_text() — テキストで要素検索
results = page.find_by_text("イヤホン", first_match=False, partial=True)
# → "ワイヤレスイヤホン Pro" を含む要素がヒット
BS4だと soup.find_all(string=re.compile("イヤホン")) と書くところを、シンプルに書けます。
これが便利な場面: 「この文字列を含む要素ってどれ?」をコードから直接調べたいとき。セレクタが分からなくても、表示テキストから要素を逆引きできます。先ほどのAdaptive誤対応のフォールバックにも使いました。
3. generate_css_selector — セレクタの逆算
name_el = page.css(".product-name").first
print(name_el.generate_css_selector)
# → "h2.product-name"
print(name_el.generate_full_css_selector)
# → "html > body > div.product-list > div.product-card > h2.product-name"
DevToolsの「Copy selector」に近いことが、コードから直接できます。
これが便利な場面: find_similar() や find_by_text() で見つけた要素の正確なセレクタを知りたいとき。探索→セレクタ確定→本番コードに反映、というワークフローがスムーズになります。
ハマりどころ
実装中にハマったポイントを共有しておきます。同じところで詰まる人がいそうなので。
css_first() は存在しない
# NG — AttributeError になる
element = page.css_first(".product-name")
# OK
element = page.css(".product-name").first
READMEには css_first() の記載があるんですが、実際は .first プロパティを使います。
Adaptive のストレージ指定
selector = Selector(
html,
url=url,
adaptive=True,
storage=SQLiteStorageSystem, # クラスを渡す(インスタンスではない)
storage_args={
"storage_file": "path/to/db",
"url": url,
},
)
SQLiteStorageSystem() とインスタンス化すると動きません。クラスそのものを渡す必要があります。
パフォーマンス比較
「Adaptiveは遅いのでは?」と思うかもしれません。実際にBS4とScraplingのパース速度を計測しました。
計測条件: ダミーECサイトのHTML(約5KB)を対象に、5要素のCSSセレクタ検索を50回実行した中央値。通信時間は含まない(同一HTMLを使い回し)。
| パーサー | 中央値 | BS4比 |
|---|---|---|
BS4 (html.parser) |
1.71 ms | 1.0x |
| Scrapling 通常パース | 0.63 ms | 0.4x(2.7倍速い) |
| Scrapling Adaptive保存 | 1.71 ms | 1.0x |
| Scrapling Adaptive復元 | 22.11 ms | 12.9x |
意外な結果として、Scraplingの通常パースはBS4より速いです。内部でlxmlを使っているため。Adaptive保存(auto_save=True)もBS4と同等の速度で、SQLiteへの書き込みコストはほぼ無視できます。
コストが発生するのは Adaptive復元 のみ。SQLiteからの指紋読み込みと類似度計算で約22msかかります。ただし、これは「サイト構造が変わったときの保険」として動くもの。構造が安定している間は通常パース(0.63ms)で十分です。
パフォーマンスの結論: パーサーとして使うだけならBS4より速い。Adaptiveの復元コスト(約13倍)は「セレクタを手動で修正→テスト→デプロイ」の人的コストと比べれば十分許容範囲。
まとめ
Scraplingの強みを整理
Scraplingの強みはAdaptiveだけではありません。スクレイピングに必要な機能がワンパッケージに集約されています。
- 壊れにくい — Adaptive Scrapingで、サイト改修後もセレクタの手動修正が不要
-
探しやすい —
find_similar()/find_by_text()で、初見のサイトでも素早く構造を把握 -
全部入り — HTTP取得(
Fetcher)、JS描画ページの操作(DynamicFetcher)、ボット検知の回避(StealthyFetcher)まで1つで完結
BS4は優れた「HTMLパーサー」ですが、あくまでパーサーです。Scraplingは パーサー + HTTP取得 + ブラウザ操作 + 自動復元 が1つにまとまっている点が最大の違いです。
どの場面で使うべきか
| 場面 | BS4 | Scrapling |
|---|---|---|
| 安定したサイトの定型スクレイピング | 十分 | OK |
| サイト改修が頻繁にある | セレクタを手動修正 | Adaptiveで自動復元 |
| 構造がわからないサイトの探索 | 手動で調査 | find_similar / find_by_text |
| ボット対策されてるサイト | 別途ライブラリが必要 | StealthyFetcher |
| JSで描画されるページ(SPAなど) | Selenium等を別途導入 | DynamicFetcher |
BS4から乗り換えるべき?
BS4で安定して動いているなら無理に乗り換える必要はありません。ただし、以下に当てはまるなら検討する価値があります。
- サイトのリニューアルでスクレイパーが壊れた経験がある
- 定期実行しているスクレイパーの保守コストを下げたい
- JS描画やアンチボット対策を別ライブラリで対処している
まずは find_similar() や find_by_text() をパーサーとして使ってみて、必要になったらAdaptiveを導入する——段階的な移行がおすすめです。同じ感覚で書けるので、試しに1スクリプトだけ移植してみると、差が実感できると思います。
リポジトリ: https://github.com/matsubara457/scrapling-verify
git clone https://github.com/matsubara457/scrapling-confirm.git
cd scrapling-confirm
pip install -r requirements.txt
python demo_site/app.py & # Flask起動
python -m scraper.adaptive full # Adaptiveフルデモ
Scrapling公式: https://github.com/D4Vinci/Scrapling
この記事の対話版もあります
先輩エンジニア×後輩の掛け合い形式で、同じ内容をもう少しカジュアルにまとめています。
→ 対話版はこちら




