サイトリニューアルでclass名が変わった。動いていたスクレイパーが突然0件を返す。——スクレイピングで一番つらい瞬間です。
Scrapling は 2024 年に登場した新興パーサーで、lxml ベースの高速パース + サイト構造の変更に自動追従する Adaptive Scraping を備えています。ただし巡回機能はありません。一方 Scrapy は巡回・リトライ・スロットリングの全部入りフレームワークですが、「ちょっとHTMLをパースしたいだけなのに重い」と感じる場面もあります。
この記事では ScraplingとScrapyを 5つの観点 で実測比較し、最後に両者を組み合わせた ハイブリッド構成 を紹介します。読み終わるころには「次のスクレイピングでどちらを選ぶか」が明確になります。
この記事でわかること
- Scrapy vs Scrapling の速度・メモリ・コード量の実測比較
- サイト構造変更への対応力の違い(Adaptive Scraping)
- Anti-Bot 耐性の違い
- Scrapy 巡回 + Scrapling パースの「ハイブリッド構成」の実装方法
前回の記事: 【Python】ScraplingでBS4を超える -- Adaptive Scrapingで壊れないスクレイパー
BS4 vs Scrapling の比較と Adaptive Scraping の検証記事です。今回はその続編として Scrapy との比較に踏み込みます。
対話版もあります
同じ内容を先輩×後輩の会話形式でわかりやすくまとめています。
→ 対話版はこちら
検証環境
Flask製のダミーECサイト(30商品 x 5ページ、ページネーション付き)を用意しました。v1/v2のHTML構造切替で「サイトリニューアル」を再現しています。
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 |
クラスのみ変更 |
セレクタの半数でタグ名まで変わるのは、実際のサイトリニューアルでもよくあるパターンです。今回の検証では6要素(レビュー数を追加)で比較しています。
| 項目 | バージョン |
|---|---|
| Python | 3.14 |
| Scrapling | 0.4.1 |
| Scrapy | 2.14 |
| Flask (ダミーサイト) | port 5002 |
比較1: 速度ベンチマーク
同じダミーECサイト(5ページ・30商品)を同条件でスクレイピングし、実行時間とメモリ使用量を計測しました。各フレームワーク5回実行の平均値です。
| 項目 | Scrapy | Scrapling | 倍率 |
|---|---|---|---|
| 平均実行時間 | 0.712s | 0.161s | 4.4倍速い |
| 最速 | 0.708s | 0.143s | -- |
| 最遅 | 0.717s | 0.180s | -- |
| 標準偏差 | 0.006s | 0.026s | -- |
| メモリピーク | 10.61MB | 1.73MB | 6.1倍少ない |
Scraplingが4.4倍速く、メモリ使用量は6.1倍少ない。 5ページ程度のタスクでこの差は大きいです。
なぜScraplingが速いのか
この差はアーキテクチャの根本的な違いから来ています。
Scrapling のパス: HTTP リクエスト(curl_cffi) → lxml パース → 結果返却。これだけです。lxml は libxml2/libxslt の C 拡張バインディングであり、HTML パースと CSS セレクタ評価のほぼ全てが C レイヤーで完結します。Python 側のオーバーヘッドは最小限です。
Scrapy のパス: Twisted の reactor.run() でイベントループを起動 → EngineStart シグナル発火 → Scheduler がリクエストをキューイング → DownloaderMiddleware チェーン(UserAgentMiddleware → RetryMiddleware → HttpCompressionMiddleware 等)を順番に通過 → HTTP リクエスト → レスポンスを逆方向にミドルウェアチェーンで処理 → Spider の parse() コールバック → Item Pipeline → シグナル通知。1リクエストあたり十数個の Python コールバックを経由します。
Scrapy のオーバーヘッドは「無駄」ではなく、大規模クローリングで必要な制御(リトライ、スロットリング、robots.txt、Cookie 管理、リダイレクト処理)を担っています。ただし、5ページ30商品程度のタスクでは、その制御機構の起動コスト自体がボトルネックになります。
公平性について: Scrapy の強みは並行リクエストと非同期 I/O です。対象サイトが数百〜数千ページの規模になると、CONCURRENT_REQUESTS による並行処理が効いてきて、この差は縮まります。今回のベンチマークは「小〜中規模タスクでのオーバーヘッド比較」として読んでください。
コード比較
同じタスク(5ページ巡回 + 30商品取得)を最もシンプルに書いた場合の違いを見てみます。
Scrapling: 手動ループで巡回
from scrapling.fetchers import Fetcher
BASE_URL = "http://localhost:5002"
products, visited, pages_to_visit = [], set(), [1]
while pages_to_visit:
page_num = pages_to_visit.pop(0)
if page_num in visited:
continue
visited.add(page_num)
page = Fetcher.get(f"{BASE_URL}/page/{page_num}?v=v1")
for card in page.css(".product-card"):
products.append({
"name": card.css("h2.product-name a").first.text,
"price": card.css("span.product-price").first.text,
})
# ページネーションリンクから次のページ番号を抽出
for link in page.css("nav.pagination a.page-link"):
href = link.attrib.get("href", "")
# /page/2 → 2 を抽出してキューに追加
...
Scrapling は「for ループでページを回す」命令的なスタイルです。URL の管理、重複排除、ページネーション追跡を全て自分で書きます。
Scrapy: Spider クラスで巡回
import scrapy
from scrapy.crawler import CrawlerProcess
class ShopSpider(scrapy.Spider):
name = "shop"
start_urls = ["http://localhost:5002/page/1?v=v1"]
def parse(self, response):
for card in response.css(".product-card"):
yield {
"name": card.css("h2.product-name a::text").get(),
"price": card.css("span.product-price::text").get(),
}
# ページネーション自動追跡(3行で完了)
for href in response.css("a.page-link::attr(href)").getall():
yield response.follow(href, self.parse)
process = CrawlerProcess(settings={"LOG_LEVEL": "ERROR"})
process.crawl(ShopSpider)
process.start()
Scrapy の response.follow() は強力です。ページネーションリンクを辿るのに3行で済みます。重複排除もフレームワークが自動で行います。
比較2: コード量と設計思想
同じタスク(5ページ巡回 + 30商品取得 + JSON保存 + v1/v2両対応 + エラーハンドリング)を実装した場合のコード行数です。
| ツール | コード行数 |
|---|---|
| Scrapling | 198行 |
| Scrapy | 213行 |
差は15行(7%)。数字だけ見ると「ほぼ同じ」ですが、書いている中身の性質がまるで違います。
Scrapy は「宣言的」です。 Spider クラスに name、start_urls、custom_settings を定義し、parse() コールバックを書く。あとはフレームワーク(CrawlerProcess → Engine → Scheduler → Downloader)が駆動します。「何を取得するか」を宣言すれば、「どう巡回するか」はフレームワークが決めるモデルです。213行のうち、巡回ロジックそのものは response.follow() の数行だけ。残りは Spider 定義のボイラープレートと、CrawlerProcess の設定です。
Scrapling は「命令的」です。 while ループでページキューを回し、visited セットで重複排除し、ページネーションリンクから次の URL を自分で組み立てる。198行のうち、かなりの割合が巡回ロジックの手書きです。
この違いが効いてくるのは 規模が拡大したとき です。
- 対象サイトが 5 → 50 ページに増えたとき: Scrapy は
CONCURRENT_REQUESTSを変えるだけ。Scrapling はasyncioかThreadPoolExecutorを自分で組む必要があります - リトライが必要になったとき: Scrapy は
RETRY_TIMES=3の 1 行。Scrapling はtry/except+ バックオフロジックを手書きします - robots.txt 対応が必要になったとき: Scrapy は
ROBOTSTXT_OBEY=True(デフォルトで有効)。Scrapling はrobotparserを自分で組み込みます
逆に、「1ページだけパースしたい」「構造変更に追従したい」という場面では、Scrapy のフレームワーク層は重荷になります。198行 vs 213行 という数字の裏には、こうした設計哲学の違いがあります。
比較3: 構造変更対応(Adaptive Scraping)
テスト内容
- v1 の HTML で Scrapling の Adaptive 保存(要素の指紋を SQLite に記録)
- v2 の HTML に対して、v1 のセレクタをそのまま使う
- Scrapy: CSS セレクタの完全一致で検索(従来のスクレイパーと同じ挙動)
- Scrapling Adaptive: 保存済みの指紋と照合して類似要素を自動発見
結果
表中の記号の意味: ✅ = 正確に復元 / ❌ = 0件(取得失敗) / ⚠️ = 要素は見つかったが中身が入れ替わっている(クロスワイヤリング)
| 要素 | Scrapy (v1セレクタ→v2) | Scrapling Adaptive |
|---|---|---|
| 商品名 | ❌ 0件 | ✅ 「ワイヤレスイヤホン Pro」 |
| 価格 | ❌ 0件 | ⚠️ 「128件のレビュー」(誤対応) |
| カテゴリ | ❌ 0件 | ✅ 「オーディオ」 |
| 評価 | ❌ 0件 | ✅ 「★ 4.5」 |
| レビュー | ❌ 0件 | ⚠️ 「¥12,800」(誤対応) |
| 説明 | ❌ 0件 | ✅ 「ノイズキャンセリング搭載の...」 |
Scrapy: 6/6全滅。Scrapling Adaptive: 4/6正確に復元。
Scrapy は構造変更に対して 「v2 用のセレクタを書き直す」以外の選択肢がない。クラス名が変わった時点でデータ 0 件。これは Scrapy に限らず、CSS セレクタに依存する従来のスクレイパー全般に共通する弱点です。
一方、Scrapling は 4/6 を正確に復元しました。2 つ(価格 ⇄ レビュー)がクロスワイヤリングを起こしています。
クロスワイヤリングの原因
価格とレビュー数が入れ替わった原因は、両方の要素が「数値テキスト + タグ変更」という類似した変化パターンを持つためです。
- v1 の
span.product-price(テキスト:¥12,800)→ v2 でdiv.costにタグ変更 - v1 の
div.product-reviews(テキスト:128件のレビュー)→ v2 でspan.review-countにタグ変更 - どちらも数値主体のテキスト + タグ名変更が重なり、指紋の類似度スコアで誤対応
実用上は バリデーション + フォールバック のパターンで対処できます。Adaptive で取得した値が期待するフォーマット(例: ¥ + 数値)かチェックし、不一致なら find_by_text() で代替検索する方法です。詳細は前回記事で解説しています。
Adaptive vs セレクタ書き直し: Scrapy で構造変更に対応するには「人間が v2 用セレクタを調べて書き直す」しかありません。Scrapling なら「Adaptive + バリデーション」で自動化できます。この差は、監視対象サイトが多いほど大きくなります。
比較4: Anti-Bot耐性
ダミーECサイトに簡易的なBot検知(User-Agentチェック)を実装し、5つのシナリオでテストしました。
| シナリオ | ツール | User-Agent | 結果 |
|---|---|---|---|
| 素の requests | requests | python-requests/2.32.5 | ✅ 200 |
| Bot偽装 | requests | MyBot/1.0 | ❌ 403 |
| Scrapling Fetcher | Scrapling | ブラウザUA自動生成 | ✅ 200 |
| Scrapy デフォルト | Scrapy | Scrapy/2.14 | ✅ 200 |
| Scrapy + ミドルウェア | Scrapy | ブラウザUA偽装 | ✅ 200 |
結果は 5 シナリオ中 4/5 成功(Bot 偽装のみブロック)ですが、注目すべきは 設定不要かどうか の違いです。
Scrapling は Fetcher.get() を呼ぶだけでリアルなブラウザ UA を自動生成します。内部で curl_cffi + browserforge を使い、TLS フィンガープリントまで本物のブラウザに寄せます。開発者が何も設定する必要がありません。
Scrapy はデフォルトで正直に Scrapy/2.14 を名乗ります。今回のダミーサイトでは「明示的に Bot を名乗る UA」だけをブロックするルールだったので通りましたが、Cloudflare のような Bot 検知サービスでは引っかかる可能性が高いです。ブラウザ UA を偽装するには RandomUserAgentMiddleware などのミドルウェア設定が別途必要になります。
今回のテストは簡易的な UA 判定のみです。実際の Bot 検知(Cloudflare、DataDome 等)では TLS 指紋、JavaScript 実行、ブラウザ挙動パターンまでチェックされます。そういったサイトでは、Scrapling の StealthyFetcher(Playwright + ステルス設定)が有利です。Scrapy で同じことをやるには scrapy-playwright プラグインの導入が必要です。
比較5: ハイブリッド構成(本記事の目玉)
ここまでの比較で見えてきた構図はシンプルです。
- Scrapy → 巡回が得意(リトライ・スロットリング・ページネーション追跡)
- Scrapling → パースが得意(4.4倍速い lxml + Adaptive 対応)
じゃあ、両方使えばいいのでは?
アーキテクチャ
Scrapy に巡回を任せて、パースだけ Scrapling に差し替える。 各ライブラリが最も得意な仕事だけをやる構成です。
実測パフォーマンス
| ステップ | 処理内容 | 所要時間 |
|---|---|---|
| Step 1 | Scrapy巡回(5ページ収集) | 0.80s |
| Step 2 | Scraplingパース(30商品) | 0.007s |
| 合計 | 0.81s |
5ページ分の HTML を 7 ミリ秒でパース。 Step 2 がほぼ無視できるほど速いことがわかります。ボトルネックはネットワーク I/O(巡回)であり、パースは一瞬で終わります。
純 Scrapy(0.712s)より合計時間がやや長い(0.81s)のは、Step 1 でHTMLをリストに溜めるオーバーヘッドと、サブプロセス起動コストが加わるためです。ハイブリッドの真価は速度ではなく、Adaptive 対応を巡回と分離できる設計の柔軟性 にあります。
実装コード
import json
import os
import scrapy
from scrapy.crawler import CrawlerProcess
from scrapling.parser import Selector
# ===== Step 1: Scrapy で HTML だけ収集 =====
collected_pages = [] # Spider から結果を受け取るリスト
class HtmlCollectorSpider(scrapy.Spider):
"""HTMLを集めるだけ。パースはしない。"""
name = "html_collector"
start_urls = ["http://localhost:5002/page/1?v=v1"]
custom_settings = {"LOG_LEVEL": "ERROR", "ROBOTSTXT_OBEY": False}
def parse(self, response):
# HTMLをそのまま保存(パースはしない!)
collected_pages.append({
"url": response.url,
"html": response.text,
})
# ページネーション追跡はScrapyに任せる
for href in response.css("a.page-link::attr(href)").getall():
yield response.follow(href, self.parse)
# CrawlerProcess を起動し、全ページの巡回が終わるまでブロック
process = CrawlerProcess()
process.crawl(HtmlCollectorSpider)
process.start() # ここで巡回完了まで待機
# ===== Step 2: Scrapling で高速パース =====
products = []
for page_data in collected_pages:
selector = Selector(page_data["html"])
for card in selector.css(".product-card"):
products.append({
"name": card.css("h2.product-name a").first.text,
"price": card.css("span.product-price").first.text,
"category": card.css("span.product-category").first.text,
"rating": card.css("div.product-rating").first.text,
})
# ===== Step 3: JSON 保存 =====
os.makedirs("data", exist_ok=True)
with open("data/hybrid_products.json", "w", encoding="utf-8") as f:
json.dump(products, f, ensure_ascii=False, indent=2)
ポイント
Spider の parse() メソッドを見てください。yield でアイテムを返していません。 HTML をリストに溜めるだけです。パースは Step 2 で Scrapling がまとめて処理します。
これが強力な理由:
- Scrapy が難しい部分を担当: リトライ制御、レート制限、robots.txt 対応、Cookie 管理、リダイレクト処理
- Scrapling が速い部分を担当: lxml ベースのパースは 4.4 倍速い
- 構造変更時: Step 2 の Scrapling パース部分だけ差し替えればいい。Step 1(巡回)は影響なし
-
Adaptive 対応: Step 2 で
Selector(html, adaptive=True)を使えば、構造変更の自動追従まで可能
Adaptive版ハイブリッド
Step 2 を Adaptive 対応にするのも簡単です。Selector の初期化時に adaptive=True を渡し、css() でも adaptive=True を指定するだけです。
from scrapling.parser import Selector
from scrapling.core.storage import SQLiteStorageSystem
STORAGE = "data/hybrid_storage.db"
URL = "http://localhost:5002"
for page_data in collected_pages:
selector = Selector(
page_data["html"],
url=URL,
adaptive=True,
storage=SQLiteStorageSystem,
storage_args={"storage_file": STORAGE, "url": URL},
)
# v1で保存した指紋をもとに、v2でも同じセレクタで取得可能
for card in selector.css(".product-card", adaptive=True):
# ...
Scrapy の巡回力 + Scrapling の速度 + Adaptive の自動復元。 全部盛りです。
ハマりどころ
ハイブリッド構成を組む際にぶつかりやすい問題をまとめておきます。
Twisted reactor は再起動できない
Scrapy は内部で Twisted の reactor を使います。CrawlerProcess.start() を呼ぶと reactor.run() が実行され、完了後に reactor.stop() されますが、一度停止した reactor は同一プロセス内で再起動できません。つまり、Python スクリプト内で process.start() を2回呼ぶと ReactorNotRestartable エラーになります。
# これは2回目で落ちる
process1 = CrawlerProcess()
process1.crawl(Spider1)
process1.start() # OK
process2 = CrawlerProcess()
process2.crawl(Spider2)
process2.start() # ReactorNotRestartable!
対策は2つあります。
- サブプロセスで実行する: 本リポジトリのハイブリッドモジュールはこの方式を採用しています。Scrapy 部分を別プロセスで起動し、結果を JSON 経由で受け渡します
-
CrawlerRunner+crochet:reactorを手動管理するパターン。複雑になるのであまりおすすめしません
Python 3.14 との互換性
Python 3.14 では @functools.lru_cache のクラスデコレーションが使えなくなりました(TypeError: unhashable type)。Scrapling 0.4.1 の内部でこれに依存している箇所があり、環境によってはインポート時にエラーが出ます。Python 3.12〜3.13 が安定です。
Scrapling のインストール
pip install scrapling ではなく pip install scrapling[all] でインストールしてください。Fetcher が内部で curl_cffi、browserforge、playwright に依存しており、[all] なしだと ImportError になります。
まとめ: 使い分けガイド
総合比較表
| 比較軸 | Scrapy | Scrapling | ハイブリッド |
|---|---|---|---|
| パース速度 | 0.712s | 0.161s (4.4x速い) | 0.81s (巡回込み) |
| メモリ使用量 | 10.61MB | 1.73MB (6.1x少ない) | -- |
| コード行数 | 213行 | 198行 | 321行 |
| 設計思想 | 宣言的 (Spider定義) | 命令的 (手動ループ) | 両者の分業 |
| 巡回機能 | ◎ 全部入り | △ 手動ループ | ◎ Scrapy担当 |
| 構造変更対応 | ✗ 手動修正 | ◎ Adaptive | ◎ Adaptive |
| Anti-Bot | △ ミドルウェア必要 | ◎ 自動UA生成 | ◎ |
| リトライ/スロットリング | ◎ 組み込み | ✗ なし | ◎ Scrapy担当 |
上の表は5つの比較結果の集約です。パース性能では Scrapling が圧倒的ですが、巡回・リトライ・スロットリングでは Scrapy に分があります。ハイブリッド構成はその両方を取れる代わりに、コード量は増えます。
選定フローチャート
場面別ガイド
| 場面 | おすすめ |
|---|---|
| 単一ページのスクレイピング | Scrapling -- Fetcher で取ってパースするだけ |
| 大規模サイトの巡回 | Scrapy -- リトライ・分散・スロットリングが必要 |
| 構造が頻繁に変わるサイト | Scrapling -- Adaptive で自動復元 |
| 大規模巡回 + 構造変更対応 | ハイブリッド -- 両者の強みを両取り |
| JS描画ページ | Scrapling -- DynamicFetcher / StealthyFetcher |
| Scrapy既存プロジェクトの高速化 | ハイブリッド -- パース部分だけ Scrapling に置換 |
結論
ScrapyとScraplingは競合ではなく 補完関係 です。
Scrapy は 10 年以上の歴史を持つ成熟したフレームワークで、大規模クローリングに必要な機能が全部入りです。Scrapling はパース速度と Adaptive Scraping に強みがある新興ライブラリです。ハイブリッド構成は「Scrapy の巡回力 + Scrapling の速度 + Adaptive の自動復元」の全部盛りです。
明日からできること
-
既存の Scrapy Spider のパース部分を Scrapling に置き換える:
parse()メソッドで HTML をリストに溜めて、後からSelectorでまとめてパースするだけ。Spider の巡回ロジックは一切変更不要 -
単発スクレイピングは Scrapling に移行する: 1〜数ページを取得するだけなら、
Fetcher.get()+css()の数行で十分。Scrapy の Spider クラス定義は不要 -
構造変更が多いサイトに Adaptive を導入する:
Selector(html, adaptive=True)で指紋を保存しておけば、次回のリニューアル時にセレクタ書き直しの工数を大幅に削減できる
どちらか一方を選ぶ必要はありません。得意なことが違うなら、組み合わせればいい。 それだけです。
リポジトリ: https://github.com/matsubara457/scrapling-vs-scrapy
git clone https://github.com/matsubara457/scrapling-vs-scrapy.git
cd scrapling-vs-scrapy
pip install -r requirements.txt
python demo_site/app.py & # Flask起動
python -m scraper.benchmark --runs 2 # 速度ベンチマーク
python -m scraper.adaptive_compare full # 構造変更対応比較
python -m scraper.hybrid --version v1 # ハイブリッド実行
Scrapling公式: https://github.com/D4Vinci/Scrapling
対話版もあります
同じ内容を先輩×後輩の会話形式でわかりやすくまとめています。
→ 対話版はこちら
前回の記事もどうぞ
BS4 vs Scrapling の比較と、Adaptive Scraping の検証を詳しくまとめています。
→ 【Python】ScraplingでBS4を超える -- Adaptive Scrapingで壊れないスクレイパー



