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

Tim*_*ker 124 java regex unicode character-properties

许多现代正则表达式实现将\w字符类简写解释为"任何字母,数字或连接标点符号"(通常为下划线).这样一来,像一个正则表达式\w+像火柴的话hello,élève,GOÄ_432gefräßig.

不幸的是,Java没有.在Java中,\w仅限于[A-Za-z0-9_].这使得像上述那些匹配的单词难以解决.

似乎\b单词分隔符在不应该的位置匹配.

什么是类似.NET,Unicode感知\w\bJava 的正确等价物?哪些其他快捷方式需要"重写"以使其具有Unicode感知功能?

tch*_*ist 237

源代码

我在下面讨论的重写函数的源代码可以在这里找到.

Java 7中的更新

Sun的PatternJDK7 更新课程有一个了不起的新旗帜UNICODE_CHARACTER_CLASS,它使一切正常运作.它可以作为嵌入(?U)模式内部使用,因此您也可以将它与String类的包装器一起使用.它也体现了各种其他属性的修正定义.现在跟踪Unicode标准,在这两个RL1.2RL1.2aUTS#18:Unicode的正则表达式.这是一项激动人心且引人注目的改进,开发团队因此做出了重要贡献.


Java的正则表达式Unicode问题

使用Java正则表达式的问题是,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}
Run Code Online (Sandbox Code Playgroud)

这是一个真正的混乱,因为这意味着一些事情,如Alpha,LowerSpace做的不是在Java中映射为Unicode Alphabetic,LowercaseWhitespace性质.这太令人讨厌了.Java的Unicode属性支持严格来说是一个千禧一代,我的意思是它支持在过去十年中没有出现的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  -
Run Code Online (Sandbox Code Playgroud)

看到了吗?

根据Unicode,几乎每个Java空白结果都是错误的.这是一个非常大的问题. Java只是搞砸了,根据现有的做法和Unicode也给出了"错误"的答案.Plus Java甚至不能让您访问真正的Unicode属性!实际上,Java不支持任何与Unicode空白对应的属性.


所有这些问题的解决方案等等

为了解决这个问题和许多其他相关问题,昨天我写了一个Java函数来重写一个模式字符串,它重写了这14个charclass转义:

\w \W \s \S \v \V \h \H \d \D \b \B \X \R
Run Code Online (Sandbox Code Playgroud)

通过用可预测和一致的方式替换实际上与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*)
Run Code Online (Sandbox Code Playgroud)

有些事要考虑......

  • 这用于\X定义Unicode现在称为遗留字形集群,而不是扩展字形集群,因为后者更复杂.Perl本身现在使用的是版本较高的版本,但旧版本仍适用于最常见的情况.编辑:见底部的附录.

  • 该怎么做\d取决于你的意图,但默认是Uniode定义.我可以看到人们不要总是想\p{Nd},但有时无论[0-9]\pN.

  • 这两个边界定义,\b并且\B,被专门编写使用\w的定义.

  • 这个\w定义过于宽泛,因为它抓住了不仅仅是带圆圈的字母.在Other_AlphabeticJDK7之前,Unicode 属性不可用,因此这是您可以做的最好的.


探索边界

自1987 年拉里·沃尔(Larry Wall)首次为Perl 1.0谈论它们的语法\b\B语法以来,边界一直是一个问题.理解如何\b以及\B两者工作的关键是消除关于它们的两个普遍的神话:

  1. 他们永远只能找\w字的字符,从来没有对非单词字符.
  2. 它们并不专门寻找字符串的边缘.

一个\b边界的机构:

    IF does follow word
        THEN doesn't precede word
    ELSIF doesn't follow word
        THEN does precede word
Run Code Online (Sandbox Code Playgroud)

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

  • 跟着这个词(?<=\w).
  • 先于词(?=\w).
  • 不遵循的(?<!\w).
  • 不先字(?!\w).

因此,由于IF-THEN被编码为and ED-一起AB在正则表达式,一个orX|Y,并且因为and是在优先级高于or,即简单地AB|CD.所以\b这意味着边界可以安全地替换为:

    (?:(?<=\w)(?!\w)|(?<!\w)(?=\w))
