อ่านไฟล์ไบนารีและวนลูปมากกว่าแต่ละไบต์


377

ใน Python ฉันจะอ่านในไฟล์ไบนารี่และวนรอบแต่ละไบต์ของไฟล์นั้นได้อย่างไร?

คำตอบ:


387

Python 2.4 และรุ่นก่อนหน้า

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)

โปรดทราบว่าคำสั่ง with ไม่พร้อมใช้งานในเวอร์ชันของ Python ต่ำกว่า 2.5 หากต้องการใช้ใน v 2.5 คุณจะต้องนำเข้า:

from __future__ import with_statement

ใน 2.6 นี้ไม่จำเป็น

Python 3

ใน Python 3 มันต่างกันเล็กน้อย เราจะไม่ได้รับตัวอักขระดิบจากสตรีมในโหมดไบต์ แต่เป็นวัตถุไบต์ดังนั้นเราจึงต้องแก้ไขเงื่อนไข:

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

หรืออย่างที่ benhoyt พูดให้ข้ามสิ่งที่ไม่เท่ากันและใช้ประโยชน์จากความจริงที่b""ประเมินว่าเป็นเท็จ ทำให้โค้ดเข้ากันได้ระหว่าง 2.6 ถึง 3.x โดยไม่มีการเปลี่ยนแปลงใด ๆ นอกจากนี้ยังช่วยให้คุณประหยัดจากการเปลี่ยนเงื่อนไขหากคุณไปจากโหมดไบต์เป็นข้อความหรือย้อนกลับ

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

หลาม 3.8

นับจากนี้ไปจนถึง: = ตัวดำเนินการโค้ดข้างต้นสามารถเขียนได้ในวิธีที่สั้นลง

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

40
อ่านไฟล์ไบต์ฉลาดเป็นฝันร้ายประสิทธิภาพ นี่ไม่ใช่ทางแก้ปัญหาที่ดีที่สุดในงูหลาม รหัสนี้ควรใช้ด้วยความระมัดระวัง
usr

7
@usr: ออบเจ็กต์ของไฟล์ถูกบัฟเฟอร์ภายในและนี่คือสิ่งที่ถูกถาม ไม่ใช่ทุกสคริปต์ที่ต้องการประสิทธิภาพสูงสุด
Skurmedel

4
@ mezhaka: ดังนั้นคุณเปลี่ยนจาก read (1) เป็น read (bufsize) และใน while-loop คุณทำการ for-in ... ตัวอย่างยังคงยืนอยู่
Skurmedel

3
@usr: แตกต่างประสิทธิภาพสามารถมากที่สุดเท่าที่ 200 ครั้งรหัสฉันได้พยายาม
jfs

2
@usr - ขึ้นอยู่กับจำนวนไบต์ที่คุณต้องการประมวลผล หากพวกเขามีไม่เพียงพอก็อาจเป็นที่ต้องการของโค้ดที่มีประสิทธิภาพ "แย่" แต่เข้าใจได้ง่าย ความสิ้นเปลืองของวงจร CPU จะถูกชดเชยสำหรับการบันทึก "ตัวอ่านซีพียูรอบ" เมื่อทำการบำรุงรักษารหัส
IllvilJa

172

ตัวสร้างนี้ให้ผลเป็นไบต์จากไฟล์อ่านไฟล์ในหน่วยย่อย:

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)

ดูเอกสารหลามสำหรับข้อมูลเกี่ยวกับiteratorsและเครื่องกำเนิดไฟฟ้า


3
@codeape สิ่งที่ฉันกำลังมองหา แต่คุณจะกำหนด chunksize ได้อย่างไร มันสามารถเป็นค่าโดยพลการหรือไม่?
swdev

3
@swdev: ตัวอย่างที่ใช้ chunksize ของ 8192 ไบต์ พารามิเตอร์สำหรับ file.read () - ฟังก์ชั่นเพียงแค่ระบุขนาดคือจำนวนไบต์ที่จะอ่าน codeape เลือก8192 Byte = 8 kB(อันที่จริงมันKiBแต่ไม่เป็นที่รู้จักกันทั่วไป) ค่าเป็นแบบสุ่ม "ทั้งหมด" แต่ 8 kB น่าจะเป็นค่าที่เหมาะสม: หน่วยความจำไม่มากเกินไปและยังไม่มีการดำเนินการอ่าน "มากเกินไป" ตามคำตอบที่ยอมรับโดย Skurmedel ...
mozzbozz

