日本全国津々浦々駅名コレクション by 駅データ.jp or Wikipedia

気圧高度の補正に「確実に通る基点」としての駅の緯度経度一覧があればあるならあるだろう。正確な標高データの「代表エリア」が欲しいのである。

例によってチマチマ Google Maps やら地理院 WEB で目視で調べて抽出してたんだけど、さすがに「東海道線の駅全部欲しいなぁ」なんて考え出すと耐えられなくなり。

これに関して以前日本全国津々浦々寺社仏閣コレクション by OpenStreetMapなんてネタを書いた。駅検索にもこのアプローチは使えるけれど、その際にも書いたように、駅の代表点ではなく形状としてのポリゴンが得られてしまうのが、今の場合は too much。(ワタシの目的の場合は「駅代表点を中心とする 750mx750m 矩形」を計算して求めたいので、駅形状の centroid を求めて…てひと手間が面倒くさい。)

探せばすぐに駅データ.jpが見つかった。無料版でもユーザ登録が必要、駅名の読みは有料。てのにひとまず臆してみて、でも目的の緯度経度もわかるしな、と、ひとまず頭の片隅にストック。

先に Wikipedia からあさってみようかと:

未完
  1 # -*- coding: utf-8 -*-
  2 import os
  3 import re
  4 import sys
  5 import json
  6 import logging
  7 from bs4 import BeautifulSoup
  8 from urllib import urlopen, unquote  # 2.7
  9 
 10 
 11 _RGX_lochref = re.compile(r'geohack.*params=([0-9._]+_[NS])_([0-9._]+_[WE])_')
 12 _BASE = 'https://ja.wikipedia.org'
 13 logging.basicConfig(
 14     stream=sys.stdout, level=logging.DEBUG)
 15 _logger = logging.getLogger(__name__)
 16 
 17 
 18 def _enum_all_index_pages():
 19     url = "https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E3%81%AE%E9%89%84%E9%81%93%E9%A7%85%E4%B8%80%E8%A6%A7"
 20     soup = BeautifulSoup(urlopen(url).read(), "lxml")
 21     return [_BASE + a.attrs['href']
 22             for a in soup.find('table',
 23                                {'border': '0',
 24                                 'style': 'font-size:95%;'}).findAll("a")]
 25 
 26 def enum_all_detail_pages():
 27     for url in _enum_all_index_pages():
 28         soup = BeautifulSoup(urlopen(url).read(), "lxml")
 29         for li in soup.find('div', {'id': 'mw-content-text'}).findAll('li'):
 30             a = li.find('a')
 31             if a:
 32                 yield _BASE + li.find('a').attrs['href']
 33 
 34 
 35 def extract_details(url):
 36     result = {}
 37 
 38     # path itself is the name of the station
 39     name = url.rpartition("/")[-1]
 40     result["name"] = unquote(name).decode('utf-8')
 41 
 42     soup = BeautifulSoup(urlopen(url).read(), "lxml")
 43 
 44     # get latlons:
 45     coord = soup.find(
 46         'span',
 47         {'id': 'coordinates', 'class': 'plainlinks'}
 48         )
 49     if not coord:
 50         coord = soup.find(
 51             'span',
 52             {'class': 'plainlinks'}
 53             )
 54         if not coord:
 55             _logger.warn(url)
 56             return None
 57     coord = coord.find('a', {'class': 'external text'}).attrs['href']
 58     _logger.debug("{}".format(coord))
 59     m = _RGX_lochref.search(coord)
 60     if not m:
 61         _logger.warn(url)
 62         return None
 63     lat_s, lon_s = m.group(1, 2)
 64     try:
 65         lat_d, lat_m, lat_s, ns = lat_s.split("_")
 66         lon_d, lon_m, lon_s, ew = lon_s.split("_")
 67         lat = (float(lat_d) + float(lat_m) / 60. + float(lat_s)/3600.) * (-1 if ns == 'S' else 1)
 68         lon = (float(lon_d) + float(lon_m) / 60. + float(lon_s)/3600.) * (-1 if ew == 'W' else 1)
 69     except ValueError:  # degrees format
 70         lat_d, ns = lat_s.split("_")
 71         lon_d, ew = lon_s.split("_")
 72         lat = float(lat_d) * (-1 if ns == 'S' else 1)
 73         lon = float(lon_d) * (-1 if ew == 'W' else 1)
 74     result["lat"] = lat
 75     result["lon"] = lon
 76 
 77     # get yomi..., but dirty structure!!
 78     try:
 79         yomi = soup.find(
 80             'table',
 81             {'class': 'infobox bordered'}
 82             ).find('span', {'style': 'font-size:120%;'}).text
 83         result["yomi"] = yomi.split(" - ")
 84     except:
 85         result["yomi"] = []
 86     _logger.info(url)
 87 
 88     return result
 89 
 90 #result_all = {}
 91 for url in enum_all_detail_pages():
 92     try:
 93         k = url.rpartition("/")[-1]
 94         fn = "_results/{}.json".format(k)
 95         if os.path.exists(fn):
 96             continue
 97         v = extract_details(url)
 98         if v:
 99             #result_all[k] = v
