「Protocol Buffersって何? 」から、「公共交通オープンデータ」を攻略する
バスのリアルタイム情報を格納するサーバを作ることになりました(経緯が色々あって)。
情報を提供しているサーバから、JSONを定期的に取りに行けばいいんだろう、とか思っていたのですが、このProtocol Bufferというデータ形式を、私は聞いたことがありません。
実際にデータを取得してセーブしてみたのですが、明らかにバイナリです。
しかも、どのエディタでも自動整形しない。ということは、どうやらテキストではない。
で、色々調べたのですが、どうも要領を得ないのですが、下の動画でやっと分かった気になりました。
乱暴に言えば、Protocol Buffersとは「圧縮されたJSON または XML(のようなもの)」です。
まあ、JSONもXMLもテキストメッセージで、しかも構造を保持したまま送信するので「通信効率悪そうだなぁ」と前々から思っていましたので、こういうものが必要となるのは分かります。
Googleが提供していることもあり、また、リアルタイム系のデータでは必要となるのは分かりますが ―― また、覚えることが増えたなぁ、と少々ウンザリしています。
とりあえず、Go言語で動かすことを目的として、ここまでの経緯を記載しておきます。
私の環境は、Windows10です。GOはインストール済みです
(1)https://protobuf.dev/downloads/ から "Latest Version" → "*-win64.zip"
をダウンロード。私はC:\の直下に展開しました。
でもって、この両方のフォルダーにpathを通しておきました。必要なのは"protoc.exe"です。
(2)私の場合、C:\Users\ebata\protocol_bufferesというディレクトリを作って、そこにソースを展開することにしました。
とりあえず、main.goという名前でファイルを作っておきます(後で必要になります)
package main
import "fmt"
func main() {
fmt.Println("Hello World!!")
}
さらに、person.proto という名前で、
syntax = "proto3";
package main;
message Person{
string name = 1;
int32 age = 2;
}
というファイルを作ります。これがxmlやらJSONのスタイルファイルにようなものです。
で、ここから、person.pb.goというファイルを作らなければならないのですが、これに苦労しました。
C:\Users\ebata\protocol_bufferesの中で、
$ protoc --go_out=. *.protoprotoc-gen-go: unable to determine Go import path for "person.proto"Please specify either:• a "go_package" option in the .proto source file, or• a "M" argument on the command line.See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.--go_out: protoc-gen-go: Plugin failed with status code 1.
のようなことを繰り返していたのですが、person.proto の中に、一行追加したら、サクっと通りました。
syntax = "proto3";
option go_package="./;main"; // 理由は分からないけど、この1行で、以下のエラーが消えた
//$ protoc --go_out=. *.proto
//protoc-gen-go: unable to determine Go import path for "person.proto"
//Please specify either:
// • a "go_package" option in the .proto source file, or
// • a "M" argument on the command line.
//See https://developers.google.com/protocol-buffers/docs/reference/go-generated#p
//ackage for more information.
//--go_out: protoc-gen-go: Plugin failed with status code 1.
package main;
message Person{
string name = 1;
int32 age = 2;
}
この結果、以下のようなperson.pb.goが生成されました。
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v4.22.2
// source: person.proto
package main
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Person struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
}
func (x *Person) Reset() {
*x = Person{}
if protoimpl.UnsafeEnabled {
mi := &file_person_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Person) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Person) ProtoMessage() {}
func (x *Person) ProtoReflect() protoreflect.Message {
mi := &file_person_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Person.ProtoReflect.Descriptor instead.
func (*Person) Descriptor() ([]byte, []int) {
return file_person_proto_rawDescGZIP(), []int{0}
}
func (x *Person) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Person) GetAge() int32 {
if x != nil {
return x.Age
}
return 0
}
var File_person_proto protoreflect.FileDescriptor
var file_person_proto_rawDesc = []byte{
0x0a, 0x0c, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04,
0x6d, 0x61, 0x69, 0x6e, 0x22, 0x2e, 0x0a, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
0x03, 0x61, 0x67, 0x65, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x3b, 0x6d, 0x61, 0x69, 0x6e, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_person_proto_rawDescOnce sync.Once
file_person_proto_rawDescData = file_person_proto_rawDesc
)
func file_person_proto_rawDescGZIP() []byte {
file_person_proto_rawDescOnce.Do(func() {
file_person_proto_rawDescData = protoimpl.X.CompressGZIP(file_person_proto_rawDescData)
})
return file_person_proto_rawDescData
}
var file_person_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_person_proto_goTypes = []interface{}{
(*Person)(nil), // 0: main.Person
}
var file_person_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_person_proto_init() }
func file_person_proto_init() {
if File_person_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_person_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Person); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_person_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_person_proto_goTypes,
DependencyIndexes: file_person_proto_depIdxs,
MessageInfos: file_person_proto_msgTypes,
}.Build()
File_person_proto = out.File
file_person_proto_rawDesc = nil
file_person_proto_goTypes = nil
file_person_proto_depIdxs = nil
}
さて、ここで、
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
)
func main() {
fmt.Println("Hello World!!")
elliot := &Person{
Name: "Elliot",
Age: 24,
}
data, err := proto.Marshal(elliot)
if err != nil {
log.Fatal("Marshalling error", err)
}
fmt.Println(data)
}
とした上で、
$ go run main.go person.pb.go
をすると、
main.go:7:2: no required module provides package google.golang.org/protobuf/prot
o: go.mod file not found in current directory or any parent directory; see 'go h
elp modules'
person.pb.go:10:2: no required module provides package google.golang.org/protobu
f/reflect/protoreflect: go.mod file not found in current directory or any parent
directory; see 'go help modules'
person.pb.go:11:2: no required module provides package google.golang.org/protobu
f/runtime/protoimpl: go.mod file not found in current directory or any parent di
rectory; see 'go help modules'というエラーがでてくるので、
$go mod init m
$ go get google.golang.org/protobuf/reflect/protoreflect
$ go get google.golang.org/protobuf/proto
$ go get google.golang.org/protobuf/runtime/protoimpl
を実行して、再度、
$ go run main.go person.pb.go
を行うと
Hello World!!
[10 6 69 108 108 105 111 116 16 24]
とい値が出力されます。
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
)
func main() {
fmt.Println("Hello World!!")
elliot := &Person{
Name: "Elliot",
Age: 24,
}
data, err := proto.Marshal(elliot)
if err != nil {
log.Fatal("Marshalling error", err)
}
fmt.Println(data)
newElliot := &Person{}
err = proto.Unmarshal(data, newElliot)
if err != nil {
log.Fatal("unmarshalling error: ", err)
}
fmt.Println(newElliot.GetAge())
fmt.Println(newElliot.GetName())
}
のプログラムを実行すると、
$ go run main.go person.pb.go
Hello World!!
[10 6 69 108 108 105 111 116 16 24]
24
Elliot
となる。
動的バス情報フォーマット(GTFSリアルタイム)ガイドライン
やっと見つけた
- データ構造:gtfs-realtime.proto
- https://developers.google.com/transit/gtfs-realtime/gtfs-realtime.proto
ここからダウンロードすると、こんな感じのprotoファイルが得られます。
で、これを以下のように修正して、
として、
$ protoc --go_out=. *.proto
を実施すると、さくっとgtfs-realtime.pb.goができました。
ここまでできれば、後はクライアントを作れば、できるはずです。
問題は、どうやってサーバにアクセスするか、を考えれば、いくはずです。
(が、基本的に最後に動くまで、どうなるかは分からないですが)。
さて、今回の私の狙いは、
https://ckan.odpt.org/dataset/b_bus_gtfs_rt-yokohamamunicipal
の、リアルタイムデータを取得して保存しておくことです。
これは、こちらに記載されているように
なので、このデータを所得するためには、「発行されたアクセストークン」というのを貰う必要があります。これ、"f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4"(江端ルールで変更済み)というやつになります。
これは、https://developer.odpt.org/ から、申請してメールで付与してもらえます(2日くらいかな)。これがないと、データの取得ができないので注意して下さい。
さて、ここから、とりあえず、この横浜市交通局の市営バスのバス関連リアルタイム情報を取得するコードを、https://gtfs.org/realtime/language-bindings/golang/ をパクって、ちょっと修正してみました。
私は、c:\Users\ebata\protocol_bufferes\gtfs-realtime というディレクトリを掘って、上記のページ通りのことを実施しました。
$ go get github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs
$ go get google.golang.org/protobuf/proto
以下のファイルを作成しました(トークンは自分のものに置き替えて下さい)。エラーの出てくるところは、コメントアウトしています。
// https://gtfs.org/realtime/language-bindings/golang/
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs"
proto "github.com/golang/protobuf/proto"
)
func main() {
var (
username = "xx@gmail.com" // 横浜市交通局の市営バスのサイトでは不要のようだからダミーを放り込んでおく
password = "xx" // (同上)
)
client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.odpt.org/api/v4/gtfs/realtime/YokohamaMunicipalBus_vehicle?acl:consumerKey=f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4", nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(username, password)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if err != nil {
log.Fatal(err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
feed := gtfs.FeedMessage{}
err = proto.Unmarshal(body, &feed)
if err != nil {
log.Fatal(err)
}
fmt.Println(feed)
/*
for _, entity := range feed.Entity {
tripUpdate := entity.TripUpdate
trip := tripUpdate.Trip
tripId := trip.TripId
fmt.Printf("Trip ID: %s\n", *tripId)
}
*/
}
で、fmt.Println(body) のところを表示するとこんな感じになっています。
[10 13 10 3 50 46 48 16 0 24 165 246 160 161 6 18 41 10 9 118 105 99 108 95 49 56 48 54 34 28 18 10 13 252 214 13 66 21 192 155 11 67 40 165 205 159 161 6 66 6 10 4 49 56 48 54 72 0 18 41 10 9 118 105 99 108 95 49 48 48 50 34 28 18 10 13 188 173 13 66 21 205 159 11 67 40 233 176 159 161 6 66 6 10 4 49 48 48 50 72 0 18 41 10 9 118 105 99 108 95 51 57 57 50 34 28 18 10 13 83 131 13 66 21 28 146 11 67 40 216 210 159 161 6 66 6 10 4 51 57 57 .....
fmt.Println(feed)は、こんな風に表示されます。
{{{} [] [] 0xc00013f800} 0 [] map[] gtfs_realtime_version:"2.0" incrementality:FULL_DATASET timestamp:1680358181 [id:"vicl_1806" vehicle:{vehicle:{id:"1806"} position:{latitude:35.459946 longitude:139.6084} timestamp:1680336549 occupancy_status:EMPTY} id:"vicl_1002" vehicle:{vehicle:{id:"1002"} position:{latitude:35.419662 longitude:139.62422} timestamp:1680332905 occupancy_status:EMPTY} id:"vicl_3992" vehicle:{vehicle:{id:"3992"} position:{latitude:35.378246 longitude:139.57074} timestamp:1680337240 occupancy_status:EMPTY} id:"vicl_1732" vehicle:{trip:{trip_id:"04117_12202301042041P01910" schedule_relationship:SCHEDULED} vehicle:{id:"1732"}.....
あとは、ここをパースすれば、必要な情報は取り出せるはずです。
さて、ここから ~/protocol_bufferes/gtfs-realtime に環境を作ってみます。
$go mod init m
$ go get google.golang.org/protobuf/reflect/protoreflect
$ go get google.golang.org/protobuf/proto
$ go get google.golang.org/protobuf/runtime/protoimpl
で、
こちらの環境でも、上記と同じ手続で、gtfs-realtime.pb.goを作り、
$ go run main.go gtfs-realtime.pb.go
を実施してみましたところ、
main.go:11:2: no required module provides package github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs; to
add it:
go get github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs
main.go:12:2: missing go.sum entry for module providing package github.com/golang/protobuf/proto; to add:
go mod download github.com/golang/protobuf
と言われたので、言われた通りのことを実施してみました
ebata@DESKTOP-P6KREM0 MINGW64 ~/protocol_bufferes/gtfs-realtime$ go get github.com/MobilityData/gtfs-realtime-bindings/golang/gtfsgo get: added github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs v1.0.0
ebata@DESKTOP-P6KREM0 MINGW64 ~/protocol_bufferes/gtfs-realtime$ go mod download github.com/golang/protobuf
$ go run main.go gtfs-realtime.pb.gogo: updates to go.mod needed; to update it:go mod tidy
$ go mod tidy
$ go run main.go gtfs-realtime.pb.gopanic: proto: file "gtfs-realtime.proto" is already registeredpreviously from: "github.com/MobilityData/gtfs-realtime-bindings/golang/gtfs"currently from: "main"See https://protobuf.dev/reference/go/faq#namespace-conflictgoroutine 1 [running]:google.golang.org/protobuf/reflect/protoregistry.glob..func1({0xea9240, 0xeb92b8}, {0xea9240, 0xc000058d70})c:/go/pkg/mod/google.golang.org/protobuf@v1.30.0/reflect/protoregistry/registry.go:56 +0x1f4(以下、色々)
$ export GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn
feed := gtfs.FeedMessage{}
err = proto.Unmarshal(body, &feed)
if err != nil {
log.Fatal(err)
}
//fmt.Println(feed)
for _, entity := range feed.Entity {
//tripUpdate := entity.TripUpdate
fmt.Println(entity)
fmt.Println()
//trip := tripUpdate.Trip
//tripId := trip.TripId
//fmt.Printf("Trip ID: %s\n", *tripId)
}
id:"vicl_1780" vehicle:{trip:{trip_id:"05406_09202304031054P00218" schedule_relationship:SCHEDULED} vehicle:{id:"1780"} position:{latitude:35.415165 longitude:139.66798} current_stop_sequence:10 stop_id:"5899_02" current_status:IN_TRANSIT_TO timestamp:1680534894 occupancy_status:EMPTY}id:"vicl_3944" vehicle:{trip:{trip_id:"03903_22202304031012A01309" schedule_relationship:SCHEDULED} vehicle:{id:"3944"} position:{latitude:35.50719 longitude:139.55861} current_stop_sequence:32 stop_id:"6219_01" current_status:IN_TRANSIT_TO timestamp:1680528244 occupancy_status:EMPTY}id:"vicl_1705" vehicle:{vehicle:{id:"1705"} position:{latitude:35.378166 longitude:139.57071} timestamp:1680522432 occupancy_status:FEW_SEATS_AVAILABLE}id:"vicl_4607" vehicle:{vehicle:{id:"4607"} position:{latitude:35.492794 longitude:139.66455} timestamp:1680526330 occupancy_status:NOT_ACCEPTING_PASSENGERS}
for _, entity := range feed.Entity {
//tripUpdate := entity.TripUpdate
//fmt.Println(entity)
// データの読み込み
uniName := *(entity.Vehicle.Vehicle.Id)
lat := *(entity.Vehicle.Position.Latitude)
lon := *(entity.Vehicle.Position.Longitude)
fmt.Println(uniName, lat, lon)
}