🤖

AtCoderで予定されたコンテストを返すAPIサーバーを作った

に公開

AtCoderコンテストの5分前にDiscordでメンションするBotがほしかったのですが、予定コンテストのAPIが生えていなくて不便に思ったので、定期的にスクレイピングしてJSONで返してくれるAPIサーバーを作りました。

https://github.com/1Step621/atcoder-upcoming-contests

https://atcoder-upcoming-contests-cs7x.shuttle.app/

Rustで作っていて、デプロイ先はShuttleにしています。サクッとAxumの雛形を作ってすんなりデプロイできたので体験がいいなと思いました。

実際のDiscord Botの実装はこのへんにあります。
https://github.com/1Step621/atcoder-bot-rs/blob/main/src/functions/periodic/check_upcomings.rs


案外書くことがなくて終わってしまったので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-upcomingIDがふられているテーブルの行一覧を取得してみました。

    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