问题
许多现代正则表达式实现将\w
字符类缩写为"任何字母,数字或连接标点符号"(通常为下划线)。这样,正则表达式像\w+
匹配,如hello
,élève
,GOÄ_432
或gefräßig
。
不幸的是,Java没有。在Java中,\w
仅限于[A-Za-z0-9_]
。除了其他问题之外,这使得如上所述的匹配单词变得困难。
似乎\b
字符分隔符在不应该的位置匹配。
Java中类似.NET,Unicode-aware\w
或\b
的正确等价物是什么?哪些其他快捷方式需要"重写"以使其具有Unicode感知功能?
#1 热门回答(225 赞)
源代码
我在下面讨论的重写函数的源代码is available here。
#7在Java 7中更新
Sun为JDK7更新的Pattern
类有一个奇妙的新旗帜,UNICODE_CHARACTER_CLASS
,这使得一切正常。它在模式中作为embeddable(?U)
提供,因此你也可以将它与String
class的包装一起使用。它也体现了各种其他属性的修正定义。它现在跟踪Unicode标准,在RL1.2和RL1.2a中从UTS#18:Unicode正则表达式。这是一个令人兴奋和戏剧性的改进,开发团队因这项重要工作而受到赞扬。
Java的正则表达式Unicode问题
Java正则表达式的问题在于Perl 1.0 charclass转义 - 意思是\w
,\b
,\s
,\d
及其补充 - 不在Java扩展中以使用Unicode。其中只有\b
具有某些扩展语义,但这些语义既不映射到\w
,也不映射到Unicode identifiers,也不映射到Unicode line-break properties。
此外,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
do之类的东西不是在Java中映射到UnicodeAlphabetic
,Lowercase
或Whitespace
属性。这太令人讨厌了。 Java的Unicode属性支持是严格的antemillennial,我的意思是它支持在过去十年中没有出现的Unicode属性。
无法正确谈论空白是非常烦人的。请考虑下表。对于每个代码点,既有Java的J结果列,也有Perl的P结果列或任何其他基于PCRE的正则表达式引擎:
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空白结果都是错误的。这是a**真的很大的问题。**Java刚搞砸了,根据现有的做法和Unicode也给出了"错误"的答案。 Plus Java甚至不能让你访问真正的Unicode属性!实际上,Java不支持与Unicode空白对应的任何属性。
##解决所有这些问题,等等
为了解决这个问题和许多其他相关问题,昨天我写了一个Java函数来重写一个模式字符串,它重写了这14个charclass转义:
\w \W \s \S \v \V \h \H \d \D \b \B \X \R
通过用可预测和一致的方式替换实际上与Unicode匹配的东西。它只是来自单个hack会话的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 ,但有时候要么[0-9]或者\ pN。
- 两个边界定义\ b和\ B专门用于使用\ w定义。
- 那个\定义过于宽泛,因为它抓住了不仅仅是带圆圈的字母。 Unicode Other_Alphabetic属性在JDK7之前不可用,因此这是你可以做的最好的。
##探索边界
自从1987年Larry Wall首次为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
ed-togetherAB
in regexes,anor
isX|Y
,并且因为and
优先于or
,所以简称为AB|CD
。所以每个\b
这意味着边界可以安全地替换为:
(?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
用\w
以适当的方式定义。
(你可能会觉得奇怪的是,2912123088和C
组件是对立的。在一个完美的世界中,你应该能够写出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。 ☹
我已经使用所有三个等价定义验证了边界的行为,测试套件每次运行检查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
我在my other answer发布的代码提供了这个以及其他一些便利。这包括自然语言单词,破折号,连字符和撇号的定义,以及更多内容。
它还允许你在逻辑代码点中指定Unicode字符,而不是在愚蠢的UTF-16代理中.**很难过分重视它的重要性!**这只是字符串扩展。
对于regex charclass替换,使得你的Java正则表中的charclass最终工作在Unicode上,并且正常工作,抓取the full source from here.当然,你可以随意使用它。如果你修复它,我很想听到它,但你没有必要。它很短。主要的正则表达式重写函数的内容很简单:
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版本,我在周末讨价还价。它不会保持这种状态。
对于测试版,我打算:
- 将代码重复折叠起来
- 提供关于unescaping字符串转义与增强正则表达式转义的更清晰的界面
- 在\ d扩展中提供一些灵活性,也许是\ b
- 提供方便的方法,处理转身和调用Pattern.compile或String.matches或诸如此类的东西
对于生产版本,它应该有javadoc和JUnit测试套件。我可能包括我的gigatester,但它不是作为JUnit测试编写的。
##附录
我有好消息和坏消息。
好消息是,我现在得到的a**非常接近于用于改进的\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üß!
#2 热门回答(14 赞)
真的很不幸,\w
无法正常工作。建议的解决方案\p{Alpha}
也不适用于我。
似乎[\p{L}]
捕获所有Unicode字母。所以Unicode等价的\w
应该是[\p{L}\p{Digit}_]
。
#3 热门回答(7 赞)
在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-savvy;它使用了Character.isLetterOrDigit(ch)
并检查了重音字母,但它识别的唯一"连接标点"字符是下划线.**编辑:**当我尝试你的示例代码时,它打印""
和élève"
它应该(在ideone.com上看到它)。