【问题标题】:Extract specific bytes from a binary file in Python从 Python 中的二进制文件中提取特定字节
【发布时间】:2016-10-04 01:17:25
【问题描述】:

我有非常大的二进制文件,其中包含 x 个用于 y 传感器的 int16 数据点,以及带有一些基本信息的标题。二进制文件被写入每个采样时间的 y 值,最多 x 个采样,然后是另一组读数,依此类推。如果我想要所有数据,我会使用numpy.fromfile(),它的效果非常好而且速度很快。但是,如果我只想要传感器数据的子集或只想要特定的传感器,我目前有一个可怕的双重 for 循环,使用 file.seek()file.read()struct.unpack(),这将永远持续下去。有没有另一种方法可以在 python 中更快地做到这一点?也许我不太了解mmap()?还是只使用整个fromfile() 然后进行子采样?

data = numpy.empty(num_pts, sensor_indices)
for i in range(num_pts):
    for j in range(sensor_indices):
        curr_file.seek(bin_offsets[j])
        data_binary = curr_file.read(2)
        data[j][i] = struct.unpack('h', data_binary)[0]

听从@rrauenza 在mmap 上的建议,这是很好的信息,我将代码编辑为

mm = mmap.mmap(curr_file.fileno(), 0, access=mmap.ACCESS_READ)
data = numpy.empty(num_pts,sensor_indices)
for i in range(num_pts):
    for j in range(len(sensor_indices)):
        offset += bin_offsets[j] * 2
        data[j][i] = struct.unpack('h', mm[offset:offset+2])[0]

虽然这比以前快,但仍然比以前慢几个数量级

shape = (x, y)
data = np.fromfile(file=self.curr_file, dtype=np.int16).reshape(shape)
data = data.transpose()
data = data[sensor_indices, :]
data = data[:, range(num_pts)]

我使用较小的 30 Mb 文件对此进行了测试,该文件只有 16 个传感器和 30 秒的数据。原始代码为 160 s,mmap 为 105 s,np.fromfile 和子采样为 0.33 s。

剩下的问题是 - 显然使用 numpy.fromfile() 对小文件更好,但是对于可能高达 20 Gb 的大文件是否会出现问题,其中包含数小时或数天的数据和多达 500 个传感器?

【问题讨论】:

  • 你研究过熊猫吗?它非常适合以几乎任何您想要的方式对大型数据集进行排序。
  • 嗨,欢迎来到 *!您的标题有点令人困惑—— strip 通常意味着删除。更准确的词可能是extract
  • 您的超大文件有多大?
  • 我认为这真的取决于您拥有的 RAM 数量。这是scalability 的问题。 mmap() 应该随着数据的增长而线性扩展,numpy.fromfile() 不会随着数据大小的增长,因为在某些时候你需要分页。
  • 您可以使用numpy.memmap 并拥有两全其美的优势。在花哨的索引之前进行切片 (0:num_pts) 以最小化副本。

标签: python numpy mmap seek fromfile


【解决方案1】:

我一定会尝试mmap():

https://docs.python.org/2/library/mmap.html

如果您为每个要提取的int16 调用seek()read(),那么您正在阅读大量包含system call overhead 的小片段。

我写了一个小测试来演示:

#!/usr/bin/python

import mmap
import os
import struct
import sys

FILE = "/opt/tmp/random"  # dd if=/dev/random of=/tmp/random bs=1024k count=1024
SIZE = os.stat(FILE).st_size
BYTES = 2
SKIP = 10


def byfile():
    sum = 0
    with open(FILE, "r") as fd:
        for offset in range(0, SIZE/BYTES, SKIP*BYTES):
            fd.seek(offset)
            data = fd.read(BYTES)
            sum += struct.unpack('h', data)[0]
    return sum


def bymmap():
    sum = 0
    with open(FILE, "r") as fd:
        mm = mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ)
        for offset in range(0, SIZE/BYTES, SKIP*BYTES):
            data = mm[offset:offset+BYTES]
            sum += struct.unpack('h', data)[0]
    return sum


if sys.argv[1] == 'mmap':
    print bymmap()

if sys.argv[1] == 'file':
    print byfile()

我对每个方法运行了两次以补偿缓存。我使用time 是因为我想测量usersys 时间。

结果如下:

