파이썬에서 큰 파일을 읽는 게으른 방법?


290

나는 4GB의 매우 큰 파일을 가지고 있으며 그것을 읽으려고 할 때 컴퓨터가 정지합니다. 그래서 조각별로 읽고 각 조각을 처리 한 후 처리 된 조각을 다른 파일에 저장하고 다음 조각을 읽습니다.

yield이 조각들에 어떤 방법 이 있습니까?

나는 게으른 방법 을 갖고 싶습니다 .

답변:


424

게으른 함수를 작성하려면 다음을 사용하십시오 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)

또 다른 옵션은 사용 iter및 도우미 기능입니다.

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

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

파일이 라인 기반 인 경우 파일 객체는 이미 지연된 라인 생성기입니다.

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

그래서 라인 f = open('really_big_file.dat')은 메모리 소비가없는 포인터 일뿐입니까? (파일 크기에 관계없이 사용 된 메모리가 동일하다는 것을 의미합니까?) f.readline () 대신 urllib.readline ()을 사용하면 성능에 어떤 영향을 미칩니 까?
sumid

4
동료를 사용하여 Posix 문제가있는 Windows와의 호환성을 위해 open ( 'really_big_file.dat', 'rb')을 사용하는 것이 좋습니다.
Tal Weiss

6
rb@Tal Weiss가 언급했듯이 누락되었습니다 . 그리고 file.close()성명서를 잃어 with open('really_big_file.dat', 'rb') as f:
버렸다 (

4
@ cod3monk3y : 텍스트와 바이너리 파일은 다릅니다. 두 가지 유형 모두 유용하지만 다른 경우입니다. 기본 (텍스트) 모드는 여기 즉, 유용 할 수 'rb'있다 되지 누락.
jfs

2
@ jf-sebastian : 사실, OP는 텍스트 또는 이진 데이터를 읽고 있는지 여부를 지정하지 않았습니다. 그는에 파이썬 2.7 사용 그러나 만약 윈도우를 하고 있다 이진 데이터를 읽고, 그가 잊어 버린 경우 에라도 것을주의 확실히 가치가 'b'자신의 데이터가됩니다 매우 가능성이 손상 될 수 . 워드 프로세서 -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

컴퓨터, OS 및 Python이 64 비트 인 경우 mmap 모듈 을 사용 하여 파일의 컨텐츠를 메모리에 맵핑하고 색인 및 슬라이스로 액세스 할 수 있습니다. 다음은 설명서의 예입니다.

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

컴퓨터, OS 또는 python이 32 비트 인 경우 mmap-ing 큰 파일은 주소 공간의 많은 부분을 예약하고 메모리 프로그램을 고갈시킬 수 있습니다.


7
이것은 어떻게 작동합니까? 32GB 파일이 있으면 어떻게합니까? 256MB RAM이있는 VM에 있으면 어떻게합니까? 이러한 거대한 파일을 매핑하는 것은 결코 좋은 일이 아닙니다.
Savino Sguera

4
이 답변은 -12 투표가 필요합니다. 큰 파일에 사용하는 사람을 죽일 것입니다.
Phyo Arkar Lwin

23
큰 파일의 경우에도 64 비트 Python에서 작동 할 수 있습니다. 파일이 메모리 매핑되어 있어도 메모리로 읽히지 않으므로 실제 메모리 양이 파일 크기보다 훨씬 작을 수 있습니다.
pts

1
@SavinoSguera는 실제 메모리 크기가 파일을 mmaping하는 데 중요합니까?
Nick T

17
@ V3ss0n : 64 비트 Python에서 32GB 파일을 mmap하려고했습니다. 작동합니다 (32GB 미만의 RAM이 있음) : 시퀀스 및 파일 인터페이스를 사용하여 파일의 시작, 중간 및 끝에 액세스 할 수 있습니다.
jfs

37

file.readlines() 리턴 된 행에서 읽은 행 수와 비슷한 선택적 크기 인수를 사용합니다.

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
특히 빅 데이터를 더 작은 데이터로 나누기 위해 defaultdict와 결합 할 때 정말 좋습니다.
Frank Wang

4
사용 .read()하지 않는 것이 좋습니다 .readlines(). 파일이 이진 파일이면 줄 바꿈이 없습니다.
마이어스 카펜터

1
파일이 하나의 거대한 문자열이라면?
MattSom

28

이미 좋은 답변이 많이 있지만 전체 파일이 한 줄에 있고 여전히 고정 크기 블록이 아닌 "행"을 처리하려는 경우 이러한 답변이 도움이되지 않습니다.

시간의 99 %, 파일을 한 줄씩 처리 할 수 ​​있습니다. 그런 다음이 답변 에서 제안한 것처럼 파일 객체 자체를 지연 생성기로 사용할 수 있습니다.

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

그러나, 나는 행 구분은 사실이 아니었다 매우 매우 큰 (거의) 한 줄 파일로 실행 된 한 번 '\n'하지만 '|'.

  • 한 줄씩 읽는 것은 옵션이 아니지만 여전히 한 줄씩 처리해야했습니다.
  • 이 csv의 일부 필드 (자유 텍스트 사용자 입력)가 포함되어 있기 때문에 처리 '|'하기 '\n'전에 변환 하는 것도 의문의 여지가 없습니다 '\n'.
  • csv 라이브러리를 사용하는 것도 최소한 초기 버전의 lib 에서 입력 행을 한 행씩 읽도록 하드 코딩되어 있기 때문에 배제되었습니다 .

이러한 종류의 상황을 위해 다음 스 니펫을 만들었습니다.

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

문제를 해결하는 데 성공적으로 사용할 수있었습니다. 다양한 청크 크기로 광범위하게 테스트되었습니다.


자신을 설득하려는 사람들을위한 테스트 스위트.

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

업데이트 : 접근법은 https : //.com/a/4566523/38592에서 가장 잘 설명됩니다.


이것은 블롭에는 효과적이지만 줄 단위로 구분 된 컨텐츠 (예 : 처리를 한 줄씩 처리해야하는 CSV, HTML 등)에는 적합하지 않을 수 있습니다.
cgseller

7

파이썬 공식 문서 https://docs.python.org/zh-cn/3/library/functions.html?#iter를 참조하십시오

어쩌면이 방법은 더 파이썬 일 수 있습니다.

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

우리는 다음과 같이 쓸 수 있다고 생각합니다.

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

저의 명성으로 인해 댓글을 달 수는 없지만 SilentGhosts 솔루션은 file.readlines ([sizehint])로 훨씬 쉬워야합니다.

파이썬 파일 메소드

편집 : SilentGhost가 옳지 만 다음보다 낫습니다.

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

알았어, 미안, 네 말이 맞아 하지만 어쩌면이 솔루션은 행복 할 것이다) : S = ""난에 대한 xrange에 (백)을 : S + = file.next를 ()
sinzi

