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.