【问题标题】:extend gsub and grepl to ignore substrings between given delimiters扩展 gsub 和 grepl 以忽略给定分隔符之间的子字符串
【发布时间】:2020-03-05 14:41:50
【问题描述】:

我希望能够仅在给定的分隔符集之外使用grepl()gsub(),例如我希望能够忽略引号之间的文本。

这是我想要的输出:

grepl2("banana", "'banana' banana \"banana\"", escaped =c('""', "''"))
#> [1] TRUE
grepl2("banana", "'banana' apple \"banana\"", escaped =c('""', "''"))
#> [1] FALSE
grepl2("banana", "{banana} banana {banana}", escaped = "{}")
#> [1] TRUE
grepl2("banana", "{banana} apple {banana}", escaped = "{}")
#> [1] FALSE

gsub2("banana", "potatoe", "'banana' banana \"banana\"")
#> [1] "'banana' potatoe \"banana\""
gsub2("banana", "potatoe", "'banana' apple \"banana\"")
#> [1] "'banana' apple \"banana\""
gsub2("banana", "potatoe", "{banana} banana {banana}", escaped = "{}")
#> [1] "{banana} potatoe {banana}"
gsub2("banana", "potatoe", "{banana} apple {banana}", escaped = "{}")
#> [1] "{banana} apple {banana}"

实际案例可能以不同的数量和顺序引用了子字符串。

我编写了以下函数来处理这些情况,但是它们很笨重,而且gsub2() 一点也不健壮,因为它临时用占位符替换了分隔的内容,并且这些占位符可能会受到后续操作的影响。

regex_escape <-
function(string,n = 1) {
  for(i in seq_len(n)){
    string <- gsub("([][{}().+*^$|\\?])", "\\\\\\1", string)
  }
  string
}

grepl2 <- 
  function(pattern, x, ignore.case = FALSE, perl = FALSE, fixed = FALSE, 
           useBytes = FALSE, escaped =c('""', "''")){
    escaped <- strsplit(escaped,"")
    # TODO check that "escaped" delimiters are balanced and don't cross each other
    for(i in 1:length(escaped)){
      close <- regex_escape(escaped[[i]][[2]])
      open <- regex_escape(escaped[[i]][[1]])
      pattern_i <- sprintf("%s.*?%s", open, close)
      x <- gsub(pattern_i,"",x)
    }
    grepl(pattern, x, ignore.case, perl, fixed, useBytes)
  }

gsub2 <- function(pattern, replacement, x, ignore.case = FALSE, perl = FALSE, 
                   fixed = FALSE, useBytes = FALSE, escaped =c('""', "''")){
  escaped <- strsplit(escaped,"")
  # TODO check that "escaped" delimiters are balanced and don't cross each other
  matches <- character()
  for(i in 1:length(escaped)){
    close <- regex_escape(escaped[[i]][[2]])
    open <- regex_escape(escaped[[i]][[1]])
    pattern_i <- sprintf("%s.*?%s", open, close)
    ind <- gregexpr(pattern_i,x)
    matches_i <- regmatches(x, ind)[[1]]
    regmatches(x, ind)[[1]] <- paste0("((",length(matches) + seq_along(matches_i),"))")
    matches <- c(matches, matches_i)
  }
  x <- gsub(pattern, replacement, x, ignore.case, perl, fixed, useBytes)
  for(i in seq_along(matches)){
    pattern <- sprintf("\\(\\(%s\\)\\)", i)
    x <- gsub(pattern, matches[[i]], x)
  }
  x
}

有没有使用正则表达式且没有占位符的解决方案?请注意,我当前的函数支持多对分隔符,但我会满足于仅支持一对分隔符的解决方案,并且不会尝试匹配简单引号之间的子字符串。

也可以使用不同的分隔符,例如 {} 而不是 2 " 或 2 ',如果有帮助的话。

我也可以强加perl = TRUE

【问题讨论】:

  • 你可以使用perl = TRUE吗?
  • 是的,绝对的

标签: r regex gsub grepl


【解决方案1】:

您可以使用start/end_escape 参数提供匹配的分隔符的左对齐和右对齐,例如{},而不会在错误的位置匹配它们(} 作为左对齐分隔符)

perl = TRUE 允许环视断言。这些评估其中的陈述的有效性,没有在模式中捕获它们This post 很好地覆盖了它们。

