파이썬에서 유니코드 스트림 다루기

원문 - Working with unicode streams in Python

번역을 허락해 준 Dave Hall 님께 고마움을 전합니다.


파이썬에서 유니코드를 다룰 때는 일반적으로 str.decode()unicode.encode() 메서드를 사용하여 unicode 타입과 str 타입을 상호 변환한다.

아래 예시에서는 'utf-16'으로 작성된 파일을 열어, 수직 탭(vertical tab) 코드포인트를 지운 다음, 'utf-8'로 저장한다. (깨진 XML을 다룰 때 이 방식이 매우 중요하다.)

# 파일 내용을 읽는다
with open("input.txt", "rb") as input:
    data = input.read()

# 바이너리 데이터를 utf-16으로 디코딩한다
data = data.decode("utf-16")

# 수직 탭을 삭제한다
data = data.replace(u"\u000B", u"")

# 유니코드 데이터를 utf-8로 인코딩한다
data = data.encode("utf-8")

# 데이터를 utf-8로 저장한다
with open("output.txt", "wb") as output:
    output.write(data)

엄청나게 큰 파일을 다룰 때가 아니라면 이 정도로도 충분하다. 하지만 큰 파일을 다룰 땐 모든 데이터가 메모리에 올라간다는 사실이 문제가 된다.

스트리밍 인코더/디코더 사용하기

파이썬 기본 라이브러리에는 codecs 모듈이 포함되어 있다. 이 모듈을 사용하면 파일을 조금씩 읽을 수 있고, 메모리에도 약간의 유니코드 데이터만 올라가게 된다.

codecs.open() 헬퍼 메서드를 사용하여 위의 예시를 최소한만 고쳐보자.

import codecs

# 입력 스트림과 출력 스트림을 연다
input = codecs.open("input.txt", "rb", encoding="utf-16")
output = codecs.open("output.txt", "wb", encoding="utf-8")

# 유니코드 데이터 조각들을 스트리밍한다
with input, output:
    while True:
        # 데이터 조각을 읽고
        chunk = input.read(4096)
        if not chunk:
            break
        # 수직 탭을 삭제한다
        chunk = chunk.replace(u"\u000B", u"")
        # 데이터 조각을 쓴다
        output.write(chunk)

파일은 끔찍해! 이터레이터 사용하기

파일은 다루기가 좀 지루하다. 복잡한 처리 과정에는 유니코드 데이터의 이터레이터를 다루는 편이 깔끔할 것이다.

아래는 iterdecode()를 사용하여, 파일을 유니코드 데이터 조각의 이터레이터로 읽는 효과적인 방법이다.

from functools import partial
from codecs import iterdecode

# 특정 path의 파일을 유니코드 조각의 이터레이터로 리턴한다
def iter_unicode_chunks(path, encoding):
    # 읽을 파일을 연다
    with open(path, "rb") as input:
        # 바이너리 파일을 바이너리 조각으로 변환한다
        binary_chunks = iter(partial(input.read, 1), "")
        # 바이너리 조각을 유니코드 조각으로 변환한다
        for unicode_chunk in iterdecode(binary_chunks, encoding):
            yield unicode_chunk

이제 iterencode() 메서드를 사용하여, 유니코드 조각의 이터레이터를 파일에 써보자.

from codecs import iterencode

# 유니코드 조각의 이터레이터를 특정 path의 파일에 쓴다
def write_unicode_chunks(path, unicode_chunks, encoding):
    # 쓸 파일을 연다
    with open(path, "wb") as output:
        # 유니코드 조각을 바이너리로 변환한다
        for binary_chunk in iterencode(unicode_chunks, encoding):
            output.write(binary_chunk)

이 두 함수와 함께 유니코드 데이터의 스트림에서 수직 탭을 없애는 일이 마법 같이 끝난다(just becomes a case of plumbing everything together).

# 파일을 유니코드 조각 형태로 읽는다
unicode_chunks = iter_unicode_chunks("input.txt", encoding="utf-16")

# 유니코드 조각을 수정한다
unicode_chunks = (
    chunk.replace(u"\u000B", u"")
    for chunk
    in unicode_chunks
)

# 유니코드 조각을 파일에 저장한다
write_unicode_chunks("output.txt", unicode_chunks, encoding="utf-8")

거창하게 codecs 모듈을 사용해야 할까?

얼핏 그냥, str.decode()unicode.encode() 메서드를 사용하여 큰 file 객체를 바이너리 조각으로 읽고, 인코딩하고 디코딩하는 편이 간단하다고 생각할 수도 있겠다.

# 나쁜 예시. 이렇게 하지 마시오!

# 입력 스트림과 출력 스트림을 연다
with open("input.txt", "rb") as input, open("output.txt", "wb") as output:
    # 바이너리 데이터 조각들을 순회한다
    while True:
        # 데이터 조각을 읽는다
        chunk = input.read(4096)
        if not chunk:
            break
        # 위험: 바이너리 데이터를 utf-16으로 디코딩한다
        chunk = chunk.decode("utf-16")
        # 수직 탭을 삭제한다
        chunk = chunk.replace(u"\u000B", u"")
        # 유니코드 데이터를 utf-8로 인코딩한다
        chunk = chunk.encode("utf-8")
        # 데이터 조각을 쓴다
        output.write(chunk)

불행히도 몇몇 유니코드 코드포인트는 바이너리 데이터의 한 바이트 이상으로 인코딩된다. 따라서 단순히 파일에서 바이트 조각들을 읽어서 decode() 메서드를 적용하면 예기치 않게 UnicodeDecodeError가 발생할 수도 있다. 이는 바이트 한 조각이 여러 바이트의 코드포인트로 분리되었기 때문이다.

codecs 모듈의 도구들을 사용하면 이러한 예기치 않은 충돌을 예방할 수 있다.

파이썬 3에서는?

파이썬 3에서는 훨씬 단순하게 유니코드 파일을 다룰 수 있다. 빌트인 메서드인 open()은 유니코드 데이터를 수정하거나 인코딩을 변경하는 데 필요한 기능을 포함하고 있다.

# 입력 스트림과 출력 스트림을 연다
input = open("input.txt", "rt", encoding="utf-16")
output = open("output.txt", "wt", encoding="utf-8")

# 유니코드 데이터 조각들을 스트리밍한다
with input, output:
    while True:
        # 데이터 조각을 읽고
        chunk = input.read(4096)
        if not chunk:
            break
        # 수직 탭을 삭제한다
        chunk = chunk.replace("\u000B", "")
        # 데이터 조각을 쓴다
        output.write(chunk)

파이썬 3의 시대다! 즐겁게 코딩하길!

COMMENTS

}