Leer un archivo binario y recorrer cada byte

377

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

Jesse Vogt
fuente

Respuestas:

387

Python 2.4 y anteriores

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

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

Tenga en cuenta que la declaración with no está disponible en versiones de Python inferiores a 2.5. Para usarlo en v 2.5 necesitarás importarlo:

from __future__ import with_statement

En 2.6 esto no es necesario.

Python 3

En Python 3, es un poco diferente. Ya no obtendremos caracteres sin procesar de la secuencia en modo byte sino objetos de byte, por lo tanto, debemos modificar la condición:

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

O como dice benhoyt, omita lo no igual y aproveche el hecho de que se b""evalúa como falso. Esto hace que el código sea compatible entre 2.6 y 3.x sin ningún cambio. También le evitaría cambiar la condición si pasa del modo byte al texto o al revés.

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

python 3.8

De ahora en adelante gracias a: = operador, el código anterior se puede escribir de una manera más corta.

with open("myfile", "rb") as f:
    while (byte := f.read(1)):
        # Do stuff with byte.
Skurmedel
fuente
40
Leer un archivo en bytes es una pesadilla de rendimiento. Esta no puede ser la mejor solución disponible en python. Este código debe usarse con cuidado.
usr
77
@ usr: Bueno, los objetos del archivo están almacenados internamente, y aun así esto es lo que se solicitó. No todas las secuencias de comandos necesitan un rendimiento óptimo.
Skurmedel
44
@mezhaka: Entonces lo cambias de read (1) a read (bufsize) y en el ciclo while haces un for-in ... el ejemplo sigue en pie.
Skurmedel
3
@ usr: la diferencia de rendimiento puede ser de hasta 200 veces para el código que he probado .
jfs
2
@usr: depende de cuántos bytes desee procesar. Si son pocos, se puede preferir un código de "mal desempeño" pero fácilmente comprensible. El desperdicio de ciclos de CPU se compensa al guardar "ciclos de CPU del lector" al mantener el código.
IllvilJa
172

Este generador produce bytes de 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 iteradores y generadores .

codeape
fuente
3
@codeape Justo lo que estoy buscando. Pero, ¿cómo se determina el tamaño del trozo? ¿Puede ser un valor arbitrario?
swdev
3
@swdev: el ejemplo utiliza un tamaño de fragmento de 8192 bytes . El parámetro para la función file.read () simplemente especifica el tamaño, es decir, el número de bytes a leer. codeape eligió 8192 Byte = 8 kB(en realidad es, KiBpero eso no es tan conocido). El valor es "totalmente" aleatorio, pero 8 kB parece ser un valor apropiado: no se desperdicia demasiada memoria y todavía no hay "demasiadas" operaciones de lectura como en la respuesta aceptada por Skurmedel ...
mozzbozz
3
El sistema de archivos ya almacena fragmentos de datos, por lo que este código es redundante. Es mejor leer un byte a la vez.
rígido
17
Si bien ya es más rápido que la respuesta aceptada, esto podría acelerarse en otro 20-25% reemplazando todo el for b in chunk:ciclo más interno con yield from chunk. Esta forma de yieldse agregó en Python 3.3 (ver Expresiones de rendimiento ).
Martineau
3
Hmm parece poco probable, enlace?
codeape
54

Si el archivo no es demasiado grande, mantenerlo en la memoria es un problema:

with open("filename", "rb") as f:
    bytes_read = f.read()
for b in bytes_read:
    process_byte(b)

donde process_byte representa alguna operación que desea realizar en el byte pasado.

Si desea procesar un fragmento a la vez:

with open("filename", "rb") as f:
    bytes_read = f.read(CHUNKSIZE)
    while bytes_read:
        for b in bytes_read:
            process_byte(b)
        bytes_read = f.read(CHUNKSIZE)

La withdeclaración está disponible en Python 2.5 y superior.

Vinay Sajip
fuente
1
Quizás te interese el punto de referencia que acabo de publicar.
Martineau
37

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

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

Llama file.read(1)hasta que no devuelve nada b''(cadena de bytes vacía). La memoria no crece de forma ilimitada para archivos grandes. Puede pasar buffering=0 a open(), para deshabilitar el almacenamiento en búfer: garantiza que solo se lea un byte por iteración (lento).

