理想未来ってなんやねん

娘可愛い。お父さん頑張る。

PHPで都道府県、市区町村、町域名以降の住所分割を高速に行う方法

一つに結合された住所文字列から、『都道府県』、『市区町村』、『町域名以降』を高速に分割したい。
簡単に思いつく方法としては、日本郵便で公開されている住所データを元に1行づつ比較していく方法が考えられますが、生成に時間が掛かってしまいます。

こんな時にmecabを使うと簡単且つ高速に分割できます。

住所辞書の作成

まずは住所辞書を作成します。
そのために住所辞書の元になるデータ用意する必要がありますが、今回は日本郵便で公開されている郵便番号データを使いました。


公開されているデータファイルはlzh形式となっているので、lhaで解凍します。
CentOSでのlhaのインストールは『CentOSでlhaを使う - 理想未来はどうなった?』を参考にしてください。

wget http://www.post.japanpost.jp/zipcode/dl/kogaki/lzh/ken_all.lzh
lha e ken_all.lzh


ken_all.csvというcsvファイルが解凍されます。
中身はこんな感じ。

01101,"060  ","0600000","ホッカイドウ","サッポロシチュウオウク","イカニケイサイガナイバアイ","北海道","札幌市中央区","以下に掲載がない場合",0,0,0,0,0,0
01101,"064  ","0640941","ホッカイドウ","サッポロシチュウオウク","アサヒガオカ","北海道","札幌市中央区","旭ケ丘",0,0,1,0,0,0
01101,"060  ","0600041","ホッカイドウ","サッポロシチュウオウク","オオドオリヒガシ","北海道","札幌市中央区","大通東",0,0,1,0,0,0
01101,"060  ","0600042","ホッカイドウ","サッポロシチュウオウク","オオドオリニシ(1-19チョウメ)","北海道","札幌市中央区","大通西(1〜19丁目)",1,0,1,0,0,0
01101,"064  ","0640820","ホッカイドウ","サッポロシチュウオウク","オオドオリニシ(20-28チョウメ)","北海道","札幌市中央区","大通西(20〜28丁目)",1,0,1,0,0,0
〜〜〜中略〜〜〜
47381,"90714","9071433","オキナワケン","ヤエヤマグンタケトミチョウ","ハイミナカ","沖縄県","八重山郡竹富町","南風見仲",0,0,0,0,0,0
47381,"90717","9071751","オキナワケン","ヤエヤマグンタケトミチョウ","ハテルマ","沖縄県","八重山郡竹富町","波照間",0,0,0,0,0,0
47381,"90715","9071544","オキナワケン","ヤエヤマグンタケトミチョウ","ハトマ","沖縄県","八重山郡竹富町","鳩間",0,0,0,0,0,0
47382,"90718","9071800","オキナワケン","ヤエヤマグンヨナグニチョウ","イカニケイサイガナイバアイ","沖縄県","八重山郡与那国町","以下に掲載がない場合",0,0,0,0,0,0
47382,"90718","9071801","オキナワケン","ヤエヤマグンヨナグニチョウ","ヨナグニ","沖縄県","八重山郡与那国町","与那国",0,0,0,0,0,0


都道府県名と市区町村だけを抜き出し、重複を取り除いた後、mecabの辞書として登録できる形式に変換します。

nkf -S -w ken_all.csv|cut -f 4-5,7-8 -d ','|sed -e 's/^"\([^"]*\)","\([^"]*\)","\([^"]*\)","\([^"]*\)"/\3 \1\
\4 \2/g'|sort|uniq|gawk '{c=int(-400 * (length($1) ^ 1.5)); if (c < -36000) c=36000; print $1 ",-1,-1," c ",名詞,固有名詞,地域,一般,*,*," $1 "," $2 "," $2}' > addr.csv


以下のようなcsvファイルができあがります。

$ cat addr.csv
あきる野市,-1,-1,-4472,名詞,固有名詞,地域,一般,*,*,あきる野市,アキルノシ,アキルノシ
あま市,-1,-1,-2078,名詞,固有名詞,地域,一般,*,*,あま市,アマシ,アマシ
あわら市,-1,-1,-3200,名詞,固有名詞,地域,一般,*,*,あわら市,アワラシ,アワラシ
いすみ市,-1,-1,-3200,名詞,固有名詞,地域,一般,*,*,いすみ市,イスミシ,イスミシ
いちき串木野市,-1,-1,-7408,名詞,固有名詞,地域,一般,*,*,いちき串木野市,イチキクシキノシ,イチキクシキノシ
〜〜〜中略〜〜〜
蕨市,-1,-1,-1131,名詞,固有名詞,地域,一般,*,*,蕨市,ワラビシ,ワラビシ
檜山郡厚沢部町,-1,-1,-7408,名詞,固有名詞,地域,一般,*,*,檜山郡厚沢部町,ヒヤマグンアッサブチョウ,ヒヤマグンアッサブチョウ
檜山郡江差町,-1,-1,-5878,名詞,固有名詞,地域,一般,*,*,檜山郡江差町,ヒヤマグンエサシチョウ,ヒヤマグンエサシチョウ
檜山郡上ノ国町,-1,-1,-7408,名詞,固有名詞,地域,一般,*,*,檜山郡上ノ国町,ヒヤマグンカミノクニチョウ,ヒヤマグンカミノクニチョウ
諫早市,-1,-1,-2078,名詞,固有名詞,地域,一般,*,*,諫早市,イサハヤシ,イサハヤシ


