Metode Malas untuk Membaca File Besar dengan Python?

290

Saya memiliki file 4GB yang sangat besar dan ketika saya mencoba membacanya komputer saya hang. Jadi saya ingin membacanya sepotong demi sepotong dan setelah memproses masing-masing menyimpan potongan diproses ke file lain dan membaca bagian selanjutnya.

Apakah ada metode untuk yield potongan-potongan ini?

Saya ingin sekali memiliki metode yang malas .

Pratik Deoghare
sumber

Jawaban:

424

Untuk menulis fungsi malas, cukup gunakan 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)

Opsi lain adalah menggunakan iterdan fungsi pembantu:

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

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

Jika file berbasis baris, objek file sudah menjadi generator baris yang malas:

for line in open('really_big_file.dat'):
    process_data(line)
nosklo
sumber
Jadi garisnya f = open('really_big_file.dat')hanyalah pointer tanpa konsumsi memori? (Maksud saya memori yang dikonsumsi sama terlepas dari ukuran file?) Bagaimana pengaruhnya terhadap kinerja jika saya menggunakan urllib.readline () dan bukannya f.readline ()?
sumid
4
Praktik yang baik untuk menggunakan open ('really_big_file.dat', 'rb') untuk kompatibilitas dengan Windows Posix kami yang tertantang menggunakan rekan kerja.
Tal Weiss
6
Hilang rbseperti yang disebutkan @Tal Weiss; dan kehilangan file.close()pernyataan (bisa digunakan with open('really_big_file.dat', 'rb') as f:untuk mencapai yang sama; Lihat di sini untuk implementasi ringkas lainnya
cod3monk3y
4
@ cod3monk3y: teks dan file biner adalah hal yang berbeda. Kedua jenis ini bermanfaat tetapi dalam kasus yang berbeda. Mode default (teks) mungkin berguna di sini yaitu, 'rb'yang tidak hilang.
jfs
2
@ jf-sebastian: benar, OP tidak menentukan apakah dia membaca data tekstual atau biner. Tapi jika dia menggunakan python 2,7 pada Windows dan sedang membaca data biner, hal ini tentunya perlu dicatat bahwa jika ia melupakan 'b'data-nya akan sangat mungkin rusak . Dari docs -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

Jika komputer Anda, OS dan python adalah 64-bit , maka Anda dapat menggunakan modul mmap untuk memetakan isi file ke dalam memori dan mengaksesnya dengan indeks dan irisan. Berikut ini contoh dari dokumentasi:

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

Jika komputer, OS, atau python Anda 32-bit , maka mmap-ing file besar dapat memesan sebagian besar ruang alamat Anda dan membuat program Anda kelaparan .

Komunitas
sumber
7
Bagaimana cara kerjanya? Bagaimana jika saya memiliki file 32GB? Bagaimana jika saya menggunakan VM dengan RAM 256MB? Memetakan file sebesar itu benar-benar tidak pernah merupakan hal yang baik.
Savino Sguera
4
Jawaban ini layak -12 suara. Ini akan membunuh siapa pun yang menggunakannya untuk file besar.
Phyo Arkar Lwin
23
Ini dapat bekerja pada Python 64-bit bahkan untuk file besar. Meskipun file tersebut dipetakan dengan memori, itu tidak dibaca ke memori, sehingga jumlah memori fisik bisa jauh lebih kecil dari ukuran file.
Poin
1
@ SavinoSguera apakah ukuran memori fisik penting dengan mmaping file?
Nick T
17
@ V3ss0n: Saya sudah mencoba mmap file 32GB pada 64-bit Python. Ini berfungsi (saya memiliki RAM kurang dari 32GB): Saya dapat mengakses awal, tengah, dan akhir file menggunakan urutan dan antarmuka file.
jfs
37

file.readlines() mengambil argumen ukuran opsional yang mendekati jumlah baris yang dibaca di baris yang dikembalikan.

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
sumber
1
itu ide yang sangat bagus, terutama ketika dikombinasikan dengan defaultdict untuk membagi data besar menjadi yang lebih kecil.
Frank Wang
4
Saya akan merekomendasikan untuk .read()tidak menggunakannya .readlines(). Jika file tersebut biner, tidak akan ada jeda baris.
Myers Carpenter
1
Bagaimana jika file tersebut adalah satu string besar?
MattSom
28

Sudah ada banyak jawaban bagus, tetapi jika seluruh file Anda berada pada satu baris dan Anda masih ingin memproses "baris" (sebagai lawan dari blok ukuran tetap), jawaban ini tidak akan membantu Anda.

99% dari waktu, adalah mungkin untuk memproses file baris demi baris. Kemudian, seperti yang disarankan dalam jawaban ini , Anda bisa menggunakan objek file itu sendiri sebagai lazy generator:

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

Namun, saya pernah bertemu dengan file baris tunggal yang sangat besar (hampir), di mana pemisah baris sebenarnya tidak '\n'tetapi '|'.

  • Membaca baris demi baris bukanlah pilihan, tetapi saya masih perlu memprosesnya baris demi baris.
  • Konversi '|'ke '\n'sebelum pemrosesan juga keluar dari pertanyaan, karena beberapa bidang csv ini berisi '\n'(input pengguna teks gratis).
  • Menggunakan perpustakaan csv juga dikesampingkan karena fakta bahwa, setidaknya dalam versi awal lib, hardcoded untuk membaca input baris demi baris .

Untuk situasi seperti ini, saya membuat cuplikan berikut:

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

Saya berhasil menggunakannya untuk menyelesaikan masalah saya. Ini telah diuji secara luas, dengan berbagai ukuran potongan.


Test suite, bagi mereka yang ingin meyakinkan diri mereka sendiri.

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)
pengguna48678
sumber
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

