🚌

淡路島発着の高速バス検索サービス「GO TO AWAJI」をリリースした話

2022/03/15に公開約4,500字4件のコメント

はじめに

2022年1月1日に本州と淡路島を結ぶ公共交通機関(高速バスとフェリー)を検索するサービス「GO TO AWAJI」をリリースしました。
本稿ではこのサービスの開発経緯やシステム構成、またサービスの開発を通して筆者が学んだことを紹介させて頂きます。

https://gotoawaji.com/

GO TO AWAJI のスクリーンキャプチャ
GO TO AWAJI のスクリーンキャプチャ

淡路島とは?

淡路島は瀬戸内海東部に位置する島です。
陸路では本州とは明石海峡大橋、四国とは大鳴門橋でそれぞれ結ばれています。陸路における公共交通機関は高速バスのみとなっています。海路では明石と淡路島を結ぶフェリーが存在します。
淡路島
淡路島(Wikipediaより)

なぜ「GO TO AWAJI」を作ったか?

筆者が満足する高速バスの検索サービスが欲しかったからです。
本州と淡路島を結ぶ高速バスを運行している会社は現在3社(共同運行を含めると5社)存在します。
利用したい高速バスを探すことに関し、筆者は以下の点に不満を感じていました。

  • Webに公開されている時刻表が探しにくい
    • 時刻表の検索機能があるバス会社のサイトは1社のみ
    • 時刻表がデスクトップ版のみであったり、PDFであったりして探しにくい(特にモバイルデバイスにおいて)
  • Webサイトがモバイルファーストではない
    • バス会社によってはモバイルデバイスでのサイトのパフォーマンスがあまり良くない
    • バス会社によってはサイト自体がデスクトップ版のみである
  • バス乗り場の間違いが起きる
    • 運行会社により発着所が異なるバス乗り場があり、間違いが起きやすい(筆者は何度か間違えたことがあります)

また既存の検索サービスにも筆者の希望に叶うものがなかったため、以下を要件に検索サービスを開発することにしました。

  • 各バス会社の時刻表を横断的に検索できる
  • モバイルファーストである
  • ユーザフレンドリーである(発着所の間違いを無くしたい)

さらにコロナ禍という状況下では、社会情勢によりバスの減便・復便がしばしば発生します。そのため、ダイヤ変更に素早く対応できることも要件としました。

GO TO AWAJI のシステム構成

GO TO AWAJI のアーキテクチャにはJamstackを採用しました。具体的には以下の3つのシステムから構成されています。

名称 概要
フロントエンド バックエンドから時刻表データを取得してユーザーに表示する
バックエンド 時刻表データを返却するAPI
クローラー 各バス会社の最新情報を取得して開発者に通知するボット

GO TO AWAJI のアーキテクチャ
GO TO AWAJI のアーキテクチャ

フロントエンド

フロントエンドはバックエンドから時刻表データを取得してユーザーに表示します。
フレームワークには Next.js を採用し、Netlify にホストしています。(最初は Firebase Hosting に静的ファイルをホストしていましたが、next export コマンドが i18n-routing をサポートしていなかったことから、多言語対応をする際に Netlify に変更しました。)

バックエンド

バックエンドは時刻表データを返却するAPIです。
事前に各バス会社から時刻表情報をスクレイピングして取得し、CSVファイルに整形して保存しています。バックエンドはこのCSVファイルを元に時刻表データを返却します。
ダイヤ改正などで時刻表に変更があった場合は、再度スクレイピングをしてCSVファイルを更新します。
開発言語には Python を採用しました。使いやすいWEBスクレイピング用ライブラリ(Beautiful Soup)や表形式データ扱うのに便利なライブラリ(pandas)が、バックエンドの開発に適していたためです。
フレームワークには FastAPI を採用しました。 インフラには GCP を採用し、 Cloud Run 上で動作させています。

クローラー

クローラーは各バス会社の最新情報を取得して開発者に通知するボットです。
こちらもフレームワークには FastAPI を採用し、GCP の Cloud Run 上で動作させています。
GCP の Cloud Scheduler で定期的に実行して、各バス会社の新着情報を監視しています。
新着情報があった場合には、 SendGrid で筆者宛にメール通知をするようにしています。
新着情報に時刻表更新のお知らせがあった場合には、バックエンドの時刻表CSVファイルの更新を行います。

時刻表の検索方法

pandas を使用すると時刻表の検索を簡単に行うことができます。
例として以下のような時刻表データを表すデータフレーム df があったとします。
ただし NaN はそのバス停にバスが停車しないことを表すとします。

バス停 路線1 路線2 路線3
A 1:00 2:00 NaN
B 1:30 NaN 3:00
C 2:00 3:00 NaN
D 2:30 3:30 4:00

このとき以下のようにしてバス停Bからバス停Dへの時刻表を取得することができます。

  1. 「バス停」列をインデックスに指定し、転置を取ります。
df = df.set_index("バス停")  # 転置後「バス停」列がヘッダーとなる
df = df.T

このときデータフレーム df は以下のようになります。

バス停 A B C D
路線1 1:00 1:30 2:00 2:30
路線2 2:00 NaN 3:00 3:30
路線3 NaN 3:00 NaN 4:00
  1. 次にバス停Bとバス停Dのカラムを抽出します。
