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

## はじめに

:::note warn
この記事はRustを用いたスクレイピングを推奨するものではありません。
:::
詳しくは [Webスクレイピングの注意事項一覧](https://qiita.com/nezuq/items/c5e827e1827e7cb29011) を参考にしていただければ幸いです。

## 本題

お手軽スクレイピング [^1] という内容が各言語で実装されているので、Rustで実装していきたいと思います。
Rustでのスクレイピング用途において、HTMLパーサーライブラリは [`scraper`](https://github.com/causal-agent/scraper), [`select.rs`](https://github.com/utkarshkukreti/select.rs) や ~~[`kuchiki`](https://github.com/kuchiki-rs/kuchiki)~~(現在アーカイブ化) がよく使われています。
HTTPクライアントではほぼ [`reqwest`](https://github.com/seanmonstar/reqwest) が使われています。
かなりの少数派ですが、[`ureq`](https://github.com/algesten/ureq), [`surf`](https://github.com/http-rs/surf) や [`isahc`](https://github.com/sagebind/isahc) を使う方もいらっしゃいます。

個人的にはreqwestとscraperを使うので、そちらで実装していきたいと思います。


今回はRust公式ブログ [The Rust Programming Language Blog](https://blog.rust-lang.org/) の記事一覧(2024/07/02時点)を投稿日降順で取得、表示したいと思います。

## 実行環境
- [Rust](https://www.rust-lang.org): 1.79.0 (129f3b996 2024-06-10)
- [scraper](https://github.com/causal-agent/scraper): 0.19.0
- [reqwest](https://github.com/seanmonstar/reqwest) (`features = ["blocking"]`): 0.12.5

## コード

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

```toml:Cargo.toml
[package]
name = "rust-blog-title-scraper"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
scraper = "0.19"
```

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

    // `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);

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

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

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

    // 一件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "Types Team Update and Roadmap");
    // 二件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "Announcing Rust 1.79.0");
    // 三件目の記事名
    // assert_eq!(elements.next().unwrap().text().next().unwrap(), "Faster linking times on nightly on Linux using `rust-lld`");
    // 最古の記事名
    // assert_eq!(elements.last().unwrap().text().next().unwrap(), "Road to Rust 1.0");

    Ok(())
}
```

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

```sh:stdout
$ cargo run
   Compiling rust-blog-title-scraper v0.1.0 (/Users/someone/Desktop/rust-blog-title-scraper)
    Finished dev [unoptimized + debuginfo] target(s) in 1.13s
     Running `target/debug/rust-blog-title-scraper`
Types Team Update and Roadmap
Announcing Rust 1.79.0
Faster linking times on nightly on Linux using `rust-lld`
Rust participates in OSPP 2024
Automatic checking of cfgs at compile-time
Announcing Rustup 1.27.1
Announcing Rust 1.78.0
Announcing Google Summer of Code 2024 selected projects
Security advisory for the standard library (CVE-2024-24576)
Changes to Rust's WASI targets
...
```

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

## まとめ

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

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

[^1]:
    - [Node.js でお手軽スクレイピング 2020 年夏](https://qiita.com/otchy/items/244c19c561ecb7211fa5)
    - [C# でお手軽スクレイピング 2020 年夏よりちょっと未来](https://qiita.com/okazuki/items/a4eb94f608bbc26c6a5f)