PHPでtextileをパースする。

textileについての詳しい情報は、以下のサイトをご参照ください。wikiフォーマットみたいなものです。

http://redmine.jp/tech_note/textile/

結論から言うと、いいのが無くていまいちなんですが、phpでtextileをパースしてくれるクラスを探したら、以下の2つのクラスが見つかりました。

しかーし、両方共php4で書かれちゃってて、メンテも長いこと止まってるんですよね。php5の環境で動かしても警告が出たりするわけではないので、問題はないのですが。。。

php用のtextileをパースするためのクラス

http://michelf.com/projects/php-markdown/

このクラスはWordPressなどのプラグイン用のインターフェースも予め記述されていますので、プラグインとして使用すれば、記事の中でtextileが使用できるそうですが、h3などが上手くパースできませんでした。

http://textile.thresholdstate.com/

wikipediaからリンクが張ってあったので期待したのですが、これも長らくメンテされておりません。

ただし、今回の要件に必要なレベルでパースしてくれました。

正規表現連発すればphp5に出来るかもって思ったんですけど。。。

というわけで、ちょっと残念な結果。

住所からGoogle MAPに変換するショートコードなどを修正しました。

久しぶりにGoogle MAPに触る機会があったのでこのサイトで配布している以下の二つのスクリプトを修正しました。

いまさらですけど。(^^;)

phpのescapeshellarg()に日本語を渡す際の注意

めっちゃハマったのでメモ。

これはマニュアルにも記載されていないみたい。

escapeshellarg() の引数に日本語を渡す

もともと別のサーバーで動作していたphpスクリプトを別のサーバーに移行した際に、以下のような不具合に遭遇した。

<?php

$tmp = '日本語';
$tmp = escapeshellarg($tmp);

echo $tmp; // $tmpの中身がカラ!?

?>

要はescapeshellarg()に日本語を渡すと値がカラ(シングルクォートのみ)になるということ。

原因

いろいろ調べた結果、どうやらescapeshellarg()は、OS側の環境変数LANGの値によって挙動が変わるらしい。

今回はインストール直後の某VPSに移行したばかりだったのでLANGが設定してなかった。

そこで、/etc/sysconfig/i18nに以下のような記述を追加したら正常に動作した。

LANG=ja_JP.UTF-8

無効なマルチバイト文字列は処理しないということらしい。

なるほど、かしこい!

PHPで条件付きGETとかEtagとかでパフォーマンス向上

先程アップデートしたWordPressプラグインで実装したコードの一部をご紹介。

以下で紹介する関数はPHPで条件付きGET(Conditional GET)に対応するための関数です。

どういうものかというと、ブラウザからリクエストがあった際に、最終更新日(Last-Modified)をレスポンスヘッダでおくると、そのブラウザは次回以降のアクセスの際に「あの日以降更新した?」と聞いてくるので、更新してなかったときには「まだしてないよ」とだけ返す(コンテンツは返さない)仕組みのことです。

ソース

function conditional_get($time = 0)
{
  $last_modified = gmdate('D, d M Y H:i:s T', $time);
  $etag = md5($last_modified);
  header('Last-Modified: '.$last_modified);
  header('ETag: "'.$etag.'"');
  if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $last_modified) {
      header('HTTP/1.1 304 Not Modified');
      exit;
    }
  }
  if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
    if (preg_match("/{$etag}/", $_SERVER['HTTP_IF_NONE_MATCH'])) {
      header('HTTP/1.1 304 Not Modified');
      exit;
    }
  }
}

使い方

あらかじめ、コンテンツの最終更新日を取得して、conditional_get() に渡すと、必要な処理をいろいろやってくれます。

そのあとで、実際のコンテンツを取得して出力です。
実際には、$time以降の更新がない場合は、conditional_get() 内で終了します。

$time = get_modified() // コンテンツの更新日時をローカルタイムスタンプで取得

conditional_get($time);

$html = get_contents(); // コンテンツを取得
echo $html;

conditioal_get() の前にコンテンツを取得しちゃうと意味が半減することがあるかもしれないので注意。

livedoor の天気予報 API 用クラスつくった。

いまさらって感じもあるかもしれませんが、livedoor の「お天気 Web サービス」用の XML をパースして、JSON とか PHP の配列で取得するためのクラスを作った。

PHPのlivedoor Weather Hacks API用クラス

でも、いざ使おうと思ったら、「お天気 Web サービス」そのものが当初予定していた用途にマッチしないことに気がついて、あららと思いながらも、せっかくなので完成(?)させた。

ところで

こういうことって、気象庁あたりがやるべきではないんだろうか?と、おもって調べたら、ここでアメダスのデータをほぼリアルタイムに掲載していた。