with-declaración cierra el archivo automáticamente, incluido el caso en que el código debajo genera una excepción.

A pesar de la presencia de almacenamiento en búfer interno de forma predeterminada, sigue siendo ineficiente procesar un byte a la vez. Por ejemplo, aquí está la blackhole.pyutilidad que se 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 está chunksize == 32768en 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éngalo en cuenta si puede reescribir su procesamiento para usar más de un byte a la vez y si necesita rendimiento.

mmaple permite tratar un archivo como un bytearrayobjeto de archivo y simultáneamente. Puede servir como una alternativa a 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 forbucle simple :

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

mmapadmite la notación de corte. Por ejemplo, mm[i:i+len]devuelve lenbytes del archivo comenzando en la posición i. El protocolo del administrador de contexto no es compatible antes de Python 3.2; necesita llamar mm.close()explícitamente en este caso. Iterar sobre cada byte usando mmapconsume más memoria que file.read(1), pero mmapes un orden de magnitud más rápido.

jfs
fuente
El último ejemplo me pareció muy interesante. Lástima que no haya numpymatrices equivalentes asignadas en memoria (byte).
Martineau
1
@martineau existe numpy.memmap()y puede obtener los datos de un byte a la vez (ctypes.data). Se podría pensar en matrices numpy como solo un poco más que blobs en memoria + metadatos.
jfs
jfs: Gracias, excelentes noticias! No sabía tal cosa que existía. Gran respuesta, por cierto.
martineau
25

Leer un archivo binario en Python y recorrer cada byte

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

import pathlib

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

Es interesante que esta sea la única respuesta para mencionar 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 sobre la memoria, lo fragmentaría, idiomáticamente, utilizando la iterfunción con la callable, sentinelfirma, 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 mencionan esto, pero pocas ofrecen un tamaño de lectura razonable).

La mejor práctica para archivos grandes o lectura almacenada 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:
            yield from chunk

Tenga en cuenta que usamos file.read1. file.readbloquea hasta que obtiene todos los bytes solicitados o EOF. file.read1nos permite evitar el bloqueo, y puede regresar más rápidamente 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 vamos a iterar sobre él y materializarlo 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 los 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 itere por líneas para archivos binarios

No haga lo siguiente: esto extrae un trozo de tamaño arbitrario hasta que llega a un carácter de nueva línea, 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
            yield from chunk

Lo anterior solo es bueno para lo que son archivos de texto legibles semánticamente humanos (como texto plano, código, marcado, marcado, etc., esencialmente cualquier cosa codificada en ASCII, UTF, latín, etc.) que debe abrir sin la 'b'bandera.

Aaron Hall
fuente
2
Esto es mucho mejor ... gracias por hacer esto. Sé que no siempre es divertido volver a una respuesta de dos años, pero agradezco que lo hayas hecho. Particularmente me gusta el subtítulo "No iterar por líneas" :-)
Floris
1
Hola Aaron, ¿hay alguna razón por la que elegiste usar en path = Path(path), with path.open('rb') as file:lugar de usar la función abierta incorporada? Ambos hacen lo mismo, ¿correcto?
Joshua Yonathan
1
@JoshuaYonathan Uso el Pathobjeto porque es una nueva forma muy conveniente de manejar rutas. En lugar de pasar una cadena a las funciones "correctas" cuidadosamente elegidas, simplemente podemos llamar a los métodos en el objeto de ruta, que esencialmente contiene la mayor parte de la funcionalidad importante que desea con lo que semánticamente es una cadena de ruta. Con IDEs que pueden inspeccionar, también podemos obtener más fácilmente el autocompletado. Podríamos lograr lo mismo con el openincorporado, pero hay muchas ventajas al escribir el programa para que el programador use el Pathobjeto en su lugar.
Aaron Hall
1
El último método que mencionó usando la función file_byte_iteratores mucho más rápido que todos los métodos que he probado en esta página. ¡Felicitaciones a usted!
Rick M.
@RickM: Quizás te interese el punto de referencia que acabo de publicar.
Martineau
19

