正则表达式:用匹配模式的长度替换已匹配的模式

3
假设我有一个像这样的字符串:
> x <- c("16^TG40")

我正在尝试获取结果c(16 2 40),其中2length(^TG)-1。例如,我可以通过以下方式找到此模式:
> gsub("(\\^[ACGT]+)", " \\1 ", x)
[1] "16 ^TG 40"

然而,我无法直接用其长度-1替换此字符串。有没有更简单的方法来替换匹配模式的长度?
经过一番搜索(在SO和Google搜索),我最终使用了stringr包,我认为它很棒。但是,归根结底,还是要找到此模式的位置(使用str_locate_all),然后用任何想要的值替换子字符串(使用str_sub)。我有超过100,000个字符串,这非常耗时(因为该模式也可能在字符串中出现多次)。
我目前正在并行运行以弥补速度缓慢的问题,但我很高兴知道是否可以直接(或快速地)解决此问题。
有什么想法吗?

你所有的字符串都以数字-字符-数字的类似模式出现吗?如果忽略问题中的字符串长度部分,你的正则表达式当前是否执行正确的拆分操作? - Justin
3个回答

8

(1) gsubfn gsubfn语句用它的长度和空格替换^...部分,而strapply从该字符串中提取数字并将其转换为数值。如果字符输出足够,则可以省略strapply

> library(gsubfn)
> xx <- gsubfn("\\^[ACGT]*", ~ sprintf(" %s ", nchar(x) - 1), x)
> strapply(xx, "\\d+", as.numeric)
[[1]]
[1] 16  2 40

(2) 循环遍历长度的集合

假设每个ACGT序列的字符数在mn和mx之间,并且使用gsub在一个循环中将长为i的ACGT序列替换为i。如果可能的长度只有几种,则迭代次数很少,因此速度很快,但如果字符串可能具有许多不同的长度,则将需要更多的循环迭代,速度会变慢。 在下面的示例中,我们假设ACGT序列的长度为2、4或6,但这些可能需要进行调整。该解决方案的一个可能的缺点是需要假设一组可能的序列长度。

x <- "4^CG5^CAGT656"

mn <- 2
mx <- 6
y <- x
for(i in seq(mn, mx, 2)) {
   pat <- sprintf("\\^[ACGT]{%d}(\\d)", i)
   replacement <- sprintf(" %d \\1", i)
   y <- gsub(pat, replacement, y)
}

(3) 循环遍历ACGT序列

这个循环遍历ACGT序列,将其中的一个替换为它的长度,直到没有ACGT序列可以替换。如果ACGT序列数量较少,则迭代次数较少,速度会很快;但如果存在大量ACGT序列,则由于需要进行更多迭代,速度会变慢。

x <- "4^CG5^CAGT656"
y <- x
while(regexpr("^", y, fixed = TRUE) > 0) {
    y <- sprintf("%s %d %s", sub("\\^.*", "", y),
        nchar(sub("^[0-9 ]+\\^([ACGT]+).*", "\\1", y)),
        sub("^[0-9 ]+\\^[ACGT]+", "", y))
}

基准测试

这里是一个基准测试。请注意,在某些解决方案中,我将字符串转换为数字(这当然需要额外的时间),但为了使基准测试可比较,我比较了创建字符串而不进行任何数字转换的速度。

x <- "4^CGT5^CCA656"
library(rbenchmark)
benchmark(order = "relative", replications = 10000,
   columns = c("test", "replications", "relative", "elapsed"),
   regmatch = {
      pat <- "(\\^[ACGT]+)"
      x2 <- x
      m <- gregexpr(pat, x2)
      regmatches(x2, m) <- sapply(regmatches(x2, m), modFun)
      x2
   },
   gsubfn = gsubfn("\\^[ACGT]*", ~ sprintf(" %s ", length(x) - 1), x),
   loop.on.len = {
    mn <- 2
    mx <- 6
    y <- x
    for(i in seq(mn, mx, 2)) {
       pat <- sprintf("\\^[ACGT]{%d}(\\d)", i)
       replacement <- sprintf(" %d \\1", i)
       y <- gsub(pat, replacement, y)
    }
   },
   loop.on.seq = {
    y <- x
    while(regexpr("^", y, fixed = TRUE) > 0) {
        y <- sprintf("%s %d %s", sub("\\^.*", "", y),
            nchar(sub("^[0-9 ]+\\^([ACGT]+).*", "\\1", y)),
            sub("^[0-9 ]+\\^[ACGT]+", "", y))
    }
  }
)

