Java 正则表达式无法匹配 ASCII 范围之外的字符,与 Python 正则表达式行为不同。

3

我希望能像sklearn的CountVectorizer一样过滤文档中的字符串。它使用以下正则表达式:(?u)\b\w\w+\b。这段Java代码应该表现出相同的行为:

Pattern regex = Pattern.compile("(?u)\\b\\w\\w+\\b");
Matcher matcher = regex.matcher("this is the document.!? äöa m²");

while(matcher.find()) {
    String match = matcher.group();
    System.out.println(match);
}

但是这并不能像Python那样产生期望的输出:
this
is
the
document
äöa
m²

它输出为:

相反,它输出:

this
is
the
document

我应该怎么做才能包含非ASCII字符,就像Python中的RegeEx一样?


1
尝试使用"(?U)\\b\\w\\w+\\b"或者仅使用"(?U)\\w{2,}" - Wiktor Stribiżew
@WiktorStribiżew 针对 äöa 可以工作,但针对 无法工作。 - ctwheels
@LanceToth 我正在使用Java,而不是JavaScript。 - Daniel Kirchner
为了仅支持上标/下标数字,您可以将模式扩展为 "(?U)[\\w\\p{No}]{2,}" - Wiktor Stribiżew
@WiktorStribiżew 非常感谢,这似乎有效! - Daniel Kirchner
显示剩余9条评论
2个回答

4

如评论中Wiktor所建议的,您可以使用(?U)来打开UNICODE_CHARACTER_CLASS标志。虽然这确实允许匹配äöa,但仍无法匹配。这是因为带有\wUNICODE_CHARACTER_CLASS不会将²识别为有效的字母数字字符。作为对\w的替代,您可以使用[\pN\pL_]。这匹配Unicode数字\pN和Unicode字母\pL(加上_)。\pN Unicode字符类包括\pNo字符类,它包括Latin 1 Supplement - Latin-1 punctuation and symbols字符类(其中包括²³¹)。或者,您可以将\pNo Unicode字符类添加到具有\w的字符类中。这意味着以下正则表达式正确匹配您的字符串:

[\pN\pL_]{2,}         # Matches any Unicode number or letter, and underscore
(?U)[\w\pNo]{2,}      # Uses UNICODE_CHARACTER_CLASS so that \w matches Unicode.
                      # Adds \pNo to additionally match ²³¹

所以为什么在Java中\w不能匹配²,但在Python中可以呢?

Java的解释

查看OpenJDK 8-b132的Pattern实现,我们可以得到以下信息(我删除了与回答问题无关的信息):

Unicode支持

以下预定义字符类POSIX字符类符合Unicode正则表达式兼容性属性附录C建议,当指定UNICODE_CHARACTER_CLASS标志时。

\w一个单词字符:[\p{Alpha}\p{gc=Mn}\p{gc=Me}\p{gc=Mc}\p{Digit}\p{gc=Pc}\p{IsJoin_Control}]

太好了!现在我们有一个定义,当使用(?U)标志时,可以对\w进行插入。将这些Unicode字符类插入this amazing tool中,将告诉您每个Unicode字符类精确匹配的内容。为了不让这篇文章变得太长,我只会告诉你以下两个类都不匹配²:

  • \p{Alpha}
  • \p{gc=Mn}
  • \p{gc=Me}
  • \p{gc=Mc}
  • \p{Digit}
  • \p{gc=Pc}
  • \p{IsJoin_Control}

Python的解释

那么为什么在使用u标志与\w一起时,Python会匹配²³¹?这个问题很难追踪,但我深入研究了Python的源代码(我使用的是Python 3.6.5rc1-2018-03-13)。在删除了很多关于如何调用它的琐碎内容后,基本上发生了以下情况:

  • \w 被定义为 CATEGORY_UNI_WORD,然后加上前缀 SRE_SRE_CATEGORY_UNI_WORD 调用 SRE_UNI_IS_WORD(ch)
  • SRE_UNI_IS_WORD 被定义为 (SRE_UNI_IS_ALNUM(ch) || (ch) == '_')
  • SRE_UNI_IS_ALNUM 调用 Py_UNICODE_ISALNUM,它又被定义为 (Py_UNICODE_ISALPHA(ch) || Py_UNICODE_ISDECIMAL(ch) || Py_UNICODE_ISDIGIT(ch) || Py_UNICODE_ISNUMERIC(ch))
  • 这里重要的是 Py_UNICODE_ISDECIMAL(ch),它被定义为 Py_UNICODE_ISDECIMAL(ch) _PyUnicode_IsDecimalDigit(ch)

现在,让我们来看一下方法 _PyUnicode_IsDecimalDigit(ch)

int _PyUnicode_IsDecimalDigit(Py_UCS4 ch)
{
    if (_PyUnicode_ToDecimalDigit(ch) < 0)
        return 0;
    return 1;
}

正如我们所看到的,如果 _PyUnicode_ToDecimalDigit(ch) < 0,这个方法会返回1。那么 _PyUnicode_ToDecimalDigit 是什么样子的呢?
int _PyUnicode_ToDecimalDigit(Py_UCS4 ch)
{
    const _PyUnicode_TypeRecord *ctype = gettyperecord(ch);

    return (ctype->flags & DECIMAL_MASK) ? ctype->decimal : -1;
}

总之,如果字符的UTF-32编码字节具有 DECIMAL_MASK 标志,则表达式结果为true,将返回大于或等于 0 的值。

² 的UTF-32编码字节值为 0x000000b2,我们的标志 DECIMAL_MASK0x020x000000b2 & 0x02 结果为 true,因此在python中,\wu 标志匹配 ² 被视为有效的Unicode字母数字字符。


0
还有最后一步:您需要指定\w也包括Unicode字符。Pattern.UNICODE_CHARACTER_CLASS来帮忙:
    Pattern regex = Pattern.compile("(?u)\\b\\w\\w+\\b", Pattern.UNICODE_CHARACTER_CLASS);
                                                   // ^^^^^^^^^^
    Matcher matcher = regex.matcher("this is the document.!? äöa m²");

    while(matcher.find()) {
        String match = matcher.group();
        System.out.println(match);
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接