何を作りたいのか
郵便番号や住所をgetパラメータで投げたら緯度経度等のJSONが貰えるRESTAPIが欲しい。精度はそこそこでいいから気軽に使えるやつ。
動機
無料の某鯖は時々サーバーエラーになったり、利用規約的にあれだったり。あと趣味。
言語
pythonで書きます。言語選択は趣味です。
データの収集
郵便番号
みんな大好きKEN_ALL.CSVですが、緯度経度は乗ってませんし、細かい所のフォーマットがストレスでマッハです。
※詳しくはこの辺 → KEN_ALL.CSV (郵便番号検索)の落とし穴
zipcloudさん(神)が公開されてる加工済みKEN_ALL.CSVを使わせて頂きます。
緯度経度
国交省が公開してるのを使おうと思ったのですが、先の郵便番号データと統合するのが手間です。こちらは郵便番号はもちろん乗ってないです。市町村コードも何故かないです。住所文字列でマッピングするしかないんです。
加えて、今回の件で知ったのですが、割と郵便番号が振られていない住所があったり、逆に高層ビルだと階層ごとに郵便番号が振られていたりと結構カオスです。後述の名寄せアルゴリズムを応用した統合プログラムで8,9割くらいマッピング出来たのですが、数が数だけに残り1,2割の確認・手作業も苦痛です。
どうしたか
郵便番号を叩いて緯度経度を返してくれる某APIの無料枠を使って、緯度経度収集プログラムの開発、少しずつ集めました。下手に統合コード書くよりも早いです。KEN_ALL.CSVに緯度経度の列を加えたCSVを作ります。数件、緯度経度を返してくれない所があったのでそこは手作業で書き加えました。
データベースにインポートするCSVを作成
データベースはsqliteを使います。どうせ運用時はselectしか使わないですし、10万ちょっとしかレコード数がないなら下手なSQL鯖よりも早いです。
カラムは以下のようにします。
CREATE TABLE Address(Id INTEGER PRIMARY KEY,Code text,Zipcode text,Coordinate text,Prefecture text,Gun text,City text,Ku text,Other text,PrefectureKana text,GunKana text,CityKana text,KuKana text,OtherKana text);
何故か郡・市町村・区がごっちゃになってるのが多いのですが、なにかと鬱陶しいので分割します。後述の名寄せアルゴリズムを応用したプログラムを使ってCSVを変換してやります。
できたやつ
これを.importで食わせます。1,2秒で食ってくれました。早いです。
住所の名寄せ
鬱陶しいやつです。
住所の書き方はある程度は決まっているのですが、フリーダムでもあります。要所要所でのスペースの有無・都道府県名や郡名の省略があります。他にもあるかも(字の取り扱いとか)ですが、とりあえずこれだけは対応します。
スペースの対応
文字列の先頭からindexを動かして一文字ずつ読み取っていくのが基本となります。スペースや改行コード等の無視したい文字の時はindexを一気に進めます。この処理をEatとか言うようです。
class JpAddressParserData:
EatenSigns = [" ", " ", "\t", "\r", "\n", "\v"]
def EatSigns(self, str, pos):
for i in range(pos, len(str) - 1):
if not str[i] in self.EatenSigns:
return i
return len(str)
都道府県の検出
Eatしたらその位置でどの都道府県か総当たりします。ここで、次の「都道府県」いずれかの文字にあたるまで、という処理を書いてしまうと、都道府県名を省略していきなり「都城市」とか「府中市」とか書かれるとバグります。バグ怖いです。検出できなかったときは省略されたとみなして、空文字列を返します。
class JpAddressParserData:
Prefs = ["北海道", "青森県", "岩手県", ・・・
def GetAddressPref(self, address, pos):
for pref in self.Prefs:
if address.find(pref, pos) == pos:
return pref
return ""
郡の検出
これも予め登録してある郡を総当たりします。次の「郡」という文字まで、という処理をすると、大和郡山市が大和郡・山市として処理される罠があります。罠怖いです。都道府県と同様に検出できなかったときは省略されたとみなして、空文字列を返します。
class JpAddressParserData:
Guns = ["石狩郡", "松前郡", "上磯郡", ・・・
def GetAddressGun(self, address, pos):
for gun in self.Guns:
if address.find(gun, pos) == pos:
return gun
return ""
市町村名の検出
市町村という文字を含む市町村名があるので(みんな大好き町田市とか)、これも総当たりしたいところですが、数があるので、罠だけに絞って総当たりします。
罠が消え去ったら、次は東京23区を総当たりで検出、その時は空文字列を返します。
そこまでやってようやく、次の市町村という文字までを市町村名とします。
class JpAddressParserData:
CityNameIncludedShiChoSon = ["余市町", "村田町", "村山市", ・・・
Tokyo23s = ["千代田区", "中央区", "港区", ・・・
def GetAddressCity(self, address, pos):
for city in self.CityNameIncludedShiChoSon:
if address.find(city, pos) == pos:
return city
for tokyo23 in self.Tokyo23s:
if address.find(tokyo23, pos) == pos:
return ""
c = address.find("市", pos)
t = address.find("町", pos)
v = address.find("村", pos)
p = min([c if 0 <= c else 1000, t if 0 <= t else 1000, v if 0 <= v else 1000])
if p == -1:
return ""
return address[pos:(p + 1)]
区を検出
政令指定都市か東京23区判定を受けたら、この処理をします。次の区の文字までを区名とします。幸い区を含む区名はありません。
class JpAddressParserData:
def GetAddressKu(self, address, pos):
p = address.find("区", pos)
if p == -1:
return ""
return address[pos:(p + 1)]
字
余ったやつを字とします。
サーバープログラム
さくらインターネットのレンタルサーバーは未だにpython3に正式対応していません。python2自体のサポートは2019年いっぱい。どないする気でしょうか。プログラムが出来てから気づきました。泣きそうです。方法はあるようなのでどうにかします。(さくらインターネット ライトプランに python3をインストール)
動作例
サンプル鯖
郵便番号
住所
サンプル鯖で返すのは京都(上京区・中京区・下京区・東山区)と郡のサンプルとして大阪府三島郡島本町山崎(某ウイスキー工場があるとこ)だけです。githubにあげてるやつは全国あります。
githubリポジトリ
免責とか
本格運用は厳しいと思いますが、趣味ベースや利用者が限定的なとこ、モック鯖としてならそこそこ使えます。多分、使えると思う。使えるんじゃないかな。ま、不具合はちょっと覚悟はしておけ下さい。
利用や改善等してくれる殊勝な方はどうぞご自由に。特に連絡も要りません。サポートや更新等は受け付けません。