PythonのGBFSライブラリ紹介

みなさん、GBFS(General Bikeshare Feed Specification)を触っていますか？
え、まだ？
いやいや、それは時代に遅れてますよ！！

あ、GBFSとはなんぞやについては、@kumatira さんの記事に詳しいのでそちらを参照ください。
https://qiita.com/kumatira/items/f9229c21d9f0db3b5ae3

今日はまだGBFSを触ったことがないという方のために、PythonでGBFSを簡単に扱えるライブラリを紹介いたします。

紹介するライブラリは下記の2点です(PyPIでGBFSを検索して出てきたライブラリ)。

* gbfs-client 0.1.8
* bicidata 0.0.5

なおPythonでバイクシェアを扱うライブラリとして、pybikes、python-citybikesといったライブラリもありますが、これらはCitybikesというGBFSとはまた別のバイクシェアデータフォーマットを扱うライブラリです。

:::note info
本記事では、OpenStreet株式会社（ハローサイクリング） / 公共交通オープンデータ協議会のバイクシェア関連情報(GBFS形式)をCC-BY4.0に従って利用しています。
https://ckan.odpt.org/dataset/c_bikeshare_gbfs-openstreet
:::

# gbfs-client

まず[gbfs-client](https://github.com/jakehadar/bikeshare-client-python)から紹介します。

gbfs-clientはここ数年、メンテナンスが止まっています。
しかし、取得したjsonをそのまま使うだけという設計が功を奏して、GBFSがバージョンアップしても問題なく動作します。

GBFSがsystem.csvをやめるなど、根本的な仕様変更をしない限りは使えるのでないかと思います。

## インストール

```Shell
pip install gbfs-client
```

## バイクシェアのデータの探し方

GBFSはその仕様に「GBFSでデータを作ったらシステムIDを申請して登録してください」と定めています。
GBFSの仕様は、githubで管理されており、リポジトリ中に登録されたシステムの一覧がCSVで含まれています。

gbfs-clientは、そのCSVを利用して、世界各国のバイクシェアを検索する、SystemDeiscoveryServiceを提供しています。

```Python
from gbfs.services import SystemDiscoveryService

sds = SystemDiscoveryService()

#システムIDの一覧
print(sds.system_ids)

#システム情報の一覧
print(sds.systems)
```

```Shell
['careem_bike', 'bike_nordelta', 'bike_buenosaires', 'biketobike', 'nextbike_al', 'Link_Linz', 
'nextbike_na', 'nextbike_ka', 'nextbike_la', 'nextbike_ta', 'nextbike_si', 'Link_Vienna', 'nextbike_vt',
'nextbike_wr', '11b14b88-3598-4d3c-821d-e6f2e37cd901', '11b14b88-3598-4d3c-821d-e6f2e37cd902',
'11b14b88-3598-4d3c-821d-e6f2e37cd903', '11b14b88-3598-4d3c-821d-e6f2e37cd904', '11b14b88-3598-4d3c-821d-e6f2e37cd909',
'greenbike_aruba', 'nextbike_bj', 'nextbike_ba', 'nextbike_bz', 'bird-antwerp', 'bluebike',
'donkey_antwerp', 'donkey_gh', 'donkey_kortrijk_leiedal', 'lime_antwerp', 'lime_brussels',(以下略)
```
```Shell
[{'Country Code': 'AE', 'Name': 'Careem BIKE', 'Location': 'Dubai, AE', 'System ID': 'careem_bike',
'URL': 'https://www.careem.com/en-ae/careem-bike/',
'Auto-Discovery URL': 'https://dubai.publicbikesystem.net/customer/gbfs/v2/gbfs.json',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fdubai.publicbikesystem.net%2Fcustomer%2Fgbfs%2Fv2%2Fgbfs.json'},
 {'Country Code': 'AR', 'Name': 'Bike Nordelta', 'Location': 'Buenos Aires, AR', 'System ID': 'bike_nordelta',
'URL': 'https://bikeitau.com.br/nordelta/',
'Auto-Discovery URL': 'https://nordelta.publicbikesystem.net/ube/gbfs/v1/',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fnordelta.publicbikesystem.net%2Fube%2Fgbfs%2Fv1%2F'},
{'Country Code': 'AR', 'Name': 'Ecobici', 'Location': 'Buenos Aires, AR', 'System ID': 'bike_buenosaires',
'URL': 'https://www.buenosaires.gob.ar/ecobici',
'Auto-Discovery URL': 'https://buenosaires.publicbikesystem.net/ube/gbfs/v1/',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fbuenosaires.publicbikesystem.net%2Fube%2Fgbfs%2Fv1%2F'},
(以下略)
```

ご覧の通り、SystemDiscoveryService.system_ids, systems共にただのリストなので、イテレータを回して、条件を設定して任意のシステムのみ表示させることが可能です。

```Python
#docomoを含むIDのみ表示
for _id in sds.system_ids:
    if 'docomo' in _id:
        print(_id)
```
```Shell
docomo-cycle-tokyo
```

```Python
#日本で運営されているシステムを抽出
jp_systems = [system for system in sds.systems if 'JP' in system['Country Code']]
print(jp_systems)
```
```Shell
[{'Country Code': 'JP', 'Name': 'docomo bike share service', 'Location': 'Tokyo, JP', 'System ID': 'docomo-cycle-tokyo',
'URL': 'https://docomo-cycle.jp/',
'Auto-Discovery URL': 'https://api-public.odpt.org/api/v4/gbfs/docomo-cycle-tokyo/gbfs.json',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fapi-public.odpt.org%2Fapi%2Fv4%2Fgbfs%2Fdocomo-cycle-tokyo%2Fgbfs.json'},
{'Country Code': 'JP', 'Name': 'HELLO CYCLING', 'Location': 'JP', 'System ID': 'hellocycling',
'URL': 'https://www.hellocycling.jp',
'Auto-Discovery URL': 'https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fapi-public.odpt.org%2Fapi%2Fv4%2Fgbfs%2Fhellocycling%2Fgbfs.json'}]
```

SystemDiscoveryService.systemsは、GBFSのsystems.csvのフィールド名をkeyとした辞書になっているので、フィールドを使って様々な検索条件を作れます。

システムIDが既知の場合は、システムIDを直接指定してシステム情報を取ることも可能です。

```Python
#システムIDを指定してシステム情報を取得
system = sds.get_system_by_id('hellocycling')
print(system)
```
```Shell
{'Country Code': 'JP', 'Name': 'HELLO CYCLING', 'Location': 'JP', 'System ID': 'hellocycling',
'URL': 'https://www.hellocycling.jp',
'Auto-Discovery URL': 'https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json',
'Validation Report': 'https://gbfs-validator.netlify.app/?url=https%3A%2F%2Fapi-public.odpt.org%2Fapi%2Fv4%2Fgbfs%2Fhellocycling%2Fgbfs.json'}
```

# GBFSの情報を取得する

gbfs-clientを使って、GBFSを取得するには、まずclientを構築する必要があります。
clientの構築には２通りのやり方があります。

## 1.SystemDiscoveryServiceから生成する
SystemDiscoveryServiceにはシステムIDを指定して、クライアントを生成する機能がありますので、それを用います。

```Python
client = sds.instantiate_client('hellocycling')
print(client)
```
```Shell
GBFSClient('https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json', 'jp')
```

なおinstantiate_client(system_id, language=None)というAPIになっており、langageを省略した場合、'en'が自動で設定されます。
ドコモ・バイクシェアとHELLO CYCLINGは日本語でしかデータ提供していないので、'jp'の指定がないとエラーになります。

## 2.URLを直接指定してclientを生成する
GBFSのうち、gbfs.jsonのURLが分かっている場合(システム情報のAuto-Discovery URLに記載)、そのURLを直接指定してclientを生成することもできます。

```Python
from gbfs.client import GBFSClient
client = GBFSClient('https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json', 'jp')
print(client)
```
```Shell
GBFSClient('https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json', 'jp')
```

こちらも同じくlanguageに'jp'を指定しないと、ドコモ・バイクシェアとHELLO CYCLINGともにエラーになります。

## サポートしているGBFS情報を確認する

GBFSにはいくつかの情報の種類があり、そのうち何の情報をサポートしているかを確認します。

```Python
print(client.feed_names)
```
```Shell
['system_information', 'station_information', 'vehicle_types', 'station_status']
```

## GBFSを取得する

サポートしているGBFS情報のフィード名を指定すると、その情報が取得できます。

```Python
print(client.request_feed('vehicle_types'))
```
```Shell
{'ttl': 60,
 'data': {
   'vehicle_types': [
     {'name': 'hellocycling bicycle',
      'form_factor': 'bicycle',
      'wheel_count': 2,
      'vehicle_assets':
        {'icon_url': 'https://d1yl7kw204zjxn.cloudfront.net/gbfs/v2/public/images/YAMAHA_HELLO.png', 
         'icon_url_dark': 'https://d1yl7kw204zjxn.cloudfront.net/gbfs/v2/public/images/YAMAHA_HELLO.png', 
         'icon_last_modified': '2022-04-01'},
      'propulsion_type': 'electric_assist',
      'vehicle_type_id': '2',
      'max_range_meters': 70000,
      'return_constraint': 'any_station',
      'max_permitted_speed': 25,
      'default_reserve_time': 30}]},
 'version': '2.3',
 'last_updated': datetime.datetime(2022, 11, 7, 14, 12, 29)}
```

request_feed()で取得した情報は、辞書形式になっているので、非常に扱いやすいです。

```Python
for datum in client.request_feed('vehicle_types')['data']['vehicle_types']:
    print(datum['wheel_count'])
```
```Shell
2
```

他のフィードも同様のやり方で取得、扱うことができます。

# bicidata

gbfs-clientがGBFSの取得に特化していたのに対し、[bicidata](https://github.com/bicidata/bicidata)はGBFSフレームワークを名乗る通り、GBFSを活用しやすい形で取得する機能が提供されています。

* Snapshot : ある一瞬のGBFSの状態を取得
* Archivers : SnapshotをDataFrame+xarrayで保存
* Reporters : 未実装。GBFSの解析機能を提供する機能のようです。サンプルデータとその解析プログラムがGithubのスクリプト例に含まれているだけでモジュール機能としては提供されていません
* Publishers : 未実装。GBFSの公開機能(サーバ機能)を実装しようとしていたようですが、これも提供されていません。

Githubを見ても最後の更新が数年前なので、おそらく未実装の機能が実装されることはないでしょう。

## インストール

公式サイトでは、

「`pip install bicidata` するとPyPIバージョンが使えるけど、`pip install git+https://github.com/bicidata/bicidata.git`が最新版使えるからオススメ」（意訳）

と書いてありますが、おすすめの方法だとインストールしても**動きません**。

setup.pyを見ると、依存パッケージにbicidataとbicidata.servicesとだけ書かれています。
しかし、bicidata.servicesは、bicidata.commonに依存しています。おすすめのインストール方法だと、services収録の機能を用いたときに、bicidata.commonがありませんと怒られます。

なので、下記手順でインストールします。

```Shell
git clone https://github.com/bicidata/bicidata.git & cd bicidata

適当なエディタでsetup.pyを開き、L.15を下記のように修正
-    packages=['bicidata', 'bicidata.services'],
+    packages=['bicidata', 'bicidata.services', 'bicidata.apps', 'bicidata.common'],

pip install -e .
```

:::note info
bicidataはgbfs-clientに依存しています。
もしbicidataをインストールしても動作しない場合、gbfs-clientをインストールしてみてください。
:::

# snapshotを取る

snapshotは、スクリプト実行時点のGBFSの状態を保存する機能です。

公式サイトには、`python -m bicidata.services.snapshot`を実行すれば、スナップショットが取れるよと書いてあります。
実行すると、gbfsフォルダが作成され、その中にバルセロナバイクシェアシステムのGBFSのstation_statusが保存されます。

任意のGBFSを取得させたい場合、cloneしたフォルダにある.env.templateを.envにリネームして設定を書けと公式サイトに書いてあります。
間違いではないですが、日本では間違いになります。

一例で、HELLO CYCLINGのGBFSを取得するよう、.envを書いてみましょう。

```
GBFS_SRC_API = "https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json"
GBFS_DST_API = "http://localhost:8000"

SNAPSHOT_SAMPLE_TIME = 60
SNAPSHOT_GBFS_FOLDER = "gbfs"
```

実行します。

```Shell
python -m bicidata.services.snapshot

Traceback (most recent call last):
  File "/Users/muto_jo/.pyenv/versions/anaconda3-2021.11/envs/gbfs/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Users/muto_jo/.pyenv/versions/anaconda3-2021.11/envs/gbfs/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/muto_jo/Study/gbfs/bicidata/bicidata/services/snapshot.py", line 134, in <module>
    GBFSResource(
  File "/Users/muto_jo/Study/gbfs/bicidata/bicidata/common/__init__.py", line 30, in __init__
    super(GBFSResource, self).__init__(url, language, RemoteGBFS())
  File "/Users/muto_jo/.pyenv/versions/anaconda3-2021.11/envs/gbfs/lib/python3.9/site-packages/gbfs/client.py", line 38, in __init__
    raise Exception('Language must be one of: {}'.format(','.join(languages)))
Exception: Language must be one of: jp
```

'jp'を使えと怒られます。先にgbfs-clientで説明したエラーですね。

さてbicidata/services/snapshot.py の__main__(L.129以降)をみてみます。

```Python
if __name__ == '__main__':

    dotenv.load_dotenv()

    snapshot = Snapshot(
        GBFSResource(
            url=os.environ.get("GBFS_SRC_API", "https://barcelona.publicbikesystem.net/ube/gbfs/v1/gbfs.json")
        ),
        FileStorageSaver(
            folder=Path(os.environ.get("SNAPSHOT_GBFS_FOLDER", "gbfs")),
            gbfs_url=os.environ.get("GBFS_DST_API", "http://localhost:8000/gbfs"),
        )
    )

    snapshot.run()
```

GBFSResourceに言語指定ができません。
言語指定ができるようsnapshot.pyを修正してもいいのですが、snapshotの実行サンプルスクリプトが公式サイトにあるので、それを参考にsnapshotを用いたサンプルを自分で実装します。

```Python:snapshot_sample.py
import time
from bicidata.services.snapshot import Snapshot,  FileStorageSaver
from bicidata.common import GBFSResource

num_snapshots = 3
snapshot_sample_time = 60  # time in seconds

snapshot = Snapshot(
    GBFSResource("https://api-public.odpt.org/api/v4/gbfs/hellocycling/gbfs.json", "jp"),
    FileStorageSaver(),
)

for i in range(num_snapshots):
    print('snapshot {} run'.format(i+1))
    snapshot.run()   
    time.sleep(snapshot_sample_time)
```

実行すると、gbfsフォルダができています。

```
gbfs/
  gbfs.json
  snapshot.json
  station_statu/
    {timestamp}.json
```

gbfs.jsonをみてみます。

```json
{
    "last_updated": 1668665669,
    "ttl": -1,
    "data": {
        "en": {
            "feeds": [
                {
                    "name": "system_information",
                    "url": "https://api-public.odpt.org/api/v4/gbfs/hellocycling/system_information.json"
                },
                {
                    "name": "station_information",
                    "url": "https://api-public.odpt.org/api/v4/gbfs/hellocycling/station_information.json"
                },
                {
                    "name": "vehicle_types",
                    "url": "https://api-public.odpt.org/api/v4/gbfs/hellocycling/vehicle_types.json"
                },
                {
                    "name": "station_status",
                    "url": "https://api-public.odpt.org/api/v4/gbfs/hellocycling/station_status.json"
                },
                {
                    "name": "station_status_snapshots",
                    "url": "http://localhost:8000/station_status/{timestamp}.json"
                },
                {
                    "name": "snapshots_information",
                    "url": "http://localhost:8000/snapshots.json"
                }
            ]
        }
    }
}
```

gbfs.jsonに2つのフィードがたされています。station_status_snapshotsとsnapshots_informationですね。
おそらくこれは将来、Publisherを実装した暁には、取得したSnapshotをサービスするためのフィード名とURLのようです。なお、このURLは.envの設定で変更可能なようです。

snapshot.jsonの中身はこうなっています。

```json
{
    "last_updated": 1668665551,
    "ttl": -1,
    "data": {
        "timestamps": [
            1668665253,
            1668665551,
            1668665551
        ]
    }
}
```

station_status/の中には、

* 1668665253.json
* 1668665551.json

が収録されていました。
1分ごと3回取得しましたが、2回目と3回目でタイムスタンプが変更なかったので、情報が更新された2回分のみ保存されました。
中身は通常のstation_statusなので割愛します。

# archiverを動か…せない？

さて次に紹介する機能は、bicidata.services.archiverです。

公式サイトに記述はありませんが、これも`python -m bicidata.services.archiver`で実行可能で、.envによる設定も可能です。
しかし、例によってlanguage指定ができません。

あとこの機能、Publisherが前提になっているように思われます。

というのも、Archiverクラスの中に、_get_snapshot_information()など、Snapshotで取得・生成したjsonがGBFS配信サーバから取得できることを前提にした処理があります。
またArchiverクラスがapi_resourceの型として指定している、bicidata.common.GBFSResourceSnapshotsは、snapshots_informationを必須としています(取得先のサーバからそれが取れることを求めている)。

bicidata.apps.serverというモジュールもあり、これがそのサーバ機能なのかなと思ったのですが、これにはGBFSの配信する機能はなく、定期的にSnapshotを取るサーバを動かすプログラムになっています。
(やっぱりlanguage指定はできません)

archiver.pyの__main__では、`"http://35.241.203.225/gbfs.json"`をGBFSの取得先に指定し、動作するようになっています。
が、このサーバ、現在動作していません。

おそらく元はPublisherとして、Snapshotで取得したGBFSを配信するサーバ機能を実装し、Archiverはそこからデータを取得、DataFrame+xarrayで保存するつもりだったのでは？と推察します。

bicidata.apps.serverを実行したり、Archiverクラスを実装したスクリプトを動作させたわけではありませんが、ソースを読む限り実行できなさそうなので、bicidataの紹介はこれで終わりたいと思います。

なおソースは簡潔ですので、bicidata.apps.serverを日本国内のGBFS向けに実行可能に修正したりは簡単です。
もし、興味があって動かした方がいれば、ぜひQiitaに投稿ください。

# まとめ

pypiでgbfsを検索して、出てきたライブラリを実際にインストールして試してみました。

2022年12月現在では、gbfs-clientが使いやすく、動作も確実なので、おすすめです。
bicidataはGBFSのようなリアルタイムなデータを扱うのに、利便性の高い機能を提供しようとしていますが、未完成かつ実装見込みが薄いのが残念なところです。

そのほか、私の知らないPythonでGBFSを扱うライブラリがあるかもしれません。
ご存じの方はぜひQiitaに投稿ください。