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

Archive

Archive for the ‘PHP’ Category

PHPのfor文の条件式にcount関数を使用すると・・・ その後・・・  

2010/8/30 月曜日 18:02:24

初めまして、今月からお世話になりますマローです。
7月にうっち~さんがphpのfor()内でcount()を行うと・・・という話がありましたが、
実際に 速度を測定しました!
test code!!


<?php

define('COUNT_NUM'    ,1000);
define('IN_COUNT'    ,true);
$ary = array();

for ($i = 0, $j = 1; $i <= constant('COUNT_NUM'); $i++, $j++) $ary[$i] = $j;
if( constant( 'IN_COUNT')) {
    $time1 = microtime();
    for ($i = 0; $i < count($ary); $i++) {}
    $time2 = microtime();
}else{
    $time1 = microtime();
    $cnt = count( $ary);
    for ($i = 0; $i < $cnt; $i++) {}
    $time2 = microtime();
}

echo $time2 - $time1."\n";

?>

10回ずつ回して、平均を算出しました。
for()内でcount()する     0.000343
for()内でcount()しない     0.000134

ヤッター!ヽ(`д´)ノ0.000209秒も早い!

しかし、こいつは何をやってるのだろう・・・と思い、さっそくstrace!

straceじゃ、システムコールしかとれないじゃん・・・orz

というわけで、みんなも持っているgdbとphp本体のソースコードです。

php-5.3.x/ext/standard/array.c

この子の中にPHP_FUNCTION(count)が居ます。


PHP_FUNCTION(count)
{
    zval *array;
    long mode = COUNT_NORMAL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) {
        return;
    }

    switch (Z_TYPE_P(array)) {
        case IS_NULL:
            RETURN_LONG(0);
            break;
        case IS_ARRAY:
            RETURN_LONG (php_count_recursive (array, mode TSRMLS_CC));
            break;

case IS_ARRAY:でRETURN_LONGを返して終了です。

php_count_recursive は同じソース内のすぐ上にありました。


static int php_count_recursive(zval *array, long mode TSRMLS_DC) /* {{{ */
{
    long cnt = 0;
    zval **element;

    if (Z_TYPE_P(array) == IS_ARRAY) {
        /*~ 一部省略 ~*/
        cnt = zend_hash_num_elements(Z_ARRVAL_P(array));

        /*~ 一部省略 ~*/
    }

    return cnt;
}

php_count_recursive で「cnt = zend_hash_num_elements(Z_ARRVAL_P(array));」

cntにzend_hash_num_elementsの値が入ってます。

zend_hash_num_elementsは別のフォルダの「Zend/zend_hash.c」にありました。


ZEND_API int zend_hash_num_elements(const HashTable *ht)
{
    IS_CONSISTENT(ht);

    return ht->nNumOfElements;
}

値を返してるだけじゃねーか!!Σ(゜д゜lll)

実はHashTableの構造体を作成するときに、すでに要素数が入ってたのです・・・orz


typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;     /* 要素数 */
    ulong nNextFreeElement;
    Bucket *pInternalPointer;       /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
    #if ZEND_DEBUG
    int inconsistent;
    #endif
} HashTable;

というわけで、count()はこれといって作業をしているわけでは無く、

単純にfunction call の呼び出しコストといわけで・・・。

要は無駄な関数は呼ぶなという事ですね!

あと、二次元配列の場合は再帰的に「php_count_recursive」が呼ばれるよ!

マロー PHP, その他 ,

PHPの閉じタグ、イラネ  

2010/8/30 月曜日 14:04:43

うっち〜で〜す。
今回はPHPの閉じタグについて検証してみました。

ZendFrameworkではコーディング規約でPHPの閉じタグは
書いてはいけないことになっています。
でもその理由までちゃんと調べていなかったので調べてみました!

PHPではその用途上、”<?php”から”?>”以外の箇所は
HTMLとして出力される仕様になっているので、
閉じタグの後ろに無駄な改行があると
HTMLとして出力されてしまうようです!大変!!

ということで、問題が起きる場合を試してみました。

require.php

<?php
function Test($hoge) {
    echo $hoge;
}
?>
// 改行

test.php

<?php
require("require.php");

header('Location: http://www.example.com/');

Test("test");
?>

test.phpを実行するとWarningが出ます。
headerの前に改行分が出力されてしまっているので当然ですね。。

この場合、原因が掴みにくいので気をつけましょう><
PHPの仕様では閉じタグは無しでも許容されているので
ZendFramework同様に書かないということで統一させた方が良いかもしれません。

ただし、HTMLの文中に書く場合はもちろん閉じタグは必要になりますよー

※検証した環境
OS:MacOSX 10.6.4
PHP:5.3.2

うっち〜 PHP

urldecodeについて  

2010/8/3 火曜日 15:57:47

お久しぶりです。つっしーです。
またちょっと空いてしまいました・・・・
なんだかんだで入社して4カ月。早いですね。

今回はurldecodeについて。

urldecode  (string $str);

urldecodeは、URL エンコードされた文字列をデコードしたり、
与えられたあらゆる文字列をデコードする関数です。

例えば、ユーザ情報を入力してもらう画面で、当然メールアドレスを入力してもらうことになるのですが、
確認ページへ移動し、確認画面で入力された情報を出す時に、入力された情報を

$data= trim(urldecode('入力された情報');

とこんな感じでデコードしてトリムしてあげてたわけです。

と、ここで話は変わってGmailについてですが、Gmailのアドレスに’+~’と付け加えれる裏技があります。
例えば、abcdefghijk@gmail.comというアカウントを

abcdefghijk+07261@gmail.com

といった感じで入力すると、ちゃんとabcdefghijk@gmail.comにメールが帰ってくるのです。

これを利用することで、メールアカウントひとつでたくさん登録が出来るので、登録処理などのデバックが楽に。

話は戻って、上記のように入力された情報をデコードしてトリムする場合、Gmailの裏技が使えないのです。

原因はurldecodeでした。
urldecodeはあらゆる文字列をデコードするんですが、なんとこいつ、プラス記号 (’+') は、スペース文字にデコードしてしまうのです。

そこで、rawurldecode()関数の登場です。
この関数は、「文字列の中にパーセント記号 (%) に続いて 2 つの 16 進数があるような表現形式を、文字定数に置き換えて返します。」
という感じの関数です。
何を言ってるんだお前は、と思いますが要はこんな感じです

echo rawurldecode('foo%20bar%40baz');
// foo bar@baz

てな感じ。このrawurldecodeはプラス記号 (’+') をデコードしないんで、今回のメールアドレスのデコードに関してはもってこいの関数です。

しかし注意。
文字列に%が入っていて、その後に続いて16進数のような表記をしちゃうと置き換えられちゃうんで、rawurldecodeする前にバリデートしましょう。

つっしー PHP, 未分類

PHPのfor文の条件式にcount関数を使用すると・・・  

2010/7/26 月曜日 19:12:22

はじめましてー
今月からお世話になりますうっち〜です。
よろしくお願いします!

さて、最初から何を書こうか迷ってしまいましたが・・・

前々から気になってたPHPのfor文の挙動について調べてみることにしました。

配列の要素をfor文で取り出す時に
for文の条件式でcount関数を使用している例をよく見るのですが

for ($i = 0; $i < count($ary); $i++) {

ループする度にcount関数が毎回処理されてそうで・・・
僕は事前にcount関数を使用する形を使用しています。

$cnt = count($ary);
for ($i = 0; $i < $cnt; $i++) {

でも、本当にそのような挙動になっているのか!?
実はPHPが最適化してくれているのかも??
と、確信がなかったのでいつも気になっていました。
(for文にcount入れた方が楽なので無駄なことしてるんじゃないかと・・・)

ということで、ちょっと調べてみました。

下記A,Bの2パターンのコードを用意し、
gdbでcount関数内にブレークポイントを設定し
それぞれ実行してみてcount関数が呼ばれる回数を調べてみました。

// パターンA
$ary = array(1, 2, 3, 4, 5);
$cnt = count($ary);
for ($i = 0; $i < $cnt; $i++) {
echo $ary[$i];
}
// パターンB
$ary = array(1, 2, 3, 4, 5);
for ($i = 0; $i < count($ary); $i++) {
echo $ary[$i];
}

■結果
count関数が呼ばれた回数は

パターンA:1回
パターンB:7回

でしたー。

無駄ではなかったようです!

ただ、
実際にどのくらいの処理時間の差が出るのか?
気にするほどの時間なのか?
が気になるところです。

なのでそれはまたの機会にでも!

※※以下追記※※
環境書いてませんでした。。

※検証した環境
OS:MacOSX 10.6.4
PHP:5.3.2

うっち〜 PHP

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