1. ホーム
  2. java

[解決済み] Java正規表現でUnicodeに対応するのは?

2022-07-16 04:48:50

質問

最近の多くの正規表現実装は \w という文字クラスの略記を解釈します。そのため、以下のような正規表現は \w+ のような単語にマッチします。 hello , élève , GOÄ_432 または gefräßig .

残念ながら、Javaはそうではありません。Javaでは \w は制限され [A-Za-z0-9_] . このため、上記のような単語のマッチングが困難であるなどの問題があります。

また、どうやら \b というワードセパレータが、マッチしないはずの場所でもマッチしているように見えます。

.NETライクでUnicode対応の \w または \b をJavaで使うことはできますか?他のどのショートカットがUnicodeに対応させるために書き換えが必要ですか?

どのように解決するのですか?

ソースコード

以下で説明する書き換え関数のソースコードです。 はここで入手できます。 .

Java 7でのアップデート

サンが更新した Pattern クラスには、すばらしい新しいフラグがあります。 UNICODE_CHARACTER_CLASS という素晴らしいフラグがあり、これによってすべてが再び正しく動作するようになります。これは、埋め込み可能な (?U) として埋め込むことができます。 String クラスのラッパーにも使用できます。 また、他の様々なプロパティの定義も修正されています。 これは、Unicode Standard を追跡するようになりました。 RL1.2 RL1.2a から UTS#18: ユニコード正規表現 . これはエキサイティングで劇的な改善であり、開発チームのこの重要な努力は賞賛されるべきものです。


Java の正規表現のユニコード問題

Java の正規表現における問題は、Perl 1.0 の charclass エスケープ、つまり \w , \b , \s , \d およびその補集合は、JavaではUnicodeで動作するように拡張されていません。 これらの中で単独で \b はある種の拡張されたセマンティクスを享受していますが、これらはどちらも \w にも、また ユニコード識別子 も、また Unicodeの改行プロパティ .

さらに、JavaのPOSIXプロパティはこのようにアクセスします。

POSIX syntax    Java syntax

[[:Lower:]]     \p{Lower}
[[:Upper:]]     \p{Upper}
[[:ASCII:]]     \p{ASCII}
[[:Alpha:]]     \p{Alpha}
[[:Digit:]]     \p{Digit}
[[:Alnum:]]     \p{Alnum}
[[:Punct:]]     \p{Punct}
[[:Graph:]]     \p{Graph}
[[:Print:]]     \p{Print}
[[:Blank:]]     \p{Blank}
[[:Cntrl:]]     \p{Cntrl}
[[:XDigit:]]    \p{XDigit}
[[:Space:]]     \p{Space}

これは本当に厄介なことで、例えば Alpha , Lower そして Space する ではない は、Javaではユニコードにマッピングされます。 Alphabetic , Lowercase または Whitespace のようなプロパティがあります。これは非常に迷惑な話です。Java の Unicode プロパティサポートは 厳格な反時代的なもので で、つまり、過去 10 年間に登場したどの Unicode プロパティもサポートしていません。

空白文字について適切に話すことができないのは、非常に腹立たしいことです。 次の表を見てください。これらのコード ポイントのそれぞれについて、Java の J-results 列と P-results 列の両方が存在します。 列があり、Perl やその他の PCRE ベースの正規表現エンジンの場合は P-結果列があります。

             Regex    001A    0085    00A0    2029
                      J  P    J  P    J  P    J  P
                \s    1  1    0  1    0  1    0  1
               \pZ    0  0    0  0    1  1    1  1
            \p{Zs}    0  0    0  0    1  1    0  0
         \p{Space}    1  1    0  1    0  1    0  1
         \p{Blank}    0  0    0  0    0  1    0  0
    \p{Whitespace}    -  1    -  1    -  1    -  1
\p{javaWhitespace}    1  -    0  -    0  -    1  -
 \p{javaSpaceChar}    0  -    0  -    1  -    1  -

ご覧ください。

Java のホワイトスペースの結果は、事実上すべて Unicode に従って ̲w̲r̲o̲n̲g̲ となっています。 になっているのです。 本当に大きな問題です。 Java はとにかくメチャクチャで、既存の慣習に従って、しかも Unicode に従って「間違っている」答えを出してしまうのです。 さらに、Java は本当の Unicode プロパティにアクセスすることさえできません! 実際、Javaは 任意の プロパティをサポートしていません。


これらの問題に対する解決策、およびその他

この問題やその他多くの関連する問題に対処するために、私は昨日、これら 14 の charclass エスケープを書き換えるパターン文字列を書き換える Java 関数を書きました。

\w \W \s \S \v \V \h \H \d \D \b \B \X \R

を、予測可能で一貫した方法でUnicodeにマッチするように実際に動作するものに置き換えることによって、これを実現しました。これは、1 回のハッキング セッションでできたアルファ プロトタイプにすぎませんが、完全に機能します。

手短に言うと、私のコードはこれらの 14 を次のように書き直しました。

\s => [\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]
\S => [^\u0009-\u000D\u0020\u0085\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]

\v => [\u000A-\u000D\u0085\u2028\u2029]
\V => [^\u000A-\u000D\u0085\u2028\u2029]

