Lecture d'un fichier binaire et boucle sur chaque octet

377

En Python, comment lire un fichier binaire et boucler sur chaque octet de ce fichier?

Jesse Vogt
la source

Réponses:

387

Python 2.4 et versions antérieures

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)

Notez que l'instruction with n'est pas disponible dans les versions de Python inférieures à 2.5. Pour l'utiliser dans la version 2.5, vous devrez l'importer:

from __future__ import with_statement

En 2.6, cela n'est pas nécessaire.

Python 3

En Python 3, c'est un peu différent. Nous n'obtiendrons plus les caractères bruts du flux en mode octet mais les objets octets, nous devons donc modifier la condition:

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

Ou, comme le dit Benhoyt, sautez l'inégal et profitez du fait que la valeur est b""fausse. Cela rend le code compatible entre 2.6 et 3.x sans aucune modification. Cela vous éviterait également de changer la condition si vous passez du mode octet au texte ou inversement.

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

python 3.8

Désormais grâce à: = opérateur le code ci-dessus peut être écrit de manière plus courte.

with open("myfile", "rb") as f:
    while (byte := f.read(1)):
        # Do stuff with byte.
Skurmedel
la source
40
La lecture d'un fichier octet est un cauchemar de performance. Ce ne peut pas être la meilleure solution disponible en python. Ce code doit être utilisé avec précaution.
usr
7
@usr: Eh bien, les objets fichier sont tamponnés en interne, et c'est pourtant ce qui a été demandé. Tous les scripts n'ont pas besoin de performances optimales.
Skurmedel
4
@mezhaka: Donc, vous passez de read (1) à read (bufsize) et dans la boucle while vous faites un for-in ... l'exemple est toujours là.
Skurmedel
3
@usr: la différence de performances peut être jusqu'à 200 fois supérieure pour le code que j'ai essayé .
jfs
2
@usr - cela dépend du nombre d'octets que vous souhaitez traiter. S'ils sont assez peu nombreux, un code «mal» performant mais facilement compréhensible peut être bien préféré. Le gaspillage des cycles CPU est compensé pour économiser les "cycles CPU du lecteur" lors de la maintenance du code.
IllvilJa
172

Ce générateur produit des octets à partir d'un fichier, en lisant le fichier par morceaux:

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)

Consultez la documentation Python pour plus d'informations sur les itérateurs et les générateurs .

codeape
la source
3
@codeape Exactement ce que je recherche. Mais, comment déterminez-vous la taille des morceaux? Peut-il s'agir d'une valeur arbitraire?
swdev
3
@swdev: l'exemple utilise une taille de bloc de 8192 octets . Le paramètre de la fonction file.read () - spécifie simplement la taille, c'est-à-dire le nombre d'octets à lire. codeape a choisi 8192 Byte = 8 kB(en fait c'est KiBmais ce n'est pas aussi connu). La valeur est "totalement" aléatoire mais 8 Ko semble être une valeur appropriée: pas trop de mémoire est gaspillée et il n'y a toujours pas "trop" d'opérations de lecture comme dans la réponse acceptée par Skurmedel ...
mozzbozz
3
Le système de fichiers met déjà en mémoire tampon des morceaux de données, donc ce code est redondant. Il vaut mieux lire un octet à la fois.
Stark
17
Bien que déjà plus rapide que la réponse acceptée, cela pourrait être accéléré de 20 à 25% supplémentaires en remplaçant la for b in chunk:boucle la plus intérieure par yield from chunk. Cette forme de a yieldété ajoutée dans Python 3.3 (voir Expressions de rendement ).
martineau
3
Hmm semble peu probable, lien?
codeape
54

Si le fichier n'est pas trop gros, le conserver en mémoire est un problème:

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

où process_byte représente une opération que vous souhaitez effectuer sur l'octet transmis.

Si vous souhaitez traiter un morceau à la fois:

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)

L' withinstruction est disponible en Python 2.5 et supérieur.

Vinay Sajip
la source
1
Vous pourriez être intéressé par l' indice de référence que je viens de publier.
martineau
37

Pour lire un fichier - un octet à la fois (en ignorant la mise en mémoire tampon) - vous pouvez utiliser la fonction intégrée à deux argumentsiter(callable, sentinel) :

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

Il appelle file.read(1)jusqu'à ce qu'il ne retourne rien b''(bytestring vide). La mémoire ne devient pas illimitée pour les fichiers volumineux. Vous pouvez passer buffering=0 à open(), pour désactiver la mise en mémoire tampon - cela garantit qu'un seul octet est lu par itération (lent).

with-statement ferme le fichier automatiquement - y compris le cas où le code en dessous déclenche une exception.

Malgré la présence de tampons internes par défaut, il est toujours inefficace de traiter un octet à la fois. Par exemple, voici l' blackhole.pyutilitaire qui mange tout ce qui lui est donné:

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

Exemple:

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

Il traite ~ 1,5 Go / s lorsque chunksize == 32768sur ma machine et seulement ~ 7,5 Mo / s quand chunksize == 1. Autrement dit, il est 200 fois plus lent à lire un octet à la fois. Tenez-en compte si vous pouvez réécrire votre traitement pour utiliser plusieurs octets à la fois et si vous avez besoin de performances.

mmapvous permet de traiter un fichier comme un bytearrayet un objet fichier simultanément. Il peut servir d'alternative au chargement de l'ensemble du fichier en mémoire si vous avez besoin d'accéder aux deux interfaces. En particulier, vous pouvez parcourir un octet à la fois sur un fichier mappé en mémoire en utilisant simplement une forboucle 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

mmapprend en charge la notation de tranche. Par exemple, mm[i:i+len]retourne des lenoctets du fichier à partir de la position i. Le protocole du gestionnaire de contexte n'est pas pris en charge avant Python 3.2; vous devez appeler mm.close()explicitement dans ce cas. L'itération sur chaque octet en utilisant mmapconsomme plus de mémoire que file.read(1), mais mmapest plus rapide d'un ordre de grandeur.

jfs
la source
J'ai trouvé le dernier exemple très intéressant. Dommage qu'il n'y ait pas de numpytableaux (octets) mappés en mémoire équivalents .
martineau
1
@martineau existe numpy.memmap()et vous pouvez obtenir les données un octet à la fois (ctypes.data). Vous pourriez penser que les tableaux numpy sont juste un peu plus que des blobs en mémoire + métadonnées.
jfs
jfs: Merci, excellente nouvelle! Je ne savais pas qu'une telle chose existait. Excellente réponse, BTW.
martineau
25

Lecture d'un fichier binaire en Python et boucle sur chaque octet

Nouveau dans Python 3.5 est le pathlibmodule, qui a une méthode pratique spécifiquement pour lire dans un fichier en octets, nous permettant d'itérer sur les octets. Je considère que c'est une réponse décente (si rapide et sale):

import pathlib

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

Il est intéressant de noter que c'est la seule réponse à mentionner pathlib.

En Python 2, vous feriez probablement cela (comme le suggère également Vinay Sajip):

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

Dans le cas où le fichier peut être trop volumineux pour itérer sur la mémoire, vous le découpez idiomatiquement, en utilisant la iterfonction avec la callable, sentinelsignature - la version 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)

(Plusieurs autres réponses le mentionnent, mais peu offrent une taille de lecture raisonnable.)

Meilleure pratique pour les fichiers volumineux ou la lecture en mémoire tampon / interactive

Créons une fonction pour ce faire, y compris des utilisations idiomatiques de la bibliothèque standard pour 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

Notez que nous utilisons file.read1. file.readbloque jusqu'à ce qu'il obtienne tous les octets demandés ou EOF. file.read1nous permet d'éviter le blocage, et il peut revenir plus rapidement à cause de cela. Aucune autre réponse ne le mentionne également.

Démonstration de l'utilisation des meilleures pratiques:

Faisons un fichier avec un mégaoctet (en fait mégaoctet) de données pseudo-aléatoires:

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

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

Maintenant, parcourons-le et matérialisons-le en mémoire:

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

Nous pouvons inspecter n'importe quelle partie des données, par exemple, les 100 derniers et les 100 premiers octets:

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

Ne pas parcourir les lignes pour les fichiers binaires

Ne faites pas ce qui suit - cela tire un morceau de taille arbitraire jusqu'à ce qu'il atteigne un caractère de nouvelle ligne - trop lent lorsque les morceaux sont trop petits, et peut-être aussi trop gros:

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

Ce qui précède n'est bon que pour les fichiers texte lisibles sémantiquement (comme le texte brut, le code, le balisage, le démarquage, etc. ... essentiellement tout ce qui est encodé en ascii, utf, latin, etc ...) que vous devez ouvrir sans le 'b'drapeau.

Aaron Hall
la source
2
C'est tellement mieux ... merci de l'avoir fait. Je sais que ce n'est pas toujours amusant de revenir à une réponse vieille de deux ans, mais je vous remercie de l'avoir fait. J'aime particulièrement la sous-rubrique "Ne pas répéter par lignes" :-)
Floris
1
Salut Aaron, y a-t-il une raison pour laquelle vous avez choisi d'utiliser path = Path(path), with path.open('rb') as file:plutôt que d'utiliser la fonction ouverte intégrée à la place? Ils font tous les deux la même chose, n'est-ce pas?
Joshua Yonathan
1
@JoshuaYonathan J'utilise l' Pathobjet car c'est une nouvelle façon très pratique de gérer les chemins. Au lieu de passer une chaîne dans les fonctions "à droite" soigneusement choisies, nous pouvons simplement appeler les méthodes sur l'objet chemin, qui contient essentiellement la plupart des fonctionnalités importantes que vous souhaitez avec ce qui est sémantiquement une chaîne chemin. Avec les IDE qui peuvent inspecter, nous pouvons également obtenir plus facilement la saisie semi-automatique. Nous pourrions accomplir la même chose avec le programme openintégré, mais il y a beaucoup d'avantages lors de l'écriture du programme pour que le programmeur utilise l' Pathobjet à la place.
Aaron Hall
1
La dernière méthode que vous avez mentionnée en utilisant la fonction file_byte_iteratorest beaucoup plus rapide que toutes les méthodes que j'ai essayées sur cette page. Bravo à vous!
Rick M.
@RickM: Vous pourriez être intéressé par le benchmark que je viens de publier.
martineau
19

Pour résumer tous les points brillants de chrispy, Skurmedel, Ben Hoyt et Peter Hansen, ce serait la solution optimale pour traiter un fichier binaire un octet à la fois:

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

Pour les versions python 2.6 et supérieures, car:

  • tampons python en interne - pas besoin de lire des morceaux
  • Principe DRY - ne pas répéter la ligne de lecture
  • avec déclaration assure une fermeture de fichier propre
  • 'byte' a la valeur false lorsqu'il n'y a plus d'octets (pas quand un octet est nul)

Ou utilisez la solution JF Sebastians pour une vitesse améliorée

from functools import partial

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

Ou si vous le souhaitez en tant que fonction de générateur comme démontré par 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
la source
2
Comme le dit la réponse liée, la lecture / le traitement d'un octet à la fois est toujours lent en Python même si les lectures sont mises en mémoire tampon. Les performances peuvent être considérablement améliorées si plusieurs octets à la fois peuvent être traités comme dans l'exemple de la réponse liée: 1,5 Go / s contre 7,5 Mo / s.
jfs
6

Python 3, lisez tout le fichier à la fois:

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

Vous pouvez répéter ce que vous voulez en utilisant datavariable.

Mircea
la source
6

Après avoir essayé tout ce qui précède et utilisé la réponse de @Aaron Hall, j'obtenais des erreurs de mémoire pour un fichier de ~ 90 Mo sur un ordinateur exécutant Windows 10, 8 Go de RAM et Python 3.5 32 bits. Un collègue m'a recommandé d'utilisernumpy place et cela fonctionne à merveille.

