【问题标题】:Searching for a string in a large text file - profiling various methods in python在大型文本文件中搜索字符串 - 分析 python 中的各种方法
【发布时间】:2011-09-07 07:53:47
【问题描述】:

这个问题已经被问过很多次了。在花了一些时间阅读答案后,我做了一些快速分析以尝试前面提到的各种方法......

  • 我有一个 600 MB 文件,其中包含 600 万 行字符串(来自 DMOZ 项目的类别路径)。
  • 每一行的条目都是唯一的。
  • 我想加载文件一次继续搜索数据中的匹配项

我在下面尝试的三种方法列出了加载文件所用的时间、否定匹配的搜索时间和任务管理器中的内存使用情况


1) set :
    (i)  data   = set(f.read().splitlines())
    (ii) result = search_str in data   

加载时间~10s,搜索时间~0.0s,内存使用~1.2GB


2) list :
    (i)  data   = f.read().splitlines()
    (ii) result = search_str in data

加载时间 ~ 6s,搜索时间 ~ 0.36s,内存使用 ~ 1.2GB


3) mmap :
    (i)  data   = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
    (ii) result = data.find(search_str)

加载时间 ~ 0s,搜索时间 ~ 5.4s,内存使用 ~ NA


4) Hash lookup (using code from @alienhard below):   

加载时间 ~ 65s,搜索时间 ~ 0.0s,内存使用 ~ 250MB


5) File search (using code from @EOL below):   
   with open('input.txt') as f:
       print search_str in f #search_str ends with the ('\n' or '\r\n') as in the file

加载时间 ~ 0s,搜索时间 ~ 3.2s,内存使用 ~ NA


6) sqlite (with primary index on url): 

加载时间 ~ 0s,搜索时间 ~ 0.0s,内存使用 ~ NA


对于我的用例,只要我有足够的可用内存,似乎使用 set 是最好的选择。我希望在这些问题上得到一些 cmets:

  1. 更好的选择,例如sqlite ?
  2. 使用 mmap 改进搜索时间的方法。我有一个 64 位的设置。 [编辑] 例如布隆过滤器
  3. 随着文件大小增长到几 GB,有什么方法可以让我继续使用“设置”,例如分批拆分..

[编辑 1]我需要经常搜索,添加/删除值,并且不能单独使用哈希表,因为我需要稍后检索修改后的值。

欢迎任何 cmets/建议!

[编辑 2] 使用答案中建议的方法的结果进行更新 [编辑 3] 使用 sqlite 结果更新

解决方案:基于所有的分析和反馈,我想我会选择 sqlite。第二种选择是方法 4。 sqlite 的一个缺点是数据库大小是带有 url 的原始 csv 文件的两倍多。这是由于 url 上的主索引

【问题讨论】:

  • 您需要在文件中查找多个字符串,还是只查找一个字符串、一次或其他?
  • @senderle No. @EOL : 我需要反复搜索字符串,并添加新的.. 我会更新原帖
  • 为什么选项1和2的内存使用一样?我尝试了一个大约 110k 行的 2.7mb 数据文件。列表的成本与数据文件的大小大致相同,而设置的对象的成本约为 4.1mb

标签: python performance search profiling large-files


【解决方案1】:

如果您需要启动许多顺序搜索,变体 1 非常适合。由于set 内部是一个哈希表,所以它比较擅长搜索。但是,构建需要时间,并且只有在您的数据适合 RAM 时才能正常工作。

变体 3 适用于非常大的文件,因为您有足够的地址空间来映射它们并且操作系统缓存了足够的数据。您进行全面扫描;一旦您的数据停止以适应 RAM,它就会变得相当缓慢。

如果您需要连续进行多次搜索并且无法将数据放入 RAM,那么 SQLite 绝对是一个好主意。将您的字符串加载到表中,构建索引,SQLite 会为您构建一个漂亮的 b-tree。即使数据不适合,树也可以放入 RAM(这有点像 @alienhard 提出的建议),即使不适合,所需的 I/O 量也会大大降低。当然,您需要创建一个基于磁盘的 SQLite 数据库。我怀疑基于内存的 SQLite 会显着击败 Variant 1。

【讨论】:

  • 我担心文件可能会增长到超出 RAM 大小并且 mmap 不够快。我得看看sqlite。感谢您的洞察力。只要查找小于 1/10 秒,并且可以管理 2-5GB 的文件,我会很高兴
【解决方案2】:

使用外部化字符串进行自定义哈希表搜索

要获得更快的访问时间更低的内存消耗,您可以执行以下操作:

  • 为每一行计算一个字符串哈希并将其添加到哈希表中,例如index[hash] = position存储该字符串)。如果发生冲突,将该键的所有文件位置存储在列表中。
  • 查找字符串,计算其哈希值并在表中查找。如果找到密钥,请从文件中读取位于position 的字符串,以验证您确实有匹配项。如果有多个位置,请检查每个位置,直到找到匹配项或没有匹配项。

编辑1:按位置替换line_number(正如评论者指出的那样,显然需要实际位置而不是行号)

编辑 2:为具有自定义哈希表的实现提供代码,这表明这种方法比提到的其他方法更节省内存:

from collections import namedtuple 
Node = namedtuple('Node', ['pos', 'next'])

def build_table(f, size):
    table = [ None ] * size
    while True:
        pos = f.tell()
        line = f.readline()
        if not line: break
        i = hash(line) % size
        if table[i] is None:
            table[i] = pos
        else:
            table[i] = Node(pos, table[i])
    return table