Para resumir todos los puntos brillantes de chrispy, Skurmedel, Ben Hoyt y Peter Hansen, esta sería la solución óptima para procesar un archivo binario de un byte a la vez:

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

Para python versiones 2.6 y superiores, porque:

  • Python almacena internamente: no es necesario leer fragmentos
  • Principio DRY: no repita la línea de lectura
  • con declaración asegura un archivo limpio cerca
  • 'byte' se evalúa como falso cuando no hay más bytes (no cuando un byte es cero)

O use la solución JF Sebastians para mejorar la velocidad

from functools import partial

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

O si lo desea como una función generadora como lo demuestra codeape:

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)
Holger Bille
fuente
2
Como dice la respuesta vinculada, leer / procesar un byte a la vez todavía es lento en Python, incluso si las lecturas están almacenadas. El rendimiento se puede mejorar drásticamente si se pudieran procesar varios bytes a la vez, como en el ejemplo de la respuesta vinculada: 1.5GB / s vs. 7.5MB / s.
jfs
6

Python 3, lea todo el archivo a la vez:

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

Puedes iterar lo que quieras usando la datavariable.

Mircea
fuente
6

Después de probar todo lo anterior y usar la respuesta de @Aaron Hall, recibía errores de memoria para un archivo de ~ 90 Mb en una computadora con Windows 10, 8 Gb RAM y Python 3.5 de 32 bits. Un colega me recomendó usar numpyen su lugar y funciona de maravilla.

De lejos, el más rápido para leer un archivo binario completo (que he probado) es:

import numpy as np

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

Referencia

Multitudes más rápidas que cualquier otro método hasta ahora. Espero que ayude a alguien!

Rick M.
fuente
3
Agradable, pero no se puede usar en archivos binarios que contengan diferentes tipos de datos.
Nirmal
@Nirmal: La pregunta es sobre recorrer el byte de alcance, por lo que no está claro si su comentario sobre los diferentes tipos de datos tiene alguna relación.
Martineau
1
Rick: Su código no está haciendo lo mismo que los demás, es decir, recorrer cada byte. Si eso se agrega, no es más rápido que la mayoría de los demás, al menos según los resultados en mi punto de referencia . De hecho, parece ser uno de los enfoques más lentos. Si el procesamiento realizado en cada byte (lo que sea que sea) fue algo que podría hacerse a través de numpy, entonces podría valer la pena.
Martineau
@martineau Gracias por sus comentarios, sí, entiendo que la pregunta es sobre recorrer cada byte y no solo cargar todo de una vez, sino que hay otras respuestas en esta pregunta que también apuntan a leer todo el contenido y, por lo tanto, mi respuesta
Rick M.
4

Si tiene muchos datos binarios para leer, puede considerar el módulo de estructura . Está documentado como una conversión "entre tipos C y Python", pero por supuesto, los bytes son bytes, y si esos fueron creados como tipos C no importa. 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 structdocumentación):

>>> 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 hacer un bucle explícito sobre el contenido de un archivo.

gerrit
fuente
4

Esta publicación en sí no es una respuesta directa a la pregunta. En cambio, es un punto de referencia extensible basado en datos que se puede utilizar para comparar muchas de las respuestas (y las variaciones de la utilización de nuevas características agregadas en versiones posteriores, más modernas, de Python) que se han publicado en esta pregunta, y por lo tanto deberían Ser útil para determinar cuál tiene el mejor rendimiento.

En algunos casos, modifiqué el código en la respuesta referenciada para que sea compatible con el marco de referencia.

Primero, aquí están los resultados de lo que actualmente son las últimas versiones de Python 2 y 3:

Fastest to slowest execution speeds with 32-bit Python 2.7.16
  numpy version 1.16.5
  Test file size: 1,024 KiB
  100 executions, best of 3 repetitions