[centos7:/tmp]$ time ./test.py file
-211990391

real    0m44.656s
user    0m35.978s
sys     0m8.697s
[centos7:/tmp]$ time ./test.py file
-211990391

real    0m43.091s
user    0m37.571s
sys     0m5.539s
[centos7:/tmp]$ time ./test.py mmap
-211990391

real    0m16.712s
user    0m15.495s
sys     0m1.227s
[centos7:/tmp]$ time ./test.py mmap
-211990391

real    0m16.942s
user    0m15.846s
sys     0m1.104s
[centos7:/tmp]$ 

(和 -211990391 只是验证两个版本做同样的事情。)

查看每个版本的第二个结果,mmap() 大约是实际时间的 1/3。用户时间约为 1/2,系统时间约为 1/5。

您可能加快速度的其他选择是:

(1) 如您所述,加载整个文件。大型 I/O 而不是小型 I/O可以加快速度。但是,如果您超出系统内存,您将退回到分页,这将比mmap() 更糟糕(因为您必须分页)。我对此并不抱太大希望,因为mmap 已经在使用更大的 I/O。

(2) 并发。 也许通过多个线程并行读取文件可以加快速度,但您将需要 Python GIL 来处理。 Multiprocessing 通过避免 GIL 会更好地工作,并且您可以轻松地将数据传递回*处理程序。但是,这将与下一个项目局部性相悖:您可能会使您的 I/O 更加随机。

(3) 局部性。以某种方式组织您的数据(或订购您的读取),以便您的数据更紧密地结合在一起。 mmap()根据系统pagesize分块分页文件:

>>> import mmap
>>> mmap.PAGESIZE
4096
>>> mmap.ALLOCATIONGRANULARITY
4096
>>> 

如果您的数据更靠近(在 4k 块内),则它已经被加载到缓冲区缓存中。

(4) 更好的硬件。就像 SSD。

我确实在 SSD 上运行过它,而且速度要快得多。我运行了 python 的配置文件,想知道 unpack 是否昂贵。不是:

$ python -m cProfile test.py mmap                                                                                                                        
121679286
         26843553 function calls in 8.369 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    6.204    6.204    8.357    8.357 test.py:24(bymmap)
        1    0.012    0.012    8.369    8.369 test.py:3(<module>)
 26843546    1.700    0.000    1.700    0.000 {_struct.unpack}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {method 'fileno' of 'file' objects}
        1    0.000    0.000    0.000    0.000 {open}
        1    0.000    0.000    0.000    0.000 {posix.stat}
        1    0.453    0.453    0.453    0.453 {range}

附录:

好奇心战胜了我,我尝试了multiprocessing。我需要仔细查看我的分区,但不同试验的解包数量 (53687092) 是相同的:

$ time ./test2.py 4
[(4415068.0, 13421773), (-145566705.0, 13421773), (14296671.0, 13421773), (109804332.0, 13421773)]
(-17050634.0, 53687092)

real    0m5.629s
user    0m17.756s
sys     0m0.066s
$ time ./test2.py 1
[(264140374.0, 53687092)]
(264140374.0, 53687092)

real    0m13.246s
user    0m13.175s
sys     0m0.060s

代码:

#!/usr/bin/python

import functools
import multiprocessing
import mmap
import os
import struct
import sys

FILE = "/tmp/random"  # dd if=/dev/random of=/tmp/random bs=1024k count=1024
SIZE = os.stat(FILE).st_size
BYTES = 2
SKIP = 10


def bymmap(poolsize, n):
    partition = SIZE/poolsize
    initial = n * partition
    end = initial + partition
    sum = 0.0
    unpacks = 0
    with open(FILE, "r") as fd:
        mm = mmap.mmap(fd.fileno(), 0, prot=mmap.PROT_READ)
        for offset in xrange(initial, end, SKIP*BYTES):
            data = mm[offset:offset+BYTES]
            sum += struct.unpack('h', data)[0]
            unpacks += 1
    return (sum, unpacks)


poolsize = int(sys.argv[1])
pool = multiprocessing.Pool(poolsize)
results = pool.map(functools.partial(bymmap, poolsize), range(0, poolsize))
print results
print reduce(lambda x, y: (x[0] + y[0], x[1] + y[1]), results)

【讨论】: