第3章終了(言語処理100本ノック2020)

Posted by johtani on Friday, September 4, 2020

目次

Rustで言語処理100本ノックの第3章です。

前回はこちら

少し間が空きましたが、再開しました。 間が空いた理由は。。。「正規表現」ですかね。。。 苦手なんです、正規表現。 なので、28はちょっとギブアップしてしまいました。

第3章の概要

個別に説明はせずに大きな流れのところだけ。 それぞれの問題の解についてはリポジトリを御覧ください(興味ある人いるのかなぁ?)

第3章はNDJSON(new line delimited JSON)という、 1行に1JSONという形式のデータを格納したファイルがgzipで圧縮された状態で提供されます。 まずは、このJSONファイルからJSONを読み込むのが主な処理になります。

読み込んだデータに「イギリス」のWikipediaの記事が入っているので、そこから正規表現で必要なデータを抽出します。

最後の問題が少し特殊で、抜き出した情報の「国旗」のファイル名を元に、MediaWikiのREST APIを叩いて、結果を取得し、その一部の情報を抜き出すというものです。

JSONの読み込み処理

gzipファイルを読み込んでから、1行ずつ抜き出してVecに入れる処理が次のようになります。 今回のgzipファイルは大した量が入っていないので、全部先に抜き出す処理としてまとめました。 もっと巨大なファイルの場合は個別のJSONに対する処理を buf.lines().map()のmapのなかで実行する形にすると思います。 gzipのファイルを開くのにflate2というクレート(ライブラリ)を利用しました。便利なのは、BufReaderlines()というメソッドがあるところですかね。

// https://docs.rs/flate2/1.0.14/flate2/read/struct.GzDecoder.html
pub fn extract_ndjson_from_gzip(input_file_name: &str) -> Vec<String> {
    let f = File::open(input_file_name).expect("file not found?");
    let gz = GzDecoder::new(f);
    let buf = BufReader::new(gz);
    let lines: Vec<String> = buf.lines().map(|l| l.unwrap()).collect();
    return lines;
}

こちらは、上記のメソッドで抜き出したVecを元に、記事の情報を抜き出す処理をしています。 JSONをパースして構造体Articleにデシリアライズするために、serdeというライブラリを使用しています。 serde自体は様々なデータ形式(JSON、YAMLなど)をパースするためのフレームワークです。今回はJSONなので、serde_jsonの実装を利用しています。 また、JSON文字列から構造体にデシリアライズするのを簡単にできるように構造体に#[derive(Deserialize)]をつけています。 あとは、let article: Article = serde_json::from_str(json.as_str())という処理を実行すればserde_jsonがJSONをパースして構造体に変換してくれます。形式がわかっているJSONの扱いはこれが楽ですね。変数に型を明記してあるので、型の推論もしてくれてるようです。

#[derive(Deserialize)]
pub struct Article {
    title: String,
    text: String,
}

// ch03-20. JSONデータの読み込み
// https://serde.rs/
pub fn load_json(input_file_name: &str, target_title: &str) -> Vec<Article> {
    let mut results = vec![];
    let ndjson = extract_ndjson_from_gzip(input_file_name);
    for json in ndjson {
        let article: Article = serde_json::from_str(json.as_str()).expect("json parse error");
        if article.title == target_title {
            results.push(article);
        }
    }
    return results;
}

後続の処理ではパースしたArticleから記事情報を取得して色々と処理をしています。

正規表現

正規表現用のクレートregexがRustに用意されています。Regex::new(正規表現)で、正規表現をコンパイルし、あとは、この構造体のメソッドを利用して文字列を処理していきます。 問題では、マッチするかどうか、マッチした一部の文字列を抜き出す、不要なタグを削除するといった処理を正規表現で行いました(Rust書くよりも正規表現の書き方とかを調べるのに大半の時間をもっていかれてます。。。)。


pub fn extract_category_lines(article: &Article) -> Vec<String> {
    let re = Regex::new(r"\[\[Category:(.*)\]\]").expect("syntax error in regex");
    let mut lines = vec![];
    article.lines_from_text().iter().for_each(|line| {
        if re.is_match(line) {
            lines.push(line.to_string());
        }
    });
    return lines;
}

MediaWiki APIリクエスト処理

最後の問題で国旗のファイル名を元にMediaWiki APIを叩いて、URLの文字列を取得しましょうという問題がありました。 ファイル名をREST APIの引数に渡してHTTP経由でリクエストを送信し、返ってくるJSONレスポンスからURLを抜き出すという処理です。

HTTPのリクエストの送受信にreqwestというクレートを利用しました。 ちょっと長いけど、APIコールしている箇所はこんな形です。

この関数にはasyncとついています。非同期処理の関数です。内部で2回ほど(リクエスト送信の結果待ちとレスポンスのパース待ち).awaitがあります。

client.get(URL)query(&[])といったメソッドが用意されているので、URLやクエリパラメータを用意してsend()でリクエスト送信します。


async fn call_api(file_name: &str) -> Result<String, String> {
    let client = reqwest::Client::new();
    let file_name2 = format!("File:{}", file_name);
    //let mut file_name2 = file_name.to_string().replace(" ", "_");
    let query = [
        ("action", "query"),
        ("format", "json"),
        ("prop", "imageinfo"),
        ("iiprop", "url"),
        ("titles", file_name2.as_str()),
    ];
    let result = client
        .get("https://en.wikipedia.org/w/api.php")
        .query(&query)
        .send()
        .await;

    match result {
        Ok(response) => match response.status() {
            StatusCode::OK => {
                let body = response.json::<MediaWikiResponse>().await;
                match body {
                    Ok(obj) => match obj.get_url() {
                        Some(url) => Ok(url),
                        None => Err(String::from("Cannot get url...")),
                    },
                    Err(error) => Err(error.to_string()),
                }
            }
            _ => Err(String::from(format!(
                "Status code is {}.",
                response.status()
            ))),
        },
        Err(error) => {
            let error_msg = format!("Error occurred... {:?}", error);
            println!("{}", error_msg.as_str());
            Err(error_msg)
        }
    }
}

あとは、呼び出し元で非同期の処理を実行するために、tokioというクレートを利用しています。 block_on()call_api()の実行をして、結果が返ってくるのを待ち受けています。結果が返ってきて、問題なければ、call_apiの戻り値Result<String, String>の左側のStringの値が取り出され、get_image_urlの戻り値となります。


fn get_image_url(file_name: &str) -> String {
    let mut _rt = tokio::runtime::Runtime::new().expect("Fail initializing runtime");
    let task = call_api(file_name);
    _rt.block_on(task).expect("Something wrong...")
}

一応、非同期に関して説明してみましたが、合っているのかどうか。。。 クレートの関係などはまだちょっと自身がないです。。。 あとは、エラーの処理の仕方とかももうちょっと勉強したいかな。

まとめ

一応、3章を終わらせました。だいぶ強引かつ1つスキップしましたが。。。 次は第4章の形態素解析です。


comments powered by Disqus

See Also by Hugo


Related by prelims-cli