在不加载内存的情况下随机播放大量项目

时间:2014-06-30 14:22:06

标签: python shuffle

我有一个包含大约20亿行文本的文件(~200gig)。我想生成一个包含相同文本行的新文件,但是按行随机洗牌。我无法将所有数据保存在内存中。有没有一种好的方法在python /命令行中执行此操作需要一段合理的时间(几天)?

我以为我可以触摸50个空文件。流过20亿行文件,并将每行随机分配到50个空文件中的一个。然后cat 50个文件。这种方法会有任何重大的系统性偏见吗?

7 个答案:

答案 0 :(得分:7)

如果你可以为这个程序保留16 GB的内存,我编写了一个名为sample的程序,通过读取它们的字节偏移,改变偏移量,然后通过搜索来打印输出来对文件的行进行混洗。文件到洗牌补偿。它为每个64位偏移使用8个字节,因此对于20亿行输入使用16个字节。

它不会很快,但在具有足够内存的系统上,sample将会混乱大到足以导致GNU shuf失败的文件。此外,它使用mmap例程来尝试最小化第二次通过文件的I / O开销。它还有一些其他选择;有关详细信息,请参阅--help

默认情况下,此程序将在不进行替换的情况下进行采样,并通过单行进行随机播放。如果您想要替换,或者您的输入是FASTA,FASTQ或其他多行格式,您可以添加一些选项来调整采样的方式。 (或者您可以应用替代方法,我将其链接到下面的Perl要点,但sample解决了这些问题。)

如果你的FASTA序列在每两行上,也就是说,它们在一行上的序列标题和下一行的序列数据之间交替,你仍然可以使用sample和内存的一半进行随机播放,因为你只是拖延了一半的补偿。请参阅--lines-per-offset选项;例如,你可以指定2来改变线对。

对于FASTQ文件,它们的记录每四行分割一次。您可以指定--lines-per-offset=4来使用洗牌单行文件所需的内存的四分之一来随机播放FASTQ文件。

或者,我有一个用Perl编写的gist here,它将对FASTA文件中的序列进行采样,而不考虑序列中的行数。请注意,这与对整个文件进行混洗并不完全相同,但您可以将此作为起点,因为它会收集偏移量。您可以删除第47行,对排序的索引进行排序,然后使用文件搜索操作来读取文件,直接使用混洗索引列表。

同样,它不会很快,因为你正在按顺序跳过一个非常大的文件,但是存储偏移比存储整行要便宜得多,并且添加mmap例程可以帮助实现基本上是一系列随机访问操作。如果您正在使用FASTA,那么存储的偏移量会更少,因此您的内存使用量(除了任何相对无关紧要的容器和程序开销)应该最多为8 GB - 并且可能更少,具体取决于其结构。

答案 1 :(得分:5)

怎么样:

import mmap
from random import shuffle

def find_lines(data):
    for i, char in enumerate(data):
        if char == '\n':
            yield i 

def shuffle_file(in_file, out_file):
    with open(in_file) as f:
        data = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
        start = 0
        lines = []
        for end in find_lines(data):
            lines.append((start, end))
            start = end + 1
        shuffle(lines)

        with open(out_file, 'w') as out:
            for start, end in lines:
                out.write(data[start:end+1])

if __name__ == "__main__":
    shuffle_file('data', 'result')

此解决方案应该只存储文件中行的所有文件偏移量,每行2个字,加上容器开销。

答案 2 :(得分:4)

您可以查看我的HugeFileProcessor工具。它类似于@ Alex-Reynolds的sample,但应该明显更快,因为没有寻求。

以下是关于改组实施的详细信息。它需要指定 batchSize - 写入输出时保留在RAM中的行数。越多越好(除非你没有RAM),因为总的洗牌时间是(sourceFile中的行数)/ batchSize *(完全读取sourceFile的时间)。请注意,程序随机播放整个文件,而不是按批次。

