【问题标题】:Finding matched string in a set在集合中查找匹配的字符串
【发布时间】:2019-11-30 17:10:35
【问题描述】:

我正在尝试比较两个大文件:b.txt 包含超过 2000 万行,a.txt 包含 50.000 行。以传统方式比较它们需要太多时间。例如以下代码在 5 小时内没有完成:

b = [line.rstrip('\n') for line in open("b.txt")]
a = [line.rstrip('\n') for line in open("a.txt")]
for i in a:
    for j in b:
        if i in j:
            print(j)

我在 Stackoverflow 中看到了针对类似问题的以下建议:

with open('b.txt') as b:
    blines = set(b)
    print(blines[0])
with open('a.txt') as a:
    for line in a:
        if line in blines:
            print(line)

set() 函数运行速度非常快,代码在几秒钟内终止。但是,我需要在包含line 变量的blines 中找到确切的字符串。由于索引无法访问set(),因此我无法实现。有没有办法找到匹配的字符串,或者你有什么其他建议可以让这个比较比第一个代码更快。

【问题讨论】:

  • 您使用的是什么操作系统?你需要用python来实现这个吗?
  • 或者你可以使用二分搜索
  • 我使用的是 macOS 和 Windows。不,不一定是Python,我只知道python
  • 可以对大文件b.txt 进行预处理(排序、索引...)吗?与在大型未索引文件中搜索随机字符串相比,这可以大大提高您的搜索速度。
  • 是的,我们可以以任何我们想要的方式处理 b.txt 文件

标签: python substring string-matching


【解决方案1】:

来自your comment,你说你需要处理(为清楚起见,略作编辑):

line = 'abc'blines = {'abcd', 'fg'};如果 line in blines 返回 true 但我需要 'abcd' 字符串,或者它是找到它的索引

