この章ではLinkExtractor
とCrawler
を結合し、コマンドライン引数で探索を始めるURLと訪問ページ数の上限を与えられるプログラムを作成します。そのために、LinkExtractor
にAdjacentNodes
トレイトを実装します。そして、コマンドライン引数をstructopt
クレートでパースして得たパラメタをCrawler
に渡して探索を実行するコードを書きます。
LinkExtractor
によるAdjacentNodes
トレイトの実装
LinkExtractor
にAdjacentNodes
トレイトを実装します。adjacent_nodes
関数の中でget_links
関数を呼び出すようにします。
impl crawler::AdjacentNodes for LinkExtractor {
type Node = Url;
fn adjacent_nodes(&self, v: &Self::Node) -> Vec<Self::Node> {
self.get_links(v.clone())
.unwrap()
}
}
おっと、ここで問題が発生しました。get_links
の戻り値はResult
型ですがadjacent_nodes
の戻り値はそうではありません。前の章で幅優先探索を実装するときにこのことをすっかり忘れていました。仕方がないのでひとまずunwrap
を呼んでお茶を濁すことにしましょう。
また、get_link
はUrl
型のオブジェクトを受け取ります。adjacent_nodes
は参照を受け取るのでclone
を呼んでコピーを渡すようにします。
LinkExtractor
とCrawler
の結合
それではmain
関数を書き換えましょう。LinkExtractor
のオブジェクトを作るところまではこれまでと変わりません。LinkExtractor
のオブジェクトを作ったら、それへの参照とスタート地点となるURLをCrawler
のコンストラクタに渡します。そして、for
の中でCrawler
のオブジェクトを使います。とりあえず10件のページを取得するためにtake(10)
を呼び出しておきます。また、ループの中でstd::thread::sleep
を呼び出します。とりあえず1件ごとに100ミリ秒スリープすることにしましょう。
use reqwest::blocking::ClientBuilder;
use url::Url;
use mini_crawler::LinkExtractor;
use mini_crawler::crawler::Crawler;
use std::time::Duration;
fn main() -> eyre::Result<()> {
env_logger::init();
let url = std::env::args()
.nth(1)
.unwrap_or("https://www.rust-lang.org".to_owned());
let url = Url::parse(&url)?;
let client = ClientBuilder::new()
.build()?;
let extractor = LinkExtractor::from_client(client);
let crawler = Crawler::new(&extractor, url);
let wait = Duration::from_millis(100);
for url in crawler.take(10) {
println!("{}", url);
std::thread::sleep(wait.clone());
}
Ok(())
}
さて、cargo run
を実行してmini-crawler
を動かしてみましょう。訪問したURLが10件表示されると思います。しかし、スリープさせた時間である100ミリ秒より長い間隔で1件ずつ表示されるでしょう。これはHTMLの解析に時間がかかるのと、それぞれのページを実際に読み込んでリンクの抽出までやっているからです。
この動作を早くしたければリンクの抽出を後回しにするようにCrawler
を作り替えることができるでしょう。しかし、そうすると各ページが持つリンクも取り出したくなったときに、かなり面倒なことになるでしょうからそのような修正はしないことにします。
CLIの作成
さて、コマンドライン引数から探索を始めるページのURLと訪問するページ数の上限を渡せるようにしましょう。
コマンドライン引数をパースするためにstructopt
を使います。Cargo.toml
に以下の行を加えましょう。
structopt = "0.3"
structopt
は手続きマクロによってコマンドライン引数を構造体に自動で変換してくれる非常に便利なクレートです。さっそく、オプションを格納するstruct Opt
を定義して、derive
アトリビュートを付けてstructopt
を使うことを宣言しましょう。
use structopt::StructOpt;
#[derive(StructOpt)]
struct Opt {
}
必要なオプションは、
- 探索を始めるページのURL
- 上限ページ数
の二つでした。それぞれ、Url
構造体、usize
型で持つことにしましょう。
#[derive(StructOpt)]
struct Opt {
maximum_pages: usize,
start_page: Url,
}
structopt
への指示は構造体の各フィールドに属性を付けることで行います。上限ページ数は-n
オプションで指定したいのでmaximum_pages
に#[structopt(short="n")]
を付けます。
#[derive(StructOpt)]
struct Opt {
#[structopt(short="n")]
maximum_pages: usize,
start_page: Url,
}
属性を何も付けなければ、位置引数として扱われオプションを付けずに指定できる引数となります。また、文字列からフィールドの型への変換は、FromStr
トレイトが実装されていれば自動で行われます。
コマンドライン引数はstruct Opt
に自動で実装されるfrom_args
メソッドを呼び出すことでパースできます。
let opt = Opt::from_args();
さて、コマンドライン引数を使うようにmain
関数を書き換えましょう。
fn main() -> eyre::Result<()> {
env_logger::init();
let opt = Opt::from_args();
let client = ClientBuilder::new()
.build()?;
let extractor = LinkExtractor::from_client(client);
let crawler = Crawler::new(&extractor, opt.start_page);
let wait = Duration::from_millis(100);
for url in crawler.take(opt.maximum_pages) {
println!("{}", url);
std::thread::sleep(wait.clone());
}
Ok(())
}
cargo run
を実行すると以下のメッセージが表示されます。
error: The following required arguments were not provided:
<start-page>
-n <maximum-pages>
USAGE:
mini-crawler <start-page> -n <maximum-pages>
For more information try --help
cargo run -- --help
を実行してみましょう。
mini-crawler 0.1.0
USAGE:
mini-crawler <start-page> -n <maximum-pages>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-n <maximum-pages>
ARGS:
<start-page>
ヘルプメッセージが表示されていますが説明がついていません。ドキュメントコメントを書くことで引数の説明が生成されるようになります。
/// A toy web crawler
#[derive(StructOpt)]
struct Opt {
/// Maximum number of pages to be crawled
#[structopt(short="n")]
maximum_pages: usize,
/// URL where this program starts crawling
start_page: Url,
}
ドキュメントコメントを書いてからcargo run -- --help
を実行するとメッセージが次のように変化します。これで引数の説明が表示されるようになりました。
mini-crawler 0.1.0
A toy web crawler
USAGE:
mini-crawler <start-page> -n <maximum-pages>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-n <maximum-pages> Maximum number of pages to be crawled
ARGS:
<start-page> URL where this program starts crawling
それではmini-crawler
を実行してみましょう。
% RUST_LOG=info cargo run -- -n 100 https://docs.rs/reqwest/0.10.9/reqwest/
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/mini-crawler -n 100 'https://docs.rs/reqwest/0.10.9/reqwest/'`
[2020-12-04T07:23:18Z INFO mini_crawler] GET "https://docs.rs/reqwest/0.10.9/reqwest/"
[2020-12-04T07:23:19Z INFO mini_crawler] Retrieved 200 OK "https://docs.rs/reqwest/0.10.9/reqwest/"
https://docs.rs/reqwest/0.10.9/reqwest/
[2020-12-04T07:23:19Z INFO mini_crawler] GET "https://docs.rs/"
[2020-12-04T07:23:19Z INFO mini_crawler] Retrieved 200 OK "https://docs.rs/"
https://docs.rs/
[2020-12-04T07:23:20Z INFO mini_crawler] GET "https://docs.rs/about"
[2020-12-04T07:23:20Z INFO mini_crawler] Retrieved 200 OK "https://docs.rs/about"
https://docs.rs/about
[2020-12-04T07:23:20Z INFO mini_crawler] GET "https://docs.rs/about/badges"
[2020-12-04T07:23:20Z INFO mini_crawler] Retrieved 200 OK "https://docs.rs/about/badges"
https://docs.rs/about/badges
[2020-12-04T07:23:20Z INFO mini_crawler] GET "https://docs.rs/about/builds"
[2020-12-04T07:23:20Z INFO mini_crawler] Retrieved 200 OK "https://docs.rs/about/builds"
https://docs.rs/about/builds
-- snip --
[2020-12-04T07:23:25Z INFO mini_crawler] GET "https://rust-lang-nursery.github.io/rust-cookbook/"
[2020-12-04T07:23:25Z INFO mini_crawler] Retrieved 200 OK "https://rust-lang-nursery.github.io/rust-cookbook/"
https://rust-lang-nursery.github.io/rust-cookbook/
[2020-12-04T07:23:26Z INFO mini_crawler] GET "https://crates.io/"
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ServerError(reqwest::Error { kind: Status(403), url: Url { scheme: "https", host: Some(Domain("crates.io")), port: None, path: "/", query: None, fragment: None } })', src/lib.rs:79:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
おっと、unwrap
の呼び出しでパニックしてしまいましたね。このunwrap
はLinkExtractor
にAdjacentNodes
を実装する際に書いたものです。最後にこの部分の処理を修正してmini-crawler
を完成させましょう。
最後の修正
今回はリンクの抽出でエラーが起きた場合は、エラーを無視して空のVec
を返してしまうようにします。ただし、発生したエラーはログに残すようにしておきます。エラーログはエラーの発生原因も表示するようにしました。
impl crawler::AdjacentNodes for LinkExtractor {
type Node = Url;
fn adjacent_nodes(&self, v: &Self::Node) -> Vec<Self::Node> {
match self.get_links(v.clone()) {
Ok(links) => links,
Err(e) => {
use std::error::Error;
log::warn!("Error occurred: {}", e);
let mut e = e.source();
loop {
if let Some(err) = e {
log::warn!("Error source: {}", err);
e = err.source();
} else {
break;
}
}
vec![]
},
}
}
}
これで
% RUST_LOG=warn cargo run --release -- -n 200 https://docs.rs/reqwest/0.10.9/reqwest/
を実行すれば訪問したURLが表示されることでしょう。
以上の例ではreqwest
のドキュメントのURLをmini-crawler
に与えていましたが、cargo doc
が生成するドキュメントは意外にたくさんのリンクを含んでいるので、例えばmdbook
のマニュアルはそれほど大きくないので、こちらの方が例としてよかったかもしれません。
% cargo run --release -- -n 100 https://rust-lang.github.io/mdBook/format/summary.html
以上でmini-crawler
は完成です。