Rust★csvからjsonに変換するWeb API

Rustで何かやりたいと思いでも何をやっていいのか模索した結果、WebAPIを作ることにしました。
自分の学習のためにやっていますが至らない点は多々あると思います。大目に見てください。

動機

新型コロナウイルス(covid-19)のまとめサイトが各都道府県で作られているのを知りそれらのサイトを何となく眺めていました。サイトの元データはcsvで作られているのに各データはjsonで提供されている!「へ~、すごいな~、どうやってるんだろう~」と気になり始めたのでRustで挑戦してみました。

実際に使われているWebAPIとは大きく異なるとは思いますが気にせずトライ。

Rustの環境

使っているクレートとバージョンです。

  • cargo 1.49.0
  • actix-web 3.3.0
  • openssl 0.10.32
  • encoding_rs 0.8.26
  • csv 1.1.5
  • serde_json 1.0

尚、今回使うcsvファイルは北海道の新型コロナウイルスのデータを使っています。

クレートの簡単な説明

actix-webRust用のwebフレームワーク
Web APIの肝
opensslOpenSSL用のクレート
encoding_rsエンコード・デコードを行う為のクレート
csvファイルを取得した時のバイト文字列→utf8変換に使用
csvcsvのリーダー・ライター用のクレート
serde_jsonjsonをシリアライズ・デシリアライズする為のフレームワーク

苦労した点

csvのヘッダーが日本語

Rustを使ってcsvからjsonへ変換する一般的な方法(ネット情報や公式サイトに載っている情報)はcsvの構造体(struct)を定義してjsonに変換するのが一般的なようです。

csv
struct
json

まずcsvをstructに変換するためにstructを定義します。

#[derive(Deserialize)]
struct Record {
    name: String,
    address: String,
    age: u16,
    sex: String,
}

取り扱うcsvがは次のようになっているとします。

"名前","住所","年齢","性別"
"ダニエル","カリフォルニア","30","男性"
"キャサリン","ニューヨーク","40","女性"
"ロバート","シカゴ","20","男性"

そしてこのcsvをcsv::StringRecord::from()で読み込む予定でした、が、csvのヘッダーの部分が日本語になっています。これではcsvを読み込むことが出来ませんでした。
(方法があるのかもしれませんが、私にはわかりませんでした。)

これが非常にやっかいでした。ネット情報内で使っているcsvはフィールドがローマ字になっています。しかし今回使うcsvのヘッダーは日本語です。なのでこの方法では変換できないことがわかりました。

この日本語問題を解決する為HashMap(連想配列)を使う事にしました。

改善点

csvをjsonに変換する為に、HashMap(配列)を利用することにしました。

csv
行毎にHashMapへ読込
そしてVecへ保存
json

行毎にcsvをHashMapに取り込んで、取り込んだHashMapをVecへ保存するという手順になります。

実際にcsv取得→HashMap読込→Vecへ保存すると次のようになります(まだjsonではありません)。

[
  {
    "名前":"ダニエル",
    "住所":"カリフォルニア",
    "年齢":"30",
    "性別":"男性",
  },
  {
    "名前":"キャサリン",
    "住所":"ニューヨーク",
    "年齢":"40",
    "性別":"女性",
  },
  {
    "名前":"ロバート",
    "住所":"シカゴ",
    "年齢":"20",
    "性別":"男性",
  },
]

ほぼjsonです。あとはこれをjsonの型に変換してあげます。

これでとりあえず、csv→jsonに変換は何とかなりそうです。

Rustのコーディング

それではコードを書いていきます。今回行う主な処理は二つ。

  1. actix-webを使ってWebAPIの作成
  2. csv→jsonの変換処理

それでは順番に見ていきます。

actix-webによるWebAPIの作成

あんまり難しことはわからないので、すごーくシンプルにWebAPIを書いています。

まずはactix-webのメインのサーバー部分。jsonを取得するget_json関数を登録してサーバーを起動しています。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(get_json)
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

