Scrapyに限界を感じ、Pythonが遅いとき:ElixirでCrawlyを使ってデータを収集する
産業規模でのウェブサイト解析とデータ収集において、ほぼすべての開発者が真っ先に考えるのはPythonとScrapyです。これは長年にわたって実証されてきた業界標準です。しかしPythonには特有の問題があります。数百ページを並行ダウンロードし、何千もの接続を維持しながらデータをリアルタイムでクリーニングする必要がある場合、スレッド、async、GILの制限にぶつかることになります。
関数型プログラミングの世界では、BEAM仮想マシンがそのようなタスクに最適です。ErlangとElixirのプロセスは隔離されており、数キロバイト程度の軽量さで、数百万個生成できます。Crawlyフレームワークは、Scrapyの最佳アーキテクチャコンセプト(スパイダー、ミドルウェア、処理パイプライン)を取り込み、Elixirにもたらします。その結果は、複雑なasyncランタイム調整なしで、大規模なネットワークリクエスト負荷を処理する準備ができたツールです。
内部構造と動作原理
Scrapyでパーサーを書いたことがある人なら、Crawlyのアーキテクチャは自然に感じるでしょう。すべての作業は3つの明確な部分に分かれています:スパイダー、ミドルウェア、パイプラインです。
スパイダーはエントリーポイントとページからのデータ抽出ロジックを記述します。
書籍カタログを閲覧し、タイトルと価格を収集してページネーションに従う典型的なスパイダーの例を示します:
defmodule BooksToScrape do
use Crawly.Spider
@impl Crawly.Spider
def base_url(), do: "https://books.toscrape.com/"
@impl Crawly.Spider
def init() do
[start_urls: ["https://books.toscrape.com/"]]
end
@impl Crawly.Spider
def parse_item(response) do
{:ok, document} = Floki.parse_document(response.body)
items =
document
|> Floki.find(".product_pod")
|> Enum.map(fn x ->
%{
title: Floki.find(x, "h3 a") |> Floki.attribute("title") |> Floki.text(),
price: Floki.find(x, ".product_price .price_color") |> Floki.text(),
url: response.request_url
}
end)
next_requests =
document
|> Floki.find(".next a")
|> Floki.attribute("href")
|> Enum.map(fn url ->
Crawly.Utils.build_absolute_url(url, response.request.url)
|> Crawly.Utils.request_from_url()
end)
%Crawly.ParsedItem{items: items, requests: next_requests}
end
end
コードはクリーンで宣言的です。HTML解析は、熟悉のCSSセレクタを持つFlokiライブラリを通じて行われます。parse_item関数からは、使用可能なデータ構造とスケジューラーへの新しいリクエストのバッチを返します。
パイプラインの設定とブロック対策
パーサーはHTTPクライアントとHTMLパーサーだけでは構成されません。URLの一意性の追跡、ドメインへの同時リクエスト数の制限、ヘッダーの変更、重複フィルタリング、保存前のスキーマ検証が必要です。
Crawlyでは、これらすべてが設定に移動されます:
import Config
config :crawly,
closespider_timeout: 10,
concurrent_requests_per_domain: 8,
closespider_itemcount: 100,
middlewares: [
Crawly.Middlewares.DomainFilter,
Crawly.Middlewares.UniqueRequest,
{Crawly.Middlewares.UserAgent, user_agents: ["Crawly Bot"]}
],
pipelines: [
{Crawly.Pipelines.Validate, fields: [:url, :title, :price]},
{Crawly.Pipelines.DuplicatesFilter, item_id: :title},
Crawly.Pipelines.JSONEncoder,
{Crawly.Pipelines.WriteToFile, extension: "jl", folder: "/tmp"}
]
ここで何が起きているのですか?リクエストはまずミドルウェアのチェーンを通過します:外国ドメインフィルタは、スパイダーが誤ってインターネット全体をスキャンすることを防ぎ、UniqueRequestは同じページへの繰り返しアクセスをカットします。スパイダーがデータを抽出すると、パイプラインに入ります。そこで必須フィールドがチェックされ、タイトルで重複がフィルタリングされ、有効な結果がJSON Linesにパックされてディスクに書き込まれます。
ジェネレーターはスタートアップの時間を節約します:mix crawly.gen.spiderコマンドは必要なすべてのコールバックを持つスパイダーテンプレートを作成し、mix crawly.gen.configはデフォルト設定を用意します。
組み込み管理パネル
興味深い詳細として、バージョン0.15.0から、プロジェクトには箱から出してすぐにWeb管理インターフェースが追加されました。localhost:4001でアクセスできます。
この管理パネルを通じて、以下のことができます:
- スパイダーの手動開始と停止、
- スケジュールされたリクエストのキュー表示、
- 収集したアイテムのダウンロードと実行ログの表示、
- リアルタイムでのクローラー状態の追跡。

既存のPhoenixまたはPlug WebアプリケーションにCrawlyを埋め込む場合、管理パネルを別のポートに保持する必要はありません。共通ルーターを通じて単にforward "/admin", Crawly.API.Routerでルート指定できます。
動的ページのレンダリングとElixirなしでの実行
現代のWebはJavaScriptで溢れています。コンテンツがAJAX経由で非同期にロードされるか、アプリケーションがReactで構築されている場合、通常のHTTP GETでは空のページスケルトンが返されます。CrawlyはChromeやSplashなどの外部レンダラーと連携できます。ヘッドレスブラウザを設定すると、フレームワークは実行されたすべてのスクリプトを含むすでにレンダリングされたDOMをフェッチします。
もう一つの注目すべき機能は、スタンドアロンモードです。チームにElixirを書く人がいない場合、クローラーだけを 위해完全なコードベースを立ち上げる必要はありません。CrawlyはシンプルなYAMLファイルでページスクレイピングルールを記述できるミニマリストなDockerコンテナで実行できます。これは他のスタックの開発者にも開かれたElixirツールの珍しい例です。
実践的なシナリオ
Crawlyが最も輝く場面:
- オンラインストアでの価格監視。何千もの商品カードを定期的にクロールし、在庫変動や割引をチェックする必要がある場合。
- ML用のトレーニングデータセットの収集。jsonl形式での保存を伴う記事、レビュー、フォーラムのパース。
- リストアグリゲーター。数十の地域掲示板からの不動産や自動車のオファーの収集。
- 歴史的コンテンツのアーカイブ。リンク切れフィルタリングを伴うブログやドキュメントの高速ダウンロード。
このプロジェクトが役立つ人
主な言語がElixirまたはErlangの場合、CrawlyはWebスクレイピングにおいて明確に最佳の選択です。メインサービス alongside でPythonに不格好な回避策を構築し、キューを通じてサービス間通信を設定する必要はありません。
Pythonで書いていて、大量データでのScrapyパフォーマンスとの戦いに疲れているなら、Crawlyは確かに見る価値があります。参入障壁は低いです:コンセプトはほとんど1対1で対応し、Pythonの後ではElixir構文は非常に読みやすいです。HexDocsの公式ドキュメントとテストサイト books.toscrape.com でのクイックチュートリアルから始められます。
関連プロジェクト