Java正则表达式中\ w和\ b的Unicode等效项?


126

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

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

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

类似于.NET的,支持Unicode的\w\bJava 的正确等效项是什么?还有哪些其他快捷方式需要“重写”以使它们能够识别Unicode?


3
简而言之,蒂姆(Tim),他们需要写作才能使其与Unicode保持一致。我仍然没有迹象表明Java 1.7除了最终增加对脚本的支持之外,还可以使用Unicode属性做更多的事情,仅此而已。如果没有更好地访问Unicode属性的全部补充,您确实无法做某些事情。如果您还没有我的uniprops单字符脚本(和uninames),他们是惊人的,令人叹为观止到这一切。
tchrist

人们可能会考虑在单词class上添加标记。由于例如ä 可以用Unicode表示为\ u0061 \ u0308或\ u00E4。
Mostowski

3
嗨,蒂姆,请查看我的更新。他们添加了一个标志,以使其全部正常工作。欢呼!
tchrist 2011年

Answers:


240

源代码

我下面讨论的重写功能的源代码可在此处获得

Java 7中的更新

Sun的PatternJDK7 更新类有一个很棒的新标志UNICODE_CHARACTER_CLASS,它使一切重新正常运行。它可以作为(?U)模式内部的可嵌入对象使用,因此您也可以将其与String类的包装器一起使用。它还可以对其他各种属性进行更正定义。现在跟踪Unicode标准,在这两个RL1.2RL1.2aUTS#18:Unicode的正则表达式。这是令人振奋的巨大进步,因此,开发团队为此付出的巨大努力值得称赞。


Java的Regex Unicode问题

使用Java正则表达式的问题是,Perl的1.0 charclass将逃逸-这意味着\w\b\s\d和它们的补-不是Java扩展工作使用Unicode。其中\b只有一个具有某些扩展的语义,但是它们既不映射\wUnicode标识符,也不映射到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}

这是一个真正的混乱,因为这意味着一些事情,如AlphaLowerSpace做的不是在Java中映射为Unicode AlphabeticLowercaseWhitespace性质。这真是令人讨厌。Java的Unicode属性严格地antemillennial也就是说我不支持最近十年来出现的Unicode属性。

无法正确谈论空白是一件令人讨厌的事情。请考虑下表。对于每个代码点,Java都有一个J结果列,Perl或任何其他基于PCRE的正则表达式引擎都有一个P结果列:

             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个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*)

要考虑的一些事情...

  • 这使用\XUnicode现在称为遗留字素簇而不是扩展字素簇的定义,因为后者更为复杂。现在,Perl本身使用的是高级版本,但是对于大多数常见情况,旧版本仍然可以正常使用。编辑:请参阅底部的附录。

  • 该做什么\d取决于您的意图,但是默认值为Uniode定义。我看到人们并非总是想要\p{Nd},但有时[0-9]还是想要\pN

  • 专门编写了两个边界定义\b\B,以使用该\w定义。

  • \w定义过于宽泛,因为它抓住了赦免字母,而不仅仅是带圆圈的字母。Unicode Other_Alphabetic属性直到JDK7才可用,所以这是您可以做的最好的事情。


探索边界

边界已自从拉里·沃尔首先创造了一个问题\b\B语法在1987年谈论他们对Perl 1.0后面的关键是了解如何\b\B这两个工作是打消她们两分无孔不入的神话:

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

一个\b边界的机构:

    IF does follow word
        THEN doesn't precede word
    ELSIF doesn't follow word
        THEN does precede word

所有这些都可以直接定义为:

  • 跟随单词 is (?<=\w)
  • 先于词(?=\w)
  • 没有遵循单词 is (?<!\w)
  • 之前没有字(?!\w)

因此,由于IF-THEN被编码为and ED-一起AB在正则表达式,一个orX|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)构造是(?(?<=\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

我在其他答案中发布的代码提供了此功能,并提供了许多其他便利。其中包括自然语言单词,破折号,连字符和撇号的定义,以及更多定义。

它还允许您在逻辑代码点中指定Unicode字符,而不是在惯用的UTF-16替代中指定。很难强调这是多么重要!那只是为了字符串扩展。

要进行正则表达式charclass替换,使Java regexes中的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;

无论如何,这些代码只是一个alpha版本,这是我在周末破解的内容。不会那样的。

对于Beta版,我打算:

  • 将代码重复折叠在一起

  • 提供关于转义字符串转义与增加正则表达式转义的更清晰接口

  • \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:.))

