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

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

似乎还出现了\b单词分隔符在不应该匹配的地方匹配的情况。

Java中类似.NET的,支持Unicode的\w\b的正确等效项是什么?其他哪些快捷方式需要“重写”以使其具有Unicode意识?

评论

简短的说来,蒂姆(Tim),他们都需要写作才能使其与Unicode保持一致。我仍然没有迹象表明Java 1.7除了最终增加对脚本的支持之外,还可以使用Unicode属性做更多的事情,仅此而已。如果没有更好地访问Unicode属性的全部补充,您确实无法做某些事情。如果您还没有我的uniprops和unichars脚本(和uninames),它们会让您大开眼界。

One might consider adding marks to the word class. Since for example ä can be represented in Unicode either as \u0061\u0308 or \u00E4.

嗨,蒂姆,请查看我的更新。他们添加了一个标志,以使其全部正常工作。万岁!

#1 楼

源代码

我在下面讨论的重写功能的源代码在这里。

Java 7中的更新

Sun为JDK7更新了Pattern类带有一个奇妙的新标志UNICODE_CHARACTER_CLASS,它使所有功能再次正常运行。它可作为可嵌入模式的(?U)提供,因此您也可以将其与String类的包装器一起使用。它还针对其他各种属性修改了定义。现在,它会在UTS#18:Unicode正则表达式的RL1.2和RL1.2a中跟踪Unicode标准。这是一个令人激动的重大改进,值得我们赞扬的开发团队。


Java的Regex Unicode问题

Java regexes的问题是Perl 1.0 charclass转义符-表示\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属性。

不能正确谈论空白太烦人了。请考虑下表。对于每个代码点,Java都有一个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空白结果中的每一个都是``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*)


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


它在\X定义中使用了Unicode,现在将其称为传统字素簇,而不是扩展字素簇,因为后者更为复杂。 Perl本身现在使用的是高级版本,但是对于大多数常见情况,旧版本仍然可以完美使用。编辑:请参阅底部的附录。
关于\d的处理方法取决于您的意图,但默认设置是Uniode定义。我可以看到人们并不总是想要\p{Nd},但有时希望[0-9]\pN
两个边界定义\b\B专门写为使用\w定义。
\w定义过于宽泛,因为它抓住了赦免的字母,而不仅仅是带圆圈的字母。 Unicode Other_Alphabetic属性在JDK7之前不可用,所以这是您可以做的最好的事情。


探索边界

自从Larry Wall于1987年首次提出\b\B语法来讨论Perl 1.0以来,边界一直是一个问题。了解\b\B两者如何工作的关键是消除关于它们的两个普遍的神话:


他们只是在寻找\w单词字符,从未在寻找非单词字符。
他们没有专门在寻找字符串的边缘。

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合在一起的AB,所以orX|Y,并且因为and的优先级高于or,也就是AB|CD。因此,每个表示边界的\b都可以安全地替换为:

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


,并以适当的方式定义了\w

(您可能会觉得AC的组件是相反的,这很奇怪。在一个完美的世界中,您应该能够编写AB|D,但是有一段时间我一直在寻找Unicode中的互斥矛盾属性-我想我已经照顾过,但是为了防止万一,我在边界上保留了double条件。此外,如果以后有更多的想法,这可以使它更具扩展性。)

对于\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)。 already

我已经使用测试套件验证了所有三个等效定义的边界行为,该套件每次运行检查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上正常工作,请从此处获取完整的源代码。当然,您可以随便使用它。如果您要修复它,我很想听听它,但您不必这样做。很短正则表达式的主要重写功能很简单:

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发行版,这是我在周末破解的。不会一直这样。

对于Beta版,我打算:


将代码重复折叠在一起
提供一个更清晰的界面,了解转义字符串转义符与增强正则表达式转义符
\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:.))";


¡Tschüß!

评论


这真太了不起了。非常感谢。

–蒂姆·皮茨克(Tim Pietzcker)
2010-11-29 20:23

基督,那是一个启蒙的答案。我没有得到Jon Skeet参考。他与此有什么关系?

– BalusC
2010-11-30在1:39



@BalusC:这是对乔恩(Jon)的引用,他说他会让我回答这个问题。但是请不要将t放在@tchrist中。它可能会传到我的头上。 :)

–基督
2010-11-30在1:56

您是否考虑过将其添加到OpenJDK?

–马丁·弗伯格(Martijn Verburg)
2010-11-30 8:45