1                  Tcll (array.array) :   3.8943 secs, rel speed   1.00x,   0.00% slower (262.95 KiB/sec)
2  Vinay Sajip (read all into memory) :   4.1164 secs, rel speed   1.06x,   5.71% slower (248.76 KiB/sec)
3            codeape + iter + partial :   4.1616 secs, rel speed   1.07x,   6.87% slower (246.06 KiB/sec)
4                             codeape :   4.1889 secs, rel speed   1.08x,   7.57% slower (244.46 KiB/sec)
5               Vinay Sajip (chunked) :   4.1977 secs, rel speed   1.08x,   7.79% slower (243.94 KiB/sec)
6           Aaron Hall (Py 2 version) :   4.2417 secs, rel speed   1.09x,   8.92% slower (241.41 KiB/sec)
7                     gerrit (struct) :   4.2561 secs, rel speed   1.09x,   9.29% slower (240.59 KiB/sec)
8                     Rick M. (numpy) :   8.1398 secs, rel speed   2.09x, 109.02% slower (125.80 KiB/sec)
9                           Skurmedel :  31.3264 secs, rel speed   8.04x, 704.42% slower ( 32.69 KiB/sec)

Benchmark runtime (min:sec) - 03:26

Fastest to slowest execution speeds with 32-bit Python 3.8.0
  numpy version 1.17.4
  Test file size: 1,024 KiB
  100 executions, best of 3 repetitions

1  Vinay Sajip + "yield from" + "walrus operator" :   3.5235 secs, rel speed   1.00x,   0.00% slower (290.62 KiB/sec)
2                       Aaron Hall + "yield from" :   3.5284 secs, rel speed   1.00x,   0.14% slower (290.22 KiB/sec)
3         codeape + iter + partial + "yield from" :   3.5303 secs, rel speed   1.00x,   0.19% slower (290.06 KiB/sec)
4                      Vinay Sajip + "yield from" :   3.5312 secs, rel speed   1.00x,   0.22% slower (289.99 KiB/sec)
5      codeape + "yield from" + "walrus operator" :   3.5370 secs, rel speed   1.00x,   0.38% slower (289.51 KiB/sec)
6                          codeape + "yield from" :   3.5390 secs, rel speed   1.00x,   0.44% slower (289.35 KiB/sec)
7                                      jfs (mmap) :   4.0612 secs, rel speed   1.15x,  15.26% slower (252.14 KiB/sec)
8              Vinay Sajip (read all into memory) :   4.5948 secs, rel speed   1.30x,  30.40% slower (222.86 KiB/sec)
9                        codeape + iter + partial :   4.5994 secs, rel speed   1.31x,  30.54% slower (222.64 KiB/sec)
10                                        codeape :   4.5995 secs, rel speed   1.31x,  30.54% slower (222.63 KiB/sec)
11                          Vinay Sajip (chunked) :   4.6110 secs, rel speed   1.31x,  30.87% slower (222.08 KiB/sec)
12                      Aaron Hall (Py 2 version) :   4.6292 secs, rel speed   1.31x,  31.38% slower (221.20 KiB/sec)
13                             Tcll (array.array) :   4.8627 secs, rel speed   1.38x,  38.01% slower (210.58 KiB/sec)
14                                gerrit (struct) :   5.0816 secs, rel speed   1.44x,  44.22% slower (201.51 KiB/sec)
15                 Rick M. (numpy) + "yield from" :  11.8084 secs, rel speed   3.35x, 235.13% slower ( 86.72 KiB/sec)
16                                      Skurmedel :  11.8806 secs, rel speed   3.37x, 237.18% slower ( 86.19 KiB/sec)
17                                Rick M. (numpy) :  13.3860 secs, rel speed   3.80x, 279.91% slower ( 76.50 KiB/sec)

Benchmark runtime (min:sec) - 04:47

También lo ejecuté con un archivo de prueba de 10 MiB mucho más grande (que tardó casi una hora en ejecutarse) y obtuve resultados de rendimiento que eran comparables a los mostrados anteriormente.

Aquí está el código utilizado para hacer la evaluación comparativa:

from __future__ import print_function
import array
import atexit
from collections import deque, namedtuple
import io
from mmap import ACCESS_READ, mmap
import numpy as np
from operator import attrgetter
import os
import random
import struct
import sys
import tempfile
from textwrap import dedent
import time
import timeit
import traceback

try:
    xrange