在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:.))";

sch!


10
这真太了不起了。非常感谢。
Tim Pietzcker 2010年

9
基督,那是一个启蒙的答案。我没有得到Jon Skeet参考。他与此有什么关系?
BalusC 2010年

12
@BalusC:这是对乔恩的引用,他说他会让我回答这个问题。但是请不要将t@tchrist放进去。它可能会传到我的头上。:)
tchrist

3
您是否考虑过将其添加到OpenJDK?
Martijn Verburg

2
@Martijn:我没有,没有;我不知道那是“开放”。:)但是我考虑过以更正式的方式发布它;我部门中的其他人希望这样做(使用某种开源许可证,可能是BSD或ASL)。我可能会更改此alpha原型中的API,清理代码等。但这对我们有很大帮助,而且我们认为它也会对其他人有所帮助。我真的希望Sun能对他们的库做些什么,但是Oracle对此没有信心。
tchrist

15

真不幸,\w这行不通。建议的解决方案\p{Alpha}对我也不起作用。

似乎[\p{L}]捕获了所有Unicode字母。因此,Unicode等效于\w应该是[\p{L}\p{Digit}_]


而且\w还可以匹配数字等等。我认为仅凭字母\p{L}就可以。
Tim Pietzcker,2010年

你是对的。\p{L}足够。我还认为只有字母是问题所在。[\p{L}\p{Digit}_]应该捕获所有字母数字字符,包括下划线。
musiKk 2010年

@MusicKk:有关完整解决方案的信息,请参见我的回答,该解决方案允许您正常编写模式,然后将其通过纠正Java巨大空白的函数传递,以使其在Unicode上正常工作。
tchrist

不,在所有愚蠢的事物中\w,Unicode定义的范围远不止于正义\pL和ASCII数字。[\pL\pM\p{Nd}\p{Nl}\p{Pc}[\p{InEnclosedAlphanumerics}&&\p{So}]]如果您要\wJava 具备Unicode意识,则必须编写代码,否则您可以unicode_charclass此处使用我的函数。抱歉!
tchrist

1
@Tim,是的,因为字母\pL确实起作用(您不需要拥抱一个字母的道具)。但是,您很少希望这样做,因为您必须非常小心,因为您的数据采用Unicode规范化形式D(又称NFD,即规范分解),而不是采用NFC(NFD后跟规范),因此您的匹配不会得到不同的答案组成)。一个示例是代码点U + E9("é")是\pLNFC形式的,但其NFD形式变为U + 65.301,因此match \pL\pM。您可以还挺解决这个问题有\X(?:(?=\pL)\X),但你需要我的Java版本的那个。:(
tchrist

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上查看)。


抱歉,艾伦,但是您真的不能说Java \b是Unicode的。它犯了无数错误。 "\u2163=",,"\u24e7=""\u0301="都无法"\\b="在Java中匹配模式,但应该如此 -如所perl -le 'print /\b=/ || 0 for "\x{2163}=", "\x{24e7}=", "\x{301}="'揭示。但是,如果(并且仅当)交换了的单词边界版本而不是\bJava 中的本机版本,那么所有这些也都适用于Java。
tchrist

@tchrist:我没有评论\b的正确性,只是指出它对Unicode字符(用Java实现)起作用,而不仅仅是像ASCII之类的\w和朋友。但是,关于\u0301何时将该字符与基本字符配对,它确实可以正常工作,如中所述e\u0301=。而且我不认为Java在这种情况下是错误的。除非组合符号是带有字母的字素簇的一部分,否则如何将组合标记视为单词字符?
艾伦·摩尔

3
@Alan,当Unicode通过讨论扩展字形集群与旧字形集群时,字形集群得到了澄清。字素簇的旧定义\X是有问题的,因为它应该能够将所有文件描述为match /^(\X*\R)*\R?$/,但是如果\pM开头是文件,甚至一行。因此,他们将其扩展为始终匹配至少一个字符。它总是这样做,但现在它使上述模式起作用。[…续…]
tchrist

2
@ Alan,Java的本机\b部分支持Unicode 弊大于利。考虑将字符串"élève"与模式匹配\b(\w+)\b。看到问题了吗?
tchrist

1
@tchrist:是的,没有边界这个词,\w+找到两个匹配项:lve,这足够糟糕。但是有了单词边界,它就什么也找不到了,因为它可以\b识别éè作为单词字符。至少,\b并且\w应该就什么是字字符和什么不是字字符达成共识。
艾伦·摩尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.