【问题标题】:Find best substring match查找最佳子字符串匹配
【发布时间】:2016-07-01 00:16:52
【问题描述】:

我正在寻找使用现有库(difflibfuzzywuzzypython-levenshtein)的库或方法,以在文本 (corpus) 中找到字符串 (query) 的最接近匹配项

我开发了一种基于difflib 的方法,我将corpus 拆分为大小为n 的ngram(长度为query)。

import difflib
from nltk.util import ngrams

def get_best_match(query, corpus):
    ngs = ngrams( list(corpus), len(query) )
    ngrams_text = [''.join(x) for x in ngs]
    return difflib.get_close_matches(query, ngrams_text, n=1, cutoff=0)

当查询和匹配字符串之间的差异只是字符替换时,它可以按我的意愿工作。

query = "ipsum dolor"
corpus = "lorem 1psum d0l0r sit amet"

match = get_best_match(query, corpus)
# match = "1psum d0l0r"

但是当区别是字符删除时,就不是了。

query = "ipsum dolor"
corpus = "lorem 1psum dlr sit amet"

match = get_best_match(query, corpus)
# match = "psum dlr si"
# expected_match = "1psum dlr"

有没有办法获得更灵活的结果大小(expected_match)?

编辑 1:

  • 此脚本的实际用途是将查询(字符串)与 混乱的 ocr 输出。
  • 正如我在问题中所说,ocr 可以混淆字符,甚至错过它们。
  • 如果可能,还要考虑单词之间缺少空格的情况。
  • 最佳匹配是指不包含查询中其他单词的字符。

编辑 2:

我现在使用的解决方案是使用(n-k)-grams for k = {1,2,3} 扩展ngram 以防止3 次删除。它比第一个版本好得多,但在速度方面效率不高,因为我们要检查的 ngram 数量超过 3 倍。这也是一个不可推广的解决方案。

【问题讨论】:

  • ngrams 将在您的示例中遇到双重删除“dlr”,levenshtein 可能会提供更好的结果?
  • 看来 Levenshtein automata 可能是您正在寻找的数据结构,但不幸的是,我不知道任何现有的 Python 实现。 更新:这里是 another blogpost,其中包含一些 Python 代码,不是生产就绪的库,但仍然可能是一个好的开始。
  • 如果我正确理解了自动机的工作方式,它会将语料库视为单独单词的列表吗?在这种情况下,它不会解决 OP 的问题,因为他正在将查询与 OCR 输出(我想是从图像识别中解析的文本)匹配,不能期望它正确分隔单词。不过真的很酷的算法,它在我的书中!

标签: python match distance n-gram


【解决方案1】:

此函数查找最佳匹配子字符串可变长度

该实现将语料库视为一个长字符串,因此避免了您对空格和未分隔单词的担忧。

代码总结: 1.step 为步长扫描语料库中的匹配值,找到最高匹配值的大致位置pos2.通过调整子串的左右位置,找到pos附近匹配值最高的子串。

from difflib import SequenceMatcher