except NameError:  # Python 3
    xrange = range


class KiB(int):
    """ KibiBytes - multiples of the byte units for quantities of information. """
    def __new__(self, value=0):
        return 1024*value


BIG_TEST_FILE = 1  # MiBs or 0 for a small file.
SML_TEST_FILE = KiB(64)
EXECUTIONS = 100  # Number of times each "algorithm" is executed per timing run.
TIMINGS = 3  # Number of timing runs.
CHUNK_SIZE = KiB(8)
if BIG_TEST_FILE:
    FILE_SIZE = KiB(1024) * BIG_TEST_FILE
else:
    FILE_SIZE = SML_TEST_FILE  # For quicker testing.

# Common setup for all algorithms -- prefixed to each algorithm's setup.
COMMON_SETUP = dedent("""
    # Make accessible in algorithms.
    from __main__ import array, deque, get_buffer_size, mmap, np, struct
    from __main__ import ACCESS_READ, CHUNK_SIZE, FILE_SIZE, TEMP_FILENAME
    from functools import partial
    try:
        xrange
    except NameError:  # Python 3
        xrange = range
""")


def get_buffer_size(path):
    """ Determine optimal buffer size for reading files. """
    st = os.stat(path)
    try:
        bufsize = st.st_blksize # Available on some Unix systems (like Linux)
    except AttributeError:
        bufsize = io.DEFAULT_BUFFER_SIZE
    return bufsize

# Utility primarily for use when embedding additional algorithms into benchmark.
VERIFY_NUM_READ = """
    # Verify generator reads correct number of bytes (assumes values are correct).
    bytes_read = sum(1 for _ in file_byte_iterator(TEMP_FILENAME))
    assert bytes_read == FILE_SIZE, \
           'Wrong number of bytes generated: got {:,} instead of {:,}'.format(
                bytes_read, FILE_SIZE)
"""

TIMING = namedtuple('TIMING', 'label, exec_time')

class Algorithm(namedtuple('CodeFragments', 'setup, test')):

    # Default timeit "stmt" code fragment.
    _TEST = """
        #for b in file_byte_iterator(TEMP_FILENAME):  # Loop over every byte.
        #    pass  # Do stuff with byte...
        deque(file_byte_iterator(TEMP_FILENAME), maxlen=0)  # Data sink.
    """

    # Must overload __new__ because (named)tuples are immutable.
    def __new__(cls, setup, test=None):
        """ Dedent (unindent) code fragment string arguments.
        Args:
          `setup` -- Code fragment that defines things used by `test` code.
                     In this case it should define a generator function named
                     `file_byte_iterator()` that will be passed that name of a test file
                     of binary data. This code is not timed.
          `test` -- Code fragment that uses things defined in `setup` code.
                    Defaults to _TEST. This is the code that's timed.
        """
        test =  cls._TEST if test is None else test  # Use default unless one is provided.

        # Uncomment to replace all performance tests with one that verifies the correct
        # number of bytes values are being generated by the file_byte_iterator function.
        #test = VERIFY_NUM_READ

        return tuple.__new__(cls, (dedent(setup), dedent(test)))


