2부 데이터 구조체 - 4. 텍스트와 바이트

seokj·2023년 2월 18일
0

FluentPython

목록 보기
5/9

파이썬3에서는 str이 유니코드를 사용한다. 과거 많은 언어에서는 문자열을 문자도 저장하고 바이트도 겸해서 저장하는 하나의 자료형이 있었지만 파이썬은 유니코드를 저장하는 str과 바이트 시퀀스를 엄격히 구분한다.


중요 키워드
코드포인트, 바이트시퀀스
str, bytes, bytearray
str.encode의 errors 매개변수, chardet, ZERO WIDTH NO-BREAK SPACE, U+FFFD, U+FEFF
유니코드 샌드위치 모델
결합문자, unicodedata.normalize, unicodedata.name, str.casefold, unicodedata.combining, str.maketrans, str.translate
locale.strxfrm, locale.setlocale, pyuca
ord, center, rjust, ljust, format
U+DC00~U+DCFF

덜 중요한 키워드
mmap, struct
codecs.register_error, 그렘린
locale.getprefferedencoding
string.ascii_letters
sys.getfilesystemencoding

참고자료
https://docs.python.org/3/library/mmap.html
http://bit.ly/1Vm7ZnI
http://bit.ly/1Vm7YjA
http://bit.ly/1Vm83DZ
http://pypi.python.org/pypi/chardet
http://bit.ly/1lqyKAI
http://bit.ly/1lqyP79


문자마다 코드포인트가 지정되어있다. 0부터 1114111까지의 숫자이며 보통 유니코드 표준으로는 U+접두사를 붙여 4자리에서 6자리 16진수 정수로 표현한다. 예를 들어 A는 U+0041이고 높은음자리표 기호는 U+1D11E이다.

실제로 문자를 사용할 땐 바이트 시퀀스로 바꿔서 사용하는데, 바꾸는 알고리즘이 인코딩이다. 인코딩에 따라 같은 문자이더라도 바이트 시퀀스가 달라진다. 예를 들어 A를 의미하는 U+0041를 utf8으로 인코딩하면 \x41이 되고 utf-16le로 인코딩하면 \x41\x00이 된다. 유로화 기호를 의미하는 U+20AC를 utf8로 인코딩하면 \xe2\x82\xac가 되고 utf16-le로 인코딩하면 \xac\x20이 된다.


앞서 말한 바이트 시퀀스, 즉 이진 시퀀스는 bytes, bytearray형이 있다. bytes는 불변형, bytearray는 가변형이다. bytes의 리터럴 표기는 문자열 앞에 b를 붙인 것이고 bytearray는 리터럴 표기가 없다. 이진 시퀀스는 출력가능한 아스키 문자는 그대로 출력하고 이스케이프 시퀀스(\t, \n, \r, \)로 출력하거나 그 외의 문자들은 16진수 \xff의 형태로 출력한다.

b'asdf'[0]은 97이고 b'asdf'[:1]은 b'a'이다. 인덱싱을 했을 땐 0이상 256미만의 정수가 나오지만 슬라이싱을 했을 땐 자료형이 바뀌지 않는다. bytearray의 경우도 똑같다.

이진 시퀀스는 format과 format_map함수를 제외한 str의 모든 함수를 사용할 수 있으며 추가적인 몇가지 함수를 더 이용할 수 있다. str처럼 %연산도 지원한다.

bytes와 bytearray는 생성하면 원본 바이트 정보를 복사하여 새로 생성한다. 반대로 memoryview는 생성할 때 원본 바이트 정보를 복사하지 않는다. 슬라이싱 할 때도 새로운 객체가 생성되므로 byte, bytearray는 원본 바이트가 복사되지만 memoryview는 복사되지 않는다. bytes, bytearray의 생성자에 bytes, bytearray, memoryview, array.array가 들어갈 수 있다.

mmap 라이브러리를 통해 더 적은 메모리로 데이터 입출력을 할 수 있다. struct 라이브러리를 통해 c언어의 구조체 형식으로 바이트 시퀀스를 묶거나 풀 수 있다.


