1
1

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スクレイピング】「Scrapy×Scrapling合体させたら最強じゃない?」〜激つよハイブリッド構成〜

1
Posted at

v1_v2_switch.gif
↑ v1→v2でHTMLの構造がガラッと変わる様子

この記事の登場人物
🧑‍💻 …Scraplingを見つけてきた先輩エンジニア(前回に引き続き)
🔰 …前回Scraplingを教わった後輩(今度はScrapyが気になっている)

🔰「前回Scrapling教えてもらいましたけど、ページ巡回は手動ループなんですよね?」
🧑‍💻「そう。while で未訪問ページを管理して、1ページずつ Fetcher.get() する感じ」
🔰「Scrapyだとそのへんフレームワークが全部やってくれるじゃないですか」
🧑‍💻「うん。巡回・リトライ・スロットリング、全部入り。じゃあ両方組み合わせたらどうなると思う?」
🔰「…合体?」
🧑‍💻「合体」
🔰「でも、Scraplingだけで十分じゃないんですか?前回けっこう便利だったし」
🧑‍💻「数ページならね。でもページ巡回で困らなかった?重複排除とかリトライとか、自分で書くの面倒だったでしょ」
🔰「…たしかに、visited セットとか自分で管理してました」
🧑‍💻「そこがScrapyの守備範囲。今回はBS4じゃなくてScrapyと比べて、最後に合体技を見せるよ」

この記事でわかること

  • ScrapyとScraplingの性能差(速度・メモリ・コード量)
  • 構造変更への対応力の違い(Adaptive vs 固定セレクタ)
  • Anti-Bot耐性の比較
  • ハイブリッド構成(Scrapy巡回 + Scraplingパース)の実装と威力

前回の記事(BS4 vs Scrapling)もあります

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


検証環境

🔰「今回もダミーサイトで試すんですか?」
🧑‍💻「うん。前回と同じFlask製だけど、今回はページネーション付きで30商品×5ページにしてある」
🔰「前回は1ページ6商品でしたよね。今回はだいぶ規模が大きくなったんですね」
🧑‍💻「そういうこと」

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

v2(ダークテーマ — クラス名・タグ名を全変更)
v2_highlighted.png

🔰「前回と同じで、v1→v2でセレクタが全滅するやつですね」

要素 v1 セレクタ v2 セレクタ
商品カード div.product-card article.item-tile
商品名 h2.product-name a h3.title a
価格 span.product-price div.cost
評価 div.product-rating div.stars
カテゴリ span.product-category span.tag
レビュー div.product-reviews span.review-count
説明 p.product-desc p.desc
ページネーション nav.pagination a.page-link nav.page-nav a.page-btn

🧑‍💻「ページネーションのセレクタも変えてあるのがポイント。巡回ロジックまで壊れる」
🔰「えぐい…」

セットアップ手順(クリックで展開)
# リポジトリをクローン
git clone https://github.com/matsubara457/scrapling-vs-scrapy.git
cd scrapling-vs-scrapy

# 仮想環境を作成して有効化
python3 -m venv .venv
source .venv/bin/activate

# 依存パッケージをインストール
pip install -r requirements.txt

# Flaskダミーサイト起動(port 5002)
python3 demo_site/app.py &

比較1: 速度ベンチマーク

🔰「まず気になるのは速度です。Scrapyって速いイメージありますけど」
🧑‍💻「同じ30商品(5ページ)を取得して比べてみた」

python3 -m scraper.benchmark --runs 2 --version v1
指標 Scrapy Scrapling
平均時間 0.712s 0.161s Scrapling 4.4倍 速い
メモリピーク 10.61MB 1.73MB Scrapling 6.1倍 少ない

🔰「え、4倍以上速いんですか?」
🧑‍💻「内部のパーサーが違う。ScraplingはlxmlベースでC言語レベルで速い」
🔰「Scrapyって非同期で速いイメージでしたけど」
🧑‍💻「大量のリクエストを並列に飛ばすなら速い。でも今回みたいにlocalhostの5ページだと、Twisted——PythonでいうNode.jsのイベントループみたいなもの——の初期化コストが支配的になる」
🔰「Twistedって何ですか?」
🧑‍💻「Scrapyの内部エンジン。非同期I/Oの仕組みで、reactor——イベントを監視して処理を振り分けるやつ——の起動やミドルウェアの読み込みに時間がかかる。数千ページ巡回するならこのコストは無視できるけど、5ページだと目立つ」
🔰「なるほど。ページ数が少ないとオーバーヘッドの割合が大きくなるんですね」

補足: メモリ差が大きいのも同じ理由です。Scrapyはリクエストキュー、スケジューラー、ダウンローダーミドルウェアなど多くのコンポーネントをメモリに常駐させます。Scraplingは Fetcher.get()Selector() の2ステップなので、メモリフットプリントが小さくなります。


