查找输入字符串和一组固定字符串之间的匹配项

Posted

技术标签:

【中文标题】查找输入字符串和一组固定字符串之间的匹配项【英文标题】:Finding matches between an input string and a fixed set of strings 【发布时间】:2021-11-23 18:07:58 【问题描述】:

我需要将一个输入字符串与多个我称为固定字符串的字符串进行比较,您可以假设后者在执行期间不会改变。比较忽略带有特殊字符的字母,只考虑从 A 到 Z 的字母,但不区分大小写。我需要尝试将我的输入字符串与每个固定字符串的开头匹配,即使不是所有字符都匹配,与approximate string matching 不同。 换句话说,想要识别输入字符串中的每个单词和固定字符串中的每个单词之间的重叠。我对完整的比赛特别感兴趣,例如(love) 和 (love) 在下面的第二个示例中。

我实际上并不想显示匹配项,而是根据它们进行一些计算:每当有完全匹配时,我想增加一个具有特定值的计数器,并且只要有部分匹配,我想增加另一个值。计数是我真正想要的,但我需要先匹配字符串。

下面是如何匹配的说明:

如果我的固定字符串是“apple”、“bullcrap”、“tomato”、“apricot”,我应该能够搜索“Applecrab”并获得以下匹配项:

(Apple)crab App(l)ecrab Applecrab   (Ap)plecrab 
    
(apple)     bul(l)crap  tomato      (ap)ricot

我也应该能够输入多个单词,例如:

固定字符串:“i love books”、“book fair”;输入字符串:“I love”;匹配:

(I) I    I      |  love  (love)  l(o)ve
(i) love books  |  I     (love)  b(o)oks
   
I     I     |  l(o)ve love
book  fair  |  b(o)ok fair

比较两个字符串的简单、简单的算法如下所示:

inputString = "love".lower()
fixedString = "love".lower()
lenComp = min(len(inputString), len(fixedString))
counter = 0
for i in range(lenComp):
    if inputString[i] == fixedString[i]:  # Partial match
        counter += 1  
if counter == len(inputString):  # Full match
    counter += 10
print(counter)  # 14

由于固定字符串的数量不是非常少,因此解决方案应该采用合适的数据结构。我在考虑树:prefix tree 似乎很合适,但它对字符串中间的松散匹配没有帮助,例如l(o)veb(o)oks,Aho–Corasick algorithm 也不会。我认为Hamming distance 也无济于事。我可以使用哪些数据结构/算法来实现这一目标?


我用 Python 标记它,因为我可能会使用它,但我现在不太关心实现。

【问题讨论】:

你如何从Applecrab bananabul(l)crap 抱歉,刚刚修好了。 您是否真的想将每个输入词与固定字符串中的每个词进行比较,或者您是否想识别每个输入词有一些重叠的固定字符串并只显示这些?如果固定字符串集足够大,需要一个聪明的数据结构,那么您肯定不想一直显示所有空匹配项,如您的问题所示? 你的意思是Levenshtein distance吗? @TMBailey 我想识别输入字符串中的每个单词和固定字符串中的每个单词之间的重叠。我对完整的比赛特别感兴趣,例如(love) 和 (love),但我实际上并不想显示这些匹配项,而是根据它们进行一些计算。我已经编辑了问题以进一步解释。 【参考方案1】:

只是一个需要探索的想法,也许修改后的前缀树可以完成这项工作,您需要添加每个字符串及其所有子字符串,并跟踪每个字符在树中其他位置的位置。

例如:ABD、BAD、RCDB可以添加到前缀树如下

假设您想查找 CAB 的匹配项,您需要一个带有字符串和起始位置的搜索函数

search(CAB,0)  -- this will try to find a first node C with position 0 but won't find one

然后您将删除 C 并搜索位置为 1 的 AB

search(AB,1)

这将找到 A 节点,该节点表示 A 存在于 BAD 节点的位置 1 中,您将遵循该链接。等等……

编辑:

您可以通过从根节点开始并将字母表中的所有字母添加到该节点来创建树。每个字母都有一个位置字典

node =  ...
   pos :  

最初,所有字母的位置字典都是空的,即它们不被用作任何单词的起始字符或中间字符

要添加单词 ABD,逻辑将类似于

Letter 0 - A
    update pos dict for A to be 0 : []
    the contents of the array for pos 0 are not used (i think)
Letter 1 - B
    add node B under A
    update pos for node B under root to 1 : ['A']
Letter 2 - D
    add node D under B
    update pos for node D under root to 2 : ['AB']
    note: it is set to AB so it helps you walk down the 
          tree quicker when doing a search