De loin, le plus rapide pour lire un fichier binaire entier (que j'ai testé) est:

import numpy as np

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

Référence

Multitudes plus rapides que toutes les autres méthodes jusqu'à présent. J'espère que cela aide quelqu'un!

Rick M.
la source
3
Bien, mais ne peut pas être utilisé sur un fichier binaire contenant différents types de données.
Nirmal
@Nirmal: La question concerne le bouclage sur l'octet de portée, il n'est donc pas clair si votre commentaire sur différents types de données a une incidence.
martineau
1
Rick: Votre code ne fait pas tout à fait la même chose que les autres - à savoir boucler sur chaque octet. Si on y ajoute, ce n'est pas plus rapide que la majorité des autres selon au moins selon les résultats de mon benchmark . En fait, il semble que ce soit l'une des approches les plus lentes. Si le traitement effectué pour chaque octet (quel qu'il soit) pouvait être effectué via numpy, cela pourrait valoir la peine.
martineau
@martineau Merci pour vos commentaires, oui je comprends que la question concerne le bouclage sur chaque octet et pas seulement le chargement de tout en une seule fois, mais il y a d'autres réponses dans cette question qui pointent également vers la lecture de tout le contenu et donc ma réponse
Rick M.
4

Si vous avez beaucoup de données binaires à lire, vous pouvez envisager le module struct . Il est documenté comme convertissant "entre les types C et Python", mais bien sûr, les octets sont des octets, et peu importe si ceux-ci ont été créés en tant que types C. Par exemple, si vos données binaires contiennent deux entiers de 2 octets et un entier de 4 octets, vous pouvez les lire comme suit (exemple tiré de la structdocumentation):

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

Vous trouverez peut-être cela plus pratique, plus rapide ou les deux que d'effectuer une boucle explicite sur le contenu d'un fichier.

gerrit
la source
4

Ce message lui-même n'est pas une réponse directe à la question. Il s'agit plutôt d'un référentiel extensible basé sur les données qui peut être utilisé pour comparer de nombreuses réponses (et des variantes d'utilisation de nouvelles fonctionnalités ajoutées dans des versions plus récentes et plus modernes de Python) qui ont été publiées sur cette question - et devraient donc être utile pour déterminer laquelle a les meilleures performances.

Dans quelques cas, j'ai modifié le code dans la réponse référencée pour le rendre compatible avec le framework de référence.

Tout d'abord, voici les résultats pour ce qui sont actuellement les dernières versions de Python 2 & 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

Je l'ai également exécuté avec un fichier de test de 10 Mio beaucoup plus volumineux (qui a pris près d'une heure à fonctionner) et j'ai obtenu des résultats de performance comparables à ceux indiqués ci-dessus.

Voici le code utilisé pour effectuer l'analyse comparative:

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
la source
Supposez-vous que je le fasse à la yield from chunkplace for byte in chunk: yield byte? Je pense que je devrais resserrer ma réponse avec ça.
Aaron Hall
@Aaron: Il existe deux versions de votre réponse dans les résultats Python 3 et l'une d'elles utilise yield from.
martineau
ok, j'ai mis à jour ma réponse. Je vous suggère également de laisser tomber enumeratecar l'itération doit être comprise comme terminée - sinon, j'ai vérifié en dernier - énumérer a un peu de frais généraux avec les coûts de la comptabilité pour l'index avec + = 1, vous pouvez donc alternativement faire la comptabilité dans votre propre code. Ou même passer à un deque avec maxlen=0.
Aaron Hall
@Aaron: D'accord sur le enumerate. Merci pour les commentaires. Ajoutera une mise à jour à mon message qui ne l'a pas (bien que je ne pense pas que cela change beaucoup les résultats). Ajoutera également la numpyréponse basée sur @Rick M.
martineau
Un peu plus de révision de code: je ne pense pas que cela ait du sens d'écrire des réponses à Python 2 à ce stade - j'envisagerais de supprimer Python 2 car je m'attendrais à ce que vous utilisiez Python 3.7 ou 3.8 64 bits. Vous pouvez définir le nettoyage pour qu'il se termine à la fin avec atexit et une application partielle. Typo: "vérifier". Je ne vois aucun sens dans la duplication des chaînes de test - sont-elles du tout différentes? J'imagine que si vous utilisez super().au lieu de tuple.dans votre, __new__vous pourriez utiliser les namedtuplenoms d'attribut au lieu d'index.
Aaron Hall
3

si vous cherchez quelque chose de rapide, voici une méthode que j'utilise et qui fonctionne depuis des années:

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 vous voulez itérer les caractères au lieu des entiers, vous pouvez simplement utiliser data = file.read(), qui devrait être un objet bytes () dans py3.

Tcll
la source
1
'array' est importé par 'from array import array'
quanly_mc
@quanly_mc oui, merci d'avoir attrapé cela, et désolé d'avoir oublié de l'inclure, de le modifier maintenant.
Tcll