[python] Lecture du fichier binaire et bouclage sur chaque octet


Answers

Ce générateur génère des octets à partir d'un fichier, en lisant le fichier en 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)

Voir la documentation Python pour plus d'informations sur les iterators et les generators .

Question

En Python, comment lire un fichier binaire et faire une boucle sur chaque octet de ce fichier?




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 si ceux-ci ont été créés en tant que types C n'a pas d'importance. 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 documentation de la struct ):

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

Vous pourriez trouver cela plus pratique, plus rapide, ou les deux, que de boucler explicitement le contenu d'un fichier.




Lecture du fichier binaire en Python et bouclage sur chaque octet

Nouveau dans Python 3.5 est le module pathlib , qui a une méthode de commodité spécifique pour lire dans un fichier en octets, ce qui nous permet d'itérer sur les octets. Je considère que c'est une réponse correcte (si rapide et sale):

import pathlib

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

Intéressant que c'est la seule réponse à mentionner pathlib .

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

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

Dans le cas où le fichier pourrait être trop volumineux pour itérer en mémoire, vous le morceleriez, idiomatiquement, en utilisant la fonction iter avec la signature callable, sentinel 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 tamponnée / interactive

Créons une fonction pour cela, y compris les 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:
            for byte in chunk:
                yield byte

Notez que nous utilisons file.read1 . file.read bloque jusqu'à obtenir tous les octets demandés ou EOF . file.read1 nous permet d'éviter le blocage, et il peut revenir plus rapidement à cause de cela. Aucune autre réponse ne le mentionne aussi.

Démonstration de l'utilisation des meilleures pratiques:

Faisons un fichier avec un mégaoctet (en fait un mébioctet) 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)))

Passons maintenant dessus 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 itérer par des lignes pour les fichiers binaires

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

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

Ce qui précède est seulement bon pour ce qui sont des fichiers texte sémantiquement lisibles par l'homme (comme le texte brut, le code, le balisage, la démarque etc ... essentiellement tout ce qui est encodé en ascii, utf, latin, etc ...).




Pour lire un fichier - un octet à la fois (en ignorant la mise en mémoire tampon) - vous pouvez utiliser la fonction intégrée iter(callable, sentinel) deux arguments iter(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 renvoie 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 - il garantit que seul un octet est lu par itération (lent).

with -statement ferme le fichier automatiquement - y compris le cas où le code en dessous soulève une exception.

Malgré la présence de la mise en mémoire tampon interne par défaut, il est toujours inefficace de traiter un octet à la fois. Par exemple, voici l'utilitaire blackhole.py 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 GB / s quand chunksize == 32768 sur ma machine et seulement ~ 7.5 MB / s quand chunksize == 1 . Autrement dit, il est 200 fois plus lent de lire un octet à la fois. Prenez en compte si vous pouvez réécrire votre traitement pour utiliser plus d'un octet à la fois et si vous avez besoin de performance.

mmap vous permet de traiter un fichier comme un bytearray et un objet fichier simultanément. Il peut servir d'alternative au chargement du fichier entier en mémoire si vous avez besoin d'accéder aux deux interfaces. En particulier, vous pouvez itérer un octet à la fois sur un fichier mappé en mémoire en utilisant simplement un point -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 supporte la notation de tranche. Par exemple, mm[i:i+len] renvoie len octets du fichier à partir de la position i . Le protocole du gestionnaire de contexte n'est pas supporté avant Python 3.2; vous devez appeler mm.close() explicitement dans ce cas. Itérer sur chaque octet en utilisant mmap consomme plus de mémoire que file.read(1) , mais mmap est un ordre de grandeur plus rapide.




Related