RustでGTFSファイルをURLから読み込む



[gtfs-structures](https://github.com/rust-transit/gtfs-structure)を利用して、日本の鉄道のGTFSファイルをURLから読み込んでみます。

# 前提

* `cargo`を利用して環境構築ができること[^1]。
* GTFSファイルがどんなものかなんとなくわかること。

# Get Started

`cargo new {プロジェクト名}`でプロジェクトを作成し、作成されたディレクトリの中に入ります。
（今回は仮に`gtfs-sample`をプロジェクト名にします）

```bash

# gtfs-sample プロジェクトを作成
$ cargo new gtfs-sample

# 作成されたディレクトリの中に移動
$ cd gtfs-sample
```

`Cargo.toml`の`dependencies`に`gtfs-structures`を追加します。

```Cargo.toml
[dependencies]
gtfs-structures = "0.16"
```

コードには、GTFSファイルの取得処理とその取得物に対する処理を記述します。
今回は「高松琴平電気鉄道株式会社[^2]」さんのGTFSファイルを利用させていただきます。

```rust:src/main.rs
use gtfs_structures::Gtfs;

fn main() {
    // GTFSファイルの取得先
    let url = "http://www.kotoden.co.jp/publichtm/gtfs/gtfsdata/latest/gtfs_kd.zip";

    // GTFSファイルの取得処理
    let gtfs = Gtfs::from_url(url).unwrap();

    // 取得したGTFSファイルに関する情報を標準出力
    gtfs.print_stats();
}
```

上記の設定でビルド、成果物を実行して、以下のような結果が返れば成功です。

```.bash
$ cargo run
   :
GTFS data:
  Read in 556 ms
  Stops: 233
  Routes: 6
  Trips: 712
  Agencies: 1
  Shapes: 6
  Fare attributes: 2652
```

※ ビルドにはopensslパッケージが必要です。ビルド中にエラーが発生した場合は、適当なパッケージをインストール[^3]してください。


# 内容の表示

Gtfsオブジェクトに対してフィールドを要求することで、内容を取得することができます。

```rust
// 停車地情報のオブジェクトを丸ごと表示
println!("{:?}", gtfs.stops);

// 停車地数の表示
println!("{}", gtfs.stops.len());
```

フィールド名がGTFSデータのtxtファイル名に似ていますが、内容は同じではありません。
返り値には各ファイルの内容が結合された構造体が返されます。

なお、GTFSデータのtxtファイル毎のデータが取得できる[RawGtfs](https://github.com/rust-transit/gtfs-structure#rawgtfs)なるモデルも存在するようです。
もう少し元データに沿った読み込み、及び利用がしたい方は、上記のモデルを利用するのがいいかもしれません。


## Example1: 停車地名称一覧

停車地名称の一覧を表示します。

```rust
// 停車地名称の表示（停車地名称が空文字の場合を除く）
for stop in gtfs.stops {
    if stop.1.name != "" {
        println!("{}", stop.1.name);
    }
}
```

以下のように停車地名称が表示されれば成功です。

```bash
$ cargo run
   :
白山
潟元
一宮
綾川
羽床
水田
八栗
栗林公園
   :
```
※ あくまで「停車地」なので、駅名称以外が表示されることもあります。


## Example2: 時刻表（二点間を通る列車の出発地の出発時刻一覧）

`瓦町`から`八栗`へ行く列車の出発地の出発時刻一覧（平日のみ）を表示します。
（つまり`瓦町`の`高松琴平電気鉄道琴平線（琴電志度方面）`の時刻表です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425840/fb2dcfa9-b086-5ff1-e70b-f2821036b31d.png)


上りや下りを判別したり、他の路線の時刻が混ざらないようにしないといけないため、少し複雑です。

```rust

// 出発地と到着地の指定
let from_point = "瓦町";
let to_point = "八栗";

// 時刻表の取得
let mut times = gtfs.trips.iter().filter_map( |trip| {

    // 方向を特定するための変数
    let mut from_stop_sequence = 0;
    let mut to_stop_sequence = 0;

    // 平日の場合のみ時間を取得する
    if trip.1.service_id == "平日" {
        match trip.1.stop_times.iter().filter_map( |stop_time| {

            // 着駅の停車地の順序を更新
            if stop_time.stop.name == to_point {
                to_stop_sequence = stop_time.stop_sequence;
            }

            // 発駅から車両が出発する時間を取得し、停車地の順序を更新する
            if stop_time.stop.name == from_point {
                match stop_time.departure_time {
                    Some(departure_time) => {
                        from_stop_sequence = stop_time.stop_sequence;
                        Some(departure_time)
                    },
                    None => None,
                }
            } else {
                None
            }
        }).collect::< Vec<u32> >() {

            // 発駅の後に着駅が来る経路の場合に到着時間を返す
            vec if vec.len() > 0 && from_stop_sequence < to_stop_sequence => {

                // 一回の運転で複数回同じ駅に停車する場合は最後に停車する時刻を返す
                Some( vec[vec.len()-1] )
            },
            _ => None,
        }
    } else {
        None
    }
}).collect::< Vec<u32> >();

// 整形して表示
times.sort();
for time in times {
    println!("{0: >02}:{1: >02}:{2: >02}", (time/3600), (time%3600/60), (time%3600%60));
}
```

以下のように時刻一覧が表示されれば成功です。

```bash
$ cargo run
   :
06:06:00
06:26:00
06:48:00
06:56:00
07:06:00
07:16:00
07:26:00
07:36:00
07:46:00
07:56:00
08:06:00
08:16:00
   :
```

高松琴平電気鉄道株式会社さんのサイト[^2]の時刻表、及び駅すぱあとforWEB[^4]の時刻表と照らし合わせてみましたが、どちらの時刻表とも結果が一致したので問題なさそうです。

※ 以下は駅すぱあとforWEBの時刻表
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425840/63d69d6b-3863-0f94-e130-79fd0f529f53.png)


# おまけ（ローカルのデータを読み込む）

データ配布元に何回もアクセスすると、配布元のサーバに負荷がかかってしまいます。
予期せぬ大量のアクセスでサーバに負荷がかかると、最悪 **サーバ自体が停止** してしまう可能性があります。

今回利用したgtfs-structuresでは、上記のようなケースを避けるために、ローカルに落としてきたデータを読み込む手段も用意されています。
やむなくデータを何回も読み込む必要がある場合は、以下のような記述でローカルから読み込むようにしましょう。

```rust

// path_of_a_directory にGTFSファイルへのパスを記述する
let gtfs = Gtfs::new("path_of_a_directory").unwrap();
```

# おわりに

DBを使うまでもない作業、もしくはハッカソンなどでサクッと作品を作りたい場合に役立つかも。


[^1]: 参考: https://qiita.com/elipmoc101/items/3c8b6d8332a9019e578c
[^2]: 配布元: http://www.kotoden.co.jp/
[^3]: 参考: https://qiita.com/nacika_ins/items/465e89a7b3fbeb373605
[^4]: 参考: https://roote.ekispert.net/ja