Binärdatei lesen und jedes Byte durchlaufen

377

Wie lese ich in Python eine Binärdatei ein und durchlaufe jedes Byte dieser Datei?

Jesse Vogt
quelle

Antworten:

387

Python 2.4 und früher

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)

Beachten Sie, dass die with-Anweisung in Python-Versionen unter 2.5 nicht verfügbar ist. Um es in Version 2.5 zu verwenden, müssen Sie es importieren:

from __future__ import with_statement

In 2.6 wird dies nicht benötigt.

Python 3

In Python 3 ist das etwas anders. Wir erhalten im Byte-Modus keine Rohzeichen mehr aus dem Stream, sondern Byte-Objekte. Daher müssen wir die Bedingung ändern:

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

Oder überspringen Sie, wie benhoyt sagt, das Ungleiche und nutzen Sie die Tatsache, dass es sich b""um falsch handelt. Dadurch ist der Code ohne Änderungen zwischen 2.6 und 3.x kompatibel. Es würde Sie auch davor bewahren, die Bedingung zu ändern, wenn Sie vom Bytemodus zum Text oder umgekehrt wechseln.

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

Python 3.8

Dank: = operator kann der obige Code von nun an kürzer geschrieben werden.

with open("myfile", "rb") as f:
    while (byte := f.read(1)):
        # Do stuff with byte.
Skurmedel
quelle
40
Das byteweise Lesen einer Datei ist ein Leistungsalptraum. Dies kann nicht die beste in Python verfügbare Lösung sein. Dieser Code sollte mit Vorsicht verwendet werden.
usr
7
@usr: Nun, die Dateiobjekte werden intern gepuffert, und trotzdem wurde danach gefragt. Nicht jedes Skript benötigt eine optimale Leistung.
Skurmedel
4
@mezhaka: Also änderst du es von read (1) in read (bufsize) und in der while-Schleife machst du ein For-In ... das Beispiel steht noch.
Skurmedel
3
@usr: Der Leistungsunterschied kann für den Code, den ich ausprobiert habe, bis zu 200 Mal betragen .
JFS
2
@usr - es hängt davon ab, wie viele Bytes Sie verarbeiten möchten. Wenn es nur wenige gibt, kann "schlecht" funktionierender, aber leicht verständlicher Code sehr bevorzugt werden. Die Verschwendung von CPU-Zyklen wird kompensiert, um "Leser-CPU-Zyklen" bei der Pflege des Codes zu sparen.
IllvilJa
172

Dieser Generator liefert Bytes aus einer Datei und liest die Datei in Blöcken:

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)

Informationen zu Iteratoren und Generatoren finden Sie in der Python-Dokumentation .

Codeape
quelle
3
@codeape Genau das, wonach ich suche. Aber wie bestimmen Sie die Blockgröße? Kann es ein beliebiger Wert sein?
Swdev
3
@swdev: Das Beispiel verwendet eine Blockgröße von 8192 Bytes . Der Parameter für die Funktion file.read () - gibt einfach die Größe an, dh die Anzahl der zu lesenden Bytes. Codeape wählte 8192 Byte = 8 kB(eigentlich ist es KiBaber das ist nicht so allgemein bekannt). Der Wert ist "völlig" zufällig, aber 8 kB scheinen ein angemessener Wert zu sein: Es wird nicht zu viel Speicher verschwendet und es gibt immer noch nicht "zu viele" Lesevorgänge wie in der akzeptierten Antwort von Skurmedel ...
mozzbozz
3
Das Dateisystem puffert bereits Datenblöcke, sodass dieser Code redundant ist. Es ist besser, jeweils ein Byte zu lesen.
stark
17
Dies ist zwar bereits schneller als die akzeptierte Antwort, könnte jedoch um weitere 20-25% beschleunigt werden, indem die gesamte innerste for b in chunk:Schleife durch ersetzt wird yield from chunk. Diese Form von yieldwurde in Python 3.3 hinzugefügt (siehe Ertragsausdrücke ).
Martineau
3
Hmm scheint unwahrscheinlich, Link?
Codeape
54

Wenn die Datei nicht zu groß ist, ist es ein Problem, sie im Speicher zu halten:

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

Dabei steht process_byte für eine Operation, die Sie für das übergebene Byte ausführen möchten.

Wenn Sie jeweils einen Block verarbeiten möchten:

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)

Die withAnweisung ist in Python 2.5 und höher verfügbar.

Vinay Sajip
quelle
1
Vielleicht interessiert Sie der Benchmark, den ich gerade gepostet habe.
Martineau
37

Um eine Datei zu lesen - jeweils ein Byte (ohne Berücksichtigung der Pufferung) - können Sie die integrierte Funktion mit zwei Argumenten iter(callable, sentinel)verwenden :

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

