대용량 파일을 끊어서 읽는 방법

파이썬에서 아주 큰 파일을 한 번에 읽지 않고 필요한 만큼만 읽어서 처리하는 방법

대용량 파일을 끊어서 읽는 방법
Photo by ün LIU / Unsplash

특정한 구분자로 연결되어 있는 데이터들을 읽어들인다면 보통 파일을 read한 다음, 그 내용을 구분자로 split 하는 방법을 떠올릴 수 있습니다. 틀린 방법도 아니거니와 많은 경우에 이런 방법을 실제로 사용합니다.

하지만 만약 다루는 데이터의 양이 아주 많다면 이 간단한 작업에서 문제가 생기기 시작합니다. 만약 텍스트 파일이 수십 기가 바이트에 달한다면 메모리로 로드하는 것조차 문제가 됩니다. 메모리에 로드한 후에 split을 하게 되면 다시 거대한 리스트를 생성해야 하기 때문에 메모리에서 리스트로 분해하는 것은 좋은 생각이 아닙니다.

이런 유사한 상황에 대해서 이미 파이썬 공식문서에서도 루프를 돌기 위해서 거대한 리스트를 만들기 보다는 제너레이터를 사용할 것을 권장합니다. 예를 들어 백 만 번의 루프를 돌기 위해서 정수 백 만 개가 있는 리스트를 사용하는 대신, range 객체를 사용하는 것으로도 충분합니다. 이와 유사하게 대용량 파일을 처리해야 하는 경우에는 특정한 크기 이내의 단위로 여러 번 나눠 읽는 것이 권장됩니다.

아래와 같이 조금씩 버퍼로 읽어들인 다음 구분자까지 떼어내어 소모하는 방식의 제너레이터를 만든다면 대용량 파일을 조금씩 읽어들여서 순차적으로 처리할 수 있습니다.

from io import IOBase

def reader(f: IOBase, /, delimiter: str='\n', chunk_size=8192):
    buffer = bytearray()
    delim = delimiter.encode()
    dellen = len(delim)
    while True:
        chunk = f.read(chunk_size)
        if not chunk:
            yield (buffer[:]).decode()
            return
        buffer.extend(chunk)
        while True:
            try:
                idx = buffer.index(delim)
                yield buffer[:idx].decode()
                buffer[:idx + dellen]  = []
            except ValueError:
                break

파라미터 f는 파일처럼 read(amt) 를 지원하는 모든 타입을 받도록 IOBase 타입으로 정의했습니다. open() 함수로 파일을 열거나, urlopen() 함수를 이용해서 네트워크를 통해 읽어들이게 되는 파일에도 동일하게 적용할 수 있습니다.

사실 "행 단위"로 처리하려는 경우에는 이런 함수를 따로 작성하지 않아도 됩니다. open() 함수를 통해서 얻을 수 있는 파일 핸들은 기본적으로 행단위로 읽어들이는 것이 가능합니다.

  with open('myfile.txt') as f:
    for line in f:
      print(line)

다만 암묵적으로 처리되기 때문에 행단위로만 처리가 가능하며, 만약 텍스트 파일 내의 내용이 개행이 아니라 콤마 등의 다른 문자라면 위와 같은 별도의 함수를 사용하여 처리하는 것이 바람직합니다.