df = df[["B", "D"]]

このときデータフレーム df は以下のようになります。

バス停 B D
路線1 1:30 2:30
路線2 NaN 3:30
路線3 3:00 4:00
  1. 次に値が NaN でない行のみを抽出します。
df = df[df["B"].notna() & df["D"].notna()]

このときデータフレーム df は以下のようになります。

バス停 B D
路線1 1:30 2:30
路線3 3:00 4:00
  1. 最後に再び転置を取ると、バス停Bからバス停Dへの時刻表が得られます。
df = df.T
バス停 路線1 路線3
B 1:30 3:00
D 2:30 4:00

GO TO AWAJI の開発サイクル

CI/CD

コードは GitLab にホストしています。GitLab CI/CD を使用し、リポジトリへ push するとデプロイが自動的に実行されるようにしています。

開発スタイル

個人開発ではありますが、アジャイル的な手法で開発を行いました。
筆者が2021年12月末にまとまった時間を取ることができたため、集中して開発を行い、約1.5週間でMVP(Minimum Viable Product)版をリリースしました。(2022年1月1日)
MVP版では最低限の高速バスの検索機能のみを実装しました。この時はフロントエンドとバックエンドのみ実装し、クローラーはまだ存在しませんでした。
リリース後親しい友人たちにサービスを触ってもらい、フィードバックを元に改善と機能追加を行いました。 デザインの改善と機能追加(フェリーの検索機能の追加、料金表対応、Google Map 対応、多言語対応、クローラーの追加など)を経て、2ヶ月程度で現在の状態となりました。

おわりに

今回のサービス開発を通して多くの学びを得ることが出来ました。特にMVP版に対して貴重なフィードバックをくれた友人たちに感謝しています。フィードバックから自分では思い浮かばなかった気づきを得ることが出来ました。

GO TO AWAJI では本州と淡路島を結ぶ高速バスとフェリーの検索をサポートしていますが、以下には対応出来ていません。

  • 四国と淡路島を結ぶ高速バス
  • 淡路島島内のバス

また、淡路島でレンタカーやレンタサイクルを利用して移動するケースも考えられます。対応する交通手段の範囲を広げ、淡路島への快適なアクセスを提供できるサービスを目指して行きたいと思います。

Discussion

コロナが落ち着いたら淡路島観光をしたいなと考えているので、その際は使わせていただきます!

一点質問なのですが、「時刻表情報をスクレイピングして取得し、CSVファイルに整形して保存」の処理自体はクローラーとは異なり手動で行っているのでしょうか?
「新着情報から時刻表更新のお知らせを検知」→「時刻表スクレイピング、CSV更新」までバッチ化できれば、メンテコストを抑えられそうだなと感じました。
※とはいえ「時刻表のフォーマットが今後変わらないとも限らない」や「お知らせの中から時刻表更新に関わるものだけ検知するのが難しい」等々あるので、全自動は怖い気もしますが・・・

nekoniki さん

コメントありがとうございます!ご利用いただけると大変励みになります!淡路島観光はぜひ楽しんでください!

一点質問なのですが、「時刻表情報をスクレイピングして取得し、CSVファイルに整形して保存」の処理自体はクローラーとは異なり手動で行っているのでしょうか?
「新着情報から時刻表更新のお知らせを検知」→「時刻表スクレイピング、CSV更新」までバッチ化できれば、メンテコストを抑えられそうだなと感じました。
※とはいえ「時刻表のフォーマットが今後変わらないとも限らない」や「お知らせの中から時刻表更新に関わるものだけ検知するのが難しい」等々あるので、全自動は怖い気もしますが・・・

紛らわしくて失礼しました。「時刻表情報をスクレイピングして取得し、CSVファイルに整形して保存」する部分も Python のスクリプトで自動化しています。しかしながら、このスクリプトは手動で実行しております。ご指摘いただいた通りの懸念事項を考慮してのことです。

現状時刻表CSVの更新作業は以下のように実施しております。

「新着情報から時刻表更新のお知らせを確認」→「スクリプトを実行し、時刻表スクレイピング、CSV作成(手動実行)」→「差分を確認(目視 + ツール)、CSVを更新」

しかしながら、CSVファイル自体も git で管理していますので、以下のようにバッチ処理でPRを作成するプロセスを含めることで自動化できそうに思いました!こちらまた検討してみたいと思います!

「新着情報から時刻表更新のお知らせを検知」→「時刻表スクレイピング、CSV作成」→「PRを作成」→「差分を確認、CSVを更新」

返信ありがとうございます!

紛らわしくて失礼しました。「時刻表情報をスクレイピングして取得し、CSVファイルに整形して保存」する部分も Python のスクリプトで自動化しています。しかしながら、このスクリプトは手動で実行しております。ご指摘いただいた通りの懸念事項を考慮してのことです。

なるほど、理解しました。

しかしながら、CSVファイル自体も git で管理していますので、以下のようにバッチ処理でPRを作成するプロセスを含めることで自動化できそうに思いました!こちらまた検討してみたいと思います!

PR作成までバッチ化できるとだいぶ楽になりそうですね。
思いつかなかった手法なのでたいへん参考になります!

とんでもないです!
コメントを頂けたからこそ、このアイデアが思い浮かびました!ありがとうございました!

ログインするとコメントできます