Leniwa metoda odczytu dużego pliku w Pythonie?


290

Mam bardzo duży plik 4 GB i kiedy próbuję go odczytać, komputer zawiesza się. Więc chcę czytać go kawałek po kawałku, a po przetworzeniu każdego elementu przechowywać przetworzony kawałek w innym pliku i czytać następny kawałek.

Czy istnieje jakaś metoda na yieldte elementy?

Chciałbym mieć leniwą metodę .

Odpowiedzi:


424

Aby napisać leniwą funkcję, po prostu użyj 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)

Inną opcją byłoby użycie iterfunkcji pomocniczej:

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

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

Jeśli plik jest oparty na liniach, obiekt pliku jest już leniwym generatorem linii:

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

Czyli linia f = open('really_big_file.dat')jest tylko wskaźnikiem bez zużycia pamięci? (Mam na myśli, że zużyta pamięć jest taka sama bez względu na rozmiar pliku?) Jak wpłynie to na wydajność, jeśli użyję urllib.readline () zamiast f.readline ()?
sumid

4
Dobrą praktyką jest używanie metody open („really_big_file.dat”, „rb”) w celu zapewnienia zgodności z naszymi systemami Windows z wyzwaniem Posix przy użyciu współpracowników.
Tal Weiss

6
Zaginiony, rbjak wspomniał @Tal Weiss; i brak file.close()oświadczenia (może posłużyć with open('really_big_file.dat', 'rb') as f:do osiągnięcia tego samego; zobacz tutaj, aby uzyskać inne zwięzłe wdrożenie
cod3monk3y

4
@ cod3monk3y: pliki tekstowe i binarne to różne rzeczy. Oba typy są przydatne, ale w różnych przypadkach. Domyślny tryb (tekst) może być tu przydatny czyli 'rb'jest nie brakuje.
jfs

2
@ jf-sebastian: true, OP nie określił, czy odczytuje dane tekstowe czy binarne. Ale jeśli on jest przy użyciu Pythona 2.7 na systemie Windows i jest odczyt danych binarnych, to z pewnością warto zwrócić uwagę, że jeśli zapomni 'b'jego dane będą bardzo prawdopodobnie uszkodzony . Z dokumentów -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

Jeśli twój komputer, system operacyjny i python są 64-bitowe , możesz użyć modułu mmap, aby zmapować zawartość pliku do pamięci i uzyskać do niego dostęp za pomocą indeksów i plasterków. Oto przykład z dokumentacji:

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

Jeśli komputer, system operacyjny lub python są 32-bitowe , mapowanie dużych plików może zarezerwować duże części przestrzeni adresowej i zagłodzić program pamięci.


7
Jak to ma działać? Co jeśli mam plik 32 GB? Co jeśli jestem na maszynie wirtualnej z 256 MB pamięci RAM? Mapowanie tak dużego pliku nigdy tak naprawdę nie jest dobrą rzeczą.
Savino Sguera

4
Ta odpowiedź zasługuje na -12 głosów. To zabije każdego, kto używa tego do dużych plików.
Phyo Arkar Lwin

23
Może to działać na 64-bitowym języku Python, nawet w przypadku dużych plików. Mimo że plik jest odwzorowany w pamięci, nie jest on odczytywany do pamięci, więc ilość pamięci fizycznej może być znacznie mniejsza niż rozmiar pliku.
pkt

1
@SavinoSguera czy rozmiar pamięci fizycznej ma znaczenie przy kształtowaniu pliku?
Nick T

17
@ V3ss0n: Próbowałem zmapować plik 32 GB na 64-bitowym języku Python. Działa (mam pamięć RAM mniejszą niż 32 GB): mogę uzyskać dostęp do początku, środka i końca pliku przy użyciu zarówno interfejsu sekwencji, jak i pliku.
jfs

37

file.readlines() przyjmuje opcjonalny argument wielkości, który jest zbliżony do liczby linii odczytanych w zwracanych liniach.

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
to naprawdę świetny pomysł, szczególnie w połączeniu z domyślnym dyktatorem, aby podzielić duże zbiory danych na mniejsze.
Frank Wang

4
Polecam .read()nie używać .readlines(). Jeśli plik jest binarny, nie będzie miał podziałów linii.
Myers Carpenter

1
Co jeśli plik ma jeden ogromny ciąg?
MattSom

28

Istnieje już wiele dobrych odpowiedzi, ale jeśli cały plik znajduje się w jednej linii i nadal chcesz przetwarzać „wiersze” (w przeciwieństwie do bloków o stałym rozmiarze), te odpowiedzi ci nie pomogą.

W 99% przypadków możliwe jest przetwarzanie plików linia po linii. Następnie, zgodnie z sugestią zawartą w tej odpowiedzi , możesz użyć samego obiektu pliku jako leniwego generatora:

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

Jednak kiedyś natknąłem się na bardzo duży (prawie) plik jednowierszowy, w którym separator wierszy był w rzeczywistości nie, '\n'ale '|'.

  • Czytanie wiersz po wierszu nie było opcją, ale nadal musiałem przetwarzać go wiersz po rzędzie.
  • Konwersja '|'do '\n'przed przetwarzaniem również nie wchodziła w rachubę, ponieważ niektóre pola tego '\n'pliku csv zawierały (dowolny tekst wprowadzany przez użytkownika).
  • Korzystanie z biblioteki csv również zostało wykluczone, ponieważ fakt, że przynajmniej we wczesnych wersjach biblioteki lib jest na stałe zakodowany, aby czytać wiersz po wierszu .

W takich sytuacjach utworzyłem następujący fragment:

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

Udało mi się to z powodzeniem wykorzystać do rozwiązania mojego problemu. Został gruntownie przetestowany, z różnymi wielkościami porcji.


Zestaw testowy dla tych, którzy chcą się przekonać.

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

AKTUALIZACJA: Podejście to najlepiej wyjaśniono w https://stackoverflow.com/a/4566523/38592


Działa to dobrze w przypadku obiektów blob, ale może nie być dobre w przypadku treści oddzielonych wierszami (takich jak CSV, HTML itp., W których przetwarzanie musi być obsługiwane wiersz po wierszu)
cgseller

7

Zapoznaj się z oficjalną dokumentacją pythona https://docs.python.org/zh-cn/3/library/functions.html?#iter

Może ta metoda jest bardziej pythonowa:

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

Myślę, że możemy napisać w ten sposób:

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

nie wolno mi komentować z powodu mojej niskiej reputacji, ale rozwiązanie SilentGhosts powinno być znacznie łatwiejsze dzięki file.readlines ([sizehint])

metody plików python

edycja: SilentGhost ma rację, ale powinno to być lepsze niż:

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

ok, przepraszam, masz absolutną rację. ale może to rozwiązanie sprawi, że będziesz szczęśliwszy;): s = "" for i in xrange (100): s + = file.next ()
sinzi