Es ruft auf, file.read(1)bis es nichts zurückgibt b''(leerer Bytestring). Der Speicher wächst nicht unbegrenzt für große Dateien. Sie können buffering=0 an übergeben open(), um die Pufferung zu deaktivieren - dies garantiert, dass nur ein Byte pro Iteration gelesen wird (langsam).

with-statement schließt die Datei automatisch - einschließlich des Falls, in dem der darunter liegende Code eine Ausnahme auslöst.

Trotz des Vorhandenseins einer internen Pufferung ist es immer noch ineffizient, jeweils ein Byte zu verarbeiten. Hier ist zum Beispiel das blackhole.pyDienstprogramm, das alles isst, was es gibt:

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

Beispiel:

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

Es verarbeitet ~ 1,5 GB / s , wenn chunksize == 32768auf meiner Maschine und nur ~ 7,5 MB / s bei chunksize == 1. Das heißt, es ist 200-mal langsamer, jeweils ein Byte zu lesen. Berücksichtigen Sie, ob Sie Ihre Verarbeitung so umschreiben können, dass mehr als ein Byte gleichzeitig verwendet wird, und ob Sie Leistung benötigen.

mmapMit dieser Option können Sie eine Datei gleichzeitig als bytearrayund als Dateiobjekt behandeln. Es kann als Alternative zum Laden der gesamten Datei in den Speicher dienen, wenn Sie Zugriff auf beide Schnittstellen benötigen. Insbesondere können Sie jeweils ein Byte über eine Speicherzuordnungsdatei iterieren, indem Sie einfach eine einfache forSchleife verwenden:

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

mmapunterstützt die Slice-Notation. Beispielsweise mm[i:i+len]kehrt lenBytes aus der Datei , beginnend an Position i. Das Kontextmanagerprotokoll wird vor Python 3.2 nicht unterstützt. Sie müssen mm.close()in diesem Fall explizit aufrufen . Das Durchlaufen jedes Bytes mit mmapverbraucht mehr Speicher als file.read(1), ist jedoch mmapum eine Größenordnung schneller.

jfs
quelle
Ich fand das letzte Beispiel sehr interessant. Schade, dass es keine äquivalenten numpyspeicherabgebildeten (Byte-) Arrays gibt.
Martineau
1
@martineau gibt es numpy.memmap()und Sie können die Daten byteweise abrufen (ctypes.data). Sie können sich numpy Arrays als etwas mehr als Blobs in Speicher + Metadaten vorstellen.
JFS
jfs: Danke, gute Neuigkeiten! Wusste nicht, dass es so etwas gibt. Tolle Antwort, übrigens.
Martineau
25

Binärdatei in Python lesen und jedes Byte durchlaufen

Neu in Python 3.5 ist das pathlibModul, das eine praktische Methode zum Einlesen einer Datei als Bytes bietet, mit der wir die Bytes durchlaufen können. Ich halte dies für eine anständige (wenn auch schnelle und schmutzige) Antwort:

import pathlib

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

Interessant, dass dies die einzige zu erwähnende Antwort ist pathlib.

In Python 2 würden Sie dies wahrscheinlich tun (wie auch Vinay Sajip vorschlägt):

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

Für den Fall, dass die Datei zu groß ist, um über den Arbeitsspeicher zu iterieren, würden Sie sie idiomatisch unter Verwendung der iterFunktion mit der callable, sentinelSignatur - der Python 2-Version - aufteilen:

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)

(Einige andere Antworten erwähnen dies, aber nur wenige bieten eine vernünftige Lesegröße.)

Best Practice für große Dateien oder gepuffertes / interaktives Lesen

Erstellen wir dazu eine Funktion, einschließlich der idiomatischen Verwendung der Standardbibliothek für 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

Beachten Sie, dass wir verwenden file.read1. file.readblockiert, bis alle angeforderten Bytes oder EOF.file.read1ermöglicht es uns, das Blockieren zu vermeiden, und es kann dadurch schneller zurückkehren. Keine anderen Antworten erwähnen dies ebenfalls.

Demonstration der Verwendung von Best Practices:

Lassen Sie uns eine Datei mit einem Megabyte (eigentlich Mebibyte) pseudozufälliger Daten erstellen:

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

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

Lassen Sie uns nun darüber iterieren und es im Gedächtnis materialisieren:

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

Wir können jeden Teil der Daten untersuchen, zum Beispiel die letzten 100 und die ersten 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]

Bei Binärdateien nicht durch Zeilen iterieren

Gehen Sie nicht wie folgt vor - dies zieht einen Block beliebiger Größe, bis ein Zeilenumbruchzeichen erreicht wird - zu langsam, wenn die Blöcke zu klein und möglicherweise auch zu groß sind:

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

