Método preguiçoso para ler arquivos grandes em Python?

290

Eu tenho um arquivo muito grande de 4 GB e quando tento lê-lo, meu computador trava. Então, eu quero ler peça por peça e depois de processar cada peça, armazene a peça processada em outro arquivo e leia a peça seguinte.

Existe algum método para yieldessas peças?

Eu adoraria ter um método preguiçoso .

Pratik Deoghare
fonte

Respostas:

424

Para escrever uma função lenta, basta usar 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)

Outra opção seria usar itere uma função auxiliar:

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

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

Se o arquivo for baseado em linhas, o objeto de arquivo já será um gerador lento de linhas:

for line in open('really_big_file.dat'):
    process_data(line)
nosklo
fonte
Então a linha f = open('really_big_file.dat')é apenas um ponteiro sem consumo de memória? (Quero dizer que a memória consumida é a mesma, independentemente do tamanho do arquivo?) Como isso afetará o desempenho se eu usar urllib.readline () em vez de f.readline ()?
sumid
4
Boas práticas para usar open ('really_big_file.dat', 'rb') para compatibilidade com nosso Windows desafiado por Posix usando colegas.
Tal Weiss
6
Falta rbcomo @Tal Weiss mencionado; e faltando uma file.close()declaração (poderia usar with open('really_big_file.dat', 'rb') as f:para realizar mesma; Consulte aqui para outra implementação concisa
cod3monk3y
4
@ cod3monk3y: arquivos de texto e binários são coisas diferentes. Ambos os tipos são úteis, mas em casos diferentes. O modo padrão (texto) pode ser útil aqui, isto 'rb'é , não está faltando.
JFS
2
@ jf-sebastian: true, o OP não especificou se ele estava lendo dados textuais ou binários. Mas se ele estiver usando o python 2.7 no Windows e estiver lendo dados binários, certamente vale a pena notar que, se ele esquecer, os 'b'dados dele provavelmente estarão corrompidos . A partir dos 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

Se o seu computador, SO e python forem de 64 bits , você poderá usar o módulo mmap para mapear o conteúdo do arquivo na memória e acessá-lo com índices e fatias. Aqui está um exemplo da documentação:

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 o seu computador, sistema operacional ou python tiver 32 bits , os arquivos grandes do mmap podem reservar grandes partes do seu espaço de endereço e deixar seu programa de memória com fome .

Comunidade
fonte
7
Como é que isso deveria funcionar? E se eu tiver um arquivo de 32 GB? E se eu estiver em uma VM com 256 MB de RAM? Mapear um arquivo tão grande nunca é realmente uma coisa boa.
Savino Sguera
4
Esta resposta merece um voto de -12. Isso matará qualquer um que usar isso para arquivos grandes.
Phyo Arkar Lwin
23
Isso pode funcionar em um Python de 64 bits, mesmo para arquivos grandes. Embora o arquivo seja mapeado na memória, ele não é lido na memória, portanto, a quantidade de memória física pode ser muito menor que o tamanho do arquivo.
pts
1
@SavinoSguera o tamanho da memória física importa com o mapeamento de um arquivo?
Nick T
17
@ V3ss0n: Tentei mmapar um arquivo de 32GB no Python de 64 bits. Funciona (tenho RAM com menos de 32 GB): posso acessar o início, o meio e o fim do arquivo usando as interfaces Sequence e File.
jfs
37

file.readlines() recebe um argumento de tamanho opcional que aproxima o número de linhas lidas nas linhas retornadas.

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
fonte
1
é realmente uma ótima idéia, especialmente quando combinada com o decreto padrão para dividir big data em menores.
Frank Wang
4
Eu recomendaria .read()não usar .readlines(). Se o arquivo for binário, não haverá quebras de linha.
Myers Carpenter
1
E se o arquivo for uma string enorme?
MattSom
28

Já existem muitas respostas boas, mas se seu arquivo inteiro estiver em uma única linha e você ainda desejar processar "linhas" (em oposição a blocos de tamanho fixo), essas respostas não ajudarão.

99% do tempo, é possível processar arquivos linha por linha. Então, como sugerido nesta resposta , você pode usar o próprio objeto de arquivo como gerador lento:

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

No entanto, uma vez encontrei um arquivo de linha única muito grande (quase), onde o separador de linha não era, de fato, '\n'mas '|'.

  • Ler linha por linha não era uma opção, mas eu ainda precisava processá-la linha por linha.
  • A conversão '|'para '\n'antes do processamento também estava fora de questão, porque alguns dos campos deste csv continham '\n'(entrada de usuário de texto livre).
  • O uso da biblioteca csv também foi descartado porque o fato de que, pelo menos nas versões anteriores da lib, é codificado para ler a entrada de linha por linha .

Para esse tipo de situação, criei o seguinte snippet:

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

Consegui usá-lo com sucesso para resolver meu problema. Foi extensivamente testado, com vários tamanhos de bloco.


Conjunto de testes, para quem quer se convencer.

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

ATUALIZAÇÃO: A abordagem é melhor explicada em https://stackoverflow.com/a/4566523/38592

myroslav
fonte
Isso funciona bem para manchas, mas pode não ser bom para a linha separada de conteúdo (como CSV, HTML, etc, onde o processamento precisa ser tratada linha por linha)
cgseller
7

Consulte a documentação oficial do python https://docs.python.org/zh-cn/3/library/functions.html?#iter

Talvez este método seja mais 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)
Bruce
fonte
3

