¿Método perezoso para leer archivos grandes en Python?


290

Tengo un archivo muy grande de 4GB y cuando intento leerlo, mi computadora se cuelga. Así que quiero leerlo pieza por pieza y después de procesar cada pieza, almacene la pieza procesada en otro archivo y lea la siguiente pieza.

¿Hay algún método para yieldestas piezas?

Me encantaría tener un método perezoso .

Respuestas:


424

Para escribir una función perezosa, solo use 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)

Otra opción sería usar iteruna función auxiliar:

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

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

Si el archivo está basado en líneas, el objeto del archivo ya es un generador de líneas diferido:

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

Entonces, ¿la línea f = open('really_big_file.dat')es solo un puntero sin consumo de memoria? (Quiero decir que la memoria consumida es la misma independientemente del tamaño del archivo). ¿Cómo afectará el rendimiento si uso urllib.readline () en lugar de f.readline ()?
Sumid

44
Es una buena práctica usar open ('really_big_file.dat', 'rb') para la compatibilidad con nuestro Windows desafiado por Posix usando colegas.
Tal Weiss

66
Desaparecido rbcomo mencionó @Tal Weiss; y falta una file.close()declaración (podría usar with open('really_big_file.dat', 'rb') as f:para lograr lo mismo; Vea aquí para otra implementación concisa
cod3monk3y

44
@ cod3monk3y: los archivos de texto y binarios son cosas diferentes. Ambos tipos son útiles pero en diferentes casos. El modo predeterminado (texto) puede ser útil aquí, 'rb'es decir, no falta.
jfs

2
@ jf-sebastian: cierto, el OP no especificó si estaba leyendo datos textuales o binarios. Pero si está usando Python 2.7 en Windows y está leyendo datos binarios, ciertamente vale la pena señalar que si olvida los 'b'datos, es muy probable que se corrompan . Desde los documentos -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

Si su computadora, sistema operativo y python son de 64 bits , entonces puede usar el módulo mmap para mapear el contenido del archivo en la memoria y acceder a él con índices y sectores. Aquí un ejemplo de la documentación:

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

Si su computadora, sistema operativo o python son de 32 bits , entonces el mapeo de archivos grandes puede reservar grandes partes de su espacio de direcciones y privar a su programa de memoria.


77
¿Cómo se supone que esto funcione? ¿Qué pasa si tengo un archivo de 32 GB? ¿Qué sucede si estoy en una máquina virtual con 256 MB de RAM? Mmapping un archivo tan grande nunca es algo bueno.
Savino Sguera

44
Esta respuesta merece un voto de -12. Esto matará a cualquiera que lo use para archivos grandes.
Phyo Arkar Lwin

23
Esto puede funcionar en un Python de 64 bits incluso para archivos grandes. Aunque el archivo está mapeado en memoria, no se lee en la memoria, por lo que la cantidad de memoria física puede ser mucho menor que el tamaño del archivo.
pts

1
@SavinoSguera ¿Importa el tamaño de la memoria física al mapear un archivo?
Nick T

17
@ V3ss0n: He intentado mmapar un archivo de 32 GB en Python de 64 bits. Funciona (tengo menos de 32 GB de RAM): puedo acceder al inicio, a la mitad y al final del archivo utilizando las interfaces de secuencia y de archivo.
jfs

37

file.readlines() toma un argumento de tamaño opcional que se aproxima al número de líneas leídas en las líneas devueltas.

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
es una idea realmente genial, especialmente cuando se combina con el veredicto predeterminado para dividir grandes datos en pequeños.
Frank Wang

44
Yo recomendaría usar .read()no .readlines(). Si el archivo es binario, no tendrá saltos de línea.
Myers Carpenter

1
¿Qué pasa si el archivo es una cadena enorme?
MattSom

28

Ya hay muchas buenas respuestas, pero si su archivo completo está en una sola línea y aún desea procesar "filas" (en lugar de bloques de tamaño fijo), estas respuestas no lo ayudarán.

El 99% del tiempo, es posible procesar archivos línea por línea. Luego, como se sugiere en esta respuesta , puede usar el objeto de archivo en sí mismo como generador diferido:

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

Sin embargo, una vez me encontré con un archivo muy, muy grande (casi) de una sola línea, donde el separador de filas no era '\n'sino '|'.

  • Leer línea por línea no era una opción, pero aún necesitaba procesarlo fila por fila.
  • La conversión '|'a '\n'antes del procesamiento también estaba fuera de discusión, porque algunos de los campos de este csv contenían '\n'(entrada del usuario de texto libre).
  • El uso de la biblioteca csv también se descartó porque el hecho de que, al menos en las primeras versiones de la lib, está codificado para leer la entrada línea por línea .

Para este tipo de situaciones, creé el siguiente fragmento:

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

Pude usarlo con éxito para resolver mi problema. Ha sido ampliamente probado, con varios tamaños de trozos.


Test suite, para aquellos que quieran convencerse a sí mismos.

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

ACTUALIZACIÓN: El enfoque se explica mejor en https://stackoverflow.com/a/4566523/38592


Esto funciona bien para blobs, pero puede no ser bueno para contenido separado por línea (como CSV, HTML, etc., donde el procesamiento debe manejarse línea por línea)
cgseller

7

Consulte la documentación oficial de Python https://docs.python.org/zh-cn/3/library/functions.html?#iter

Quizás este método sea más pitónico:

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

Creo que podemos escribir así:

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

No puedo comentar debido a mi baja reputación, pero la solución SilentGhosts debería ser mucho más fácil con file.readlines ([sizehint])

métodos de archivo de Python

editar: SilentGhost tiene razón, pero esto debería ser mejor que:

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

Ok, lo siento, tienes toda la razón. pero tal vez esta solución te haga más feliz;): s = "" para i en xrange (100): s + = file.next ()
sinzi

1
-1: Solución terrible, esto significaría crear una nueva cadena en la memoria de cada línea y copiar todos los datos del archivo leídos en la nueva cadena. El peor rendimiento y memoria.
nosklo

¿por qué copiaría todos los datos del archivo en una nueva cadena? de la documentación de Python: para que un bucle for sea la forma más eficiente de recorrer las líneas de un archivo (una operación muy común), el método next () utiliza un búfer de lectura anticipada oculto.
sinzi

3
@sinzi: "s + =" o concatenando cadenas hace una nueva copia de la cadena cada vez, ya que la cadena es inmutable, por lo que está creando una nueva cadena.
nosklo

1
@nosklo: estos son detalles de implementación, lista por comprensión se puede utilizar en su lugar
SilentGhost

1

Estoy en una situación algo similar. No está claro si conoce el tamaño del fragmento en bytes; Usualmente no, pero se conoce la cantidad de registros (líneas) que se requiere:

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

Actualización : Gracias nosklo. Esto es lo que quise decir. Casi funciona, excepto que pierde una línea 'entre' trozos.

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

¿El truco sin perder ninguna línea, pero no se ve muy bien?


1
es este pseudocódigo? No funcionará. También es innecesariamente confuso, debe hacer que el número de líneas sea un parámetro opcional para la función get_line.
nosklo

0

Para procesar línea por línea, esta es una solución elegante:

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

Mientras no haya líneas en blanco.


66
Esto es simplemente un equivalente demasiado complicado, menos robusto y más lento de lo que openya le ofrece. Un archivo ya es un iterador sobre sus líneas.
abarnert

-2

Puedes usar el siguiente código.

file_obj = open('big_file') 

open () devuelve un objeto de archivo

luego use os.stat para obtener el tamaño

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

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

no leer todo el archivo si el tamaño no es un múltiplo del 1024
kmaork
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.