Rustでお手軽スクレイピング 2020年夏


はじめに

この記事はRustを用いたスクレイピングを推奨するものではありません。
詳しくは Webスクレイピングの注意事項一覧 を参考にしていただければ幸いです。

本題

お手軽スクレイピング 1 という内容が各言語で実装されているので、Rustで実装していきたいと思います。
Rustでのスクレイピング用途において、HTMLパーサーライブラリは scraperselect.rskuchiki がよく使われています。
HTTPクライアントではほぼ reqwest が使われています。
かなりの少数派ですが、ureq, surfisahc を使う方もいらっしゃいます。

個人的にはreqwestとscraperを使うので、そちらで実装していきたいと思います。
(ちなみにscraperは更新が滞っていて、メンテナーを募集しているそうです。Looking for maintainer(s) · Issue #36 · causal-agent/scraper)

今回はRust言語の公式ブログ The Rust Programming Language Blog の記事一覧(2021/02/13時点)を降順で取得、表示したいと思います。

実行環境

  • Rust: 1.50.0 (cb75ad5db 2021-02-10)
  • scraper: 0.12.0
  • reqwest (features = ["blocking"]): 0.11.0

コード

ライブラリ名を明示的にするために今回はインポートしていませんが、scraper では use scraper::{Selector, Html}; がよく使われています。
HTTP通信ライブラリ reqwest は実装を簡素にするため、同期版を使用しています。

main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {

    // セレクターをパース (このセレクターは記事のアンカーノード群(タイトル)を指す。 <a href="link">Title</a>)
    let selector = scraper::Selector::parse("td.bn > a").unwrap();

    // `https://blog.rust-lang.org/` へHTTPリクエスト
    let body = reqwest::blocking::get("https://blog.rust-lang.org/")?.text()?;

    // HTMLをパース
    let document = scraper::Html::parse_document(&body);

    // セレクターを用いて要素を取得
    let elements = document.select(&selector);

    // 全記事名を出力
    elements.for_each(|e| println!("{}", e.text().next().unwrap()));

    // 一件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "Announcing Rust 1.50.0");
    // 二件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "mdBook security advisory");
    // 三件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "Announcing Rust 1.49.0");
    // 最古の記事名
    // assert_eq!(elements.last().unwrap().text().next().unwrap(), "Road to Rust 1.0");

    Ok(())
}

出力結果は全記事数が多いので11件目以降は省略しています。

stdout
$ cargo run
   Compiling rust-blog-title-scraper v0.1.1 (/Users/someone/Desktop/rust-blog-title-scraper)
    Finished dev [unoptimized + debuginfo] target(s) in 11.77s
     Running `target/debug/rust-blog-title-scraper`
Announcing Rust 1.50.0
mdBook security advisory
Announcing Rust 1.49.0
Rust Survey 2020 Results
Next steps for the Foundation Conversation
Launching the Lock Poisoning Survey
The Foundation Conversation
Announcing Rustup 1.23.0
Announcing Rust 1.48.0
Marking issues as regressions
...

手順はコメントをご参照ください。
クセのあるポイントはセレクターを事前にパースしなければいけないことと、 e.text() の箇所で返ってくる値が単純なテキストではなく、イテレーターを実装したテキストノード群(型)であることです。(他にもクセがあります。)

まとめ

Python, Node.js, Rubyなどのスクレイピングを得意とする言語よりはそこそこに実装疲労度が高いです。 (逆に言うと細かい粒度で様々なクレートを組み合わせることが出来ますので、柔軟度が高いです。)
ただ、今回の様な簡単なスクレイピングであれば簡単に出来ます。
タイトルにお手軽スクレイピングとありますがRustの場合、難しいことをしようとすればする程ドキュメントの熟読が必要になるかと思われますので、うまく判断してご利用ください。

知識や記述の間違いがあればご指摘いただけれると嬉しいです。