def search(string, table, f):
    i = hash(string) % len(table)
    entry = table[i]
    while entry is not None:
        pos = entry.pos if isinstance(entry, Node) else entry
        f.seek(pos)
        if f.readline() == string:
            return True
        entry = entry.next if isinstance(entry, Node) else None
    return False

SIZE = 2**24
with open('data.txt', 'r') as f:
    table = build_table(f, SIZE)
    print search('Some test string\n', table, f)

行的散列仅用于索引表(如果我们使用普通字典,散列也将存储为键)。该行的文件位置存储在给定的索引处。冲突通过链式解决,即我们创建一个链表。但是,第一个条目永远不会包含在节点中(这种优化使代码更加复杂,但节省了相当多的空间)。

对于一个有 600 万行的文件,我选择了 2^24 的哈希表大小。使用我的测试数据,我得到了 933132 次碰撞。 (一半大小的哈希表在内存消耗方面相当,但导致更多的冲突。由于更多的冲突意味着更多的文件访问以进行搜索,我宁愿使用大表。)

Hash table: 128MB (sys.getsizeof([None]*(2**24)))
Nodes:       64MB (sys.getsizeof(Node(None, None)) * 933132)
Pos ints:   138MB (6000000 * 24)
-----------------
TOTAL:      330MB (real memory usage of python process was ~350MB)

【讨论】:

  • 存储行号不会有任何帮助。您必须存储文件位置。
  • @alienhard 好主意,值得一试。任何已经这样做的轻量级库?
  • 我也想过这个,但我检查了它,至少在我的机器上,一个 6000000 项的字典,每个项有两个整数(= 大约 120 + 24 + 24 个字节每个项)仍然需要几乎一个千兆字节。事实上,由于 set 占用的内存是相同大小的 dict 的 2/3,并且由于您只需在 set 中的每个项目存储一个字符串,因此 set 解决方案实际上可能占用更少的内存,具体取决于平均字符串长度(每个项目大约 80 + 40 + len(s) 个字节)。
  • @buffer 我编辑了我的答案并添加了一个完整的实现。我很想知道这对您的数据集有何影响?
  • @senderle 你说得对,使用字典会占用太多内存。但是使用自定义实现(参见代码)我们可以做得更好,因为我们不需要存储哈希键,并且在最好的情况下只存储位置 int 在表中。实际内存消耗取决于冲突次数,但根据我的测试数据,我得到了 330MB,比其他解决方案少 3.5 倍。
【解决方案3】:

你也可以试试

with open('input.txt') as f:
    # search_str is matched against each line in turn; returns on the first match:
    print search_str in f

search_str 以正确的换行符结尾('\n''\r\n')。这应该使用很少的内存,因为文件是逐步读取的。它也应该很快,因为只读取文件的一部分。

【讨论】:

  • 会比 mmap 快吗?
  • @buffer:是的,它比mmap 快。使用mmap 查找不在文件中的字符串比使用上述解决方案慢 50% 以上(在我的机器上,mmap 为 4 秒,in 为 2.4 秒)。 in 解决方案的内存占用也可以忽略不计。
  • 谢谢,我已经更新了结果。我猜这个方法只适用于全行搜索
  • @buffer:是的,它仅用于全行搜索(如您原始帖子中的方法(1)和(2)和(4))。
【解决方案4】:

我猜想许多路径在 DMOZ 上都是一样的。 您应该使用 trie data structure 并将各个字符存储在节点上。

在保存大型字典或树状数据时,尝试具有 O(m) 查找时间(其中 m 是密钥长度)也可以节省大量空间。

您还可以将路径部分存储在节点上以减少节点数——这称为 Patricia Trie。但这会使查找变慢平均字符串长度比较时间。有关实现的更多信息,请参阅 SO question Trie (Prefix Tree) in Python

Python 包索引上有几个 trie 实现,但它们不是很好。我用 Ruby 和 Common Lisp 写过一个,它特别适合这个任务——如果你问得好,我可以将它作为开源发布...... :-)

【讨论】:

  • 好的,但是使用 trie 仍然值得考虑,如果您可以对数据进行分区以便许多项目(例如行、子句等)以相同的开头。
  • 同意。在阅读了维基百科的文章后,我意识到我对可能超过我现在需要的规模的 10 倍的东西有一些模糊的相似之处。寻找快速解决方案。
  • 如需快速解决方案,您可以尝试Judy Arrays。有一个名为 PyJudy 的 Python C 库
【解决方案5】:

文本索引解决方案怎么样?

我会在 Java 世界中使用 Lucene,但有一个名为 Whoosh 的 Python 引擎

https://bitbucket.org/mchaput/whoosh/wiki/Home

【讨论】:

  • 我会看看.. 但如果它是在 Lucene 的线路上,Sphinx 可能是一个更好的选择,正如下面@Creotiv 所建议的那样。
【解决方案6】:

如果不建立索引文件,您的搜索将会很慢,这不是那么简单的任务。所以最好使用已经开发的软件。最好的方法是使用Sphinx Search Engine

【讨论】:

  • Sphinx 是一款很棒的软件,但对我来说似乎有点过头了。我一直在寻找一种轻量级的解决方案。
  • 我认为没有轻量级的解决方案。如果您愿意,您可以尝试自己制作某种索引,以加快搜索速度,但我所说的并不是那么简单,因此需要时间才能使某些东西正常工作。
  • 但是有一个时刻,你必须用C写这个,因为基于python的算法不会给出好的性能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-21
  • 1970-01-01
  • 1970-01-01
  • 2013-04-27
  • 2016-10-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多