最強のケンオールを求めて - Parse::JapanesePostalCode

yappo
2012-12-12

こんばんは、最近自己紹介する機会があったので「空前のKEN_ALL.CSVブームを作った事で有名です」って言ったら結構通じた yappo です。

と言う事で今日はわけあってどのように KEN_ALL.CSV を処理しているかについて書こうと思います。

使い方に関しては

https://metacpan.org/module/Parse::JapanesePostalCode
https://github.com/yappo/p5-Parse-JapanesePostalCode

こちらをご覧頂ければいいです。マジ簡単だから誰でも使えます。

前提知識

http://www.post.japanpost.jp/zipcode/dl/readme.html

こちらの 郵便番号データファイルの形式等 を良く読むとこのエントリの理解が深まります。

Parse::JapanesePostalCode が目指しているのは、超ガンバって厳密にパースしようと思うと KEN_ALL.CSV が更新されると使えなくなるリスクがあるし、そもそもそこまで細かい処理をしても人間の住所の扱いって適当で使いにくいだけなので、実用上困らない程度の厳密さで簡単に使いやすいデータが作れるようなものです。

複数の小字を列挙するケース

基本的には KEN_ALL.CSV に含まれる住所は大字もしくは町域どまりなのですが、わりかし大きめな字や北海道のように丁目をある程度区切って郵便番号を割り当ててる場合には、だいぶカジュアルに町域の直後に()を使って対象となる小字を書いています。
もしも、対象となる小字が複数あるときは「、」で区切って複数列挙したり「1〜20丁目」のような感じで範囲指定しています。

複数列挙されてるときはそれぞれ分割して $row->subtown の中に取り込んでおきます。
範囲指定されている時は、「1〜20丁目」の場合だと内部的に「1丁目、2丁目、...、19丁目、20丁目」で列挙されてると見なして処理しています。
ただし、番地や号などが範囲指定されてる時はキリがないので範囲指定されていてもそのまま $row->subtown の中につっこみます。

以下のようなデータの場合に

13123,"132  ","1320015","トウキョウト","エドガワク","ニシミズエ(2-3チョウメ、4チョウメ3-9バン)","東京都","江戸川区","西瑞江(2〜3丁目、4丁目3〜9番)",1,0,1,0,0,0

このようなコードを書くと

for my $subtown (@{ $row->subtown }) {
    my $address = join '', $row->pref, $row->district, $row->city, $row->ward, $row->town, $subtown;
    say( join ",", $row->zip, $row->region_id, $address );
}

こう処理されます。

13123,1320015,東京都江戸川区西瑞江2〜3丁目
13123,1320015,東京都江戸川区西瑞江4丁目3〜9番

自働更新処理

僕はよく上のコードで作った $address と郵便番号でユニークなインデックスを作って月次の郵便番号自働更新を行っています。
郵便番号をプライマリーにすれば良いとも思われますが、実は一意な値じゃないので住所と組み合わせます。

あ、差分データとかを使うとややこしくなるので毎月 KEN_ALL.CSV ぶっこ抜いてます。

データベーススキーマとしては updated_on とか is_deleted みたいなカラムも追加しておいて、更新処理が終わった時に updated_on が古いままだったら is_deleted を立てる。もしも逆に is_deleted が立ってたんだけど updated_on が更新されてたら is_deleted を落とすみたいにしています。

稀に KEN_ALL.CSV のいちぎょういちぎょうとか、小字の分割をしないでデータベース管理してる人とかも居ますが。
僕の方式でデータの更新をしておくと

更新前

13123,"132  ","1320015","トウキョウト","エドガワク","ニシミズエ(2-3チョウメ、4チョウメ3-9バン)","東京都","江戸川区","西瑞江(2〜3丁目)",1,0,1,0,0,0

更新後

13123,"132  ","1320015","トウキョウト","エドガワク","ニシミズエ(2-3チョウメ、4チョウメ3-9バン)","東京都","江戸川区","西瑞江(2〜3丁目、4丁目3〜9番)",1,0,1,0,0,0

のような感じで月次のデータ更新が有った場合に

