[python] Чтение двоичного файла и цикл по каждому байту



Answers

Этот генератор дает байты из файла, считывая файл в виде кусков:

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)

Информацию об iterators и generators см. В документации Python.

Question

В Python, как я читаю в двоичном файле и перебираю каждый байт этого файла?




Чтобы прочитать файл - по одному байту за раз (игнорируя буферизацию) - вы можете использовать встроенную функцию с двумя аргументами iter(callable, sentinel) :

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 которая ест все, что ей дано:

#!/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)

Пример:

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

Он обрабатывает ~ 1,5 ГБ / с, когда chunksize == 32768 на моей машине и только ~ 7.5 МБ / с, когда chunksize == 1 . То есть, в 200 раз медленнее читать по одному байту за раз. Учитывайте это, если вы можете переписать свою обработку для использования более одного байта за раз, и если вам нужна производительность.

mmap позволяет обрабатывать файл как bytearray и файл одновременно. Он может служить альтернативой загрузке всего файла в память, если вам нужен доступ к обоим интерфейсам. В частности, вы можете перебирать один байт за один раз по файлу с отображением памяти, используя простоту for -loop:

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] возвращает len байты из файла, начиная с позиции i . Протокол менеджера контекста не поддерживается до Python 3.2; в этом случае вам нужно явно вызвать mm.close() . Итерация по каждому байту с использованием mmap потребляет больше памяти, чем file.read(1) , но mmap на порядок быстрее.




Если у вас есть много двоичных данных для чтения, вы можете рассмотреть модуль структуры . Он документируется как преобразование «между типами C и Python», но, конечно, байты являются байтами, и независимо от того, были ли они созданы как типы C, не имеет значения. Например, если ваши двоичные данные содержат два целых 2 байта и одно целое число из 4 байтов, вы можете прочитать их следующим образом (пример из документации по struct ):

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

Вы можете найти это более удобным, быстрым или и тем, и другим, чем явным образом перебирать содержимое файла.




Чтение двоичного файла в Python и цикл по каждому байту

Новое в Python 3.5 является модулем pathlib , который имеет удобный метод, специально предназначенный для чтения в файле в виде байтов, что позволяет нам перебирать байты. Я считаю, что это достойный (если быстрый и грязный) ответ:

import pathlib

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

Интересно, что это единственный ответ на упоминание pathlib .

В Python 2 вы, вероятно, сделаете это (как предлагает Vinay Sajip):

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

В случае, если файл может быть слишком большим для перебора в памяти, вы бы его именовали, идиоматически, используя функцию iter с callable, sentinel сигнальной сигнатурой - версией Python 2:

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+:

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.read1 . file.read пока не получит все запрошенные байты или EOF . file.read1 позволяет избежать блокировки, и из-за этого он может вернуться быстрее. Никакие другие ответы не упоминают об этом.

Демонстрация использования наилучшей практики:

Давайте сделаем файл с мегабайтом (фактически mebibyte) псевдослучайных данных:

import random
import pathlib
path = 'pseudorandom_bytes'
pathobj = pathlib.Path(path)

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

Теперь давайте перейдем к нему и материализуем его в памяти:

>>> l = list(file_byte_iterator(path))
>>> len(l)
1048576

Мы можем проверять любую часть данных, например, последние 100 и первые 100 байт:

>>> 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]

Не перебирайте строки для двоичных файлов

Не делайте следующего: это вытаскивает кусок произвольного размера, пока не дойдет до символа новой строки - слишком медленно, когда куски слишком малы и, возможно, слишком большие:

    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 и т. Д.).




Related