Eu acho que podemos escrever assim:

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
fonte
2

não tenho permissão para comentar devido à minha baixa reputação, mas a solução SilentGhosts deve ser muito mais fácil com file.readlines ([sizehint])

métodos de arquivo python

edit: SilentGhost está certo, mas isso deve ser melhor do que:

s = "" 
for i in xrange(100): 
   s += file.next()
sinzi
fonte
ok, desculpe, você está absolutamente certo. mas talvez esta solução vai fazer você feliz;): s = "" for i in xrange (100): s + = file.next ()
Sinzi
1
-1: solução terrível, isso significaria criar uma nova string na memória de cada linha e copiar todos os dados do arquivo lidos na nova string. O pior desempenho e memória.
6339 nosklo
por que copiaria todos os dados do arquivo em uma nova string? da documentação do python: Para tornar um loop for a maneira mais eficiente de repetir as linhas de um arquivo (uma operação muito comum), o método next () usa um buffer de leitura antecipada oculto.
Sinzi
3
@sinzi: "s + =" ou concatenar strings faz uma nova cópia da string a cada vez, uma vez que a string é imutável, então você está criando uma nova string.
6339 nosklo
1
@nosklo: estes são detalhes de implementação, compreensão da lista pode ser usado em seu lugar
SilentGhost
1

Estou em uma situação parecida. Não está claro se você sabe o tamanho do pedaço em bytes; Normalmente não, mas o número de registros (linhas) necessário é conhecido:

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

Atualização : Obrigado nosklo. Aqui está o que eu quis dizer. Quase funciona, exceto que perde uma linha 'entre' pedaços.

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

O truque sem perder linhas, mas não parece muito agradável.

SilentGhost
fonte
1
é esse pseudo-código? não vai funcionar. Também é desnecessário confundir, você deve tornar o número de linhas um parâmetro opcional para a função get_line.
6339 nosklo
0

Para processar linha por linha, esta é uma solução elegante:

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

Contanto que não haja linhas em branco.

crizCraig
fonte
6
Este é apenas um equivalente excessivamente complicado, menos robusto e mais lento ao que openvocê já oferece. Um arquivo é um iterador sobre suas linhas.
22813 abarnert #
-2

você pode usar o seguinte código.

file_obj = open('big_file') 

open () retorna um objeto de arquivo

então use os.stat para obter tamanho

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

for i in range( file_size/1024):
    print file_obj.read(1024)
estridente
fonte
não iria ler o arquivo inteiro se o tamanho não é uma multiplicação de 1024
kmaork