关于python:读取二进制文件并循环遍历每个字节

Reading binary file and looping over each byte

在Python中,如何读取二进制文件并循环遍历该文件的每个字节?


Python 2.4及更早版本

1
2
3
4
5
6
7
8
f = open("myfile","rb")
try:
    byte = f.read(1)
    while byte !="":
        # Do stuff with byte.
        byte = f.read(1)
finally:
    f.close()

Python 2.5-2.7

1
2
3
4
5
with open("myfile","rb") as f:
    byte = f.read(1)
    while byte !="":
        # Do stuff with byte.
        byte = f.read(1)

请注意,with语句在2.5以下的Python版本中不可用。要在v 2.5中使用它,您需要导入它:

1
from __future__ import with_statement

在2.6中,这不是必需的。

Python 3

在Python 3中,它有点不同。我们将不再以字节模式从流中获取原始字符,而是字节对象,因此我们需要更改条件:

1
2
3
4
5
with open("myfile","rb") as f:
    byte = f.read(1)
    while byte != b"":
        # Do stuff with byte.
        byte = f.read(1)

或者正如benhoyt所说,跳过不等于并利用b""评估为假的事实。这使代码在2.6和3.x之间兼容,无需任何更改。如果从字节模式转换为文本或反向,它还可以避免更改条件。

1
2
3
4
5
with open("myfile","rb") as f:
    byte = f.read(1)
    while byte:
        # Do stuff with byte.
        byte = f.read(1)


此生成器从文件中生成字节,以块的形式读取文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
def bytes_from_file(filename, chunksize=8192):
    with open(filename,"rb") as f:
        while True:
            chunk = f.read(chunksize)
            if chunk:
                for b in chunk:
                    yield b
            else:
                break

# example:
for b in bytes_from_file('filename'):
    do_stuff_with(b)

有关迭代器和生成器的信息,请参阅Python文档。


如果文件不是太大,将其保存在内存中就是一个问题:

1
2
3
bytes_read = open("filename","rb").read()
for b in bytes_read:
    process_byte(b)

其中process_byte表示要对传入的字节执行的某些操作。

如果要一次处理一个块:

1
2
3
4
5
6
7
8
9
file = open("filename","rb")
try:
    bytes_read = file.read(CHUNKSIZE)
    while bytes_read:
        for b in bytes_read:
            process_byte(b)
        bytes_read = file.read(CHUNKSIZE)
finally:
    file.close()


要读取文件 - 一次一个字节(忽略缓冲) - 您可以使用双参数iter(callable, sentinel)内置函数:

1
2
3
with open(filename, 'rb') as file:
    for byte in iter(lambda: file.read(1), b''):
        # Do stuff with byte

它调用file.read(1)直到它不返回任何b''(空字节串)。对于大文件,内存不会无限增长。您可以将buffering=0传递给open(),以禁用缓冲 - 它保证每次迭代只读取一个字节(慢)。

with -statement自动关闭文件 - 包括下面的代码引发异常的情况。

尽管默认情况下存在内部缓冲,但一次处理一个字节仍然是低效的。例如,这是blackhole.py实用程序,它会占用给定的所有内容:

1
2
3
4
5
6
7
8
#!/usr/bin/env python3
"""Discard all input. `cat > /dev/null` analog."""
import sys
from functools import partial
from collections import deque

chunksize = int(sys.argv[1]) if len(sys.argv) > 1 else (1 << 15)
deque(iter(partial(sys.stdin.detach().read, chunksize), b''), maxlen=0)

例:

1
$ dd if=/dev/zero bs=1M count=1000 | python3 blackhole.py

它在我的机器上chunksize == 32768时处理~1.5GB / s,在chunksize == 1时仅处理~7.5MB / s。也就是说,一次读取一个字节要慢200倍。如果您可以重写处理以一次使用多个字节并且需要性能,请将其考虑在内。

mmap允许您同时将文件视为bytearray和文件对象。如果您需要访问两个接口,它可以作为将整个文件加载到内存中的替代方法。特别是,您可以使用普通的for -loop在内存映射文件上一次迭代一个字节:

