Odczytywanie pliku binarnego i zapętlanie każdego bajtu

377

W Pythonie, jak mam czytać plik binarny i zapętlać każdy bajt tego pliku?

Jesse Vogt
źródło

Odpowiedzi:

387

Python 2.4 i wcześniejsze

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)

Zauważ, że instrukcja with nie jest dostępna w wersjach Pythona poniżej 2.5. Aby użyć go w wersji 2.5, musisz go zaimportować:

from __future__ import with_statement

W 2.6 nie jest to potrzebne.

Python 3

W Pythonie 3 jest nieco inaczej. Nie będziemy już pobierać nieprzetworzonych znaków ze strumienia w trybie bajtowym, ale obiekty bajtowe, dlatego musimy zmienić warunek:

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

Lub, jak mówi Benhoyt, pomiń nierównomierne i skorzystaj z faktu, że b""wartość jest fałszywa. Dzięki temu kod jest zgodny między wersjami 2.6 a 3.x bez żadnych zmian. Oszczędziłoby to również zmiany warunku, jeśli przejdziesz z trybu bajtowego na tekstowy lub odwrotnie.

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

python 3.8

Od teraz dzięki: = operator powyższy kod można napisać w krótszy sposób.

with open("myfile", "rb") as f:
    while (byte := f.read(1)):
        # Do stuff with byte.
Skurmedel
źródło
40
Czytanie pliku bajtowo to koszmar wydajności. To nie może być najlepsze rozwiązanie dostępne w Pythonie. Tego kodu należy używać ostrożnie.
usr
7
@usr: Cóż, obiekty plików są buforowane wewnętrznie, a mimo to o to proszono. Nie każdy skrypt wymaga optymalnej wydajności.
Skurmedel
4
@mezhaka: Więc zmieniasz to z read (1) na read (bufsize), aw pętli while robisz for-in ... przykład nadal istnieje.
Skurmedel
3
@ usr: różnica wydajności może być nawet 200 razy większa niż w przypadku kodu, który wypróbowałem .
jfs
2
@usr - zależy od tego, ile bajtów chcesz przetworzyć. Jeśli jest ich mało, preferowany może być „źle” działający, ale łatwo zrozumiały kod. Marnowanie cykli procesora jest kompensowane przez zapisywanie „cykli procesora czytnika” podczas konserwacji kodu.
IllvilJa
172

Ten generator generuje bajty z pliku, odczytując go w porcjach:

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)

Informacje na temat iteratorów i generatorów można znaleźć w dokumentacji Pythona .

Codeape
źródło
3
@codeape Właśnie tego szukam. Ale jak określić wielkość porcji? Czy może to być dowolna wartość?
swdev
3
@swdev: W przykładzie użyto rozmiaru 8192 bajtów . Parametr dla funkcji file.read () określa po prostu rozmiar, tj. Liczbę bajtów do odczytania. Codeape wybrał 8192 Byte = 8 kB(właściwie to, KiBale to nie jest tak powszechnie znane). Wartością jest „całkowicie” random ale 8 kB wydaje się być odpowiednia wartość: nie zbyt dużo pamięci jest marnowana i nadal nie są „zbyt wiele” operacji odczytu jak w przyjętym Odpowiedź Skurmedel ...
mozzbozz
3
System plików buforuje już fragmenty danych, więc ten kod jest zbędny. Lepiej jest czytać bajt na raz.
surowy
17
Chociaż jest już szybsza niż zaakceptowana odpowiedź, można ją przyspieszyć o kolejne 20-25%, zastępując całą wewnętrzną for b in chunk:pętlę yield from chunk. Ta forma yieldzostała dodana w Pythonie 3.3 (patrz Wyrażenia wydajności ).
martineau
3
Hmm wydaje się mało prawdopodobne, link?
codeape
54

Jeśli plik nie jest zbyt duży, problemem jest trzymanie go w pamięci:

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

gdzie bajt_procesu reprezentuje operację, którą chcesz wykonać na przekazanym bajcie.

Jeśli chcesz przetwarzać porcję na raz:

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)

withStwierdzenie jest dostępny w Pythonie 2.5 i większej.

Vinay Sajip
źródło
1
Być może interesuje Cię test, który właśnie opublikowałem.
martineau
37

Aby odczytać plik - jeden bajt na raz (ignorując buforowanie) - możesz użyć wbudowanej funkcji dwóch argumentówiter(callable, sentinel) :

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

