【问题标题】:How to get possibly overlapping matches in a string如何在字符串中获得可能重叠的匹配
【发布时间】:2016-01-23 14:07:16
【问题描述】:

我正在寻找一种方法,无论是在 Ruby 还是 Javascript 中,它都会在一个字符串中针对正则表达式提供所有匹配项,可能是重叠的。


假设我有str = "abcadc",我想找到a 后跟任意数量的字符,然后是c。我正在寻找的结果是["abc", "adc", "abcadc"]。关于如何实现这一点的任何想法?

str.scan(/a.*c/) 会给我["abcadc"]str.scan(/(?=(a.*c))/).flatten 会给我["abcadc", "adc"]

【问题讨论】:

  • 读者:第一句话清楚地表明,需要的是一个调用all_matches(string, regex)的方法,它适用于任意字符串和正则表达式。
  • 通常使用正则表达式引擎,匹配是“最左边最长”的,除非你指定一个非贪婪量词,你会得到“最左边最短”匹配。您不能真的期望在一个表达式中同时获得最短和最长的表达式。得到所有最短的,然后找到串联的所有排列将是最好的策略。
  • @theTinMan,最初的问题是关于给定正则表达式的匹配问题,这只是一个带有“让我们说”的例子。编辑后,这个特定的正则表达式匹配看起来像是问题的重点。我不同意你的修改。
  • 随意编辑。这就是 SO 的工作原理。不过,“让我们说”是虚构的。我们正在处理编程并创建一本关于 SO 的参考书,所以推测很少是合适的。

标签: javascript ruby regex


【解决方案1】:
def matching_substrings(string, regex)
  string.size.times.each_with_object([]) do |start_index, maching_substrings|
    start_index.upto(string.size.pred) do |end_index|
      substring = string[start_index..end_index]
      maching_substrings.push(substring) if substring =~ /^#{regex}$/
    end
  end
end

matching_substrings('abcadc', /a.*c/) # => ["abc", "abcadc", "adc"]
matching_substrings('foobarfoo', /(\w+).*\1/) 
  # => ["foobarf",
  #     "foobarfo",
  #     "foobarfoo",
  #     "oo",
  #     "oobarfo",
  #     "oobarfoo",
  #     "obarfo",
  #     "obarfoo",
  #     "oo"]
matching_substrings('why is this downvoted?', /why.*/)
  # => ["why",
  #     "why ",
  #     "why i",
  #     "why is",
  #     "why is ",
  #     "why is t",
  #     "why is th",
  #     "why is thi",
  #     "why is this",
  #     "why is this ",
  #     "why is this d",
  #     "why is this do",
  #     "why is this dow",
  #     "why is this down",
  #     "why is this downv",
  #     "why is this downvo",
  #     "why is this downvot",
  #     "why is this downvote",
  #     "why is this downvoted",
  #     "why is this downvoted?"]

【讨论】:

  • @mudasobwa,你的没有回答这个问题(又名给定正则表达式,获取与其匹配的子字符串)。我最初的解决方案遇到了同样的问题。
  • 我显然没有拒绝,但你的反对是愚蠢的:你提供了代码,就像我一样,而不仅仅是一个神奇的正则表达式。这种情况的神奇正则表达式不存在,原因很明显:这个问题不能用简单的状态机来解决。
  • @mudasobwa,你是什么意思?假设正则表达式中没有环视,我的解决方案适用于随机正则表达式。即使环顾四周,问题也是可以解决的。我的反对意见是您的解决方案无法参数化正则表达式。
  • 我建议您删除/替换 “为什么这个投票不赞成?” 示例,因为它很可能只会吸引更多反对票。虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
【解决方案2】:

在 Ruby 中,您可以使用以下方法实现预期结果:

str = "abcadc"
[/(a[^c]*c)/, /(a.*c)/].flat_map{ |pattern| str.scan(pattern) }.reduce(:+)
# => ["abc", "adc", "abcadc"]