\h => [\u0009\u0020\u00A0\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]
\H => [^\u0009\u0020\u00A0\u1680\u180E\u2000\u2001-\u200A\u202F\u205F\u3000]

\w => [\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]
\W => [^\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]

\b => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))
\B => (?:(?<=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?=[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])|(?<![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]])(?![\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]))

\d => \p{Nd}
\D => \P{Nd}

\R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])

\X => (?>\PM\pM*)

考慮すべきいくつかの点...

  • は、その \X 定義 何 ユニコードは現在 として レガシー書記素クラスタ ではなく 拡張書記素クラスタ ではなく、後者の方がより複雑だからです。Perl 自身は現在、よりファンシーなバージョンを使っていますが、古いバージョンでも最も一般的な状況では完全に動作可能です。 EDITです。 一番下の追記をご覧ください。

  • についてどうするか \d はあなたの意図によりますが、デフォルトはUniodeの定義です。私は、人々が常に \p{Nd} が必要なわけではなく、時には [0-9] または \pN .

  • 2つの境界の定義です。 \b\B は、特に \w の定義を使うように書かれています。

  • その \w の定義は広すぎます。なぜなら、丸で囲んだ文字だけでなく、括弧で囲んだ文字も捕捉してしまうからです。ユニコードの Other_Alphabetic プロパティは JDK7 まで使用できないので、これが最善の方法です。


境界を探る

境界は、Larry Wall が最初に「境界」という言葉を作って以来、ずっと問題になっています。 \b\B の構文は、1987年にPerl 1.0のためにそれらについて話したものです。を理解する鍵は \b\B の2つの作品は、それらにまつわる2つの神話を払拭するためのものです。

  1. それらは だけを見て のために \w の単語文字だけを探しています。 は決して を使用します。
  2. 特に文字列の端を探すことはありません。

A \b の境界を意味する。

    IF does follow word
        THEN doesn't precede word
    ELSIF doesn't follow word
        THEN does precede word

というように、これらはすべて完全に素直に定義されています。

  • 単語に従う (?<=\w) .
  • の前にある単語 (?=\w) .
  • は単語を追わない (?<!\w) .
  • が単語の前にない場合 (?!\w) .

したがって、以下のように IF-THEN としてエンコードされます。 and  ed-together AB は、正規表現では orX|Y であるため、また and よりも優先順位が高いので or というのは、単に AB|CD . ですから,すべての \b で置き換えることができます。

    (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))

\w を適切な方法で定義してください。

(不思議に思うかもしれませんが AC のコンポーネントは反対である。完璧な世界では、次のように書くことができるはずです。 AB|D と書けるはずなのですが、しばらくの間、私は Unicode プロパティの相互排除の矛盾を追いかけていました - これは私が と思う と思っているのですが、念のため境界の二重条件を残しておきました。さらに、これは、後で余分なアイデアを得た場合に、より拡張可能なものになります)。

については \B の非境界では、ロジックは

    IF does follow word
        THEN does precede word
    ELSIF doesn't follow word
        THEN doesn't precede word

のすべてのインスタンスを許可する。 \B に置き換える。

    (?:(?<=\w)(?=\w)|(?<!\w)(?!\w))

これは本当にどのように \b\B を動作させる。 これらに相当するパターンは

  • \b を使用して ((IF)THEN|ELSE) の構成は (?(?<=\w)(?!\w)|(?=\w))
  • \B を使用して ((IF)THEN|ELSE) の構成は (?(?=\w)(?<=\w)|(?<!\w))

しかし AB|CD のみのバージョンもありますが、特にJavaのような正規表現言語で条件付きパターンがない場合は、問題ありません。 ☹

私はすでに、1 回の実行で 110,385,408 件の一致をチェックするテスト スイートで、3 つの同等の定義すべてを使用して境界の動作を検証しており、それに従って 12 の異なるデータ構成で実行しました。

     0 ..     7F    the ASCII range
    80 ..     FF    the non-ASCII Latin1 range
   100 ..   FFFF    the non-Latin1 BMP (Basic Multilingual Plane) range
 10000 .. 10FFFF    the non-BMP portion of Unicode (the "astral" planes)

しかし、人々はしばしば別の種類の境界を求めます。彼らは、空白と文字列の端を認識するものを求めています。

  • 左端 として (?:(?<=^)|(?<=\s))
  • 右端 として (?=$|\s)

Java を Java で修正する

で投稿したコードは で投稿したコード で投稿したコードは、これとかなり多くの他の便利な機能を提供します。これには、自然言語の単語、ダッシュ、ハイフン、アポストロフィの定義と、もう少しのものが含まれています。

また、Unicode 文字をバカげた UTF-16 サロゲートではなく、論理的なコードポイントで指定することができます。 それがどれほど重要であるか、強調しすぎることはありません! そして、それは文字列展開のためだけです。

Java正規表現でcharclassを作る正規表現charclass置換のために 最終的に は Unicode で動作します。 で、正しく動作します。 つかむ ここから完全なソースを取得します。 . もちろん、あなたの好きなように使ってください。もし、あなたが修正したら、私はそれを聞きたいのですが、あなたはそうする必要はありません。かなり短いです。メインの正規表現書き換え関数のガワはシンプルです。

