Faule Methode zum Lesen großer Dateien in Python?

290

Ich habe eine sehr große Datei mit 4 GB und wenn ich versuche, sie zu lesen, hängt mein Computer. Also möchte ich es Stück für Stück lesen und nach der Verarbeitung jedes Stücks das verarbeitete Stück in einer anderen Datei speichern und das nächste Stück lesen.

Gibt es eine Methode für yielddiese Stücke?

Ich hätte gerne eine faule Methode .

Pratik Deoghare
quelle

Antworten:

424

Um eine Lazy-Funktion zu schreiben, verwenden Sie einfach yield:

def read_in_chunks(file_object, chunk_size=1024):
    """Lazy function (generator) to read a file piece by piece.
    Default chunk size: 1k."""
    while True:
        data = file_object.read(chunk_size)
        if not data:
            break
        yield data


with open('really_big_file.dat') as f:
    for piece in read_in_chunks(f):
        process_data(piece)

Eine andere Option wäre die Verwendung iterund eine Hilfsfunktion:

f = open('really_big_file.dat')
def read1k():
    return f.read(1024)

for piece in iter(read1k, ''):
    process_data(piece)

Wenn die Datei zeilenbasiert ist, ist das Dateiobjekt bereits ein verzögerter Zeilengenerator:

for line in open('really_big_file.dat'):
    process_data(line)