如果没有组合爆炸,set 无法正确完成您正在做的事情。您想处理任意子字符串,而不仅仅是行或单词,这意味着 blines 需要在其中包含每一行的每个子字符串才能使查找成功(因此,您还需要存储 @ 而不仅仅是 'abcd' 987654332@、'b''c''d''ab''bc''、'cd''abc''bcd',这只是一条短线,而不是 20M 长线) .

更好的解决方案是一种数据结构,它可以让您在给定字符串中找到所有目标词,该数据结构不会遭受组合爆炸,例如Aho-Corasick,为此a Python package, pyahocorasick, already exists to implement it efficiently

不用将b 的所有 2000 万行(谁知道每行有多少子字符串)都加载到内存中,只需从 a 中的 50,000 个字符串构建一个自动机,然后检查 b 的每一行针对那个自动机:

import ahocorasick
auto = ahocorasick.Automaton()    
with open("a.txt") as needles:
    for needle in needles:
        needle = needle.rstrip('\n')
        auto.add_word(needle, needle)
auto.make_automaton()

with open("b.txt") as haystacks:
    for lineno, line in enumerate(haystacks):
        for end_ind, found in auto.iter(line):
            print(f"Found {found!r} on line {lineno} at index {end_ind - len(found) + 1}: {line!r}")

这会在O(n) 时间(相对于a.txt 的大小)生成一个单独的Aho-Corasick 自动机,然后在O(n) 时间(相对于b.txt 的大小)扫描它;内存使用量将与a.txt 的大小大致成比例(根据之前的测试,对于 50,000 个 3-12 个字符长的随机针,自动机的内存使用量可能在 10-20 MB 范围内),并且不会受到影响所有子串的set 的组合爆炸。它会找到所有a 的元素,无论它们出现在b 中的什么地方(即使在单词的中间,如您需要在abcd 中查找abc 的示例),而不会产生额外的内存开销。

如果您需要知道a.txt 中的行号,而不仅仅是b.txt 中的行号,只需更改自动机构建过程以存储行号以及针本身(您可以将任何内容与每个添加了单词,所以tuplestr 一样好):

for lineno, needle in enumerate(needles):
    needle = needle.rstrip('\n')
    auto.add_word(needle, (lineno, needle))

然后在以后迭代:

for blineno, bline in enumerate(haystacks):
    for end_ind, (alineno, found) in auto.iter(line):

根据需要调整输出。

【讨论】:

  • 这似乎是一个正确的答案。 TIL 关于pyahocorasick 模块。谢谢,点赞!
  • @AndrejKesely:是的,这个问题类似于我多年前用a similar answer 回答的问题。我没有将其标记为重复的唯一原因是我需要解决 OP 的错误印象,即set(或dict,或任何简单的相关结构)是一种可行的方法。 pyahocorasick 是一个小众工具,但当它有用时,它真的很有用,性能随着要搜索的字数呈亚线性增长(因此搜索 10 倍的字符串会增加不到 2 倍的运行时间) 和内存扩展(大致)线性。
【解决方案2】:

如果您可以保证b.txt 中的行是唯一的,那么您可以使用查找表来查找任何给定行的位置。 (编辑: 行在使用 defaultdict 时实际上不必是唯一的。感谢 @ShadowRanger)

这意味着,您可以创建一个以行为键,行号为值的字典,然后使用line_in_a in b_dict 来检查文件 b 中的文件 a 中是否存在行。

这应该会给您类似的性能,因为 setdict 都进行哈希查找,但会以内存空间为代价。

看起来像这样:

from io import StringIO
from collections import defaultdict
import itertools

b = StringIO('''321
543
654
123
123
234''')

a = StringIO('''123
234
345''')



with b:
    lines = (line.rstrip('\n') for line in b)
    blookup = defaultdict(set)
    for i, line in enumerate(lines):
        blookup[line].add(i)

with a:    
    for line in a:
        line = line.rstrip('\n')
        if line in blookup:
            line_nos = blookup[line]
            print(line, line_nos)

输出:

123 {3, 4}
234 {5}

注意:请记住,这种方法只考虑精确匹配,它不搜索子字符串。

【讨论】:

  • 如果dict 将每一行映射到行号的list,则行不必是唯一的。 collections.defaultdict(list) 使构建变得容易。
  • 好主意,让我看看。编辑:完成
  • 谢谢你的工作,但我也可以打印 'blookup[line]' 保存的字符串吗?
  • 这是line 变量??如果您将 a 中的一行与 b 中的一行匹配,则您已经知道它的定义。
  • 嗯。再想一想,@ShadowRanger 的回答触及了一个重要点。您似乎希望在两组行之间进行子字符串查找。这个答案会给你完全匹配。
【解决方案3】:

注意:请记住,这种方法只考虑完全匹配,它不搜索子字符串 - 要搜索子字符串,请查看 @ShadowRanger 答案。

此脚本首先从大文件 (word:{set of line numbers where the word exists in large file}) 构建索引:

data_large_file = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.'''

data_search = '''
ad minim veniam
non proident
non NOTFOUND proident'''

data = [d.rstrip() for d in data_large_file.splitlines() if d.rstrip()]
data_s = [d.rstrip() for d in data_search.splitlines() if d.rstrip()]

#build index
from collections import defaultdict
index = defaultdict(set)
for row_number, row in enumerate(data):
    for word in row.split():
        index[word].add(row_number)

#search:
for line in data_s:
    first_word = line.split(maxsplit=1)[0]
    for line_num in index[first_word]:
        if line in data[line_num]:
            print(data[line_num])

打印:

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia

【讨论】:

  • 您应该将defaultdict(list) 替换为defaultdict(set)
  • @abdusco:为什么?行号自然是唯一的(因此set 对重复数据删除没有帮助),并且没有执行特定行号的搜索(因此没有包含测试,setO(1) 成员资格测试胜过listO(n)),所以使用set 只是意味着使用更多的内存,并失去排序。
  • @ShadowRanger 但是在每一行中我们可以有重复的单词,这意味着我们以该行的重复行号结尾。
  • @AndrejKesely:OP 的代码似乎是面向行的,而不是面向单词的,但可以肯定的是,如果他们歪曲了问题,set 可能会有所帮助。
  • 感谢您的回答,但它没有给我正确的结果。它打印我正在搜索的字符串,而不是匹配的字符串。例如,当我将最后一行更改为 'print(line,data[line_num])' 时,它会打印两个相同的字符串
猜你喜欢
  • 2019-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-30
相关资源
最近更新 更多