Wywołuje, file.read(1)dopóki nic nie zwróci b''(puste bajtowanie). Pamięć nie rośnie bez ograniczeń dla dużych plików. Możesz przejść buffering=0 do open(), aby wyłączyć buforowanie - gwarantuje to, że tylko jeden bajt jest odczytywany na jedną iterację (powoli).

with-statement zamyka plik automatycznie - w tym przypadek, gdy kod poniżej generuje wyjątek.

Mimo obecności domyślnego buforowania wewnętrznego przetwarzanie jednego bajtu na raz nadal jest nieefektywne. Na przykład, oto blackhole.pynarzędzie, które zjada wszystko, co zostało podane:

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

Przykład:

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

Przetwarza ~ 1,5 GB / s, gdy chunksize == 32768na moim komputerze i tylko ~ 7,5 MB / s, kiedy chunksize == 1. Oznacza to, że czytanie bajtu na raz jest 200 razy wolniejsze. Weź to pod uwagę, jeśli możesz przepisać swoje przetwarzanie, aby używać więcej niż jednego bajtu na raz i jeśli potrzebujesz wydajności.

mmappozwala traktować plik jednocześnie jako bytearrayobiekt pliku. Może służyć jako alternatywa dla ładowania całego pliku do pamięci, jeśli potrzebujesz dostępu do obu interfejsów. W szczególności możesz iterować jeden bajt naraz po pliku odwzorowanym w pamięci, używając zwykłej forpętli:

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

mmapobsługuje notację plastra. Na przykład mm[i:i+len]zwraca lenbajty z pliku zaczynając od pozycji i. Protokół menedżera kontekstu nie jest obsługiwany przed Pythonem 3.2; mm.close()w takim przypadku musisz zadzwonić jawnie. Iteracja po każdym bajcie mmapzużywa więcej pamięci niż file.read(1), ale mmapjest o rząd wielkości szybsza.

jfs
źródło
Uważam ostatni przykład za bardzo interesujący. Szkoda, że ​​nie ma równoważnych numpytablic odwzorowanych w pamięci (bajtowych).
martineau,
1
@martineau istnieje numpy.memmap()i możesz pobrać dane jeden bajt na raz (ctypes.data). Mógłbyś myśleć o tablicach numpy jako o czymś więcej niż o obiektach blob w pamięci + metadanych.
jfs
jfs: Dzięki, wspaniałe wiadomości! Nie wiedziałem, że istnieje. Świetna odpowiedź, BTW.
martineau,
25

Odczytywanie pliku binarnego w Pythonie i zapętlanie każdego bajtu

Nowością w Pythonie 3.5 jest pathlibmoduł, który ma wygodną metodę wczytywania pliku jako bajtów, co pozwala nam na iterację po bajtach. Uważam to za przyzwoitą (choć szybką i brudną) odpowiedź:

import pathlib

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

Ciekawe, że to jedyna wspomniana odpowiedź pathlib.

W Pythonie 2 prawdopodobnie zrobiłbyś to (jak sugeruje również Vinay Sajip):

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

W przypadku, gdy plik może być zbyt duży, aby iterować w pamięci, można go porcjować idiomatycznie, używając iterfunkcji z callable, sentinelpodpisem - wersja 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)

(Kilka innych odpowiedzi wspomina o tym, ale niewiele z nich oferuje rozsądny rozmiar odczytu.)

Najlepsza praktyka w przypadku dużych plików lub odczytu buforowanego / interaktywnego

Utwórzmy w tym celu funkcję, w tym idiomatyczne zastosowania standardowej biblioteki dla 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

Pamiętaj, że korzystamy file.read1. file.readblokuje, dopóki nie otrzyma wszystkich wymaganych bajtów lub EOF. file.read1pozwala nam uniknąć blokowania i dzięki temu może szybciej wrócić. Żadne inne odpowiedzi również o tym nie wspominają.

Pokaz stosowania najlepszych praktyk:

Stwórzmy plik z megabajtem (właściwie mebibajtem) danych pseudolosowych:

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

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

Teraz powtórzmy to i zmaterializujmy w pamięci:

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

Możemy sprawdzić dowolną część danych, na przykład ostatnie 100 i pierwsze 100 bajtów:

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

Nie iteruj według wierszy dla plików binarnych

Nie wykonuj następujących czynności - spowoduje to wyciągnięcie kawałka o dowolnym rozmiarze, aż dojdzie do znaku nowej linii - zbyt wolno, gdy fragmenty są zbyt małe, a być może również zbyt duże:

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

