PR

Rust でスクレイピングを行う方法

1. はじめに

Rust でスクレイピングを行う方法を調べてみました。
スクレイピングは Webサイトのデータを取得することですがお行儀が良いとは言えないため実行する際はサイトの利用規約を守るようにしてください。

2. reqwest と scraper を使用

Rust でスクレイピングを行う際には、reqwest というライブラリを使用します。
何も指定しないと非同期通信になるため、blocking を指定して同期通信にします。
また、cookies を指定することでクッキーを使用することができます。
scraper のライブラリも使用します。
こちらを使用することで HTML 内のパースや要素を取得します。

reqwest = { version = "0.12.9", features = ["blocking", "cookies"] }
scraper = "0.22.0"

3. GET リクエスト

GET リクエストを行う際には、reqwest::blocking::get を使用します。
また、query を使用してクエリパラメータを指定することができます。

use reqwest::blocking::Client;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let res = client.get("https://httpbin.org/get")
        .query(&[("key1", "value1"), ("key2", "value2")])
        .send()?;
    println!("{:?}", res.status());
    Ok(())
}

4. POST リクエスト

POST リクエストを行う際には、reqwest::blocking::post を使用します。
また、body を使用してリクエストボディを指定することができます。

use reqwest::blocking::Client;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let res = client.post("https://httpbin.org/post")
        .body("key1=value1&key2=value2")
        .send()?;
    println!("{:?}", res.status());
    Ok(())
}

5. ヘッダーの指定

ヘッダーを指定する際には、reqwest::header を使用します。
また、reqwest::header::HeaderMap を使用してヘッダーを指定することができます。
最低限必要なヘッダーのみ指定しています、必要に応じて追加してください。
ACCEPT_ENCODING は gzip, deflate, br, zstd としていますが、response body が圧縮されるためテキストで取得する場合はコメントアウトすると良いです。
USER_AGENT は Firefox 133.0 としていますが、実際には最新のバージョンを指定してください。

use reqwest;
use reqwest::blocking::Client;

const HOST: &str = "httpbin.org";
const ACCEPT: &str = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
const ACCEPT_ENCODING: &str = "gzip, deflate, br, zstd";
const ACCEPT_LANGUAGE: &str = "ja,en-US;q=0.7,en;q=0.3";
const CONNECTION: &str = "keep-alive";
const CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0";

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(reqwest::header::HOST, HOST.parse().unwrap());
    headers.insert(reqwest::header::ACCEPT, ACCEPT.parse().unwrap());
    headers.insert(reqwest::header::ACCEPT_ENCODING, ACCEPT_ENCODING.parse().unwrap());
    headers.insert(reqwest::header::ACCEPT_LANGUAGE, ACCEPT_LANGUAGE.parse().unwrap());
    headers.insert(reqwest::header::CONNECTION, CONNECTION.parse().unwrap());
    headers.insert(reqwest::header::CONTENT_TYPE, CONTENT_TYPE.parse().unwrap());
    headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse().unwrap());

    let client = Client::builder()
        .default_headers(headers)
        .build()?;

    let res = client.get("https://httpbin.org/get")
        .send()?;
    println!("{:?}", res.status());
    Ok(())
}

6. クッキーの使用

クッキーを使用する際には、reqwest::cookie を使用します。
reqwest::cookie::Jar を使用して cookie_store を作成し、add_cookie_str を使用してクッキーを追加します。
add_cookie_str への追加の際には、クッキーの文字列と URL を指定します。
クッキーの文字列は、key=value の形式で指定します。
Client::builder に cookie_store(true) を指定してクッキーを使用することを指定します。
また、cookie_providerArc::clone(&cookie_store) を指定して使用するクッキーを指定します。

use reqwest::cookie;
use reqwest::blocking::Client;
use std::sync::Arc;

const URL: &str = "http://httpbin.org";