Das Obige ist nur für semantisch lesbare Textdateien (wie Klartext, Code, Markup, Markdown usw.) geeignet, die Sie ohne das 'b'Flag öffnen sollten .

Aaron Hall
quelle
2
Das ist so viel besser ... danke, dass du das getan hast. Ich weiß, dass es nicht immer Spaß macht, auf eine zwei Jahre alte Antwort zurückzukommen, aber ich weiß es zu schätzen, dass Sie es getan haben. Ich mag besonders die Überschrift "Nicht durch Linien iterieren" :-)
Floris
1
Hallo Aaron, gibt es einen Grund, warum Sie sich dafür entschieden haben, path = Path(path), with path.open('rb') as file:stattdessen die eingebaute offene Funktion zu verwenden? Sie machen beide dasselbe richtig?
Joshua Yonathan
1
@JoshuaYonathan Ich benutze das PathObjekt, weil es eine sehr bequeme neue Art ist, mit Pfaden umzugehen . Anstatt eine Zeichenfolge in die sorgfältig ausgewählten "richtigen" Funktionen zu übergeben, können wir einfach die Methoden für das Pfadobjekt aufrufen, das im Wesentlichen die meisten wichtigen Funktionen enthält, die Sie mit einer semantisch angegebenen Pfadzeichenfolge benötigen. Mit IDEs, die inspizieren können, können wir auch die automatische Vervollständigung einfacher erreichen. Wir könnten dasselbe mit dem openeingebauten Programm erreichen, aber es gibt viele Vorteile beim Schreiben des Programms, damit der Programmierer das PathObjekt stattdessen verwenden kann.
Aaron Hall
1
Die letzte Methode, die Sie mit der Funktion erwähnt haben, file_byte_iteratorist viel schneller als alle Methoden, die ich auf dieser Seite ausprobiert habe. Hut ab!
Rick M.
@ RickM: Vielleicht interessiert Sie der Benchmark, den ich gerade gepostet habe.
Martineau
19

Um alle brillanten Punkte von chrispy, Skurmedel, Ben Hoyt und Peter Hansen zusammenzufassen, wäre dies die optimale Lösung, um eine Binärdatei byteweise zu verarbeiten:

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

Für Python-Versionen 2.6 und höher, weil:

  • Python-Puffer intern - keine Notwendigkeit, Chunks zu lesen
  • DRY-Prinzip - Wiederholen Sie die Lesezeile nicht
  • with-Anweisung sorgt für ein sauberes Schließen der Datei
  • 'Byte' wird als falsch ausgewertet, wenn keine Bytes mehr vorhanden sind (nicht, wenn ein Byte Null ist).

Oder verwenden Sie die JF Sebastians-Lösung für eine verbesserte Geschwindigkeit

from functools import partial

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

Oder wenn Sie es als Generatorfunktion haben möchten, wie von codeape demonstriert:

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
quelle
2
Wie in der verknüpften Antwort angegeben, ist das Lesen / Verarbeiten von jeweils einem Byte in Python immer noch langsam, selbst wenn die Lesevorgänge gepuffert sind. Die Leistung kann drastisch verbessert werden, wenn mehrere Bytes gleichzeitig verarbeitet werden können, wie im Beispiel in der verknüpften Antwort: 1,5 GB / s gegenüber 7,5 MB / s.
JFS
6

Python 3, lesen Sie die gesamte Datei auf einmal:

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

Sie können mit dataVariablen alles iterieren, was Sie wollen .

Mircea
quelle
6

Nachdem ich alles oben Genannte ausprobiert und die Antwort von @Aaron Hall verwendet hatte, wurden Speicherfehler für eine ~ 90-MB-Datei auf einem Computer mit Windows 10, 8 GB RAM und Python 3.5 32-Bit angezeigt. Ich wurde von einem Kollegen empfohlen, numpystattdessen zu verwenden, und es wirkt Wunder.

Bei weitem ist das Lesen einer gesamten Binärdatei (die ich getestet habe) am schnellsten:

import numpy as np

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

Referenz

Viele schneller als alle anderen Methoden bisher. Hoffe es hilft jemandem!

Rick M.
quelle
3
Schön, kann aber nicht für Binärdateien mit unterschiedlichen Datentypen verwendet werden.
Nirmal
@Nirmal: Bei der Frage geht es um das Durchlaufen des Reichweitenbytes. Es ist daher nicht klar, ob Ihr Kommentar zu verschiedenen Datentypen von Bedeutung ist.
Martineau
1
Rick: Ihr Code macht nicht ganz das Gleiche wie die anderen - er durchläuft jedes Byte. Wenn das hinzugefügt wird, ist es nicht schneller als die Mehrheit der anderen, zumindest gemäß den Ergebnissen in meinem Benchmark . Tatsächlich scheint es einer der langsameren Ansätze zu sein. Wenn die Verarbeitung für jedes Byte (was auch immer das sein mag) etwas war, über das durchgeführt werden könnte numpy, dann könnte es sich lohnen.
Martineau
@martineau Vielen Dank für Ihre Kommentare. Ja, ich verstehe, dass es bei der Frage darum geht, jedes Byte zu durchlaufen und nicht nur alles auf einmal zu laden, sondern es gibt auch andere Antworten in dieser Frage, die auch darauf hinweisen, alle Inhalte und damit meine Antwort zu lesen
Rick M.
4

