R如何处理正则表达式中的特殊字符?

5
我对以下三个测试的输出结果感到困惑:
第一个测试包含特殊字符「°」,但输出结果正确。
sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160")
[1] "01160"

这个包含引用的内容可以带来好的结果:
sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "01160 'aa")
[1] "01160"

但是这个包含了°和引号的内容却返回了奇怪的结果。
sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160 'aa")
[1] "0 'aa"

顺便说一下,我也对一个问题感到困惑:如果我将相同的输入作为向量给出,结果并不相同:

sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = c("A°C 01160", "01160 'aa", "A°C 01160 'aa"))
[1] "01160" "0 'aa" "0 'aa"

有人知道如何理解我的问题的起源吗?

我在Mac OS 10.8上运行R 3.02,并使用法语UTF-8编码选项:

> sessionInfo()
R version 3.0.2 (2013-09-25)
Platform: x86_64-apple-darwin10.8.0 (64-bit)

locale:
[1] fr_FR.UTF-8/fr_FR.UTF-8/fr_FR.UTF-8/C/fr_FR.UTF-8/fr_FR.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
[1] tools_3.0.2

2
我尝试了你的代码,但在所有情况下都得到了“好”的结果。你的示例似乎无法复制。你使用的是哪个版本的R?请将sessionInfo()的结果发布到你的问题中。 - Andrie
我能够在R 3.1.0和OS X 10.9.2上的英语UTF-8区域设置中重现此问题。 - joran
请注意,此问题特别与您的字符串中是否有 ° 有关。它还特定于使用预定义字符类,并且可以通过将 perlsetBytes 设置为 TRUE 来防止。不过我不知道这个问题是由什么引起的。 - MattLBeck
3个回答

4
命名字符类的解释(例如包括[:digit:])取决于相应的语言环境。它们可以包含非ASCII字符。 [[:digit:]]将匹配Unicode Nd类别中的任何字符。
如果您只想匹配ASCII十进制数字,请使用[0-9]
> sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160 'aa")
[1] "0 'aa"
> sub(pattern = ".*([0-9]{5}).*", replacement = "\\1", x = "A°C 01160 'aa")
[1] "01160"
> 

此外,你的观察并不特定于 R。引用自 regex

某些命名字符类是预定义的。它们的解释取决于语言环境(请参见locales);下面的解释是 POSIX 语言环境的解释。


编辑:上述内容的演示:

> Sys.getlocale()
[1] "LC_CTYPE=en_US.UTF-8;LC_NUMERIC=C;LC_TIME=en_US.UTF-8;LC_COLLATE=en_US.UTF-8;LC_MONETARY=en_US.UTF-8;LC_MESSAGES=en_US.UTF-8;LC_PAPER=C;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=en_US.UTF-8;LC_IDENTIFICATION=C"
> sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160 'aa")
[1] "0 'aa"
> Sys.setlocale("LC_ALL", "C") 
[1] "LC_CTYPE=C;LC_NUMERIC=C;LC_TIME=C;LC_COLLATE=C;LC_MONETARY=C;LC_MESSAGES=en_US.UTF-8;LC_PAPER=C;LC_NAME=C;LC_ADDRESS=C;LC_TELEPHONE=C;LC_MEASUREMENT=en_US.UTF-8;LC_IDENTIFICATION=C"
> sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160 'aa")
[1] "01160"
> 

为了详细说明演示,相同的替换在不同的地区返回了不同的结果。当切换到C语言环境时,结果是如预期的。

Unicode Nd并不包括例如aa这样的字符,但在示例中似乎与[[:digit:]]匹配。因此,除非我漏掉了什么,否则这并不能完全解释观察到的输出... - MattLBeck

3
问题并没有完全被你的测试定义。问题具体与°字符和其他特殊的UTF字符有关,例如Ĉ也会导致相同的问题。
奇怪的输出实际上总是你输入字符串的最后五个字符。因此,在你的初始“好”测试中添加另一个字符将显示该测试也给出了不正确的结果:
sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", x = "A°C 01160a")
[1] "1160a"

引用不涉及问题,是一个转移话题。使用perl=TRUEuseBytes=TRUE也可以避免这个问题的发生。
我认为这个问题与?regexp中以下摘录有关:
 In UTF-8 mode the named character classes only match ASCII
 characters

预定义的字符类可能无法正确处理UTF-8文本。在这种情况下,[0-9] 而不是 [[:digit:]] 似乎能够正常工作,因为它不是预定义类。
尽管如此,我仍不太清楚导致最后5个字符的具体输出原因。我的猜测是预定义的类在字符串使用UTF-8编码时匹配所有内容,因为你可以通过模式".*(.{5}).*"得到相同的输出。但至少我们更加确定了问题所在:预定义的字符类处理UTF-8字符集。

这实际上涉及到当前的语言环境。就是这样。 - devnull

2

试着使用 perl = TRUE

> sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", 
      x = "A°C 01160 'aa",perl = TRUE)
[1] "01160"

似乎对于其他版本也可行:

> sub(pattern = ".*([[:digit:]]{5}).*", replacement = "\\1", 
    x = c("A°C 01160", "01160 'aa", "A°C 01160 'aa"),perl = TRUE)
[1] "01160" "01160" "01160"

谢谢。它能工作,但它没有解释为什么另一个版本失败了。 - PAC
绕过方法固然很好,但我认为问题更多的是“为什么会发生这种情况?” - MattLBeck
@Mattrition <耸肩> 我尽我所能地贡献。 - joran

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