Ruby 文化に属している人、もしくは Unix しか使わない人にはこの感覚はわからんかもしれんけれど。
一つ前の json schema ネタの中で YAML について少しだけ言及したのでついでに。かつては ConfigParser の話の中で軽く触れたが、そこでは少々ネガティブな要素を強調した。けど、やっぱさ、これ、流行って欲しいんだ、扱いやすいのだもの。普及活動してみようかしら、と思ったわけ。
yaml と ruby は非常に仲良しである。正確な話はわからんけれど、感覚的には「yaml が使われてれば Ruby プロジェクトだし、Ruby プロジェクトであれば yaml が使われてる」印象は非常に強い。すなわち、「yaml の普及度」というのは「ruby の普及度」にかなり密接に関わっている気がしている。そもそも GitHub が ruby で書かれていることなどが理由で、GitHub の何か統合機能の設定などでは yaml は結構見かける。けれども、python が関係するプロジェクトにおいて yaml が主役として抜擢されて活用される例はまだまだ稀であるし、「WEB クライアント」の領域では「javascript が主役なので、当然 json から離れがたい」ので、なかなか yaml が普及するにはハードルが高いわけである。
ワタシは yaml が大好きだ。非常にコンパクトで読みやすく、なおかつコメントも書ける。xml に対する文句で良く言うのは「型」の扱いに関してだが、yaml も json と同じく、いくつかの組み込み型をサポートする。ゆえに「データ記述用の人間可読なシリアライズ」として大変扱いやすい。だから本当に本当に、yaml が json を駆逐するほどに流行って欲しい。けれども、現実は全然そうなってない。python での普及度に関しては pyyaml の罪が大きいのと、何よりライバルの json が「標準ライブラリとしてバンドルされる」ことが理由で yaml が躊躇されがちである。というかワタシもその理由で常に躊躇してる。なんというか yaml はその競合するライバルが強烈ということなのだよなぁ、json はあらゆる言語とのカップリングが強過ぎる。(YAML さえ標準になってくれたなら、こんなバカげたことは考えなくて良くなる。)
して。
Redis の Spatial をはじめ、cartopy からはじまった「近頃の GIS ネタ」の流れで、「geojson があるのだから、geoyaml もあって欲しいなぁ」と思ったのだが、まぁないね、全然ない。辛うじて node.js のプロジェクトで少しある程度。
先日の Redis のネタの追記にて json で結果を書き出したのだけれど、pyyaml を入れて yaml で書き出すとさ、まぁ読み書きしやすいのよね:
1 # -*- coding: utf-8 -*-
2 # redis を R-Tree インデクスみたいな気分で使ってみるとするならば。
3 import io
4 import csv
5 from datetime import date
6 import json
7 import yaml
8 import shapely.geometry as sgeom
9 import redis
10 # ↓redis georadius でヒットしたものを overpass で OSM にお問い合わせ、的なことを。
11 import overpy
12
13
14 def _load_from_geoshape_city_csv():
15 # 『歴史的行政区域データセットβ版』(CODH作成)
16 # https://geonlp.ex.nii.ac.jp/dictionary/geoshape-city/
17 def _map(rec, fields):
18 import re
19 temp = []
20 for v in rec:
21 if re.match(r"d+.d+", v):
22 temp.append(float(v))
23 elif re.match(r"d+-d+-d+", v):
24 temp.append(date(*map(int, v.split("-"))))
25 else:
26 temp.append(v)
27 return {k: v for k, v in zip(fields, temp)}
28 reader = csv.reader(
29 io.open("geoshape-city.csv", encoding="utf-8"))
30 fields = next(reader)
31 return (_map(rec, fields) for rec in reader)
32
33
34 def _build_geoshape_city_index(redis_cli):
35 # マインドとしては R-Tree の insert をしているような気分で。
36 # 実用で考えるなら geoadd に与える「lon lat name」の name としては
37 # 何かキー項目、geoshape-city なら entry_id を使い、これに紐づく
38 # 情報も引けるようにするのがベストだが、今回の例では単にわかりやすさの
39 # ために「address」を使う。(geoshape-city のミッションの性質上、
40 # これにより「すでに存在していない都市」がそうであるとわからないまま
41 # 平気でヒットしまくるが、今回の例ではそこは問わない。
42 allcol = [] # lon, lat, dat, lon, lat, dat, ...
43 for rec in _load_from_geoshape_city_csv():
44 # R-Tree とは違って MBR を考える必要はない。(というか geoadd
45 # 内部がまさに R-Tree を使ってたりするんじゃないかしらね?)
46 allcol.extend(
47 [rec["longitude"], rec["latitude"], rec["address"]])
48 #
49 redis_cli.geoadd(*(["geoshape-city"] + allcol))
50
51
52 def _overpass_query(
53 redis_cli, center_pt, radius_km_redis, buffer_deg_overpass):
54 # たとえば「仙台市の lon, lat はざっくりとしか知らない」状態
55 # の際に、そのおよそ知ってる lon, lat から「本当の仙台市代表地点」
56 # を探り当て、そしてその中心から改めて overpass API で「何かを
57 # 検索」ということ。あんまり複雑な例から試そうとするとハマるので、
58 # ここでの overpass 検索は「"religion"="shinto"」でのみ。
59 # (神社検索てこと。)
60
61 # GEORADIUS 検索結果は先頭のものだけ使うことにする。
62 founds = redis_cli.georadius(
63 "geoshape-city", center_pt[0], center_pt[1], radius_km_redis, "km",
64 "WITHDIST", "WITHCOORD")
65 if not founds:
66 return
67 # たとえば「北海道函館市, (140.72910800, 41.76871200)」
68 address, _, (rptlon, rptlat) = founds[0]
69
70 ex = buffer_deg_overpass / 2
71 # bb: lat-min,lon-min,lat-max,lon-max
72 bb = [rptlat - ex, rptlon - ex, rptlat + ex, rptlon + ex]
73 #
74 api = overpy.Overpass()
75 opresult = api.query("""
76 [timeout:60][bbox:{}, {}, {}, {}];
77 way["religion"="shinto"];
78 out;
79 >; /* recurse down */
80 out;""".format(*bb))
81 # geojson 互換の構造にしておく。
82 result = {"type": "FeatureCollection"}
83 features = result["features"] = []
84 for way in opresult.ways:
85 pts = []
86 for node in way.nodes:
87 pts.append((node.lon, node.lat))
88 if len(pts) == 1:
89 poly = sgeom.Point(pts)
90 elif len(pts) == 2:
91 poly = sgeom.LineString(pts)
92 else:
93 poly = sgeom.Polygon(pts)
94
95 r = {"geometry": sgeom.mapping(poly)}
96 r["properties"] = way.tags
97 features.append(r)
98
99 # ほんとはシンプルにコンソールにダンプしたいんだけどね、どうせ
100 # 「日本語なんかきらいだ」問題が起こるでしょ、windows では。
101 json.dump(
102 result,
103 io.open("exam_redis_result.json", "w", encoding="utf-8"),
104 ensure_ascii=False, indent=2)
105 yaml.safe_dump(
106 result,
107 io.open("exam_redis_result.yml", "w", encoding="utf-8"),
108 allow_unicode=True, indent=2)
109
110
111 def main(args):
112 redis_cli = redis.Redis(host='127.0.0.1', port=6379, db=0)
113 if not redis_cli.exists("geoshape-city"):
114 _build_geoshape_city_index(redis_cli)
115 _overpass_query(
116 redis_cli,
117 (args.rough_lon, args.rough_lat),
118 args.radius_km_redis,
119 args.buffer_deg_overpass)
120
121
122 if __name__ == '__main__':
123 import argparse
124 ap = argparse.ArgumentParser()
125 ap.add_argument("rough_lon", type=float)
126 ap.add_argument("rough_lat", type=float)
127 ap.add_argument("radius_km_redis", type=float)
128 ap.add_argument("buffer_deg_overpass", type=float)
129 args = ap.parse_args()
130 main(args)
1 {
2 "type": "FeatureCollection",
3 "features": [
4 {
5 "geometry": {
6 "type": "Polygon",
7 "coordinates": [
8 [
9 [
10 140.7557014,
11 42.9005077
12 ],
13 [
14 140.7564933,
15 42.9004441
16 ],
17 [
18 140.7564825,
19 42.9002851
20 ],
21 [
22 140.7556797,
23 42.9003129
24 ],
25 [
26 140.7557014,
27 42.9005077
28 ]
29 ]
30 ]
31 },
32 "properties": {
33 "amenity": "place_of_worship",
34 "name": "俱知安神社",
35 "religion": "shinto"
36 }
37 },
38 {
39 "geometry": {
40 "type": "LineString",
41 "coordinates": [
42 [
43 140.7853232,
44 42.6328839
45 ],
46 [
47 140.7853679,
48 42.6329023
49 ]
50 ]
51 },
52 "properties": {
53 "man_made": "torii",
54 "name": "鳥居",
55 "religion": "shinto"
56 }
57 },
58 {
59 "geometry": {
60 "type": "Polygon",
61 "coordinates": [
62 [
63 [
64 140.7869773,
65 42.5147707
66 ],
67 [
68 140.7871639,
69 42.5147696
70 ],
71 [
72 140.7871703,
73 42.5148671
74 ],
75 [
76 140.7869725,
77 42.5148707
78 ],
79 [
80 140.7869773,
81 42.5147707
82 ]
83 ]
84 ]
85 },
86 "properties": {
87 "amenity": "place_of_worship",
88 "building": "yes",
89 "religion": "shinto"
90 }
91 },
92 {
93 "geometry": {
94 "type": "Polygon",
95 "coordinates": [
96 [
97 [
98 140.8490419,
99 42.679623
100 ],
101 [
102 140.8490453,
103 42.6795687
104 ],
105 [
106 140.8490458,
107 42.6795605
108 ],
109 [
110 140.8491588,
111 42.6795642
112 ],
113 [
114 140.849155,
115 42.6796268
116 ],
117 [
118 140.8490419,
119 42.679623
120 ]
121 ]
122 ]
123 },
124 "properties": {
125 "amenity": "place_of_worship",
126 "building": "yes",
127 "name": "大原神社",
128 "religion": "shinto"
129 }
130 },
131 {
132 "geometry": {
133 "type": "Polygon",
134 "coordinates": [
135 [
136 [
137 140.9577061,
138 42.7993299
139 ],
140 [
141 140.9577034,
142 42.7992709
143 ],
144 [
145 140.9578294,
146 42.7992729
147 ],
148 [
149 140.9578321,
150 42.7993359
151 ],
152 [
153 140.9577061,
154 42.7993299
155 ]
156 ]
157 ]
158 },
159 "properties": {
160 "amenity": "place_of_worship",
161 "building": "yes",
162 "name": "伏見神社",
163 "religion": "shinto"
164 }
165 },
166 {
167 "geometry": {
168 "type": "Polygon",
169 "coordinates": [
170 [
171 [
172 140.9433672,
173 42.7795868
174 ],
175 [
176 140.9434051,
177 42.7795557
178 ],
179 [
180 140.94336,
181 42.7795261
182 ],
183 [
184 140.9433222,
185 42.7795572
186 ],
187 [
188 140.9433672,
189 42.7795868
190 ]
191 ]
192 ]
193 },
194 "properties": {
195 "amenity": "place_of_worship",
196 "building": "yes",
197 "name": "尻別神社",
198 "religion": "shinto"
199 }
200 }
201 ]
202 }
1 features:
2 - geometry:
3 coordinates:
4 - - - 140.7557014
5 - 42.9005077
6 - - 140.7564933
7 - 42.9004441
8 - - 140.7564825
9 - 42.9002851
10 - - 140.7556797
11 - 42.9003129
12 - - 140.7557014
13 - 42.9005077
14 type: Polygon
15 properties:
16 amenity: place_of_worship
17 name: 俱知安神社
18 religion: shinto
19 - geometry:
20 coordinates:
21 - - 140.7853232
22 - 42.6328839
23 - - 140.7853679
24 - 42.6329023
25 type: LineString
26 properties:
27 man_made: torii
28 name: 鳥居
29 religion: shinto
30 - geometry:
31 coordinates:
32 - - - 140.7869773
33 - 42.5147707
34 - - 140.7871639
35 - 42.5147696
36 - - 140.7871703
37 - 42.5148671
38 - - 140.7869725
39 - 42.5148707
40 - - 140.7869773
41 - 42.5147707
42 type: Polygon
43 properties:
44 amenity: place_of_worship
45 building: 'yes'
46 religion: shinto
47 - geometry:
48 coordinates:
49 - - - 140.8490419
50 - 42.679623
51 - - 140.8490453
52 - 42.6795687
53 - - 140.8490458
54 - 42.6795605
55 - - 140.8491588
56 - 42.6795642
57 - - 140.849155
58 - 42.6796268
59 - - 140.8490419
60 - 42.679623
61 type: Polygon
62 properties:
63 amenity: place_of_worship
64 building: 'yes'
65 name: 大原神社
66 religion: shinto
67 - geometry:
68 coordinates:
69 - - - 140.9577061
70 - 42.7993299
71 - - 140.9577034
72 - 42.7992709
73 - - 140.9578294
74 - 42.7992729
75 - - 140.9578321
76 - 42.7993359
77 - - 140.9577061
78 - 42.7993299
79 type: Polygon
80 properties:
81 amenity: place_of_worship
82 building: 'yes'
83 name: 伏見神社
84 religion: shinto
85 - geometry:
86 coordinates:
87 - - - 140.9433672
88 - 42.7795868
89 - - 140.9434051
90 - 42.7795557
91 - - 140.94336
92 - 42.7795261
93 - - 140.9433222
94 - 42.7795572
95 - - 140.9433672
96 - 42.7795868
97 type: Polygon
98 properties:
99 amenity: place_of_worship
100 building: 'yes'
101 name: 尻別神社
102 religion: shinto
103 type: FeatureCollection
そもそも yaml って読みやすい、と思わん? そしてね、「読み書き」と言った通りなわけ。たとえば長い文字列を記述しなければならなくなってもこういう書き方が許される、とか結構至れてて尽くせてる。それと、ポータビリティは少し落ちる可能性は出てくるものの、「固有型」を指示することも出来て、たとえば一つ前のスクリプトで “geometry” の値として sgeom.mapping の値ではなく shapely のオブジェクトそのものを渡し、safe_dump ではなく dump を使った場合:
1 features:
2 - geometry: !!python/object/apply:shapely.geometry.polygon.Polygon
3 state: !!binary |
4 AQMAAAABAAAABQAAAFPRs7QumGFA6aUY1kNzRUCU5G8xNZhhQCawlMBBc0VA4q/JGjWYYUC9ycqK
5 PHNFQAm4MYcumGFAXtX+cz1zRUBT0bO0LphhQOmlGNZDc0VA
6 properties:
7 amenity: place_of_worship
8 name: 俱知安神社
9 religion: shinto
10 - geometry: !!python/object/apply:shapely.geometry.linestring.LineString
11 state: !!binary |
12 AQIAAAACAAAASJkeXiGZYUAdVfJWAlFFQIG63LshmWFAFghM8QJRRUA=
13 properties:
14 man_made: torii
15 name: 鳥居
16 religion: shinto
17 - geometry: !!python/object/apply:shapely.geometry.polygon.Polygon
18 state: !!binary |
19 AQMAAAABAAAABQAAADjGBOsumWFAMricAeRBRUDw4lhyMJlhQM18YvjjQUVAPtzEfzCZYUCnJEYq
20 50FFQD7L8+AumWFAlhV5SOdBRUA4xgTrLplhQDK4nAHkQUVA
21 properties:
22 amenity: place_of_worship
23 building: 'yes'
24 religion: shinto
25 - geometry: !!python/object/apply:shapely.geometry.polygon.Polygon
26 state: !!binary |
27 AQMAAAABAAAABgAAAOEt61krm2FAAU7v4v1WRUBSigxhK5thQE/xbhv8VkVAzPkYYiubYUCjk6XW
28 +1ZFQIpjE08sm2FAJkSv9ftWRUCERxtHLJthQBm+zwL+VkVA4S3rWSubYUABTu/i/VZFQA==
29 properties:
30 amenity: place_of_worship
31 building: 'yes'
32 name: 大原神社
33 religion: shinto
34 - geometry: !!python/object/apply:shapely.geometry.polygon.Polygon
35 state: !!binary |
36 AQMAAAABAAAABQAAAMBVQ4elnmFAg5sxcVBmRUCUyJmBpZ5hQH8SRIJOZkVAt4TXiaaeYUAfCguT
37 TmZFQOMRgY+mnmFAZoKGo1BmRUDAVUOHpZ5hQIObMXFQZkVA
38 properties:
39 amenity: place_of_worship
40 building: 'yes'
41 name: 伏見神社
42 religion: shinto
43 - geometry: !!python/object/apply:shapely.geometry.polygon.Polygon
44 state: !!binary |
45 AQMAAAABAAAABQAAANADaRAwnmFAVzIRgMljRUAmbORfMJ5hQIh0LnvIY0VAWYtPATCeYUBw8OCC
46 x2NFQOjSCbIvnmFAQK7Dh8hjRUDQA2kQMJ5hQFcyEYDJY0VA
47 properties:
48 amenity: place_of_worship
49 building: 'yes'
50 name: 尻別神社
51 religion: shinto
52 type: FeatureCollection
このようにシリアライズされる。ポータビリティはない、けれども、これは場合によっては非常に便利である。
そう、とても良いものなわけよ、yaml。ほんと、爆発的に流行ってくんないかねぇ…。(10年待っても今のこの状態なので、あまり期待出来ない気もするけれどさ…。)
様式美を好む巨大組織は GML3 にしがみつくわけだけれど、おそらく関わってるエンジニアが疲弊してると思うよ。なんであぁまで複雑怪奇なものを使って疑問に思わんのだろう? てのは、間違いなく「アーキテクトがクラス図の美しさを求めるから」ね。中身なんかどうでもいいのだ。
おそらく現時点での既存のもので GIS のデータ構造を扱う場合の理想は、「データ交換を人間可読フォーマットで行いたい場合は、コンテナを yaml とし、構造は geojson 互換」「人間可読である必要がない場合は msgpack や protocol_buffer 内に geojson 互換構造」であって、今のような「GML や geojson と shapefile」ではないんだよね。
ワタシが初めて YAML を知ったのも、2011年頃で、これはまさにワタシが「Python や Ruby など、スクリプティングとして採用するなら「ワレワレのプロジェクトにとって」何が相応しいか」を、正規の仕事として検証したのと同じタイミング。というかその検証の過程で見つけたんだったかもしれない、少し記憶曖昧だけど。その初対面時点からずっと感じてるのは、(reStructuredText と同じく)思想がかなり Python に似ていて、非常に Python にフィットする、てことだったんだよね。
「Python が苦手とすること」と「YAML が苦手とすること」の共通点もある。「インライン記述にはあまり向かない」。bash などのコマンドラインから「スクリプトファイルを与えるのではなくスクリプト文字列を渡して使う」場合、こうした高級言語の中ではたぶん perl が一番これをするのは快適である:
1 [me@host: ~]$ ls | perl -e 'while (<>) { print $_ if /^x.*\.py$/ }'
この「デメリット」は、「人間が読み書きしやすい書き方しか許さない」ことから来る制約なので、「制約だとは考えないもんね」と割り切るのが正解。ともあれそういう「思想的類似」により、python コミュニティが真っ先に飛びついてくれたら良かったのに、と思うのだよなぁ。けれども飛びついたのは Guido ではなく松本のほうだった。かなり意外なんだよなこれ。(「K-R C でなければならない」みたいな役に立たないこだわりを人に強制するようなヒトなのに。)
こんなバカげたなことと言ったが、結局のところは「YAML を諦める」場合はこのバカげたことか 折衷案かがせいぜいの最善なのだけれど、やっぱね、設定ファイルとして YAML を採用することを考えるだけで、一気に世界が平和になると思うのだよね。そういう意味では、(json schema も盛り込んで)こういうのがいいのかもしれないと思う:
1 # -*- coding: utf-8 -*-
2 import io
3 import json # json は標準ライブラリに含まれている。
4
5
6 def _readmyconfig(cfgfn, schema=None):
7 if cfgfn.endswith(".json"):
8 # 何かしら template エンジンを前提にするのも手。
9 # (「コメントアウト」のためとか。)
10 loaded = json.load(io.open(cfgfn, encoding="utf-8"))
11 else:
12 # 利用者には出来れば yaml をインストールしてもらってその良さを
13 # 知ってほしいが、強制はしたくない。
14 try:
15 import yaml
16 except ImportError:
17 raise EnvironmentError(
18 "cannot load '{}'. please consider to install pyyaml.".format(
19 cfgfn))
20 loaded = yaml.load(
21 io.open(cfgfn, encoding="utf-8"),
22 Loader=yaml.loader.FullLoader)
23 #
24 # 設定ファイルのフォーマットを強制出来たら、それを使うスクリプト
25 # にとっては非常にありがたい。これを json schema でバリデート
26 # すると便利なんじゃないか、て発想。
27 if schema:
28 try:
29 from jsonschema import (
30 validate,
31 # この「いつまでも draft」なのがちょっと困るは困る。
32 # こういう柔軟性の担保を考えるなら、こうやって
33 # 内部に閉じちゃわない方がいいはいいんだけれどね。
34 # (これは validator についても同じ。draft3~7
35 # まで選べる、最新バージョンでは。)
36 draft7_format_checker
37 )
38 validate(
39 instance=loaded, schema=schema,
40 format_checker=draft7_format_checker)
41 except ImportError:
42 return loaded
43 return loaded
44
45
46 if __name__ == '__main__':
47 schema = {
48 "title": "Point2D",
49 "type": "object",
50 "properties": {
51 "x": {
52 "type": "number"
53 },
54 "y": {
55 "type": "number"
56 },
57 "label": {
58 "type": "string"
59 }
60 }
61 }
62
63 print(_readmyconfig("a.json", schema=schema))
64 print(_readmyconfig("a.yaml", schema=schema))
YAML よ流行れ、というなら、きっとワタシがこういう形で率先して使うようにすべきなのだろうなぁ。今度からそうする? (ネタの内容次第だなこれは。)
まぁ、プラットフォームを Unix に限ったり、あるいは GitHub べったりの人たちからすれば、わりかし「流行ってると言えなくもない」のではあるけれどもね。結構ツール類の設定ファイルとして yaml が採用されるのは見かける。けどさ、Windows ユーザに徹してれば少なくとも「yaml が大流行していますっ」と言われたとしても誰一人として信じないだろう。
良いものが流行るわけではない、ということについては、日本人は「VHS vs ベータ」で学んだんだと思う。ワタシにとっては「plan9」なんかもそうなんだけれど、最近の興味は Go の行く末かな。
その Go と同じくらい、yaml の未来は気になる。お願いだから流行って…。ま、「spatial な yaml」はその先の話だ。(というか今回例にしたレベルであれば「__geo_interface__ を理解するかどうか」だけなので、何か特別なものが必要だ、てことではなくて、その先のもの、たとえば「検索」とかね。XML の XPATH もそうなんだけれど、要は「GIS を解する YPATH」ね、そういうものが作られるかどうか、てこと。)
ちなみに、「Ruby の普及度」の話。うーんいつのまにこんなことに。ともかく…、YAML の普及が Ruby 人気の翳りに影響されないでくれるといいなぁ、って思う、ってハナシ。思い返してみれば、json だって、これほどまでに至るところで使われるようになるまでは10年かかってるからね、気長に待つしかないのも事実、てことだわな。
2021-10-24追記:
emacs 使いがどれだけいるのかはわからんし、そうであるなら自力でどうにかするんだろうよ、なので書くまでもないかなぁとも思ったのだが一応: yaml-mode.el