파이썬은 기본 코덱(인코더/디코더)을 다양하게 지원한다. ascii, latin1, cp1252, cp437, gb2312, utf-8, utf-16le등이 있다.

  • latin1(=iso8859_1)에서 몇몇 기호를 추가하여 cp1252가 만들어졌다.
  • utf-8은 아스키코드를 포함하고 전체 웹사이트의 80%가 이 코덱을 기준으로 쓰였다.

유니코드를 다룰 때 마주치는 에러에는 UnicodeEncodeError, UnicodeDecodeError, SyntaxError가 있다.

UnicodeEncodeError는 인코딩할 때 발생한다. 문자에 해당하는 바이트코드가 해당 인코딩에 정의되어있지 않을 때 발생한다. str.encode의 errors매개변수로 다음과 같은 값을 전달하여 처리할 수 있다.

  • 'ignore': 바이트로 변환이 불가능한 문자는 무시한다.
  • 'replace': 바이트로 변환이 불가능한 문자를 ?로 치환한다. 원래 어떤 문자였는지는 알 수 없게 된다.
  • 'xmlcharrefreplace': 바이트로 변환이 불가능한 문자를 xml개체로 치환한다.

UnicodeDecodeError는 디코딩할 때 발생한다. 해당 인코딩에 정의되어있지 않는 바이트코드를 문자로 변환하려 할 때 발생한다. 단, 인코딩 방식이 맞지 않아도 다른 문자로 해당하는 바이트코드가 정의되어있을 수 있는데, 이 경우에는 글이 깨진 상태로 오류 없이 디코딩된다. bytes.decode의 errors매개변수로 'replace'를 전달하여 변환이 불가능한 문자를 U+FFFD(�)로 치환할 수 있다.

문서가 어떤 방식으로 인코딩 되었는지 모를 경우 chardet 외부 라이브러리를 통해 추정할 수 있다. 어떤 바이트 시퀀스가 등장한다면 특정 인코딩 방식은 아니라고 배제할 수는 있지만 어떤 바이트 시퀀스가 등장하지 않는다고 해서 특정 인코딩 방식이라고 확정짓지는 못하기 때문에 확정적으로 인코딩 방식을 알 수는 없다.

utf-16은 한 문자를 2바이트로 할당한다. utf-16은 두 가지 방식으로 나뉘는데 하위 바이트가 먼저 나오는 리틀 엔디언 방식을 utf-16le, 상위 바이트가 먼저 나오는 빅 엔디언 방식을 utf-16be라 한다. utf-16으로 인코딩하면 가장 앞에 BOM이 붙는다. BOM은 ZERO WIDTH NO-BREAK SPACE문자로, 출력해도 아무것도 표시되지 않는다. BOM의 코드포인트는 U+FEFF인데 이를 통해 utf-16이 리틀 엔디언인지 빅 엔디언인지를 알 수 있다(\xff\xfe이면 리틀 엔디언, \xfe\xff이면 빅 엔디언이다.). utf-16le, utf-16be로 인코딩하면 BOM이 붙지 않는다. utf-16으로 인코딩하면 BOM이 붙으면서 컴퓨터 환경에 따라 리틀 엔디언인지 빅 엔디언인지가 결정된다. utf-16으로 디코딩하면 BOM을 확인하여 그에 맞게 디코딩하고, utf-16le, utf-16be로 디코딩하면 BOM을 무시하면서 해당 인코딩 방식이 맞다고 간주하고 디코딩을 진행한다. utf-8으로 BOM문자를 인코딩하면 3바이트인 \xef\xbb\xbf가 되는데, 파이썬에서는 그냥 무시된다.


유니코드 샌드위치 모델에 의하면 외부 파일을 읽을 때 최대한 빨리 인코딩하고 외부로 파일을 쓸 때 최대한 나중에 디코딩하는 것이 안전하다. 또한 인코딩 방식을 지정하지 않으면 기본 인코딩 방식에 의존한다. 하지만 다음 사실에 따라 기본 인코딩 방식이 달라지기 때문에 인코딩 방식은 항상 명시하는 것이 좋다.

  • OS의 종류에 따라
  • 출력을 리다이렉션 했는지에 따라
  • PYTHONIOENCODING 환경변수가 선언되어있는지에 따라

