Phương pháp lười biếng để đọc tệp lớn trong Python?


290

Tôi có một tệp 4GB rất lớn và khi tôi cố đọc nó thì máy tính của tôi bị treo. Vì vậy, tôi muốn đọc từng mảnh một và sau khi xử lý từng mảnh lưu trữ mảnh đã xử lý vào một tệp khác và đọc đoạn tiếp theo.

Có phương pháp nào cho yieldnhững mảnh này không?

Tôi rất thích có một phương pháp lười biếng .

Câu trả lời:


424

Để viết một hàm lười, chỉ cần sử dụng 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)

Một tùy chọn khác là sử dụng itervà chức năng trợ giúp:

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

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

Nếu tệp dựa trên dòng, đối tượng tệp đã là trình tạo dòng lười biếng:

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

Vì vậy, dòng f = open('really_big_file.dat')chỉ là một con trỏ mà không có bất kỳ tiêu thụ bộ nhớ? (Ý tôi là bộ nhớ tiêu thụ là như nhau bất kể kích thước tệp?) Nó sẽ ảnh hưởng đến hiệu suất như thế nào nếu tôi sử dụng urllib.readline () thay vì f.readline ()?
sumid

4
Thực hành tốt để sử dụng open ('really_big_file.dat', 'rb') để tương thích với Windows bị thách thức Posix của chúng tôi khi sử dụng các đồng nghiệp.
Tal Weiss

6
Mất tích rbnhư @Tal Weiss đã đề cập; và thiếu một file.close()tuyên bố (có thể sử dụng with open('really_big_file.dat', 'rb') as f:để thực hiện tương tự; Xem tại đây để thực hiện ngắn gọn khác
cod3monk3y

4
@ cod3monk3y: tệp văn bản và tệp nhị phân là những thứ khác nhau. Cả hai loại đều hữu ích nhưng trong các trường hợp khác nhau. Chế độ (văn bản) mặc định có thể hữu ích ở đây tức 'rb'không bị thiếu.
jfs

2
@ jf-sebastian: đúng, OP không chỉ định liệu anh ta đang đọc dữ liệu văn bản hay nhị phân. Nhưng nếu anh ta sử dụng python 2.7 trên Windowsđang đọc dữ liệu nhị phân, điều đáng lưu ý là nếu anh ta quên 'b'dữ liệu của mình thì rất có thể sẽ bị hỏng . Từ các tài liệu -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

Nếu máy tính, HĐH và python của bạn là 64 bit , thì bạn có thể sử dụng mô-đun mmap để ánh xạ nội dung của tệp vào bộ nhớ và truy cập nó bằng các chỉ mục và lát. Dưới đây là một ví dụ từ tài liệu:

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

Nếu máy tính, HĐH hoặc python của bạn là 32 bit , thì các tệp lớn của mmap có thể dành phần lớn không gian địa chỉ của bạn và bỏ đói chương trình bộ nhớ của bạn.


7
Công việc này dự định làm như thế nào nhỉ? Nếu tôi có tệp 32 GB thì sao? Nếu tôi đang dùng VM với RAM 256 MB thì sao? Mmapping một tập tin lớn như vậy thực sự không bao giờ là một điều tốt.
Savino Sguera

4
Câu trả lời này xứng đáng với số phiếu -12. Họ sẽ giết bất cứ ai sử dụng nó cho các tập tin lớn.
Phyo Arkar Lwin

23
Điều này có thể hoạt động trên Python 64 bit ngay cả đối với các tệp lớn. Mặc dù tệp được ánh xạ bộ nhớ, nhưng nó không được đọc vào bộ nhớ, do đó dung lượng bộ nhớ vật lý có thể nhỏ hơn nhiều so với kích thước tệp.
pts

1
@SavinoSguera kích thước của bộ nhớ vật lý có quan trọng với việc định hình một tập tin không?
Nick T

17
@ V3ss0n: Tôi đã thử mmap tệp 32 GB trên Python 64 bit. Nó hoạt động (Tôi có RAM dưới 32GB): Tôi có thể truy cập vào phần bắt đầu, phần giữa và phần cuối của tệp bằng cả giao diện Sequence và tệp.
jfs

37

file.readlines() đưa vào một đối số kích thước tùy chọn gần bằng số lượng dòng được đọc trong các dòng được trả về.

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
đó là một ý tưởng thực sự tuyệt vời, đặc biệt là khi nó được kết hợp với defaultdict để chia dữ liệu lớn thành các dữ liệu nhỏ hơn.
Frank Wang

4
Tôi khuyên bạn nên sử dụng .read()không .readlines(). Nếu tệp là nhị phân, nó sẽ không có ngắt dòng.
Myers Carpenter

1
Điều gì nếu tập tin là một chuỗi lớn?
MattSom

28

Đã có nhiều câu trả lời hay, nhưng nếu toàn bộ tệp của bạn nằm trên một dòng và bạn vẫn muốn xử lý "hàng" (trái ngược với các khối có kích thước cố định), những câu trả lời này sẽ không giúp bạn.

99% thời gian, có thể xử lý các tệp theo từng dòng. Sau đó, như được đề xuất trong câu trả lời này , bạn có thể sử dụng chính đối tượng tệp làm trình tạo lười biếng:

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

Tuy nhiên, tôi đã từng chạy vào một tệp dòng rất lớn (gần như), trong đó thực tế là dấu phân cách hàng '\n'nhưng không phải '|'.

  • Đọc từng dòng không phải là một lựa chọn, nhưng tôi vẫn cần xử lý từng hàng.
  • Chuyển đổi '|'sang '\n'trước khi xử lý cũng không nằm trong câu hỏi này, bởi vì một số trường của csv này có chứa '\n'(đầu vào người dùng văn bản miễn phí).
  • Sử dụng thư viện csv cũng bị loại trừ vì thực tế là, ít nhất là trong các phiên bản đầu của lib, nó được mã hóa cứng để đọc từng dòng đầu vào .

Đối với các loại tình huống này, tôi đã tạo đoạn trích sau:

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

Tôi đã có thể sử dụng nó thành công để giải quyết vấn đề của tôi. Nó đã được thử nghiệm rộng rãi, với các kích cỡ khác nhau.


Bộ thử nghiệm, cho những người muốn thuyết phục bản thân.

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

CẬP NHẬT: Cách tiếp cận được giải thích tốt nhất trong https://stackoverflow.com/a/4566523/38592


Điều này hoạt động tốt cho các đốm màu, nhưng có thể không tốt cho nội dung được phân tách dòng (như CSV, HTML, v.v ... trong đó việc xử lý cần được xử lý theo từng dòng)
csseller

7

Tham khảo tài liệu chính thức của python https://docs.python.org/zh-cn/3/l Library / fiances.html?#iter

Có lẽ phương pháp này là pythonic hơn:

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

Tôi nghĩ rằng chúng ta có thể viết như thế này:

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

Tôi không được phép bình luận do uy tín thấp của mình, nhưng giải pháp SilentGhosts sẽ dễ dàng hơn nhiều với file.readlines ([sizehint])

phương pháp tập tin python

chỉnh sửa: SilentGhost là đúng, nhưng điều này sẽ tốt hơn:

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

ok, xin lỗi, bạn hoàn toàn đúng nhưng có lẽ giải pháp này sẽ khiến bạn hạnh phúc hơn;): s = "" cho i trong xrange (100): s + = file.next ()
sinzi

