午前2時のOOM(メモリ不足)という悪夢
数ヶ月前、私は市場調査レポートを作成するために、大手ECサイトから50万件の商品データを取得するタスクを引き受けました。その時の私はかなり楽観的でした。「ただのデータ収集だし、requestsとBeautifulSoupを使えば大丈夫だろう」と考えていたのです。
しかし、深夜2時、サーバーは真っ赤なエラーを吐き出しました。スクリプトは3時間実行されたところでフリーズ。RAMの使用量は4GBまで急上昇し、クラッシュ(Out of Memory)しました。さらに悪いことに、短時間に大量のリクエストを送りすぎたため、サーバーのIPが相手側のブラックリストに入れられてしまいました。最初はわずか200行程度のコードでしたが、リトライ処理やプロキシ、マルチスレッドなどの修正を重ねるうちに2000行近くまで膨れ上がり、それでも動作は不安定なままでした。
その時、私はある事実に気づきました。大規模なデータ収集システム(Large-scale Web Crawler)を構築するには、バラバラのライブラリを組み合わせるだけでは不十分です。真のフレームワークが必要なのです。Scrapyこそが、その状況を完全に変えてくれるソリューションでした。
なぜ大規模プロジェクトでBeautifulSoupは力尽きるのか?
根本的な問題は、requestsが同期(synchronous)メカニズムで動作することです。1つのリクエストを送信し、レスポンスを待ってから次へ進みます。50万ページあり、1ページあたり平均1秒かかるとすると、138時間連続で待機する必要があります。これはプロダクション環境では現実的ではありません。
また、エラー管理、自動リトライ、処理をブロックせずにデータベースへ保存する仕組みなどを自前で書くのは苦行です。Scrapyは、Twistedフレームワークに基づいた非同期(asynchronous)アーキテクチャにより、この問題を根本から解決します。レスポンスを待つ代わりに、Scrapyは数十のリクエストを同時に連続して送り出し、帯域幅を最大限に活用します。
クローラーの3つのアプローチ:あなたはどこにいますか?
データ収集の世界では、エンジニアは通常、次の3つの道のいずれかを選択します。
- Threading/Multiprocessingを使用する:
requestsと組み合わせる方法です。この方法は状態(state)の管理が非常に難しく、スレッド数を適切に制御しないと簡単にメモリが溢れてしまいます。 - Selenium/Playwrightを使用する: 本物のブラウザをシミュレートするため、非常に「豪華」な方法です。しかし、リソース消費が激しいため、API経由でデータを抽出できない複雑なJavaScriptを多用したサイトにのみ使用すべきです。
- Scrapyを使用する: パフォーマンスにおける「ラスボス」です。RAMの消費を安定させつつ、数百の並列リクエストを処理できます。
Scrapyの実装:ゼロから実践的なシステムへ
まず、Scrapyをインストールしましょう。プロジェクトをクリーンに保つために、常に仮想環境(virtualenv)を使用することをお勧めします。
pip install scrapy
1. 標準的なプロジェクト構造
コードを1つのファイルに詰め込んではいけません。Scrapyはモジュールごとに作業することを強制します。この構成は、プロジェクトが後で肥大化したときにあなたを救ってくれます。
# プロジェクトの作成
scrapy startproject my_crawler
cd my_crawler
# スパイダーの生成
scrapy genspider ecommerce spider_site.com
2. Itemの定義(データモデル)
乱雑な辞書(dictionary)を使う代わりに、items.pyでデータフィールドを明確に宣言しましょう。これはデータの設計図(Schema)としての役割を果たします。
import scrapy
class ProductItem(scrapy.Item):
name = scrapy.Field() # 商品名
price = scrapy.Field() # 価格
url = scrapy.Field() # URL
sku = scrapy.Field() # SKUコード
3. Spiderを書く – 制御を司る頭脳
ここは抽出ロジックを処理する場所です。ScrapyはBeautifulSoupのfind()よりも強力なCSSセレクタとXPathをサポートしています。以下は、データを取得し、自動的にページ遷移(ページネーション)を行う方法です。
import scrapy
from my_crawler.items import ProductItem
class EcommerceSpider(scrapy.Spider):
name = "ecommerce"
start_urls = ['https://example-shop.com/products']
def parse(self, response):
# 商品リストをループ処理
for product in response.css('div.product-item'):
item = ProductItem()
item['name'] = product.css('h2::text').get()
item['price'] = product.css('span.price::text').get()
item['url'] = response.urljoin(product.css('a::attr(href)').get())
yield item
# 次のページへのリンクを取得して再帰的に実行
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
大規模運用における秘訣
実際に運用する場合、デフォルト設定では不十分です。以下は、サーバーでの実戦経験から得た調整ポイントです。
Item Pipelines:データのクレンジング
収集したデータには、余計な空白や特殊文字などのゴミが含まれていることがよくあります。これをSpiderの中で処理してはいけません。pipelines.pyに任せることで、抽出コードを常に綺麗に保つことができます。
class CleanDataPipeline:
def process_item(self, item, spider):
if item['price']:
# "$1,200" を 1200.0 のような数値型に変換
item['price'] = float(item['price'].replace('$', '').replace(',', '').strip())
return item
Middlewares:ブロックを回避するテクニック
単一のIPから毎分1000リクエストを送るのは、アカウントを凍結される最短ルートです。settings.pyを開き、User-Agentのローテーションやプロキシサービスの設定を行いましょう。
# settings.py
DOWNLOAD_DELAY = 0.5 # 疑われないようにリクエスト間に待機時間を設定
CONCURRENT_REQUESTS = 32 # 並列リクエスト数
# 複数のブラウザを模倣するためにUser-Agentを自動で切り替える
DOWNLOADER_MIDDLEWARES = {
'scrapy.middlewares.useragent.UserAgentMiddleware': None,
'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
}
リソース管理の教訓:RAMを過負荷にしない
よくある間違いは、すべてのデータをメモリに溜め込んでからファイルに書き出すことです。100万行もあると、スクリプトは確実にクラッシュします。Scrapyには非常に賢いFeed Exports機能があります。
これにより、データを1行ずつJSONやCSVファイルに直接ストリーミングできます。
scrapy crawl ecommerce -o data.jsonl
ヒント:.jsonl (JSON Lines) 形式を使用しましょう。もしサーバーが突然停電しても、それまでに収集したデータは保存されており、従来のJSON形式のようにファイル全体が壊れることはありません。
最後に
Scrapyは単なるライブラリではなく、システム的な思考そのものです。それはあなたに、クリーンなコード、明確なモジュール化、そして常にパフォーマンスを優先することを強います。数サイトからデータを取るだけならBeautifulSoupで十分です。しかし、24時間365日稼働し、数百万件のレコードを処理する安定したシステムを目指すなら、Scrapyを選んでください。最初に学習時間を投資することは、午前2時にぐっすり眠るための最良の選択となるはずです。

