Scrapy は、requests スクリプトがスケールしなくなったときに次に進むツールです。Python 向けの本格的なクローリングフレームワークで、何を抽出し、どのリンクをたどるか を宣言する小さな spider クラスを書けば、スケジューリング、並行処理、リトライ、重複排除、エクスポートは Scrapy が引き受けます。このチュートリアルは、scrapy startproject から本番向けの安全な設定を備えた 2 段階クロールまでを、一貫して現行の Scrapy 2.x 構文で案内します。
なぜ requests + BeautifulSoup ではなく Scrapy なのか?
requests と BeautifulSoup でスクレイパーをいくつか書いたことがあれば——Python web scraping guide や BeautifulSoup tutorial で扱っているスタックだ——すでにパターンはご存じだろう。ページを取得し、パースし、繰り返す。これは 1 ページや 1 つのリストなら見事に機能する。だが仕事が クローリング になった途端に破綻する。多数のページを訪れ、進みながらリンクを発見し、同じ URL を二度取得しないこと。この発見とスケジューリングのループこそが what web crawling actually is であり、まさに Scrapy が担う部分だ。
- スケジューラとリクエストキュー。 あなたはリクエストを yield し、Scrapy がいつ送るか、いくつ並行して実行するかを決め、失敗を再試行する。
- 重複排除つきの URL フロンティア。 5 つのページから同じリンクをたどっても、Scrapy はそれを一度だけ取得する。
- item パイプライン。 スクレイプされた各レコードは、それをクリーンにし、検証し、あるいは保存する小さなクラスの連なりを流れていく——パースのロジックとは切り離されている。
- フィードエクスポート。 コマンドラインのフラグひとつで JSON、JSON Lines、CSV を出力でき、シリアライズのコードは要らない。
トレードオフをはっきりさせておこう。BeautifulSoup はパース用ライブラリであり、しかも優れたものだ。Scrapy は独自のセレクタエンジンを備えるが、より重い依存関係で、学習曲線も急だ。1 ページだけなら Scrapy は大げさすぎる。ページネーションの奥に 500 ページがあるなら、Scrapy は、さもなければ一度は下手に書いて延々とデバッグするはめになるキューとリトライの配管コード 100 行を置き換えてくれる。
プロジェクトのセットアップ: startproject が実際に作るもの
Scrapy をインストールし(仮想環境を強く推奨します)、プロジェクトを生成します。
pip install scrapy
scrapy startproject quotes_crawler
cd quotes_crawler
生成されるツリーは次のようになり、各ファイルには役割があります。
quotes_crawler/
├── scrapy.cfg # deploy/config entry point — tells Scrapy where settings live
└── quotes_crawler/
├── items.py # optional typed containers for scraped data
├── middlewares.py # hooks into the request/response cycle (headers, proxies)
├── pipelines.py # post-processing for scraped items (clean, validate, store)
├── settings.py # project-wide knobs: concurrency, delays, throttling
└── spiders/ # your spider classes live here — one class per crawl target
items.py と middlewares.py はいまは無視してかまいません——最初のプロジェクトでは素の dict とデフォルトのミドルウェアで十分です。今日あなたが実際に触る 2 つのファイルは、spiders/ の中の新しい spider と、後で扱う settings.py と pipelines.py です。
genspider で spider の雛形を作ります。
scrapy genspider quotes quotes.toscrape.com
はじめての spider
公式 Scrapy チュートリアルが使うサンドボックスサイト、quotes.toscrape.com をスクレイプします。生成された spiders/quotes.py を次の内容で置き換えてください。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/page/1/"]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
3 つの考え方がフレームワーク全体を支えます。
start_urlsはクロールが始まる場所です。Scrapy は各 URL を取得し、そのレスポンスをparse()に渡します。(Scrapy 2.13 以降は、動的な開始リクエストのために代わりにasync def start(self)を定義できます。start_urlsは依然として慣用的でシンプルなケースですが、古いstart_requests()メソッドは非推奨になっている点に注意してください。)response.css()は CSS セレクタで要素を選びます——::textはテキストノードを、::attr(href)は属性を抽出し、.get()は最初のマッチまたは None を、.getall()はすべてのマッチをリストとして返します。CSS で表現できないことが必要なときのためにresponse.xpath()があります。内部的には、CSS セレクタはいずれにせよ XPath へ変換されます。これは BeautifulSoup が解く HTML parsing と同じ問題を、Scrapy のセレクタ API で扱うものです。yieldで dict を返す と、Scrapy はそれをスクレイプされた item として扱います——戻り値も、蓄積するリストも要りません。
実行して、そのまま JSON にエクスポートします。
scrapy crawl quotes -O quotes.json
-O フラグは実行のたびに出力ファイルを上書きします。小文字の -o は代わりに追記し、これは主に JSON Lines 形式(-o quotes.jsonl)で役立ちます。quotes.json を開くと、構造化されたレコードが 10 件——1 ページ分——見つかります。まだ何もリンクをたどっていないからです。
リンクをたどる: ページネーションと 2 段階クロール
クローリングは、parse() が item とともに リクエスト を yield したときに始まります。Quotes サイトには Next ボタンがあり、response.follow() が 2 行でそれを処理します——相対 URL をそのまま受け取るので、手動の urljoin は不要です。
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
もう一度 scrapy crawl quotes -O quotes.json を実行すると、10 ページすべて——およそ 100 件の引用——が得られます。スケジューラは各 Next リンクをキューに入れ、取得し、そのレスポンスを parse() へ戻しました。重複した URL は自動的に除外されていたはずです。
実際のプロジェクトを解き放つパターンが 2 段階クロール です。リスティングページが詳細ページへのリンクを yield し、2 つ目のコールバックが各詳細ページをパースします。次の spider は、引用のリスティングをたどりつつ、各著者の経歴ページからデータを抽出します。
import scrapy
class AuthorSpider(scrapy.Spider):
name = "authors"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
# Level 1: listing pages — follow every author link
author_links = response.css(".author + a")
yield from response.follow_all(author_links, callback=self.parse_author)
# ...and keep paginating the listing itself
yield from response.follow_all(css="li.next a", callback=self.parse)
def parse_author(self, response):
# Level 2: detail pages
yield {
"name": response.css("h3.author-title::text").get().strip(),
"born": response.css(".author-born-date::text").get(),
"bio": response.css(".author-description::text").get().strip()[:200],
}
response.follow_all() はマッチしたリンクごとに 1 つのリクエストを yield し、各コールバックは自分のページタイプだけを気にすればよくなります。多くの引用が同じ著者を指していても、各経歴は一度だけ取得されます——ここでも重複排除です。同じ形は、EC カタログ(books.toscrape.com のようにカテゴリページから商品ページへ)や求人ボード(検索結果から求人へ)にもスケールします。
「このパターンに一致するすべてのリンクをたどる」ことが戦略のすべてであるようなクロールのために、Scrapy には CrawlSpider が用意されています。フォローのロジックを手書きする代わりに、LinkExtractor を持つ Rule オブジェクトを宣言するサブクラスです。まずは素の Spider から始め——何を取得するかを正確に理解できます——フォローのルールが繰り返しばかりになってきたら CrawlSpider に手を伸ばしましょう。
本番設定: 速く、しかし無礼にならない
デフォルト値はサンドボックス向けに調整されていて、他人のサーバー向けではありません。Scrapy は平気で 16 の並行リクエストを遅延ゼロで走らせます。これは rate limiting に引っかかったり、小さなサイトに負担をかけたりする格好の方法です。実際のクロールの前に、settings.py を開きましょう。
# settings.py — a polite production baseline
# Respect robots.txt (on by default in new projects — leave it on)
ROBOTSTXT_OBEY = True
# Identify yourself honestly
USER_AGENT = "quotes_crawler (+https://yourdomain.example/contact)"
# Cap concurrency per site and add a base delay
CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 1.0 # seconds; Scrapy randomizes it a bit by default
# Let Scrapy adapt speed to the server's actual latency
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5.0
AUTOTHROTTLE_MAX_DELAY = 60.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
AutoThrottle は、たいていのチュートリアルが飛ばす設定であり、そして最も知る価値のある設定です。レスポンスのレイテンシを測定し、遅延を動的に調整します——サーバーが遅くなったりエラーを返したりすると、Scrapy は AUTOTHROTTLE_MAX_DELAY に向けて後退します。健全なときは、目標の並行数に向けて加速します。DOWNLOAD_DELAY は決して下回らない下限として働き、CONCURRENT_REQUESTS_PER_DOMAIN は厳格な上限のままです。スロットルの判断をログで見るには、一時的に AUTOTHROTTLE_DEBUG = True を設定します。
ROBOTSTXT_OBEY には一文を割く価値があります。これは Scrapy に、クロールの前に各サイトの robots.txt ルールを取得して尊重させます。生成されたプロジェクトではデフォルトで有効です。有効なままにしておきましょう。
最小の item パイプライン
パイプラインは、クリーンアップをあなたのパースコードの外に保ちます。スクレイプされた各 item は、有効なすべてのパイプラインクラスの process_item() を通過します。
# pipelines.py
from scrapy.exceptions import DropItem
class CleanQuotePipeline:
def process_item(self, item, spider):
if not item.get("text"):
raise DropItem("missing quote text")
item["text"] = item["text"].strip("“” ") # strip curly quotes
item["author"] = item["author"].strip()
return item
settings.py で有効にします——数字は順序の優先度で、小さいほど先に実行されます。
ITEM_PIPELINES = {
"quotes_crawler.pipelines.CleanQuotePipeline": 300,
}
同じ形が、検証、フィールドによる重複排除、あるいはデータベースへの書き込みを扱います(パイプラインは接続のために open_spider と close_spider のフックも受け取ります)。パースはセレクタの話にとどまり、データ品質はパイプラインが担います。
- spider は parse() から dict を yield し、response.follow / follow_all でリンクをたどる
- セレクタは spider に入れる前に scrapy shell でテスト済み
- ROBOTSTXT_OBEY が有効で、USER_AGENT がクローラーを名乗っている
- AUTOTHROTTLE_ENABLED が true で、DOWNLOAD_DELAY が下限になっている
- クリーンアップと検証は parse() ではなくパイプラインにある
- 出力を scrapy crawl name -O output.json で確認済み
Scrapy の限界: JavaScript とアンチボットシステム
Scrapy は自分の仕事において卓越しています——静的な HTML を大規模にクロールする用途では、なかなか敵いません。ですが、ぶつかる前に知っておく価値のある壁が 2 つあります。
JavaScript で描画されるページ。 Scrapy のダウンローダは生の HTML を取得します。ブラウザエンジンは動かしません。データがクライアントサイドの描画のあとにしか現れないなら、あなたのセレクタは何もマッチしません。標準的な対処は scrapy-playwright プラグインで、Scrapy のスケジューリングを保ちつつリクエストをヘッドレスブラウザ経由でルーティングします——あるいは、Playwright scraping guide で扱う独立したブラウザスタックです。いずれにせよ、描画はページごとに実際の CPU とメモリを消費します。
大規模なアンチボットシステム。 大手プラットフォームは TLS ハンドシェイクのフィンガープリントを取り、IP レピュテーションを採点し、JavaScript チャレンジを出します。うまくスロットルされた Scrapy spider でも、1 つの IP からではそこで依然としてブロックされ、プロキシミドルウェアやヘッダーの小細工を積み重ねることは第 2 のエンジニアリングプロジェクトになります——その軍拡競争は scraping sites that block bots で扱っています。
そうしたターゲットに対する現実的な一手は、Scrapy が扱えるサイトには Scrapy を使い続け、扱えないサイトにはスクレイピング API を呼ぶことです。Crawlora は、堅牢化されたプラットフォームを、構造化された JSON を返すドキュメント化されたエンドポイントとして公開しています——こちらは Amazon 検索で、あなたの側にプロキシもパーサーもありません。
import requests
resp = requests.get(
"https://api.crawlora.net/api/v1/amazon/search",
params={"k": "mechanical keyboard"},
headers={"x-api-key": "YOUR_API_KEY"},
)
results = resp.json()["data"] # structured listings, not HTML
パイプラインや spider の内側から呼び出して、クロールしたレコードを補強することもできます。課金は成功課金(pay-on-success)——成功した 2xx レスポンスにのみ課金されます——で、無料枠は月 2,000 クレジット、カード不要です。エンドポイントのカタログは docs で見られますし、ボリュームの計算は pricing をご確認ください。
まとめ
これで Scrapy のループが一通りそろいました。startproject と genspider で雛形を作り、response.css で抽出し、dict を yield し、response.follow でリンクをたどり、-O でエクスポートし、AutoThrottle でスロットルし、パイプラインでクリーンにする。これで実際のクローリング作業のかなり大きな割合をカバーできます。ターゲットが JavaScript で描画されたり、アンチボットシステムで反撃してきたりしたら、scrapy-playwright を後付けするか、その 1 サイトだけを API に任せましょう——そして、あなたの spider には本当に得意なことを続けさせてあげてください。
spider がクロールできないサイトにぶつかっていますか?
簡単なターゲットには Scrapy を、難しいものにはドキュメント化されたエンドポイントを——構造化された JSON、マネージドなプロキシとリトライ。毎月 2,000 クレジット無料、カード不要。
あわせて読みたい: 全体像には Python web scraping guide、より軽いスタックが欲しいなら BeautifulSoup tutorial、JavaScript 主体のターゲットには web scraping with Playwright。
よくある質問
Scrapy は何に使いますか?
Scrapy は、ウェブサイトをクロールして構造化データを抽出するための Python フレームワークです。パース用ライブラリと違い、クロール全体を管理します。リクエストのスケジューリング、リンクの追跡、URL の重複排除、失敗の再試行、速度のスロットリング、そして結果の JSON や CSV へのエクスポートです。数ページを取得する規模を超えたプロジェクトでは、これが標準ツールになります。
Scrapy は BeautifulSoup より優れていますか?
両者は異なる問題を解きます。BeautifulSoup は 1 つの HTML ドキュメントをパースし、Scrapy は独自のセレクタに加えてスケジューリング、並行処理、重複排除、パイプライン、フィードエクスポートを備えた本格的なクローリングフレームワークです。1 ページだけなら requests と BeautifulSoup のほうがシンプルです。ページネーションの奥にある多数のページをクロールしたり、リンクをたどったりするなら、Scrapy は自分で書くことになるキューとリトライの配管を置き換えてくれます。
Scrapy は JavaScript を扱えますか?
単体では扱えません。Scrapy は生の HTML を取得し、ブラウザエンジンを動かさないため、クライアントサイドで描画されるコンテンツはセレクタから見えません。一般的な対処は scrapy-playwright プラグインで、Scrapy のスケジューリングを保ちつつヘッドレス Chromium でページを描画します。あるいは、レンダリングを代わりに処理してくれるスクレイピング API に JavaScript 主体のサイトを任せる方法もあります。
Scrapy の spider を実行して出力を保存するには?
プロジェクトディレクトリから scrapy crawl spidername -O output.json を実行します。-O フラグは実行のたびにファイルを上書きし、小文字の -o は追記します。これは JSON Lines(output.jsonl)と最も相性がよいです。Scrapy のフィードエクスポートは、追加のコードなしで CSV や XML にも対応します。
Scrapy はどのようにリンクをたどってページ送りしますか?
parse() コールバックが item とともにリクエストを yield します。response.follow() はセレクタから得た相対 URL をそのまま受け取り、response.follow_all() はマッチしたリンクごとに 1 つのリクエストを yield します——たとえば各ページの次リンクや、2 つ目のコールバックへ渡す各詳細ページのリンクをたどります。重複した URL は Scrapy が自動的に除外します。
Scrapy がウェブサイトに過負荷をかけないようにするには?
AutoThrottle を有効にして(AUTOTHROTTLE_ENABLED = True)、Scrapy がサーバーのレイテンシに応じて遅延を調整するようにし、DOWNLOAD_DELAY を下限として設定し、CONCURRENT_REQUESTS_PER_DOMAIN を制限し、ROBOTSTXT_OBEY を有効なままにします。これらの設定はプロジェクトの settings.py にあります。
Scrapy の item パイプラインとは何ですか?
パイプラインは、パース後にスクレイプされた各 item が通過する小さなクラスです。それぞれが process_item() を定義し、item をクリーンにし、検証し、重複排除し、あるいは保存します——不良なレコードを捨てるには DropItem を投げます。settings.py の ITEM_PIPELINES に優先度の数字とともに登録され、データ品質のロジックを spider の parse コードの外に保ちます。
