【问题标题】:How can I do fuzzy substring matching in Ruby?如何在 Ruby 中进行模糊子字符串匹配?
【发布时间】:2011-08-30 21:52:00
【问题描述】:

我发现了很多关于模糊匹配的链接,将一个字符串与另一个字符串进行比较,看看哪个获得了最高的相似度分数。

我有一个很长的字符串,它是一个文档和一个子字符串。子字符串来自原始文档,但经过多次转换,因此可能引入了奇怪的伪影,例如这里的空格,那里的破折号。子字符串将匹配原始文档中的一段文本 99% 或更多。我不匹配以查看此字符串来自哪个文档,我正在尝试在文档中查找该字符串开始的索引。

如果字符串是相同的,因为没有引入随机错误,我会使用document.index(substring),但是如果有一个字符差异,这将失败。

我认为差异可以通过删除字符串和子字符串中除 az 之外的所有字符,比较,然后使用我在压缩字符串时生成的索引将压缩字符串中的索引转换为索引真实的文件。这在空格和标点符号不同的地方效果很好,但只要一个字母不同,它就会失败。

文档一般是几页到上百页,子串从几句到几页。

【问题讨论】:

标签: ruby string fuzzy-search


【解决方案1】:

这取决于最终可能出现在子字符串中的工件。在它们不属于[a-z] 的更简单的情况下,您可以使用解析子字符串,然后在文档上使用Regexp#match

document = 'Ulputat non nullandigna tortor dolessi illam sectem laor acipsus.'
substr = "tortor - dolessi _%&#   +illam"

re = Regexp.new(substr.split(/[^a-z]/i).select{|e| !e.empty?}.join(".*"))
md = document.match re
puts document[md.begin(0) ... md.end(0)]
# => tortor dolessi illam

(这里,由于我们没有在正则表达式中设置任何括号,我们在MatchData 的第一个(完全匹配)元素0 上使用beginend

如果只对起始位置感兴趣,可以使用=~操作符:

start_pos = document =~ re

【讨论】:

  • 这确实是一个比我在多行代码中实现的解决方案更优雅的解决方案,如果人工制品从不属于 [a-z] 的一部分,则能够正确匹配。谢谢你。然而,事实证明,它们有时是,因此我需要一些“模糊”的东西(尽管,正如我所建议的,它们将匹配 99%,甚至 99.9%,这对于 Ruby 的 String.index 来说还不够)。跨度>
【解决方案2】:

我一个都没用过,但我只是通过在rubygems.org 中搜索“diff”找到了一些库。所有这些都可以通过 gem 安装。您可能想尝试一下。我自己很感兴趣,所以如果你已经知道这些或者如果你尝试过,如果你留下你的评论会很有帮助。

【讨论】:

    【解决方案3】:

    您可以尝试匹配。它可以作为 ruby​​ gem 使用,虽然我很长时间没有使用模糊逻辑,但它看起来有你需要的东西。 amatch的主页是:http://flori.github.com/amatch/

    只是对这个想法感到无聊和混乱,下面是一个完全未经优化和未经测试的解决方案:

    include 'amatch'
    
    module FuzzyFinder
      def scanner( input )
        out = [] unless block_given?
        pos = 0
        input.scan(/(\w+)(\W*)/) do |word, white|
          startpos = pos
          pos = word.length + white.length
          if block_given?
            yield startpos, word
          else
            out << [startpos, word]
          end
        end
      end
    
      def find( text, doc )
        index = scanner(doc)
        sstr = text.gsub(/\W/,'')
        levenshtein = Amatch::Levensthtein.new(sstr)
        minlen = sstr.length
        maxndx = index.length
        possibles = []
        minscore = minlen*2
        index.each_with_index do |x, i|
          spos = x[0]
          str = x[1]
          si = i
          while (str.length < minlen)
            i += 1
            break unless i < maxndx
            str += index[i][1]
          end
          str = str.slice(0,minlen) if (str.length > minlen)
          score = levenshtein.search(str)
          if score < minscore
            possibles = [spos]
            minscore = score
          elsif score == minscore
            possibles << spos
          end
        end
        [minscore, possibles]
      end
    end
    

    显然有许多改进可能而且可能是必要的!上面有几个:

    1. 处理文档一次并存储 结果,可能在数据库中。
    2. 确定字符串的可用长度 进行初步检查,处理 首先针对该初始子字符串 在尝试匹配整个 片段。
    3. 跟进上一个, 预先计算的起始片段 那个长度。

    【讨论】:

    • 它做了很多很棒的事情,包括搜索子字符串,但是它没有做的是告诉我它在哪里找到了子字符串!例如,我可以这样做:irb(main):003:0&gt; m = Sellers.new("pattern") =&gt; #&lt;Amatch::Sellers:0x0000010282d450&gt; irb(main):004:0&gt; m.search ("this is a very complex pxttern which I wnat you to have") =&gt; 1.0 它会告诉我它找到了匹配项,并且 Levenshtein 编辑距离,但是我需要知道的是 where 它找到了匹配项。我想知道是否可以通过编辑匹配的代码来获得它。
    • 如果子字符串总是以原始文档中的句子开头,您可以将文档分成句子数组,并使用 amatch 比较子字符串的前 n 个字符,记录分数(n 是原始文档中的句子长度)。这将为您提供一个可能的起点。
    【解决方案4】:

    您应该查看此处详细介绍的 StrikeAMatch 实现: A better similarity ranking algorithm for variable length strings

    这不是依赖于某种字符串距离(即两个字符串之间的变化次数),而是着眼于字符对模式。每个字符串中出现的字符对越多,匹配就越好。它非常适合我们的应用程序,我们在纯文本文件中搜索输入错误/可变长度的标题。

    还有一个 gem 结合了 StrikeAMatch(Dice's coefficient 在字符级二元组上的实现)和 Levenshtein 距离来查找匹配项:https://github.com/seamusabshere/fuzzy_match

    【讨论】:

      【解决方案5】:

      一个简单的就是fuzzy_match

      require 'fuzzy_match'
      FuzzyMatch.new(['seamus', 'andy', 'ben']).find('Shamus') #=> seamus
      

      一个更详细的(虽然你不会从这个例子中说出来)是levenshein,它计算差异的数量。

      require 'levenshtein' 
      Levenshtein.distance('test', 'test')    # => 0
      Levenshtein.distance('test', 'tent')    # => 1
      

      【讨论】:

        猜你喜欢
        • 2018-03-23
        • 1970-01-01
        • 2012-02-14
        • 2014-11-02
        • 1970-01-01
        • 1970-01-01
        • 2011-05-21
        • 2018-05-31
        • 2021-04-19
        相关资源
        最近更新 更多