つづいてget_json()関数。jsonを取得してHttpResponseのbodyに埋め込んでその値を返還しています。

#[get("/csv")]
async fn get_json() -> impl Responder {
    // client_for_csv_to_jsonはcsvをjsonに変換する為に作ったモジュールです。
    let result = client_for_csv_to_json::module_csv_to_json::get_csv_to_json().await;
    HttpResponse::Ok()
    .content_type("application/json")
    .body(result)
}

csvからjsonへの変換処理

ここではcsvファイルを取得してjson形式に変換します。一連の処理をモジュールとして作成します。

csvファイルの取得

まずはcsvファイルの取得。ポイントは接続にopensslクレートを使っていること、メソッドがいっぱいぶら下がっている、ということでしょうか。

let builder = SslConnector::builder(SslMethod::tls()).unwrap();
let client = Client::builder()
              .connector(Connector::new().ssl(builder.build()).finish())
              .finish();
let result = client
              // csvファイル:北海道の新型コロナウイルス感染症に関するデータ
              .get("https://www.harp.lg.jp/opendata/dataset/1369/resource/2828/patients.csv")
              .send()
              .await
              .unwrap()
              .body()
              .limit(20000000) // スキームタイプ毎の最大同時接続数
              .await
              .unwrap();

csvのデコード処理

読み込んだcsvをデコードして変数に代入します。

// csvのデコード処理
let (result_decode, _, _) = encoding_rs::SHIFT_JIS.decode(&result);
let rchange = result_decode.into_owned();
let mut rdr = csv::Reader::from_reader(rchange.as_bytes());

読み込んだcsvをjsonに変換

つづいてcsv→jsonの変換処理。この辺のメソッドの使い方がややこしい。
処理の流れとしては一行ごとに各データを日本語のキーをつけてHashMapにインサートしています(日本語のキーはcsvのヘッダーに相当します)。インサートしたHashMapをVecにプッシュしています。これをループで全行に対して行います。

// csvデータを一行毎HashMapに収納
let mut csv_hash = HashMap::new();
// 全てのcsvを収納(行毎に収納したHashMapをVecに収納)
let mut csv_vec_hash = Vec::new();

for rc in rdr.records() {
  // csv一行毎のデータ
  let rc_c = rc.unwrap().clone();
        
  // csvデータをHashMapに保存
  csv_hash.insert("No", rc_c.get(0).unwrap().to_owned());
  csv_hash.insert("リリース日", rc_c.get(1).unwrap().to_owned());
  csv_hash.insert("曜日", rc_c.get(2).unwrap().to_owned());
  csv_hash.insert("居住地", rc_c.get(3).unwrap().to_owned());
  csv_hash.insert("年代", rc_c.get(4).unwrap().to_owned());
  csv_hash.insert("性別", rc_c.get(5).unwrap().to_owned());
  csv_hash.insert("属性", rc_c.get(6).unwrap().to_owned());
  csv_hash.insert("備考", rc_c.get(7).unwrap().to_owned());
  csv_hash.insert("補足", rc_c.get(8).unwrap().to_owned());
  csv_hash.insert("退院", rc_c.get(9).unwrap().to_owned());
  csv_hash.insert("周囲の状況", rc_c.get(10).unwrap().to_owned());
  csv_hash.insert("濃厚接触者の状況", rc_c.get(11).unwrap().to_owned());
  csv_hash.insert("age_group", rc_c.get(12).unwrap().to_owned());
  csv_hash.insert("sex_en", rc_c.get(13).unwrap().to_owned());

  // HAshMapをVecに保存
  csv_vec_hash.push(csv_hash.clone());
}

json形式に変換

Vecに収納した全データをjsonとして使えるようにString型に変換してあげます。

// data形式を揃える為jsonのルートにdata追加
// 元のデータとjsonの形式を揃える為にやっています。無くても問題ありません。
let mut json_data = HashMap::new();
json_data.insert("data", csv_vec_hash);
    
// ハッシュマップをjson形式の文字列に変換
serde_json::to_string(&json_data).unwrap()