Powyższe jest dobre tylko dla tego, co jest semantycznie czytelnym dla człowieka plikiem tekstowym (takim jak zwykły tekst, kod, znaczniki, markdown itp.) Zasadniczo wszystko, co jest zakodowane w ascii, utf, latin itp.), Które powinieneś otworzyć bez 'b'flagi.

Aaron Hall
źródło
2
To jest O wiele lepsze ... dziękuję za zrobienie tego. Wiem, że powrót do dwuletniej odpowiedzi nie zawsze jest przyjemny, ale doceniam to, że to zrobiłeś. Szczególnie podoba mi się nagłówek „Don't iterate by lines” :-)
Floris,
1
Cześć Aaron, czy jest jakiś powód, dla którego zdecydowałeś się użyć path = Path(path), with path.open('rb') as file:zamiast wbudowanej funkcji otwartej? Oboje robią to samo, prawda?
Joshua Yonathan
1
@JoshuaYonathan Korzystam z Pathobiektu, ponieważ jest to bardzo wygodny nowy sposób obsługi ścieżek. Zamiast przekazywać ciąg znaków do starannie wybranych „właściwych” funkcji, możemy po prostu wywołać metody na obiekcie ścieżki, który zasadniczo zawiera większość ważnych funkcji, których potrzebujesz z semantycznie ciągiem ścieżki. Dzięki IDE, które mogą kontrolować, łatwiej możemy również uzyskać autouzupełnianie. To samo możemy osiągnąć dzięki openwbudowanemu programowi, ale przy pisaniu programu programiści mogą korzystać z Pathtego obiektu.
Aaron Hall
1
Ostatnia metoda, o której wspomniałeś przy użyciu tej funkcji, file_byte_iteratorjest znacznie szybsza niż wszystkie metody, które wypróbowałem na tej stronie. Uznanie dla ciebie!
Rick M.
@ RickM: Być może interesuje Cię test, który właśnie opublikowałem.
martineau
19

Podsumowując wszystkie genialne punkty chrispy, Skurmedel, Ben Hoyt i Peter Hansen, byłoby to optymalne rozwiązanie do przetwarzania pliku binarnego po jednym bajcie:

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

Dla wersji Python 2.6 i nowszych, ponieważ:

  • bufory Pythona wewnętrznie - nie trzeba czytać fragmentów
  • Zasada DRY - nie powtarzaj linii odczytu
  • z instrukcją zapewnia czyste zamknięcie pliku
  • „bajt” zwraca wartość fałsz, gdy nie ma już bajtów (nie gdy bajt jest równy zero)

Lub użyj rozwiązania JF Sebastians dla zwiększenia prędkości

from functools import partial

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

Lub jeśli chcesz to jako funkcję generatora, jak pokazano w 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
źródło
2
Jak wynika z połączonej odpowiedzi, odczyt / przetwarzanie jednego bajtu na raz w Pythonie jest nadal wolne, nawet jeśli odczyty są buforowane. Wydajność można znacznie poprawić, jeśli można przetwarzać jednocześnie kilka bajtów, jak w przykładzie w odpowiedzi połączonej: 1,5 GB / s vs. 7,5 MB / s.
jfs
6

Python 3, przeczytaj cały plik na raz:

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

Możesz iterować dowolnie, używając datazmiennej.

Mircea
źródło
6

Po wypróbowaniu wszystkich powyższych informacji i użyciu odpowiedzi z @Aaron Hall otrzymywałem błędy pamięci dla pliku o wielkości ~ 90 Mb na komputerze z systemem Windows 10, 8 Gb RAM i 32-bitowym Python 3.5. Polecił mi kolega z pracynumpyZamiast tego i działa to cuda.

Zdecydowanie najszybszy odczyt całego pliku binarnego (który przetestowałem) to:

import numpy as np

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

Odniesienie

Mnóstwo szybciej niż jakiekolwiek inne metody do tej pory. Mam nadzieję, że to komuś pomaga!

