Python随机从大文件N行(没有重复的行)

时间:2012-09-05 10:06:34

标签: python random line large-files readline

我需要使用python从大型txt文件中获取N行。这些文件基本上是制表符分隔的表。我的任务有以下限制:

  • 这些文件可能包含标题(某些文件包含多行标题)。
  • 标题需要以相同的顺序出现在输出中。
  • 每行只能拍摄一次。
  • 目前最大的文件大约是150GB(约6,000 000行)。
  • 行在文件中的长度大致相同,但在不同文件之间可能会有所不同。
  • 我通常会随机抽取5000行(我可能需要多达1 000 000行)

目前我已经编写了以下代码:

inputSize=os.path.getsize(options.input)
usedPositions=[] #Start positions of the lines already in output

with open(options.input) as input:
    with open(options.output, 'w') as output:

        #Handling of header lines
        for i in range(int(options.header)):
            output.write(input.readline())
            usedPositions.append(input.tell())

        # Find and write all random lines, except last
        for j in range(int(args[0])):
            input.seek(random.randrange(inputSize)) # Seek to random position in file (probably middle of line)
            input.readline() # Read the line (probably incomplete). Next input.readline() results in a complete line.
            while input.tell() in usedPositions: # Take a new line if current one is taken
                input.seek(random.randrange(inputSize))
                input.readline() 
            usedPositions.append(input.tell()) # Add line start position to usedPositions
            randomLine=input.readline() # Complete line
            if len(randomLine) == 0: # Take first line if end of the file is reached
                input.seek(0)
                for i in range(int(options.header)): # Exclude headers
                    input.readline()
                randomLine=input.readline()
            output.write(randomLine)            

此代码似乎正常运行。

我知道这段代码更喜欢输入中最长行的行,因为seek()最有可能返回最长行的位置,下一行写入输出。这是无关紧要的,因为输入文件中的行长度大致相同。 另外我知道如果N大于输入文件中的行数,则此代码会导致无限循环。我不会对此进行检查,因为获取行计数需要花费很多时间。

RAM和硬盘限制无关紧要。我只关心程序的速度。有没有办法进一步优化此代码?或许还有更好的方法?

编辑:为了澄清,一个文件中的行长度大致相同。但是,我有多个文件需要运行此脚本,并且这些文件的行的平均长度将不同。例如,文件A每行可以有大约100个字符,每行可以有B~50000个字符。我事先并不知道任何文件的平均行长度。

5 个答案:

答案 0 :(得分:6)

只有一种方法可以避免顺序读取所有文件直到您正在采样的最后一行 - 我很惊讶到目前为止没有一个答案提到它:

你必须寻找文件内的任意位置,读取一些字节,如果你有一个典型的行长度,正如你所说的那样,该值应该是3到4倍。然后在新行字符(“\ n”)上拆分您读取的块,然后选择第二个字段 - 即随机位置的一行。

此外,为了能够始终如一地搜索文件,应该以“二进制读取”模式打开,因此,应该手动处理行结束标记的转换。

此技术无法为您提供已读取的行号,因此您可以在文件中保留选定的行偏移量以避免重复:

#! /usr/bin/python
# coding: utf-8

import random, os


CHUNK_SIZE = 1000
PATH = "/var/log/cron"

def pick_next_random_line(file, offset):
    file.seek(offset)
    chunk = file.read(CHUNK_SIZE)
    lines = chunk.split(os.linesep)
    # Make some provision in case yiou had not read at least one full line here
    line_offset = offset + len(os.linesep) + chunk.find(os.linesep) 
    return line_offset, lines[1]

def get_n_random_lines(path, n=5):
    lenght = os.stat(path).st_size
    results = []
    result_offsets = set()
    with open(path) as input:
        for x in range(n):
            while True:
                offset, line = pick_next_random_line(input, random.randint(0, lenght - CHUNK_SIZE))
                if not offset in result_offsets:
                    result_offsets.add(offset)
                    results.append(line)
                    break
    return results

if __name__ == "__main__":
    print get_n_random_lines(PATH)

答案 1 :(得分:4)

如果您的文件中需要N行的统一样本,则需要知道要选择的的确切数量;随意搜索不会这样做,较长的线条会使结果偏向于直接跟随最长线条的线条。

幸运的是,您只需要读取一次的文件即可选择这N行。你基本上选择N个第一行(按随机顺序),然​​后根据读取的行数随机地用新的行替换拾取的行。

对于N == 1,第n行读取的机会取代之前的随机选择是randint(0, n) < 1,因此,第二行有50%的机会被选中,第三行有33.33%的机会,对于较大的N,随着更多行的读取,随机替换集合中已经拾取的行之一,具有相同的分布。

Python random lines from subfolders中,Blkknght编写了一个非常有用的函数,用于从可迭代中选择大小为N的随机样本:

import random

def random_sample(n, items):
    results = []

    for i, v in enumerate(items):
        r = random.randint(0, i)
        if r < n:
            if i < n:
                results.insert(r, v) # add first n items in random order
            else:
                results[r] = v # at a decreasing rate, replace random items

    if len(results) < n:
        raise ValueError("Sample larger than population.")

    return results

这与您的要求相结合以保留一组标题非常简单:

from itertools import islice

with open(options.input) as input:
    with open(options.output, 'w') as output:

        # Handling of header lines
        # Use islice to avoid buffer issues with .readline()
        for line in islice(input, int(options.header)):
            output.write(line)

        # Pick a random sample
        for line in random_sample(int(args[0]), input):
            output.write(line)

这将一次性读取整个文件,选择一个统一随机样本,并将其写入输出文件。因此,这具有Θ(L)复杂度,L是文件中的行数。

答案 2 :(得分:3)

我相信随机选择N个行号会更快,然后逐行检查文件,并列出列表中的数字。目前你必须寻找每个随机数的随机位置,所以它是O(N * M),其中M是文件的大小。我建议的是O(M)。

答案 3 :(得分:1)

  • 明显的改进是将set()用于usedPositions变量 - 查找会更快,而且由于您需要处理最多10 ^ 6个已使用的位置,因此查找时间并不相关。
  • 在for循环中使用xrange代替range。分配完整的整数列表似乎没有必要。

答案 4 :(得分:0)

未经测试(并且需要两次读取文件):

import random

N = 5000
with open('file.in') as fin:
    line_count = sum(1 for i in fin)
    fin.seek(0)
    to_take = set(random.sample(xrange(line_count), N))
    for lineno, line in enumerate(fin):
        if lineno in to_take:
            pass # use it

但是,既然你提到这些行的大小相同,那么你可以使用os.path.getsize并将其除以平均行长度(无论是已知的,还是从文件中的N行中嗅出) ,然后用它来生成line_count - 它足够接近随机样本。

您还可以mmap该文件,并使用文件大小,平均线长度,行数最佳猜测和随机行号的组合来“搜索”,然后只需向后或向前搜索下一行开始。 (由于mmap可以让您将其视为字符串,因此您可以将.index与偏移量一起使用,或者如果您真的想使用re,则可以使用{{1}}。