[ カテゴリー » 開発日誌 » PHP ]

gethostbyaddrのキャッシュ化

category-icon

 こんばんは、martinです。前回に引き続いて、高速化のお話。

 ppBlogは、それなりに高機能だと思っているアクセス解析機能が初めから付いていますが、ログ取得のためにアクセス時のIPアドレスから、PHPのgethostbyaddr()Link 関数を使ってホスト名を取得できれば取得するようにしています。gethostbyaddr()関数はDNSサーバに問い合わせてIPアドレスに対応するホスト名を調べる、いわゆる逆引きを行ってくれます。なので、ともすると、この段階がページ表示スピードのボトルネックになる可能性があります。

 なので、IPアドレスに対応するホスト名のキャッシュ化を考えてみました。情報をどこに保持するかですが、ひとつのIPアドレスに対して、ひとつの対応するホスト名さえ分かればOKなので、ここはクッキーに情報を保持するのが良さそうです。その際に、「生」の値を保持するのは、何となく気持ち悪いので暗号化してクッキーに保持するようにします。

 アクセス解析のための情報取得は、stat/log.phpが担当してるのですが、ここ1-2日間、log.phpを結構書き換えています。IPアドレス/ホスト名のクッキー化に関する記述は、今のところ以下のような感じです。

/*
 * gethostbyaddrのキャッシュ化による高速化。クッキーによる実装。
 * 暗号化されたホスト情報がクッキーに入ってるなら、それを取得。そうでなければ逆引きをする。
*/
if(isset($_COOKIE['PPBLOG_DNS']) && strpos(($DNS = ' '.my_decrypt($_COOKIE['PPBLOG_DNS'])), $ip)){
  list($host,) = explode("¥t", ltrim($DNS), 2); // ホスト名の取得。gethostbyaddr()関数よりずっと高速。
}

if(!isset($host)){ // クッキーに記録されていなければ、そこで初めてgethostbyaddr()関数を使う
 $host = ($host = gethostbyaddr($ip)) ? $host : $ip;
 if($host != '') setcookie('PPBLOG_DNS', my_encrypt("$host¥t$ip"), time() + 30*24*3600);
}

 gethostbyaddr()関数と比べて、どれくらい速いか計測したところ、少なくとも40倍以上高速になりました。この実装で問題がないようであれば、次期バージョンアップ時に採用したいと思っていますが、試してみたいという方のために添付しておきます。単なる上書きで良いです。その際、元のlog.phpのバックアップは取っておいて下さい。

添付ファイル: log.phpattachedIcon 

— posted by martin at 04:18 am   commentComment [8]  pingTrackBack [0]

array_shiftとreset

category-icon

 こんばんは、martinです。ppBlogでは、「軽快な動作」を常に念頭において開発しています。PHPには、実に様々な関数が最初から提供されていて、似たような振る舞いをする関数も結構あります。なので、こういう場合は、それぞれの関数で処理時間を計測して、より処理速度が速いほうを採用するようにしていますが、最近、そんなやつをひとつ見つけました。

 ppBlogでは、内部でいろんな配列が定義されていて、それを切ったり貼ったりして使用してます。その際に、配列の先頭の要素を取り出すという処理を至る所でしているのですが、これまではarray_shiftを用いていました(というかそれしか知らなかった)。

 

array_shift() は、array の最初の値を取り出して返します。配列 array は、要素一つ分だけ短くなり、全ての要素は前にずれます。 数値添字の配列のキーはゼロから順に新たに振りなおされますが、 リテラルのキーはそのままになります

引用元: PHP: array_shift - ManualLink
 上の赤字にあるように、元の配列を変えてしまうという点に留意していれば、便利な関数です。で、最近別の似た振る舞いをする関数を見つけました。reset()関数です。

reset() は、array の内部ポインタの先頭の要素に戻し、配列の最初の要素の値を返します

引用元: PHP: reset - ManualLink

 何となく、内部ポインタを先頭に移すんだなという印象でファイルオープンfopen()などを使う場面ぐらいでしか使わないと思っていたのですが、その要素も返すんですね。

<?php
$stack = array("orange", "banana", "apple", "raspberry");
$fruit = array_shift($stack); // 結果はorange
 // $stackはarray("banana", "apple", "raspberry")になる