UPDATE: Pendekatan ini paling baik dijelaskan di https://stackoverflow.com/a/4566523/38592

myroslav
sumber
Ini bekerja dengan baik untuk gumpalan, tetapi mungkin tidak baik untuk konten yang dipisahkan baris (seperti CSV, HTML, dll. Di mana pemrosesan perlu ditangani baris demi baris)
cgseller
7

Lihat dokumentasi resmi python, https://docs.python.org/zh-cn/3/library/functions.html?#iter

Mungkin metode ini lebih pythonic:

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
sumber
3

Saya pikir kita bisa menulis seperti ini:

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

saya tidak diperbolehkan berkomentar karena reputasi saya yang rendah, tetapi solusi SilentGhosts harus lebih mudah dengan file.readlines ([sizehint])

metode file python

sunting: SilentGhost benar, tetapi ini harus lebih baik daripada:

s = "" 
for i in xrange(100): 
   s += file.next()
sinzi
sumber
ok, maaf, kamu benar sekali. tapi mungkin solusi ini akan membuat Anda lebih bahagia;): s = "" untuk saya di xrange (100): s + = file.next ()
sinzi
1
-1: Solusi mengerikan, ini berarti membuat string baru di memori setiap baris, dan menyalin seluruh data file yang dibaca ke string baru. Kinerja dan memori terburuk.
nosklo
mengapa itu menyalin seluruh data file ke string baru? dari dokumentasi python: Untuk membuat for for cara yang paling efisien dari perulangan di atas baris file (operasi yang sangat umum), metode next () menggunakan buffer baca-depan yang tersembunyi.
sinzi
3
@sinzi: "s + =" atau string gabungan membuat salinan baru dari string setiap kali, karena string tidak dapat diubah, jadi Anda membuat string baru.
nosklo
1
@nosklo: ini adalah rincian implementasi, pemahaman daftar dapat digunakan di tempat itu
SilentGhost
1

Saya dalam situasi yang agak mirip. Tidak jelas apakah Anda tahu ukuran chunk dalam byte; Saya biasanya tidak, tetapi jumlah catatan (baris) yang diperlukan diketahui:

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

Pembaruan : Terima kasih nosklo. Inilah yang saya maksud. Ini hampir berhasil, kecuali bahwa ia kehilangan garis 'antara' potongan.

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

Apakah triknya tanpa kehilangan garis, tetapi tidak terlihat bagus.

SilentGhost
sumber
1
apakah ini kode semu? itu tidak akan berhasil. Ini juga tidak perlu membingungkan, Anda harus menjadikan jumlah baris sebagai parameter opsional untuk fungsi get_line.
nosklo
0

Untuk memproses baris demi baris, ini adalah solusi elegan:

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

Selama tidak ada garis kosong.

crizCraig
sumber
6
Ini hanya ekuivalen yang terlalu rumit, kurang kuat, dan lebih lambat dari apa yang opensudah memberi Anda. File sudah menjadi iterator di atas barisnya.
abarnert
-2

Anda dapat menggunakan kode berikut.

file_obj = open('big_file') 

open () mengembalikan objek file

lalu gunakan os.stat untuk mendapatkan ukuran

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

for i in range( file_size/1024):
    print file_obj.read(1024)
nyentrik
sumber
tidak akan membaca seluruh file jika ukurannya bukan kelipatan 1024
kmaork