@Martijn:我没有,没有;我不知道那是“开放”。 :)但是我考虑过以更正式的意义发布它;我部门中的其他人希望这样做(使用某种开源许可证,可能是BSD或ASL)。我可能会从此alpha原型中的API更改它,清理代码,等等。但是它极大地帮助了我们,而且我们认为它也会帮助其他人。我真的希望Sun能对他们的库做些什么,但是Oracle对此没有信心。

–基督
2010年11月30日,11:13

#2 楼

真不幸,\w无法正常工作。建议的解决方案\p{Alpha}对我也不起作用。

[\p{L}]似乎捕获了所有Unicode字母。因此,与\w等效的Unicode应为[\p{L}\p{Digit}_]

评论


但是\ w也可以匹配数字等等。我认为\ p {L}仅适用于字母。

–蒂姆·皮茨克(Tim Pietzcker)
2010-11-29 15:29

你是对的。 \ p {L}就足够了。我还认为只有字母是问题所在。 [\ p {L} \ p {Digit} _]应该捕获所有字母数字字符,包括下划线。

– MusiKk
2010-11-29 15:39

@MusicKk:有关完整的解决方案,请参见我的回答,该解决方案允许您正常编写模式,然后将其通过纠正Java巨大空白的函数传递,以使其在Unicode上正常工作。

–基督
2010-11-29 21:22

不,在所有愚蠢的事物中,\ w被Unicode定义为比\ pL和ASCII数字宽得多。如果您想要Java的Unicode感知\ w,则必须写[\ pL \ pM \ p {Nd} \ p {Nl} \ p {Pc} [\ p {InEnclosedAlphanumerics} && \ p {So}]]]或您可以从这里使用我的unicode_charclass函数。抱歉!

–基督
2010-11-29 22:17

@Tim,是的,对于字母\ pL确实有效(您不需要拥抱一个字母的道具)。但是,您很少希望这样做,因为您必须非常小心,因为您的数据采用Unicode规范化形式D(又称NFD,表示规范分解)而不是采用NFC(NFD后接规范),因此您的匹配不会得到不同的答案组成)。例如,代码点U + E9(“é”)是NFC形式的\ pL,但其NFD形式变为U + 65.301,因此匹配\ pL \ pM。您可以使用\ X:(?:(?= \ pL)\ X)来解决这个问题,但是您需要使用Java的该版本。 :(

–基督
2010-11-29 22:28



#3 楼

在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上查看)。

评论


抱歉,艾伦,但是您真的不能说Java的\ b是Unicode的。它犯了无数错误。 “ \ u2163 =”,“ \ u24e7 =”和“ \ u0301 =”在Java中都无法匹配模式“ \\ b =”,但是应该像– perl -le'print / \ b = / ||显示“ \ x {2163} =“,“ \ x {24e7} =”,“ \ x {301} =“”为0。但是,如果(并且仅当)交换了我的单词边界版本而不是Java中的本机\ b时,所有这些也都在Java中起作用。

–基督
2010-11-29 22:13



@tchrist:我没有评论\ b的正确性,只是指出它在Unicode字符(如Java中实现)上运行,而不仅仅是像\ w和朋友这样的ASCII字符。但是,当该字符与基本字符配对时,它对于\ u0301确实可以正常工作,例如e \ u0301 =。而且我不认为Java在这种情况下是错误的。除非组合符号是带有字母的字素簇的一部分,否则如何将组合标记视为单词字符?

–艾伦·摩尔
2010年11月30日,0:33

@Alan,这是当Unicode通过讨论扩展字形簇与旧式字形簇来澄清字形簇时所清除的。字素簇的旧定义是有问题的,因为\ X表示无标记,后跟任意数量的标记,因为您应该能够将所有文件描述为匹配/ ^(\ X * \ R)* \ R $ /,但如果文件开头甚至一行没有\ pM,则不会。因此,他们将其扩展为始终匹配至少一个字符。它总是这样做,但是现在它使上述模式起作用。 […继续…]

–基督
10-11-30在0:46



@ Alan,Java的本机\ b部分支持Unicode弊大于利。考虑将字符串“élève”与模式\ b(\ w +)\ b进行匹配。看到问题了吗?

–基督
2010-11-30 12:24

@tchrist:是的,没有边界,\ w +会找到两个匹配项:l和ve,这已经够糟糕了。但是对于单词边界,它什么也找不到,因为\ b会将é和è识别为单词字符。 \ b和\ w至少应就什么是字字符和什么不是字字符达成共识。

–艾伦·摩尔
2010-11-30 17:29