Java正则expression式中\ w和\ b的Unicode等价物?
许多现代正则expression式实现将\w
字符类速记解释为“任何字母,数字或连接标点符号”(通常是:下划线)。 这样,像\w+
这样的正则expression式就可以匹配hello
, élève
, GOÄ_432
或gefräßig
等词语。
不幸的是,Java不。 在Java中, \w
仅限于[A-Za-z0-9_]
。 这使得上面提到的那些匹配词难以解决,还有其他一些问题。
它也似乎是\b
字分隔符匹配的地方,它不应该。
在Java中,类似.NET的,支持Unicode的\w
或\b
的正确等价物是什么? 哪些其他快捷方式需要“重写”,使他们的Unicode感知?
源代码
下面我将讨论重写函数的源代码。
在Java 7中更新
Sun为JDK7更新的Pattern
类有一个奇妙的新标志UNICODE_CHARACTER_CLASS
,它使所有的工作都能正常工作。 它可以作为模式内部的embedded式(?U)
使用,所以也可以在String
类的包装器中使用它。 它也修正了各种其他性质的修正定义。 它现在跟踪来自UTS#18:Unicode正则expression式的 RL1.2和RL1.2a中的Unicode标准。 这是一个激动人心的戏剧性的改善,开发团队将为这个重要的努力而受到表彰。
Java的正则expression式Unicode的问题
Java正则expression式的问题在于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的P结果列或任何其他基于PCRE的正则expression式引擎:
Regex 001A 0085 00A0 2029 JPJPJPJP \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空白结果都是错误的。 这是一个很大的问题。 Java只是搞砸了,根据现有的做法,也根据Unicode给出了“错误”的答案。 再加上Java甚至不允许你访问真正的Unicode属性! 事实上,Java不支持任何对应于Unicode空格的属性。
解决所有这些问题,等等
为了解决这个问题以及其他许多相关的问题,我昨天写了一个Java函数来重写一个模式string,以重写这14个charclass转义符:
\w \W \s \S \v \V \h \H \d \D \b \B \X \R
通过用可预测和一致的方式实际工作来匹配Unicode的东西来替代它们。 这只是一个黑客会话的alpha原型,但它是完全function的。
简而言之,我的代码重写了这些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本身现在使用了fancier版本,但是旧版本对于最常见的情况仍然是可行的。 编辑:见底部的附录。 -
如何处理
\d
取决于您的意图,但默认是Uniode定义。 我可以看到人们并不总是想要\p{Nd}
,但有时可能是[0-9]
或\pN
。 -
两个边界定义
\b
和\B
是专门为使用\w
定义而编写的。 -
这个定义是过于宽泛的,因为它抓住了不平常的字母,而不是被圈起来的字母。 Unicode
Other_Alphabetic
属性在JDK7之前不可用,所以这是您可以做的最好的。
探索边界
自从Larry Wall在1987年首次提出用Perl和Perl语言“ \b
和“ \B
语法之后,边界就成为了一个问题。理解\b
和\B
两者如何工作的关键是消除两个普遍的关于它们的神话:
- 他们只是在寻找
\w
字的字符,而不是对于非字的字符。 - 他们没有专门查找string的边缘。
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
在正则expression式中被编码为AB
and
AB
,因此a or
是X|Y
,并且因为and
优先级高于or
仅仅是AB|CD
。 所以每个\b
这意味着一个边界可以被安全地replace为:
(?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
用\w
以适当的方式定义。
(你可能会觉得奇怪的是, A
和C
组件是相反的,在一个完美的世界中,你应该能够写出AB|D
,但是有一段时间我正在追逐Unicode属性中的相互排斥矛盾 – 我认为已经照顾好了,但是为了以防万一,我把这个双重条件放在了边界上,如果以后有更多的想法的话,这会使它更具有可扩展性。)
对于\B
非界限,逻辑是:
IF does follow word THEN does precede word ELSIF doesn't follow word THEN doesn't precede word
允许将\B
所有实例replace为:
(?:(?<=\w)(?=\w)|(?<!\w)(?!\w))
这确实是\b
和\B
行为。 他们的等效模式是
-
((IF)THEN|ELSE)
结构是(?(?<=\w)(?!\w)|(?=\w))
- 使用
((IF)THEN|ELSE)
构造的B是(?(?=\w)(?<=\w)|(?<!\w))
但是只有AB|CD
的版本没有问题,特别是如果你在正则expression式语言中缺less条件模式 – 比如Java。 ☹
我已经使用所有三个等价的定义来validation边界的行为,一个testing套件每次运行检查110,385,408个匹配,以及我已经在十几种不同的数据configuration上运行,这些数据configuration依据:
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替代品中。 这很难超负荷! 这只是为了扩展string。
对于正则expression式的charclassreplace,它使Java正则expression式中的charclass 最终能够在Unicode上工作 , 并能正常工作, 从这里获取完整的源代码 。 当然,你可以随心所欲地做。 如果你修复它,我很乐意听到它,但你不必。 这很短。 主要的正则expression式重写函数的内容很简单:
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版本,这个东西我在周末被黑了。 它不会保持这种方式。
对于testing我打算:
-
把代码重叠在一起
-
提供一个更清晰的界面,关于逃避string转义与增加正则expression式转义
-
在
\d
扩展中提供一些灵活性,也许\b
-
提供方便的方法,处理转身和调用Pattern.compile或String.matches或什么不适合你
对于产品发布,它应该有javadoc和一个JUnittesting套件。 我可能会包含我的gigatester,但它不是作为JUnittesting书写的。
附录
我有好消息和坏消息。
好消息是,我现在已经非常接近一个扩展的字形群来用于改进的\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üß!
\w
不起作用真是不幸。 build议的解决scheme\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“字符类”应该是locale敏感的,但在Java中他们只匹配ASCII字符)。 如果你想匹配Unicode的“单词字符”,你必须拼写出来,例如[\pL\p{Mn}\p{Nd}\p{Pc}]
,用于字母,非间距修饰符(重音),十进制数字和连接标点符号。
但是,Java的\b
是 Unicode精明的; 它使用Character.isLetterOrDigit(ch)
并检查重音字母,但它唯一识别的“连接标点符号”是下划线。 编辑:当我尝试你的示例代码,它打印""
和élève"
因为它应该( 在ideone.com上看到它 )。