?>
<?php
$stack = array("orange", "banana", "apple", "raspberry");
$fruit = reset($stack); // 結果はorange、$stackは元のまま
?>

 さてどちらが処理速度が速いかというと、ローカルな環境(PHP5.2.9)では、実はそんなに変わりません。100万回の試行で、前者が3.4秒のところが後者のreset()関数だと3.2秒と、ちょっとだけ速いかなという感じです。でもタイピングも少なくて済むし、こういうのの積み重ねが大事なので、今後はreset()を使うようにしようと思います。余談ですが、ラズベリー(raspberry)の綴りはちょっと意外でした。

 ここでひとつ押さえておくべきことは、上の$stackの例のように、もし配列のキーが定義されていない、つまりキーインデックスがゼロから始まるような配列ならば、わざわざ関数を持ち出さなくても、
<?php
$stack = array("orange", "banana", "apple", "raspberry");
$fruit = $stack[0]; // 結果はorange、$stackは元のまま
?>
でこと足りるということです(これだと100万回の試行では2.4秒と最速)。これで済む場合はそうしますが、ppBlogの内部では、
Array
(
     [3] => orange
    [11] => banana
    [23] => apple
    [29] => raspberry
)
みたいに、キーの添え字にも意味がある処理が多いです。

 

— posted by martin at 06:36 pm   commentComment [0]  pingTrackBack [0]

ppBlogをサーバー上でZIP展開

category-icon

 こんばんは。どうも昨日辺りから自宅のネット接続がとろいです。ppBlogは、ファイル数は390個(決して多くはないと思いますけど)近くあるのですが、それが昨日は全部アップロード出来ずじまい。愛用しているFFFTPGの転送画面が何度も途中でフリーズしたようになってました。昔、28.8kbps?ぐらいのモデムでピーガラガラとかやってたのを思い出しはしませんでしたが。

 というわけで、ブログシステムというものは、多機能になればなるほど、当然のようにそのサイズが肥大化していきます。なので、サーバー上に、圧縮されたZIPファイルをさくっとアップロードして、そこで展開するのが楽だよなぁ、と思ったので、PHPの充実したオンラインマニュアルを片手に、そういうスクリプトを書いてみました。最初に書いたのが以下です。ローカルのテスト環境でわけなく作動したので、こりゃいけるとサーバー上で試したら、zip_open関数がないと怒られました(--)

function my_unzip($zipFile=''){
 $zip_dir = getcwd().'/';  // カレントディレクトリの取得
 if(is_dir(str_replace('.zip', '', $zipFile))){ // 既に展開されたディレクトリがあるならば
  exit("Already exists!");
 }
 $i = 0; // どれくらいファイルがあるのか知りたい
 if($zip = zip_open($zip_dir.$zipFile)){
  while($zip_entry = zip_read($zip)){
   $file = zip_entry_name($zip_entry);
   $zdir = $zip_dir.dirname($file);
   if(!is_dir($zdir)){
    @mkdir($zdir, 0777); // ディレクトリないなら作成
    echo "Directory: ".dirname($file)."/ created.<br />¥n"; // メッセージ表示
   }
   if(zip_entry_open($zip, $zip_entry, 'rb')){ // ファイルの書き出し
    if($fp = @fopen($file, 'wb+')){
     fputs($fp, zip_entry_read($zip_entry, zip_entry_filesize($zip_entry)));
     fclose($fp);
     echo " File ".++$i.": $file created.<br />¥n";
    }
   }
   zip_entry_close($zip_entry);
  } #while
 }
 zip_close($zip);
 return TRUE; // 無事に終わったらTRUEを返す
}

これを以下のようにしてローカルで走らせました。

if(my_unzip('ppBlog180b.zip')){
 echo "Successfully extracted!¥n";
} else echo "Failure!¥n";

したら、ファイル数が390個近くあることが判明。で、実際にオンラインのサーバー上でもやってみたら駄目だったわけです。なので、シェルコマンドでトライしてみました。

$zip_file = getcwd().'/ppBlog180b.zip'; // 展開するファイルを指定