é(U+00e9)는 코드포인트 하나의 문자로 나타낼 수 있다. 하지만 e와 ◌́(U+0301)로 분할하여 두 문자를 연달아 쓰는 것으로 é를 나타낼 수 있다. 전자와 후자 모두 출력하면 é로 동일하게 나온다. 하지만 비교연산을 하면 False가 나오고, len은 각각 1, 2로 다르게 나온다. 이러한 두 문자열을 올바르게 다루기 위해 정규화를 해야 한다.

unicodedata.normalize함수의 첫 번째 인자에 다음 중 하나를 넣고 두 번째 인자에 문자열을 넣어 정규화를 할 수 있다.

  • NFC: 문자와 알파벳 기호를 결합한다.
  • NFD: 문자와 알파벳 기호를 분리한다.

한글도 NFC를 하면 한 글자 당 하나로 결합되고 NFD를 하면 초성, 중성, 종성으로 분리된다. 사용자 입력은 NFC로 정규화된 형태로 받는다. 파일을 저장할 땐 NFC로 정규화하여 저장하는 것이 좋다. unicodedata.name함수로 한 문자의 정식 이름이 무엇인지 알 수 있다.

NFC와 NFD에 K가 붙은 NFKC, NFKD 방식도 있는데, 각각 NFC, NFD의 방식을 수행하면서 추가로 다른 문자와의 호환성을 위해 마이크로 기호를 뮤 기호로 바꾸거나 ½기호를 1/2로 바꾸는 등의 작업을 해준다. 원본 데이터가 손실되기 때문에 변환해서 저장하는 것은 옳지 않고, 검색 기능 등에 쓰인다.

str.casefold는 모든 문자를 소문자로 바꾸는 연산을 해주며 str.lower와 비슷하지만 마이크로 기호를 뮤 기호로 바꾸거나 에스체트를 ss로 바꾸는 등의 추가적인 작업을 해준다. lower와 casefold의 결과가 다른 문자는 유니코드 전체 문자에서 0.11%를 차지한다. K가 들어가는 정규화처럼 검색 기능 등에 쓰인다.

unicodedata.combining함수를 통해 문자가 결합기호인지 아닌지를 확인할 수 있다. string.ascii_letters를 통해 아스키 문자로 구성된 문자열을 얻을 수 있다. str.maketrans로 문자를 다른 문자로, 문자를 다른 문자열로 바꿀 수 있는 dict를 생성할 수 있고 str.translate함수를 통해 dict를 전달하여 번역할 수 있다.


locale.strxfrm을 sorted의 key로 넘겨주어 유니코드 문자열을 악센트 기호를 고려하여 정렬할 수 있다. 이렇게 정렬하기 전에는 locale.setlocale으로 로케일 설정을 해야한다. 외부 라이브러리인 pyuca를 이용할 수도 있다.


ord로 코드포인트를 구할 수 있다. center, ljust, rjust로 가운데, 왼쪽, 오른쪽 정렬을 할 수 있다. format으로 포매팅 할 수 있다. str.isidentifier, str.isprintable, str.isdecimal, str.isnumeric, unicodedata.numeric 등 유용한 함수가 있다.


정규표현식 라이브러리 re는 str와 이진시퀀스를 모두 받을 수 있게 되어있는 이중모드를 지원한다. 파일시스템 라이브러리 os 또한 이중모드를 지원하며 파일시스템의 인코딩 방식은 독자적으로 만들어진 경우가 많아 sys.getfilesystemencoding을 통해 지정된 인코딩 방식으로 수행된다. os.listdir로 경로의 폴더 목록을 확인할 수 있다.

bytes.decode함수의 errors매개변수로 surrogateescape를 전달하면 해당 인코딩으로 변환이 불가능한 문자는 U+DC00부터 U+DCFF사이의 값으로 치환된다.

profile
안녕하세요

0개의 댓글