13123,1320015,東京都江戸川区西瑞江4丁目3〜9番

のレコードの追加だけで済むのでわりと管理しやすい形で更新出来ます。
「4丁目3〜9番」がふわっとしてて使いにくいと思われるかもしれませんが、我々には住所正規化APIがあるのでその辺も strict な形に成形しやすいので問題無いです。
むしろ「東京都江戸川区西瑞江(2〜3丁目、4丁目3〜9番)」って入ってたほうが大体のケースで使いにくい。。。。

京都の場合

京都の場合は通り名が()の中に入りますが、住所正規化API通せばその辺りも何とかなるので上のロジックで気にせず処理しています。

26106,"600  ","6008406","キョウトフ","キョウトシシモギョウク","カメヤチョウ(タカクラドオリゴジョウアガル、タカクラドオリマンジュウジサガル)","京都府","京都市下京区","亀屋町(高倉通五条上る、高倉通万寿寺下る)",0,0,0,0,0,0

複数行にまたがった処理

KEN_ALL.CSV は

26102,"602  ","6028062","キョウトフ","キョウトシカミギョウク","カメヤチョウ","京都府","京都市上京区","亀屋町(油小路通上長者町下る、油小路通下長者町上る、油小路通",0,0,0,0,0,0
26102,"602  ","6028062","キョウトフ","キョウトシカミギョウク","カメヤチョウ","京都府","京都市上京区","中長者町上る、油小路通中長者町下る、上長者町通油小路西入、上長者町通油小",0,0,0,0,0,0
26102,"602  ","6028062","キョウトフ","キョウトシカミギョウク","カメヤチョウ","京都府","京都市上京区","路東入)",0,0,0,0,0,0

のようなかたちで、特定のフィールドに文字長制限を設けて1レコードを複数行に分割すると言うCSVの定義を探す旅に出たくなる程の業の深いフォーマットになっていますが、町域の中に「(」が出て来てから同一行の中で「)」で終わっていなければ「)」で終わる町域が含まれる行が出てくるまで町域を連結していって同一行として処理するようになっています。

ビル処理

高層ビルの各フロア毎に郵便番号が振られているというのは日本国民の一般常識として浸透していますが、何も気にしないで処理すると高層ビルが所属する町域のデータがおかしくなってしまうんです。
名駅ビルを除く全ての高層ビルは以下のような始まりになっています。

13104,"160  ","1600023","トウキョウト","シンジュクク","ニシシンジュク(ツギノビルヲノゾク)","東京都","新宿区","西新宿(次のビルを除く)",0,0,1,0,0,0
13104,"16307","1630790","トウキョウト","シンジュクク","ニシシンジュクオダキュウダイイチセイメイビル(チカイ・カイソウフメイ)","東京都","新宿区","西新宿小田急第一生命ビル(地階・階層不明)",0,0,0,0,0,0
13104,"16307","1630701","トウキョウト","シンジュクク","ニシシンジュクオダキュウダイイチセイメイビル(1カイ)","東京都","新宿区","西新宿小田急第一生命ビル(1階)",0,0,0,0,0,0

ビルが属する町域が先頭にきていて「(次のビルを除く)」と言うのが入っています。これをビル開始行のマーカーとして「(次のビルを除く)」を抜いて処理をして、あとに続くビルの処理をするわけですが、ビル名に注目すると「西新宿小田急第一生命ビル」ってなってるんですね。
実際には「西新宿」って部分は町域に当たる部分でビル名とは関係ないので、ビル開始行のマーカー行で出てきた町域で始まる場合には問答無用でビル名から削除する。という処理もします。
あとは「(地階・階層不明)」だったら floor を undef にして「(1階)」の部分から階数を作って floor に埋めるという事をやって高層ビルを処理しています。

地割り

岩手県辺りにある地割りは、表記がだいぶ揺れてるので正規化する処理入ってるけど行数の割にニーズ少ないのでスルーします。

まとめ

すっかり書き忘れていて困った事になったので Parse::JapanesePostalCode ならではの工夫や、それを活用したお気楽運用する方法を解説しました。