3
ระบบไฟล์บัฟเฟอร์ข้อมูลจำนวนหนึ่งอยู่แล้วดังนั้นโค้ดนี้จึงซ้ำซ้อน ควรอ่านทีละไบต์ดีกว่า
สิ้นเชิง

17
ในขณะที่มีอยู่แล้วได้เร็วกว่าคำตอบที่ได้รับการยอมรับนี้อาจจะเร่งขึ้นอีก 20-25% โดยการเปลี่ยนทั้งด้านในสุดของวงด้วยfor b in chunk: yield from chunkรูปแบบนี้yieldถูกเพิ่มใน Python 3.3 (ดูนิพจน์ผลตอบแทน )
martineau

3
อืมดูเหมือนไม่แน่ลิงค์เลยเหรอ?
codeape

54

หากไฟล์ไม่ใหญ่เกินไปที่ถือไว้ในหน่วยความจำเป็นปัญหา:

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

โดยที่ process_byte แสดงถึงการดำเนินการบางอย่างที่คุณต้องการดำเนินการกับไบต์ที่ส่งผ่าน

หากคุณต้องการประมวลผลข้อมูลครั้งละหนึ่งรายการ:

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)

withคำสั่งที่สามารถใช้ได้ในหลาม 2.5 และมากขึ้น


1
คุณอาจสนใจมาตรฐานที่ฉันเพิ่งโพสต์
martineau

37

หากต้องการอ่านไฟล์ - ทีละหนึ่งไบต์ (ละเว้นการบัฟเฟอร์) - คุณสามารถใช้ฟังก์ชั่นบิวด์อินสองอาร์กิวเมนต์iter(callable, sentinel) :

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

มันจะเรียกfile.read(1)จนกว่าจะไม่มีอะไรกลับb''มา หน่วยความจำไม่เติบโตไม่ จำกัด สำหรับไฟล์ขนาดใหญ่ คุณสามารถส่งผ่านbuffering=0 ไปยังopen()เพื่อปิดการใช้งานบัฟเฟอร์ - มันรับประกันว่ามีเพียงหนึ่งไบต์ที่อ่านต่อการทำซ้ำ (ช้า)

with- สถานะปิดไฟล์โดยอัตโนมัติ - รวมกรณีและปัญหาเมื่อรหัสภายใต้ทำให้เกิดข้อยกเว้น

แม้ว่าจะมีการบัฟเฟอร์ภายในโดยค่าเริ่มต้น แต่ก็ยังไม่มีประสิทธิภาพในการประมวลผลทีละหนึ่งไบต์ ตัวอย่างเช่นนี่คือblackhole.pyยูทิลิตี้ที่กินทุกอย่างที่ได้รับ:

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

ตัวอย่าง:

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

มันจะประมวลผล~ 1.5 Gb / sเมื่อchunksize == 32768อยู่บนเครื่องและมีเพียงฉัน~ 7.5 MB / schunksize == 1เมื่อ นั่นคือมันช้ากว่า 200 เท่าในการอ่านทีละครั้ง พิจารณาหากคุณสามารถเขียนการประมวลผลของคุณใหม่เพื่อใช้มากกว่าหนึ่งไบต์ในแต่ละครั้งและหากคุณต้องการประสิทธิภาพ

mmapช่วยให้คุณสามารถจัดการไฟล์เป็นbytearrayและวัตถุไฟล์พร้อมกัน มันสามารถใช้เป็นทางเลือกในการโหลดไฟล์ทั้งหมดในหน่วยความจำหากคุณต้องการเข้าถึงทั้งสองอินเตอร์เฟส โดยเฉพาะอย่างยิ่งคุณสามารถวนซ้ำทีละหนึ่งไบต์บนไฟล์ที่แม็พหน่วยความจำโดยใช้forลูปธรรมดา:

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รองรับสัญกรณ์ชิ้น ตัวอย่างเช่นmm[i:i+len]ผลตอบแทนไบต์จากไฟล์เริ่มต้นที่ตำแหน่งlen iโปรโตคอลตัวจัดการบริบทไม่ได้รับการสนับสนุนก่อน Python 3.2; คุณต้องโทรหาmm.close()อย่างชัดเจนในกรณีนี้ การวนซ้ำแต่ละไบต์โดยmmapใช้หน่วยความจำมากกว่าfile.read(1)แต่mmapเป็นลำดับความสำคัญเร็วกว่า


