在大型文本文件中搜索字符串的便宜方法

42

我需要在一个非常大的文本文件中搜索特定字符串。这是一个包含约5000行文本的构建日志。有什么最好的方法可以做到这一点?使用正则表达式不应该会有任何问题,对吧?我将继续阅读每个块的行,并使用简单的查找。


17
5000行?这可不算是“相当大”的规模 :-) - eumiro
1
代码块?听起来你的优化成本比节省的更高(仅针对5000行文件...)。你不会在循环中连接字符串,是吧? :) - JoshD
“pretty large”是什么意思?@eumiro - OuuGiii
1
@OuuGiii 是一个比你的内存更大的文件,因此你无法一次性读取它。 - eumiro
9个回答

57
如果文件很大,那么按顺序访问行,不要将整个文件读入内存:
with open('largeFile', 'r') as inF:
    for line in inF:
        if 'myString' in line:
            # do_something

15
注意:如果您的字符串跨越多行,则此方法无效。 - bfontaine
@bfontaine 如果有多行怎么办? - D09r
在我的情况下,它太慢了。 - keramat

17

您可以进行简单的查找:

f = open('file.txt', 'r')
lines = f.read()
answer = lines.find('string')

如果可以使用简单的查找来实现,那么速度会比正则表达式快得多。


1
我刚刚尝试了这段代码,但我正在打印答案以找出它是什么,为什么当字符串未被找到时,答案等于“-1”,但当它被找到时,答案可以是许多不同的数字? - Mark O'Sullivan
@MarkO'Sullivan find 命令返回第一个匹配项的索引。-1 表示没有匹配项;其他值是起始索引。 - Chen A.
3
这段代码效率低下。 f.read()会将整个文件加载到内存中,当处理非常大的文件时会很慢并且没有意义。最好改为逐行迭代(使用生成器或简单的for循环)。 - Chen A.
1
@Vinny 如果你要查找的字符串跨越多行,逐行迭代就不起作用了。这个答案在内存上可能不是很高效,但如果你的文件不是很大(5000行不算大文件:)),那么这可能是最好的答案。 - bfontaine
1
@bfontaine 关于“最佳”答案,我认为这不是针对所提出问题的最佳答案。如果这是针对大文件(正如标题所示,并且搜索引擎的原因将到达;我们不仅仅受限于OP的5000行规范,而应该将自己作为资源进行优化),那么laurasia下面的答案是理想的选择,既快速又内存高效。毕竟,进入交换空间会使您的程序减速到极慢的状态。 - Graham

14
以下函数适用于文本文件和二进制文件(返回的仅为字节计数中的位置),它的好处是即使字符串重叠了一行或缓冲区,它也能够找到这些字符串,并且不会在按行或按缓冲区搜索时被忽略。
def fnd(fname, s, start=0):
    with open(fname, 'rb') as f:
        fsize = os.path.getsize(fname)
        bsize = 4096
        buffer = None
        if start > 0:
            f.seek(start)
        overlap = len(s) - 1
        while True:
            if (f.tell() >= overlap and f.tell() < fsize):
                f.seek(f.tell() - overlap)
            buffer = f.read(bsize)
            if buffer:
                pos = buffer.find(s)
                if pos >= 0:
                    return f.tell() - (len(buffer) - pos)
            else:
                return -1

这个想法的原理是:

  • 在文件中查找起始位置
  • 从文件读取到缓冲区(搜索字符串必须比缓冲区大小小),但如果不在开头,则后退1个字节,以便在上次读取缓冲区结束时开始并继续下一次。
  • 返回位置或-1(未找到)。

我曾经使用类似于这种方法来在更大的ISO9660文件中查找文件的标识,速度相当快且不占用太多内存,您也可以使用更大的缓冲区来提高速度。


“s” 应该代表什么?哦,也许它是你想要查找的字符串?是的,我现在明白了。 - harperville
我创建了一份受此答案启发的回答 - Graham

8

这是一个文件文本搜索的多进程示例。TODO: 如何在找到文本后停止进程并可靠地报告行号?

import multiprocessing, os, time
NUMBER_OF_PROCESSES = multiprocessing.cpu_count()

def FindText( host, file_name, text):
    file_size = os.stat(file_name ).st_size 
    m1 = open(file_name, "r")

    #work out file size to divide up to farm out line counting
        
    chunk = (file_size / NUMBER_OF_PROCESSES ) + 1
    lines = 0
    line_found_at = -1

    seekStart = chunk * (host)
    seekEnd = chunk * (host+1)
    if seekEnd > file_size:
        seekEnd = file_size

    if host > 0:
        m1.seek( seekStart )
        m1.readline()
            
    line = m1.readline()
    
    while len(line) > 0:
        lines += 1
        if text in line:
            #found the line
            line_found_at = lines
            break
        if m1.tell() > seekEnd or len(line) == 0:
            break
        line = m1.readline()
    m1.close()
    return host,lines,line_found_at
        
# Function run by worker processes
def worker(input, output):
    for host,file_name,text in iter(input.get, 'STOP'):
        output.put(FindText( host,file_name,text ))