比較2: コード量

🔰「書きやすさはどうなんですか?コード量に差あります?」

指標 Scrapy Scrapling
実効行数(空行・コメント除く) 213行 198行

🔰「あれ、ほぼ同じですね」
🧑‍💻「量はね。でも性格が全然違う。まずScrapyの方を見てみて」

scrapy_scraper.py
import scrapy
from scrapy.crawler import CrawlerProcess

class TechShopSpider(scrapy.Spider):
    name = "techshop"
    # 最初にアクセスするURL
    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("").strip(),
                "price": card.css("span.product-price::text").get(""),
            }
        # 次のページへのリンクを辿る(フレームワークが管理)
        for href in response.css("a.page-link::attr(href)").getall():
            yield response.follow(href, self.parse)

🧑‍💻「ポイントは yield response.follow()。次のページを辿る処理を3行で書ける。重複排除もリトライもフレームワークがやってくれる」
🔰「便利ですね。Scraplingだとどうなるんですか?」

scrapling_scraper.py
from scrapling.fetchers import Fetcher

def scrape_all_pages(version="v1"):
    all_products = []
    visited = set()            # 訪問済みページを自分で管理
    pages_to_visit = [1]       # 未訪問ページのキュー

    while pages_to_visit:
        page_num = pages_to_visit.pop(0)
        if page_num in visited:
            continue
        visited.add(page_num)

        # 1ページずつHTTP取得
        url = f"http://localhost:5002/page/{page_num}?v={version}"
        page = Fetcher.get(url)

        # 商品データを抽出
        for card in page.css(".product-card"):
            all_products.append({
                "name": card.css("h2.product-name a").first.text.strip(),
                "price": card.css("span.product-price").first.text.strip(),
            })
    return all_products

🧑‍💻「visited セットとか pages_to_visit とか、全部自分で管理してるでしょ」
🔰「たしかに…。巡回の仕組みを自分で書かないといけないんですね」
🧑‍💻「弱点というか、そもそもScraplingはパーサーであってクローラーじゃない。得意分野が違うだけ」

全文はリポジトリにあります: 上のコードは核心部分だけ抜粋しています。v1/v2両対応やエラーハンドリング込みの完全版はリポジトリを参照してください。


比較3: 構造変更対応(Adaptive)

🔰「前回教えてもらったAdaptiveですね。Scrapyだとどうなるんですか?」
🧑‍💻「Scrapyには構造変更に自動対応する機能はない。v1のセレクタでv2をスクレイピングしたら、当然全滅する」

python3 -m scraper.adaptive_compare full
要素 Scrapy(v1セレクタ→v2) Scrapling Adaptive
商品名 ❌ 0件 ✅ 復元
価格 ❌ 0件 ⚠️ レビュー数と誤対応
カテゴリ ❌ 0件 ✅ 復元
評価 ❌ 0件 ✅ 復元
レビュー ❌ 0件 ⚠️ 価格と誤対応
説明 ❌ 0件 ✅ 復元

🔰「Scrapy 0/6全滅、Scrapling 4/6正常復元 + 2件は取れてるけど中身が入れ替わってる」
🧑‍💻「そう。価格とレビューがクロスワイヤリングしてる」
🔰「クロスワイヤリング?」
🧑‍💻「配線が交差してる状態。span.product-price(¥12,800)の指紋で探したら span.review-count(128件のレビュー)を引っ張ってきた。タグも span で、数値テキストを持ってて、構造的な位置も近いから間違えちゃう」
🔰「でも取れてることは取れてるんですよね」
🧑‍💻「そう。だからバリデーションで弾ける。こんな感じ」

validate_example.py
# 価格なら ¥ で始まるかチェックする
def validate_price(text: str) -> bool:
    return text.startswith("¥") or text.startswith("")

# Adaptiveで取得した値を検証
found = selector.css(".product-price", adaptive=True)
text = found.first.text.strip()

if validate_price(text):
    price = text       # OK: 正しい値
else:
    print(f"⚠️ 価格として不正な値: {text}")
    # find_by_text 等でフォールバック...

🧑‍💻「ここのポイントは、Scrapyだとそもそもセレクタが見つからないから、バリデーション以前の問題ってこと」
🔰「0件 vs 復元できてるけど要検証なら、後者の方が圧倒的にマシですね」


比較4: Anti-Bot耐性

🔰「最近のサイトってBot弾きますよね。そのへんはどっちが強いんですか?」
🧑‍💻「ダミーサイトにAnti-Botシミュレーション付けて5パターン試した」