ฉันพบตัวอย่างสุดท้ายที่น่าสนใจมาก น่าเสียดายที่ไม่มีnumpyหน่วยความจำเทียบเท่ากับอาร์เรย์ (ไบต์)
martineau

1
@ Smartineau มีอยู่numpy.memmap()และคุณสามารถรับข้อมูลทีละหนึ่งไบต์ (ctypes.data) คุณอาจนึกถึงอาร์เรย์ numpy เป็นมากกว่า blobs ใน memory + metadata เล็กน้อย
jfs

jfs: ขอบคุณข่าวดี! ไม่ทราบว่ามันมีอยู่จริง คำตอบที่ดี BTW
martineau

25

การอ่านไฟล์ไบนารีใน Python และวนซ้ำแต่ละไบต์

ใหม่ใน Python 3.5 เป็นpathlibโมดูลซึ่งมีวิธีการอำนวยความสะดวกโดยเฉพาะในการอ่านไฟล์เป็นไบต์ทำให้เราสามารถวนซ้ำได้มากกว่าไบต์ ฉันคิดว่านี่เป็นคำตอบที่ดี (ถ้ารวดเร็วและสกปรก):

import pathlib

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

pathlibที่น่าสนใจที่ว่านี้เป็นคำตอบเดียวที่จะกล่าวถึง

ใน Python 2 คุณอาจทำเช่นนี้ (ตามที่ Vinay Sajip แนะนำเช่นกัน):

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

ในกรณีที่ไฟล์มีขนาดใหญ่เกินกว่าที่จะวนซ้ำในหน่วยความจำคุณจะต้องใช้ไฟล์ที่iterมีcallable, sentinelลายเซ็น - รุ่น 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)

(คำตอบอื่น ๆ อีกมากมายพูดถึงสิ่งนี้ แต่มีเพียงไม่กี่ขนาดที่เหมาะสมสำหรับการอ่าน)

วิธีปฏิบัติที่ดีที่สุดสำหรับไฟล์ขนาดใหญ่หรือการอ่านบัฟเฟอร์ / การโต้ตอบ

มาสร้างฟังก์ชั่นเพื่อทำสิ่งนี้รวมถึงการใช้สำนวนของไลบรารี่มาตรฐานสำหรับ 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

file.read1โปรดทราบว่าเราจะใช้ บล็อกจนกว่าจะได้รับไบต์ที่ร้องขอทั้งหมดของมันหรือfile.readEOFfile.read1ช่วยให้เราสามารถหลีกเลี่ยงการปิดกั้นและมันสามารถกลับมาได้เร็วขึ้นด้วยเหตุนี้ ไม่มีคำตอบอื่น ๆ ที่พูดถึงเรื่องนี้เช่นกัน

การสาธิตวิธีการใช้งานที่ดีที่สุด:

มาสร้างไฟล์ที่มีเมกะไบต์ (อันที่จริงแล้วคือ mebibyte) ของข้อมูลเทียมหลอก:

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

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

ทีนี้ลองทำซ้ำแล้วสร้างเป็นหน่วยความจำ:

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

เราสามารถตรวจสอบส่วนใด ๆ ของข้อมูลตัวอย่างเช่น 100 และ 100 ไบต์แรก:

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

อย่าวนซ้ำตามบรรทัดสำหรับไฟล์ไบนารี

อย่าทำสิ่งต่อไปนี้ - สิ่งนี้จะดึงขนาดที่ไม่เจาะจงจนกว่าจะถึงอักขระบรรทัดใหม่ - ช้าเกินไปเมื่อชิ้นมีขนาดเล็กเกินไปและอาจใหญ่เกินไปเช่นกัน:

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

