Rustによるウェブスクレイピング:reqwestとscraperを使った実践ガイド
Expert in Web Scraping Technologies
TL;DR:
- 2026年のRustによるウェブスクレイピングは、HTTPリクエストに
reqwest、CSSセレクタ解析にscraperの2つのクレートが使用され、非同期ランタイムはtokioが駆動します。 - コンパイラはすべての失敗パスを処理することを強制するため、ビルドが成功するRustスクレイパーは、通常、構文が不正なマークアップや非200レスポンスに耐えるスクレイパーです。
tokio::spawnは、ページごとのクローラーを約5行で同時実行可能に変換し、このガイドでは5ページを通じて50件のデータを取得します。reqwestもscraperもJavaScriptを実行しないため、クライアント生成のページは、解析結果がゼロ件のマークアップを返します。- Scrapeless Universal Scraping APIは最初にページをレンダリングし、同じ変わっていない
scraperコードが10件を解析するHTMLを返します。 - Scrapelessの無料プランから始めて、自分のターゲットに対してピボット例を実行してください。
Rustがスクレイピング作業に登場するのは特定の理由がある:クローラーは通常、誰もが制御できないマークアップに対して数時間無人で実行されるデータパイプラインの一部です。ガーベジコレクタを持たないバイナリは、すべての境界で明示的なResult処理をし、ポーズすることがないため、その仕事に適しています。
このガイドは、現在のクレートリリースを使用して動作するスクレイパーを構築し、それを実行し、純粋なRustスタックがどこで停止するかを、警告ではなく測定された数値で示します。
必要なもの
Rustによるウェブスクレイピングには、3つのクレートと安定したツールチェーンが必要です。以下のバージョンは執筆時点のRustコミュニティのクレートレジストリで現在発表されているリリースであり、ここに示すすべての例はまさにこれらに対してコンパイルおよび実行されました:
| クレート | バージョン | 役割 |
|---|---|---|
reqwest |
0.13.4 | HTTPクライアント |
scraper |
0.27.0 | HTML解析とCSSセレクタ |
tokio |
1.53.0 | 非同期ランタイム |
serde_json |
1 | APIレスポンスのJSON処理 |
scraperクレートは、Servoで使用されているのと同じ解析エンジンhtml5everをラップしているため、マークアップをテキストとして正規表現の対象とするのではなく、HTML解析仕様に従います。これは、閉じられていないタグが通常である実際のページでは重要です。
インストール
プロジェクトを作成し、依存関係を追加します:
bash
cargo new rust-scraper
cd rust-scraper
次に、Cargo.tomlに依存関係ブロックを宣言します:
toml
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.13.4", features = ["json"] }
scraper = "0.27.0"
tokio = { version = "1.53.0", features = ["macros", "rt-multi-thread"] }
serde_json = "1"
reqwestのjson機能は、リクエストとレスポンスのシリアライズを取り込みます。tokioのmacrosおよびrt-multi-thread機能が、#[tokio::main]属性を機能させる要因です。
ページの取得と解析
完全なRustスクレイパーは、その評価よりも短いです。これは、サーバーでレンダリングされたページを取得し、すべての引用コンテナを選択し、各々から2つのフィールドを取得します:
rust
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let body = reqwest::get("https://quotes.toscrape.com/")
.await?
.error_for_status()?
.text()
.await?;
let document = Html::parse_document(&body);
let quote_sel = Selector::parse("div.quote").unwrap();
let text_sel = Selector::parse("span.text").unwrap();
let author_sel = Selector::parse("small.author").unwrap();
let mut count = 0;
for quote in document.select("e_sel) {
let text = quote.select(&text_sel).next().map(|e| e.inner_html());
let author = quote.select(&author_sel).next().map(|e| e.inner_html());
if let (Some(t), Some(a)) = (text, author) {
if count == 0 {
println!("最初の引用: {} — {}", t, a);
}
count += 1;
}
}
println!("解析された引用数: {}", count);
Ok(())
}
これを実行すると、次のように表示されます:
text
最初の引用: “我々が創造した世界は、我々の思考のプロセスです。それは我々の思考を変えずには変わることはできません。” — アルバート・アインシュタイン
解析された引用数: 10
そのコードの中の三つの詳細が、ほとんどの重みを持っています。
error_for_status()は、4xxまたは5xxレスポンスをErrに変換し、エラーページをデータであるかのように解析できなくします。これがなければ、403のボディはゼロのセレクタ一致に変わり、空の結果セットと同じに見えます。これらのステータスクラスの区別は、HTTP意味仕様で定義されています。
Selector::parse は選択子文字列がマッチ時に解釈されるのではなく、コンパイルされるため、失敗する可能性があります。ループの外で選択子を一度コンパイルすること — 各要素ごとにではなく — が、ネストされた select 呼び出しが安価である理由です。この構文は W3C セレクター レベル 4 仕様 に従っています。
main から外に伝播する ? 演算子は、Box<dyn std::error::Error> を実用的な戻り値型に変えます。すべての失敗する可能性のある呼び出しは同じ場所まで戻り、処理は失敗時に非ゼロで終了します — これはスクレイパーがスケジューラーから実行されるときに便利です。
ページを並行してスクレイピングする
Rust の並行性のストーリーは、ここでの使用の主な理由であり、tokio::spawn でそれが現れます。各ページは独立したタスクとなり、すべてが1つの接続プールクライアントを共有し、結果は順番に収集されます:
rust
use scraper::{Html, Selector};
async fn page_quote_count(client: &reqwest::Client, page: u32) -> Result<usize, reqwest::Error> {
let url = format!("https://quotes.toscrape.com/page/{page}/");
let body = client.get(&url).send().await?.error_for_status()?.text().await?;
let quote_sel = Selector::parse("div.quote").unwrap();
Ok(Html::parse_document(&body).select("e_sel).count())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let tasks: Vec<_> = (1..=5)
.map(|page| {
let client = client.clone();
tokio::spawn(async move { (page, page_quote_count(&client, page).await) })
})
.collect();
let mut total = 0;
for task in tasks {
let (page, result) = task.await?;
let count = result?;
println!("page {page}: {count} quotes");
total += count;
}
println!("total quotes across 5 pages: {total}");
Ok(())
}
実行結果:
text
page 1: 10 quotes
page 2: 10 quotes
page 3: 10 quotes
page 4: 10 quotes
page 5: 10 quotes
total quotes across 5 pages: 50
reqwest::Client は基盤となる接続プールを共有するため複製が安価です。タスクごとに新しいクライアントを作成すると、毎回新しいプールを開設し、その利点を失います。ページ範囲を制限し、ターゲットが快適に提供できるサイズに保ってください — 並行性は既知のワークロードを完了させるためのツールであり、ランタイムが許すだけの同時リクエストを発行するためのものではありません。
サーバーサイドでレンダリングされたターゲットに向ける準備はできましたか? 無料の Scrapeless アカウントを作成し、既に持っている解析コードを維持してください。
reqwest と scraper の限界
どちらのクレートも JavaScript を実行しません。reqwest はサーバーが送信したバイトを返し、scraper は正確にそのバイトを解析します — したがって、ブラウザでコンテンツを構築するページでは、セレクターは何も一致しません。
これは理論的ではなく、測定可能です。上記で使用されたサイトは、同じデータのクライアントレンダリングされたツインを /js/ で公開しています。それに同一の解析ロジックを向けると:
rust
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let body = reqwest::get("https://quotes.toscrape.com/js/")
.await?
.error_for_status()?
.text()
.await?;
let document = Html::parse_document(&body);
let quote_sel = Selector::parse("div.quote").unwrap();
let count = document.select("e_sel).count();
println!("html bytes: {}", body.len());
println!("quotes parsed: {}", count);
Ok(())
}
text
html bytes: 5808
quotes parsed: 0
リクエストは成功しました。ステータスは200でした。パーサーは5,808バイトの有効なHTMLに対して正しく機能しましたが、その中にはクオート要素が含まれていません — それらはスクリプトが実行された後にDOMに書き込まれます。HTTPの失敗のみをチェックするスクレイパーは、これは空のページとして扱いますが、機能が欠如しているためであり、これが以前の error_for_status() 呼び出しが必要でありながら十分ではない理由です。
Universal Scraping API でページをレンダリングする
Scrapeless Universal Scraping API は、クラウドブラウザでページをレンダリングし、結果のHTMLを返すことによって、そのギャップを埋めます。これにより、Rust 側は平凡なHTTP呼び出しのままです。js_render を true に設定すると、レスポンスボディにはスクリプト後のDOMが含まれます。
環境からキーで認証します:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
次に、取得レイヤーだけを交換します — 下のセレクターコードは最初の例と同じです:
rust
use scraper::{Html, Selector};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("SCRAPELESS_API_KEY")?;
let payload = json!({
text
最初の引用: 「私たちが作り上げた世界は、私たちの考え方のプロセスです。それは私たちの考え方を変えずには変わりません。」 — アルベルト・アインシュタイン
HTMLバイト数: 8985
解析した引用数: 10
同じページ、同じセレクター、同じクレートのバージョン。唯一の変更は、どのレイヤーがHTMLを取得するかで、レコード数は0から10に増加し、ペイロードは5,808バイトから8,985バイトに増加します — 違いは、スクリプトがDOMに書き込んだ引用のマークアップです。
応答は、data に文字列としてレンダリングされたドキュメントを含むJSONエンベロープとして到着します。このため、例は最初に serde_json::Value に解析され、data を Html::parse_document に渡します。レンダリングオプションの詳細は Scrapeless のドキュメント にあり、同じ js_render の動作は JSレンダリングガイド でより詳しく説明されています。
トラブルシューティング
セレクターがブラウザで見ることができるページで何も一致しない。 上記のJS例が示すように、応答の長さを最初に印刷します。マッチのない数千バイトは、通常、コンテンツがクライアントでレンダリングされていることを意味し、セレクターが間違っているというわけではありません。インスペクタではなくページソースを表示します — インスペクタはスクリプトが実行された後のDOMを表示しますが、それは reqwest が受け取ったものではありません。
Selector::parse が起動時にパニックを引き起こす。 セレクター文字列が無効なCSSです。擬似要素や一部のjQueryスタイルの拡張は仕様の一部ではなく、コンパイルされません。
ビルドがTLSバックエンドで失敗する。 reqwest はTLSスタックをコンパイルします。最小限のコンテナでは、システムにはCツールチェーンとCMakeが必要であるか、または機能フラグを通じて純粋なRustの rustls バックエンドに切り替えることができます。
inner_html() がテキストの代わりにマークアップを返す。 このメソッドは、要素内のすべてを返します。タグを含みます。フィールドがネストされた要素を含む可能性がある場合は、text() を使用し、断片を収集します。
これらのいずれかをライブターゲットに向ける前に、サイトの利用規約とその /robots.txt 指示を確認してください。これはロボット排除プロトコル標準に従います。収集を公共データに限定し、ターゲットが快適に提供できる量に保ちます。
結論
Rustは、失敗を明示的に処理し、ほとんど儀式なしで並列化するスクレイパーを提供します — 輸送には reqwest、セレクターには scraper、同時実行には tokio、そして Result を無視させないコンパイラ。これらのスタックは、サーバーがレンダリングしたページを完全にカバーします。
- それが行わないのはJavaScriptを実行することであり、そのギャップのコストはエラーではなく静かなゼロです。これを測定するのは有用な習慣です:サーバーでレンダリングされたページで10レコード、クライアントでレンダリングされたツインで0、Rustが解析する前に何かがページをレンダリングした後、再び10です。
Scrapelessの無料プランを始める で、レンダーステップを自分のターゲットに対して実行し、ジョブの規模を決定するときに現在の Scrapelessの料金 を確認してください。
よくある質問
Q: Webスクレイピングに最適なRustクレートはどれですか?
`reqwest`と`scraper`を組み合わせることで、ほとんどの作業をカバーできます。`reqwest`は非同期ファーストのAPIでHTTPを処理し、`scraper`は`html5ever`パースツリー上でCSSセレクタによるクエリを提供します。ページがクライアント側でコンテンツを構築する場合にのみ、ヘッドレスブラウザクレートやレンダリングAPIに手を伸ばしてください。
**Q: RustはPythonと比較してウェブスクレイピングに適していますか?**
Rustは、スクレイパーが長時間実行される場合、同時に実行される場合、または unattended(無人で)実行される場合に適しています。なぜなら、ガーベジコレクタのポーズがなく、コンパイラがすべてのエラーパスを処理することを強制するからです。Pythonは、ワンオフの抽出に関してエコシステムの広さや反復速度でまだ優れています。パーシングの概念は、両者間で直接移行できます。
**Q: reqwestはJavaScriptを実行できますか?**
いいえ。`reqwest`はHTTPクライアントであり、サーバーが送信したバイトを返します。クライアントでレンダリングされたページでは、それは内容が含まれていない有効なHTMLを意味します — 上記の例は5,808バイトを解析して0レコードを返します。レンダリングはヘッドレスブラウザまたはレンダリングされたDOMを返すAPIのいずれかで行う必要があります。
**Q: シンプルなスクレイパーにasyncとtokioは必要ですか?**
厳密には必要ありません — `reqwest`はフィーチャーフラグの後ろにブロッキングクライアントを提供しており、シングルの逐次リクエストには問題ありません。1ページ以上を取得すると、非同期クライアントは`tokio`の依存関係の価値があり、そこで`tokio::spawn`が逐次取得を同時取得に変えます。
**Q: サイトが変更されたときにRustスクレイパーが壊れないようにするにはどうすればよいですか?**
構造に対して主張し、信頼しないでください。フィールドを抽出する前に、コンテナセレクタが妥当な数の要素にマッチしたことを確認し、突然のゼロを空の結果としてではなく失敗として扱ってください。起動時にセレクタを一度コンパイルすると、不正なCSSもすぐに明らかになります。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



