Metodo pigro per leggere file di grandi dimensioni in Python?


290

Ho un file molto grande da 4 GB e quando provo a leggerlo il mio computer si blocca. Quindi voglio leggerlo pezzo per pezzo e dopo aver elaborato ogni pezzo immagazzinare il pezzo elaborato in un altro file e leggere il pezzo successivo.

Esiste un metodo per yieldquesti pezzi?

Mi piacerebbe avere un metodo pigro .

Risposte:


424

Per scrivere una funzione pigra, basta usare 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)

Un'altra opzione sarebbe quella di utilizzare itere una funzione di supporto:

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

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

Se il file è basato su linee, l'oggetto file è già un generatore di linee pigro:

for line in open('really_big_file.dat'):
    process_data(line)

Quindi la linea f = open('really_big_file.dat')è solo un puntatore senza alcun consumo di memoria? (Voglio dire che la memoria consumata è la stessa indipendentemente dalle dimensioni del file?) In che modo influirà sulle prestazioni se utilizzo urllib.readline () invece di f.readline ()?
— somma

4
Buona pratica per usare open ('really_big_file.dat', 'rb') per la compatibilità con i nostri Windows con problemi di Posix tramite i colleghi.
— Tal Weiss,

6
Manca rbcome menzionato @Tal Weiss; e manca una file.close()dichiarazione (potrebbe essere utile with open('really_big_file.dat', 'rb') as f:per ottenere lo stesso risultato; vedere qui per un'altra concisa implementazione
— cod3monk3y,

4
@ cod3monk3y: i file di testo e binari sono cose diverse. Entrambi i tipi sono utili ma in diversi casi. La modalità di default (testo) può essere utile qui vale a dire, 'rb'è non manca.
— jfs,

2
@ jf-sebastian: vero, l'OP non ha specificato se stava leggendo dati testuali o binari. Ma se sta usando Python 2.7 su Windows e sta leggendo dati binari, vale sicuramente la pena notare che se dimentica i 'b'suoi dati molto probabilmente saranno danneggiati . Dai documenti -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

Se il tuo computer, sistema operativo e python sono a 64 bit , puoi utilizzare il modulo mmap per mappare il contenuto del file in memoria e accedervi con indici e sezioni. Ecco un esempio dalla documentazione:

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

Se il computer, il sistema operativo o il python sono a 32 bit , i file di grandi dimensioni che possono essere mappati possono riservare gran parte dello spazio degli indirizzi e ridurre la fame del programma di memoria.


7
Come dovrebbe funzionare? Cosa succede se ho un file da 32 GB? Cosa succede se sono su una VM con 256 MB di RAM? Creare un file così grande non è mai una buona cosa.
— Savino Sguera,

4
Questa risposta merita un voto di -12. Questo ucciderà chiunque lo usi per file di grandi dimensioni.
— Phyo Arkar Lwin

23
Questo può funzionare su un Python a 64 bit anche per file di grandi dimensioni. Anche se il file è mappato in memoria, non viene letto in memoria, quindi la quantità di memoria fisica può essere molto più piccola della dimensione del file.
— punti

1
@SavinoSguera è importante la dimensione della memoria fisica con il mmaping di un file?
— Nick T,

17
@ V3ss0n: ho provato a mmap file da 32 GB su Python a 64 bit. Funziona (ho una RAM inferiore a 32 GB): posso accedere all'inizio, al centro e alla fine del file utilizzando sia l'interfaccia Sequence che il file.
— jfs,

37

file.readlines() accetta un argomento di dimensione opzionale che approssima il numero di righe lette nelle righe restituite.

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)

1
è davvero una grande idea, specialmente quando è combinato con il default dict per dividere i big data in quelli più piccoli.
— Frank Wang,

4
Consiglierei di .read()non usare .readlines(). Se il file è binario non avrà interruzioni di riga.
— Myers Carpenter,

1
Cosa succede se il file è una stringa enorme?
— MattSom

28

Esistono già molte risposte valide, ma se l'intero file si trova su una sola riga e si desidera ancora elaborare "righe" (anziché blocchi di dimensioni fisse), queste risposte non saranno di aiuto.

