PHPのPCRE関数にあるUTF-8モードが厄介な件

\w に平仮名や漢字が引っ掛かっちゃったり \d に全角数字が引っ掛かっちゃったりするお話です
1

事の発端はあるバグチケットから

発端となったのはこのバグから。

Bug(バグ) #3289: 自動リンク機能で末尾のスラッシュを省略すると後続の日本語テキストもURLとしてリンクされてしまう - OpenPNE 3 - OpenPNE Issue Tracking System

大雑把に説明すると、
http://example.comあああ
といふ文字列に対して自動リンク機能を通したところ
「<a href="http://example.com">http://example.com</a>あああ」
ではなく
「<a href="http://example.comあああ">http://example.comあああ</a>」
のような残念な感じにリンクが張られてしまふといふバグである。

このバグを報告し終へた後、単純な正規表現のミスだらうと高を括って修正に取り掛かってゐた。が、原因が一向に掴めない。
そこで、次のやうなPHPコードを実行したところ preg_match が奇妙な挙動をすることに気がついた。

<?php
preg_match('/^([[:alnum:]]*)([[:^alnum:]]*)$/u', 'aaaaああああ', $match);
var_dump($match);

結果は次のとほりになる (PHP 5.4.4)

array(3) {
[0] =>
string(16) "aaaaああああ"
[1] =>
string(16) "aaaaああああ"
[2] =>
string(0) ""
}

をかしい、どう見ても [[:alnum:]] にひらがながマッチしてゐる。
ちなみに、正規表現の末尾に付いてゐる「u」は UTF-8 モード を有効にするオプションで、始めに書いた自動リンク機能の正規表現でも使はれてゐる。(参照: PHP: 正規表現パターンに使用可能な修飾子 - Manual

upsilon @kim_upsilon

「\w」はロケール依存なのか / PHP: エスケープシーケンス - Manual http://t.co/VedWsfM9

2012-12-27 17:10:11
upsilon @kim_upsilon

うわああpreg_matchのUTF-8モードが謎いいいい

2012-12-27 18:30:41
upsilon @kim_upsilon

[[:alnum:]] って書いたら [0-9a-zA-Z] にマッチしろよおおおおああああ

2012-12-27 18:35:47
upsilon @kim_upsilon

「UTF-8 モードでは、128 より大きなコードはどの POSIX 文字クラスにもマッチしません。」って書いてあるのにーーーー / PHP: 文字クラス - Manual http://t.co/lNZ6IQFj

2012-12-27 18:39:58
upsilon @kim_upsilon

/([[:alnum:]]*)([[:^alnum:]]*)/u と書くとですね、「aaaaああああ」だと \1 が 'aaaaああああ' で \2 が '' になるんですよ。これってトリビアになりませんかね

2012-12-27 18:43:33

ここで海老原さん (@co3k) から手掛かりが

Kousuke Ebihara💉💉 💉 💉 @co3k

@kim_upsilon http://t.co/dEOawVG9 読む限り [:alnum:] が \p{Xan} になるモードがあるようなので、コンパイル時にそういうフラグが立っちゃってるとかですかね?

2012-12-27 18:44:49
Kousuke Ebihara💉💉 💉 💉 @co3k

U+3042 は Other_Letter カテゴリに含まれるので Xan が L と N の組み合わせってことならマッチしそう

2012-12-27 18:57:19

http://www.pcre.org/pcre.txt を読んでみると、PCRE_UCP オプションとやらが有効になってゐる場合には [:alnum:] は \p{Xan} と同等に解釈されると書かれてゐる。
\p{Xan} はひらがなもマッチするやうなので、どこかで PCRE_UCP が使はれてゐるのが原因であると見てよささうだ。さうなれば PCRE_UCP の出所さえ分かれば原因が掴めるか。

PHP の当該ソースを読んでみる

upsilon @kim_upsilon

落ち着いてPHPのソースを読んでみる

2012-12-27 18:47:50
upsilon @kim_upsilon

落ち着くんだ、「PHP ソースコード」ではリポジトリの在処は出てこないぞ

2012-12-27 18:48:41
upsilon @kim_upsilon

PCRE_UTF8 と PCRE_UCP があったりなかったり https://t.co/voUCPtOl

2012-12-27 19:01:56

ext/pcre/php_pcre.c のUTF-8モードに関係する箇所 (359行目から366行目)
https://github.com/php/php-src/blob/master/ext/pcre/php_pcre.c#L359-366

upsilon @kim_upsilon

. \d,\D, \s, \S, \w, and \W recognize only ASCII characters, even in UTF-8 mode. However, this can be changed by setting the PCRE_UCP option

2012-12-27 19:10:08

そこで見たものは、「u」オプションによって無慈悲にも PCRE_UTF8 とともに問答無用で追加される PCRE_UCP オプションであった。
PHP では UTF-8 モードを使用する以上、PCRE_UCP オプションも強制的に有効となってしまふのだ。

upsilon @kim_upsilon

といふことで、 http://t.co/lNZ6IQFj にあった「UTF-8 モードでは…」は PCRE_UCP が無かった頃の話で今はさうでもないと

2012-12-27 19:12:47
upsilon @kim_upsilon

@co3k 解決しました。ありがとうございます。UTF-8モード有効時にPCREに PCRE_UCP オプションが渡されるようになったことが原因でした https://t.co/oHXmurB5

2012-12-27 19:20:32
Kousuke Ebihara💉💉 💉 💉 @co3k

@kim_upsilon なるほどです。てことは 5.3.4 から常に有効になったのか。すげえや

2012-12-27 19:24:35
upsilon @kim_upsilon

といふことは、\d は全角数字でもマッチするのか。うわあ

2012-12-27 19:31:25