为什么strsplit使用正向预查和反向预查匹配不同?

27

常识和使用gregexpr()进行的合理性检查表明,下面的顾前顾后断言应该在testString中各匹配一次:

testString <- "text XX text"
BB  <- "(?<= XX )"
FF  <- "(?= XX )"

as.vector(gregexpr(BB, testString, perl=TRUE)[[1]])
# [1] 9
as.vector(gregexpr(FF, testString, perl=TRUE)[[1]][1])
# [1] 5

strsplit()使用这些匹配位置来不同的方式,如果使用lookbehind断言,则在一个位置分割testString,但是如果使用lookahead断言,则在两个位置分割,第二个位置似乎不正确。

strsplit(testString, BB, perl=TRUE)
# [[1]]
# [1] "text XX " "text"    

strsplit(testString, FF, perl=TRUE)
# [[1]]
# [1] "text"    " "       "XX text"

我有两个问题:(Q1) 这里发生了什么?以及 (Q2) 如何使strsplit()行为更佳?


更新:Theodore Lytras的出色回答解释了发生了什么,因此解决了(Q1)。我的答案建立在他的基础之上,提出了一种解决方法,解决了(Q2)


1
FYI,关于为什么stringr::str_split的行为与strsplit不同,有一个相关的讨论在https://github.com/hadley/stringr/pull/23。 - hadley
3个回答

29

我不确定这是否算作一个程序错误,因为我认为这是基于 R 文档的预期行为。来自 ?strsplit:

应用于每个输入字符串的算法是

repeat {
    if the string is empty
        break.
    if there is a match
        add the string to the left of the match to the output.
        remove the match and all to the left of it.
    else
        add the string to the output.
        break.
}
注意,这意味着如果一个(非空)字符串开头有匹配项,则输出的第一个元素是‘""’,但是如果一个字符串结尾有匹配项,则输出与删除匹配项后相同。
问题在于前瞻(和后顾)断言是零长度的。因此例如在这种情况下:
FF <- "(?=funky)"
testString <- "take me to funky town"

gregexpr(FF,testString,perl=TRUE)
# [[1]]
# [1] 12
# attr(,"match.length")
# [1] 0
# attr(,"useBytes")
# [1] TRUE

strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "take me to " "f"           "unky town" 

发生的情况是孤立的前瞻(?=funky)在位置12匹配。因此,第一个分割包括字符串直到位置11(匹配左侧),它与匹配一起从字符串中删除,但是匹配长度为零。

现在剩余的字符串是funky town,前瞻在位置1匹配。但是没有什么可以删除的,因为匹配左侧没有任何东西,而匹配本身长度为零。所以算法陷入了无限循环。显然R通过分割单个字符来解决这个问题,这碰巧是使用空正则表达式(当参数split="")进行strsplit时的文档行为。之后,剩余字符串为unky town,由于没有匹配,因此将其作为最后一个分割返回。

回溯没问题,因为每个匹配都被分割并从剩余字符串中删除,因此算法永远不会陷入困境。

诚然,这种行为乍一看似乎很奇怪。然而,如果行为不同,就会违反前瞻零长度的假设。考虑到strsplit算法已经记录在文档中,我认为这并不符合漏洞的定义。


没错,这听起来很正确。似乎是避免无限循环的不幸方式,但这确实是strsplit算法所遵循的方式。谢谢! - Josh O'Brien

17

根据Theodore Lytras对substr()行为的仔细解释,一个相当干净的解决方法是在要匹配的前瞻断言之前加上正向回顾断言来匹配任何单个字符:

testString <- "take me to funky town"
FF2 <- "(?<=.)(?=funky)"
strsplit(testString, FF2, perl=TRUE)
# [[1]]
# [1] "take me to " "funky town" 

1
@TheodoreLytras -- 谢谢!根据您的解释,我在尝试之前就知道它会起作用。 - Josh O'Brien

5

在我看来,这似乎是一个错误。这不仅与空格有关,而是与任何孤立的前瞻(正向或负向)有关:

FF <- "(?=funky)"
testString <- "take me to funky town"
strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "take me to " "f"           "unky town"  

FF <- "(?=funky)"
testString <- "funky take me to funky funky town"
strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "f"                "unky take me to " "f"                "unky "           
# [5] "f"                "unky town"       


FF <- "(?!y)"
testString <- "xxxyxxxxxxx"
strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "xxx"       "y"       "xxxxxxx"

如果在零宽断言之外添加一些需要捕获的内容,例如:

FF <- " (?=XX )"
testString <- "text XX text"
strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "text"    "XX text"

FF <- "(?= XX ) "
testString <- "text XX text"
strsplit(testString,FF,perl=TRUE)
# [[1]]
# [1] "text"    "XX text"

也许像这样的东西可以作为一种解决方法。

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