Il 99% delle volte, è possibile elaborare i file riga per riga. Quindi, come suggerito in questa risposta , è possibile utilizzare l'oggetto file stesso come generatore pigro:

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

Tuttavia, una volta mi sono imbattuto in un file a riga singola molto grande (quasi), in cui il separatore di riga non era in realtà '\n'ma '|'.

  • La lettura riga per riga non era un'opzione, ma avevo ancora bisogno di elaborarla riga per riga.
  • Anche la conversione '|'in '\n'prima dell'elaborazione era fuori questione, poiché alcuni dei campi di questo CSV contenevano '\n'(input utente di testo libero).
  • L'uso della libreria CSV è stato anche escluso perché il fatto che, almeno nelle prime versioni della libreria, è hardcoded per leggere l'input riga per riga .

Per questo tipo di situazioni, ho creato il seguente frammento:

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

Sono stato in grado di usarlo con successo per risolvere il mio problema. È stato ampiamente testato, con varie dimensioni di pezzi.


Test suite, per chi vuole convincersi.

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)

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

AGGIORNAMENTO: l'approccio è meglio spiegato in https://stackoverflow.com/a/4566523/38592


Funziona bene con i BLOB, ma potrebbe non essere utile per i contenuti separati da riga (come CSV, HTML, ecc. Dove l'elaborazione deve essere gestita riga per riga)
— cgseller

7

Fare riferimento alla documentazione ufficiale di Python https://docs.python.org/zh-cn/3/library/functions.html?#iter

Forse questo metodo è più pitonico:

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)

3

Penso che possiamo scrivere così:

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)

2

non posso commentare a causa della mia scarsa reputazione, ma la soluzione SilentGhosts dovrebbe essere molto più semplice con file.readlines ([sizehint])

metodi di file python

modifica: SilentGhost ha ragione, ma questo dovrebbe essere migliore di:

s = "" 
for i in xrange(100): 
   s += file.next()

ok, scusa, hai assolutamente ragione. ma forse questa soluzione ti renderà più felice;): s = "" per i in xrange (100): s + = file.next ()
— sinzi

1
-1: soluzione terribile, ciò significherebbe creare una nuova stringa in memoria ogni riga e copiare i dati dell'intero file letti nella nuova stringa. Le peggiori prestazioni e memoria.
— nosklo,

perché dovrebbe copiare tutti i dati del file in una nuova stringa? dalla documentazione di Python: al fine di rendere un ciclo for il modo più efficiente di eseguire il looping sulle righe di un file (un'operazione molto comune), il metodo next () utilizza un buffer read-ahead nascosto.
— sinzi,

3
@sinzi: "s + =" o concatenare stringhe crea ogni volta una nuova copia della stringa, poiché la stringa è immutabile, quindi stai creando una nuova stringa.
— nosklo,

1
@nosklo: questi sono i dettagli dell'implementazione, la comprensione dell'elenco può essere usata al suo posto
— SilentGhost

1

Sono in una situazione un po 'simile. Non è chiaro se si conoscono le dimensioni del blocco in byte; Di solito no, ma è noto il numero di record (righe) richiesti:

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

Aggiornamento : grazie nosklo. Ecco cosa intendevo dire. Funziona quasi, tranne per il fatto che perde una linea "tra" blocchi.

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

Il trucco non perde alcuna linea, ma non sembra molto bello.


1
è questo pseudo codice? non funzionerà. È anche inutile confondere, dovresti rendere il numero di righe un parametro opzionale per la funzione get_line.
— nosklo,

0

Per elaborare riga per riga, questa è una soluzione elegante:

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

Finché non ci sono righe vuote.


6
Questo è solo un equivalente eccessivamente complicato, meno robusto e più lento di quello che opengià ti dà. Un file è già un iteratore sulle sue linee.
— abarnert,

-2

puoi usare il seguente codice.

file_obj = open('big_file') 

open () restituisce un oggetto file

quindi utilizzare os.stat per ottenere le dimensioni

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

for i in range( file_size/1024):
    print file_obj.read(1024)

non leggerebbe l'intero file se la dimensione non fosse un moltiplicatore di 1024
— kmaork il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.