この記事の登場人物
🧑💻 …Scraplingを見つけてきた先輩エンジニア
🔰 …BeautifulSoup4でスクレイピングしてたら壊れた後輩
🔰「先輩、スクレイパー動かなくなりました」
🧑💻「またセレクタ?」
🔰「またセレクタです」
サイトがリニューアルされて、CSSセレクタ(HTMLから要素を指定するための記法、.product-name のようなもの)が合わなくなる。
スクレイピングをやっていると、だいたい一度はこうなります。
🧑💻「Scrapling ってライブラリ知ってる? GitHub Stars 17,700+」
🔰「知らないです。今使ってるBeautifulSoup4じゃダメなんですか」
🧑💻「ダメじゃないけど、こいつ HTML構造が変わっても要素を自動で再発見してくれる らしいよ」
🔰「…何を言っているんですか?」
実際どのくらい使えるのか、ダミーECサイトを作って検証してみました。
結論から言うと、5要素中4要素が正しく復元できました。
この記事でわかること
- Scraplingの基本的な使い方(BS4との対応表つき)
- Adaptive機能の仕組みと検証結果
-
find_similar()/find_by_text()など、BS4にはない便利機能
📝 この記事の解説版(通常の技術記事形式)もあります
まず、セレクタを全滅させる
🧑💻「検証用にFlaskでダミーECサイトを作った。ボタン一つでv1とv2を切り替えられる」
🔰「だいぶ変わりましたね」
🧑💻「見た目もだけど、大事なのはHTMLの中身。クラス名だけじゃなくてタグ名まで変えてある」
| 要素 | 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 |
🔰「
h2→h3、span→div…これは完全にアウトですね」
🧑💻「そう。v1向けのセレクタは確実に全滅する」
Adaptive Scraping の仕組み
🔰「で、Scraplingはこの状況をどうやって解決するんですか」
🧑💻「2ステップで動く」
Step 1: v1のHTMLで要素を取得 → 「指紋」をSQLiteに保存
Step 2: v2のHTMLに対して、保存した指紋と照合 → 類似要素を自動発見
🔰「指紋?」
🧑💻「各要素のテキスト内容・タグ名・属性・親要素の構造・兄弟の並びとかを記録しておく。セレクタじゃなくて要素そのものの特徴を覚えておく感じ」
🔰「名前が変わっても顔で分かる、みたいな」
🧑💻「まあそんな感じ。内部的にはDOMツリー上の位置関係——親ノードや兄弟ノードとの構造的な距離も類似度スコアに入れてる。だからテキストやタグ名が変わっても、ツリー内の相対位置が近ければマッチする。逆に言うと、タグも構造も似ている別の要素には間違えやすい」
🔰「あとで出てくる誤対応の話はそれが原因…?」
🧑💻「そういうこと」
Step 1:指紋の保存
🧑💻「まずv1のHTMLで要素を取得して、指紋を保存する」
from scrapling.parser import Selector
from scrapling.fetchers import Fetcher
from scrapling.core.storage import SQLiteStorageSystem
STORAGE = "elements_storage.db" # カレントディレクトリに保存
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)
🔰「
Fetcher.get()で取得してauto_save=Trueつけるだけ?」
🧑💻「そう。HTTP取得もScraplingに任せて、これだけで各要素の指紋がSQLiteに保存される」
Step 2:v2での自動復元
🧑💻「次に、v2のHTMLに対して同じセレクタで検索する。もちろんv2にはそのクラス名はもう存在しない」
🔰「じゃあ普通なら0件ですよね」
🧑💻「そう。でもadaptive=Trueをつけると、さっき保存した指紋を使って探してくれる」
v2でも同じ要素を見つけられるか?(元のセレクタ→新セレクタの対応):

