流行ればいいのに、YAML

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)
exam_redis_result.json
  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 }
exam_redis_result.yml
  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 が一番これをするのは快適である:

これと同じことをする python ワンライナーを書くのはとても大変。
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



Related Posts