で、クローラーつくって取り込んでしまえと思ったら、HTML内に重複したIDが指定されているらしくパーサーがエラー。。。

「自動巡回ソフトは原則としてお断り」みたいなことが書いてあったが、新手のクローラー対策だろうか?(笑)

PHP5のDOM拡張モジュールでHTMLをパースする

PHP5で追加されたDOM拡張モジュールではHTMLのパースも可能である。
さらに、このDOM拡張モジュールは標準でインストールされている。

今回の記事では、このDOMDocumentクラスについてのおぼえがき。

ちなみに、DOMが苦手な人は”要素”とか”属性”を理解できずに、まとめてタグとしてしか理解出来ていないひとが多い気がする。

HTMLを読み込む

まずはじめに、DOMDocument オブジェクトを生成する。

$doc = new DOMDocument();

そのあとで、HTMLを読み込む。
以下のようにloadHTML()などを使用すると閉じタグのないHTMLでもパース可能になる。

$doc->loadHTML('<html></html>'); // HTMLソースを読み込む
$doc->loadHTMLFile('/path/to/index.html'); // HTMLファイルを読み込む

DOM拡張モジュールによるHTMLの読み込みには上記のように2種類の方法が用意されており、HTMLファイルを読み込む方法は、http:// からはじまるURLでも指定可能である。

2011/03/04 追記

HTMLを読み込む際に、上記の処理では文字化けするサイトがあることがわかった。
以下のような処理を行えば解消した。

$html = file_get_contents('/path/to/index.html');
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'auto');
$dom->loadHTML($html);

参考: 2008-05-30 – hamacoの日記

HTMLをパースするために実際の処理

HTMLパースの流れはJavaScript等のDOM操作と同じなので、わかるひとにはすぐにわかると思う。

HTML内の全てのA要素を取得する。

$elements = $doc->getElementsByTagName('a');

ちなみに、パラメータで指定する要素名(上記の例ではa)は、大文字小文字を区別するようなので注意。

取得した要素がいくつあるかを取得する。

以下のような感じ。

$elements->length

取り出した全ての $elements のhref 属性を出力する。

foreach ($element as $e) {
    print $e->getAttribute('href');
}

これも大文字小文字を区別するのかな?

取り出した全ての $elements のノード値を取得する。

<a>~</a> で囲まれた部分を取得するには。

foreach ($element as $e) {
    print $e->nodeValue;
}

取り出した $elements の n 番目の属性値を取得する。

取得した最初のa要素の href 属性を取得するには?

$elements->item(0)->getAttribute('href');

参考

Mail_CheckUserをGitHubに移行しました。

本サイトで配布しているメールアドレスをより厳密にチェックするためのクラス”Mail_CheckUser”をGitHubに移行しました。

miya0001′s Mail_CheckUser at master – GitHub

PEARに登録しかかっているので、早々に何とかしたいのですが、例外処理をきちんとしなさいとの難題を押し付けられて(笑)、どんづまり状態を打破すべく、GitHubで一緒に勉強しながら手伝ってくれるかたを募集中です。

PHP向けに軽くて簡単なテンプレートエンジン作った。

PHP向けに軽くて簡単なテンプレートエンジンを作った。

tinyTemplate

Smartyを使うのは面倒だけど、ビューとロジックはできればわけたい。みたいなときにご利用いただくと便利だと思います。

私は、メールフォームのメールテンプレートで使いたくて、開発しました。

考え方はいろいろあると思いますが、デフォルトではすべての出力をHTMLエスケープするのも特徴の一つです。

PHPのcall_user_func()での参照渡しに注意

どうもよくわからんのでメモ。

参照渡しとは?

関数に値を渡す通常の方法。

<?php

$i = 0;

test($i);

print $i; // 0を出力。$iの値そのものは変わらない

function test($int){
  $int++;
}

?>

参照渡しにする。(関数の定義の引数の部分に&をつける。)

<?php

$i = 0;

test($i);

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

通常の関数の引数は、渡された変数の値のみが渡されるが、参照渡しをすると変数そのものが渡されるので、このような挙動になる。

PHP5での仕様変更

ここまでは、特に問題はない。

しかし、PHP4では、以下のように呼び出し側でも参照渡しの指定ができたが、これはPHP5ではphp.iniを変えないと動作しない。

<?php

$i = 0;

test(&$i); // 呼び出す際に&をつける

print $i; // 1を出力。$iの値がかわった!

function test($int){
  $int++;
}

?>

これは実際、そうするべきだと思う。

なんで呼び出し側で参照渡しをできるようにしていたのか、よくわからんので、改善だと思う。

が!問題はここから!

