08-23追記: ここにあげたコードは bitbucket の公開レポジトリとしてあげた。少しだけインターフェイス変えてます。
緯度から気温なんかわからんかなぁ、なんて少しだけ探ってみるも、そりゃ無謀なのな。宇宙空間の気温だったら、solar radiation だけで求まっちゃうけれど、大気圏内はそうはいかぬ。6月の南中高度が最も高いのに気温のピークが7~8月に来るのは水の比熱の関係だよ、とかね。エラく楽しそうな計算だなぁ、とちょっと惹かれはしたけど、無理無理。
とちょっとばかし頭を巡らせてて、そういや METAR があったなと。例によって NOAA が API サービスしている。知らない人のために…というかほとんどの人は知らんと思うけど…、これは、「定時飛行場実況気象通報式」といって、基本的に航空関係者向けの気象通報。かなり歴史あるはず。(なお、METAR とその他空港関係補助情報をくっつけた ATIS てのもある。これは基本的に「放送」。一般人でも「聞く気になれば」聞けるけど普通は聞けない。)
昔から使われているものであるし、何十年も前のハードウェア向けに/あるいは「訓練されたスペシャリストが読めればいいという割り切り」で、かなり独特な「電文」になってて、初めて読む人には暗号にしかみえない:
1 RJTT 221100Z 20017KT 9999 -SHRA FEW015 BKN100 29/25 Q1000 NOSIG
1 RJAA 221100Z 21014KT 170V250 9999 -SHRA FEW015 FEW030CB SCT100 BKN140 27/25 Q1000 NOSIG
全部を読める必要がある人とない人がいるわけで、「ぱんぴー」はまずは気温/露点温度のとこだけ知っとくだけでも「はじめられる」。27/25 が「気温℃/露点温度℃」の意味。(ただ、負数は -25 とかではなくて M25。)
さて。NOAA の API サービスで好きなだけ METAR お取り寄せできるのはいい。なんだけど、「XML 化」であれ「JSON 化」であれ、なんか中途半端なのな。全情報を抜き出してるわけではなくて、なので「RMK 読みたきゃ自力で raw_text 読め!」と突如どん底、てわけだ。
なのでなんかインフラないかなぁ、と、python-metarを見つけた。が…、動かす前から既に気に入らない。そもそも依存物が多過ぎる:
- matplotlib
- pandas
- six
- pip
- nose
- ipython-notebook
- openpyxl
six, pip, nose はいいとしても、METAR 解析だけのために pandas はなかろ? PC で使うぶんにはいいんだけど、あたしゃ android スマホで使いたいのだ。それだけじゃなくて、metar.py の作りが気に入らない。
実際問題 METAR/SPECI/TAF とかを「機械処理」するのにキツいのは「フィールド分解」のほうであって、「OVC は overcast の意味なんだぜ」なんてことは別に「あとでいい」のだ。慣れりゃこんなん憶えてられるもんだから。なのに metar.py はここで全部の「human readable」化を頑張っちゃう。このさぁ、「_handleXxx」を外から差し込めるようにしたら? など、ソフトウェアとしてもちょっと気に喰わない。
ちょっとダルいかなぁ、とは思ったんだけれども、ただ、そもそも書かれてる正規表現も仕様の網羅性もざっと見問題はなさそうなので、基本正規表現だけぱくって全書き換えはすぐに出来るだろうし、何せ http://weather.noaa.gov/pub/data/observations/metar/cycles/01Z.TXT (01Z 部分は 00~23) でテストデータが簡単に手に入る。
というわけで…実はほんの少しだけ(オリジナルに引きずられて)迷走したんだけれど、もう思い切ってばっさり単純化した:
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (c) 2016, hhsprings <https://bitbucket.org/hhsprings>
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # - Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 #
13 # - Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in
15 # the documentation and/or other materials provided with the
16 # distribution.
17 #
18 # - Neither the name of the hhsprings nor the names of its contributors
19 # may be used to endorse or promote products derived from this software
20 # without specific prior written permission.
21 #
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
24 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
25 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
26 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 #
34 # =======================================================================
35 #
36 # This script is almost all based on
37 # python-metar <https://github.com/phobson/python-metar> by Tom Pollard,
38 # but my version is very simple to avoid depend on many third party
39 # libraries.
40 #
41 # Explanations from python-metar:
42 # --------------------------------------------------------------
43 # US conventions for METAR/SPECI reports are described in chapter 12 of
44 # the Federal Meteorological Handbook No.1. (FMH-1 1995), issued by NOAA.
45 # See <http://metar.noaa.gov/>
46 #
47 # International conventions for the METAR and SPECI codes are specified in
48 # the WMO Manual on Codes, vol I.1, Part A (WMO-306 I.i.A).
49 #
50 # This module handles a reports that follow the US conventions, as well
51 # the more general encodings in the WMO spec. Other regional conventions
52 # are not supported at present.
53 #
54 # The current METAR report for a given station is available at the URL
55 # http://weather.noaa.gov/pub/data/observations/metar/stations/<station>.TXT
56 # where <station> is the four-letter ICAO station code.
57 #
58 # The METAR reports for all reporting stations for any "cycle" (i.e., hour)
59 # in the last 24 hours is available in a single file at the URL
60 # http://weather.noaa.gov/pub/data/observations/metar/cycles/<cycle>Z.TXT
61 # where <cycle> is a 2-digit cycle number (e.g., "00", "05" or "23").
62 # --------------------------------------------------------------
63 #
64 # =======================================================================
65 #
66 #
67 #
68 #
69 import re
70
71 # ---------------------------------------------------------------------------
72 #
73 # Module globals
74 #
75
76 # regular expressions for header informations
77 _HEADERS_RGXES = [
78 ("type", re.compile(r"^(?P<type>METAR|SPECI)\s+")),
79 ("station", re.compile(r"^(?P<station>[A-Z][A-Z0-9]{3})\s+")),
80 ("time", re.compile(r"""^(?P<time>\d\d\d\d\d\dZ?)\s+""")),
81 ("modifier", re.compile(r"^(?P<mod>AUTO|FINO|NIL|TEST|CORR?|RTD|CC[A-G])\s+")),
82 ]
83
84 # common regexp
85 _WIND_RGX = re.compile(r"""^(?P<dir>[\dO]{3}|[0O]|///|MMM|VRB)
86 (?P<speed>P?[\dO]{2,3}|[0O]+|[/M]{2,3})
87 (G(?P<gust>P?(\d{1,3}|[/M]{1,3})))?
88 (?P<units>KTS?|LT|K|T|KMH|MPS)?
89 (\s+(?P<varfrom>\d\d\d)V
90 (?P<varto>\d\d\d))?\s+""",
91 re.VERBOSE)
92 _VISIBILITY_RGX = re.compile(r"""^(?P<vis>(?P<dist>(M|P)?\d\d\d\d|////)
93 (?P<dir>[NSEW][EW]? | NDV)? |
94 (?P<distu>(M|P)?(\d+|\d\d?/\d\d?|\d+\s+\d/\d))
95 (?P<units>SM|KM|M|U) |
96 CAVOK )\s+""",
97 re.VERBOSE)
98 _WEATHER_RGX = re.compile(r"""^(?P<int>(-|\+|VC)*)
99 (?P<desc>(MI|PR|BC|DR|BL|SH|TS|FZ)+)?
100 (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP|/)*)
101 (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
102 (?P<other>PO|SQ|FC|SS|DS|NSW|/+)?
103 (?P<int2>[-+])?\s+""",
104 re.VERBOSE)
105 _SKY_RGX = re.compile(r"""^(?P<cover>VV|CLR|SKC|SCK|NSC|NCD|BKN|SCT|FEW|[O0]VC|///)
106 (?P<height>[\dO]{2,4}|///)?
107 (?P<cloud>([A-Z][A-Z]+|///))?\s+""",
108 re.VERBOSE)
109 _COLOR_RGX = re.compile(r"""^(BLACK)?(BLU|GRN|WHT|RED)\+?
110 (/?(BLACK)?(BLU|GRN|WHT|RED)\+?)*\s*""",
111 re.VERBOSE)
112
113 # [(name, rgx, repeatable), ...]
114 _BASEINFO_RGXES = [
115 #
116 ("wind", _WIND_RGX, False),
117
118 #
119 ("visibility", _VISIBILITY_RGX, True),
120
121 #
122 ("RVR", re.compile(r"""^(RVRNO |
123 R(?P<name>\d\d(RR?|LL?|C)?)/
124 (?P<low>(M|P)?\d\d\d\d)
125 (V(?P<high>(M|P)?\d\d\d\d))?
126 (?P<unit>FT)?[/NDU]*)\s+""",
127 re.VERBOSE), True),
128
129 #
130 ("weather", _WEATHER_RGX, True),
131
132 #
133 ("sky", _SKY_RGX, True),
134
135 #
136 ("temp/dewp", re.compile(r"""^(?P<temp>(M|-)?\d+|//|XX|MM)/
137 (?P<dewpt>(M|-)?\d+|//|XX|MM)?\s+""",
138 re.VERBOSE), False),
139
140 #
141 ("qnh", re.compile(r"""^(?P<unit>A|Q|QNH|SLP)?
142 (?P<press>[\dO]{3,4}|////)
143 (?P<unit2>INS)?\s+""",
144 re.VERBOSE), True),
145
146 #
147 ("recent weather", re.compile(r"""^RE(?P<desc>MI|PR|BC|DR|BL|SH|TS|FZ)?
148 (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP)*)?
149 (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
150 (?P<other>PO|SQ|FC|SS|DS)?\s+""",
151 re.VERBOSE), True),
152 #
153 ("wind shear", re.compile(r"^(WS\s+)?(ALL\s+RWY|RWY(?P<name>\d\d(RR?|L?|C)?))\s+"), True),
154
155 # what is this? i don't know... (hhs)
156 ("color", _COLOR_RGX, True),
157
158 #
159 ("runway state", re.compile(r"""((?P<name>\d\d) | R(?P<namenew>\d\d)(RR?|LL?|C)?/?)
160 ((?P<special> SNOCLO|CLRD(\d\d|//)) |
161 (?P<deposit>(\d|/))
162 (?P<extent>(\d|/))
163 (?P<depth>(\d\d|//))
164 (?P<friction>(\d\d|//)))\s+""",
165 re.VERBOSE), True),
166 ]
167
168 # trend
169 _TREND_RGX = re.compile(r"^(?P<trend>TEMPO|BECMG|FCST|NOSIG)\s*")
170
171 _TRENDINFO_RGXES = [
172 ("trend time", re.compile(r"(?P<when>(FM|TL|AT))(?P<hour>\d\d)(?P<min>\d\d)\s+"), True),
173 # FIXME: maybe not ok... (wind=7000?)
174 ("wind", _WIND_RGX, True),
175 ("visibility", _VISIBILITY_RGX, True),
176 ("weather", _WEATHER_RGX, True),
177 ("sky", _SKY_RGX, True),
178 ("color", _COLOR_RGX, True),
179 ]
180
181 # remarks
182 _REMARK_RGX = re.compile(r"^(RMKS?|NOSPECI|NOSIG)\s*")
183
184 _REMARKINFO_RGXES = [
185 ("auto", re.compile(r"^AO(?P<type>\d)\s+")),
186 ("sealvl press", re.compile(r"^SLP(?P<press>\d\d\d)\s+")),
187 ("peak wind", re.compile(r"""^P[A-Z]\s+WND\s+
188 (?P<dir>\d\d\d)
189 (?P<speed>P?\d\d\d?)/
190 (?P<hour>\d\d)?
191 (?P<min>\d\d)\s+""",
192 re.VERBOSE)),
193 ("wind shift", re.compile(r"""^WSHFT\s+
194 (?P<hour>\d\d)?
195 (?P<min>\d\d)
196 (\s+(?P<front>FROPA))?\s+""",
197 re.VERBOSE)),
198 ("precipitation 1hr", re.compile(r"^P(?P<precip>\d\d\d\d)\s+")),
199 ("precipitation 24hr", re.compile(r"""^(?P<type>6|7)
200 (?P<precip>\d\d\d\d)\s+""",
201 re.VERBOSE)),
202 ("press 3hr", re.compile(r"""^5(?P<tend>[0-8])
203 (?P<press>\d\d\d)\s+""",
204 re.VERBOSE)),
205 ("temp 1hr", re.compile(r"""^T(?P<tsign>0|1)
206 (?P<temp>\d\d\d)
207 ((?P<dsign>0|1)
208 (?P<dewpt>\d\d\d))?\s+""",
209 re.VERBOSE)),
210 ("temp 6hr", re.compile(r"""^(?P<type>1|2)
211 (?P<sign>0|1)
212 (?P<temp>\d\d\d)\s+""",
213 re.VERBOSE)),
214 ("temp 24hr", re.compile(r"""^4(?P<smaxt>0|1)
215 (?P<maxt>\d\d\d)
216 (?P<smint>0|1)
217 (?P<mint>\d\d\d)\s+""",
218 re.VERBOSE)),
219 ("lightning", re.compile(r"""^((?P<freq>OCNL|FRQ|CONS)\s+)?
220 LTG(?P<type>(IC|CC|CG|CA)*)
221 ( \s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
222 [NSEW][EW]? (-[NSEW][EW]?)* )+) )?\s+""",
223 re.VERBOSE)),
224 ("ts loc", re.compile(r"""TS(\s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
225 [NSEW][EW]? (-[NSEW][EW]?)* )+))?
226 ( \s+MOV\s+(?P<dir>[NSEW][EW]?) )?\s+""",
227 re.VERBOSE)),
228 ("snowdepth", re.compile(r"""^4/(?P<snowdepth>\d\d\d)\s+""")),
229 ("__unk", re.compile(r"(?P<group>\S+)\s+")),
230 ]
231
232
233 # ------------------------------------------------
234
235 def metar_splitter(code):
236 def _compactdict(d):
237 return {k: v for k, v in d.items() if v is not None}
238
239 code += " "
240
241 # headers
242 result = {}
243 result["header"] = {}
244 for n, rgx in _HEADERS_RGXES:
245 m = rgx.match(code)
246 if m:
247 result["header"][n] = m.group(1)
248 code = code[m.end():]
249
250 # base
251 result["base"] = []
252 for n, rgx, repeatable in _BASEINFO_RGXES:
253 m = rgx.match(code)
254 while m:
255 r = (m.group(0).rstrip(), _compactdict(m.groupdict()))
256 result["base"].append((n, r))
257 code = code[m.end():]
258 if not repeatable:
259 break
260 m = rgx.match(code)
261
262 # trend
263 m = _TREND_RGX.match(code)
264 if m:
265 result["trend"] = (m.group(0).rstrip(), [])
266 code = code[m.end():]
267 for n, rgx, repeatable in _TRENDINFO_RGXES:
268 m = rgx.match(code)
269 while m:
270 r = (m.group(0).rstrip(), _compactdict(m.groupdict()))
271 result["trend"][1].append((n, r))
272 code = code[m.end():]
273 if not repeatable:
274 break
275 m = rgx.match(code)
276
277 # remarks
278 m = _REMARK_RGX.match(code)
279 if m:
280 result["remark"] = (m.group(0).rstrip(), [])
281 code = code[m.end():]
282 while code:
283 for n, rgx in _REMARKINFO_RGXES:
284 m = rgx.match(code)
285 if m:
286 if n != "__unk":
287 r = (m.group(0).rstrip(), _compactdict(m.groupdict()))
288 else:
289 r = (m.group(0).rstrip(),)
290 result["remark"][1].append((n, r))
291 code = code[m.end():]
292 break
293
294 return result
295
296
297 #
298 if __name__ == '__main__':
299 for line in open("11Z.txt", "r").readlines():
300 result = metar_splitter(line.strip())
301 print(result)
302
303 # print header
304 #print(result["header"])
305
306 # print all bases
307 #print(result["base"])
308
309 # print only originals
310 #print([r[1][0] for r in result["base"]])
311 # print only parsed
312 #print([str(r[1]) for r in result["base"]])
313 #if "remark" in result:
314 # print(" ".join([str(r[1][0]) for r in result["remark"][1]]))
基本的には正規表現が分解する group のレベルでの「親切」は引き継いでいるけれど、そのあとの「一般人でも理解出来る」ように読み替える処理は完全に削っているし、なんなら「数値を python 数値に」でさえやってない。それをするのは「splitter」の責務ではないであろう。(なお、「FIXME」とある部分、おかしいと思うんだけど、METAR の仕様が今ひとつあいまいでよくわからない。wind として「7000」て形式はないと思うんだけど、そんなデータが今日のにあった。)
一応、さっきの「RJAA」のレコードを処理すると result はこんな構造(読みやすさのために json で示す)で返って来る:
1 {
2 "trend": [
3 "NOSIG",
4 []
5 ],
6 "header": {
7 "station": "RJAA",
8 "time": "221100Z"
9 },
10 "base": [
11 [
12 "wind",
13 [
14 "21014KT 170V250",
15 {
16 "units": "KT",
17 "speed": "14",
18 "varfrom": "170",
19 "varto": "250",
20 "dir": "210"
21 }
22 ]
23 ],
24 [
25 "visibility",
26 [
27 "9999",
28 {
29 "vis": "9999",
30 "dist": "9999"
31 }
32 ]
33 ],
34 [
35 "weather",
36 [
37 "-SHRA",
38 {
39 "int": "-",
40 "prec": "RA",
41 "desc": "SH"
42 }
43 ]
44 ],
45 [
46 "sky",
47 [
48 "FEW015",
49 {
50 "cover": "FEW",
51 "height": "015"
52 }
53 ]
54 ],
55 [
56 "sky",
57 [
58 "FEW030CB",
59 {
60 "cover": "FEW",
61 "cloud": "CB",
62 "height": "030"
63 }
64 ]
65 ],
66 [
67 "sky",
68 [
69 "SCT100",
70 {
71 "cover": "SCT",
72 "height": "100"
73 }
74 ]
75 ],
76 [
77 "sky",
78 [
79 "BKN140",
80 {
81 "cover": "BKN",
82 "height": "140"
83 }
84 ]
85 ],
86 [
87 "temp/dewp",
88 [
89 "27/25",
90 {
91 "dewpt": "25",
92 "temp": "27"
93 }
94 ]
95 ],
96 [
97 "qnh",
98 [
99 "Q1000",
100 {
101 "press": "1000",
102 "unit": "Q"
103 }
104 ]
105 ]
106 ]
107 }
全て「(“フィールドの名前”, [“もとのテキスト”, {正規表現の名前付きパターンで分解されたフィールド辞書}])」みたいな形にしてあって、なおかつトップレベルで「header」「base」「trend」「remark」と特定して取りだせるようにした。普通この base にしか興味がないとかそんななんだよね。remark は読むと面白いけどねぇ。「B737 が waypoint BANBI の 8000feet で晴天乱気流を報告」みたいなことが書かれてる。あといわゆる「強烈な天気(航空に支障をきたす気象現象)」が実は(知ってれば)一撃でわかるようになってたりもする。(例えば「凄まじく雲頂の高い積乱雲」だとか。)
さて。あとは NOAA の API からのダウンローダだな。それと「現在地点から一番近くて海水面相当の空港はどこだ」検索も必要だ、っと。
ところで、METAR あてにするんだったらこれを航空の方式と同じく QNH てのもあるよなぁ、なんて思ったりもした。(Q1000 が QNH 規正値が 1000hPa、てイミ。)