SUNの標準の正規表現ライブラリであるjava.util.regexパッケージについて調べてみた。java.util.regexは非決定性有限オートマトン(NFA)に基づく(Javaには多数の正規表現パッケージがあるが決定性有限オートマトン(DFA)を用いた実装はないらしい)。
いきなりですが、ここで問題です。次のJavaの式の値は何でしょう(ちょっと引っ掛けかも。ヒントはサロゲートペア)。
"\uD835\uDD21".matches(".*[\\uD834\\uDD21]+.*")
ちょっといやらしい例だったので、もっと素直な例を。以下のJavaの式の値は何か(String#matchesは全体一致である事に注意)
"\uD834\uDD21".matches(".")
解説、\uD834\uDD21 は Java の Unicode エスケープでコードポイント U+1D121(一文字)を表す(サロゲートペア)。Unicode の正規表現での扱いはコードポイント単位であることが規定されている。
U+1D121 のカテゴリは OTHER_SYMBOL (So)。ちなみに Java 1.4 と 1.5 では扱いが異なる(はず)*1。
基本を確認したところで再び問題。以下のJavaの式の値はそれぞれどうなるか。(最後の式の結果は論理的には導けません。あしからず)
"\uD834\uDD21".matches("\\p{So}")
"\uD834\uDD21".matches("\\p{Cs}+")
"\uD834".matches("\\p{Cs}+")
一応、正解(というか実際の動作結果)を記しておく。結果は全てbooleanである。true を t、false を fで記す。出題順に / で区切り、複数の式がある場合は連続して記述する。
f/t/tft(Java version 1.5.0_09 での結果)
ちなみに、サロゲートペアは以下のように範囲で使用することもできる。
"\uD834\uDD22".matches("[\uD834\uDD21-\uD834\uDD24]+")
評価結果は true となる。
これはもちらん、
"\uD834\uDD22".matches("[\\uD834\\uDD21-\uD834\uDD24]+")
と書いても同じである。ところが、正規表現を
"[\\uD834\\uDD21-\\uD834\\uDD24]+"
と記述した場合、java.util.regex.PatternSyntaxException: Illegal character range が発生する。どうやら正規表現の文字クラス内のUnicodeエスケープの処理にバグがあるようである。
--------
*1: Java 1.4 (J2SE 1.4) はUnicode 3.0に準拠している。Unicode 3.0にはサロゲートペアの規定はあるものの、実際にBMP以外の領域(サロゲートペアを必要とする範囲)に文字は割り当てられていない。このため、J2SE 1.4のサロゲートペアへの対応は最小限にとどまっており、実質的には使えないに等しい。サロゲートペアに対応したのは Java 1.5 (J2SE 5.0)以降である。
ここまで、主にUnicodeのコードポイントに着目してJavaの正規表現(java.lang.regex)について調べてみた。しかし、Unicodeは単にコードポイントが決まれば文字が決まるような単純な体系ではない。同じ文字を表現する多くのコード列が存在する。同じものが複数あると不便なので「正規形(Normalization Forms)」という形が決められている(正規形と正規表現(regular expression)、英語だと全然違うのに日本語だと混乱しますね)。
ところが、Unicodeでは正規形も複数決められているのである(一体、どこが "Uni" code なんだ?という感じだが)。NFD, NFC, NFKD, NFKC の4通りある。NFDが最も基本的なもので、全ての「合成文字」を「分解」した形になる。と言っても解りにくいので例を挙げると、「が(U+304C)」という文字は「か(U+304B)+結合用濁点(U+3099)」に分解される。
さて、java.lang.regexでは CANON_EQ をサポートしている。CANON_EQ とは、Canonical Equivalents の事でJavaDocの日本語版では標準等価と訳しているが、一般的ではないようなのでここではCANON_EQ とする。問題は CANON_EQ の定義であるが、Unicode Technical Standard #18 に規定されている。しかし、規定をみても具体的な方法までは決められていない。Javaの正規表現では、「2 つの文字の完全な標準分解がマッチした場合に限り、それらの文字がマッチするとみなされます」とあるので、NFDに基づいている事が解る。
ところが、実際にjava.util.regexを使ってみると、どうも不思議な動きをする。具体的には合成文字を与えた場合と、結合文字列(基底文字と1以上の結合文字を続けた列)を与えた場合で動作が変わるのである。たとえば、合成文字を与えた場合は1文字と数えるのに対して結合文字列を与えた場合は複数文字と数えたり、合成文字を与えてもマッチしなかったパターンに結合文字列を与えればマッチする事がある。単純にNFDに変換してから比較している訳ではないらしい。
誤解していたかもしれないので補足しておく。「2 つの文字の完全な標準分解がマッチした場合に限り、それらの文字がマッチするとみなされます」は Canonical Equivalents の定義(文字列のNFDが等しい)を述べているに過ぎない。つまりJavaの「実装」がNFDに基づいているという意味はない。と読める(文字単位の比較である事は述べているように見えるが、文字=コードポイントとすると文字単位の比較がCannonicalというのは矛盾が残る)。JDK 6のドキュメントでは用語が「正規等価」、「正規分解」に改められている。
結局のところ、java.util.regex はコードポイント単位で処理するのが大原則であるという事につきるらしい。したがって、「が」は一文字だし、「か+濁点」は2文字と数える。「が」は「か」や「濁点」にはマッチしないが、「か+濁点」は「か」にも「濁点」にもマッチする。
従って、CANON_EQ の有無は、この例では「が」が「か+濁点」にマッチするかしないか、「か+濁点」が「が」にマッチするかしないかの違いだけである。
利用者から見れば、画面上に表示される文字(Grapheme Clusterと呼ぶらしい)単位で処理される事を期待しそうだが、java.util.regex はそうなっていない。あくまでもコードポイント単位である。従って同じ文字(Grapheme Cluster)であってもコードポイントの表現の方法によって正規表現のマッチの結果が変わる(たとえ CANON_EQ でも)。
Unicode Technical Standard #18 によればコードポイント単位の処理は誤りではない。例えばドット (.) は1つのコードポイントにマッチするのが標準らしい。Grapheme Cluster にマッチする記号の例としては \X が挙げられている(Javaでは実装されていない)。正規化と CANON_EQ はあくまでも別物と考えるべきであろう。
これはJavaの処理の場合であって、Unicode Technical Standard #18 ではあらかじめ正規化してから処理する方法も認めている。この場合は処理単位のコードポイントが元の文字列と変わる事になる。その代わり同じ正規形を持つ文字列であれば同じマッチ結果を持つことが保証される。
最近のコメント