switch (code_point) {

    case 'b':  newstr.append(boundary);
               break; /* switch */
    case 'B':  newstr.append(not_boundary);
               break; /* switch */

    case 'd':  newstr.append(digits_charclass);
               break; /* switch */
    case 'D':  newstr.append(not_digits_charclass);
               break; /* switch */

    case 'h':  newstr.append(horizontal_whitespace_charclass);
               break; /* switch */
    case 'H':  newstr.append(not_horizontal_whitespace_charclass);
               break; /* switch */

    case 'v':  newstr.append(vertical_whitespace_charclass);
               break; /* switch */
    case 'V':  newstr.append(not_vertical_whitespace_charclass);
               break; /* switch */

    case 'R':  newstr.append(linebreak);
               break; /* switch */

    case 's':  newstr.append(whitespace_charclass);
               break; /* switch */
    case 'S':  newstr.append(not_whitespace_charclass);
               break; /* switch */

    case 'w':  newstr.append(identifier_charclass);
               break; /* switch */
    case 'W':  newstr.append(not_identifier_charclass);
               break; /* switch */

    case 'X':  newstr.append(legacy_grapheme_cluster);
               break; /* switch */

    default:   newstr.append('\\');
               newstr.append(Character.toChars(code_point));
               break; /* switch */

}
saw_backslash = false;

とにかく、このコードはアルファ版のリリースで、週末にハックしたものです。このままではありません。

ベータ版ではそうするつもりです。

  • 重複するコードをまとめる

  • 文字列エスケープの解除と正規表現エスケープの拡張について、より明確なインターフェイスを提供します。

  • での柔軟性を提供する。 \d の拡張、そしておそらく \b

  • Pattern.compileやString.matchesなどを呼び出すための便利なメソッドを提供します。

製品版リリースには、javadocとJUnitテストスイートが必要です。私のgigatesterを入れるかもしれませんが、JUnitのテストとして書かれているわけではありません。


追記

良い知らせと悪い知らせがあります。

良いニュースは、今、私が とても に近いものができたということです。 拡張書記素クラスタ を使用して、改良された \X .

悪い知らせ☺は、そのパターンが

(?:(?:\u000D\u000A)|(?:[\u0E40\u0E41\u0E42\u0E43\u0E44\u0EC0\u0EC1\u0EC2\u0EC3\u0EC4\uAAB5\uAAB6\uAAB9\uAABB\uAABC]*(?:[\u1100-\u115F\uA960-\uA97C]+|([\u1100-\u115F\uA960-\uA97C]*((?:[[\u1160-\u11A2\uD7B0-\uD7C6][\uAC00\uAC1C\uAC38]][\u1160-\u11A2\uD7B0-\uD7C6]*|[\uAC01\uAC02\uAC03\uAC04])[\u11A8-\u11F9\uD7CB-\uD7FB]*))|[\u11A8-\u11F9\uD7CB-\uD7FB]+|[^[\p{Zl}\p{Zp}\p{Cc}\p{Cf}&&[^\u000D\u000A\u200C\u200D]]\u000D\u000A])[[\p{Mn}\p{Me}\u200C\u200D\u0488\u0489\u20DD\u20DE\u20DF\u20E0\u20E2\u20E3\u20E4\uA670\uA671\uA672\uFF9E\uFF9F][\p{Mc}\u0E30\u0E32\u0E33\u0E45\u0EB0\u0EB2\u0EB3]]*)|(?s:.))

というように、Javaではこう書きます。

String extended_grapheme_cluster = "(?:(?:\\u000D\\u000A)|(?:[\\u0E40\\u0E41\\u0E42\\u0E43\\u0E44\\u0EC0\\u0EC1\\u0EC2\\u0EC3\\u0EC4\\uAAB5\\uAAB6\\uAAB9\\uAABB\\uAABC]*(?:[\\u1100-\\u115F\\uA960-\\uA97C]+|([\\u1100-\\u115F\\uA960-\\uA97C]*((?:[[\\u1160-\\u11A2\\uD7B0-\\uD7C6][\\uAC00\\uAC1C\\uAC38]][\\u1160-\\u11A2\\uD7B0-\\uD7C6]*|[\\uAC01\\uAC02\\uAC03\\uAC04])[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]*))|[\\u11A8-\\u11F9\\uD7CB-\\uD7FB]+|[^[\\p{Zl}\\p{Zp}\\p{Cc}\\p{Cf}&&[^\\u000D\\u000A\\u200C\\u200D]]\\u000D\\u000A])[[\\p{Mn}\\p{Me}\\u200C\\u200D\\u0488\\u0489\\u20DD\\u20DE\\u20DF\\u20E0\\u20E2\\u20E3\\u20E4\\uA670\\uA671\\uA672\\uFF9E\\uFF9F][\\p{Mc}\\u0E30\\u0E32\\u0E33\\u0E45\\u0EB0\\u0EB2\\u0EB3]]*)|(?s:.))";

ツッコミどころ満載!