[Python] Lectura de archivo binario y bucle sobre cada byte



Answers

Este generador produce bytes desde un archivo, leyendo el archivo en fragmentos:

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)

Consulte la documentación de Python para obtener información sobre iterators y generators .

Question

En Python, ¿cómo leo en un archivo binario y recorro cada byte de ese archivo?




Lectura de archivos binarios en Python y bucles sobre cada byte

Nuevo en Python 3.5 es el módulo pathlib , que tiene un método de conveniencia específicamente para leer en un archivo como bytes, lo que nos permite iterar sobre los bytes. Considero que esta es una respuesta decente (si es rápida y sucia):

import pathlib

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

Es interesante que esta es la única respuesta para mencionar a pathlib .

En Python 2, probablemente harías esto (como también sugiere Vinay Sajip):

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

En el caso de que el archivo sea demasiado grande para iterar en la memoria, lo dividiría, idiomáticamente, utilizando la función iter con la firma callable, sentinel la versión de 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)

(Varias otras respuestas lo mencionan, pero pocas ofrecen un tamaño de lectura razonable).

Práctica recomendada para archivos grandes o lectura en búfer / interactiva

Creemos una función para hacer esto, incluidos los usos idiomáticos de la biblioteca estándar para 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

Tenga en cuenta que usamos file.read1 . file.read bloques hasta que obtenga todos los bytes solicitados o EOF . file.read1 nos permite evitar el bloqueo, y puede regresar más rápido debido a esto. Ninguna otra respuesta menciona esto también.

Demostración del uso de mejores prácticas:

Hagamos un archivo con un megabyte (en realidad mebibyte) de datos pseudoaleatorios:

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

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

Ahora iteremos sobre esto y materialicémoslo en la memoria:

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

Podemos inspeccionar cualquier parte de los datos, por ejemplo, los últimos 100 y primeros 100 bytes:

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

No iterar por líneas para archivos binarios

No haga lo siguiente: esto tira de un trozo de tamaño arbitrario hasta que llegue a un carácter de línea nueva, demasiado lento cuando los trozos son demasiado pequeños, y posiblemente demasiado grandes también:

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

Lo anterior solo es bueno para lo que son semánticamente archivos de texto legibles por el ser humano (como texto sin formato, código, marcado, rebajas, etc ... esencialmente cualquier cosa ascii, utf, latino, etc ... codificado).




Para leer un archivo, un byte a la vez (ignorando el almacenamiento en búfer), puede usar la función incorporada de dos argumentos iter(callable, sentinel) :

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

Llama a file.read(1) hasta que no devuelva nada b'' (empty bytestring). La memoria no crece ilimitadamente para archivos grandes. Puede pasar el buffering=0 para open() , para desactivar el almacenamiento en búfer: garantiza que solo se lea un byte por iteración (lento).

with -statement cierra el archivo automáticamente, incluido el caso cuando el código subyacente genera una excepción.

A pesar de la presencia de almacenamiento intermedio interno por defecto, todavía es ineficiente procesar un byte a la vez. Por ejemplo, aquí está la utilidad blackhole.py que come todo lo que se le da:

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

Ejemplo:

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

Procesa ~ 1.5 GB / s cuando chunksize == 32768 en mi máquina y solo ~ 7.5 MB / s cuando chunksize == 1 . Es decir, es 200 veces más lento leer un byte a la vez. Tómelo en cuenta si puede reescribir su procesamiento para usar más de un byte a la vez y si necesita rendimiento.

mmap permite tratar un archivo como bytearray y un objeto de archivo simultáneamente. Puede servir como una alternativa para cargar todo el archivo en la memoria si necesita acceder a ambas interfaces. En particular, puede iterar un byte a la vez sobre un archivo mapeado en memoria simplemente usando un simple 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 admite la notación de división. Por ejemplo, mm[i:i+len] devuelve len bytes desde el archivo que comienza en la posición i . El protocolo del administrador de contexto no es compatible antes de Python 3.2; necesita llamar a mm.close() explícitamente en este caso. La iteración sobre cada byte usando mmap consume más memoria que file.read(1) , pero mmap es un orden de magnitud más rápido.




Si tiene muchos datos binarios para leer, puede considerar el módulo struct . Está documentado como la conversión "entre C y tipos de Python", pero, por supuesto, los bytes son bytes, y si esos se crearon como tipos C no importan. Por ejemplo, si sus datos binarios contienen dos enteros de 2 bytes y un entero de 4 bytes, puede leerlos de la siguiente manera (ejemplo tomado de la documentación de struct ):

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

Puede encontrar esto más conveniente, más rápido o ambos, que alinear explícitamente el contenido de un archivo.




Links