nosklo
quelle
Die Zeile f = open('really_big_file.dat')ist also nur ein Zeiger ohne Speicherverbrauch? (Ich meine, der verbrauchte Speicher ist unabhängig von der Dateigröße gleich?) Wie wirkt sich dies auf die Leistung aus, wenn ich urllib.readline () anstelle von f.readline () verwende?
Sumid
4
Es wird empfohlen, open ('Really_big_file.dat', 'rb') zu verwenden, um die Kompatibilität mit unserem von Posix herausgeforderten Windows unter Verwendung von Kollegen zu gewährleisten.
Tal Weiss
6
Vermisst rbwie @Tal Weiss erwähnt; und fehlende file.close()Aussage (könnte verwendet werden with open('really_big_file.dat', 'rb') as f:, um dasselbe zu erreichen; siehe hier für eine andere prägnante Implementierung
cod3monk3y
4
@ cod3monk3y: Text- und Binärdateien sind verschiedene Dinge. Beide Typen sind nützlich, aber in unterschiedlichen Fällen. Der Standard (Text) Modus kann nützlich sein , hier also 'rb'wird nicht fehlen.
JFS
2
@ jf-sebastian: true, das OP hat nicht angegeben, ob er Text- oder Binärdaten liest. Aber wenn er mit Python 2.7 auf Windows - und ist Lesen von Binärdaten, es ist sicherlich erwähnenswert, dass , wenn er das vergisst 'b'seine Daten sehr wahrscheinlich beschädigt werden . Aus den Dokumenten -Python on Windows makes a distinction between text and binary files; [...] it’ll corrupt binary data like that in JPEG or EXE files. Be very careful to use binary mode when reading and writing such files.
cod3monk3y
41

Wenn Ihr Computer, Betriebssystem und Python 64-Bit sind , können Sie das mmap-Modul verwenden , um den Inhalt der Datei in den Speicher abzubilden und mit Indizes und Slices darauf zuzugreifen. Hier ein Beispiel aus der Dokumentation:

import mmap
with open("hello.txt", "r+") as f:
    # memory-map the file, size 0 means whole file
    map = mmap.mmap(f.fileno(), 0)
    # read content via standard file methods
    print map.readline()  # prints "Hello Python!"
    # read content via slice notation
    print map[:5]  # prints "Hello"
    # update content using slice notation;
    # note that new content must have same size
    map[6:] = " world!\n"
    # ... and read again using standard file methods
    map.seek(0)
    print map.readline()  # prints "Hello  world!"
    # close the map
    map.close()

Wenn entweder Ihr Computer, Ihr Betriebssystem oder Python 32-Bit sind , kann das Zuordnen großer Dateien große Teile Ihres Adressraums reservieren und Ihr Speicherprogramm hungern lassen .

Gemeinschaft
quelle
7
Wie soll das funktionieren? Was ist, wenn ich eine 32-GB-Datei habe? Was ist, wenn ich auf einer VM mit 256 MB RAM bin? Das Zuordnen einer so großen Datei ist wirklich nie eine gute Sache.
Savino Sguera
4
Diese Antwort verdient eine -12 Stimme. Dies wird jeden töten, der das für große Dateien verwendet.
Phyo Arkar Lwin
23
Dies kann auf einem 64-Bit-Python auch für große Dateien funktionieren. Obwohl die Datei speicherabgebildet ist, wird sie nicht in den Speicher gelesen, sodass der physische Speicher viel kleiner als die Dateigröße sein kann.
Punkte
1
@SavinoSguera spielt die Größe des physischen Speichers beim MMaping einer Datei eine Rolle?
Nick T
17
@ V3ss0n: Ich habe versucht, eine 32-GB-Datei auf 64-Bit-Python zuzuordnen. Es funktioniert (ich habe weniger als 32 GB RAM): Ich kann über die Sequenz- und die Dateischnittstelle auf den Anfang, die Mitte und das Ende der Datei zugreifen.
JFS
37

file.readlines() Nimmt ein optionales Größenargument an, das sich der Anzahl der in den zurückgegebenen Zeilen gelesenen Zeilen annähert.

bigfile = open('bigfilename','r')
tmp_lines = bigfile.readlines(BUF_SIZE)
while tmp_lines:
    process([line for line in tmp_lines])
    tmp_lines = bigfile.readlines(BUF_SIZE)
Anshul
quelle
1
Dies ist eine großartige Idee, insbesondere wenn sie mit dem Standarddikt kombiniert wird, um große Datenmengen in kleinere aufzuteilen.
Frank Wang
4
Ich würde empfehlen, .read()nicht zu verwenden .readlines(). Wenn die Datei binär ist, kommt es nicht zu Zeilenumbrüchen.
Myers Carpenter
1
Was ist, wenn die Datei eine große Zeichenfolge ist?
MattSom
28

Es gibt bereits viele gute Antworten, aber wenn sich Ihre gesamte Datei in einer einzelnen Zeile befindet und Sie dennoch "Zeilen" verarbeiten möchten (im Gegensatz zu Blöcken mit fester Größe), helfen Ihnen diese Antworten nicht weiter.

In 99% der Fälle ist es möglich, Dateien zeilenweise zu verarbeiten. Dann können Sie, wie in dieser Antwort vorgeschlagen , das Dateiobjekt selbst als Lazy Generator verwenden:

with open('big.csv') as f:
    for line in f:
        process(line)

Allerdings bin ich einmal auf eine sehr sehr große (fast) einzeilige Datei gestoßen, in der das Zeilentrennzeichen eigentlich nicht '\n'aber war '|'.

  • Zeile für Zeile zu lesen war keine Option, aber ich musste es trotzdem Zeile für Zeile verarbeiten.
  • Eine Konvertierung '|'in '\n'vor der Verarbeitung kam ebenfalls nicht in Frage, da einige der Felder dieser CSV enthalten waren '\n'( Freitext- Benutzereingabe).
  • Die Verwendung der CSV-Bibliothek wurde ebenfalls ausgeschlossen, da es zumindest in früheren Versionen der Bibliothek schwierig ist, die Eingabe zeilenweise zu lesen .

Für diese Art von Situationen habe ich das folgende Snippet erstellt:

def rows(f, chunksize=1024, sep='|'):
    """
    Read a file where the row separator is '|' lazily.

    Usage:

    >>> with open('big.csv') as f:
    >>>     for r in rows(f):
    >>>         process(row)
    """
    curr_row = ''
    while True:
        chunk = f.read(chunksize)
        if chunk == '': # End of file
            yield curr_row
            break
        while True:
            i = chunk.find(sep)
            if i == -1:
                break
            yield curr_row + chunk[:i]
            curr_row = ''
            chunk = chunk[i+1:]
        curr_row += chunk

Ich konnte es erfolgreich einsetzen, um mein Problem zu lösen. Es wurde ausgiebig mit verschiedenen Blockgrößen getestet.


Testsuite für diejenigen, die sich selbst überzeugen wollen.

test_file = 'test_file'

def cleanup(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        os.unlink(test_file)
    return wrapper

@cleanup
def test_empty(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1_char_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_1_char(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1025_chars_1_row(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1025):
            f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1024_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1023):
            f.write('a')
        f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_1025_chars_1026_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1025):
            f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1026

@cleanup
def test_2048_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1022):
            f.write('a')
        f.write('|')
        f.write('a')
        # -- end of 1st chunk --
        for i in range(1024):
            f.write('a')
        # -- end of 2nd chunk
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_2049_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1022):
            f.write('a')
        f.write('|')
        f.write('a')
        # -- end of 1st chunk --
        for i in range(1024):
            f.write('a')
        # -- end of 2nd chunk
        f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

if __name__ == '__main__':
    for chunksize in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]:
        test_empty(chunksize)
        test_1_char_2_rows(chunksize)
        test_1_char(chunksize)
        test_1025_chars_1_row(chunksize)
        test_1024_chars_2_rows(chunksize)
        test_1025_chars_1026_rows(chunksize)
        test_2048_chars_2_rows(chunksize)
        test_2049_chars_2_rows(chunksize)
user48678
quelle
11
f = ... # file-like object, i.e. supporting read(size) function and 
        # returning empty string '' when there is nothing to read

def chunked(file, chunk_size):
    return iter(lambda: file.read(chunk_size), '')