def get_best_match(query, corpus, step=4, flex=3, case_sensitive=False, verbose=False):
    """Return best matching substring of corpus.

    Parameters
    ----------
    query : str
    corpus : str
    step : int
        Step size of first match-value scan through corpus. Can be thought of
        as a sort of "scan resolution". Should not exceed length of query.
    flex : int
        Max. left/right substring position adjustment value. Should not
        exceed length of query / 2.

    Outputs
    -------
    output0 : str
        Best matching substring.
    output1 : float
        Match ratio of best matching substring. 1 is perfect match.
    """

    def _match(a, b):
        """Compact alias for SequenceMatcher."""
        return SequenceMatcher(None, a, b).ratio()

    def scan_corpus(step):
        """Return list of match values from corpus-wide scan."""
        match_values = []

        m = 0
        while m + qlen - step <= len(corpus):
            match_values.append(_match(query, corpus[m : m-1+qlen]))
            if verbose:
                print(query, "-", corpus[m: m + qlen], _match(query, corpus[m: m + qlen]))
            m += step

        return match_values

    def index_max(v):
        """Return index of max value."""
        return max(range(len(v)), key=v.__getitem__)

    def adjust_left_right_positions():
        """Return left/right positions for best string match."""
        # bp_* is synonym for 'Best Position Left/Right' and are adjusted 
        # to optimize bmv_*
        p_l, bp_l = [pos] * 2
        p_r, bp_r = [pos + qlen] * 2

        # bmv_* are declared here in case they are untouched in optimization
        bmv_l = match_values[p_l // step]
        bmv_r = match_values[p_l // step]

        for f in range(flex):
            ll = _match(query, corpus[p_l - f: p_r])
            if ll > bmv_l:
                bmv_l = ll
                bp_l = p_l - f

            lr = _match(query, corpus[p_l + f: p_r])
            if lr > bmv_l:
                bmv_l = lr
                bp_l = p_l + f

            rl = _match(query, corpus[p_l: p_r - f])
            if rl > bmv_r:
                bmv_r = rl
                bp_r = p_r - f

            rr = _match(query, corpus[p_l: p_r + f])
            if rr > bmv_r:
                bmv_r = rr
                bp_r = p_r + f

            if verbose:
                print("\n" + str(f))
                print("ll: -- value: %f -- snippet: %s" % (ll, corpus[p_l - f: p_r]))
                print("lr: -- value: %f -- snippet: %s" % (lr, corpus[p_l + f: p_r]))
                print("rl: -- value: %f -- snippet: %s" % (rl, corpus[p_l: p_r - f]))
                print("rr: -- value: %f -- snippet: %s" % (rl, corpus[p_l: p_r + f]))

        return bp_l, bp_r, _match(query, corpus[bp_l : bp_r])

    if not case_sensitive:
        query = query.lower()
        corpus = corpus.lower()

    qlen = len(query)

    if flex >= qlen/2:
        print("Warning: flex exceeds length of query / 2. Setting to default.")
        flex = 3

    match_values = scan_corpus(step)
    pos = index_max(match_values) * step

    pos_left, pos_right, match_value = adjust_left_right_positions()

    return corpus[pos_left: pos_right].strip(), match_value

例子:

query = "ipsum dolor"
corpus = "lorem i psum d0l0r sit amet"
match = get_best_match(query, corpus, step=2, flex=4)
print(match)
('i psum d0l0r', 0.782608695652174)

一些好的启发式建议是始终保留step &lt; len(query) * 3/4flex &lt; len(query) / 3。我还添加了区分大小写,以防万一这很重要。当您开始使用 step 和 flex 值时,它工作得很好。小步长值可提供更好的结果,但计算时间更长。 flex 控制生成的子字符串长度的灵活性。

重要提示:这只会找到第一个最佳匹配,因此如果有多个同样好的匹配,则只会返回第一个。要允许多个匹配项,请更改 index_max() 以返回输入列表中 n 最高值的索引列表,并遍历 adjust_left_right_positions() 以获取该列表中的值。

【讨论】:

  • 感谢您的回答。这是一个做得很好的函数,因为它有参数。我已经进行了测试以将其与我的方法进行比较。您的函数通常在匹配单词的末尾保留一个空格,但这是我可以轻松解决的问题。我认为我可以结合这两种方法来改善结果。 +1 +赏金
  • 谢谢,很高兴能帮上忙。在删除它不会改变编辑距离的情况下,可以在末尾保留空格,因为替换和插入的成本相同。我编辑了答案以在输出上调用 .strip() 函数来纠正这个问题。
  • @UlfAslak 的回答是否假定corpus 必须比query 长?
  • @RhoPhi 不,它适用于比语料库更长的查询。不知道你为什么要这样做,但是如果你想在你的代码中使用它并且你担心它会抛出一个错误,它不会。它只会返回语料库。
  • 是否有 Python 3 版本可用?
【解决方案2】:

解决方案的主要路径使用某种有限状态自动机 (FSA)。如果您想详细了解该主题,请查看此dissertation(PDF 链接)。基于错误的模型(包括 Levenshtein 自动机和传感器,Sergei 提到了前者)是解决此问题的有效方法。然而,随机模型,包括与 FSA 集成的各种机器学习方法,目前非常流行。

由于我们正在查看编辑距离(实际上是拼写错误的单词),因此 Levenshtein 方法很好且相对简单。 This paper(以及论文;还有 PDF)给出了基本思想的大致轮廓,并且还明确提到了 OCR 任务的应用。不过,我将回顾下面的一些关键点。

基本思想是您要构建一个 FSA 来计算有效字符串以及直到某个错误距离 (k) 的所有字符串。在一般情况下,这个 k 可能是无限的或文本的大小,但这与 OCR 几乎无关(如果您的 OCR 甚至可能返回 bl*h 其中 * 是整个其余部分文本,我建议找一个更好的 OCR 系统)。因此,我们可以从搜索字符串blah 的有效答案集中限制像bl*h 这样的正则表达式。对于您的上下文,一个通用、简单且直观的 k 可能是字符串 (w) 的长度减去 2。这允许 b--h 成为 @987654328 的有效字符串@。它也允许bla--h,但这没关系。另外,请记住,错误可以是您指定的任何字符,包括空格(因此“多字”输入是可以解决的)。

下一个基本任务是设置一个简单的加权传感器。任何 OpenFST Python 端口都可以执行此操作 (here's one)。逻辑很简单:插入和删除会增加权重,而相等会增加输入字符串中的索引。您也可以像 Sergei 的评论链接中的人那样手动编写代码。

获得权重和权重的相关索引后,您只需排序并返回。计算复杂度应该是 O(n(w+k)),因为我们将在最坏的情况下为文本中的每个字符 (n) 预测 w+k 个字符。

从这里,你可以做各种各样的事情。您可以将换能器转换为 DFA。您可以通过将文本分解为 w+k-gram 来并行化系统,然后将它们发送到不同的进程。您可以开发一个语言模型或混淆矩阵来定义输入集中每个字母存在哪些常见错误(从而限制有效转换的空间和相关 FSA 的复杂性)。文献数量庞大且仍在增长,因此可能有与解决方案一样多的修改(如果不是更多的话)。

希望在不提供任何代码的情况下回答您的一些问题。

【讨论】:

    【解决方案3】:

    我会尝试从查询字符串构建一个正则表达式模板。然后可以使用该模板在语料库中搜索可能与查询匹配的子字符串。然后使用 difflib 或 blurwuzzy 检查子字符串是否与查询匹配。

    例如,一个可能的模板将匹配查询的前两个字母中的至少一个,查询的最后两个字母中的至少一个,并且在两者之间具有大约正确数量的字母:

    import re
    
    query = "ipsum dolor"
    corpus = ["lorem 1psum d0l0r sit amet",
              "lorem 1psum dlr sit amet",
              "lorem ixxxxxxxr sit amet"]
    
    first_letter, second_letter = query[:2]
    minimum_gap, maximum_gap = len(query) - 6, len(query) - 3
    penultimate_letter, ultimate_letter = query[-2:]
    
    fmt = '(?:{}.|.{}).{{{},{}}}(?:{}.|.{})'.format
    pattern = fmt(first_letter, second_letter,
                  minimum_gap, maximum_gap,
                  penultimate_letter, ultimate_letter)
    
    #print(pattern) # for debugging pattern
    
    m = difflib.SequenceMatcher(None, "", query, False)
    
    for c in corpus:
        for match in re.finditer(pattern1, c, re.IGNORECASE):
            substring = match.group()
            m.set_seq1(substring)
            ops = m.get_opcodes()
    
            # EDIT fixed calculation of the number of edits
            #num_edits = sum(1 for t,_,_,_,_ in ops if t != 'equal')
            num_edits = sum(max(i2-i1, j2-j1) for op,i1,i2,j1,j2 in ops if op != 'equal' )
            print(num_edits, substring)
    

    输出:

    3 1psum d0l0r
    3 1psum dlr
    9 ixxxxxxxr
    

    另外一个思路是在构建正则表达式的时候利用ocr的特性。例如,如果 ocr 总是正确地获取某些字母,那么当查询中有任何这些字母时,请在正则表达式中使用其中的一些。或者,如果 ocr 混淆了 '1'、'!'、'l' 和 'i',但从不替换其他内容,则如果查询中包含其中一个字母,请在正则表达式中使用 [1!il]

    【讨论】:

    • 我不认为正则表达式模板是解决这类问题的好方法。您的解决方案输出 'ixxxxxxxr' 作为 num_edits = 1 的良好解决方案。
    • @Ghilas 感谢您发现错误。正则表达式仅用于查找查询可能匹配的位置。然后使用 difflib.SequenceMatcher 进行比较。我错误地计算了编辑距离。固定。
    猜你喜欢
    • 1970-01-01
    • 2015-10-03
    • 2014-06-08
    • 1970-01-01
    • 2020-04-12
    • 2014-08-26
    • 2017-07-13
    • 2014-08-02
    • 2016-08-17
    相关资源
    最近更新 更多