RustでGitHub Activityをスクレイピングするライブラリを作ってる

RustでGitHub Activityをスクレイピングするライブラリを作ってる
TaKO8Ki
December 20, 2020

こんにちは、TaKO8Ki です。 これはRust Advent Calendar 2020 12/20の記事です。あまりRustならではなものではありませんが、個人的に欲しいものを作ってみました。

はじめに

⚠️ この記事はRustを用いたスクレイピングを積極的に推奨するものではありません。

作っているもの

wadachi というGitHub Activityをスクレイピングして取得するライブラリを作っています。crates.ioで公開はしてますが、ゴリゴリのβ版です。一応ちゃんと動きます。

img

https://crates.io/crates/wadachi

動機

最近OSSへのコントリビュートをよくするようになったのですが、始めて間も無い頃は何から始めたらいいか分からず、他の最強プログラマー達がOSSコントリビュートし始めた頃のGitHub Activityをよく見たりしてました。しかしながら、GitHub Activityには検索機能(特に特定のリポジトリに対するActivityの絞りこみなど)がなく、そういった用途には向いていません。

そこで、「ユーザー単位のGitHub Activityをよしなに検索・絞り込みできるCLIなりWEBサービスが欲しい」というのが動機で一旦これをライブラリにしてみることにしました。

ここで「公式のREST APIは?」と思った方もいるかもしれません。公式に用意されているREST APIの中にはActivityを取得できるエンドポイント もあります。 しかしながら、こいつでは300件までしかActivityを取得することができません。もう少し詳しくいうと、「過去90日以内のアクティビティの中で上限300件」しか取得できません。(下記引用文参考)

Events support pagination, however the per_page option is unsupported. The fixed page size is 30 items. Fetching up to ten pages is supported, for a total of 300 events. For information, see “Traversing with pagination.”

Only events created within the past 90 days will be included in timelines. Events older than 90 days will not be included (even if the total number of events in the timeline is less than 300).

https://docs.github.com/en/free-pro-team@latest/rest/reference/activity#events

Rustでスクレイピング

まず、対応するHTMLをひっこぬかないとダメなのでreqwestsurf を使用してGETリクエストします。今回はsurfを使用しました。

また、要素の走査には高機能なHTMLパーサーであるhtml5ever のラッパーライブラリである scraper を使用しました。(本当はラッパーライブラリから自作したかったんですが、時間がなかったので断念)

scraperの使い方

ざっくり下記のように使います。

use scraper::{Html, Selector};

let html = r#"
    <ul>
        <li>Foo</li>
        <li>Bar</li>
        <li>Baz</li>
    </ul>
"#;

let fragment = Html::parse_fragment(html);
let selector = Selector::parse("li").unwrap();

for element in fragment.select(&selector) {
    println!("{}", element.value().name());
}

要素の中でさらに走査をしたい場合はループの中でさらにループさせます。

さらに要素ごとにclassなどを取得したい場合は下記のようにattrという関数に取得したいattributeを渡してやればいいです。

for element in fragment.select(&selector) {
    println!("{}", element.value().attr("class").unwrap());
}

また、テキストを取得したい場合は下記のようにします。よくある余分なスペースを削除したい場合はこれにtrim()を使ってやればいいでしょう。

for element in fragment.select(&selector) {
    println!("{}", element.text().collect::<String>());
}

ライブラリの簡単な説明

GitHubのプロフィール画面下部にアクティビティの一覧がありますが、そこにあるshow more activitiesというボタンをクリックするとhttps://github.com/[username]?tab=overview&from=2020-10-01&to=2020-10-31のように一ヶ月単位でリクエストが飛ぶようになっています。(下記GIF参考)

これを使ってwadachiでは下記のように年月での絞り込みができるようにしました。

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let activities = wadachi::new("TaKO8KI").from(2019, 12).to(2020, 3).execute().await?;
    println!("{:?}", activities);
    Ok(())
}

今のところwadachiでは一ヶ月単位での絞り込みしか実装していませんが、 https://github.com/[username]?tab=overview&from=2020-10-10&to=2020-10-31(fromを月の途中にした)のようにさらに細かい絞り込みもできたので、これを利用すれば日単位での絞り込みも実装できそうです。

また、今のところ取得できるのは「行ったcommit」・「作ったpull request」・「レビューしたpull request」・「作ったrepository」だけです。今後Issueなどを追加していく予定です。

引き続き実装進めて、CLIとWEBサービスの両方で公開しようと思います!以上!