Wenn Sie viele Binärdaten lesen müssen, sollten Sie das Strukturmodul in Betracht ziehen . Es wird dokumentiert, dass "zwischen C- und Python-Typen" konvertiert wird, aber natürlich sind Bytes Bytes, und es spielt keine Rolle, ob diese als C-Typen erstellt wurden. Wenn Ihre Binärdaten beispielsweise zwei 2-Byte-Ganzzahlen und eine 4-Byte-Ganzzahl enthalten, können Sie diese wie folgt lesen (Beispiel aus der structDokumentation):

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

Möglicherweise ist dies bequemer, schneller oder beides, als den Inhalt einer Datei explizit zu durchlaufen.

gerrit
quelle
4

Dieser Beitrag selbst ist keine direkte Antwort auf die Frage. Stattdessen handelt es sich um einen datengesteuerten erweiterbaren Benchmark, mit dem viele der Antworten (und Variationen der Verwendung neuer Funktionen, die in späteren, moderneren Versionen von Python hinzugefügt wurden) verglichen werden können, die zu dieser Frage veröffentlicht wurden - und daher sollten Seien Sie hilfreich bei der Ermittlung der besten Leistung.

In einigen Fällen habe ich den Code in der Antwort, auf die verwiesen wird, geändert, um ihn mit dem Benchmark-Framework kompatibel zu machen.

Hier sind zunächst die Ergebnisse für die aktuellsten Versionen von 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

Ich habe es auch mit einer viel größeren 10-MiB-Testdatei (deren Ausführung fast eine Stunde dauerte) ausgeführt und Leistungsergebnisse erhalten, die mit den oben gezeigten vergleichbar waren.

Hier ist der Code für das Benchmarking:

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
quelle
Gehen Sie davon aus, dass ich es yield from chunkstattdessen tue for byte in chunk: yield byte? Ich denke, ich sollte meine Antwort damit verschärfen.
Aaron Hall
@ Aaron: Es gibt zwei Versionen, auf die Sie in den Python 3-Ergebnissen antworten, und eine davon verwendet yield from.
Martineau
ok, ich habe meine Antwort aktualisiert. Außerdem schlage ich vor, dass Sie fallen lassen, enumerateda die Iteration so verstanden werden sollte, dass sie abgeschlossen ist - wenn nicht, habe ich sie zuletzt überprüft -, hat die Aufzählung einen gewissen Aufwand mit Kosten für die Buchhaltung für den Index mit + = 1, sodass Sie alternativ die Buchhaltung in Ihrem durchführen können eigener Code. Oder gehen Sie sogar zu einem Deque mit maxlen=0.
Aaron Hall
@ Aaron: Stimmen Sie dem zu enumerate. Danke für die Rückmeldung. Fügt meinem Beitrag ein Update hinzu, das es nicht enthält (obwohl ich nicht denke, dass es die Ergebnisse stark verändert). Wird auch die numpyAntwort von @Rick M. hinzufügen .
Martineau
Ein bisschen mehr Codeüberprüfung: Ich denke, es ist an dieser Stelle nicht sinnvoll, Antworten auf Python 2 zu schreiben. Ich würde in Betracht ziehen, Python 2 zu entfernen, da ich erwarten würde, dass Sie 64-Bit-Python 3.7 oder 3.8 verwenden. Sie können die Bereinigung so einstellen, dass sie am Ende mit atexit und einer Teilanwendung beginnt. Tippfehler: "verifizieren". Ich sehe keinen Sinn in der Vervielfältigung der Testzeichenfolgen - sind sie überhaupt unterschiedlich? Ich stelle mir vor, wenn Sie super().anstelle von tuple.in verwenden __new__, könnten Sie die namedtupleAttributnamen anstelle von Indizes verwenden.
Aaron Hall
3

Wenn Sie nach etwas Schnellem suchen, ist hier eine Methode, die ich seit Jahren verwende:

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)

Wenn Sie Zeichen anstelle von Ints iterieren möchten, können Sie einfach data = file.read()das Objekt bytes () in py3 verwenden.

Tcll
quelle
1
'array' wird von 'from array import array' importiert
quanly_mc
@quanly_mc Ja, danke, dass du das verstanden hast, und es tut mir leid, dass ich vergessen habe, das aufzunehmen.
Tcll