def main(file_name,text):
    t_start = time.time()
    # Create queues
    task_queue = multiprocessing.Queue()
    done_queue = multiprocessing.Queue()
    #submit file to open and text to find
    print 'Starting', NUMBER_OF_PROCESSES, 'searching workers'
    for h in range( NUMBER_OF_PROCESSES ):
        t = (h,file_name,text)
        task_queue.put(t)

    #Start worker processes
    for _i in range(NUMBER_OF_PROCESSES):
        multiprocessing.Process(target=worker, args=(task_queue, done_queue)).start()

    # Get and print results
    
    results = {}
    for _i in range(NUMBER_OF_PROCESSES):
        host,lines,line_found = done_queue.get()
        results[host] = (lines,line_found)

    # Tell child processes to stop
    for _i in range(NUMBER_OF_PROCESSES):
        task_queue.put('STOP')
#        print "Stopping Process #%s" % i
    
    total_lines = 0
    for h in range(NUMBER_OF_PROCESSES):
        if results[h][1] > -1:
            print text, 'Found at', total_lines + results[h][1], 'in', time.time() - t_start, 'seconds'
            break
        total_lines += results[h][0]
    
if __name__ == "__main__":
    main( file_name = 'testFile.txt', text = 'IPI1520' )

4
我很惊讶没有人提到将文件映射到内存中:mmap 使用这种方法,您可以像访问已加载到内存中的文件一样访问文件,并且操作系统会负责在可能的情况下映射它。此外,如果您从两个独立进程执行此操作并且它们共享映射文件,则它们将共享底层内存。
一旦映射完成,它将表现得像bytearray。您可以使用常规表达式、查找或任何其他常见方法。
请注意,此方法有点特定于操作系统。它不会自动可移植。

2
我喜欢Javier的解决方案。虽然我没有尝试过,但听起来很酷!
如果要阅读任意大的文本并想知道字符串是否存在,可以使用比正则表达式更快且适用于非常大文件的Flashtext。
编辑:
从开发人员页面:
>>> from flashtext import KeywordProcessor
>>> keyword_processor = KeywordProcessor()
>>> # keyword_processor.add_keyword(<unclean name>, <standardised name>)
>>> keyword_processor.add_keyword('Big Apple', 'New York')
>>> keyword_processor.add_keyword('Bay Area')
>>> keywords_found = keyword_processor.extract_keywords('I love Big Apple and Bay Area.')
>>> keywords_found
>>> # ['New York', 'Bay Area']

当提取偏移量时:

>>> from flashtext import KeywordProcessor
>>> keyword_processor = KeywordProcessor()
>>> keyword_processor.add_keyword('Big Apple', 'New York')
>>> keyword_processor.add_keyword('Bay Area')
>>> keywords_found = keyword_processor.extract_keywords('I love big Apple and Bay Area.', span_info=True)
>>> keywords_found
>>> # [('New York', 7, 16), ('Bay Area', 21, 29)]

限制:我想指出,这个解决方案并不是针对所提出问题的最佳解决方案。对于给定问题,来自eumiro的解决方案中的in(在相关评论中由@bfontaine给出警告)肯定是最好的答案。

flashtext是一个强大的解决方案,如果您想要找到给定文本中所有的字符串出现次数。这是in无法做到的(并且没有被设计用于这样做)。


尝试在你的回答中提供一个最简化的例子,这样更多的人可以从你的回答中获得帮助。 - Zeeshan Adil

2
如果无法确定字符串的位置(前半部分、后半部分等),则除了使用内置的“find”函数外,真的没有优化搜索的方法。您可以通过以4kb块的方式读取文件(通常是硬盘块的大小)来减少I/O时间和内存消耗。这不会使搜索更快,除非字符串在文件的第一部分中,但在所有情况下都会减少内存消耗,如果文件很大,这可能是个好主意。

取决于有多大。如果大约是1MB,我预计这种方式会比加载整个文件慢,因为每次读取所有256个块的延迟。如果可以的话,我更喜欢每次读取更大的块大小。也许需要进行测试... - JoshD
延迟可能确实更高,但并不一定重要的是读取物理块大小的倍数,而不是浪费读取数据。事实上,我不会称一个1MB的文本文件为“巨大”,我认为应该是几百兆字节左右。如果文件小于10或甚至50MB,我完全同意您的看法,没有必要分块读取。 - Bitgamma

2
这完全是受laurasia上面的答案启发,但它改进了结构。
它还增加了一些检查:
  • 当在空文件中搜索空字符串时,它将正确地返回0。 在laurasia的答案中,这是一个边缘情况,会返回-1
  • 它还预先检查目标字符串是否大于缓冲区大小,并在这种情况下引发错误。
实际上,为了提高效率,目标字符串应该比缓冲区小得多,并且如果目标字符串的大小非常接近缓冲区的大小,则有更有效的搜索方法。
def fnd(fname, goal, start=0, bsize=4096):
    if bsize < len(goal):
        raise ValueError("The buffer size must be larger than the string being searched for.")
    with open(fname, 'rb') as f:
        if start > 0:
            f.seek(start)
        overlap = len(goal) - 1
        while True:
            buffer = f.read(bsize)
            pos = buffer.find(goal)
            if pos >= 0:
                return f.tell() - len(buffer) + pos
            if not buffer:
                return -1
            f.seek(f.tell() - overlap)

-3

5000行不算大(好吧,这取决于每行有多长...)

无论如何:假设字符串是一个单词,并且由空格分隔...

lines=open(file_path,'r').readlines()
str_wanted="whatever_youre_looking_for"


    for i in range(len(lines)):
        l1=lines.split()
        for p in range(len(l1)):
            if l1[p]==str_wanted:
                #found
                # i is the file line, lines[i] is the full line, etc.

2
l1=lines.split() 属性错误:'list'对象没有'split'属性。 - misguided

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接