algorithms = {

    'Aaron Hall (Py 2 version)': Algorithm("""
        def file_byte_iterator(path):
            with open(path, "rb") as file:
                callable = partial(file.read, 1024)
                sentinel = bytes() # or b''
                for chunk in iter(callable, sentinel):
                    for byte in chunk:
                        yield byte
    """),

    "codeape": Algorithm("""
        def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
            with open(filename, "rb") as f:
                while True:
                    chunk = f.read(chunksize)
                    if chunk:
                        for b in chunk:
                            yield b
                    else:
                        break
    """),

    "codeape + iter + partial": Algorithm("""
        def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
            with open(filename, "rb") as f:
                for chunk in iter(partial(f.read, chunksize), b''):
                    for b in chunk:
                        yield b
    """),

    "gerrit (struct)": Algorithm("""
        def file_byte_iterator(filename):
            with open(filename, "rb") as f:
                fmt = '{}B'.format(FILE_SIZE)  # Reads entire file at once.
                for b in struct.unpack(fmt, f.read()):
                    yield b
    """),

    'Rick M. (numpy)': Algorithm("""
        def file_byte_iterator(filename):
            for byte in np.fromfile(filename, 'u1'):
                yield byte
    """),

    "Skurmedel": Algorithm("""
        def file_byte_iterator(filename):
            with open(filename, "rb") as f:
                byte = f.read(1)
                while byte:
                    yield byte
                    byte = f.read(1)
    """),

    "Tcll (array.array)": Algorithm("""
        def file_byte_iterator(filename):
            with open(filename, "rb") as f:
                arr = array.array('B')
                arr.fromfile(f, FILE_SIZE)  # Reads entire file at once.
                for b in arr:
                    yield b
    """),

    "Vinay Sajip (read all into memory)": Algorithm("""
        def file_byte_iterator(filename):
            with open(filename, "rb") as f:
                bytes_read = f.read()  # Reads entire file at once.
            for b in bytes_read:
                yield b
    """),

    "Vinay Sajip (chunked)": Algorithm("""
        def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
            with open(filename, "rb") as f:
                chunk = f.read(chunksize)
                while chunk:
                    for b in chunk:
                        yield b
                    chunk = f.read(chunksize)
    """),

}  # End algorithms

#
# Versions of algorithms that will only work in certain releases (or better) of Python.
#
if sys.version_info >= (3, 3):
    algorithms.update({

        'codeape + iter + partial + "yield from"': Algorithm("""
            def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
                with open(filename, "rb") as f:
                    for chunk in iter(partial(f.read, chunksize), b''):
                        yield from chunk
        """),

        'codeape + "yield from"': Algorithm("""
            def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
                with open(filename, "rb") as f:
                    while True:
                        chunk = f.read(chunksize)
                        if chunk:
                            yield from chunk
                        else:
                            break
        """),

        "jfs (mmap)": Algorithm("""
            def file_byte_iterator(filename):
                with open(filename, "rb") as f, \
                     mmap(f.fileno(), 0, access=ACCESS_READ) as s:
                    yield from s
        """),

        'Rick M. (numpy) + "yield from"': Algorithm("""
            def file_byte_iterator(filename):
            #    data = np.fromfile(filename, 'u1')
                yield from np.fromfile(filename, 'u1')
        """),

        'Vinay Sajip + "yield from"': Algorithm("""
            def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
                with open(filename, "rb") as f:
                    chunk = f.read(chunksize)
                    while chunk:
                        yield from chunk  # Added in Py 3.3
                        chunk = f.read(chunksize)
        """),

    })  # End Python 3.3 update.

if sys.version_info >= (3, 5):
    algorithms.update({

        'Aaron Hall + "yield from"': Algorithm("""
            from pathlib import Path

            def file_byte_iterator(path):
                ''' Given a path, return an iterator over the file
                    that lazily loads the file.
                '''
                path = Path(path)
                bufsize = get_buffer_size(path)

                with path.open('rb') as file:
                    reader = partial(file.read1, bufsize)
                    for chunk in iter(reader, bytes()):
                        yield from chunk
        """),

    })  # End Python 3.5 update.

if sys.version_info >= (3, 8, 0):
    algorithms.update({

        'Vinay Sajip + "yield from" + "walrus operator"': Algorithm("""
            def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
                with open(filename, "rb") as f:
                    while chunk := f.read(chunksize):
                        yield from chunk  # Added in Py 3.3
        """),

        'codeape + "yield from" + "walrus operator"': Algorithm("""
            def file_byte_iterator(filename, chunksize=CHUNK_SIZE):
                with open(filename, "rb") as f:
                    while chunk := f.read(chunksize):
                        yield from chunk
        """),

    })  # End Python 3.8.0 update.update.


#### Main ####

