Java 正则表达式中 \w 和 \b 的 Unicode 等效项?

2022-08-31 08:23:58

许多现代正则表达式实现将字符类速记解释为“任何字母、数字或连接标点符号”(通常为:下划线)。这样,类似正则表达式的正则表达式就会匹配 、 或 .\w\w+helloélèveGOÄ_432gefräßig

不幸的是,Java没有。在 Java 中,限制为 。这使得匹配上述单词以及其他问题变得困难。\w[A-Za-z0-9_]

单词分隔符似乎在不应该匹配的地方也匹配。\b

什么是正确的等效项。类NET,Unicode感知还是在Java中?还有哪些快捷方式需要“重写”才能使它们能够识别 Unicode?\w\b


答案 1

源码

我在下面讨论的重写函数的源代码可以在这里找到

Java 7 中的更新

Sun的JDK7更新类有一个奇妙的新标志,它使一切正常。它可以作为嵌入的模式,所以你也可以把它与类的包装器一起使用。它还对各种其他属性进行了更正。它现在在UTS#18:Unicode正则表达式RL1.2和RL1.2a中跟踪Unicode标准。这是一个令人兴奋和戏剧性的改进,开发团队的这一重要努力值得赞扬。PatternUNICODE_CHARACTER_CLASS(?U)String


Java的正则表达式Unicode问题

Java正则表达式的问题在于Perl 1.0 charclass转义 - 意思是, , 和它们的补码 - 在Java中没有扩展为与Unicode一起使用。其中独一无二,具有某些扩展语义,但这些语义既不映射到 \w,也不映射到 Unicode 标识符,也不映射到 Unicode 换行符属性\w\b\s\d\b

此外,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}

这是一个真正的混乱,因为它意味着像、、和在Java中这样的东西映射到Unicode,或属性。这非常烦人。Java的Unicode属性支持是严格意义上的,我的意思是它不支持过去十年中出现的Unicode属性。AlphaLowerSpaceAlphabeticLowercaseWhitespace

不能正确地谈论空格是非常烦人的。请考虑下表。对于这些代码点中的每一个,都有一个 Java 的 J-results 列和一个用于 Perl 或任何其他基于 PCRE 的正则表达式引擎的 P-results 列:

             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  -

看到了吗?

根据 Unicode,这些 Java 空白结果中的每一个都是 ̲w̲r̲o̲n̲g̲。这是一个非常大的问题。Java只是搞砸了,根据现有实践和Unicode给出了“错误”的答案。另外,Java甚至不允许您访问真正的Unicode属性!实际上,Java 不支持任何与 Unicode 空格对应的属性。


所有这些问题的解决方案,以及更多

为了处理这个问题和许多其他相关问题,昨天我写了一个Java函数来重写一个模式字符串,该字符串重写了这14个charclass转义:

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

通过用实际工作的东西来替换它们,以一种可预测和一致的方式匹配Unicode。它只是来自单个黑客会话的alpha原型,但它是完全功能性的。

简而言之,我的代码重写了这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*)

需要考虑的一些事项...

  • 这使用Unicode现在称为传统字素簇的定义,而不是扩展的字形簇,因为后者相当复杂。Perl本身现在使用更花哨的版本,但旧版本仍然完全适用于最常见的情况。编辑:见下文的增编。\X

  • 做什么取决于您的意图,但默认值是Uniode定义。我可以看到人们并不总是想要,但有时是或。\d\p{Nd}[0-9]\pN

  • 两个边界定义 和 专门编写为使用该定义。\b\B\w

  • 这个定义过于宽泛,因为它抓住了parenned字母,而不仅仅是带圆圈的字母。Unicode 属性在 JDK7 之前不可用,因此这是您能做的最好的事情。\wOther_Alphabetic


探索边界

自从 Larry Wall 在 1987 年首次为 Perl 1.0 创造和语法来谈论边界以来,边界一直是一个问题。理解它们如何以及两者如何工作的关键是消除关于它们的两个普遍存在的神话:\b\B\b\B

  1. 他们只是在寻找单词字符,从不寻找非单词字符。\w
  2. 它们不会专门寻找琴弦的边缘。

边界表示:\b

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

这些都被完美地直接定义为:

  • 后面的单词是 。(?<=\w)
  • 前面的单词是 。(?=\w)
  • 不跟着单词是 .(?<!\w)
  • 不前面的单词是 。(?!\w)

因此,由于在正则表达式中被编码为 ed-together,因此 an 是 ,并且因为 的优先级高于 ,所以很简单。因此,每个这意味着边界都可以安全地替换为:IF-THENandABorX|YandorAB|CD\b

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

以适当的方式定义。\w

(您可能会觉得很奇怪,和 组件是相反的。在一个完美的世界里,你应该能够写出来,但是有一段时间我一直在追逐Unicode属性中的互斥矛盾 - 我想我已经处理过了,但我把双重条件留在了边界以防万一。此外,如果您以后获得额外的想法,这将使它更具可扩展性。ACAB|D

对于非边界,逻辑是:\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))

但是使用just的版本很好,特别是如果你在你的正则表达式语言中缺乏条件模式 - 比如Java。☹AB|CD

我已经使用所有三个等效定义验证了边界的行为,测试套件每次运行检查110,385,408个匹配项,并且我已经根据以下十几种不同的数据配置运行了该套件:

     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 代理项中指定 Unicode 字符。很难过分强调这有多重要!这只是为了字符串扩展。

对于使 Java 正则表达式中的 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;

无论如何,该代码只是一个alpha版本,这是我在周末破解的东西。它不会保持这种状态。

对于测试版,我打算:

  • 将代码重复折叠在一起

  • 提供有关取消转义字符串转义与增强正则表达式转义的更清晰界面

  • 在扩展中提供一些灵活性,也许\d\b

  • 提供方便的方法来处理转身和调用Pattern.compile或String.matches或其他什么不适合你

对于生产版本,它应该有javadoc和一个JUnit测试套件。我可能包括我的gigatster,但它不是作为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:.))";

■特舒斯!


答案 2

这真的很不幸,这不起作用。建议的解决方案也不适合我。\w\p{Alpha}

它似乎捕获了所有Unicode字母。所以 Unicode 的等价物应该是 。[\p{L}]\w[\p{L}\p{Digit}_]