如何在Python中将文本文件随机播放到磁盘上

时间:2013-10-10 19:11:53

标签: python text-files bigdata shuffle

我正在处理一个大约12 * 10 ^ 6行的文本文件,该文件存储在我的硬盘上。 该文件的结构是:

data|data|data|...|data\n
data|data|data|...|data\n
data|data|data|...|data\n
...
data|data|data|...|data\n

没有标题,并且没有唯一标识行的ID。

由于我想将它用于机器学习目的,我需要确保文本文件中没有可能影响随机学习的顺序。

通常我会将这类文件上传到内存中,然后在将它们重写到磁盘之前对其进行随机播放。不幸的是,由于文件的大小,这次不可能,所以我必须直接在磁盘上管理洗牌(假设我没有磁盘空间的问题)。关于如何有效地(尽可能低的复杂性,即写入磁盘)用Python管理这样的任务的任何想法?

4 个答案:

答案 0 :(得分:6)

除了其中一个想法之外的所有想法都使用O(N)内存 - 但是如果你使用array.arraynumpy.ndarray我们正在谈论N * 4个字节,这明显小于整个文件。 (为简单起见,我将使用简单列表;如果您需要帮助转换为更紧凑的类型,我也可以显示。)


使用临时数据库和索引列表:

with contextlib.closing(dbm.open('temp.db', 'n')) as db:
    with open(path) as f:
        for i, line in enumerate(f):
            db[str(i)] = line
    linecount = i
    shuffled = random.shuffle(range(linecount))
    with open(path + '.shuffled', 'w') as f:
        for i in shuffled:
            f.write(db[str(i)])
os.remove('temp.db')

这是2N单行磁盘操作,以及2N单dbm-key磁盘操作,应该是2NlogN单磁盘操作等效操作,因此总复杂度为O(NlogN)。


如果你使用像sqlite3这样的关系数据库而不是dbm,你甚至不需要索引列表,因为你可以这样做:

SELECT * FROM Lines ORDER BY RANDOM()

这具有与上述相同的时间复杂度,并且在理论上空间复杂度是O(1)而不是O(N)。在实践中,您需要一个RDBMS,它可以从100M行集中一次为您提供一行,而不会在任何一侧存储100M。


一个不同的选项,不使用临时数据库 - 理论上是O(N ** 2),但实际上可能更快,如果你碰巧有足够的内存让行缓存有用:

with open(path) as f:
    linecount = sum(1 for _ in f)
shuffled = random.shuffle(range(linecount))
with open(path + '.shuffled', 'w') as f:
    for i in shuffled:
        f.write(linecache.getline(path, i))

最后,通过将索引列表的大小加倍,我们可以消除临时磁盘存储。但实际上,这可能会慢得多,因为你正在做更多的随机访问读取,这些驱动器的效果并不差。

with open(path) as f:
    linestarts = [f.tell() for line in f]
    lineranges = zip(linestarts, linestarts[1:] + [f.tell()])
    shuffled = random.shuffle(lineranges)
    with open(path + '.shuffled', 'w') as f1:
        for start, stop in shuffled:
            f.seek(start)
            f1.write(f.read(stop-start))

答案 1 :(得分:2)

这是基于我上面评论的建议。它依赖于压缩线仍然能够适应内存。如果不是这样,则需要其他解决方案。

import zlib
from random import shuffle

def heavy_shuffle(filename_in, filename_out):
    with open(filename_in, 'r') as f:
        zlines = [zlib.compress(line, 9) for line in f]
    shuffle(zlines)
    with open(filename_out, 'w') as f:
        for zline in zlines:
            f.write(zlib.decompress(zline) + '\n')

我的经验是zlib很快,而bz2提供更好的压缩,所以你可能想比较。

另外,如果你可以将n个线路组合在一起,那么这样做可能会提高你的压缩率。


我想知道有用压缩的可能性,所以这是一个IPython实验。我不知道你的数据是什么样的,所以我只是将浮点数(作为字符串)舍入到3个位置并用管道串起来:

最佳情况(例如,许多行具有相同的数字):

In [38]: data = '0.000|'*200

In [39]: len(data)
Out[39]: 1200

In [40]: zdata = zlib.compress(data, 9)

In [41]: print 'zlib compression ratio: ',1.-1.*len(zdata)/len(data)
zlib compression ratio:  0.98

In [42]: bz2data = bz2.compress(data, 9)

In [43]: print 'bz2 compression ratio: ',1.-1.*len(bz2data)/len(data)
bz2 compression ratio:  0.959166666667

正如预期的那样,最好的情况非常好,> 95%的压缩率。

最坏情况(随机数据):

In [44]: randdata = '|'.join(['{:.3f}'.format(x) for x in np.random.randn(200)])

In [45]: zdata = zlib.compress(randdata, 9)

In [46]: print 'zlib compression ratio: ',1.-1.*len(zdata)/len(data)
zlib compression ratio:  0.5525

In [47]: bz2data = bz2.compress(randdata, 9)

In [48]: print 'bz2 compression ratio: ',1.-1.*len(bz2data)/len(data)
bz2 compression ratio:  0.5975

令人惊讶的是,最糟糕的情况不是太差〜压缩率为60%,但如果你只有8 GB的内存(15 GB的60%是9 GB),则可能会出现问题。

答案 2 :(得分:0)

这个问题可以被认为是有效的内存页管理问题,以减少交换文件I / O.让您的缓冲区buf成为您希望存储到输出文件中的多个文件块的列表。让一个连续的文件块是固定数量的整行列表。

现在,生成一个随机序列并重新映射返回的值,以便在该块中包含块数和行偏移量。

此操作会为您留下一系列数字[1..num of chunks],可以将其描述为对[1..num of chunks]之间数字页面中包含的内存片段的访问序列。对于在线变体(如在实际操作系统中),没有针对此问题的最佳策略,但由于您知道实际的页面引用顺序,因此可以找到最佳解决方案here

这种方法有什么好处?最常用的页面最少从HDD重读,意味着读取数据的I / O操作较少。此外,考虑到您的块大小足以最小化与内存占用相比的页面交换,输出文件行之后的很多次将从存储在内存中的相同块(或任何其他块,但尚未交换到驱动器而不是从驱动器重新读取。

也许这不是最简单的解决方案(虽然最佳的页面交换算法很容易编写),这可能是一项有趣的练习,不是吗?

答案 3 :(得分:0)

假设磁盘空间不是问题,我正在创建多个文件来保存数据。

import random
import os

PMSize = 100 #Lesser value means using more primary memory
shuffler = lambda x: open(x, 'w')
shufflers = [shuffler('file'+str(x)) for x in range(PMSize)]

with open('filename') as file:
    for line in file:
        i = random.randint(0, len(shufflers)-1)
        shufflers[i].write(line)

with open('filename', 'w') as file:
    for file in shufflers:
        newfile.write(file.read())

for file in shufflers:
    os.remove(file)

您的内存复杂性将由PMSize控制。时间复杂度将在O(N + PMSize)附近。