Rick M.
źródło
3
Fajnie, ale nie można go używać w pliku binarnym zawierającym różne typy danych.
Nirmal
@Nirmal: Pytanie dotyczy zapętlenia bajtu zasięgu, więc nie jest jasne, czy Twój komentarz na temat różnych typów danych ma jakikolwiek wpływ.
martineau
1
Rick: Twój kod nie robi dokładnie tego samego, co inne - mianowicie zapętlał każdy bajt. Jeśli zostanie to dodane, nie będzie szybsze niż większość innych, przynajmniej według wyników w moim teście . W rzeczywistości wydaje się, że jest to jedno z wolniejszych podejść. Jeśli przetwarzanie wykonane dla każdego bajtu (cokolwiek to może być) było czymś, co można wykonać za pomocą numpy, to może być opłacalne.
martineau
@martineau Dziękuję za komentarze, tak Rozumiem, że pytanie dotyczy zapętlania każdego bajtu i nie tylko ładowania wszystkiego za jednym razem, ale istnieją inne odpowiedzi w tym pytaniu, które również wskazują na przeczytanie całej zawartości, a zatem mojej odpowiedzi
Rick M.
4

Jeśli masz dużo danych binarnych do odczytania, możesz rozważyć moduł struct . Jest to udokumentowane jako konwersja „między typami C i Python”, ale oczywiście bajty są bajtami i to, czy zostały utworzone jako typy C, nie ma znaczenia. Na przykład, jeśli dane binarne zawierają dwie 2-bajtowe liczby całkowite i jedną 4-bajtową liczbę całkowitą, możesz je odczytać w następujący sposób (przykład wzięty z structdokumentacji):

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

Może się to okazać wygodniejsze, szybsze lub jedno i drugie, niż jawne zapętlanie zawartości pliku.

gerrit
źródło
4

Sam post nie jest bezpośrednią odpowiedzią na pytanie. Zamiast tego jest oparty na danych, rozszerzalny test porównawczy, którego można użyć do porównania wielu odpowiedzi (i wariantów wykorzystania nowych funkcji dodanych w późniejszych, bardziej nowoczesnych wersjach Pythona), które zostały opublikowane na to pytanie - i dlatego powinny być pomocnym w określeniu, która ma najlepszą wydajność.

W kilku przypadkach zmodyfikowałem kod w cytowanej odpowiedzi, aby był zgodny z frameworkiem testu porównawczego.

Po pierwsze, oto wyniki dla najnowszych wersji Python 2 i 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

Uruchomiłem go również ze znacznie większym plikiem testowym 10 MiB (którego uruchomienie zajęło prawie godzinę) i uzyskałem wyniki wydajności porównywalne do pokazanych powyżej.

Oto kod użyty do przeprowadzenia testu porównawczego:

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
źródło
Czy yield from chunkzamiast tego zakładasz, że tak for byte in chunk: yield byte? Myślę, że powinienem zaostrzyć moją odpowiedź.
Aaron Hall
@Aaron: Istnieją dwie wersje twojej odpowiedzi w wynikach Python 3 i jedna z nich używa yield from.
martineau
ok, zaktualizowałem swoją odpowiedź. również sugeruję, abyś upuścił, enumerateponieważ należy rozumieć, że iteracja jest zakończona - jeśli nie, to ostatnio sprawdziłem - wyliczenie ma trochę kosztów ogólnych związanych z prowadzeniem księgowości dla indeksu z + = 1, więc możesz alternatywnie wykonywać księgowanie w swoim własny kod. Lub nawet przejść do deque z maxlen=0.
Aaron Hall
@Aaron: Zgadzam się na temat enumerate. Dzięki za opinie. Dodam aktualizację do mojego postu, która jej nie ma (chociaż nie sądzę, żeby zmieniała to znacznie wyniki). Będzie również dodanie @Rick M. numpyopartych odpowiedź.
martineau
Trochę więcej recenzji kodu: nie sądzę, aby pisanie odpowiedzi na Python 2 miało w tym momencie sens - rozważę usunięcie Pythona 2, ponieważ oczekiwałbym, że użyjesz 64-bitowego Pythona 3.7 lub 3.8. Możesz ustawić czyszczenie na koniec z atexit i częściową aplikacją. Literówka: „veriify”. Nie widzę sensu w powielaniu ciągów testowych - czy w ogóle są one różne? Wyobrażam sobie, że jeśli użyjesz super().zamiast tuple.w swoim, __new__możesz użyć namedtuplenazw atrybutów zamiast indeksów.
Aaron Hall
3

jeśli szukasz czegoś szybkiego, oto metoda, której używałem, która działała przez lata:

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)

jeśli chcesz iterować znaki zamiast ints, możesz po prostu użyć data = file.read(), który powinien być obiektem bytes () w py3.

Tcll
źródło
1
„tablica” jest importowana przez „z tablicy importu tablic”
quanly_mc
@ quanly_mc tak, dziękuję za wyłapanie tego i przepraszam, że zapomniałem o tym, edytując teraz.
Tcll,