call_user_funcを使用した際の参照渡し

以下のようにcall_user_funcを使用すると、関数側で参照渡しするよう指定していても参照渡しされないばかりか、警告が出る!

<?php

$i = 0;

call_user_func('test', $i);

print $i; // 0

function test(&$int){
  $int++;
}

?>

出力結果

Warning: Parameter 1 to test() expected to be a reference, value given in...

ためしに、call_user_func(‘test’, &$i); としたら予想通り以下のエラー。

Deprecated: Call-time pass-by-reference has been deprecated in

なんじゃそりゃ!?

解決策

いろいろ試した結果、以下のような方法なら大丈夫なようだが、全く納得がいかない。

call_user_func_array() を使用する

<?php

$i = 0;

call_user_func_array('test', array(&$i));

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

関数名を変数に代入してコールする

<?php

$i = 0;

$func = 'test';

$func($i);

print $i; // 1を出力。$iの値がかわった!

function test(&$int){
  $int++;
}

?>

たまたま、今のバージョンで警告が出ていないだけなのか?
もしそうなら、 call_user_funcしたいときは、どうやって参照渡しするんだ?

参考

とても小さなPHPのテンプレートエンジン “bTemplate”

WordPressのプラグイン開発でビューとロジックを分離するためにテンプレートエンジンを使いたくなった。

PHPのテンプレートエンジンでは、Smartyが有名だが、WordPressのプラグインに同梱するにはあまりにも高機能すぎるしファイル数も多い。

そこで、できれば1ファイルで構成されていて、シンプルで簡単なテンプレートエンジンはないものかと探してみたら、bTemplateというのがあった。

bTemplate

このbTemplateは、わずか289行のPHPで書かれたテンプレートエンジンで、思わず未完成のものではないかと心配してしまうぐらいシンプルなソースで記述されているが、ほぼ期待通りの動作を確認できた。

実装は、なんとなくPerlのHTML::Templateと似ている。

ちなみに、本家で配布しているファイルは、最終行に余分な改行が含まれており、そのままでは空白が出力されてしまうので使用する前に削除しておく必要がある。

主な機能

  • 変数の割り当て
  • ループ
  • 条件分岐(ifまたはelseのみ)

Smartyなどのような高機能なテンプレートエンジンとは違い、キャッシュ機能は実装されていないが、そのためにパーミッションの設定などに煩わされることがない。

やや残念なのが、条件分岐が変数のtrueまたはfalseによるifまたはelseしか実装されていない点であるが、これはロジック側で頑張れば良いことなので問題なさそう。

サンプル

単純なテンプレート

<html>
  <head>
    <title><tag:title /></title>
  </head>

  <body>
    <p>Hello, <tag:name /></p>
  </body>
</html>

上記テンプレート用のロジック(PHP)

<?php
$title = 'My Simple Template Example';
$name = 'L.E. Modesitt, Jr.';

include_once('bTemplate.php');
$tpl = new bTemplate();

$tpl->set('title', $title);
$tpl->set('name', $name);

echo $tpl->fetch('simple.tpl');
?>

シンプルなループ

<ul>
  <loop:names>
    <li><tag:names[] /></li>
  </loop:names>
</ul>

上記テンプレート用のロジック(PHP)

<?php
$names = array('Tom', 'Dick, 'Harry');

include_once('bTemplate.php');
$tpl = new bTemplate();

$tpl->set('names', $names);

echo $tpl->fetch('loop.tpl');
?>

SQLで得た結果をテーブルに出力

<table border="1">
  <tr>
    <td><b>id</b></td>
    <td><b>last_name</b></td>
    <td><b>first_name</b></td>
    <td><b>phone_number</b></td>
  </tr>
  <loop:users>
    <tr>
      <td><tag:users[].id /></td>
      <td><tag:users[].last_name /></td>
      <td><tag:users[].first_name /></td>
      <td><tag:users[].phone_number /></td>
    </tr>
  </loop:users>
</table>

PHP側

<?php
// Assuming you've already connected to the database
$result = mysql_query('SELECT * FROM users');

// This loop populates an array with the data from the query
while($row = mysql_fetch_assoc($result)) {
  $users[] = $row;
}

include_once('bTemplate.php');
$tpl = new bTemplate();

$title = 'Database Results';
$tpl->set('title', $title);

// Now we set the array we populated
$tpl->set('users', $users);

// And here we fetch the users.tpl file and set it to content
$tpl->set('content', $tpl->fetch('users.tpl'));

echo $tpl->fetch('master.tpl');
?>

ちなみに、テンプレートのHTMLをファイルからではなく、変数から取得したい場合は、以下のようにすればいい。

echo $tpl->parse($content);