100             json.dump(v, open(fn, "wb"))
101             _logger.debug(v['yomi'])
102     except IOError:
103         pass  # ignore

悪くもないんだけれど、当たり前だがとんでもなく時間がかかる。「か」あたりまで処理出来たことを確認したが、さすがにいやになってやめた。何度も失敗することを踏まえて一駅ずつ json にしてるわけだが、これをあとからまとめあげることまで考えると気が滅入るし、途中までやってから他の情報も欲しくなって、こりゃキリがないぞと。(あといくつかデータが取れてないみたいなんだけど、その原因解析も面倒でさ。)

まぁユーザ登録つぅてもね、メールアドレスだけみたいだしな、と、駅データ.jpに登録。めでたく csv ダウンロード。

データの意味は 仕様書/駅 など。

csv? 毎度 csv を手にするたびにどうやって持っておこうかねぇ、と頭をひねる。結局「型なし」なのとなんだかんだ読み込みもダルいので、「何か」に変換して持っておきたくなる(xml も同様だけど)。やっぱ「検索しやすさ」を考えると「データベース的なもの」に入れておくのが良いか。同じようなこと何度もやってるなぁ、と、この際なので今後も別途なニーズに使えそうな「ふうに」SQLite3 への変換を書いてみた:

ekidata_csv_to_sqlite.py
  1 # -*- coding: utf-8 -*-
  2 # -------------------------------------------------------------
  3 import sqlite3
  4 
  5 _TYMAP = {
  6     float: "FLOAT",
  7     int: "INTEGER",
  8     long: "INTEGER",
  9     str: "TEXT",
 10     unicode: "TEXT",
 11     }
 12 
 13 _DBKEYWORDS_TRANS = {
 14     "add": "address",
 15 }
 16 
 17 #
 18 class _Table(object):
 19     def __init__(self, conn, table_name, coldefs):
 20         self._conn = conn
 21         self._table_name = table_name
 22         self._coldefs = coldefs
 23 
 24     #
 25     def _csv_read(self, fn):
 26         with open(fn) as fi:
 27             headers = fi.readline().strip().split(",")
 28             cnvs = []
 29             for k, ty in self._coldefs:
 30                 i = headers.index(k)
 31                 cnvs.append((i, k, ty["type"]))
 32             rows = []
 33             for line in fi.readlines():
 34                 vals = line.decode("utf-8").strip().split(",")
 35                 rowres = {}
 36                 for cd in cnvs:
 37                     v = cd[2](vals[cd[0]])
 38                     rowres[cd[1]] = v
 39                 rows.append(rowres)
 40     
 41                 # for ristriction for a number of bindings (<500)
 42                 if len(rows) * len(rowres) > 450:
 43                     yield rows
 44                     rows = []
 45             if rows:
 46                 yield rows
 47 
 48     def _insert(self, rows):
 49         with self._conn:
 50             cols = []
 51             vals = []
 52             for k, ty in self._coldefs:
 53                 # sqlite3 keyword...
 54                 cols.append(_DBKEYWORDS_TRANS.get(k, k))
 55             for row in rows:
 56                 for k, ty in self._coldefs:
 57                     vals.append(row[k])
 58             sql = "INSERT INTO " + self._table_name + " \n"
 59             sql += "\nUNION ALL ".join(["SELECT " + ",".join(["? AS 'c'".format(c) for c in cols])] * len(rows))
 60             self._conn.execute(sql, vals)
 61 
 62     def from_csv(self, fn):
 63         for data in self._csv_read(fn):
 64             self._insert(data)
 65 
 66 #
 67 class ToSqlite3(object):
 68     def __init__(self, dbname):
 69         self._conn = sqlite3.connect(dbname)
 70 
 71     def create_table(self, table_name, coldefs):
 72         with self._conn:
 73             cols = []
 74             for k, ty in coldefs:
 75                 dbty = _TYMAP[ty["type"]]
 76                 if ty["pk"]:
 77                     dbty += " PRIMARY KEY "
 78                 if ty["uk"]:
 79                     dbty += " UNIQUE "
 80                 if ty["nn"]:
 81                     dbty += " NOT NULL "
 82                 cols.append("{} {}".format(_DBKEYWORDS_TRANS.get(k, k), dbty))
 83             self._conn.execute(
 84                 "CREATE TABLE " + table_name + " (" + ",".join(cols) + ")")
 85 
 86         return _Table(self._conn, table_name, coldefs)
 87 # -------------------------------------------------------------
 88 
 89 
 90 # company
 91 coldefs_company = [
 92     ("company_cd", {"type": int, "pk": True, "uk": False, "nn": True,}),
 93     ("rr_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
 94     ("company_name", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
 95     ("company_name_k", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
 96     ("company_name_h", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
 97     ("company_name_r", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
 98     ("company_url", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
 99     ("company_type", {"type": int, "pk": False, "uk": False, "nn": True,}),
100     ("e_status", {"type": int, "pk": False, "uk": False, "nn": True,}),
101     #("e_sort", {"type": int, "pk": False, "uk": False, "nn": True,}),
102 ]
103 
104 # join
105 coldefs_join = [
106     ("line_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
107     ("station_cd1", {"type": int, "pk": False, "uk": False, "nn": True,}),
108     ("station_cd2", {"type": int, "pk": False, "uk": False, "nn": True,}),
109 ]
110 
111 # line
112 coldefs_line = [
113     ("line_cd", {"type": int, "pk": True, "uk": False, "nn": True,}),
114     ("company_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
115     ("line_name", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
116     ("line_name_k", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
117     ("line_name_h", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
118     #("line_color_c", {"type": int, "pk": False, "uk": False, "nn": True,}),
119     #("line_color_t", {"type": int, "pk": False, "uk": False, "nn": True,}),
120     #("line_type", {"type": int, "pk": False, "uk": False, "nn": True,}),
121     ("lon", {"type": float, "pk": False, "uk": False, "nn": True,}),
122     ("lat", {"type": float, "pk": False, "uk": False, "nn": True,}),
123     #("zoom", {"type": int, "pk": False, "uk": False, "nn": True,}),
124     ("e_status", {"type": int, "pk": False, "uk": False, "nn": True,}),
125     #("e_sort", {"type": int, "pk": False, "uk": False, "nn": True,}),
126 ]
127 
128 # station
129 coldefs_station = [
130     ("station_cd", {"type": int, "pk": True, "uk": False, "nn": True,}),
131     ("station_g_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
132     ("station_name", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
133     #("station_name_k", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
134     #("station_name_r", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
135     ("line_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
136     ("pref_cd", {"type": int, "pk": False, "uk": False, "nn": True,}),
137     ("post", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
138     ("add", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
139     ("lon", {"type": float, "pk": False, "uk": False, "nn": True,}),
140     ("lat", {"type": float, "pk": False, "uk": False, "nn": True,}),
141     ("open_ymd", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
142     ("close_ymd", {"type": unicode, "pk": False, "uk": False, "nn": True,}),
143     ("e_status", {"type": int, "pk": False, "uk": False, "nn": True,}),
144     #("e_sort", {"type": int, "pk": False, "uk": False, "nn": True,}),
145 ]
146 
147 tsqll = ToSqlite3("_ekidata.dat")
148 tsqll.create_table("t_company", coldefs_company).from_csv("company20160401.csv")
149 tsqll.create_table("t_join", coldefs_join).from_csv("join20160401.csv")
150 tsqll.create_table("t_line", coldefs_line).from_csv("line20160402free.csv")
151 tsqll.create_table("t_station", coldefs_station).from_csv("station20160401free.csv")

「# —-」で囲まれた部分は「結構汎用」なので、以後使いまわそうと思えば使いまわせる、はず。もっと格調高く書くことも出来よう。頑張ればほかの DBMS のためにも書けるだろう…なんてことをやりだすと、この手の変換ツールの大規模で「イケてる」ものなんかたくさんあるので、そうしたいならそうしたほうがいい。

にしてもアレな。sqlite が Disk I/O エラー起こすのって android 版だけじゃないのな。Windows でも起こった。措置は面倒だから何もせず、失敗したら諦めてやり直す、の巻。

ちなみに _ekidata.dat のサイズは 1.3MB。これくらいならスマホの SDカードに突っ込んでおけるサイズかもね。

とりあえずデータ取れる?:

 1 # -*- coding: utf-8 -*-
 2 import sqlite3
 3 import sys, codecs ; sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
 4 
 5 dbname = "_ekidata.dat"
 6 conn = sqlite3.connect(dbname)
 7 cur = conn.cursor()
 8 cur.execute("SELECT station_name, lon, lat FROM t_station")
 9 for row in cur:
10     print(u"{},{},{}".format(row[0], row[1], row[2]))
 1 函館,140.726413,41.773709
 2 五稜郭,140.733539,41.803557
 3 桔梗,140.722952,41.846457
 4 大中山,140.71358,41.864641
 5 七飯,140.688556,41.886971
 6 新函館北斗,140.646525,41.9054
 7 ...
 8 九州鉄道記念館,130.962439,33.944392
 9 出光美術館,130.965292,33.947792
10 ノーフォーク広場,130.964254,33.955973
11 関門海峡めかり,130.967347,33.960627

おけ。「東海道線の駅全部」とかは、路線の構造を少し理解してないとハマりそうだけど、まぁないよりはずっと楽だろう。ん? 「東海道線全部」てね、こんななの:

1 line_name,line_name_k,line_name_h
2 JR東海道本線(東京~熱海),トウカイドウホンセン,JR東海道本線(東京~熱海)
3 JR東海道本線(熱海~浜松),トウカイドウホンセン,JR東海道本線(熱海~浜松)
4 JR東海道本線(浜松~岐阜),トウカイドウホンセン,JR東海道本線(浜松~岐阜)
5 JR東海道本線(岐阜~美濃赤坂・米原),トウカイドウホンセン,JR東海道本線(岐阜~美濃赤坂・米原)
6 琵琶湖線,ビワコセン,JR東海道本線(米原~京都)
7 京都線,キョウトセン,JR東海道本線(京都~大阪)
8 JR神戸線(大阪~神戸),コウベセン,JR東海道本線(大阪~神戸)







ちょっとオマケのはなし。

当然「一件ずつ insert」なんてのは、どんな DBMS 使おうと、激しく遅い。そんなことはひとつでもデータベース関係の開発をしたことがあれば常識。そしてどの DBMS でも「バルクインサート」の手段が一つくらいは必ずある。bulk は「巨漢、大柄」とかの意味なので、つまりはバルクインサートといった場合「大規模大量インサートをしたい」という「意図」を表している。

これをググると面白いのよね。「バルクインサートが一番高速です!」。うーん、当たり前? もしくは「どの?」。とつっこみたくなる。これって別に専門用語でもないし特定のやり方を指す言葉でもないのよ。だからぶっちゃけ「遅いバルクインサートのやり方」もありえる。当然バルクインサート手段の提供は DBMS ごとに異なる。けどまぁだいたいほとんど同じか、似てるよね。

これ:

1 INSERT INTO 'tablename' ('column1', 'column2') VALUES
2   ('data1', 'data2'),
3   ('data1', 'data2'),
4   ('data1', 'data2'),
5   ('data1', 'data2');

が sqlite 3.7.11 以降出来るとあったので試してみたが、Python 2.7 にバンドルのバージョンはこれより古いんかね、出来ない。ので

1      INSERT INTO 'tablename'
2           SELECT 'data1' AS 'column1', 'data2' AS 'column2'
3 UNION ALL SELECT 'data1', 'data2'
4 UNION ALL SELECT 'data1', 'data2'
5 UNION ALL SELECT 'data1', 'data2'

でやってるんだけど、この「一括で許される件数」についても結構な頻度でガセが見つかるので注意。実際に自分で手を動かしてエラーメッセージを喰らってみればわかるはずなのにね、なんでウソ書くの。「行数」は関係ない。「列数」の方が制限にひっかかるの。これは遥か遠い記憶の DB2、少し遠い記憶の PostgreSQL も同じだったと思う。(おそらく内部的には実装に使っているバッファサイズから来る制限なんだと思う。)