大規模なWebスクレイピングでは、速度と安定性、そしてサイトへの配慮が欠かせません。
本記事では、非同期処理を内蔵したScrapyを使い、Python初心者でも段階的に学べるように、プロジェクト作成からSpider実装、速度最適化、安全運用のための設定までを一気通貫で解説します。
大量データを高速かつ安全に収集するための実践的な設計と設定がポイントです。
Scrapyとは
Scrapyは、Python製の高速なWebスクレイピングフレームワークです。
非同期I/Oを基盤にしたダウンローダ、キューイング、リトライ、重複排除、パイプライン、フィード出力など、スケールに必要な機能を一式で提供します。
大規模Webスクレイピングに強い理由
Scrapyは内部でTwistedベースの非同期ダウンローダを採用し、1プロセスで多数のHTTPリクエストを同時に処理します。
これにより、スレッドやプロセスを自前で管理せずとも高いスループットが得られます。
さらに、スケジューラがURLキューを管理し、重複排除や優先度制御を自動化します。
AutoThrottleでの負荷調整、HTTPキャッシュ、堅牢なリトライとタイムアウト、アイテムの加工と保存を担うPipelineなど、運用に必要な機能が標準で揃っている点が、大規模対応の強さです。
Python初心者でも扱いやすいポイント
Scrapyはコマンドでプロジェクトを作り、ひな形に沿ってファイルを埋めるだけで動きます。
response.css
やresponse.xpath
を使った直感的なセレクタ、scrapy crawl
での実行、-O
オプションでのCSVやJSON出力など、最短ルートで成果が得られます。
最初は少ないコードで動かし、徐々に設定を厚くしていく流れが作りやすいのが利点です。
requests/Beautiful Soupとの違い
requests+Beautiful Soupは小規模や単発取得に適していますが、大規模化すると並列化、再試行、重複排除、保存の最適化など多くを自前で実装する必要が出てきます。
Scrapyはこれらを内蔵し、構成の一貫性を保ちながら拡張できます。
以下に主な違いをまとめます。
観点 | requests+Beautiful Soup | Scrapy |
---|---|---|
並列処理 | 自前実装が必要 | 内蔵の非同期で高速 |
リトライ/タイムアウト | 自前設定 | 標準設定で容易 |
重複排除 | 自前管理 | 既定で対応 |
データ保存 | 自前でCSV/DB | FEEDS/Pipelineで一発 |
クロール制御 | 自前で実装 | スケジューラとルールで容易 |
ログ/統計 | 手作り | 標準で詳細統計 |
大量ページを安定して回すならScrapyを基盤にするのが実務では一般的です。
Scrapyの準備と基本
インストール
Python 3.8以上を想定します。
仮想環境の使用を推奨しますが、ここでは最小手順を示します。
# インストール
pip install -U scrapy
# バージョン確認
python -c "import scrapy, sys; print(scrapy.__version__)"
2.11.2
環境によっては依存パッケージのビルドに時間がかかることがあります。
エラーが出た場合は、pip
のアップグレードや開発ツールチェーンの導入を確認してください。
プロジェクト作成
学習用に公式のデモサイト(https://quotes.toscrape.com)を対象にします。
このサイトはスクレイピング練習用に公開されており、学習目的でのアクセスが想定されています。
# 新規プロジェクト作成
scrapy startproject quotesproj
cd quotesproj
作成される構成の例:
quotesproj/
scrapy.cfg
quotesproj/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py
主要ファイルと構成
- items.py: 取得データの型定義を行います。スキーマのような役目で、後段のPipelineや保存処理と噛み合わせます。
- spiders/: クロールの中心であるSpiderを置きます。URLの開始点、要素抽出、リンク追跡を記述します。
- pipelines.py: 取得したアイテムを整形し、保存先へ送ります。大規模時はここで軽量化が重要です。
- settings.py: 並列度、アクセスポリシー、出力などの設定を集中管理します。
- scrapy.cfg: 複数プロジェクトや環境実行時のラッパ設定です。
設定はsettings.pyに集約し、Spiderは抽出ロジックだけに集中させると、保守性が大きく向上します。
初めてのSpider作成と実行
Item定義で取得項目を決める
はじめに取得したい項目を列挙します。
引用文、著者、タグの3項目です。
# quotesproj/items.py
import scrapy
class QuoteItem(scrapy.Item):
# 引用文
text = scrapy.Field()
# 著者名
author = scrapy.Field()
# タグのリスト
tags = scrapy.Field()
Spiderの基本
ページを巡回し、各引用を抽出し、次ページへのリンクを辿ります。
1つのSpiderに「何を開始点にするか」「どこまで辿るか」「何を取るか」を明確に書くのが基本です。
# quotesproj/spiders/quotes_spider.py
import scrapy
from quotesproj.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"] # クロール対象のドメインを制限
start_urls = ["https://quotes.toscrape.com/"] # 開始URL
def parse(self, response):
# 各引用ブロックを走査
for q in response.css("div.quote"):
item = QuoteItem()
# .cssで要素抽出、.get()で文字列取得
item["text"] = q.css("span.text::text").get()
item["author"] = q.css("small.author::text").get()
# 複数要素は.getall()でリストに
item["tags"] = q.css("div.tags a.tag::text").getall()
yield item
# 次ページがあれば辿る
next_page = response.css("li.next a::attr(href)").get()
if next_page:
# response.followで相対URLを解決して次へ
yield response.follow(next_page, callback=self.parse)
実行と簡易出力
ScrapyはFEEDS機能で保存が簡単です。
JSON Lines形式は大規模時にメモリ効率がよく、追記にも向きます。
# JSON Linesに保存(-Oは上書き、-oは追記)
scrapy crawl quotes -O quotes.jl
実行ログの例:
2025-09-21 10:00:00 [scrapy.utils.log] INFO: Scrapy 2.11.2 started
2025-09-21 10:00:00 [scrapy.core.engine] INFO: Spider opened
2025-09-21 10:00:01 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/>
{'text': '“The world as we have created it is a process of our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
...
2025-09-21 10:00:02 [scrapy.core.engine] INFO: Closing spider (finished)
2025-09-21 10:00:02 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_count': 10,
'item_scraped_count': 100,
'response_received_count': 10,
...}
出力ファイルの先頭例:
{"text": "“The world as we have created it is a process of our thinking.”", "author": "Albert Einstein", "tags": ["change", "deep-thoughts", "thinking", "world"]}
{"text": "“It is our choices, Harry, that show what we truly are, far more than our abilities.”", "author": "J.K. Rowling", "tags": ["abilities", "choices"]}
セレクタの書き方
ScrapyではCSSとXPathの両方が使えます。
初心者にはCSSが読みやすく、複雑な条件にはXPathが有効です。
# CSSセレクタの例
quote = response.css("div.quote")[0]
text = quote.css("span.text::text").get()
author = quote.css("small.author::text").get()
# XPathの例
text_x = quote.xpath(".//span[@class='text']/text()").get()
author_x = quote.xpath(".//small[@class='author']/text()").get()
CSSは短く、XPathは柔軟です。
どちらも.get()
と.getall()
の使い分けが重要です。
データ保存
FEEDSをsettings.pyに記述しておけば、コマンドだけで保存先や形式を切り替えられます。
大規模時はJSON LinesやCSVを推奨します。
# quotesproj/settings.py の一部
FEEDS = {
"outputs/quotes_%(time)s.jl": { # タイムスタンプ付きファイル名
"format": "jsonlines",
"encoding": "utf8",
"store_empty": False,
"item_export_kwargs": {"ensure_ascii": False}, # 日本語保持
}
}
保存は実行時に自動で行われます。
まずはFEEDSで手早く成果を出し、その後Pipelineに切り替えると設計が分かりやすいです。
大量データを高速かつ安全に収集する設定
本章では、高速化と安全性の両立を、Scrapyの設定で実現する方法を示します。
サイトの利用規約とrobots.txtを必ず確認し、相手サーバに過度な負荷をかけないことが大前提です。
並列リクエスト(CONCURRENT_REQUESTS)の目安
並列リクエストは速度の要です。
ただし大きすぎる値は相手に負荷をかけ、ブロックの原因になります。
まずは中庸から始め、AutoThrottleと併用して調整します。
# settings.py
# グローバル並列数
CONCURRENT_REQUESTS = 16 # 初心者の初期目安: 8〜16
# ドメイン単位の並列制御
CONCURRENT_REQUESTS_PER_DOMAIN = 8
# IP単位の並列制御(逆プロキシ等の事情により使い分け)
CONCURRENT_REQUESTS_PER_IP = 0 # 0は無効
画像や静的ページ中心なら並列を上げ、重いページやAPIなら抑えるのが実務的な方針です。
AutoThrottleで負荷を自動調整
AutoThrottleは応答時間に応じてリクエスト間隔を自動調整します。
最初は様子見として遅めに開始し、上限も設定します。
# settings.py
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 0.5 # 初期遅延
AUTOTHROTTLE_MAX_DELAY = 10.0 # 最大遅延
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 # 平均同時リクエスト数の目標
AUTOTHROTTLE_DEBUG = False # Trueにすると調整ログ詳細を出力
ログ例:
2025-09-21 10:05:00 [scrapy.extensions.throttle] INFO: New delay: 0.75 (avgresponse: 0.35, latency: 0.12)
robots.txtとアクセス間隔
Scrapyは既定でROBOTSTXT_OBEY = True
です。
必ずTrueのままにし、禁止されたパスにはアクセスしないでください。
さらにDOWNLOAD_DELAY
で1リクエスト間の間隔を設けると、相手への負荷が平準化されます。
# settings.py
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 0.25 # 250ms間隔、AutoThrottleと併用可
robots.txtやサイト利用規約(ToS)で禁止されているクロールやスクレイピングは行わないことを徹底してください。
タイムアウトとリトライ
ネットワーク異常は必ず発生します。
タイムアウトは短すぎず長すぎず、リトライは有限回にします。
どんなエラーでも無限リトライは禁物です。
# settings.py
DOWNLOAD_TIMEOUT = 20 # 秒
RETRY_ENABLED = True
RETRY_TIMES = 2 # 合計3回まで試行
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]
# 429(Too Many Requests)が出る場合はAutoThrottleやDOWNLOAD_DELAYを増やし、礼節を優先
クロール範囲の制御
範囲を絞るほど安全で高速です。
allowed_domains、URLパターン、深さ制限を組み合わせます。
規則的な巡回にはCrawlSpiderが便利です。
# 例: CrawlSpiderで特定パスだけを辿る
# quotesproj/spiders/quotes_crawlspider.py
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from quotesproj.items import QuoteItem
class QuotesCrawlSpider(CrawlSpider):
name = "quotes_rules"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com/"]
rules = (
# ページネーションだけ辿る
Rule(LinkExtractor(allow=r"/page/\d+/?$"), callback="parse_item", follow=True),
)
def parse_item(self, response):
for q in response.css("div.quote"):
yield QuoteItem(
text=q.css("span.text::text").get(),
author=q.css("small.author::text").get(),
tags=q.css("div.tags a.tag::text").getall(),
)
深さ制限や不要リンクの制限:
# settings.py
DEPTH_LIMIT = 5 # 深さ上限。小さくするほど安全で高速
# 不要なファイル拡張子を回避する例はLinkExtractorのdenyで指定可能
重複排除とキャッシュ
Scrapyは既定でRFPDupeFilter
によるURL重複排除が有効です。
大規模ジョブでは途中停止と再開を考慮してJOBDIR
を使います。
開発中はHTTPキャッシュを有効にすると検証が高速化します。
# settings.py
# ジョブの中断再開に必要な状態保存ディレクトリ
JOBDIR = "job_state/quotes_job"
# HTTPキャッシュ(開発やデバッグで有効化、本番では要検討)
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0 # 0は無期限。学習時のみ推奨
HTTPCACHE_DIR = "httpcache"
HTTPCACHE_IGNORE_HTTP_CODES = [500, 502, 503, 504, 408]
HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"
キャッシュは相手に負荷をかけずにデバッグを反復できるため、選択的に活用します。
Pipelineで軽量な整形と保存
Pipelineはアイテムのバリデーションや整形、保存を担当します。
大規模時はCPUやI/Oの重い処理を避け、ストリームに順次書き出す設計が基本です。
# quotesproj/pipelines.py
import json
import gzip
class CleanAndSavePipeline:
def open_spider(self, spider):
# 圧縮JSON Linesに書き出し(大規模時のI/O削減)
self.fp = gzip.open("outputs/quotes.gz", "wt", encoding="utf-8")
def close_spider(self, spider):
self.fp.close()
def process_item(self, item, spider):
# 軽い整形: 前後空白除去とタグ整列
text = (item.get("text") or "").strip()
author = (item.get("author") or "").strip()
tags = item.get("tags") or []
# タグはソートして安定化
item["text"] = text
item["author"] = author
item["tags"] = sorted(set(tags))
# 1行ずつ追記
line = json.dumps(dict(item), ensure_ascii=False)
self.fp.write(line + "\n")
return item
上記Pipelineを有効化します。
# settings.py
ITEM_PIPELINES = {
"quotesproj.pipelines.CleanAndSavePipeline": 300,
}
# FEEDSと二重書き出しにしないよう、FEEDSはコメントアウトか不要設定に
FEEDS = {}
出力ファイル例:
# outputs/quotes.gz の中身をzcat等で閲覧
{"text": "“Life is what happens to us while we are making other plans.”", "author": "Allen Saunders", "tags": ["fate", "life", "misattributed-john-lennon", "plans"] }
1アイテムずつ追記するストリーミング出力は、メモリ使用を一定に保てるため大規模向きです。
DB保存を行う場合も、バルクインサートやコネクションプーリングを使い、一件ずつのコミットを避けると効率的です。
実行ログと統計で進捗を確認する
Scrapyは詳細な統計を自動で集計します。
リクエスト数、成功率、平均応答時間、ドロップ件数などを確認し、設定値を微調整します。
# settings.py
LOG_LEVEL = "INFO" # 学習時はINFO、本番調査時はDEBUGも選択
STATS_DUMP = True # 終了時に統計をダンプ
プログラムから終了時に統計をファイルへ落とす例です。
# quotesproj/spiders/quotes_spider_with_stats.py
import json
import scrapy
from scrapy import signals
from quotesproj.items import QuoteItem
class QuotesSpiderWithStats(scrapy.Spider):
name = "quotes_stats"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com/"]
@classmethod
def from_crawler(cls, crawler):
spider = super().from_crawler(crawler)
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
return spider
def parse(self, response):
for q in response.css("div.quote"):
yield QuoteItem(
text=q.css("span.text::text").get(),
author=q.css("small.author::text").get(),
tags=q.css("div.tags a.tag::text").getall(),
)
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def spider_closed(self, spider):
stats = spider.crawler.stats.get_stats()
with open("outputs/run_stats.json", "w", encoding="utf-8") as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
実行と出力例:
scrapy crawl quotes_stats
{
"downloader/request_count": 10,
"downloader/response_status_count/200": 10,
"item_scraped_count": 100,
"response_received_count": 10,
"finish_reason": "finished",
"finish_time": "2025-09-21T10:12:34"
}
進捗の可視化ができると、どの設定がスループットや安定性に効いたかを客観的に評価できます。
以下は速度と安全性の初期目安です。
まずは控えめに始め、相手の応答と統計を見ながら調整します。
設定 | おすすめ初期値 | 目的 |
---|---|---|
CONCURRENT_REQUESTS | 8〜16 | 全体並列数 |
CONCURRENT_REQUESTS_PER_DOMAIN | 4〜8 | ドメイン単位の抑制 |
DOWNLOAD_DELAY | 0.25〜1.0秒 | 負荷の平準化 |
AUTOTHROTTLE_ENABLED | True | 自動調整 |
DOWNLOAD_TIMEOUT | 15〜30秒 | ハング防止 |
RETRY_TIMES | 1〜3 | 一時障害の吸収 |
ROBOTSTXT_OBEY | True | マナー遵守 |
DEPTH_LIMIT | 3〜5 | 範囲制御 |
HTTPCACHE_ENABLED | True(学習時) | デバッグ高速化 |
繰り返しになりますが、対象サイトの規約に従い、利用許諾がない領域の収集や高頻度アクセスは行わないでください。
特に認証が必要な領域や有料ページ、動的レンダリングが重いサイトでは別の配慮が必要です。
まとめ
本記事では、Scrapyを使って大量データを高速かつ安全に収集する実践的方法を解説しました。
プロジェクトの作成からItemとSpiderの基本、CSSやXPathによる抽出、FEEDSやPipelineによる保存まで、初心者でも迷わず動かせる手順を示しています。
さらに、並列リクエスト、AutoThrottle、robots.txt遵守、タイムアウトやリトライ、範囲制御、重複排除とキャッシュ、ログと統計といった大規模運用に必須の設定をまとめました。
大切なのは、速度と礼節の両立です。
控えめな並列度から開始し、統計を見ながら少しずつ調整します。
アイテム処理は軽量に保ち、出力はストリーミングで安定化します。
これらの原則を押さえれば、Scrapyは個人でも企業でも通用する強力なデータ収集基盤になります。