ข้างต้นเป็นเพียงที่ดีสำหรับสิ่งที่มีความหมายของมนุษย์ไฟล์ข้อความที่อ่านได้ (เช่นข้อความธรรมดา, รหัส, มาร์กอัป markdown ฯลฯ ... อะไรเป็นหลัก ASCII, UTF ละติน ฯลฯ ... เข้ารหัส) ที่คุณควรเปิดโดยไม่มี'b'ธง


2
นี่มันดีกว่ามาก ... ขอบคุณที่ทำเช่นนี้ ฉันรู้ว่ามันไม่สนุกเสมอไปที่จะได้คำตอบอายุสองปี แต่ฉันก็ยินดีที่คุณทำเช่นนั้น ฉันชอบโดยเฉพาะอย่างยิ่ง "อย่าย้ำตามเส้น" หัวข้อย่อย :-)
Floris

1
สวัสดีแอรอนมีเหตุผลใดบ้างที่คุณเลือกใช้path = Path(path), with path.open('rb') as file:แทนที่จะใช้ฟังก์ชั่นเปิดในตัวแทน พวกเขาทั้งสองทำสิ่งเดียวกันถูกต้องหรือไม่
โยชู

1
@JoshuaYonathan ฉันใช้Pathวัตถุเพราะเป็นวิธีใหม่ที่สะดวกมากในการจัดการเส้นทาง แทนที่จะส่งผ่านสตริงไปยังฟังก์ชั่น "ถูกต้อง" ที่เลือกสรรอย่างระมัดระวังเราสามารถเรียกวิธีการบนวัตถุพา ธ ซึ่งโดยพื้นฐานแล้วจะมีฟังก์ชั่นที่สำคัญส่วนใหญ่ที่คุณต้องการด้วยความหมายของสตริงเส้นทาง ด้วย IDEs ที่สามารถตรวจสอบได้เราจะสามารถเติมข้อมูลอัตโนมัติได้ง่ายขึ้นเช่นกัน เราสามารถทำสิ่งเดียวกันกับopenbuiltin ได้ แต่มี Upside มากมายเมื่อเขียนโปรแกรมให้โปรแกรมเมอร์ใช้Pathวัตถุแทน
Aaron Hall

1
วิธีสุดท้ายที่คุณพูดถึงการใช้ฟังก์ชั่นfile_byte_iteratorนั้นเร็วกว่าวิธีที่ฉันลองในหน้านี้มาก ขอชื่นชมคุณ!
Rick M.

@RickM: คุณอาจสนใจในเกณฑ์มาตรฐานที่ฉันเพิ่งโพสต์
martineau

19

เพื่อสรุปคะแนนยอดเยี่ยมทั้งหมดของ chrispy, Skurmedel, Ben Hoyt และ Peter Hansen นี่จะเป็นทางออกที่ดีที่สุดสำหรับการประมวลผลไฟล์ไบนารีทีละหนึ่งไบต์:

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

สำหรับไพ ธ อนรุ่น 2.6 ขึ้นไปเพราะ:

  • หลามบัฟเฟอร์ภายใน - ไม่จำเป็นต้องอ่านชิ้น
  • หลักการของ DRY - อย่าทำซ้ำบรรทัดการอ่าน
  • ด้วยคำสั่งทำให้มั่นใจได้ว่าไฟล์ที่สะอาดปิด
  • 'byte' หาค่าเป็น false เมื่อไม่มีไบต์มากขึ้น (ไม่ใช่เมื่อไบต์เป็นศูนย์)

หรือใช้โซลูชัน JF Sebastians เพื่อความเร็วที่ดีขึ้น

from functools import partial

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

หรือถ้าคุณต้องการให้มันเป็นฟังก์ชั่นเครื่องกำเนิดไฟฟ้าเช่นการสาธิตโดย 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)

2
ตามที่คำตอบที่เชื่อมโยงกล่าวไว้การอ่าน / การประมวลผลทีละหนึ่งไบต์ยังคงช้าใน Python แม้ว่าการอ่านจะถูกบัฟเฟอร์ สามารถปรับปรุงประสิทธิภาพได้อย่างมากหากสามารถประมวลผลทีละหลายไบต์ได้ตามตัวอย่างในคำตอบที่เชื่อมโยง: 1.5GB / s เทียบกับ 7.5MB / s
jfs