Run Code Online (Sandbox Code Playgroud)

\w适当的方式定义.

(你可能会觉得很奇怪,组件AC组件是对立的.在一个完美的世界中,你应该能够写出来AB|D,但有一段时间我正在追逐Unicode属性中的互斥矛盾 - 我认为我已经处理过了,但是为了以防万一,我在边界中留下了双重条件.如果你以后得到额外的想法,这会使它更具可扩展性.)

对于\B非边界,逻辑是:

    IF does follow word
        THEN does precede word
    ELSIF doesn't follow word
        THEN doesn't precede word
Run Code Online (Sandbox Code Playgroud)

允许所有实例\B替换为:

    (?:(?<=\w)(?=\w)|(?<!\w)(?!\w))
Run Code Online (Sandbox Code Playgroud)

这真的是如何\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)
Run Code Online (Sandbox Code Playgroud)

然而,人们通常想要一种不同的边界.他们想要一些空白和字符串边缘感知的东西:

  • 左边(?:(?<=^)|(?<=\s))
  • 右边(?=$|\s)

使用Java修复Java

我在其他答案中发布的代码提供了这个以及其他一些便利.这包括自然语言单词,破折号,连字符和撇号的定义,以及更多内容.

它还允许您在逻辑代码点中指定Unicode字符,而不是在愚蠢的UTF-16代理中.很难过分重视它的重要性!这只是字符串扩展.

对于regex charclass替换,使得Java正则表达式中的charclass 最终使用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;
Run Code Online (Sandbox Code Playgroud)

无论如何,那段代码只是一个alpha版本,我在周末被黑了.它不会那样.

对于测试版,我打算:

  • 折叠代码重复

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

  • \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]+|[^[\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:.))
Run Code Online (Sandbox Code Playgroud)

在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:.))";
Run Code Online (Sandbox Code Playgroud)

¡Tschüß!

  • @BalusC:这是乔恩先前说过他让我提出这个问题.但是请不要放弃@tchrist中的`t`.它可能会发生在我脑海中.:) (12认同)
  • 这真太了不起了.非常感谢. (10认同)
  • 基督,这是一个很有启发性的答案.我只是没有得到Jon Skeet参考.他怎么处理这件事? (9认同)
  • 你有没有想过将这个添加到OpenJDK? (3认同)
  • @Martijn:我没有,没有; 我不知道那是"开放"的.:)但我考虑过以更正式的方式发布它; 我部门的其他人希望看到这样做(使用某种开源许可证,可能是BSD或ASL).我可能会改变这个alpha原型中的API,清理代码等等.但它有助于*我们*非常有用,我们认为它也会帮助其他人.我真的希望Sun能为他们的图书馆做些什么,但甲骨文却没有信心. (2认同)

mus*_*iKk 15

真的很不幸,\w不起作用.建议的解决方案\p{Alpha}对我也不起作用.

它似乎[\p{L}]捕获所有Unicode字母.所以Unicode的等价物\w应该是[\p{L}\p{Digit}_].


Ala*_*ore 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的; 它也使用Character.isLetterOrDigit(ch)和检查重音字母,但它识别的唯一"连接标点"字符是下划线. 编辑:当我尝试您的示例代码时,它会打印""élève"应该(在ideone.com上查看).

  • @Alan,当Unicode通过讨论扩展的和遗留的字形集群来澄清字形集群时,这是一个被清除的东西.字形集群的旧定义,其中`\ X`代表非标记后跟任意数量的标记,是有问题的,因为您应该能够将所有文件描述为匹配`/ ^(\ X*\R)*\R?$ /`,但如果你在文件的开头有一个`\ pM`,甚至是一行,你就不能.所以他们扩大了它总是匹配至少一个角色.它总是这样,但现在它使上述模式起作用.*[…继续…]* (3认同)
  • @Alan,Java的本地`\ b`部分支持Unicode是弊大于利.考虑将字符串""élève"`与模式`\ b(\ w +)\ b`匹配.看到问题? (2认同)