1
-1: Straszne rozwiązanie, oznaczałoby to utworzenie nowego ciągu w pamięci dla każdego wiersza i skopiowanie wszystkich danych pliku odczytanych do nowego ciągu. Najgorsza wydajność i pamięć.
nosklo

dlaczego miałby kopiować całe dane pliku do nowego ciągu? z dokumentacji Pythona: Aby uczynić pętlę for najbardziej wydajnym sposobem zapętlenia linii pliku (bardzo częsta operacja), metoda next () używa ukrytego bufora odczytu z wyprzedzeniem.
sinzi

3
@sinzi: "s + =" lub łączenie ciągów tworzy nową kopię ciągu za każdym razem, ponieważ ciąg jest niezmienny, więc tworzysz nowy ciąg.
nosklo

1
@nosklo: są to szczegóły dotyczące implementacji, w tym miejscu można zastosować
analizę list

1

Jestem w nieco podobnej sytuacji. Nie jest jasne, czy znasz wielkość porcji w bajtach; Zwykle nie, ale liczba wymaganych rekordów (wierszy) jest znana:

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

Aktualizacja : Dzięki nosklo. Oto co miałem na myśli. Prawie działa, z tym wyjątkiem, że traci linię „między” kawałkami.

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

Trik bez utraty linii, ale nie wygląda ładnie.


1
czy to pseudo kod? to nie zadziała. Jest to również niepotrzebne mylące, powinieneś uczynić liczbę linii opcjonalnym parametrem funkcji get_line.
nosklo

0

To eleganckie rozwiązanie do przetwarzania linia po linii:

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

Dopóki nie ma pustych linii.


6
Jest to po prostu zbyt skomplikowany, mniej solidny i wolniejszy odpowiednik tego, co openjuż daje. Plik jest już iteratorem ponad swoimi liniami.
abarnert

-2

możesz użyć następującego kodu.

file_obj = open('big_file') 

open () zwraca obiekt pliku

następnie użyj os.stat, aby uzyskać rozmiar

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

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

nie odczytałby całego pliku, jeśli rozmiar nie jest wielokrotnością 1024
kmaork
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.