接下来我们添加 BAD

Letter 0 - B
    update pos for node B to be 0:[], 1:['A']
Letter 1 - A
    add node A under B
    update pos for Node A under root to be 0:[],1:['B']
letter 2 - D
    add node D under B of the chain BA
    update pos for node D under root to be 2 : ['AB','BA']

搜索 CAD 一词

检查***根目录下的节点C,它有空的pos dict 所以没有单词以 C 开头,所以我们可以跳过 C 对于 pos = 1,移动到根下的下一个字符检查节点 A,它有 pos = 0:[],1:['B'] 所以取键 1 的数组,这告诉你你会在根 B 节点下找到一个 A,从这里遍历树以找到最长的匹配,这将为您提供 A(BD) 移动到下一个字符 D,检查根节点 D 的位置 2,它有 2 : ['AB','BA'],所以你遍历树找到匹配的 AB(D) 和 BA( D)

编辑 2:

这是一个非常简单(而且效率低下)的 python 实现:

import string, pprint

pp = pprint.PrettyPrinter(indent=4)

root = x: for x in string.ascii_lowercase

def add_word(word):
    def _add_word(node, hist, word, pos):
        if word[0] not in node:
            node[word[0]] = 
        root[word[0]].setdefault("pos",).setdefault(pos,set()).add(hist+word[0])
        if len(word) == 1:
            node[word[0]]['is_word'] = True
        else:
            _add_word(node[word[0]], hist+word[0], word[1:], pos+1)

    _add_word(root, "", word.lower(), 0)


def search(word):
    found_words = set()
    def find(node,prefix,word,pos,result):
        # print("prefix:",prefix,", word:",word,", pos:", pos,", result:",result)
        if prefix:
            find(node[prefix[0]], prefix[1:], word, pos, result+prefix[0])
        else:
            if node.get('is_word',False): found_words.add(result)
            for x in string.ascii_lowercase:
                if x in node: find(node[x], "", "", pos, result+x)

    for i in range(len(word)):
        if i in root[word[i]].get("pos",):
            for prefix in root[word[i]]["pos"][i]:
                # print("calling find with: ", prefix, word, i)
                find(root,prefix,word[i:],i,"")

    print(f"result for word:", found_words)


add_word('apple')
add_word('bullcrap')
add_word('tomato')
add_word('apricot')
# pp.pprint(root)
search('applecrab')
search('dove')

上面运行的输出是:

result for applecrab: 'apple', 'bullcrap', 'apricot'
result for dove: 'tomato'

【讨论】:

这是有道理的,但我担心如何跟踪树周围每个字符的位置,以及它的所有子字符串到底是什么意思(在你的例如,CD 在哪里,RCDB 的子字符串?)。你能澄清你的答案吗? @AtilioA,我已经编辑了我的答案以进一步澄清,您需要注意的另一件事是不要将不需要的条目添加到数组中。例如,当添加单词 BADLY 时,您不需要将“BA”添加到 D 的位置数组中,因为它已经存在于 BAD 这样更好,谢谢。但是,“检查根节点 D 的位置 2,它有 2 : ['AB','BA'],所以你走树”。你到底为什么要从树下走,有什么关系?并通过哪个分支?仅前缀树的根就可以容纳 26 个节点(A-Z)。我还假设您只会在 pos 小于原始输入字符串的长度时遍历它。 您检查根节点 D 的 pos 2 因为 D 位于单词 CAD 的 pos 2 中(基于 0 的计数),这告诉您 D 位于从根节点 A 开始的树的第二个位置-> B 然后你会找到 D,这简化了搜索,因为根节点 A 最多有 26 个子节点,其中任何一个都可能有一个子节点 D,所以在创建原始前缀树时,我们将“AB”添加到位置 2 的 rood 节点 D,这样我们就可以通过遍历树直接到达那里,而无需进行搜索。你对BA做同样的事情,遍历节点B然后A然后你会发现另一个D 我添加了一个简单的python实现来展示如何实现

以上是关于查找输入字符串和一组固定字符串之间的匹配项的主要内容,如果未能解决你的问题,请参考以下文章

在另一个字符串向量中查找字符串向量的匹配项

在 Python 中查找输入字符串到元组列表的所有可能匹配项(以任何顺序/顺序)

正则表达式基础

Notepad++编辑器对文本行首、行尾加上固定字符;每行之间增加空行

字符串匹配(KMP)

正则表达式查找所有匹配项,除了那些被字符包围的匹配项