1
-1 : 끔찍한 해결책, 이것은 각 줄의 메모리에 새로운 문자열을 만들고 전체 파일 데이터를 새로운 문자열로 복사하는 것을 의미합니다. 최악의 성능과 메모리.
nosklo

왜 전체 파일 데이터를 새로운 문자열로 복사합니까? 파이썬 문서에서 : for 루프를 파일 라인을 반복하는 가장 효율적인 방법 (매우 일반적인 작업)으로 만들기 위해 next () 메서드는 숨겨진 미리 읽기 버퍼를 사용합니다.
sinzi

3
@sinzi : "s + ="또는 문자열 연결은 매번 문자열을 변경할 수 없으므로 문자열을 변경할 수 없으므로 새 문자열을 만듭니다.
nosklo

1
@nosklo : 이것들은 구현의 세부 사항이며, 목록 이해가 그 자리에서 사용될 수 있습니다
SilentGhost

1

나는 다소 비슷한 상황에 처해있다. 청크 크기를 바이트 단위로 알고 있는지 확실하지 않습니다. 보통은 아니지만 필요한 레코드 수 (줄)는 다음과 같습니다.

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

업데이트 : nosklo 감사합니다. 여기 제가 의미하는 바가 있습니다. 그것은 '중간'청크를 잃는 것을 제외하고는 거의 작동합니다.

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

트릭은 줄을 잃지 않고하지만 잘 보이지 않습니다.


1
이 의사 코드입니까? 작동하지 않습니다. 또한 혼동 될 필요가 없으므로, 행 수를 get_line 함수에 대한 선택적 매개 변수로 만들어야합니다.
nosklo

0

한 줄씩 처리하기 위해 이것은 우아한 해결책입니다.

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

빈 줄이없는 한.


6
이것은 단지 open당신 에게 이미주는 것과 지나치게 복잡하고 덜 강력하며 느리다 . 파일은 이미 해당 줄의 반복자입니다.
abarnert

-2

다음 코드를 사용할 수 있습니다.

file_obj = open('big_file') 

open ()은 파일 객체를 반환

그런 다음 크기를 얻기 위해 os.stat를 사용하십시오.

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

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

크기는 곱셈의 1024없는 경우 전체 파일을 읽을 것
kmaork
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.