6

Python 3 อ่านไฟล์ทั้งหมดในครั้งเดียว:

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

คุณสามารถทำซ้ำสิ่งที่คุณต้องการโดยใช้dataตัวแปร


6

หลังจากลองทั้งหมดข้างต้นและใช้คำตอบจาก @Aaron Hall ฉันได้รับข้อผิดพลาดของหน่วยความจำสำหรับไฟล์ ~ 90 Mb บนคอมพิวเตอร์ที่ใช้ Window 10, 8 Gb RAM และ Python 3.5 32 บิต เพื่อนร่วมงานของฉันได้รับคำแนะนำให้ใช้numpyแทนและใช้งานได้อย่างมหัศจรรย์

เท่าที่ผ่านมาเร็วที่สุดในการอ่านไฟล์ไบนารีทั้งหมด (ที่ฉันได้ทดสอบ) คือ:

import numpy as np

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

การอ้างอิง

หลากหลายเร็วกว่าวิธีอื่นใด หวังว่ามันจะช่วยให้ใครบางคน!


3
ดี แต่ไม่สามารถใช้กับไฟล์ไบนารีที่มีชนิดข้อมูลต่าง ๆ ได้
Nirmal

@Nirmal: คำถามเกี่ยวกับการวนซ้ำไปถึงไบต์ดังนั้นจึงไม่ชัดเจนหากความคิดเห็นของคุณเกี่ยวกับชนิดข้อมูลที่แตกต่างกันมีผลกระทบใด ๆ
martineau

1
Rick: รหัสของคุณไม่ได้ทำในสิ่งเดียวกันกับคนอื่น หากที่เพิ่มลงไปก็ไม่ได้เร็วกว่าที่ใด ๆ ส่วนใหญ่ของผู้อื่นตามอย่างน้อยตามผลในของฉันมาตรฐาน ในความเป็นจริงดูเหมือนว่าจะเป็นหนึ่งในวิธีที่ช้ากว่า หากการประมวลผลที่ทำกับแต่ละไบต์ (สิ่งที่อาจเป็น) เป็นสิ่งที่สามารถทำได้ผ่านnumpyก็อาจจะคุ้มค่า
martineau

@ Martineau ขอบคุณสำหรับความคิดเห็นของคุณใช่ฉันเข้าใจว่าคำถามนั้นเกี่ยวกับการวนซ้ำแต่ละไบต์และไม่เพียงแค่โหลดทุกอย่างในครั้งเดียว แต่มีคำตอบอื่น ๆ ในคำถามนี้ซึ่งชี้ไปที่การอ่านเนื้อหาทั้งหมดและด้วยเหตุนี้คำตอบของฉัน
Rick M.

4

หากคุณมีจำนวนมากของข้อมูลไบนารีเพื่ออ่านคุณอาจต้องการที่จะต้องพิจารณาโมดูล struct มีการบันทึกไว้ว่าเป็นการแปลง "ระหว่างชนิด C และ Python" แต่แน่นอนไบต์เป็นไบต์และไม่ว่าจะถูกสร้างเป็นประเภท C หรือไม่ก็ตาม ตัวอย่างเช่นหากข้อมูลไบนารีของคุณมีจำนวนเต็ม 2 ไบต์และสองจำนวนเต็ม 4 ไบต์คุณสามารถอ่านได้ดังนี้ (ตัวอย่างที่นำมาจากstructเอกสารประกอบ):

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

คุณอาจพบว่าสะดวกกว่าเร็วกว่าหรือทั้งสองอย่างมากกว่าการวนลูปอย่างชัดเจนกับเนื้อหาของไฟล์


4

