AtCoderで予定されたコンテストを返すAPIサーバーを作った
AtCoderコンテストの5分前にDiscordでメンションするBotがほしかったのですが、予定コンテストのAPIが生えていなくて不便に思ったので、定期的にスクレイピングしてJSONで返してくれるAPIサーバーを作りました。
https://atcoder-upcoming-contests-cs7x.shuttle.app/
Rustで作っていて、デプロイ先はShuttleにしています。サクッとAxumの雛形を作ってすんなりデプロイできたので体験がいいなと思いました。
実際のDiscord Botの実装はこのへんにあります。
案外書くことがなくて終わってしまったのでscraper
を使ったスクレイピングの方法について解説します。
let res = reqwest::get("https://atcoder.jp/contests/")
.await
.unwrap()
.error_for_status()
.unwrap()
.text()
.await
.unwrap();
まずはreqwest
などの適当なHTTPクライアントを使ってほしいサイトのHTMLを文字列で取得します。
let document = Html::parse_document(&res);
これをscraper::Html::parse_document
でHTMLのツリーとして解析しています。
let selector = Selector::parse("#contest-table-upcoming tbody tr").unwrap();
let rows = document.select(&selector);
scraper::Selector::parse()
で文字列をCSSセレクタとしてパースし、さっき得たHTMLツリーの.select()
を呼んであげるとマッチするHTML要素の列がイテレータとして取得できます。
今回は#contest-table-upcoming
IDがふられているテーブルの行一覧を取得してみました。
let contests = rows
.map(|row| {
let parsed_as_text = row
.child_elements()
.map(|e| e.text().next().unwrap())
.collect::<Vec<_>>();
あとはここから色々読み取ってやるだけです。
今回はテーブルの行のHTML要素に対して.child_elements()
を呼ぶことで子要素のイテレータを取得し、さらにその要素それぞれが中に持っている文字列を取得しています。
要素の.text()
は子要素ツリーの中にある文字列ノードをすべてイテレータで返すようなので、.next().unwrap()
して最初の要素だけを得ています。
こんなところです。スクレイピングをするときは行儀よくサーバーに負荷をかけない程度にやりましょう。
Discussion