这种方式是否适合您在很大程度上取决于您真正想要实现的目标。

我试图把它放在一个表达式中,但我无法让它工作。我真的很想知道是否有一些科学原因无法通过正则表达式解析,或者我对 Ruby 的解析器 Oniguruma 的了解不够。

【讨论】:

  • 假设 OP 的字符串和正则表达式只是一个例子,这并不能给出问题的通用答案。
  • 如果是这样,请举一个不起作用的例子。
  • 如果问题是关于匹配/b.*d/ 怎么办?还是关于/x.*y.*z.*[^m]*foo/
  • 该解决方案可以很容易地适应您的第一个示例。对于第二个,你可能是对的。我完全不知道如何适应它。这就是为什么我写了这句话说这取决于 OP 究竟想要达到什么目标。
  • @WilliamFeng abcadcdc 中的expected result 不应该包括abcadcadcdc 吗?
【解决方案3】:

您想要所有可能的匹配项,包括重叠的匹配项。正如您所指出的,“How to find overlapping matches with a regexp?”中的前瞻技巧不适用于您的情况。

在一般情况下,我能想到的唯一可行的方法是生成字符串的所有可能子字符串,并根据正则表达式的锚定版本检查每个子字符串。这是蛮力,但它有效。

鲁比:

def all_matches(str, regex)
  (n = str.length).times.reduce([]) do |subs, i|
     subs += [*i..n].map { |j| str[i,j-i] }
  end.uniq.grep /^#{regex}$/
end

all_matches("abcadc", /a.*c/) 
#=> ["abc", "abcadc", "adc"]

Javascript:

function allMatches(str, regex) {
  var i, j, len = str.length, subs={};
  var anchored = new RegExp('^' + regex.source + '$');
  for (i=0; i<len; ++i) {
    for (j=i; j<=len; ++j) {
       subs[str.slice(i,j)] = true;
    }
  }
  return Object.keys(subs).filter(function(s) { return s.match(anchored); });
}