for data in chunked(f, 65536):
    # process the data

UPDATE: Der Ansatz wird am besten unter https://stackoverflow.com/a/4566523/38592 erläutert

myroslav
quelle
Dies funktioniert gut für Blobs, ist aber möglicherweise nicht gut für
zeilengetrennte
7

Weitere Informationen finden Sie in der offiziellen Dokumentation von Python unter https://docs.python.org/zh-cn/3/library/functions.html?#iter

Vielleicht ist diese Methode pythonischer:

from functools import partial

"""A file object returned by open() is a iterator with
read method which could specify current read's block size"""
with open('mydata.db', 'r') as f_in:

    part_read = partial(f_in.read, 1024*1024)
    iterator = iter(part_read, b'')

    for index, block in enumerate(iterator, start=1):
        block = process_block(block)    # process block data
        with open(f'{index}.txt', 'w') as f_out:
            f_out.write(block)
Bruce
quelle
3

Ich denke, wir können so schreiben:

def read_file(path, block_size=1024): 
    with open(path, 'rb') as f: 
        while True: 
            piece = f.read(block_size) 
            if piece: 
                yield piece 
            else: 
                return

for piece in read_file(path):
    process_piece(piece)
TonyCoolZhu
quelle
2

Ich darf aufgrund meines schlechten Rufs keine Kommentare abgeben, aber die SilentGhosts-Lösung sollte mit file.readlines ([sizehint]) viel einfacher sein.

Python-Dateimethoden

edit: SilentGhost ist richtig, aber das sollte besser sein als:

s = "" 
for i in xrange(100): 
   s += file.next()
sinzi
quelle
ok, sorry, du hast absolut recht. aber vielleicht macht dich diese Lösung glücklicher;): s = "" für i in xrange (100): s + = file.next ()
sinzi
1
-1: Schreckliche Lösung, dies würde bedeuten, in jeder Zeile eine neue Zeichenfolge im Speicher zu erstellen und die gesamten gelesenen Dateidaten in die neue Zeichenfolge zu kopieren. Die schlechteste Leistung und Speicher.
Nosklo
Warum sollte es die gesamten Dateidaten in eine neue Zeichenfolge kopieren? aus der Python-Dokumentation: Um eine for-Schleife zur effizientesten Methode zum Durchlaufen der Zeilen einer Datei zu machen (eine sehr häufige Operation), verwendet die next () -Methode einen versteckten Vorauslesepuffer.
Sinzi
3
@sinzi: "s + =" oder das Verketten von Zeichenfolgen erstellt jedes Mal eine neue Kopie der Zeichenfolge, da die Zeichenfolge unveränderlich ist und Sie eine neue Zeichenfolge erstellen.
Nosklo
1
@nosklo: Dies sind Details der Implementierung, Listenverständnis kann an seiner Stelle verwendet werden
SilentGhost
1

Ich bin in einer ähnlichen Situation. Es ist nicht klar, ob Sie die Blockgröße in Bytes kennen. Normalerweise nicht, aber die Anzahl der erforderlichen Datensätze (Zeilen) ist bekannt:

def get_line():
     with open('4gb_file') as file:
         for i in file:
             yield i

lines_required = 100
gen = get_line()
chunk = [i for i, j in zip(gen, range(lines_required))]

Update : Danke nosklo. Hier ist was ich meinte. Es funktioniert fast, außer dass es eine Linie zwischen den Stücken verliert.

chunk = [next(gen) for i in range(lines_required)]

Hat der Trick ohne Zeilenverlust zu verlieren, sieht aber nicht sehr gut aus.

SilentGhost
quelle
1
ist das Pseudocode? es wird nicht funktionieren. Es ist auch unnötig verwirrend, Sie sollten die Anzahl der Zeilen zu einem optionalen Parameter für die Funktion get_line machen.
Nosklo
0

Dies ist eine elegante Lösung, um Zeile für Zeile zu verarbeiten:

  def stream_lines(file_name):
    file = open(file_name)
    while True:
      line = file.readline()
      if not line:
        file.close()
        break
      yield line

Solange es keine Leerzeilen gibt.

crizCraig
quelle
6
Dies ist nur ein übermäßig kompliziertes, weniger robustes und langsameres Äquivalent zu dem, was Sie openbereits erhalten. Eine Datei ist bereits ein Iterator über ihre Zeilen.
abarnert
-2

Sie können folgenden Code verwenden.

file_obj = open('big_file') 

open () gibt ein Dateiobjekt zurück

Verwenden Sie dann os.stat, um die Größe zu ermitteln

file_size = os.stat('big_file').st_size

for i in range( file_size/1024):
    print file_obj.read(1024)
Shrikant
quelle
würde nicht die ganze Datei lesen, wenn die Größe nicht ein Multiplikator von 1024 ist
kmaork