算法如下。

  1. 计算 sourceFile 中的行数。这可以通过逐行读取整个文件来完成。 (参见一些比较here。)这也可以衡量一次读取整个文件需要多长时间。因此,我们可以估计完成一次shuffle需要多少次,因为它需要 Ceil(linesCount / batchSize)完整的文件读取。

  2. 我们现在知道 linesCount 的总数,我们可以创建 linesCount 大小的索引数组并使用Fisher–Yates对其进行随机播放(称为代码中的orderArray 。这将给我们一个订单,我们希望在洗牌文件中包含行。请注意,这是整个文件的全局顺序,而不是每批或块或其他内容。

  3. 现在是实际的代码。我们需要按照我们刚刚计算的顺序从 sourceFile 获取所有行,但是我们无法在内存中读取整个文件。所以我们只是拆分任务。

    • 我们将通过 sourceFile 读取所有行,并在内存中仅存储 orderArray 的第一个 batchSize 中的那些行。当我们获得所有这些行时,我们可以按要求的顺序将它们写入 outFile ,并且它是 batchSize / linesCount 完成的工作。
    • 接下来,我们将一遍又一遍地重复整个过程,然后再从 orderArray 的下一部分开始,并从头到尾为每个部分读取 sourceFile 。最终整个 orderArray 得到处理,我们就完成了。
  4. 为什么会有效?

    因为我们所做的只是从头到尾阅读源文件。没有寻求前进/后退,这就是硬盘驱动器所喜欢的。文件根据内部HDD缓冲区,FS块,CPU cahce等以块的形式读取,所有内容都按顺序读取。

    有些数字

    在我的机器上(Core i5,16GB RAM,Win8.1,HDD Toshiba DT01ACA200 2TB,NTFS)我可以使用 batchSize <在大约5小时内将132 GB(84 000 000行)的文件随机播放/ em>为3 500 000. batchSize 为2 000 000,耗时约8小时。读取速度约为每秒118000行。

答案 3 :(得分:1)

我认为在你的情况下最简单的是做一个递归的shuffle&amp; split - shuffle - merge。 您定义了两个数字:要分割一个文件的文件数:N(典型地介于32和256之间),以及您可以直接在内存中随机播放的大小M(通常大约为128) MO)。然后你有伪代码:

def big_shuffle(file):
    if size_of(file) < M :
        memory_shuffle(file)
    else:
        create N files
        for line in file:
            write_randomly_to_one_of_the_N_files
        for sub_file in (N_files):
            big_shuffle(file)
        merge_the_N_files_one_line_each

当每个子文件被洗牌时,你应该没有偏见。

它将远远低于Alex Reynolds解决方案(因为有很多磁盘io),但你唯一的限制是磁盘空间。

答案 4 :(得分:0)

您可以创建一个给出置换的迭代器。您将读入的文件偏移给它的数量。因为迭代器提供了排列,所以您永远不会读取相同的数据两次。

一组N个元素的所有置换都可以通过换位生成,换位是置换第0个和第ith个元素(假定从0开始索引)并保留所有其他元素的置换。因此,您可以通过组合一些随机选择的换位来进行随机排列。这是用Python编写的示例:

import random

class Transposer:
    def __init__(self,i):
        """
        (Indexes start at 0)
        Swap 0th index and ith index, otherwise identity mapping.
        """
        self.i = i
    def map(self,x):
        if x == 0:
            return self.i
        if x == self.i:
            return 0
        return x

class RandomPermuter:
    def __init__(self,n_gens,n):
        """
        Picks n_gens integers in [0,n) to make transposers that, when composed,
        form a permutation of a set of n elements. Of course if there are an even number of drawn
        integers that are equal, they cancel each other out. We could keep
        drawing numbers until we have n_gens unique numbers... but we don't for
        this demo.
        """
        gen_is = [random.randint(0,n-1) for _ in range(n_gens)]
        self.trans = [Transposer(g) for g in gen_is]
    def map(self,x):
        for t in self.trans:
            x = t.map(x)
        return x

rp = RandomPermuter(10,10)

# Use these numbers to seek into a file
print(*[rp.map(x) for x in range(10)])

答案 5 :(得分:0)

我必须解决上述问题,以便重新整理庞大的文本文件。这样,脚本会将项目放置在缓冲区中。此外,在打开原始文件和写入新文件之间没有任何对象,这意味着该脚本不会使用太多RAM。您还可以节省对文件的一次迭代,而不是对文件/对象进行多次迭代。一旦制作了这些较小的随机文件,重新组合这些文件就很简单。只需将每个文件读入新文件即可。 Python代码:

import random
import io
from tqdm import tqdm

file_in = "file\\to\\randomize"
file_out = "base\\path\\to\\place\\randomized\\files\\"
files_out = []

NUM_OF_FILES = 1_000

for i in range(NUM_OF_FILES):
    f_ = file_out + str(i)
    files_out.append(io.open(f_, 'w', encoding='utf-8'))

with io.open(file_in, 'r', encoding='utf-8') as source:
    for f in tqdm(source):
        files_out[random.randint(0, NUM_OF_FILES - 1)].write(f)
    for i in range(NUM_OF_FILES):
        files_out[i].close()

for i in range(NUM_OF_FILES):
    f_ = file_out + str(i)
    data = []
    with io.open(f_, 'r', encoding='utf-8') as file:
        data = [(random.random(), line) for line in tqdm(file)]
    data.sort()
    with io.open(f_, 'w', encoding='utf-8') as file:
        for _, line in tqdm(data):
            file.write(line)

答案 6 :(得分:0)

似乎与 How can I shuffle a very large list stored in a file in Python?

相同的问题

如果你会使用java或者愿意翻译一些代码,我建议使用https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/wiki/utilities中的ImmutableList解决方案。如果您的原始文件具有随机访问权限(以便您可以获得项目 N),那么您甚至不需要创建第二个混洗文件。