โพสต์นี้ไม่ได้เป็นคำตอบสำหรับคำถามโดยตรง สิ่งที่เป็นแทนคือเบนช์มาร์กแบบขยายข้อมูลที่ขับเคลื่อนด้วยข้อมูลซึ่งสามารถนำมาใช้เพื่อเปรียบเทียบคำตอบมากมาย (และความหลากหลายของการใช้คุณสมบัติใหม่ที่เพิ่มเข้ามาในภายหลัง, เวอร์ชั่นใหม่ที่ทันสมัยกว่าของ Python) ที่โพสต์คำถามนี้ มีประโยชน์ในการพิจารณาว่ามีประสิทธิภาพดีที่สุด

ในบางกรณีฉันได้แก้ไขโค้ดในคำตอบอ้างอิงเพื่อให้เข้ากันได้กับเกณฑ์มาตรฐาน

อันดับแรกนี่คือผลลัพธ์สำหรับ 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

ฉันวิ่งไปพร้อมกับไฟล์ทดสอบ 10 MiB ที่มีขนาดใหญ่กว่ามาก (ซึ่งใช้เวลาเกือบหนึ่งชั่วโมงในการรัน) และได้รับผลการดำเนินงานซึ่งเทียบได้กับที่แสดงไว้ด้านบน

นี่คือรหัสที่ใช้ในการทำการเปรียบเทียบ:

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()

คุณกำลังสมมติว่าฉันทำyield from chunkแทนfor byte in chunk: yield byte? ฉันคิดว่าฉันควรกระชับคำตอบของฉันด้วย
Aaron Hall

@Aaron: มีสองรุ่นคำตอบของคุณในหลาม 3 yield fromผลการหนึ่งของพวกเขาใช้และ
martineau

ตกลงฉันได้อัพเดตคำตอบแล้ว ฉันขอแนะนำให้คุณทำแบบหล่นenumerateเนื่องจากควรทำซ้ำเพื่อให้สมบูรณ์ - ถ้าไม่ฉันตรวจสอบครั้งสุดท้าย - การแจกแจงมีค่าใช้จ่ายเล็กน้อยกับค่าใช้จ่ายในการทำบัญชีสำหรับดัชนีด้วย + = 1 ดังนั้นคุณอาจทำบัญชีใน รหัสของตัวเอง หรือแม้กระทั่งการส่งผ่านไปยัง deque maxlen=0ด้วย
Aaron Hall

@Aaron: enumerateเห็นด้วยเกี่ยวกับ ขอบคุณสำหรับความคิดเห็น. จะเพิ่มการอัปเดตในโพสต์ของฉันที่ไม่มี (แม้ว่าฉันไม่คิดว่ามันจะเปลี่ยนแปลงผลลัพธ์มากนัก) จะเพิ่มnumpyคำตอบที่เป็นพื้นฐานของ @ M. Rick
martineau

การทบทวนโค้ดอีกเล็กน้อย: ฉันไม่คิดว่ามันสมเหตุสมผลนักที่จะเขียนคำตอบของ Python 2 ในตอนนี้ - ฉันจะลองลบ Python 2 เพราะฉันคาดว่าคุณจะใช้ 64 bit Python 3.7 หรือ 3.8 คุณสามารถตั้งค่าการล้างข้อมูลให้จบได้ด้วย atexit และแอปพลิเคชั่นบางส่วน พิมพ์ผิด: "ยืนยัน" ฉันไม่รู้สึกถึงความซ้ำซ้อนของชุดทดสอบ - พวกเขาแตกต่างกันหรือไม่? ฉันคิดว่าถ้าคุณใช้super().แทนtuple.ในของคุณ__new__คุณสามารถใช้namedtupleชื่อแอตทริบิวต์แทนของดัชนี
Aaron Hall

3

หากคุณกำลังมองหาบางสิ่งที่รวดเร็วนี่เป็นวิธีที่ฉันใช้มาซึ่งได้ผลมาหลายปี:

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)

ถ้าคุณต้องการที่จะวนซ้ำตัวอักษรแทน ints คุณสามารถใช้data = file.read()ซึ่งควรจะเป็นวัตถุไบต์ () ใน py3


1
'array' ถูกนำเข้าโดย 'จากอาร์เรย์การนำเข้า array'
quanly_mc

@quanly_mc ใช่ขอบคุณสำหรับการจับและขอโทษที่ฉันลืมที่จะรวมแก้ไขในขณะนี้
Tcll
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.