24
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】「サイト変わってデータ取れません」「自動で直るやつあるよ」〜次世代スクレイピングライブラリScrapling〜

24
Last updated at Posted at 2026-03-01

v1_v2_switch.gif

この記事の登場人物
🧑‍💻 …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を切り替えられる」

v1(ライトテーマ)
v1_highlighted.png

v2(ダークテーマ)
v2_highlighted.png

🔰「だいぶ変わりましたね」

🧑‍💻「見た目もだけど、大事なのは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

🔰「h2h3spandiv…これは完全にアウトですね」
🧑‍💻「そう。v1向けのセレクタは確実に全滅する」


Adaptive Scraping の仕組み

🔰「で、Scraplingはこの状況をどうやって解決するんですか」
🧑‍💻「2ステップで動く」

Step 1: v1のHTMLで要素を取得 → 「指紋」をSQLiteに保存
Step 2: v2のHTMLに対して、保存した指紋と照合 → 類似要素を自動発見

🔰「指紋?」
🧑‍💻「各要素のテキスト内容・タグ名・属性・親要素の構造・兄弟の並びとかを記録しておく。セレクタじゃなくて要素そのものの特徴を覚えておく感じ」
🔰「名前が変わっても顔で分かる、みたいな」
🧑‍💻「まあそんな感じ。内部的にはDOMツリー上の位置関係——親ノードや兄弟ノードとの構造的な距離も類似度スコアに入れてる。だからテキストやタグ名が変わっても、ツリー内の相対位置が近ければマッチする。逆に言うと、タグも構造も似ている別の要素には間違えやすい」
🔰「あとで出てくる誤対応の話はそれが原因…?」
🧑‍💻「そういうこと」


Step 1:指紋の保存

🧑‍💻「まずv1のHTMLで要素を取得して、指紋を保存する」

v1の各要素をハイライト表示(セレクタ名つき):
v1_highlighted.png

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_highlighted.png

# 今度は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通信、browserforgeStealthyFetcher がボット検知を回避するためのブラウザ指紋偽装ライブラリです。

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つ作っておくといいよ。毎回 storagestorage_args を書かなくて済む」


まとめ — Scraplingの強み

🔰「いろいろ見てきましたけど、Scraplingの強みってどこなんですか?一言でまとめてほしいです」
🧑‍💻「ざっくり3つ」

Scraplingの強み

  1. 壊れにくい — サイトのHTMLが変わっても、Adaptiveが要素を自動で再発見してくれる。セレクタを毎回手で直さなくていい
  2. 探しやすいfind_similar() で同じ構造の要素を一括取得、find_by_text() でテキスト検索。初めて見るサイトでもすぐ構造を把握できる
  3. 全部入り — 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


この記事の解説版もあります
対話なしのストレートな技術記事として、同じ内容をまとめています。
解説版はこちら

24
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?