\w
字符类的速记解释为“任何字母,数字或连接标点符号”(通常:下划线)。这样,诸如\w+
之类的正则表达式会匹配诸如hello
,élève
,GOÄ_432
或gefräßig
之类的单词。不幸的是,Java没有。在Java中,
\w
限于[A-Za-z0-9_]
。这样会使匹配上述单词的单词变得困难,还有其他问题。似乎还出现了
\b
单词分隔符在不应该匹配的地方匹配的情况。 Java中类似.NET的,支持Unicode的
\w
或\b
的正确等效项是什么?其他哪些快捷方式需要“重写”以使其具有Unicode意识?#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}
这真是一团糟,因为这意味着
Alpha
,Lower
和Space
之类的内容在Java中不会映射到Unicode Alphabetic
,Lowercase
或Whitespace
属性。这真是令人讨厌。 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
,所以or
是X|Y
,并且因为and
的优先级高于or
,也就是AB|CD
。因此,每个表示边界的\b
都可以安全地替换为: (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
,并以适当的方式定义了
\w
。 (您可能会觉得
A
和C
的组件是相反的,这很奇怪。在一个完美的世界中,您应该能够编写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
评论
简短的说来,蒂姆(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.
嗨,蒂姆,请查看我的更新。他们添加了一个标志,以使其全部正常工作。万岁!