许多现代正则表达式实现将\w
字符类速记解释为“任何字母、数字或连接标点符号”(通常:下划线)。这样,像这样的正则表达式\w+
匹配像hello
, élève
,GOÄ_432
或 之类的词gefräßig
。
不幸的是,Java 没有。在 Java 中,\w
仅限于[A-Za-z0-9_]
. 这使得匹配上面提到的词变得困难,还有其他问题。
似乎\b
单词分隔符在不应该匹配的地方也匹配。
什么是类似 .NET、Unicode 感知\w
或\b
Java的正确等价物?哪些其他快捷方式需要“重写”以使其能够识别 Unicode?
源代码
我在下面讨论的重写函数的源代码可以在这里找到。
Java 7 中的更新
Sun
Pattern
为 JDK7更新的类有一个了不起的新标志UNICODE_CHARACTER_CLASS
,它使一切重新正常工作。它可以作为(?U)
模式内部的可嵌入对象使用,因此您也可以将它与String
类的包装器一起使用。它还修正了各种其他属性的定义。它现在跟踪来自UTS#18: Unicode Regular Expressions 的RL1.2和RL1.2a 中的 Unicode 标准。这是一个令人兴奋和戏剧性的改进,开发团队的这一重要努力值得表扬。Java 的正则表达式 Unicode 问题
使用Java正则表达式的问题是,Perl的1.0 charclass将逃逸-这意味着
\w
,\b
,\s
,\d
和它们的补-不是Java扩展工作使用Unicode。仅在其中,\b
享有某些扩展语义,但这些既不映射\w
到Unicode 标识符,也不映射到Unicode 换行符属性。此外,Java 中的 POSIX 属性可以通过以下方式访问:
这是一个真正的混乱,因为这意味着一些事情,如
Alpha
,Lower
和Space
做的不是在Java中映射为UnicodeAlphabetic
,Lowercase
或Whitespace
性质。这是非常烦人的。Java 的 Unicode 属性支持是严格的 antemillennial,我的意思是它不支持过去十年中出现的 Unicode 属性。不能正确地谈论空白是非常烦人的。考虑下表。对于这些代码点中的每一个,都有一个用于 Java 的 J-results 列和一个用于 Perl 或任何其他基于 PCRE 的正则表达式引擎的 P-results 列:
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 个字符类转义:
通过用可预测和一致的方式实际匹配 Unicode 的东西替换它们。它只是来自单个黑客会话的 alpha 原型,但它是完整的功能。
简而言之,我的代码将这 14 个重写如下:
需要考虑的一些事情...
这使用
\X
了Unicode 现在所指的旧字素簇,而不是扩展字素簇,因为后者更为复杂。Perl 本身现在使用更高级的版本,但旧版本在最常见的情况下仍然完全可用。编辑:见底部附录。要做什么
\d
取决于您的意图,但默认值是 Uniode 定义。我可以看到人们不要总是想\p{Nd}
,但有时无论[0-9]
或\pN
。两个边界定义
\b
和\B
是专门为使用该\w
定义而编写的。这个
\w
定义过于宽泛,因为它抓住了括号内的字母,而不仅仅是圈出的字母。UnicodeOther_Alphabetic
属性在 JDK7 之前不可用,所以这是您能做的最好的事情。探索边界
边界已自从拉里·沃尔首先创造了一个问题
\b
和\B
语法在1987年谈论他们对Perl 1.0后面的关键是了解如何\b
与\B
这两个工作是打消她们两分无孔不入的神话:\w
字的字符,从来没有对非单词字符。一个
\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
,因此or
是X|Y
,并且因为 的and
优先级高于or
,所以就是AB|CD
。所以每\b
一个意味着边界可以安全地替换为:以
\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
替换为:这确实是如何
\b
和\B
行为。它们的等效模式是\b
使用((IF)THEN|ELSE)
构造是(?(?<=\w)(?!\w)|(?=\w))
\B
使用((IF)THEN|ELSE)
构造是(?(?=\w)(?<=\w)|(?<!\w))
但是带有 just 的版本
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
我在其他答案中发布的代码提供了这一点以及许多其他便利。这包括自然语言单词、破折号、连字符和撇号的定义,以及更多。
它还允许您在逻辑代码点中指定 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 版本,是我在周末修改的内容。它不会一直这样。
对于测试版,我打算:
将重复的代码折叠在一起
提供关于非转义字符串转义与增加正则表达式转义的更清晰的界面
在
\d
扩展中提供一些灵活性,也许\b
提供方便的方法来处理转向和调用 Pattern.compile 或 String.matches 或诸如此类的东西
对于生产版本,它应该有 javadoc 和一个 JUnit 测试套件。我可能包括我的 gigatester,但它不是作为 JUnit 测试编写的。
附录
我有好消息和坏消息。
好消息是,我现在得到了一个非常接近扩展字素簇的近似值,可用于改进的
\X
.坏消息 ☺ 是这种模式是:
在 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üß!