function unzip($zip_file){
 return shell_exec("unzip $zip_file"); // UNIX系には、まず付いてるであろうunzipを使う
}

if($zip = unzip($zip_file)){
 echo "<pre>$zip</pre>¥n"; // これでずらっと展開されたファイルが表示される
 echo "$zip_file: Successfully extracted!
"; echo '<a href="./ppBlog180b/install.php">installation</a>'; // 続けてppBlogのインストール画面へ誘導 } else{ echo("Failed to extract: $zip_file¥n"); }

そしたら、今度は上手く行きました。すべてのファイルがそっくりサーバー上で展開されていました。これは楽チンですねぇ。上のスクリプトは、単にZIP圧縮されたフォルダを展開するだけなので、サーバー上に作成されたディレクトリは、圧縮前のフォルダの名前と同じになります。多分、copyコマンドとかchdirで、展開先やら名前を指定できるでしょうね。

 一応、この簡単なスクリプトを添付しておきます。お使いのサーバーで動くかどうかは保証しませんが。。と、その前にppBlog180b.zipがないことには・・・。

添付ファイル: ppblogunzip.phpattachedIcon 

— posted by martin at 03:58 am   commentComment [0]  pingTrackBack [0]

UNIXタイムスタンプからハッシュ生成

category-icon

 ppBlogバージョン1.7.7から採用している機能のひとつに、コメント通知のメールからダイレクトでログイン出来てコメント返信できるというのがあります。その際に、リンクのURLに付加する文字列が今いちランダムになっていないので、ちょっとアルゴリズムの見直しをしてみました。時限性を持たせるために、UNIXのタイムスタンプから生成したハッシュをリンクのURLに付加しているのですが、タイムスタンプをチェックしたいので、復元可能なものであることが必要です(一方向で良ければ、sha1関数Link を使えば良いけどニーズに合わない)。

 単純に、UNIXのタイムスタンプをppBlogで用意している暗号化関数my_encrypt()に通すと、タイムスタンプの性質上、最後数桁しか変化せず、それに対応して生成されたハッシュも下数桁しか変化しません。まぁ、これはmy_encrypt()の性能の問題なんですが、この部分はそんな高性能でなくてよいし、過去の資産との互換性も考えると my_encrypt()はいじりたくないです。

$unix_time1 = 1234560000; // 10桁のUNIXタイムスタンプ
$unix_time2 = 1234567890; // 上と下4桁が違うだけ
echo my_encrypt($unix_time1); // 結果は、VwoLV1MOAwhVBg
echo my_encrypt($unix_time2); // 結果は、VwoLV1MOBABcBg

こんな感じでえらく似通った文字列になる(--) なので、このタイムスタンプを逆順にして更に基数変換すればよいかなと。逆順にすれば、大きく数値が変動します。そこで、文字列を逆順にする関数はないかstr_reverseという関数名で探してみましたがなさそうです。なので、

$unix_time_reverse = join("", array_reverse(preg_split("//", time())));

という力技で暫くローカルのテスト環境で動かしていたんですが、いやきっとあるはずだと、もう一度マニュアルを探してみたらありました。

$unix_time_reverse = strrev(time());

strrevLink という名前でしたか・・・。

 実は、単に逆順にしただけでは、my_encrypt()のアルゴリズムからしてそう大きな変化は望めないので、更に base_convert()Link 関数を利用して10進数から2進数に変換します。decbinLink でも行けるかなと思ったんですが、扱える最大の数が10 進数の4294967295とのことで、これは使えない。

$unix_time_binary = base_convert(strrev(time()), 10, 2); // 10進数を2進数に変換

 これで最初に挙げた例だと、

$encoded1 = my_encrypt(base_convert(strrev("1234560000"), 10, 2));
 // 結果は、VwgIUlcJAglUBgEJU1RUUgdRVAk
$encoded2 =  my_encrypt(base_convert(strrev("1234567890"), 10, 2));
 // 結果は、VwkJU1cIAglVBwEJU1VVUgZRVQgJVgIEVFcGAwhT
$encoded3 =  my_encrypt(base_convert(strrev("1235064688"), 10, 2)); // 別の例
 // 結果は、VwgIU1YJAwhVBgAJUlRUUgZQVAgIVgMFVVYHAwhSUAdWCQ

まぁ、最初に比べるとだいぶマシかなと思います。さて、この復元ですが、逆の操作をするだけです。

$decoded = strrev(sprintf("%010s", base_convert(my_decrypt($encoded1), 2, 10)));
 // この場合は、1234560000 を返す

これで、元の10桁のタイムスタンプを取得できます。ひとつ注意する点は、順序を逆にした文字列を戻す際に、元のタイムスタンプによっては逆順にした文字列の先頭がゼロになる場合があるということです(上の例だと反転した0000654321654321と解釈される)。なので、sprintfLink 関数を用いて、ゼロで10桁になるように埋め合わせをしておきます。

sprintf("%010s", "654321"); // この結果は、0000654321

 実際の実装(?)としては、これらとsha1関数を組み合わせたりして、長い文字列を生成してます。

— posted by martin at 02:52 am   commentComment [0]  pingTrackBack [0]

マルチバイトを考慮したstr_replace関数

category-icon

 こんばんは。最近、めっきり寒くなってきました。ラボは基本的に中央暖房なんですが、古いせいか壊れていて温度計は14度ぐらい。冷え症の身には応えます。

 さて、ユーザーの方から、パスワードやIDを変更したタイミングでログが壊れるという報告がありました。こちらでは再現出来なかったんですが、調べたところ、ログの書き換えにマルチバイトに対応していないstr_replace関数を使っているのが原因かと思いますので、マルチバイトに対応したやつを考えてみました。PHPには、マルチバイトに対応した色んな関数が揃っているLink のですが、頻用するstr_replaceのマルチバイト版mb_str_replaceが(何故か)ありません。オンラインのPHPマニュアルには、ユーザー寄稿のメモが沢山載せてあって、色々と参考になります。この中に、mb_str_replace()があったので、それに若干修正を加えて以下のようなものを用意しました。

/* マルチバイトを考慮したstr_replace */
function my_str_replace($search, $replace, $target, $encoding = ENCODE){
 $notArray = !is_array($target) ? TRUE : FALSE;
 $target = $notArray ? array($target) : $target;
 $search_len = mb_strlen($search, $encoding);
 $replace_len = mb_strlen($replace, $encoding);
 foreach ($target as $i => $tar){
  $offset = mb_strpos($tar, $search);
  while ($offset !== FALSE){
   $tar = mb_substr($tar, 0, $offset).$replace.mb_substr($tar, $offset + $search_len);
   $offset = mb_strpos($tar, $search, $offset + $replace_len);
  }
  $target[$i] = $tar;
 }
 return $notArray ? $target[0] : $target;
}

 これで、str_replace関数と大体似たような挙動をしてくれますが、本家str_replaceと違う点は、引数(ひきすう)の$search$replaceに配列を指定出来ないことです。なので配列に対応した関数を用意しましょう。上記の関数を使います。

/*
 str_replace関数のマルチバイト版
 上記 my_str_replace()が必要
*/
function mb_str_replace($search, $replace, $target){
 if(is_array($search)){ // $searchが配列なら
  if(!is_array($replace)) $replace = array($replace);
  foreach ($search as $i => $needle){
   $rep = isset($replace[$i]) ? $replace[$i] : $replace[0];
   $target = my_str_replace($needle, $rep, $target);
  }
  return $target;
 } else return my_str_replace($search, $replace, $target); // $searchが配列でないとき
}

これを使ったサンプルをば。全角空白を□(四角)に、「空白」という文字を「四角」に置換します。

$target = " おか機 ←全角空白です。";
$search = array(' ', '空白');
$replace = array('□', '四角');
$result = mb_str_replace($search, $replace, $target);

var_dump($result); // これで結果を出力

これの結果は、以下の通り。パフォーマンスもそんなに悪くないと思います。

string(39) "□おか機□←全角四角です。"

これの修正を施したutils_admin.phpの最新版を添付しておきます。replace_log_by_target()関数界隈が変わっています。

添付ファイル: utilsadmin.phpattachedIcon 

— posted by martin at 05:19 am   commentComment [0]  pingTrackBack [0]

T: Y: ALL: Online:
Created in 0.0071 sec.
prev
2017.12
next
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31