我正在寻找使用现有库(difflib
,fuzzywuzzy
,python-levenshtein
)的库或方法来查找字符串(query
)的最接近匹配项在文本(corpus
)
我开发了一种基于difflib
的方法,我将corpus
拆分为大小为n
的ngrams(长度为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
)?
我现在使用的解决方案是使用(n-k)-grams for k = {1,2,3}
扩展ngrams以防止3次删除。它比第一个版本要好得多,但在速度方面效率不高,因为我们要检查的ngrams数量超过3倍。它也是一种不可推广的解决方案。
答案 0 :(得分:6)
此函数查找可变长度的最佳匹配子字符串。
该实现将语料库视为一个长字符串,因此避免了您对空格和未分隔单词的关注。
代码摘要:
1。以大小为step
的步长扫描语料库中的匹配值,以查找最高匹配值pos
的大致位置。
2. 通过调整子字符串的左/右位置,找到匹配值最高的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(xrange(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 < len(query) * 3/4
和flex < len(query) / 3
。我还添加了区分大小写,以防万一。当您开始使用step和flex值时,它可以很好地工作。小步长值可以提供更好的结果,但计算时间更长。 flex控制允许生成的子字符串长度的灵活性。
需要注意的重要事项:这只会找到第一个最佳匹配,因此如果有多个同样好的匹配,则只会返回第一个匹配。要允许多个匹配,请更改index_max()
以返回输入列表的n
最高值的索引列表,并对该列表中的值循环adjust_left_right_positions()
。
答案 1 :(得分:3)
解决方案的主要途径使用某种类型的有限状态自动机(FSA)。如果您需要有关该主题的详细摘要,请查看此dissertation(PDF链接)。基于误差的模型(包括Levenshtein自动机和传感器,谢尔盖提到的前者)是有效的方法。然而,随机模型,包括与FSA集成的各种机器学习方法,目前非常受欢迎。
由于我们正在研究编辑距离(有效拼写错误的单词),因此Levenshtein方法很好且相对简单。 This paper(以及论文;也是PDF)给出了基本概念的正确概述,并且还明确提到了对OCR任务的应用。但是,我将回顾下面的一些要点。
基本思想是你要构建一个FSA来计算有效字符串以及所有字符串,直到某个错误距离( k )。在一般情况下,此 k 可能是无限的或文本的大小,但这与OCR无关(如果您的OCR甚至可能返回bl*h
,其中*是其余的整篇文章,我建议找一个更好的OCR系统)。因此,我们可以从搜索字符串bl*h
的有效答案集中限制blah
之类的正则表达式。您的上下文的一般,简单和直观的 k 可能是字符串的长度( w )减去2.这允许b--h
成为有效的字符串blah
。它也允许bla--h
,但这没关系。另外,请记住,错误可以是您指定的任何字符,包括空格(因此“多字”输入是可解决的。)
下一个基本任务是设置一个简单的加权传感器。任何OpenFST Python端口都可以执行此操作(here's one)。逻辑很简单:插入和删除会增加权重,而相等性会增加输入字符串中的索引。您也可以像Sergei评论链接中的人那样手工编写代码。
一旦你有权重和权重的相关索引,你只需排序并返回。计算复杂度应该是O(n(w + k)),因为我们将在最坏的情况下向文本中的每个字符( n )展望w + k个字符。
从这里,你可以做各种各样的事情。您可以将传感器转换为DFA。您可以通过将文本分成w + k-gram来并行化系统,这些文本被发送到不同的进程。您可以开发一个语言模型或混淆矩阵,用于定义输入集中每个字母存在的常见错误(从而限制有效转换的空间和相关FSA的复杂性)。文献很多,而且还在不断增长,所以可能有很多修改,因为有解决方案(如果不是更多)。
希望在不提供任何代码的情况下回答您的一些问题。
答案 2 :(得分:1)
我会尝试从查询字符串构建正则表达式模板。然后可以使用该模板在语料库中搜索可能与查询匹配的子串。然后使用difflib或fuzzywuzzy来检查子字符串是否与查询匹配。
例如,可能的模板将匹配查询的前两个字母中的至少一个,查询的最后两个字母中的至少一个,并且在它们之间具有大致正确数量的字母:
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]
。