#[derive(Clone, Debug)]
struct Cookie {
    key: String,
    value: String,
}

impl Cookie {
    pub fn new(key: &str, value: &str) -> Self {
        Self {
            key: key.to_string(),
            value: value.to_string(),
        }
    }
    pub fn name(&self) -> &str {
        &self.key
    }
    pub fn value(&self) -> &str {
        &self.value
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = [
        Cookie::new("cookie1", "value1"),
        Cookie::new("cookie2", "value2"),
    ];

    let cookie_store = Arc::new(cookie::Jar::default());
    for cookie in cookies {
        let cookie_str = format!("{}={}", cookie.name(), cookie.value());
        println!("cookie_str: {:?}", cookie_str);
        cookie_store.add_cookie_str(&cookie_str, &URL.parse().unwrap());
        println!("cookie_store: {:?}", cookie_store);
    }

    let client = Client::builder()
        .cookie_store(true)
        .cookie_provider(Arc::clone(&cookie_store))
        .build()?;

    let res = client.get("https://httpbin.org/get")
        .send()?;
    println!("{:?}", res.status());
    Ok(())
}

7. パラメータの使用

パラメータを使い送信する際には、POST リクエストの body を使用してリクエストボディを指定します。
パラメータは、key=value の形式で指定します。Cookie と同じですが、パラメータは & で区切ります。
また、params_length でパラメータの長さを取得し、params_length - 1 で最後の & を削除します。
params_length は header::CONTENT_LANGUAGE を送る際に使用します。

use reqwest;
use reqwest::blocking::Client;

#[derive(Clone, Debug)]
struct Param {
    key: String,
    value: String,
}

impl Param {
    pub fn new(key: &str, value: &str) -> Self {
        Self {
            key: key.to_string(),
            value: value.to_string(),
        }
    }
    pub fn name(&self) -> &str {
        &self.key
    }
    pub fn value(&self) -> &str {
        &self.value
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let params = [
        Param::new("name", "John"),
        Param::new("age", "30"),
        Param::new("city", "New York"),
    ];

    let param_store = params
        .iter()
        .map(|data| (data.name(), data.value()))
        .collect::<Vec<(&str, &str)>>();
    let params_length = param_store.len();
    let mut params_str = String::new();
    if params_length > 0 {
        params_str = param_store.iter()
            .map(|(key, value)| format!("{}={}&", key, value))
            .collect();
        println!("{:?}", params_str);
        let params_length = params_str.len() - 1;
        println!("{:?}", params_length);
    }

    let client = Client::builder()
        .build()?;

    let res = client.post("https://httpbin.org/post").body(params_str).send()?;
    println!("{:?}", res.status());
    Ok(())
}

8. 本文をスクレイピング

Yahoo から h1 タグのテキストを取得する方法となります。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let res = client.get("https://yahoo.co.jp")
        .send()?;
    println!("{:?}", res.status());

    // HTML の本文を取得
    let body = res.text()?;
    // HTML の本文をパース
    let document = scraper::Html::parse_document(&body);
    // h1 タグを取得
    let selector = scraper::Selector::parse("h1").unwrap();
    // h1 タグのテキストを取得
    for element in document.select(&selector) {
        println!("{:?}", element.text().collect::<String>());
    }
    Ok(())
}

9. まとめ

Rust でスクレイピングを行う方法を調べてみました。
上記の組み合わせでスクレイピングを行うことができます。
情報はそれなりにありましたが割と苦労したので、参考になれば幸いです。

今回の記事も、Github Copilot が7割くらい書いてくれました。

A. 参考サイト

RustでAtCoderのログイン認証を通す
Reqwestを用いたRustのhttp通信方法メモ
Reqwest params with get request
reqwestで取得するデータのエンコードを指定する
Webサーバから謎のバイナリが返ってくる場合の対処法
Rustでお手軽スクレイピング 2024年夏

B. 参考書籍

コメント