hnwの日記

PCREはUnicode文字プロパティをサポートするとは限らない


(2011/05/19追記)CentOS5のpcreパッケージについて言えば、2010年7月以降Unicode文字プロパティが有効になっているそうです。安心ですね!(via「 CentOS5.5でCakePHP1.3系のInflector::slugを正常動作させる方法 - Lism.in * blog - nekoya (id:studio-m)」)


PCREというのは、Perl互換の正規表現ライブラリです。PCREは例えばPHPのpreg系関数で利用されていますし、他の処理系でも多く利用されているかと思います。ところで、PCREの挙動は環境ごとに異なる可能性があることをご存知でしょうか。具体的には、Unicode文字プロパティをサポートする環境としない環境とがあり、同じ正規表現でも挙動が変わることがあります。僕はそんなことを考えた事もなかったので、ビックリしました。


同じ原因で、以前の記事「PHPでマルチバイト対応のtrim関数を作る」の内容が環境によっては動かないことがわかりました。本記事でこの問題を解決し、よりポータビリティの高いmb_trim関数を作成します。

PCREのコンパイルオプションによる差

PCREって実はコンパイルオプションによって挙動が変わるんです。今まで考えたこともありませんでしたが、確かにそれくらいの自由度はあってもいいですよね。しかし、自分の書いた正規表現が動かない環境がある、となると大問題です。


具体的に問題となるのは、Red Hat Entreprise Linux 5 / CentOS 5の環境で「Unicode properties」のサポートが無いことです。これはpcretestコマンドで確認できます。

$ pcretest -C
PCRE version 6.6 06-Feb-2006
Compiled with
  UTF-8 support
  No Unicode properties support
  Newline character is LF
  Internal link size = 2
  POSIX malloc threshold = 10
  Default match limit = 10000000
  Default recursion depth limit = 10000000
  Match recursion uses stack


確かに、RHEL5/CentOS5の環境では「No Unicode properties support」となっています。「Unicode properties」というのは、「\p{Zs}」でUnicodeの空白文字1文字にマッチするような正規表現の記法です。


PHP:正規表現の詳細 - Unicode 文字プロパティ」によれば、この記法がPHP5.1.0以降で利用できるような事が書いてありますが、実はPHPのバージョンだけでなく、PCREのUnicode Propertiesサポートが有効になっている必要があるのです。


RHEL5/CentOS5について言うと、標準のPHP 5.1.6はシステム標準のlibpcre.so.0を動的リンクしており、システム標準のpcreパッケージは前述の通り「No Unicode properties support」でコンパイルされていますので、RHEL5/CentOS5標準のPHPではpreg系関数でUnicode文字プロパティが使えません。

続・PHPでマルチバイト対応のtrim関数を作る

実は、「PHPでマルチバイト対応のtrim関数を作る」で紹介したmb_trim関数ではUnicode文字プロパティを利用しています。内容として間違ってはいないのですが、RHEL5/CentOS5で動かないというのは大きな問題ですので、mb_ereg系関数を使って書き直してみます。

<?php
  /**
   * マルチバイト対応のtrim
   *
   * 文字列先頭および最後の空白文字を取り除く
   *
   * @param  string  空白文字を取り除く文字列
   * @return  string
   *
   */
  function mb_trim($string)
  {
    mb_regex_encoding("UTF-8"); // 本当はmb_trim呼び出し以前に1回実行すれば十分
    $whitespace = '[\0\s]';
    $ret = mb_ereg_replace(sprintf('(^%s+|%s+$)', $whitespace, $whitespace),
                        '', $string);
    return $ret;
  }


ちなみにmb_regex_encoding("UTF-8")は必須です。mb_ereg系関数は、どの文字エンコーディングを対象にするかによって挙動が変わるため、これが無いと挙動が変わってしまう環境があるのです。

mb_ereg_matchの\sは何にマッチするのか

mb_ereg系関数でUTF-8を対象にしている場合、\sはUnicodeの空白っぽい文字全てにマッチします。(参考:http://www.geocities.jp/kosako3/oniguruma/doc/RE.ja.txt


前回記事同様、Unicodeの全部の文字について調査してみました。

<?php
mb_regex_encoding("UTF-8");
for ($i = 0x00; $i <= 0xff; $i++) {
  for ($j = 0x00; $j <= 0xff; $j++) {
    $str = pack("CC", $i, $j);
    $ret = mb_ereg_match('^[\s]+$',
                         mb_convert_encoding($str, "UTF-8", "UTF-16BE"));
    if ($ret === true) {
      printf("%s: %d\n", bin2hex($str), $ret);
    }
  }
}


これをCentOS5の環境で実行すると下記の結果になりました。また、手元の環境(MacOSX)で確認したところ、PHP5.0.4以降の全バージョンで同じ挙動でした。

0009: 1
000a: 1
000b: 1
000c: 1
000d: 1
0020: 1
0085: 1
00a0: 1
1680: 1
180e: 1
2000: 1
2001: 1
2002: 1
2003: 1
2004: 1
2005: 1
2006: 1
2007: 1
2008: 1
2009: 1
200a: 1
2028: 1
2029: 1
202f: 1
205f: 1
3000: 1


前回記事でpreg系関数を利用した場合に比べてU+000Bが増えています。これは文字としては垂直タブであり、PHPtrim関数でも除去する対象の文字ですから、前回より良い実装になったということにします。


そもそも、これをホントに空白文字として除去していいのかどうかはアプリケーション次第だと思います。つまり、何を目的としてtrimしたいのかによるということです。ユーザーが無意識に入力した空白文字を混乱防止のためにtrimしたいのであれば、全角空白以外のマルチバイト文字を除去するのはやりすぎかもしれません。U+2028などがウッカリ入力しやすい文字だというなら別ですが…。

まとめ

  • PHP正規表現で日本語を扱うなら、preg系関数で/uフラグを使うか、mb_ereg系関数を使うかの2択
    • preg系関数のUnicode文字プロパティは使えない環境があるので注意
    • mb_ereg系関数を使うときはmb_regex_encoding("UTF-8")を忘れないように注意