正则表达式独立工作,但在 strsplit 中一起使用时不起作用

Regexes works on their own, but not when used together in strsplit

我正在尝试使用 strsplit 和 perl 正则表达式在 R 中拆分字符串。该字符串由以句点或连字符分隔的各种字母数字标记组成,例如 "WXYZ-AB-A4K7-01A-13B-J29Q-10"。我想拆分字符串:

例如,"WXYZ-AB-A4K7-01A-13B-J29Q-10" 应生成 ["WXYZ", "AB", "01", "A", "13", "B", "J29Q", "10"]

我当前的正则表达式是 ((?<=[-.]\d{2})(?=[A-Z][-.]))|[.-],它在 this online regex tester 中完美运行。

此外,备选方案的两个部分 ((?<=[-.]\d{2})(?=[A-Z][-.]))[.-] 在单独使用时都用于按 R 中的预期拆分字符串:

#correctly splits on periods and hyphens
strsplit("WXYZ-AB-A4K7-01A-13B-J29Q-10", "[.-]", perl=T)
[[1]]
[1] "WXYZ" "AB"   "A4K7" "01A"  "13B"  "J29Q" "10"

#correctly splits tokens where a letter follows two digits
strsplit("WXYZ-AB-A4K7-01A-13B-J29Q-10", "((?<=[-.]\d{2})(?=[A-Z][-.]))", perl=T)
[[1]]
[1] "WXYZ-AB-A4K7-01" "A-13"            "B-J29Q-10"

但是当我尝试使用替代方法组合它们时,第二个正则表达式停止工作,并且字符串仅按句点和连字符拆分:

#only second alternative is used
strsplit("WXYZ-AB-A4K7-01A-13B-J29Q-10", "((?<=[-.]\d{2})(?=[A-Z][-.]))|[.-]", perl=T)
[[1]]
[1] "WXYZ" "AB"   "A4K7" "01A"  "13B"  "J29Q" "10"

为什么会这样?是我的正则表达式有问题,还是 strsplit 有问题?我怎样才能达到预期的行为?

期望的输出:

## [[1]]
## [1] "WXYZ" "AB"   "A4K7" "01"   "A"    "13"   "B"    "J29Q" "10"

另一种方法可以让您不必考虑 strsplit 算法的工作原理,即使用带有 gsub 的原始正则表达式在所有正确的位置插入一个简单的拆分字符,然后执行使用 strsplit 进行直接拆分。

strsplit(
    gsub("((?<=[-.]\d{2})(?=[A-Z][-.]))|[.-]", "-", x, perl = TRUE),
    "-", 
    fixed = TRUE)
#[[1]]
#[1] "XYZ"  "02"   "01"   "C"    "33"   "D"    "2285"

当然,RichScriven 的回答和 Wiktor Stribiżew 的评论可能更好,因为它们只有一个函数调用。

您可以使用 consuming 版本的正前瞻(匹配重置运算符 \K)来确保 strsplit 在 R 中正常工作并避免在正向后视中使用负向后视的问题。

"(?<![^.-])\d{2}\K(?=[A-Z](?:[.-]|$))|[.-]"

参见 R demo online (and a regex demo here)。

strsplit("XYZ-02-01C-33D-2285", "(?<![^.-])\d{2}\K(?=[A-Z](?:[.-]|$))|[.-]", perl=TRUE)
## => [[1]]
##    [1] "XYZ"  "02"   "01"   "C"    "33"   "D"    "2285"

strsplit("WXYZ-AB-A4K7-01A-13B-J29Q-10", "(?<![^.-])\d{2}\K(?=[A-Z](?:[.-]|$))|[.-]", perl=TRUE)
## => [[1]]
##    [1] "WXYZ" "AB"   "A4K7" "01"   "A"    "13"   "B"    "J29Q" "10" 

此处,模式匹配:

  • (?<![^.-])\d{2}\K(?=[A-Z](?:[.-]|$)) - 一系列:
    • (?<![^.-])\d{2} - 除了 .- 之外没有以字符开头的 2 个数字 (\d{2})(即以 [=17= 开头) ] 或 - 或字符串开头,这是避免环视内交替的常用技巧)
    • \K - 使正则表达式引擎丢弃到目前为止匹配的文本并继续匹配后续子模式(如果有的话)的匹配重置运算符
  • | - 或
  • [.-] - 匹配 .-.

感谢 Rich Scriven 和 Jota,我得以解决问题。每次 strsplit 找到匹配项时,它都会在查找下一个匹配项之前删除该匹配项及其左侧的所有内容。这意味着当后视与之前的匹配项重叠时,依赖后视的正则表达式可能无法按预期运行。在我的例子中,令牌之间的连字符在匹配时被删除,这意味着第二个正则表达式无法使用它们来检测令牌的开头:

#first match found
"WXYZ-AB-A4K7-01A-13B-J29Q-10"
     ^

#match + left removed
"AB-A4K7-01A-13B-J29Q-10"

#further matches found and removed
"01A-13B-J29Q-10"

#second regex fails to match because of missing hyphen in lookbehind:
#((?<=[-.]\d{2})(?=[A-Z][-.]))
# ^^^^^^^^
"01A-13B-J29Q-10"

#algorithm continues
"13B-J29Q-10"

根据 Jota 的建议,已通过将 [.-] class 替换为 boundary anchor 来检测后视中标记的边缘来修复此问题:

> strsplit("WXYZ-AB-A4K7-01A-13B-J29Q-10", "[-.]|(?<=\b\d{2})(?=[A-Z]\b)", perl=T)
[[1]]
[1] "WXYZ" "AB"   "A4K7" "01"   "A"    "13"   "B"    "J29Q" "10"