perl = FALSE 中会出现错误,因为 R 的默认正则表达式引擎 TRE 不支持它们。

  gsub3 <- function(pattern, replacement, x, escape = NULL, start_escape = NULL, end_escape = NULL) {
      if (!is.null(escape) || !is.null(start_escape)) 
      left_escape <- paste0("(?<![", paste0(escape, paste0(start_escape, collapse = ""), collapse = ""), "])")
      if (!is.null(escape) || !is.null(end_escape))
      right_escape <- paste0("(?![", paste0(escape, paste0(end_escape, collapse = ""), collapse = ""), "])")
      patt <- paste0(left_escape, "(", pattern, ")", right_escape)
      gsub(patt, replacement, x, perl = TRUE)
    }
    gsub3("banana", "potatoe", "'banana' banana \"banana\"", escape = "'\"")
    #> [1] "'banana' potatoe \"banana\""
    gsub3("banana", "potatoe", "'banana' apple \"banana\"", escape = '"\'')
    #> [1] "'banana' apple \"banana\""
    gsub3("banana", "potatoe", "{banana} banana {banana}", escape = "{}")
    #> [1] "{banana} potatoe {banana}"
    gsub3("banana", "potatoe", "{banana} apple {banana}", escape = "{}")
    #> [1] "{banana} apple {banana}"

下面是grepl3 - 注意这不需要perl = TRUE,因为我们不关心模式捕获了什么,只关心它匹配。

grepl3 <- function(pattern, x, escape = "'", start_escape = NULL, end_escape = NULL) {
  if (!is.null(escape) || !is.null(start_escape)) 
  start_escape <- paste0("[^", paste0(escape, paste0(start_escape, collapse = ""), collapse = ""), "]")
  if (!is.null(escape) || !is.null(end_escape))
  end_escape <- paste0("[^", paste0(escape, paste0(end_escape, collapse = ""), collapse = ""), "]")
  patt <- paste0(start_escape, pattern, end_escape)
  grepl(patt, x)
}

grepl3("banana", "'banana' banana \"banana\"", escape =c('"', "'"))
#> [1] TRUE
grepl3("banana", "'banana' apple \"banana\"", escape =c('""', "''"))
#> [1] FALSE
grepl3("banana", "{banana} banana {banana}", escape = "{}")
#> [1] TRUE
grepl3("banana", "{banana} apple {banana}", escape = "{}")
#> [1] FALSE

编辑:

这应该可以解决 gsub 而不会出现 Andrew 提到的问题,只要您可以使用一组成对的运算符。我认为您可以修改它以允许多个分隔符。感谢这个有趣的问题,在regmatches 发现了一个新的宝石!

gsub4 <-
  function(pattern,
           replacement,
           x,
           left_escape = "{",
           right_escape = "}") {
    # `regmatches()` takes a character vector and
    # output of `gregexpr` and friends and returns
    # the matching (or unmatching, as here) substrings
    string_pieces <-
      regmatches(x,
                 gregexpr(
                   paste0(
                     "\\Q",  # Begin quote, regex will treat everything after as fixed.
                     left_escape,
                     "\\E(?>[^", # \\ ends quotes.
                     left_escape,
                     right_escape,
                     "]|(?R))*", # Recurses, allowing nested escape characters
                     "\\Q",
                     right_escape,
                     "\\E",
                     collapse = ""
                   ),
                   x,
                   perl = TRUE
                 ), invert =NA) # even indices match pattern (so are escaped),
                                # odd indices we want to perform replacement on.
for (k in seq_along(string_pieces)) {
    n_pieces <- length(string_pieces[[k]])
  # Due to the structure of regmatches(invert = NA), we know that it will always
  # return unmatched strings at odd values, padding with "" as needed.
  to_replace <- seq(from = 1, to = n_pieces, by = 2)
  string_pieces[[k]][to_replace] <- gsub(pattern, replacement, string_pieces[[k]][to_replace])
}
    sapply(string_pieces, paste0, collapse = "")
  }
gsub4('banana', 'apples', "{banana's} potatoes {banana} banana", left_escape = "{", right_escape = "}")
#> [1] "{banana's} potatoes {banana} apples"
gsub4('banana', 'apples', "{banana's} potatoes {banana} banana", left_escape = "{", right_escape = "}")
#> [1] "{banana's} potatoes {banana} apples"
gsub4('banana', 'apples',  "banana's potatoes", left_escape = "{", right_escape = "}")
#> [1] "apples's potatoes"
gsub4('banana', 'apples', "{banana's} potatoes", left_escape = "{", right_escape = "}")
#> [1] "{banana's} potatoes"