変換したcsvデータを元にmecabの辞書を生成します。
以下の例ではaddr.dicという辞書ファイルが生成されます。

mecab-dict-indexおよびipadicのパスは環境によりことなると思いますので適当に置き換えてください。

/usr/local/libexec/mecab/mecab-dict-index -d/usr/local/lib/mecab/dic/ipadic -u addr.dic -f utf8 -t utf8 addr.csv

辞書テスト

実際にうまく分割されるか試してみます。


まずは生成した辞書ファイルを使わない場合の例。
東京都港区芝公園4-2-8の場合、『東京』『都』『港』『区』と分割されます。

$ echo '東京都港区芝公園4-2-8' | mecab
東京	名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー
都	名詞,接尾,地域,*,*,*,都,ト,ト
港	名詞,固有名詞,地域,一般,*,*,港,ミナト,ミナト
区	名詞,接尾,地域,*,*,*,区,ク,ク
芝公園	名詞,固有名詞,地域,一般,*,*,芝公園,シバコウエン,シバコーエン
4	名詞,数,*,*,*,*,*
-	名詞,サ変接続,*,*,*,*,*
2	名詞,数,*,*,*,*,*
-	名詞,サ変接続,*,*,*,*,*
8	名詞,数,*,*,*,*,*
EOS

デフォルトの辞書の場合、
『東京都』が、『東京』『都』
『港区』が『港』『区』
と分かれてしまうので、このままでは使用できません。


次に、今回生成した辞書ファイルを使用した場合の例です。
期待通り『東京都』『港区』と分割されます。

$ echo '東京都港区芝公園4-2-8' | mecab -u./addr.dic
東京都	名詞,固有名詞,地域,一般,*,*,東京都,トウキョウト,トウキョウト
港区	名詞,固有名詞,地域,一般,*,*,港区,ミナトク,ミナトク
芝公園	名詞,固有名詞,地域,一般,*,*,芝公園,シバコウエン,シバコーエン
4	名詞,数,*,*,*,*,*
-	名詞,サ変接続,*,*,*,*,*
2	名詞,数,*,*,*,*,*
-	名詞,サ変接続,*,*,*,*,*
8	名詞,数,*,*,*,*,*
EOS

変換スクリプト

上記で生成した辞書を使うことで、都道府県、市区町村までは期待どおり分割できます。
分割は、『都道府県』、『市区町村』、『町域名以降』の三つで良いので、町域名以降は分割されたものを結合します。

id:rskyさんのphp_mecabを使わせていただき、PHPで処理してみました。

<?php
$dictfile = './addr.dic';
$addrfile = 'testdata.lst';

$options = array('-u', $dictfile);

$mecab = mecab_new($options);

$handle = @fopen($addrfile, 'r');
while (!feof($handle))
{
	$addr = fgets($handle);
	if (!$addr)
		continue;

	$addr_splits = array();
	$node = mecab_sparse_tonode($mecab, $addr);
	while($node = mecab_node_next($node))
	{
		$addr_splits[] = mecab_node_surface($node);
	}
	$addr1 = array_shift($addr_splits);
	$addr2 = array_shift($addr_splits);
	$addr3 = implode("", $addr_splits);

	echo "$addr1,$addr2,$addr3\n";
}
fclose($handle);
mecab_destroy($mecab);


上記のスクリプトをconvaddr.phpという名前で保存します。
$addrfileで指定したファイルに改行で区切った住所データを用意し実行すると、カンマ区切りで3つに区切られた住所データとして出力されます。

$ cat testdata.lst|wc -l
5000
$ time (php convaddr.php > output.csv)

real	0m0.448s
user	0m0.352s
sys	0m0.096s
$ head output.csv
千葉県,木更津市,下郡1-18-16ハウス下郡405
群馬県,桐生市,仲町3-3-7ステーション仲町210
埼玉県,吉川市,吉川2-16-2吉川パレス103
東京都,小平市,上水南町4-8-9
滋賀県,彦根市,平田町4-14-6
秋田県,大館市,幸町1-7-19メゾン幸町119
岡山県,苫田郡鏡野町,下森原3-5-11
沖縄県,浦添市,港川2-6-5
大分県,中津市,三光森山3-6三光森山アパート414
香川県,観音寺市,木之郷町3-17-7

お名前.com VPS(スペック)にて5000件のレコードを使いテストしたところ0.458秒で変換できました。
用途にもよると思いますが、速度的に実用十分ではないかと思います。


尚、この方法を応用することで住所から郵便番号を高速に調べることもできますが、また別の機会に紹介したいと思います。