在 Java 正则表达式中等效于 w 和 b 的 Unicode?

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

不幸的是,Java 没有。在 Java 中,\w仅限于 [A-Za-z0-9_]。这使得像上面提到的那些匹配词很困难,还有其他问题。

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

正确的等价物是什么。在 Java 中类似 NET、支持 Unicode 的 \w还是 \b?还有哪些快捷方式需要“重写”才能支持 Unicode?

69774 次浏览

很不幸,\w不起作用,我提出的解决方案 \p{Alpha}也不起作用。

看起来 [\p{L}]能够捕获所有 Unicode 字母,所以相当于 \w的 Unicode 应该是 [\p{L}\p{Digit}_]

在 Java 中,\w\d不支持 Unicode; 它们只匹配 ASCII 字符 [A-Za-z0-9_][0-9]\p{Alpha}和好友也是如此(它们所基于的 POSIX“字符类”应该是区分语言环境的,但是在 Java 中它们只匹配 ASCII 字符)。如果你想匹配 Unicode“单词字符”,你必须拼写出来,例如 [\pL\p{Mn}\p{Nd}\p{Pc}],用于字母、非间距修饰符(重音符号)、十进制数字和连接标点符号。

然而,Java 的 \b Unicode 精通; 它也使用 Character.isLetterOrDigit(ch)并检查重音字母,但它识别的唯一“连接标点符号”字符是下划线。当我尝试你的样本代码,它打印 ""élève",因为它应该(请登陆 ideone.com)。

源代码

我在 < a href = “ http://training ing.perl.com/script/tchrist-unicode-charclasses _ _ alpha.java”rel = “ noReferrer”> 可在这里获得 。下面讨论的重写函数的源代码

用 Java7更新

Sun’s updated Pattern class for JDK7 has a marvelous new flag, UNICODE_CHARACTER_CLASS, which makes everything work right again. It’s available as an embeddable (?U) for inside the pattern, so you can use it with the String class’s wrappers, too. It also sports corrected definitions for various other properties, too. It now tracks The Unicode Standard, in both RL1.2 and RL1.2 a from UTS # 18: Unicode 正则表达式. This is an exciting and dramatic improvement, and the development team is to be commended for this important effort.


Java 的 Regex Unicode 问题

Java 正则表达式的问题在于,Perl 1.0字符类的转义ーー即 \w\b\s\d及其补充ーー没有在 Java 中进行扩展,无法用于 Unicode。其中,\b享有一定的扩展语义,但这些映射既不到 \w,也不到 Unicode 标识符,也不到 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}

这是一个真正的混乱,因为这意味着像 AlphaLowerSpace这样的东西在 Java 中将 没有映射到 Unicode AlphabeticLowercaseWhitespace属性。这真是太烦人了。Java 的 Unicode 属性支持是 严格来说是千禧年前的,我的意思是它不支持过去十年出现的 Unicode 属性。

不能正确地讨论空格是非常烦人的。考虑下表。对于每个代码点,都有一个 J- 结果列 以及 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  -

看到了吗?

根据 Unicode,几乎每一个 Java 空白结果都是 wr n g。这是一个 真是个大问题。 Java 只是一团糟,给出的答案是“错误的”根据现有的做法,也根据 Unicode。而且 Java 甚至不允许您访问真正的 Unicode 属性!事实上,Java 不支持对应于 Unicode 空格的 任何属性。


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

为了解决这个问题以及许多其他相关问题,昨天我编写了一个 Java 函数来重写一个模式字符串,该模式字符串重写了以下14个字符类转义:

\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*)

有些事情需要考虑。

  • 它使用的 \X定义是什么 Unicode 现在引用作为一个 遗留图形集群遗留图形集群,而不是一个 扩展字符串扩展字符串,因为后者是相当复杂的。Perl 本身现在使用了更高级的版本,但是旧版本对于大多数常见情况仍然是完全可行的。见底部附录。

  • 如何处理 \d取决于您的意图,但默认的是 Uniode 定义。我可以看到人们并不总是想要 \p{Nd},但有时无论是 [0-9]\pN

  • \b\B这两个边界定义是专门为使用 \w定义而编写的。

  • \w的定义过于宽泛,因为它捕获的是带括号的字母,而不仅仅是圈起来的字母。Unicode Other_Alphabetic属性在 JDK7之前是不可用的,所以这是最好的选择。


探索界限

自从 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)

因此,由于 IF-THEN在正则表达式中被编码为 and混合的 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的行为方式

  • 使用 ((IF)THEN|ELSE)构造的 \b(?(?<=\w)(?!\w)|(?=\w))
  • 使用 ((IF)THEN|ELSE)构造的 \B(?(?=\w)(?<=\w)|(?<!\w))

但是仅使用 AB|CD的版本是可以的,特别是在您的正则表达式语言(如 Java)中缺乏条件模式的情况下。Something

我已经用一个测试套件验证了边界的行为,这个测试套件每次运行检查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代理中。这只是字符串展开。

对于正则表达式字符类替换,使得 Java 正则表达式中的字符类在 Unicode 上工作,正常工作,抓取 完整的源代码。当然,您可以随意处理它。如果你能修好它,我很乐意听,但你不必听。很短。主 regex 重写函数的核心很简单:

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.edit 或 String.match 或其他类似的事情

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

呸!