Java正则表达式中\w和\b的Unicode等价物?

Posted

技术标签:

【中文标题】Java正则表达式中\\w和\\b的Unicode等价物?【英文标题】:Unicode equivalents for \w and \b in Java regular expressions?Java正则表达式中\w和\b的Unicode等价物? 【发布时间】:2011-05-17 08:05:41 【问题描述】:

许多现代正则表达式实现将\w 字符类速记解释为“任何字母、数字或连接标点符号”(通常:下划线)。这样,像\w+ 这样的正则表达式会匹配像helloélèveGOÄ_432gefräßig 这样的词。

不幸的是,Java 没有。在 Java 中,\w 仅限于 [A-Za-z0-9_]。这使得匹配上述单词变得困难,以及其他问题。

似乎\b 单词分隔符在不应该匹配的地方匹配。

在 Java 中,类似 .NET、支持 Unicode 的 \w\b 的正确等价物是什么?哪些其他快捷方式需要“重写”以使其支持 Unicode?​​p>

【问题讨论】:

Tim 的短篇故事是,他们需要编写以使它们与 Unicode 保持一致。我仍然看不到任何迹象表明 Java 1.7 会在 Unicode 属性上做更多的事情,而不是 finally 添加对脚本的支持,但仅此而已。如果没有更好地访问完整的 Unicode 属性,有些事情你真的无法做到。如果你还没有我的 uniprops 和 unichars 脚本(和 uninames),它们会让你大开眼界。 可以考虑在单词类中添加标记。因为例如 ä可以用 Unicode 表示为 \u0061\u0308 或 \u00E4。 嘿蒂姆,看看我的更新。他们添加了一个标志以使其全部正常工作。万岁! 【参考方案1】:

源代码

我在下面讨论的重写函数的源代码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:]]     \pLower
[[:Upper:]]     \pUpper
[[:ASCII:]]     \pASCII
[[:Alpha:]]     \pAlpha
[[:Digit:]]     \pDigit
[[:Alnum:]]     \pAlnum
[[:Punct:]]     \pPunct
[[:Graph:]]     \pGraph
[[:Print:]]     \pPrint
[[:Blank:]]     \pBlank
[[:Cntrl:]]     \pCntrl
[[:XDigit:]]    \pXDigit
[[:Space:]]     \pSpace

这真是一团糟,因为这意味着像 AlphaLowerSpace 这样的东西在 Java 中映射到 Unicode AlphabeticLowercase、或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
            \pZs    0  0    0  0    1  1    0  0
         \pSpace    1  1    0  1    0  1    0  1
         \pBlank    0  0    0  0    0  1    0  0
    \pWhitespace    -  1    -  1    -  1    -  1
\pjavaWhitespace    1  -    0  -    0  -    1  -
 \pjavaSpaceChar    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\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]]
\W => [^\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]]

\b => (?:(?<=[\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])(?![\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])|(?<![\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])(?=[\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]]))
\B => (?:(?<=[\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])(?=[\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])|(?<![\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]])(?![\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&&\pSo]]))

\d => \pNd
\D => \PNd

\R => (?:(?>\u000D\u000A)|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029])

\X => (?>\PM\pM*)

需要考虑的一些事情...

它使用 \X 定义 Unicode now refers to 作为一个遗留的字素集群,而不是一个扩展的字素集群,因为后者更多复杂的。 Perl 本身现在使用更高级的版本,但旧版本在最常见的情况下仍然可以完美运行。 编辑:见底部的附录。

如何处理 \d 取决于您的意图,但默认是 Uniode 定义。我可以看到人们并不总是想要\pNd,但有时需要[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

这些都被完美地直接定义为:

跟随词(?&lt;=\w)前面的单词(?=\w)不跟随单词(?&lt;!\w)单词前面没有(?!\w)

因此,由于IF-THEN 在正则表达式中被编码为and ed-together ABorX|Y,并且因为and 的优先级高于or,所以就是AB|CD。所以每一个 \b 这意味着一个边界可以安全地替换为:

    (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))

以适当的方式定义\w

(你可能觉得AC 组件是相反的很奇怪。在一个完美的世界中,你应该可以写成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) 构造是 (?(?&lt;=\w)(?!\w)|(?=\w)) \B 使用 ((IF)THEN|ELSE) 构造是 (?(?=\w)(?&lt;=\w)|(?&lt;!\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)

但是,人们通常想要不同类型的边界。他们想要一些可以识别空格和字符串边缘的东西:

左边缘(?:(?&lt;=^)|(?&lt;=\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 版本,是我在周末破解的东西。不会一直这样。

对于测试版,我打算:

将重复的代码折叠在一起

提供关于非转义字符串转义与增强正则表达式转义的更清晰的界面

\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]+|[^[\pZl\pZp\pCc\pCf&&[^\u000D\u000A\u200C\u200D]]\u000D\u000A])[[\pMn\pMe\u200C\u200D\u0488\u0489\u20DD\u20DE\u20DF\u20E0\u20E2\u20E3\u20E4\uA670\uA671\uA672\uFF9E\uFF9F][\pMc\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]+|[^[\\pZl\\pZp\\pCc\\pCf&&[^\\u000D\\u000A\\u200C\\u200D]]\\u000D\\u000A])[[\\pMn\\pMe\\u200C\\u200D\\u0488\\u0489\\u20DD\\u20DE\\u20DF\\u20E0\\u20E2\\u20E3\\u20E4\\uA670\\uA671\\uA672\\uFF9E\\uFF9F][\\pMc\\u0E30\\u0E32\\u0E33\\u0E45\\u0EB0\\u0EB2\\u0EB3]]*)|(?s:.))";

