【问题标题】:R Conditional Replace/Trim with Fill (regex,gsub,gregexpr,regmatches)R 条件替换/用填充修剪(正则表达式、gsub、gregexpr、regmatches)
【发布时间】:2024-04-12 11:35:01
【问题描述】:

我有一个涉及条件替换的问题。

我基本上想找到每一个数字字符串,并且对于 4 之后的每个连续数字,用空格替换它。

我需要矢量化解决方案,速度至关重要。

这是一个可行的(但效率低下的解决方案):

data <- data.frame(matrix(NA, ncol=2, nrow=6, dimnames=list(c(), c("input","output"))), 
                              stringsAsFactors=FALSE)
data[1,] <- c("STRING WITH 2 FIX(ES): 123456    098765  1111   ",NA)
data[2,] <- c(" PADDED STRING WITH 3 FIX(ES): 123456    098765  111111   ",NA)
data[3,] <- c(" STRING WITH 0 FIX(ES): 12        098     111   ",NA)
data[4,] <- c(NA,NA)
data[5,] <- c("1234567890",NA)
data[6,] <- c("   12345   67890    ",NA)

x2 <- data[,"input"]
x2

p1 <- "([0-9]+)"

m1 <- gregexpr(p1, x2,perl = TRUE)

nchar1 <- lapply(regmatches(x2, m1), function(x){
  if (length(x)==0){ x <- NA  } else ( x <- nchar(x))
  return(x) })

x3 <- mapply(function(match,length,text,cutoff) {
  temp_comb <- data.frame(match=match, length=length, stringsAsFactors=FALSE)

  for(i in which(temp_comb[,"length"] > cutoff))
  {
    before <- substr(text, 1, (temp_comb[i,"match"]-1))
    middle_4 <- substr(text, temp_comb[i,"match"], temp_comb[i,"match"]+cutoff-1)
    middle_space <-  paste(rep(" ", temp_comb[i,"length"]-cutoff),sep="",collapse="")
    after <-  substr(text, temp_comb[i,"match"]+temp_comb[i,"length"], nchar(text))
    text <- paste(before,middle_4,middle_space,after,sep="")
  }
  return(text)

},match=m1,length=nchar1,text=x2,cutoff=4)

data[,"output"] <- x3

有没有更好的办法?

我正在查看 regmatches 的帮助部分,并且有一个类似类型的问题,但它是用空白完全替换而不是有条件的。

我会写一些替代方案并对其进行基准测试,但老实说,我想不出其他方法来做到这一点。

提前感谢您的帮助!

更新

斑点,

使用您的方式但将 cutoff 作为输入,我收到 NA 案例的错误:

#replace numbers afther the 4th with spaces for those matches
zz<-lapply(regmatches(data$input, m), function(x,cutoff) {

    # x <- regmatches(data$input, m)[[4]]
    # cutoff <- 4

    mapply(function(x, n, cutoff){
      formatC(substr(x,1,cutoff), width=-n)
    }, x=x, n=nchar(x),cutoff=cutoff)

},cutoff=4)

【问题讨论】:

    标签: regex r replace trim


    【解决方案1】:

    这是一种只需一个 gsub 命令的快速方法:

    gsub("(?<!\\d)(\\d{4})\\d*", "\\1", data$input, perl = TRUE)
    # [1] "STRING WITH 2 FIX(ES): 1234    0987  1111   "        
    # [2] " PADDED STRING WITH 3 FIX(ES): 1234    0987  1111   "
    # [3] " STRING WITH 0 FIX(ES): 12        098     111   "    
    # [4] NA                                                    
    # [5] "1234"                                                
    # [6] "   1234   6789    "  
    

    字符串(?&lt;!\\d) 是负前瞻:前面没有数字的位置。字符串(\\d{4}) 表示4 个连续数字。最后,\\d* 代表任意位数。匹配此正则表达式的字符串部分被第一组(前 4 位)替换。


    不改变字符串长度的方法:

    matches <- gregexpr("(?<=\\d{4})\\d+", data$input, perl = TRUE)
    mapply(function(m, d) {
      if (!is.na(m) && m != -1L) {
        for (i in seq_along(m)) {
          substr(d, m[i], m[i] + attr(m, "match.length") - 1L) <- paste(rep(" ", attr(m, "match.length")[i]), collapse = "")
        }
      }
      return(d)
    }, matches, data$input)
    
    # [1] "STRING WITH 2 FIX(ES): 1234      0987    1111   "          
    # [2] " PADDED STRING WITH 3 FIX(ES): 1234      0987    1111     "
    # [3] " STRING WITH 0 FIX(ES): 12        098     111   "          
    # [4] NA                                                          
    # [5] "1234      "                                                
    # [6] "   1234    6789     "  
    

    【讨论】:

    • 因此,您实际上是在缩短字符串,而不是用空格替换多余的数字。我认为这与 OP 所要求的不同。
    • Sven,感谢您的帮助,但您的回答与我的回答不同。我看到了一些差异:第 1 行应该在 1234 和 0987 之间有 6 个空格,第 4 行没有尾随空格。我不想只删除 4 之后的字符,我想用空格替换它们,所以替换后字符串的长度应该相同。
    • @Brad 查看更新。我没有测试这种方法是否比你的更快。
    • 谢谢斯文。你说得通
    【解决方案2】:

    你可以在一行中做同样的事情(一个数字一个空格)

    gsub("(?:\\G(?!\\A)|\\d{4})\\K\\d", " ", data$input, perl = TRUE)
    

    详情:

    (?:        # non-capturing group: the two possible entry points
        \G     # either the position after the last match or the start of the string
        (?!\A) # exclude the start of the string position
      |        # OR
        \d{4}  # four digits
    )          # close the non-capturing group
    \K         # removes all on the left from the match result
    \d         # a single digit
    

    【讨论】:

    • @SvenHohenstein:禁止\G匹配字符串的开头。因为第一个匹配的入口点必须是第二个选择(即\d{4}),如果我允许\G 匹配字符串的开头并且如果字符串开头有数字,则该数字将为已删除。
    【解决方案3】:

    这是gregexprregmatches 的一种方式

    #find all numbers with more than 4 digits
    m <- gregexpr("\\d{5,}", data$input)
    
    #replace numbers afther the 4th with spaces for those matches
    zz<-lapply(regmatches(data$input, m), function(x) {
            mapply(function(x, n) formatC(substr(x,1,4), width=-n), x, nchar(x))
    })
    
    #combine with original values
    data$output2 <- unlist(Map(function(a,b) paste0(a,c(b,""), collapse=""), 
        regmatches(data$input, m, invert=T), zz))
    

    这里的不同之处在于它将NA 值转换为""。我们可以添加其他检查来防止这种情况发生,或者在最后将所有零长度字符串变成缺失值。我只是不想通过安全检查使代码过于复杂。

    【讨论】:

    • 谢谢弗里克。我喜欢这种方法。
    • Flick,当我使截止动态时出现错误:
    • 好吧,不要那样做。其实,我不明白你的意思。你到底做了什么改变,究竟是什么错误?
    • 您错误地使用了mapply。您不能只添加cutoff=cutoff,因为它的长度只有一并且mappply 期望所有参数都具有相同的长度。您必须使用 MoreArgs= 参数添加它。例如MoreArgs=list(cutoff=cutoff)。请参阅?mapply 了解更多信息。
    • 再次感谢!真的很感激!