1
2
3
4
5
from mmap import ACCESS_READ, mmap

with open(filename, 'rb', 0) as f, mmap(f.fileno(), 0, access=ACCESS_READ) as s:
    for byte in s: # length is equal to the current file size
        # Do stuff with byte

mmap支持切片表示法。例如,mm[i:i+len]从位置i开始的文件返回len个字节。 Python 3.2之前不支持上下文管理器协议;在这种情况下,你需要明确地调用mm.close()。使用mmap迭代每个字节会消耗比file.read(1)更多的内存,但mmap的速度要快一个数量级。


总结一下chrispy,Skurmedel,Ben Hoyt和Peter Hansen的所有优点,这将是一次一个字节处理二进制文件的最佳解决方案:

1
2
3
4
5
6
with open("myfile","rb") as f:
    while True:
        byte = f.read(1)
        if not byte:
            break
        do_stuff_with(ord(byte))

对于Python 2.6及更高版本,因为:

  • 内部的python缓冲区 - 无需读取块
  • 干燥原理 - 不要重复读取线
  • with语句确保关闭文件
  • 当没有更多字节时(不是字节为零时),'byte'的计算结果为false

或者使用J. F. Sebastians解决方案来提高速度

1
2
3
4
5
from functools import partial

with open(filename, 'rb') as file:
    for byte in iter(partial(file.read, 1), b''):
        # Do stuff with byte

或者如果你想将它作为代码表示的生成器函数:

1
2
3
4
5
6
7
8
9
10
11
def bytes_from_file(filename):
    with open(filename,"rb") as f:
        while True:
            byte = f.read(1)
            if not byte:
                break
            yield(ord(byte))

# example:
for b in bytes_from_file('filename'):
    do_stuff_with(b)


Reading binary file in Python and looping over each byte

Python 3.5中的新功能是pathlib模块,它具有一种特殊的方法,可以将文件作为字节读入,从而允许我们迭代字节。我认为这是一个体面的(如果快速和肮脏)答案:

1
2
3
4
import pathlib

for byte in pathlib.Path(path).read_bytes():
    print(byte)

有趣的是,这是提及pathlib的唯一答案。

在Python 2中,你可能会这样做(正如Vinay Sajip也建议的那样):

1
2
3
with open(path, 'b') as file:
    for byte in file.read():
        print(byte)

如果文件可能太大而无法在内存中进行迭代,那么您可以使用带有callable, sentinel签名的iter函数(Python 2版本)来解决它:

1
2
3
4
5
6
with open(path, 'b') as file:
    callable = lambda: file.read(1024)
    sentinel = bytes() # or b''
    for chunk in iter(callable, sentinel):
        for byte in chunk:
            print(byte)

(其他几个答案提到了这一点,但很少有人提供合理的读取大小。)

大文件或缓冲/交互式阅读的最佳实践

让我们创建一个执行此操作的函数,包括Python 3.5+标准库的惯用用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pathlib import Path
from functools import partial
from io import DEFAULT_BUFFER_SIZE

def file_byte_iterator(path):
   """given a path, return an iterator over the file
    that lazily loads the file
   """

    path = Path(path)
    with path.open('rb') as file:
        reader = partial(file.read1, DEFAULT_BUFFER_SIZE)
        file_iterator = iter(reader, bytes())
        for chunk in file_iterator:
            for byte in chunk:
                yield byte

请注意,我们使用file.read1file.read阻塞,直到获得所请求的所有字节或EOFfile.read1允许我们避免阻塞,因此可以更快地返回。没有其他答案也提到这一点。

演示最佳实践用法:

让我们创建一个具有兆字节(实际上是mebibyte)的伪随机数据的文件:

1
2
3
4
5
6
7
import random
import pathlib
path = 'pseudorandom_bytes'
pathobj = pathlib.Path(path)

pathobj.write_bytes(
  bytes(random.randint(0, 255) for _ in range(2**20)))

