※ パクレゼルヴではWeb開発エンジニアを大募集中!詳細はこちら

Archive

Author Archive

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

2010/7/26 月曜日 14:51:41

macbook-black
マカーです。

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

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

住所辞書の作成

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

公開されているデータファイルはlzh形式となっているので、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

変換スクリプト

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

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秒で変換できました。
用途にもよると思いますが、速度的に実用十分ではないかと思います。

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

マカー PHP

PHPのガーベジコレクタについて  

2010/6/30 水曜日 16:48:12

PHP 5.2までのガーベジコレクタは参照カウント方式です。
循環参照が発生すると、その参照に含まれるオブジェクトが回収できませんので、デーモンなど長時間実行するようなスクリプトを作る場合は循環参照を起こさないように注意する必要があります。

尚、PHP 5.3からは循環参照コレクタが使え、デフォルトで有効になっています。
実際にPHP 5.1.6と5.3.0を比較し、実際にどのような動きになるか以下のコードで試してみました。

< ?php

ini_set('memory_limit', -1);
// gc_disable();

class A
{
}

$loop_count = 1000000;
for($i = 1; $i <= $loop_count; $i++)
{
    $a = new A;
    $a->self = $a; // 循環参照
    if (($i % round($loop_count / 10)) ==  0)
    {
        printf("%10d: %10d KB\n", $i, memory_get_usage() / 1024);
    }
    // unset($a->self);
}

if ( function_exists('memory_get_peak_usage') )
{
    // (PHP 5 >= 5.2.0)
    printf("max %10d KB\n", memory_get_peak_usage() / 1024);
}

PHP 5.1.6の場合

PHP 5.1.6だと、以下のような結果になります。
循環参照されている状態だと、どんどん増え続けます。

$ php mem_test.php
    100000:      31947 KB
    200000:      63872 KB
    300000:     101942 KB
    400000:     127723 KB
    500000:     153504 KB
    600000:     203861 KB
    700000:     229643 KB
    800000:     255424 KB
    900000:     281205 KB
   1000000:     306986 KB

unsetの所のコメントアウトを外して開放するようにすると以下のような結果になります。
循環参照されていないので、メモリの開放は正しく行われます。

    100000:        622 KB
    200000:        622 KB
    300000:        622 KB
    400000:        622 KB
    500000:        622 KB
    600000:        622 KB
    700000:        622 KB
    800000:        622 KB
    900000:        622 KB
   1000000:        622 KB

PHP 5.3.0の場合

PHP 5.3.0の結果。一定以上から変化しません。
循環参照コレクタがうまく働いているようです。

$ php mem_test.php
    100000:       1582 KB
    200000:       1582 KB
    300000:       1582 KB
    400000:       1582 KB
    500000:       1582 KB
    600000:       1582 KB
    700000:       1582 KB
    800000:       1582 KB
    900000:       1582 KB
   1000000:       1582 KB
max       5139 KB

循環参照コレクタの有効/無効の変更はgc_enable/gc_disable関数で行います。
gc_disableの所のコメントアウトを外すと、PHP 5.3.0でもメモリが増え続けます。

$ php mem_test.php
    100000:      43125 KB
    200000:      85692 KB
    300000:     136451 KB
    400000:     170827 KB
    500000:     205202 KB
    600000:     272345 KB
    700000:     306720 KB
    800000:     341096 KB
    900000:     375471 KB
   1000000:     409846 KB
max     409863 KB

尚、gc_collect_cycles関数で強制的にガベージを収集することができますが、gc_disableで循環参照コレクタを無効にしている時に確保したメモリは収集されないようですので、わざわざ無効にする必要はないと思います。
また、gc_collect_cycles関数を使うと、わずかながらメモリが増えていく現象が見られましたので、ガベージの収集も基本的にはPHPに任せるようにした方が良いようです。

当然正しく開放されるようにプログラムを組むべきですが、デーモンのように長時間実行されるようなスクリプトを作る場合、石橋を叩いて渡るなら5.3以降を使うようにした方が良さそうです。

個人的な意見としては、開発時はgc_disableで開発し、テスト・運用時にgc_enableで動かすのが良いのではないかと思います。

マカー PHP

コマンド名やファイル名からパッケージを探す  

2010/5/23 日曜日 23:32:45

Linuxでパッケージを探すときに使うTips

コマンド名やファイル名がわかっていてパッケージ名が分からない時に、以下の方法でパッケージを探すことができます。

RPM系(Fedora/RHEL/CentOS/etc…)

準備

yumが使える環境であれば特に準備の必要はありません。

ファイルを探す

yum provides "*/ファイル名"

コマンドを探す

yum provides "*bin/コマンド名"

debian系(debian/Ubuntu/etc…)

準備

apt-fileがインストールされている必要があります。

sudo aptitude install apt-file
sudo apti-file update

ファイルを探す

apt-file search "ファイル名"

コマンドを探す

apt-file search "bin/コマンド名"

以上、簡単ですがパッケージを探す際の参考になれば幸いです。

マカー Linux

mod_rewriteでGETパラメータ名を変更する  

2010/4/16 金曜日 20:22:56

mod_rewriteのお話

旧システムから新システムの以降など、システムの仕様が変わる時でも古いURLを引き継ぎたい場合に役に立つ小技です。

問題1

GETパラメータの名前aaaをbbbに変更したい場合はどのようにすれば良いか?

ついつい以下のように設定してしまうかと思いますが、うまく動きません。

RewriteRule filename.html\?aaa=(.*) filename.html?bbb=$1 [L]

RewriteCondを使い、QUERY_STRINGの条件を指定すると、うまく動きます。

RewriteCond %{QUERY_STRING}     aaa=(.+)
RewriteRule filename.html filename.html?bbb=%1 [L]

%1で、RewriteCondでマッチしたパターンを取得できる。

問題2

GETパラメータの名前aaaをbbbに変更したいが、aaa以外のパラメータはそのまま残したい場合はどのようにすれば良いか?

解答1

QSAをつける。

RewriteCond %{QUERY_STRING}     aaa=(.+)
RewriteRule filename.html filename.html?bbb=%1 [L,QSA]

解答2

以下の場合、aaaのみbbbに書き換える事ができる。

RewriteCond %{QUERY_STRING}     ^(.*)aaa=(.+)$
RewriteRule filename.html filename.html?%1bbb=%2 [L]

オマケ

『上記試してみましたが、うまく動きません!』という場合

上から順に評価されるので、こんな事しているとうまく動かない。

#ファイルが存在する場合はそれを表示する
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.* - [L]
# ↑ファイルが存在した時点で、上記が評価されて終わってしまう。

#パラメータ名を書き換える。
RewriteCond %{QUERY_STRING}     ^(.*)aaa=(.+)$
RewriteRule filename.html filename.html?%1bbb=%2 [L]

順番を変更する事でうまく動く。

#パラメータ名を書き換える。
RewriteCond %{QUERY_STRING}     ^(.*)aaa=(.+)$
RewriteRule filename.html filename.html?%1bbb=%2 [L]
#↑先に評価される。

#ファイルが存在する場合はそれを表示する
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.* - [L]

そんな感じで、お役に立てれば幸いです。

マカー Apache, Tips

ProxyCommandを使った踏み台サーバー経由のssh接続  

2009/3/1 日曜日 2:54:29

MacやLinuxなど、sshクライアントにsshコマンドを使っている方のためのお話

夜間や休日で緊急事態のときは家からリモートメンテナンスを行うことがあるかと思います。
その際に、インターネットからサービスを行っているサーバーに直接sshでログインできるようにしておくのは不安がありますので、社内の特定のサーバーからのみ許可するようになっていることがあります。

この様な運用形態の場合、一度社内のサーバーにログインして、それから再度 sshコマンドで対象となるサーバにログインすることになり、一手間増えてしまいます。
できればワンステップでログインしたいところです。

sshコマンドにオプションをつけることで、ワンラインでログインできます。

ssh -o 'ProxyCommand ssh ユーザ名@中継サーバー nc %h %p' ユーザ名@ターゲットサーバー

これで一度にログインできるのですが、ProxyCommandと毎回入力するのは面倒なので、できれば更に短くしたいところです。

ローカルの ~/.ssh/config に以下のように記述しておくことで一度にログインできるようになります。

Host ターゲットサーバー
ProxyCommand ssh ユーザ名@中継サーバー nc %h %p

ターゲットサーバー名はワイルドカードが使えますので、*.xxxx.jpのような指定が可能です。

これで、

ssh ユーザ名@ターゲットサーバー

だけでログインできるようになります。

MacでSFTPおよびSCPクライアントを使用する場合は、Fuguを使用すると、~/.ssh/configに記述した内容を解釈し接続してくれます。
現時点ではCyberduckは中継をサポートしていませんので、Fuguを使用するとよいでしょう。

マカー Tips, ssh