以下是结果。两种循环解决方案在所示输入上最快,但它们的性能将根据需要执行的迭代次数而变化,因此实际数据可能有所不同。loop.on.len解决方案的缺点是ACGT长度必须在假定集合中。Josh的regmatch解决方案不涉及循环且速度快。gsubfn解决方案的优点是仅一行代码且特别直接。
         test replications relative elapsed
4 loop.on.seq        10000    1.000    1.93
3 loop.on.len        10000    1.140    2.20
1    regmatch        10000    1.803    3.48
2      gsubfn        10000    7.145   13.79

更新:增加了两种循环解决方案并删除了此前无法处理多个ACGT序列的解决方案(根据评论澄清问题)。还重新进行了基准测试,只包括能够处理多个ACGT序列的解决方案。

更新:删除了一种无法处理多个^…序列的解决方案。它之前已从基准测试中删除,但代码尚未删除。在(1)中改进了解释。


目前gsubfn始终使用R引擎,因此tcltk在这里无法帮助。 gsubfn包中的strapplyc是最快的函数,但我尝试了一种基于它的变体,虽然比示例中的gsubfn解决方案更快,但仍然比其他所有解决方案都要慢。 - G. Grothendieck
ACGT序列是否与数字“23^AC23^GT45”交替出现,还是可以连续出现“4^AC^GA5”?它是否总是以数字开头和结尾?此外,如果重复出现,次数是否有限制?您能描述可能遇到的最一般形式吗?此外,您的向量中有许多这样的序列,还是只有一个? - G. Grothendieck
已添加了2个循环解决方案,删除了不能处理多个ACGT序列的解决方案,并重新进行了基准测试。 - G. Grothendieck

8
这里提供了一种基于 R 语言的方法。
尽管语法不够直观,但只要紧密遵循这个模板,你就可以执行所有形式的匹配子字符串的操作和替换(参见 ?gregexpr 获取更复杂的示例)。
x2 <- x <- c("16^TG40", "16^TGCT40", "16^TG40^GATTACA40")

pat <- "(\\^[ACGT]+)"              ## A pattern matching substrings of interest
modFun <- function(ss) {           ## A function to modify them
    paste0(" ", nchar(ss) - 1, " ")
}

## Use regmatches() <- regmatches(gregexpr()) to search, modify, and replace.
m <- gregexpr(pat, x2)
regmatches(x2, m) <- sapply(regmatches(x2, m), modFun)
x2
## [1] "16 2 40"      "16 4 40"      "16 2 40 7 40"

感谢Josh和其他人。我对我的数据进行了快速基准测试,您的函数运行速度最快(5.9秒),而Grothendieck的'gsubfn'解决方案需要12.8秒。但是,我没有使用'tcltk'来运行它。所以,我想我真的不能比较。无论如何,在这里我得到了很多提示。我会尝试优化。我想知道,既然“ \1”已经保持了该模式,为什么不可能对其进行“ length(.)”操作。 - Arun
我几天前意识到gregexpr也会给出匹配模式的长度.. :) 所以我用它来替换nchar,这显著提高了速度。觉得分享一下很好。再次感谢Josh。 - Arun
1
@Arun 是的,那是个好观点。我发现第一次使用 gregexprregmatches 有些棘手,但后来意识到这主要是因为它们是真正的强大工具。对于任何需要使用正则表达式并已经投入了必要的努力来掌握它们的人,我绝对会建议,“再花一个小时将这两个优秀的函数添加到你的工具箱中。” - Josh O'Brien
1
尽管如此,gsubfnstringr提供了实现gregexprregmatches大部分功能的替代方法。我只是偏好于使用基本的R解决方案,只要它们可用! - Josh O'Brien

2

我支持非常流畅的gsubfn答案,但是因为我已经有这个笨重的代码:

mod <- gsub("(\\^[ACGT]+)", " \\1 ", x)
locs <- gregexpr(" ", mod , fixed=TRUE)[[1]]
paste( substr( x, 1, locs[1]-1), 
       diff(locs)-2, 
       substr(mod, locs[2]+1, nchar(mod) ) , sep=" ")
#[1] "16 2 40"

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