源代码
我在下面讨论的重写函数的源代码is available here。
Java 7 中的更新
Sun 为 JDK7 更新的 Pattern 类有一个了不起的新标志 UNICODE_CHARACTER_CLASS,它可以让一切恢复正常。它可以作为模式内部的可嵌入(?U) 使用,因此您也可以将它与String 类的包装器一起使用。它还对各种其他属性进行了更正的定义。它现在在 UTS#18:Unicode 正则表达式 中跟踪 RL1.2 和 RL1.2a 中的 Unicode 标准。这是一个令人兴奋和戏剧性的改进,开发团队的这一重要努力值得赞扬。
Java 的正则表达式 Unicode 问题
Java 正则表达式的问题在于 Perl 1.0 字符类转义——意思是 \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 这样的东西在 Java 中不映射到 Unicode Alphabetic、Lowercase、或Whitespace 属性。这非常烦人。 Java 对 Unicode 属性的支持是strictly antemillennial,我的意思是它不支持过去十年出现的任何 Unicode 属性。
不能正确地谈论空白是非常烦人的。考虑下表。对于这些代码点中的每一个,都有一个 J-results 列
用于 Java 和用于 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 空白的 any 属性。
所有这些问题的解决方案,以及更多
为了处理这个和许多其他相关问题,昨天我写了一个 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 now refers to 作为一个遗留的字素集群,而不是一个扩展的字素集群,因为后者更多复杂的。 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字字符,从不寻找非字字符。
- 它们并不专门寻找字符串的边缘。
\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-together AB,or 是X|Y,并且因为and 的优先级高于or,所以就是AB|CD。所以每一个 \b 这意味着一个边界可以安全地替换为:
(?:(?<=\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 的所有实例替换为:
(?:(?<=\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 代理项中。 很难过分强调它的重要性!这只是为了字符串扩展。
对于使 Java 正则表达式中的字符类最终在 Unicode 上工作的正则表达式 charclass 替换,并正常工作,获取 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 版本,是我在周末破解的东西。不会一直这样。
对于测试版,我打算:
对于生产版本,它应该有 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üß!