1
-1: Giải pháp khủng khiếp, điều này có nghĩa là tạo một chuỗi mới trong bộ nhớ mỗi dòng và sao chép toàn bộ dữ liệu tệp được đọc vào chuỗi mới. Hiệu suất và bộ nhớ tồi tệ nhất.
nosklo

Tại sao nó sẽ sao chép toàn bộ dữ liệu tệp vào một chuỗi mới? từ tài liệu python: Để tạo một vòng lặp for một cách hiệu quả nhất để lặp qua các dòng của một tệp (một thao tác rất phổ biến), phương thức next () sử dụng bộ đệm đọc trước ẩn.
sinzi

3
@sinzi: "s + =" hoặc nối chuỗi tạo ra một bản sao mới của chuỗi mỗi lần, vì chuỗi này là bất biến, vì vậy bạn đang tạo một chuỗi mới.
nosklo

1
@nosklo: đây là những chi tiết triển khai, có thể sử dụng tính năng hiểu danh sách ở vị trí của nó
SilentGhost

1

Tôi đang ở trong một tình huống hơi giống nhau. Không rõ liệu bạn có biết kích thước khối theo byte hay không; Tôi thường không biết, nhưng số lượng hồ sơ (dòng) được yêu cầu đã biết:

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

Cập nhật : Cảm ơn nosklo. Ý tôi là đây. Nó gần như hoạt động, ngoại trừ việc nó mất một dòng 'giữa các khối.

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

Có phải mẹo không bị mất bất kỳ dòng nào, nhưng nó trông không đẹp lắm.


1
mã giả này là? nó sẽ không hoạt động. Nó cũng không gây nhầm lẫn, bạn nên biến số dòng thành một tham số tùy chọn cho hàm get_line.
nosklo

0

Để xử lý từng dòng, đây là một giải pháp tao nhã:

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

Miễn là không có dòng trống.


6
Đây chỉ là một quá phức tạp, ít mạnh mẽ hơn và chậm hơn tương đương với những gì openđã cung cấp cho bạn. Một tập tin đã là một trình vòng lặp trên các dòng của nó.
abarnert

-2

bạn có thể sử dụng mã sau đây.

file_obj = open('big_file') 

open () trả về một đối tượng tệp

sau đó sử dụng os.stat để lấy kích thước

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

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

sẽ không đọc toàn bộ tệp nếu kích thước không phải là bội số của 1024
kmaork
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.