【讨论】:

  • 看起来很棒!我一有时间就会在标记上打勾
  • 非常聪明!请注意,对于grepl3,如果转义字符包含在开始/结束转义之间,则此解决方案可能会遇到问题。例如,如果有人在文本中使用 banana's(而 ' 是转义字符之一。请记住或更新。
  • gsub 解决方案的一个限制是我们不能使用"\\1" 作为替代
  • 您可以使用它,但它会在每个未转义的字符串片段处重置。您需要这个的实际示例是什么?
【解决方案2】:

我尝试了grepl2,但还没有破解(或想到一个明确的解决方案)gsub2。无论如何,这只会删除所提供的最短 escaped 字符对之间的任何字符(不包括新行)。它也应该可以很好地扩展。如果您使用此解决方案,您可能需要内置检查以确保有 pairsescaped 字符没有空格(或者以其他方式适应 substr() 的使用。希望这有帮助!

grepl3 <- 
  function(pattern, x, ignore.case = FALSE, perl = FALSE, fixed = FALSE, 
           useBytes = FALSE, escaped =c('""', "''")){

    new_esc1 <- gsub("([][{}().+*^$|\\?])", "\\\\\\1", substr(escaped, 1, 1))
    new_esc2 <- gsub("([][{}().+*^$|\\?])", "\\\\\\1", substr(escaped, 2, 2))
    rm_pat <- paste0(new_esc1, ".*?", new_esc2, collapse = "|")
    new_arg <- gsub(rm_pat, "", arg)
    grepl(pattern, new_arg)

  }

grepl3(pattern = "banana", x = "'banana' apple \"banana\" {banana}", escaped =c("''", '""', "{}"))
[1] FALSE

【讨论】:

    【解决方案3】:

    我的意见是,您可能需要将左括号和右括号分开以使代码正常工作。 在这里,我正在使用正则表达式环视功能。这可能不适用于 R 之外的通用(尤其是回溯 ?

    grepl2 = function(pattern, x, escapes = c(open="\"'{", close="\"'}")){
         grepl(paste0("(?<![", escapes[[1]], "])",
                      pattern, 
                      "(?![", escapes[[2]], "])"), 
               x, perl=T)
    }
    grepl2("banana", "'banana' banana \"banana\"")
    #> [1] TRUE
    grepl2("banana", "'banana' apple \"banana\"")
    #> [1] FALSE
    grepl2("banana", "{banana} banana {banana}")
    #> [1] TRUE
    grepl2("banana", "{banana} apple {banana}")
    #> [1] FALSE
    

    【讨论】:

    • 由于我们使用的是正则表达式功能,只需将 grepl 更改为 gsub 即可。
    【解决方案4】:

    这是一个在字符类中使用否定运算符的简单正则表达式解决方案。它只满足您的简单案例。我无法使其满足配对的多个分隔符请求:

    grepl2 <- function(patt, escape="'", arg=NULL) {
                 grepl( patt=paste0("[^",escape,"]", 
                                    patt,
                                    "[^",escape,"]"), arg) }
    
    grepl2("banana", "'banana' apple \"banana\"", escape =c( "'"))
    #[1] TRUE
    
    grepl2("banana", "'banana' apple ", escape =c( "'"))
    [#1] FALSE
    

    【讨论】:

    • 在此之下,gsub2("banana", "potatoe", "{banana} banana {banana}", escaped = "{}") 将产生"{banana}potatoe{banana}"。这就是否定字符集的问题。
    • 首先,我不清楚为什么那是错误的答案。而且...您确实有两个单独的请求。您应该将您的问题分为grepl 版本和gsub 版本。
    • 它正在剥离内部香蕉周围的空间,因为您匹配的模式是说“不是转义字符”。因此,空格被匹配并被删除。如果您在字符集中包含空格,则根本找不到匹配项。
    • 很好。提出两个问题。
    • 这是我的问题,不是他的:)。我认为这些问题有太多共同之处,无法分开。 grepl 更容易,但 gsub 解决方案可能也可以解决 grepl 问题,并且 grepl 解决方案可以为 gsub 提供线索
    猜你喜欢
    • 1970-01-01
    • 2012-03-28
    • 2012-04-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-08
    • 1970-01-01
    相关资源
    最近更新 更多