python3 -m scraper.anti_bot_compare
シナリオ ツール User-Agent 結果
requests デフォルト requests python-requests/2.32.5 ✅ 200
Bot偽装 (MyBot/1.0) requests MyBot/1.0 ❌ 403
Scrapling Fetcher Scrapling リアルなブラウザUA(自動生成) ✅ 200
Scrapy デフォルト Scrapy Scrapy/2.14 (+https://scrapy.org) ✅ 200
Scrapy + ミドルウェア Scrapy Mozilla/5.0 (Windows...) ✅ 200

🔰「あれ、ScrapyデフォルトUAでも通ってますね?」
🧑‍💻「今回のダミーサイトはUA内の scrapy 文字列をホワイトリストに入れてるからね。本番のBot検知はもっと厳しいよ」
🔰「本番だとどんな検知があるんですか?」
🧑‍💻「たとえばTLS fingerprint——HTTPSの接続方法でブラウザかBotか判別する技術——とか、JavaScriptを実行してブラウザの挙動パターンを見たりする。何層にもチェックが重なってる」
🔰「それ全部突破するの大変そう…」
🧑‍💻「Scraplingの StealthyFetcher はその辺を自動でやってくれる。内部でChromiumを起動して、ブラウザのように振る舞いながらアクセスする」

stealthy_example.py
from scrapling.fetchers import StealthyFetcher

# Bot検知を自動回避してページ取得
page = StealthyFetcher.fetch("https://bot-protected-site.com")

🔰「Scrapyだと?」
🧑‍💻「scrapy-playwright でPlaywrightを組み込むか、scrapy-fake-useragent とかのミドルウェアを自分で設定する。動くけどセットアップが面倒」

注意: 今回のAnti-BotテストはダミーサイトのUAチェックのみです。実サイトのBot検知(Cloudflare、PerimeterX等)はもっと多層的なので、この結果がそのまま当てはまるわけではありません。


比較5: ハイブリッド構成(この記事の目玉)

🔰「ここまで見ると、巡回はScrapyが強くて、パースはScraplingが強いですよね。でもどっちかを選ばないといけないんですか?」
🧑‍💻「選ばなくていい。巡回はScrapyに任せて、パースはScraplingでやる構成が組めるんだよ」
🔰「おお、最初に言ってた合体ですか!」

アーキテクチャ

🧑‍💻「ポイントはScrapyにパースさせないこと。巡回・リトライ・ページネーション追跡だけやらせて、生HTMLを集める」
🔰「で、集まったHTMLをScraplingに渡す。なるほど」

Step 1: Scrapy で HTML を収集するだけの Spider

🧑‍💻「まずStep 1のコードを見て」

hybrid.py(Step 1)
import scrapy

class HtmlCollectorSpider(scrapy.Spider):
    """HTMLだけ集めてパースしない Spider"""
    name = "html_collector"
    start_urls = ["http://localhost:5002/page/1?v=v1"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.collected_pages = []  # 生HTMLを溜めるリスト

    def parse(self, response):
        # HTMLだけ保存(パースはしない!)
        self.collected_pages.append({
            "url": response.url,
            "html": response.text,  # 生HTMLをそのまま保持
        })
        # ページネーションリンクを辿る(Scrapyの得意分野)
        for href in response.css("a.page-link::attr(href)").getall():
            yield response.follow(href, self.parse)

🧑‍💻「本当にHTMLを集めてるだけ。yield でアイテムを返してないのがポイント」
🔰「巡回はScrapyの得意分野だから任せると」

Step 2: Scrapling で高速パース

🧑‍💻「次にStep 2。集めたHTMLをScraplingでパースする」

hybrid.py(Step 2)
from scrapling.parser import Selector

def parse_with_scrapling(pages: list[dict]) -> list[dict]:
    """収集したHTMLをScraplingでパース"""
    all_products = []
    for page_data in pages:
        # 生HTMLからSelectorを生成(lxmlベースで高速)
        selector = Selector(page_data["html"])
        for card in selector.css(".product-card"):
            product = {
                "name": card.css("h2.product-name a").first.text.strip(),
                "price": card.css("span.product-price").first.text.strip(),
                "category": card.css("span.product-category").first.text.strip(),
            }
            all_products.append(product)
    return all_products

🔰「ここにAdaptive入れることもできるんですか?」
🧑‍💻「もちろん。Step 2 の Selector を呼び出すときに adaptive=True を足すだけだよ。こんな感じ」

hybrid_adaptive.py
from scrapling.core.storage import SQLiteStorageSystem

selector = Selector(
    html,
    url=url,
    adaptive=True,                    # Adaptive機能を有効化
    storage=SQLiteStorageSystem,      # 指紋の保存先
    storage_args={"storage_file": "storage.db", "url": url},
)
# v1の指紋があれば、v2のHTMLでも自動復元を試みる
cards = selector.css(".product-card", adaptive=True)

実行結果

python3 -m scraper.hybrid --version v2
ステップ 処理内容 時間
Step 1 Scrapy で5ページ巡回 0.80s
Step 2 Scrapling で30商品パース 0.007s
合計 30商品 x 5ページ 0.81s

🔰「Step 2 が 0.007秒!?」
🧑‍💻「もうHTMLがメモリにあるからね。lxmlで5ページ分のHTMLをパースするだけだから一瞬で終わる」
🔰「待ってください、これ --version v2 で実行してますよね? v2のHTMLなのにデータがちゃんと取れてる…しかも5ページ全部!」
🧑‍💻「そう、そこが本題。Scrapyの巡回力で5ページを安定して回りつつ、ScraplingのAdaptiveで壊れたHTMLからデータを引っこ抜く。さっきの比較3でScrapy単体だと0/6全滅だったのに、ハイブリッドなら30商品ちゃんと取れてる」
🔰「巡回に0.8秒かかってるのはScrapyのオーバーヘッドですか?」
🧑‍💻「Twisted——さっき話したイベントループ——のreactor起動 + 5ページのHTTP取得。localhostだからネットワーク遅延はほぼゼロだけど、フレームワークの初期化コストがある」
🔰「…でもこの構成って、Scrapy単体で全部やるのと速度変わらなくないですか?」
🧑‍💻「変わらない。このスケールだとね。ハイブリッドの真価は速度じゃない。構造が壊れたサイトでも巡回を止めずにデータを取りきれること。Scrapy単体なら0件で終わってた場面で、30商品持って帰れる」
🔰「それが"激つよ"の意味か…」


まとめ

🔰「全部見てきましたけど、結局どっちが強いんですか?」
🧑‍💻「"どっちが強いか"じゃなくて、"何が得意か"で考えるべき」

総合比較

比較軸 Scrapy Scrapling ハイブリッド
パース速度 0.71s 0.16s 0.81s(巡回込み)
メモリ使用量 10.6MB 1.7MB
ページ巡回 (フレームワーク管理) △(手動ループ) (Scrapy担当)
構造変更対応 ✗(固定セレクタ) (Adaptive自動復元) (Scrapling担当)
Anti-Bot耐性 △(ミドルウェア追加が必要) (StealthyFetcher内蔵) ◎(Scraplingで補完)

🧑‍💻「フローチャートにするとこんな感じ」

🔰「おお、これわかりやすい」

どれを使えばいい?

やりたいこと おすすめ
数ページの定型スクレイピング Scrapling 単体 — 軽量・高速・シンプル
数百〜数千ページの大規模巡回 Scrapy 単体 — 非同期巡回・リトライ・ジョブ管理
巡回 + 構造変更に自動対応したい ハイブリッド — Scrapy巡回 + Scrapling Adaptive
Bot対策が厳しいサイト Scrapling(StealthyFetcher) or Scrapy + scrapy-playwright
安定して動いてるBS4スクレイパー そのままでOK

🔰「ScrapyとScraplingって競合じゃなくて補完関係なんですね」
🧑‍💻「そう。得意なことが違うから、組み合わせると最強」
🔰「じゃあ今BS4で安定して動いてるやつは、わざわざ変えなくていいんですか?」
🧑‍💻「BS4で安定してるなら無理に変える必要はないよ。動いてるものを壊すメリットはない」
🔰「ですよね」
🧑‍💻「でも新規プロジェクトなら選択肢として知っておくべき。Scraplingはパーサーとしても速いし、Adaptiveは他にない強み。Scrapyユーザーでもパース部分だけ差し替える選択肢が増える」
🔰「まずはScraplingのドキュメント読んでみます!」
🧑‍💻「いいね。リポジトリにデモ一式入ってるから、30分あれば一通り試せるよ。得意なことが違うなら、組み合わせればいい。それだけの話」


リポジトリ: https://github.com/matsubara457/scrapling-vs-scrapy

※環境構築手順は記事上部の「セットアップ手順」を参照

python3 demo_site/app.py &                # Flask起動(port 5002)
python3 -m scraper.benchmark --runs 2     # 速度ベンチマーク
python3 -m scraper.adaptive_compare full  # 構造変更対応比較
python3 -m scraper.hybrid --version v1    # ハイブリッド実行

Scrapling公式: https://github.com/D4Vinci/Scrapling


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

前回の記事(BS4 vs Scrapling)
BeautifulSoup4からの乗り換えを検討している方はこちらもどうぞ。
【Python】「サイト変わってデータ取れません」「自動で直るやつあるよ」〜次世代スクレイピングライブラリScrapling〜

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?