¡Tschüß!

【讨论】:

这太棒了。非常感谢。 天哪,这是一个开明的答案。我只是没有得到 Jon Skeet 的参考资料。他和这有什么关系? @BalusC:Jon 早些时候说他会让我回答这个问题。但请不要在@tchrist 中删除t。它可能会进入我的脑海。 :) 你有没有想过把它添加到 OpenJDK 中? @Martijn:我没有,不;我不知道它是那么“开放”。 :) 但我考虑过以更正式的方式发布它;我部门的其他人希望看到这样做(使用某种开源许可证,可能是 BSD 或 ASL)。我可能会改变这个 alpha 原型中的 API,清理代码等。但它极大地帮助了 我们,我们认为它也会帮助其他人。我真的希望 Sun 能为他们的库做点什么,但 Oracle 没有信心。【参考方案2】:

在 Java 中,\w\d 不支持 Unicode;它们只匹配 ASCII 字符 [A-Za-z0-9_][0-9]\pAlpha 和朋友也是如此(他们所基于的 POSIX“字符类”应该是区域敏感的,但在 Java 中他们只匹配 ASCII 字符)。如果你想匹配 Unicode “单词字符”,你必须把它拼出来,例如[\pL\pMn\pNd\pPc],用于字母、非间距修饰符(重音)、十进制数字和连接标点符号。

然而,Java 的 \b Unicode 的;它使用Character.isLetterOrDigit(ch) 并检查重音字母,但它识别的唯一“连接标点符号”字符是下划线。 编辑:当我尝试您的示例代码时,它会打印出 ""élève" 应有的内容 (see it on ideone.com)。

【讨论】:

对不起,Alan,但你真的不能说 Java 的 \b 是 Unicode 的。它会犯很多错误。 "\u2163=""\u24e7=""\u0301=" 在 Java 中都无法匹配模式 "\\b=",但假定到 — 正如 perl -le 'print /\b=/ || 0 for "\x2163=", "\x24e7=", "\x301="' 所揭示的那样。但是,如果(且仅当)您使用 my 版本的单词边界而不是 Java 中的原生 \b,那么这些都可以在 Java 中使用。 @tchrist:我没有评论\b 的正确性,只是指出它对Unicode 字符(如Java 中实现的那样)进行操作,而不仅仅是像\w 和朋友这样的ASCII。但是,当\u0301 与基本字符配对时,它确实可以正常工作,如e\u0301=。而且我不相信 Java 在这种情况下是错误的。除非它是带有字母的字素簇的一部分,否则如何将组合标记视为单词字符? @Alan,当 Unicode 通过讨论扩展和遗留字素集群来澄清字素集群时,这一点已经得到澄清。字素簇的旧定义(其中\X 代表非标记后跟任意数量的标记)是有问题的,因为您应该能够将所有文件描述为匹配/^(\X*\R)*\R?$/,但如果您不能在文件的开头有一个\pM,甚至是一行。因此,他们将其扩展为始终匹配至少一个字符。它总是如此,但现在它使上述模式起作用。 [...继续...] @Alan,Java 的原生 \b 部分支持 Unicode,弊大于利。考虑将字符串"élève" 与模式\b(\w+)\b 进行匹配。看到问题了吗? @tchrist:是的,没有单词边界,\w+ 会找到两个匹配项:lve,这已经够糟糕的了。但是 with 单词边界它什么也找不到,因为 \béè 识别为单词字符。至少,\b\w 应该就什么是单词字符和什么不是字符达成一致。【参考方案3】:

很遗憾\w 不起作用。建议的解决方案 \pAlpha 也不适用于我。

似乎[\pL] 捕获了所有Unicode 字母。所以\w的Unicode等价物应该是[\pL\pDigit_]

【讨论】:

\w 也匹配数字等等。我认为对于字母,\pL 会起作用。 你是对的。 \pL 就够了。我还认为只有字母才是问题所在。 [\pL\pDigit_] 应该捕获所有字母数字字符,包括下划线。 @MusicKk:请参阅我的答案以获得一个完整的解决方案,该解决方案允许您正常编写模式,然后将其传递给一个函数,该函数可以纠正 Java 的空白,以便它在 Unicode 上正常工作。跨度> 不,\w 被 Unicode 定义为比 \pL 和 ASCII 数字更广泛,所有愚蠢的事情。如果你想要一个能识别 Unicode 的 \w 用于 Java,你必须写 [\pL\pM\pNd\pNl\pPc[\pInEnclosedAlphanumerics&amp;&amp;\pSo]] — 或者你可以使用 here 中的 unicode_charclass 函数。对不起! @Tim,是的,对于字母 \pL 确实有效(你不需要接受一个字母的道具)。但是,您很少需要这样,因为您必须非常小心,您的匹配不会仅仅因为您的数据采用 Unicode 规范化形式 D(又名 NFD,意思是规范 分解)而不是在 NFC 中(NFD 后跟规范 composition)。例如,代码点 U+E9 ("é") 是 NFC 形式的 \pL,但其 NFD 形式变为 U+65.301,因此匹配 \pL\pM。你可以有点\X:(?:(?=\pL)\X)来解决这个问题,但你需要我的Java版本。 :(

以上是关于Java正则表达式中\w和\b的Unicode等价物?的主要内容,如果未能解决你的问题,请参考以下文章

Java正则表达式学习

Python re模块 正则表达式之compile函数

python模块--re

python正则表达式re.findall(r"\b\w+\b", s)中的r是啥意思?

Oracle 正则表达式中的 ?= 等价于啥

正则表达式