def main():
    global TEMP_FILENAME

    def cleanup():
        """ Clean up after testing is completed. """
        try:
            os.remove(TEMP_FILENAME)  # Delete the temporary file.
        except Exception:
            pass

    atexit.register(cleanup)

    # Create a named temporary binary file of pseudo-random bytes for testing.
    fd, TEMP_FILENAME = tempfile.mkstemp('.bin')
    with os.fdopen(fd, 'wb') as file:
         os.write(fd, bytearray(random.randrange(256) for _ in range(FILE_SIZE)))

    # Execute and time each algorithm, gather results.
    start_time = time.time()  # To determine how long testing itself takes.

    timings = []
    for label in algorithms:
        try:
            timing = TIMING(label,
                            min(timeit.repeat(algorithms[label].test,
                                              setup=COMMON_SETUP + algorithms[label].setup,
                                              repeat=TIMINGS, number=EXECUTIONS)))
        except Exception as exc:
            print('{} occurred timing the algorithm: "{}"\n  {}'.format(
                    type(exc).__name__, label, exc))
            traceback.print_exc(file=sys.stdout)  # Redirect to stdout.
            sys.exit(1)
        timings.append(timing)

    # Report results.
    print('Fastest to slowest execution speeds with {}-bit Python {}.{}.{}'.format(
            64 if sys.maxsize > 2**32 else 32, *sys.version_info[:3]))
    print('  numpy version {}'.format(np.version.full_version))
    print('  Test file size: {:,} KiB'.format(FILE_SIZE // KiB(1)))
    print('  {:,d} executions, best of {:d} repetitions'.format(EXECUTIONS, TIMINGS))
    print()

    longest = max(len(timing.label) for timing in timings)  # Len of longest identifier.
    ranked = sorted(timings, key=attrgetter('exec_time')) # Sort so fastest is first.
    fastest = ranked[0].exec_time
    for rank, timing in enumerate(ranked, 1):
        print('{:<2d} {:>{width}} : {:8.4f} secs, rel speed {:6.2f}x, {:6.2f}% slower '
              '({:6.2f} KiB/sec)'.format(
                    rank,
                    timing.label, timing.exec_time, round(timing.exec_time/fastest, 2),
                    round((timing.exec_time/fastest - 1) * 100, 2),
                    (FILE_SIZE/timing.exec_time) / KiB(1),  # per sec.
                    width=longest))
    print()
    mins, secs = divmod(time.time()-start_time, 60)
    print('Benchmark runtime (min:sec) - {:02d}:{:02d}'.format(int(mins),
                                                               int(round(secs))))

main()
Martineau
fuente
¿Estás asumiendo que lo hago en su yield from chunklugar for byte in chunk: yield byte? Estoy pensando que debería ajustar mi respuesta con eso.
Aaron Hall
@Aaron: Hay dos versiones que su respuesta en los resultados de Python 3 y una de ellas usa yield from.
Martineau
Ok, he actualizado mi respuesta. También le sugiero que descarte, enumerateya que debe entenderse que la iteración se completa; si no, la última vez que lo verifiqué, enumerate tiene un poco de sobrecarga con los costos sobre la contabilidad para el índice con + = 1, por lo que alternativamente puede hacer la contabilidad en su código propio O incluso pasar a una deque con maxlen=0.
Aaron Hall
@ Aaron: De acuerdo sobre el enumerate. Gracias por la respuesta. Agregaré una actualización a mi publicación que no la tiene (aunque no creo que cambie mucho los resultados). También agregará la numpyrespuesta basada en @Rick M.
Martineau
Un poco más de revisión de código: no creo que tenga sentido escribir respuestas a Python 2 en este punto; consideraría eliminar Python 2, ya que esperaría que use Python 3.7 o 3.8 de 64 bits. Puede configurar la limpieza al final con atexit y una aplicación parcial. Error tipográfico: "verificar". No veo sentido en la duplicación de las cadenas de prueba: ¿son diferentes? Me imagino que si usa en super().lugar de tuple.en su __new__podría usar los namedtuplenombres de los atributos en lugar de los índices.
Aaron Hall
3

Si está buscando algo rápido, aquí hay un método que he estado usando que funcionó durante años:

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)

si desea iterar caracteres en lugar de ints, simplemente puede usar data = file.read(), que debería ser un objeto bytes () en py3.

Tcll
fuente
1
'array' es importado por 'from array import array'
quanly_mc
@quanly_mc sí, gracias por captar eso, y lo siento, olvidé incluir eso, editando ahora.
Tcll