群馬県公共交通オープンデータ（GTFS）を一括してデータベースに格納する方法

2018年12月26日に、群馬県から[県内（ほぼ？）全ての路線バスと一部の私鉄の時刻表データがオープンデータとして公開](https://gma.jcld.jp/GMA_OPENDATA/)されました。群馬県のバス会社は、まさに群雄割拠というか中小コミュニティバスが入り乱れている状況ですが、データという点では、「[標準的なバス情報フォーマット（GTFS-JP）](http://www.mlit.go.jp/sogoseisaku/transport/sosei_transport_tk_000067.html)」という形式で揃ったことになります。ちなみにこの形式は、世界的なデファクトスタンダードである[GTFS形式](https://developers.google.com/transit/gtfs/)と互換性があり、Google Mapsや[Yahoo!路線情報](https://transit.yahoo.co.jp/main/buslist)などで幅広く使われています。

[群馬県交通まちづくり戦略](https://www.pref.gunma.jp/contents/100059524.pdf)によると、群馬県は「公共交通の分担率はわずか2.8 %(鉄道2.5%、バス0.3%)と低く、年間を通して一度も鉄道・バスを利用しない県民は約60%」という極端な車社会です。そのため、オープンデータを活用してバスの利便性を高め、県民の足としてバスの認知を高めてゆくことが望まれています。

そこで、この記事では先日書いた「[GTFSファイルをマージしてPostgreSQL+PostGISに投入する方法](https://qiita.com/niyalist/items/22b743d973e4a6aeef86)」を参考に、群馬県のデータを一括してダウンロード、DBに格納する方法を説明します。

データの活用方法に関しては、「[PostgreSQL+PostGISに格納したGTFS形式の公共交通オープンデータを操作する](https://qiita.com/niyalist/items/39142d03fbfe241166ea)」という記事を同時に書いているので、参考にしてください。
# チート

本来は以下に説明する手順を踏まえて作業する必要がありますが、アプリ開発を容易にするため、ここで作成したデータを共有します（~~2018年12月26日公開時点で作成~~2018年12月28日時点に更新）。本データは無保証であり、また以下の作業の性質上各IDが書き換わっています。利用される場合は、元データのライセンスに従って利用してください。

- 統合GTFSファイル（5.1MB）
[http://www.niya.net/gunma/GTFS.zip](http://www.niya.net/gunma/GTFS.zip)

- PostgreSQLのダンプファイル（11.5MB）
 [http://www.niya.net/gunma/gtfs-gunma.dump](http://www.niya.net/gunma/gtfs-gunma.dump)

`pg_dump -Fc gtfs-gunma > gtfs-gunma.dump` で作成したダンプファイルです。PostgreSQL+PostGISがインストールされてる環境で、 `pg_restore -C -d postgres gtfs-gunma.dump` とすることでデータベースを作成出来ます。

pgAdmin4を用いてデータベースを作成する場合、(1)Databasesを右クリック→Create→Database で gtfs-gunma という名称のDBを作成、(2)作成したgtfs-gunmaを右クリック→Restore→Filenameを指定してRestoreを実行します。

# 詳細な作業手順
##データの一括ダウンロード
公開されたデータは「[群馬県内バス路線情報（標準的なバス情報フォーマット）](https://gma.jcld.jp/GMA_OPENDATA/)」というページからダウンロードが出来ますが、事業者ごとに分割された40個ものGTFSファイルなので、まずはこれを一括してダウンロードする必要があります。このページではダウンロードがJavaScriptで処理されており、ファイルのURLが不明ですが、htmlファイルの中身をよく見ると、[`https://gma.jcld.jp/GMA_OPENDATA/opendata.json`](https://gma.jcld.jp/GMA_OPENDATA/opendata.json) というURLでこのページを構成するJSONデータにアクセスできることが分かります。以下にデータの一部分を抜粋します。ここに入っているzip_pathがGTFSファイルのURLのようですね。

```json:opendata.json（一部分）
[
  {
    "title": "上信電鉄",
    "zip_file": [
      {
        "zip_title": "上信電鉄",
        "zip_path": "//gma.jcld.jp/GMA_OPENDATA/GTFS/gtfs.1013.joshin_bus.zip",
        "date": "20180530",
        "lines": [
          "高崎駅～前橋駅・中央前橋駅",
          "高崎問屋町駅～鶴辺団地"
        ],
        "file_date": "20181220"
      }
    ]
  },
  {
    "title": "関越交通",
    "zip_file": [
      {
        "zip_title": "関越交通",
        "zip_path": "//gma.jcld.jp/GMA_OPENDATA/GTFS/gtfs.1001.kanetsu_kotu.zip",
        "date": "20181201",
        "lines": [],
        "file_date": "20181225"
      }
    ]
  },
  {
    "title": "群馬バス",
    "zip_file": [
      {
        "zip_title": "群馬バス",
        "zip_path": "//gma.jcld.jp/GMA_OPENDATA/GTFS/gtfs.1007.gunma_bus.zip",
        "date": "20181127",
        "lines": [],
        "file_date": "20181220"
      }
    ]
  }
]
```

次に、このこのURLのファイルを一括ダウンロードするプログラムを作ります。JSONの処理を考え、Rubyで作ってみました。

```ruby:01-download-gunma.rb
#! /usr/bin/ruby

require 'open-uri'
require 'json'

url='https://gma.jcld.jp/GMA_OPENDATA/opendata.json'
download_path = 'gtfs' # ダウンロードしたファイルを保存するディレクトリ

file= open(url)
json = JSON.load(file)
json.each do |element|
  data = element['zip_file']
  name = data[0]['zip_title'] #1つの事業者に2個以上のファイルを格納した例がないため0で決め打ち
  gtfs_url = 'http:' + data[0]['zip_path']
  base_name = File.basename(gtfs_url)
  write_file = download_path + '/' + base_name
  
  puts name
  puts gtfs_url

  open(gtfs_url) do |file|
    open(write_file, "w+b") do |out|
      out.write(file.read)
    end
    File::utime(file.last_modified, file.last_modified, write_file)
  end
end

# 最後にjsonファイルを保存する
file.rewind
open(File.basename(url), "w+b") do |out|
  out.write(file.read)
end
```

適当にディレクトリを作成し、このスクリプトを実行してまずはファイルをダウンロードします。以下ではUNIX環境の例を書きましたが、コマンドなどはそれぞれの環境に合わせて読み替えてください。

```bash
$ mkdir gunma               # 適当にディレクトリを作成
$ cd gunma
$ vi 01-download-gunma.rb   # 上記スクリプトを作成
$ mkidr gtfs                # ダウンロードするディレクトリを作成
$ ruby 01-download-gunma.rb # スクリプトの実行
```

## GTFSデータの解凍

GTFSファイルをマージするために、一括で解凍します。UNIX環境ならば、シェルスクリプトで実行しましょう。このコマンドで、`uncompressed`というディレクトリ内にサブディレクトリが作られ、その中にGTFSファイルがそれぞれ展開されます。

```bash:02-uncompress.sh
#!/bin/bash

GTFSDIR='gtfs'
EXTRACTDIR='uncompressed'

rm -rf $EXTRACTDIR
mkdir $EXTRACTDIR

for FILE in $GTFSDIR/*.zip
do
    FILENAME=`basename $FILE`
    DIRNAME=${FILENAME%.*}
    echo '==============='
    echo $FILENAME
    mkdir $EXTRACTDIR/$DIRNAME
    unzip $FILE -d $EXTRACTDIR/$DIRNAME
done
```

## 仕様に沿わないデータの修正（2018年12月27日更新でこの作業は不要になりました）
~~このままデータのマージ作業を始めたいところですが、一部のデータに仕様に従っていないところがあり、このまま進めるとエラーとなるため、手作業で修正します。具体的には、 `gtfs.0002.express.GMA/agency.txt`（高速バス）、`gtfs.0003.lim.GMA/agency.txt`（空港リムジンバス） それぞれの、agency_urlが空欄となっています（仕様では必須）。適当なエディタで、ここに適当なURLを記入します。~~

## GTFSデータのマージ
[GTFSファイルをマージしてPostgreSQL+PostGISに投入する方法](https://qiita.com/niyalist/items/22b743d973e4a6aeef86)で利用した[onebusaway-gtfs-merge-cli](http://developer.onebusaway.org/modules/onebusaway-gtfs-modules/1.3.3/onebusaway-gtfs-merge-cli.html)をここでも利用します。ビルド手法などは以前の記事を参照してください。この手法だと、マージの際にIDが書き換わってしまうのが問題なのですが、とりあえず目をつぶります。今回もファイル数が多いのでシェルスクリプトで実行します。

```bash:03-merge.sh
#!/bin/bash

MERGEDDIR='merged'
JARFILE='onebusaway-gtfs-merge-cli-1.3.48.jar'

rm -rf $MERGEDDIR
mkdir  $MERGEDDIR

unset FILES; 
for DIR in uncompressed/*;
do
    FILES=$FILES\ $DIR;
done
echo $FILES;

java -jar $JARFILE $FILES $MERGEDDIR

cd $MERGEDDIR
zip GTFS.zip *.txt
```

なおこの処理はとても時間がかかります。私の環境（MacBook Pro 15inch 2016年モデル）では2時間16分かかりました。

## データベースへの投入
出力された`merged/GTFS.zip`をPostgreSQL+PostGISに投入します。詳細は[GTFSファイルをマージしてPostgreSQL+PostGISに投入する方法#GTFSファイルのデータベースへの投入](https://qiita.com/niyalist/items/22b743d973e4a6aeef86#gtfsファイルのデータベースへの投入)をご覧ください。ここでは、これまでの流れと同じくスクリプトにして実行します。

```bash:04-import-db.sh
#!/bin/bash

DBNAME='gtfs-gunma'
GTFSFILE='merged/GTFS.zip'

dropdb --if-exists $DBNAME
createdb $DBNAME
psql -d $DBNAME -c "create extension postgis"
../gtfsdb/bin/gtfsdb-load --database_url postgresql://localhost:5432/$DBNAME --is_geospatial $GTFSFILE
```
これでデータベースへの投入が完了しました。