hnwの日記

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


(2009/06/29)追記4:本記事のmb_trim関数が動かない環境があったので、詳細を「PCREはUnicode文字プロパティをサポートするとは限らない」にまとめました。よりポータブルなmb_trim関数も紹介していますので、併せてご覧ください。



追記:「mb_ereg_match('^[\0[:space:]]+$', $str);」で、今回pregで作った正規表現'/^[\s\0\x0b\p{Zs}\p{Zl}\p{Zp}]+$/u'と同一になりました。mb_regex_encoding関数が使える分だけmb_ereg版の方が使い勝手も上です。ちょっとショック。



(2009/02/24 17:00)追記2:もっと簡潔に、「mb_ereg_match('^[\0\s]+$', $str);」でいいことがわかりました。POSIX正規表現風の表記がキモいな、と思っていたので、これは素晴らしい。参考資料:http://www.geocities.jp/kosako3/oniguruma/doc/RE.ja.txt



(2009/02/26 12:00)追記3:mb_regex_encoding関数でSJISEUC-JPを指定した場合、\sなどでは全角スペースにマッチしません。ですので、上の「追記」の内容はUTF-8専用くらいに考えた方が良さそうです。mb_ereg系関数は、Unicode系のエンコーディングとその他のエンコーディングの場合とで挙動が結構変わるようですね。


PHPtrim関数というのは、文字列の先頭および末尾にあるホワイトスペースを取り除く関数です。しかし、実際には全角空白も除去したいよね、ということがよくあります。


さらに内部エンコーディングUTF-8の場合、全角空白だけ除去すればいいのか?他にも除去すべき文字があるんじゃないか?という疑問がわいてきます。今回はそんな疑問からスタートして、マルチバイト文字もうまく扱えるtrim関数を作ってみました。

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

trimを作るにあたり、今回はpreg_match関数を使います。これはPerl互換の正規表現マッチ関数です。


まず、pregの正規表現「\s」が何にマッチするのか調べてみましょう。次のようにUTF-8の1文字を全部検査してみました(サロゲートペアは無視していますが)。

<?php

for ($i = 0x00; $i <= 0xff; $i++) {
  for ($j = 0x00; $j <= 0xff; $j++) {
    $str = pack("CC", $i, $j);
    $ret = preg_match('/^\s+$/', mb_convert_encoding($str, "UTF-8", "UTF-16BE"));
    if ($ret !== 0) {
      printf("%s: %d\n", bin2hex($str), $ret);
    }
  }
}


結果は次のようになりました。

0009: 1
000a: 1
000c: 1
000d: 1
0020: 1


U+000C(Form Feed)が意外な気がしますが、残りは期待通りといえます(タブ、ラインフィード、キャリッジリターン、半角空白)。

/uフラグをつけてみる

次に、正規表現に/uフラグをつけて同じ実験をしてみます。PHPでは、preg_系の関数の正規表現に/uフラグをつけると、UTF-8の文字列に対しunicodeの1文字を理解した正規表現マッチを行います。

<?php

for ($i = 0x00; $i <= 0xff; $i++) {
  for ($j = 0x00; $j <= 0xff; $j++) {
    $str = pack("CC", $i, $j);
    $ret = preg_match('/^\s+$/u', mb_convert_encoding($str, "UTF-8", "UTF-16BE"));
    if ($ret !== 0) {
      printf("%s: %d\n", bin2hex($str), $ret);
    }
  }
}


結果、/uフラグが無いときよりマッチする文字列が増えました。

0009: 1
000a: 1
000c: 1
000d: 1
0020: 1
0085: 1
00a0: 1


/uフラグありのときに新たにマッチするようになったのは次の文字です。これは空白文字とみなせるような文字ですが、あまり身近ではないので少し意外な気がしますね。

  • U+0085 next line
  • U+00A0 no-break space


man perlreすると次のような記述も見つかりますが、PHPのpreg_match関数ではU+2028やU+2029は\sにマッチしないようです。

If Unicode is in effect, "\s" matches also "\x{85}", "\x{2028}, and "\x{2029}", see perlunicode for more details about "\pP", "\PP", and "\X", and perluniintro about Unicode in general.

他の空白文字にもマッチさせてみる

さて、そこでPHP 5.1.0からの記法を利用して他の空白っぽい文字にもマッチさせてみましょう。

<?php

for ($i = 0x00; $i <= 0xff; $i++) {
  for ($j = 0x00; $j <= 0xff; $j++) {
    $str = pack("CC", $i, $j);
    $ret = preg_match('/^[\s\p{Zs}\p{Zl}\p{Zp}]+$/u',
                      mb_convert_encoding($str, "UTF-8", "UTF-16BE"));
    if ($ret !== 0) {
      printf("%s: %d\n", bin2hex($str), $ret);
    }
  }
}


Zs(Space separator)、Zl(Line separator)、Zp(Paragraph separator)というのは、Unicodeで規定されているプロパティ名だそうです(参考:PHP:正規表現の詳細 - Unicode 文字プロパティ)。結果は次のようになりました。

0009: 1
000a: 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


U+3000(全角空白)や、先に挙っていたU+2028やU+2029など、それっぽい文字が全てマッチするようになりました。

マルチバイト対応のtrim関数

以上の結果を利用して、trim関数を作ってみました。


UTF-8の文字列を受け取って、文字列先頭および最後の空白文字(上記の文字すべてと、0x0bと0x00)を取り除きます。「Gauche:空白文字」を参考にしましたが、PHPのtrim関数にならい、U+0000(ヌル文字)も除去するようにしました。

<?php
  /**
   * マルチバイト対応のtrim
   *
   * 文字列先頭および最後の空白文字を取り除く
   *
   * @param  string  空白文字を取り除く文字列
   * @return  string
   *
   */
  function mb_trim($string)
  {
    $whitespace = '[\s\0\x0b\p{Zs}\p{Zl}\p{Zp}]';
    $ret = preg_replace(sprintf('/(^%s+|%s+$)/u', $whitespace, $whitespace),
                        '', $string);
    return $ret;
  }

最後に

これでいいのかどうか、自信はありません。Unicodeマニアの方のご意見をお待ちしております。

参考リンク

  • Gauche:空白文字
    • おそらく近い内容だと思ったので、このリストの文字を空白文字とみなすようにしてみました。
  • String.Trim メソッド
    • .NETにはマルチバイト文字に対応したtrim関数がるようです。標準でこんな関数があるなんて面白いですね。空白文字とみなす文字は今回実装したものとは少し異なっています。
  • Unicode文字のマッピング - Wikipedia
    • 今回空白文字とした文字以外にも、目に見えない文字がたくさんあることがわかります。何を基準に除去すればいいのか、難しいところです。