処理の流れは以上です。

全コード

最後に全コードを掲載しておきます。

mod client_for_csv_to_json;
use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn get_top() -> impl Responder {
    HttpResponse::Ok().body("top page")
}

#[get("/csv")]
async fn get_json() -> impl Responder {
    let result = client_for_csv_to_json::module_csv_to_json::get_csv_to_json().await;
    HttpResponse::Ok()
    .content_type("application/json")
    .body(result)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(get_top)
            .service(get_json)
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

pub mod module_csv_to_json {
    use actix_web::client::{Client, Connector};
    use openssl::ssl::{SslConnector, SslMethod};
    use encoding_rs;
    use serde_json;
    use csv;
    use std::collections::HashMap;

    pub async fn get_csv_to_json() -> String {
      let builder = SslConnector::builder(SslMethod::tls()).unwrap();
      let client = Client::builder()
                    .connector(Connector::new().ssl(builder.build()).finish())
                    .finish();
      let result = client
                    // csvファイル:北海道の新型コロナウイルス感染症に関するデータ
                    .get("https://www.harp.lg.jp/opendata/dataset/1369/resource/2828/patients.csv")
                    .send()
                    .await
                    .unwrap()
                    .body()
                    .limit(20000000) // スキームタイプ毎の最大同時接続数
                    .await
                    .unwrap();


        // csvのデコード処理
        let (result_decode, _, _) = encoding_rs::SHIFT_JIS.decode(&result);
        let rchange = result_decode.into_owned();
        let mut rdr = csv::Reader::from_reader(rchange.as_bytes());

        // csvデータを一行毎HashMapに収納
        let mut csv_hash = HashMap::new();
        // 全てのcsvを収納(行毎に収納したHashMapをVecに収納)
        let mut csv_vec_hash = Vec::new();

        for rc in rdr.records() {
          // csv一行毎のデータ
          let rc_c = rc.unwrap().clone();
                
          // csvデータをHashMapに保存
          csv_hash.insert("No", rc_c.get(0).unwrap().to_owned());
          csv_hash.insert("リリース日", rc_c.get(1).unwrap().to_owned());
          csv_hash.insert("曜日", rc_c.get(2).unwrap().to_owned());
          csv_hash.insert("居住地", rc_c.get(3).unwrap().to_owned());
          csv_hash.insert("年代", rc_c.get(4).unwrap().to_owned());
          csv_hash.insert("性別", rc_c.get(5).unwrap().to_owned());
          csv_hash.insert("属性", rc_c.get(6).unwrap().to_owned());
          csv_hash.insert("備考", rc_c.get(7).unwrap().to_owned());
          csv_hash.insert("補足", rc_c.get(8).unwrap().to_owned());
          csv_hash.insert("退院", rc_c.get(9).unwrap().to_owned());
          csv_hash.insert("周囲の状況", rc_c.get(10).unwrap().to_owned());
          csv_hash.insert("濃厚接触者の状況", rc_c.get(11).unwrap().to_owned());
          csv_hash.insert("age_group", rc_c.get(12).unwrap().to_owned());
          csv_hash.insert("sex_en", rc_c.get(13).unwrap().to_owned());
        
          // HAshMapをVecに保存
          csv_vec_hash.push(csv_hash.clone());
        }

        // data形式を揃える為ルートにdata追加
        let mut json_data = HashMap::new();
        json_data.insert("data", csv_vec_hash);
            
        // ハッシュマップをjson形式の文字列に変換
        serde_json::to_string(&json_data).unwrap()
    }
}

[package]
name = "actix-web-csv-json-api"
version = "0.1.0"
authors = ["vagrant"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = {version = "3.3.0", features = ["openssl"]}
serde = { version = "1.0", features = ["derive"] }
openssl = "0.10.32"
encoding_rs = "0.8.26"
csv = "1.1.5"
serde_json = "1.0"

GitHubにも置いています。

お疲れ様でした。

Rustactix-web

Posted by Bright_Noah