【讨论】:

    【解决方案4】:

    在 JS 中:

    function extract_ov_matches(r, s) {
      var res = [], cur;
      r = RegExp('^(?:' + r.source + ')$', r.toString().replace(/^[\s\S]*\/(\w*)$/, '$1').replace('g',''));
      for (var q = 0; q < s.length; ++q)
        for (var w = q; w <= s.length; ++w)
          if (r.test(cur = s.substring(q, w)))
            res.push(cur);
      return res;
    }
    document.body.innerHTML += "<pre>" + JSON.stringify(extract_ov_matches( /a.*c/g, 'abcadc' ), 0, 4) + "</pre>";

    这里的重点是您需要创建输入字符串的所有可能排列,然后返回完全匹配的那些提供的模式。

    extract_ov_matches 函数概述

    • r 是提供的正则表达式(已编译的正则表达式对象,带有标志)
    • s 是输入字符串
    • RegExp('^(?:' + r.source + ')$', r.toString().replace(/^[\s\S]*\/(\w*)$/, '$1').replace('g','')); 重新创建带有锚点的正则表达式(^ 用于字符串的开头,$ 用于字符串的结尾)以匹配整个字符串并删除g 标志(因为正则表达式将为used with RegExp#test
    • for (var q = 0; q &lt; s.length; ++q) for (var w = q; w &lt;= s.length; ++w) 用于创建所有输入字符串排列
    • if (r.test(cur = s.substring(q, w))) res.push(cur);:如果当前排列完全匹配模式,则添加到res,最终将返回。

    【讨论】:

    • @mudasobwa:效果很好,因为它会尝试输入字符串的所有可能子字符串。
    【解决方案5】:
    ▶ str = "abcadc"
    ▶ from = str.split(/(?=\p{L})/).map.with_index { |c, i| i if c == 'a' }.compact
    ▶ to   = str.split(/(?=\p{L})/).map.with_index { |c, i| i if c == 'c' }.compact
    ▶ from.product(to).select { |f,t| f < t }.map { |f,t| str[f..t] }
    #⇒ [
    #  [0] "abc",
    #  [1] "abcadc",
    #  [2] "adc"
    # ]
    

    我相信,有一种奇特的方法可以找到字符串中字符的所有索引,但我找不到它:( 有什么想法吗?

    在“unicode char 边界”上进行拆分使其可以处理'ábĉ''Üve Østergaard' 等字符串。

    对于更通用的解决方案,它接受任何“from”和“to”序列,只需进行一点修改:找到字符串中“from”和“to”的所有索引。

    【讨论】:

    • @ndn 我不能,感谢您指出我也不能split(//)。试试'ábĉ'
    • 假设 OP 的字符串和正则表达式只是一个例子,这并没有给出问题的通用答案。
    • 在 ruby​​ 2 中,您可以使用 split 方法代替:from = str.chars.to_a.map.with_index { |c, i| i if c == 'a' }.compact
    • @CasimiretHippolyte 我不能使用chars,因为组合了变音符号。
    • @ndn 它适用于 1 符号分隔符。
    【解决方案6】:

    这是一种类似于@ndn 和@Mark 的方法,适用于任何字符串和正则表达式。我已将此作为String 的方法实现,因为那是我想看到的地方。对String#[]String#scan 来说不是一个很好的赞美吗?

    class String
      def all_matches(regex)
        return [] if empty?
        r = /^#{regex}$/
        1.upto(size).with_object([]) { |i,a|
          a.concat(each_char.each_cons(i).map(&:join).select { |s| s =~ r }) }
      end
    end
    
    'abcadc'.all_matches /a.*c/
      # => ["abc", "abcadc", "adc"]
    'aaabaaa'.all_matches(/a.*a/)
      #=> ["aa", "aa", "aa", "aa", "aaa", "aba", "aaa", "aaba", "abaa", "aaaba",
      #    "aabaa", "abaaa", "aaabaa", "aabaaa", "aaabaaa"] 
    

    【讨论】:

      【解决方案7】:

      RegExp/(a.c)|(a.*c)/g的做法是匹配"a"字符后跟任意字符后跟"c""a.*c" 是匹配 "a" 后跟任意字符后跟前一个字符后跟 "c" 字符;注意RegExp(a.*c) 可能会得到改进。 if 的条件检查输入字符串中的最后一个字符是否为 "c" ,如果是 true ,则将完整的输入字符串推送到 res 结果数组

      var str = "abcadc"
      , res = str.match(/(a.c)|(a.*c)/g); 
      if (str[str.length - 1] === "c") res.push(str);
      
      document.body.textContent = res.join(" ")

      【讨论】:

      • 请解释为什么这很有用。建议代码很棒,但解释为什么代码是正确的可以教育那些正在寻找解决方案的人,以便他们以后可以重用它。
      • 把它放在答案中而不是放在评论中。
      【解决方案8】:

      这种 JavaScript 方法提供了优于 Wiktor's answer 的优势,通过 lazily 使用 generator function 迭代给定字符串的子字符串,这允许您一次使用单个匹配项来处理非常大的输入使用for...of 循环的字符串,而不是一次生成整个匹配数组,这可能导致内存不足异常,因为字符串的子字符串数量随长度呈二次方增长:

      function * substrings (str) {
        for (let length = 1; length <= str.length; length++) {
          for (let i = 0; i <= str.length - length; i++) {
            yield str.slice(i, i + length);
          }
        }
      }
      
      function * matchSubstrings (str, re) {
        const subre = new RegExp(`^${re.source}$`, re.flags);
        
        for (const substr of substrings(str)) {
          if (subre.test(substr)) yield substr;
        }
      }
      
      for (const match of matchSubstrings('abcabc', /a.*c/)) {
        console.log(match);
      }

      【讨论】:

      • 您的 sn-ps 清楚地显示了生成器的工作原理 +1 :)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-12-04
      • 2015-06-18
      • 1970-01-01
      • 2019-10-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多