现在让我们迭代它并在内存中实现它:

1
2
3
>>> l = list(file_byte_iterator(path))
>>> len(l)
1048576

我们可以检查数据的任何部分,例如,最后100个字节和前100个字节:

1
2
3
4
>>> l[-100:]
[208, 5, 156, 186, 58, 107, 24, 12, 75, 15, 1, 252, 216, 183, 235, 6, 136, 50, 222, 218, 7, 65, 234, 129, 240, 195, 165, 215, 245, 201, 222, 95, 87, 71, 232, 235, 36, 224, 190, 185, 12, 40, 131, 54, 79, 93, 210, 6, 154, 184, 82, 222, 80, 141, 117, 110, 254, 82, 29, 166, 91, 42, 232, 72, 231, 235, 33, 180, 238, 29, 61, 250, 38, 86, 120, 38, 49, 141, 17, 190, 191, 107, 95, 223, 222, 162, 116, 153, 232, 85, 100, 97, 41, 61, 219, 233, 237, 55, 246, 181]
>>> l[:100]
[28, 172, 79, 126, 36, 99, 103, 191, 146, 225, 24, 48, 113, 187, 48, 185, 31, 142, 216, 187, 27, 146, 215, 61, 111, 218, 171, 4, 160, 250, 110, 51, 128, 106, 3, 10, 116, 123, 128, 31, 73, 152, 58, 49, 184, 223, 17, 176, 166, 195, 6, 35, 206, 206, 39, 231, 89, 249, 21, 112, 168, 4, 88, 169, 215, 132, 255, 168, 129, 127, 60, 252, 244, 160, 80, 155, 246, 147, 234, 227, 157, 137, 101, 84, 115, 103, 77, 44, 84, 134, 140, 77, 224, 176, 242, 254, 171, 115, 193, 29]

不要按行迭代二进制文件

不要执行以下操作 - 这会拉出任意大小的块直到它到达换行符 - 当块太小时太慢,并且可能太大:

1
2
3
4
    with open(path, 'rb') as file:
        for chunk in file: # text newline iteration - not for bytes
            for byte in chunk:
                yield byte

以上只适用于语义上人类可读的文本文件(如纯文本,代码,标记,降价等...基本上任何ascii,utf,latin等编码)。


在尝试了以上所有内容并使用@Aaron Hall的答案后,我在运行Window 10,8 Gb RAM和Python 3.5 32位的计算机上获得了大约90 Mb文件的内存错误。我被一位同事推荐使用numpy而且它可以创造奇迹。

到目前为止,读取整个二进制文件(我测试过的)最快的是:

1
2
3
4
import numpy as np

file ="binary_file.bin"
data = np.fromfile(file, 'u1')

参考

到目前为止,比任何其他方法都要快。希望它可以帮到某人!


Python 3,一次读取所有文件:

1
2
3
4
with open("filename","rb") as binary_file:
    # Read the whole file at once
    data = binary_file.read()
    print(data)

您可以使用data变量迭代任何您想要的内容。


如果要读取大量二进制数据,则可能需要考虑struct模块。它被记录为转换"在C和Python类型之间",但当然,字节是字节,并且这些是否被创建为C类型并不重要。例如,如果二进制数据包含两个2字节整数和一个4字节整数,则可以按如下方式读取它们(示例来自struct文档):

1
2
>>> struct.unpack('hhl', b'\x00\x01\x00\x02\x00\x00\x00\x03')
(1, 2, 3)

您可能会发现这比显式循环文件内容更方便,更快捷或两者兼而有之。


如果你正在寻找快速的东西,这是我一直在使用的方法多年来一直工作:

1
2
3
4
5
6
7
8
9
from array import array

with open( path, 'rb' ) as file:
    data = array( 'B', file.read() ) # buffer the file

# evaluate it's data
for byte in data:
    v = byte # int value
    c = chr(byte)

如果你想迭代字符而不是整数,你可以简单地使用data = file.read(),它应该是py3中的bytes()对象。