Chapter 04

構造体とメソッドを作る

Shotaro Tsuji
Shotaro Tsuji
2020.12.08に更新

これまで書いてきたリンクを抽出するコードをmain関数の外に移します。ウェブページを取得してリンクを抽出する機能を部品として扱えるようにして、後で作る幅優先探索の機能から呼び出しやすいようにします。

src/lib.rs

リンクを抽出する機能を実装する構造体の名前をLinkExtractorとしましょう。この構造体はreqwest::blocking::Clientのオブジェクトを保持します。ClientはHTTPクライアントとしての設定等を保持する構造体です。reqwest::blocking::get関数は呼び出すたびにこのClientオブジェクトを作るので、効率のために一度作ったら使い回すようにします。

ClientClientBuilderを使って作成することでUser-Agentなどの設定が可能です。LinkExtractorが直接Clientを作るようにすると、LinkExtractorに設定を渡す必要が出てきて煩雑になるので、LinkExtractorにはmain関数が作ったClientオブジェクトを渡すようにします。

src/lib.rs
use reqwest::blocking::Client;

pub struct LinkExtractor {
    client: Client,
}

impl LinkExtractor {
    pub fn from_client(client: Client) -> Self {
        Self {
            client: client,
        }
    }
}

それではこれまでsrc/main.rsに書いたコードをLinkExtractorの実装に移しましょう。LinkExtractorの実装をsrc/lib.rsに書きます。LinkExtractorには&selfUrlを受け取ってResult<Vec<Url>, eyre::Report>を返す、get_linksという名前のメソッドを定義します。また、抽出したURLを保存するために、Vec<Url>型の変数linksを宣言します。そして、抽出したURLをlinkspushしていくようにコードを書き換えます。

src/lib.rs
    pub fn get_links(&self, url: Url) -> Result<Vec<Url>, eyre::Report> {
        let response = self.client.get(url)?;
        let base_url = response.url().clone();
        let body = response.text()?;
        let doc = Document::from(body.as_str());
        let mut links = Vec::new();

        for href in doc.find(Name("a")).filter_map(|a| a.attr("href")) {
            match Url::parse(href) {
                Ok(url) => { links.push(url); },
                Err(UrlParseError::RelativeUrlWithoutBase) => {
                    let url = base_url.join(href)?;
                    links.push(url);
                },
                Err(e) => { println!("Error: {}", e); },
            }
        }

        Ok(links)
    }

cargo buildを実行してプログラムをビルドしてみましょう。今のコードではエラーが出てコンパイルが通らないはずです。なぜなら、Client構造体のgetメソッドはResponseではなくRequestBuilderを返すからです。

RequestBuilder構造体は、ヘッダやボディを設定してHTTPリクエストを表現するRequest構造体を生成するビルダーです。今回は特に何も設定せずにsendメソッドを呼んでしまいましょう。

というわけで、

        let response = self.client.get(url)?;

となっている行を

        let response = self.client.get(url).send()?;

に書き換えましょう。これでコンパイルできるようになります。

src/main.rs

それでは、main関数に書いていたコードを消してしまって、今書いたLinkExtractorを使ってリンクを抽出するようにします。ClientBuilderClientを作成して、LinkExtractorに渡します。そして、get_linksメソッドにUrlオブジェクトを渡してURLのベクタを得ます。最後にベクタの中身を表示します。

src/main.rs
use reqwest::blocking::ClientBuilder;
use url::Url;
use mini_crawler::LinkExtractor;

fn main() -> eyre::Result<()> {
    let url = Url::parse("https://www.rust-lang.org")?;
    let client = ClientBuilder::new()
        .build()?;
    let extractor = LinkExtractor::from_client(client);

    let links = extractor.get_links(url)?;
    for link in links.iter() {
        println!("{}", link);
    }

    Ok(())
}

この章で作成したファイル

演習

  1. コマンドライン引数から取得したいページのURLを渡せるようにしましょう。コマンドライン引数でURLが指定されなかった場合は、https://www.rust-lang.org を読みにいくようにしましょう。