# 今度はv2を取得
page = Fetcher.get(f"{URL}?v=v2")
selector = Selector(
page.html_content, # v2のHTML(セレクタが全部変わっている方)
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」
BS4(v1のセレクタでv2を検索)
| セレクタ | ヒット数 |
|---|---|
.product-name |
0件 |
.product-price |
0件 |
.product-rating |
0件 |
.product-category |
0件 |
.product-desc |
0件 |
🔰「まあそうなりますよね」
🧑💻「次、Scrapling Adaptive」
Scrapling Adaptive(v1で保存 → v2で復元)
| 元のセレクタ | 復元先 | 取得テキスト | 結果 |
|---|---|---|---|
.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 |
ノイズキャンセリング搭載の... | 成功 |
🔰「おお、5個中4個当たってる」
🧑💻「タグ名もクラス名も変わってるのに、ちゃんと対応する要素を見つけてる」
🔰「でも価格だけ間違ってますね。レビュー数を引っ張ってきてる」
1件の誤対応について
🧑💻「
span.product-price(¥12,800)とspan.review-count(128件のレビュー)、どっちもspanタグで数値テキストを持ってて、構造的な位置も近いんだよね」
🔰「あー、似てるから間違えたってことですか」
🧑💻「そう。正解のdiv.costはタグがdivに変わってたから、類似度で負けたっぽい」
🔰「なるほど…じゃあ実運用だと怖くないですか?」
🧑💻「バリデーション入れればいい。価格なら¥で始まるか、数値に変換できるかをチェックするだけで弾ける」
Adaptiveの結果は信頼しつつ検証する、くらいのスタンスが良さそうです。
BS4ユーザー向け:Scraplingの基本
🔰「ちなみにScraplingって使い方難しいんですか?」
🧑💻「BS4使ったことあるならすぐ分かると思う」
インストール
pip install scrapling[all]
scrapling だけだとFetcherが動きません。[all] で curl_cffi / playwright / browserforge もまとめて入ります。curl_cffi はHTTP通信、browserforge は StealthyFetcher がボット検知を回避するためのブラウザ指紋偽装ライブラリです。
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"] |
🔰「ほぼ同じじゃないですか」
🧑💻「APIの感覚はかなり近い。乗り換えのハードルは低い」
Adaptive以外の便利機能
🧑💻「Adaptive以外にもBS4にはない機能がいくつかある」
find_similar() — 1つ見つけたら残りは自動
first_card = page.css(".product-card").first
# 同じ構造の要素を自動発見
similar = first_card.find_similar()
# → 残り5枚の商品カードが返る
🔰「1個見つければ "同じ構造のやつ全部ちょうだい" ができるんですね」
🧑💻「初見のサイトで便利。DevToolsから1要素だけ特定すれば残りはお任せ」
find_by_text() — テキストで要素検索
results = page.find_by_text("イヤホン", first_match=False, partial=True)
# → "ワイヤレスイヤホン Pro" を含む要素がヒット
🔰「BS4だと
soup.find_all(string=re.compile("イヤホン"))ですよね」
🧑💻「こっちの方がシンプルに書ける」
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" がコードからできるのは地味にいいですね」
🧑💻「セレクタを自分で組むより確実だし、デバッグにも使える」
用途に合わせたFetcher
🧑💻「あとScraplingは用途に合わせてFetcherを切り替えられる。これもBS4にはない強み」
from scrapling.fetchers import Fetcher, StealthyFetcher, DynamicFetcher
# 基本のHTTP取得(curl_cffiベース、高速)
page = Fetcher.get("https://example.com")
# JSで描画されるSPAサイト(Playwrightでブラウザ描画)
page = DynamicFetcher.fetch("https://spa-example.com")
# ボット検知を自動回避(ステルスブラウザ)
page = StealthyFetcher.fetch("https://bot-protected-site.com")
🔰「
StealthyFetcherってどういう仕組みなんですか?」
🧑💻「内部でChromiumを立ち上げて、ブラウザのフィンガープリントを偽装しながらアクセスする。Cloudflareとかのボット検知を自動で回避してくれる」
🔰「BS4だとそのへん全部自分で組まないといけないですもんね」
🧑💻「requests+ ヘッダー偽装 + プロキシ + …ってやるより圧倒的にラク」
おまけ:ビジュアルスクレイピング
🧑💻「あとScraplingの凄いところなんだけど、こいつ内部にPlaywrightのエンジンも内蔵してる(
DynamicFetcher)んだよね」
🔰「え、じゃあJSでゴリゴリ描画されるSPAとかもいけるってことですか?」
🧑💻「そう。で、せっかくPlaywrightが使えるなら、ブラウザ上に要素をハイライトしながらスクレイピングする仕組みも作れる」
🔰「ハイライト?」
🧑💻「今どの要素を処理してるか、赤枠で囲んで目で見えるようにする。ただしDynamicFetcherはfetch+parseに特化してて、DOMを直接操作する機能はない。だからここは生のPlaywrightを使う」
🔰「使い分けがあるんですね」
🧑💻「そう。データ取得ならDynamicFetcher、ブラウザ上の可視化やDOM操作なら生のPlaywright。こんな感じ」
from playwright.sync_api import sync_playwright
# DynamicFetcherではなく生のPlaywrightを使う(DOM操作が必要なため)
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=False, slow_mo=100)
page = browser.new_page()
page.goto(url, wait_until="networkidle")
cards = page.locator(".product-card")
for i in range(cards.count()):
card = cards.nth(i)
card.evaluate("""el => {
el.style.outline = '3px solid #ff4444';
el.style.boxShadow = '0 0 15px rgba(255, 68, 68, 0.5)';
}""")
🔰「何取ってるか一目でわかるのいいですね。デバッグのとき重宝しそう」
🧑💻「チームに共有するときのデモとしても使える」
Playwrightを初めて使う場合は playwright install でブラウザの実行ファイルをダウンロードしてください。pip install scrapling[all] だけではブラウザ本体は入りません。
playwright install
ハマりどころ
🔰「実装中にハマったところとかありました?」
🧑💻「いくつかある。同じところで詰まる人いそうだから書いておく」
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()とインスタンス化すると動かない。クラスそのものを渡す」
🔰「それは分かりづらい…」
🧑💻「実運用ならSelector生成をラップする関数を1つ作っておくといいよ。毎回storageとstorage_argsを書かなくて済む」
まとめ — Scraplingの強み
🔰「いろいろ見てきましたけど、Scraplingの強みってどこなんですか?一言でまとめてほしいです」
🧑💻「ざっくり3つ」
Scraplingの強み
- 壊れにくい — サイトのHTMLが変わっても、Adaptiveが要素を自動で再発見してくれる。セレクタを毎回手で直さなくていい
-
探しやすい —
find_similar()で同じ構造の要素を一括取得、find_by_text()でテキスト検索。初めて見るサイトでもすぐ構造を把握できる - 全部入り — HTTP取得、JavaScriptで描画されるページの操作、ボット検知の回避まで、これ1つで完結する
🔰「BS4だと
requestsとかSeleniumとか色々組み合わせますもんね」
🧑💻「そう。Scraplingは パーサー + 通信 + ブラウザ操作 + 自動復元 のワンパッケージ。たとえばDynamicFetcherでJSページをブラウザごと取得、StealthyFetcherでボット検知を回避、みたいなことが全部組み込みでできる。しかもAPIはBS4とほぼ同じだから学習コストも低い」
🔰「場面ごとの使い分けってどうなります?」
| 場面 | BS4 | Scrapling |
|---|---|---|
| 安定したサイトの定型スクレイピング | 十分 | OK |
| サイト改修が頻繁にある | セレクタを手動修正 | Adaptiveで自動復元 |
| 構造がわからないサイトの探索 | 手動で調査 | find_similar / find_by_text |
| ボット対策されてるサイト | 別途ライブラリが必要 | StealthyFetcher(自動で回避) |
| JSで描画されるページ(SPAなど) | Selenium等を別途導入 | DynamicFetcher(ブラウザ内蔵) |
🔰「デメリットはないんですか?」
🧑💻「あるよ。Adaptive復元の処理速度。実測したら↓こんな感じだった」
| パーサー | 中央値 | BS4比 |
|---|---|---|
BS4 (html.parser) |
1.71 ms | 1.0x |
| Scrapling 通常パース | 0.63 ms | 0.4x(速い) |
| Scrapling Adaptive復元 | 22.11 ms | 12.9x |
🔰「え、通常パースはBS4より速いんですか」
🧑💻「内部でlxml使ってるからね。コストがかかるのはAdaptive復元だけ。SQLiteから指紋読んで類似度計算するから約13倍。ただ22msだよ」
🔰「22ms…人間には分からないレベルですね」
🧑💻「そう。"セレクタ壊れた→手動修正→テスト→デプロイ" の人的コストと比べたら誤差。ただし安定してるサイトをひたすら高速にパースしたいだけなら、通常パースで十分」
🔰「だから "安定したサイトならBS4で十分" なんですね」
🧑💻「そう。Adaptiveはあくまで構造変更への保険。常に有効にするものじゃなくて、壊れやすいサイトにピンポイントで使うのが正解」
🔰「結局、BS4から乗り換えるべきなんですか?」
🧑💻「安定して動いてるなら無理に変える必要はない。ただ構造が変わりうるサイトを継続的にスクレイピングしたいなら、知っておいて損はないと思う」
🔰「まずはパーサーとして使ってみて、必要ならAdaptive導入って感じですかね」
🧑💻「それがいい。find_similar()とかfind_by_text()はAdaptive関係なく普通に便利だしね」
リポジトリ: 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
この記事の解説版もあります
対話なしのストレートな技術記事として、同じ内容をまとめています。
→ 解説版はこちら



