[Python][텍스트와 문자열] 컴퓨터가 문자를 다루는 방식, unicode, encode & decode
컴퓨터의 문자 인식, 전송 그리고 아스키 코드
사람들은 인식할 수 있는 문자들(영어 알파벳 또는 한글 등등)을 컴퓨터는 문자 그대로 인식하지는 못한다. 컴퓨터는 0과 1밖에 몰라서, 컴퓨터가 어떤 특정 문자를 인식하고 사용할 수 있도록 하기 위해선 그 문자에 숫자를 붙여 넘버링하는 방법이 좋을 것이다. (그리고 모든 숫자는 이진법으로 나타낼 수 있으니 가능하다) 예를 들면 a라는 문자에 1, b라는 문자에는 2 이런 식으로 임의의 표를 만들어 정해두는 것이다. 그러면 사용자가 a라는 문자를 입력하면 컴퓨터는 이 표에서 a를 찾고 그에 대응되는 숫자인 1을 찾아 해당 문자를 인식할 수 있게 될 것이다.
이렇게 사용자가 입력한 문자, 기호들을 컴퓨터가 이용할 수 있는 신호로 만드는 과정을 문자 인코딩(character encoding), 또는 텍스트 인코딩 (text encoding) 또는 간략히 인코딩(encoding)이라고 한다.
일반적으로 우리가 일상생활에서 쓰고 읽는 숫자들은 10진법이다. 이 10진법 숫자들은 모두 이진법 숫자들로 바꿀 수 있다. 이 이진법이 0과 1만 포함하므로 이 이진법이 컴퓨터가 알아들을 수 있는 가장 기초적인 언어라 볼 수 있을 것이다. 따라서 문자에 넘버링한 십진법 숫자들을 이진법으로 바꾸면 컴퓨터는 이 문자들을 저장하거나 처리할 수 있게 될 것이다. 이렇게 이진법으로 표현되는 데이터를 컴퓨터에서 처리하기 위해 존재하는 개념이 비트, 바이트이다.
컴퓨터에 데이터를 저장하는 기초적인 단위는 바이트 (byte)이다. 1바이트에는 8개의 비트(bit)가 있으며, 한 비트에는 0 또는 1이 들어갈 수 있다. 따라서 1바이트에는 최대 2의 8제곱인 256개의 고유 데이터들을 표현할 수 있는 것이다. 즉, 256개의 문자들을 이 1바이트에 표현할 수 있게 되는 것이다.
따라서 인코딩은 간략히 표현하자면 문자를 바이트로 바꾸는 과정이다. 즉 컴퓨터가 이해할 수 있는 언어로 바꾸는 것이라 볼 수 있을 것이다. 반대로 바이트를 문자로 바꾸는 과정, 즉 인간이 인식할 수 있는 문자로 바꾸는 과정을 디코딩(decoding)이라 한다.
인간의 문자를 컴퓨터가 인식할 수 있도록 각 문자에 숫자를 대응시킨 표같은 것, 즉 문자셋이 필요한데, 초기에 나온 것이 아스키(ASCII) 코드이다. 1960년대에 나온 미국의 정보교환용 7비트 부호체계이다. 미국에서 나온 것이라 인간이 쓰는 언어 중에는 영어만 들어가 있다. 이 표에는 영어 알파벳 소문자, 대문자 뿐만 아니라 숫자, 그리고 각종 기호와, 역사적인 이유로 포함된 출력 불가능한 제어 코드까지 총 128개의 부호를 포함하고 있다. 256개가 아닌 128개인 이유는, 8비트에서 한 비트는 통신 에러 검출 용도로 쓰기 위해서라고 한다. 즉, 신호를 전송했는데 전송 과정에서 이 신호가 변질되거나 에러가 났는지를 확인하기 위한 용도이다.
그런데 시대가 지남에 따라 인터넷이 전 세계적으로 쓰이게 되었고, 따라서 영어만 포함된 아스키 코드는 전 세계 언어들을 포함하지 못한다는 한계에 부딪히고 말게 된다. 이에 대한 대안으로 유니코드가 등장하게 된다.
유니코드
유니코드는 전 세계 언어의 문자들과 수학이나 여러 분야에서 쓰이는 특수 기호까지 모두를 포함하는 국제 기준이다. 역시나 각 문자에 고유 숫자를 부여하는 방식이다. 유니코드에는 문자와 그 문자를 설명해주는 이름, 그리고 유니코드 식별숫자 (ID)가 연결되어 있는 구조이다. 전 세계에는 문자들이 워낙 많으니 당연하게도 유니코드는 거대한 표들로 존재하게 될 것이다. 전체 표는 다음의 사이트에서 확인해볼 수 있다.
유니코드에는 평면 (plane)이라는 개념이 존재한다. 아무래도 전 세계 언어와 더불어 수학 등 여러 분야의 문자들까지 포함하다보니 너무 많은 이 문자들을 논리적으로 분류할 필요가 생길 것이다. 평면은 이를 위해 유니코드 전체를 논리적으로 나눈 구획이라고 볼 수 있다. 0번부터 16번까지 모두 17개로 나뉜다고 하며 각 평면은 2의 16제곱인 65536개의 코드로 구성된다고 한다. 더 자세히 말하자면, 각 평면은 또 256개의 작은 평면들로 나뉘며, 그 작은 평면 하나에는 256개의 코드들을 포함할 수 있는 구조이다. 그래서 65536개의 코드를 포함할 수 있는 것이다. 그 중 유니코드의 0번 평면은 다국어 기본 평면(basic multilingual plane, BMP)이라고 하며, 유니코드 ID는 U + 0000부터 U + FFFF까지의 코드를 차지한다고 한다. 전 세계 국가들이 현재 쓰는 여러 언어들과 특수 문자들이 있으며, 한글도 여기에 포함된다.
또 다른 평면에는 옛 문자나 음악 기호, 수학 기호를 표현하는 1번 다국어 보충 평면, 초기 유니코드에 포함되지 않은 한중일 통합 한자를 포함하는 2번 상형 문자 보충 평면 등이 있다.
다음 그림은 유니코드 0번 다국어 기본 평면을 그림으로 나타낸 것이며, 1칸 당 256자를 포함할 수 있다.
출처: 레퍼런스 [5] 참고. cjk문자는 한중일 문자를 의미.
참고로 유니코드 번호는 유니코드 사이트에서 보면 알 수 있듯이 보통 U+xxxx 이렇게 U와 뒤에 4자리 숫자로 이루어져 있는데, 이 때 쓰이는 숫자들은 모두 16진법이다. 그래서 위 그림에서도 유니코드 번호에 A~F의 영문자까지 쓰이는 것이다.
그렇다면 파이썬에선 이 유니코드를 어떻게 이용할 수 있을까?
파이썬 3 유니코드 문자열
파이썬 3 버전에서는 파이썬 2 버전과는 다르게 바이트 배열이 아닌 유니코드 문자열을 이용한다.
만약 어떤 문자에 대해 그 문자의 유니코드 숫자나 유니코드 이름을 알면 해당 문자를 파이썬 문자열 내에서 쓸 수 있다. 파이썬에서 유니코드 문자를 다루는 방법은 다음과 같다.
- \uxxxx처럼 \u 뒤에 4자리의 16진법 숫자들을 쓰면 유니코드의 다국어 기본 평면 중 한 문자를 특정할 수 있다. 4자리 숫자 중 처음 두 자리는 평면 숫자로, 00부터 FF까지 있다. (위의 다국어 기본 평면 그림을 참고하면 이해가 쉬울 것이다) 그리고 다음 두 자리 숫자는 한 평면 안에 있는 문자의 인덱스를 지칭한다. 참고로 00 평면에는 아스키 코드가 그대로 있다고 한다.
- 더 높은 평면에서의 문자를 사용하기 위해서는 더 많은 비트가 필요하다. 파이썬에서는 대문자 U를 써서 \Uxxxxxxxx 형식으로 표현한다. U 뒤에 8개의 x자리에 16진법 숫자를 쓰면 된다. 그런데 맨 왼쪽 자리에는 0이어야만 한다.
- 모든 문자에는 유니코드 표준 이름이 있다고 했다. 이 이름을 특정하고자 한다면 \N{이름} 이라고 치면 된다.
특히, 파이썬에서는 유니코드를 처리할 수 있게 해주는 unicodedata라는 모듈이 존재한다. 여기에 있는 함수 중 다음의 두 함수를 소개한다.
- lookup(이름) : 대소문자 구별하지 않고 이름을 인수로 받으며, 그 결과로 유니코드 문자를 반환한다.
- name(유니코드 문자) : 유니코드 문자를 인수로 받으며 그 결과로 영어 대문자로 표현된 유니코드 이름을 반환한다.
이 때 유니코드 문자는 인터넷 어디선가 (유니코드 전체 표 사이트라든가) 그대로 복사해서 인수로 넘겨도 되고 또는 앞서 설명했듯 \u 등을 이용하여 유니코드 문자의 번호를 언급해도 된다.
다음 예제는 유니코드 문자를 받고 그 문자의 이름과 문자 자체를 출력해주는 함수이다.
def unicode_test(value):
import unicodedata
name = unicodedata.name(value)
uni_char = unicodedata.lookup(name)
print('argument={}, name={}, uni_char={}'.format(value, name, uni_char))
unicode_test('\u0041')
unicode_test('%')
unicode_test('\u2603')
argument=A, name=LATIN CAPITAL LETTER A, uni_char=A
argument=%, name=PERCENT SIGN, uni_char=%
argument=☃, name=SNOWMAN, uni_char=☃
참고로 위 예제의 마지막 줄에서처럼 특수한 기호나 이모지도 유니코드로 표현할 수 있는데, 가끔 플레이스 홀더 (placeholder, 빠져 있는 다른 것을 대신하는 기호나 텍스트의 일부) 가 대신해서 뜰 수도 있다고 한다.
다음은 café란 단어를 쓰기 위해 é의 유니코드 이름을 얻고, 이 이름으로 해당 문자를 출력하는 코드이다.
import unicodedata
def get_unicode_name(character):
return unicodedata.name(character)
def get_unicode_char(uni_name):
return unicodedata.lookup(uni_name)
place = 'café'
e_name = get_unicode_name('é')
e_char = get_unicode_char(e_name)
print(e_name)
print(e_char)
LATIN SMALL LETTER E WITH ACUTE
é
그리고 이 문자를 이용해 café란 단어를 문자열에 삽입하고 이를 변수에 할당하고 출력한다.
place2 = 'caf\u00e9'
print(place2)
place3 = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
print(place3)
café
café
참고로 다음의 코드는 에러를 발생시킨다.
place3 = 'caf\N{}'.format(e_name)
print(place3)
place3 = 'caf\N{}'.format(e_name)
^
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 3-7: unknown Unicode character name
함수를 통해 얻은 유니코드 이름을 일일이 타이핑하거나 복사, 붙여넣기 하지 않고 e_name에 저장하여 이를 그대로 쓰려고 했지만 동작하지 않는다. 이를 해결하려면 어떻게 해야 할 지 아직 모르겠다. 아니면 아예 방법이 없는 걸지도 모르겠다.
utf-8
만약 우리가 쓴 파이썬 코드 파일이 예를 들어 웹과 통신하거나 외부와 데이터를 교환하는 상황이 나타난다면 다음의 두 가지가 필요하다
- 문자열에서 바이트로 인코딩하는 방법
- 바이트에서 문자열로 디코딩하는 방법
그렇다면 텍스트 인코딩 또는 디코딩 하는 방법은 무엇이 있을까?
사실 다양하지만, 파이썬이나 리눅스, html 등 여러 곳에서 공통적으로 쓸 수 있는 표준 텍스트 인코딩으로 utf-8이 존재한다. 이 utf-8은 하나의 유니코드 문자에 해당하는 유니코드 숫자를 저장, 전송하기 위해 4바이트를 쓴다. 4바이트는 각각 다음과 같이 사용되도록 고안되었다고 한다.
- 1바이트는 아스키 코드
- 2바이트는 키릴 문자를 제외한 대부분의 라틴 계열 언어
- 3바이트는 그 외 나머지 다국어 기본 평면 내의 문자들
- 4바이트는 앞서 설명한 문자들 외에 모든 것들. 여기엔 아시아 언어와 몇몇 기호들도 포함한다.
하지만 어떤 웹페이지에서는 utf-8이 아닌 latin-1이라든가 window 1252와 같은 다른 인코딩 방식으로 인코딩되어 있는 곳들도 많기에 해당 인코딩 방식으로 써진 문자를 그대로 옮겨다가 utf-8로 쓰려고 하면 글자가 깨지거나 하는 등의 에러가 발생할 수 있으니 주의하자.
인코딩
파이썬에서는 인코딩을 위해 encode()라는 함수를 제공한다. 첫 째 인자로는 인코딩 이름을 넣는다. 인코딩 방식의 종류는 다음의 표를 참조한다.
- 인코딩
이름 | 설명 |
---|---|
‘ascii’ | 7비트를 사용하는 예전 방식. |
‘utf-8’ | 8비트 인코딩. 대부분의 경우에 쓰인다고 한다. |
‘latin-1’ | ISO 8859-1로도 알려져 있다. |
‘cp-1252’ | 윈도우에서 쓰이는 인코딩 |
‘unicode-escape’ | 파이썬 유니코드 포맷. \uxxxx 또는 \Uxxxxxxxx 방식 |
이제 인코딩을 이용하는 예제를 살펴보자. 앞선 예제에서 보였던 스노우맨 글자를 이용하고자 한다. 이 스노우맨의 유니코드 숫자를 snowman 변수에 할당하였다.
snowman = '\u2603'
print(len(snowman))
1
☃
위 예제에서는 해당 유니코드 문자의 길이를 출력해보았다. 해당 유니코드 문자를 저장하기 위해 필요한 바이트 수와는 관련없다. 해당 문자는 한 글자이기 때문에 위 실행결과에서도 1이 출력된 것이다.
자, 이제 이 문자를 일련의 바이트로 인코드 해보자.
snowman = '\u2603'
encoded_snowman = snowman.encode('utf-8') # 바이트를 담는 변수
print(len(encoded_snowman))
print(encoded_snowman)
3
b'\xe2\x98\x83'
위 예제에서는 utf-8방식으로 스노우맨 문자를 인코딩하였고, 그 결과로 나온 일련의 바이트들을 encoded_snowman이라는 변수에 할당하였다. 그 후 이 변수의 길이와 내용을 출력해본 것이다. 보다시피 이제는 바이트로 인코드 되었기에, 해당 문자는 3바이트를 써서 길이가 3이 출력된 것이다. (위 실행결과의 마지막 줄을 보면 x로 시작하는 데이터들이 \에 의해 3개로 나뉘어져 있는 것으로 보아 3바이트가 쓰였고, 각각의 바이트에 유니코드 숫자를 인코드하여 넣은 것으로 보인다.)
만약 스노우맨 문자를 utf-8이 아닌 ascii로 인코딩한다면 어떨까?
snowman = '\u2603'
encoded_snowman = snowman.encode('ascii') # ascii로 변경
print(len(encoded_snowman))
print(encoded_snowman)
Exception has occurred: UnicodeEncodeError
'ascii' codec can't encode character '\u2603' in position 0: ordinal not in range(128)
해당 문자는 유니코드에는 존재하나 아스키 코드에는 존재하지 않기에 발생하는 에러이다.
encode() 함수의 두 번째 인자로는 인코딩 예외를 피하기 위한 특정 문자열을 대입한다. 기본값으로는 ‘strict’가 설정되어 있는데, 이는 아스키 코드가 아닌 문자를 인코딩하려는 경우 UnicodeEncodeError를 발생시킨다.
하지만 두 번째 인자에 ‘ignore’를 넣으면 인코드 되지 않을 시 빈 데이터를 반환한다.
snowman = '\u2603'
encoded_snowman = snowman.encode('ascii', 'ignore')
print(len(encoded_snowman))
print(encoded_snowman)
b''
그 외 두 번째 인자로 들어가는 몇몇 인수들과 인코딩 실패 시 그에 따른 반환값은 다음과 같다.
두 번째 인자 | 인코딩 실패 시 기능 |
---|---|
‘ignore’ | 빈 문자열 반환 |
‘replace’ | 물음표(?) 반환 |
‘backslashreplace’ | 해당 문자의 유니코드 숫자를 unicode-escape 방식 (\uxxxx’)의 문자열로 반환한다. |
snowman = '\u2603'
encoded_snowman = snowman.encode('ascii', 'backslashreplace')
print(len(encoded_snowman))
print(encoded_snowman)
6
b'\\u2603'
6이 반환된 이유는 ‘\u2603’ 자체의 길이를 반환한 것으로 추정된다.
디코딩
우리가 파일이나 데이터베이스, 웹사이트, API등 외부 요소로부터 텍스트 데이터를 얻어 파이썬 코드 내에서 사용하고자 한다면, 이 외부에서 온 텍스트들은 보통 바이트 문자열로 인코딩되어 있는 상태이기에 이를 유니코드 문자로 디코딩할 필요가 있다. 파이썬에서는 인코딩 함수인 encode() 함수와 더불어 디코딩 함수인 decode() 함수가 존재한다. 인코딩과 마찬가지로 decode() 함수 내에 디코딩 방식 이름을 문자열로 넣어주면 해당 방식으로 디코딩해준다.
다음 예제에서는 외부에서 인코딩된 텍스트를 가져왔다고 가정하여 이를 place 변수에 할당한다. 그 후 해당 문자가 무엇인지 알기 위해 이를 디코딩하는 과정을 코드로 구현하고 있다.
place = 'caf\u00e9'
place_byte = place.encode('utf-8') #1
print('=====')
print(place) #2
print(type(place))
print(place_byte)
print(type(place_byte))
print('=====') #3
place_decoded = place_byte.decode('utf-8') #4
print(place_decoded)
place_decoded_latin = place_byte.decode('latin-1') #5
print(place_decoded_latin)
=====
café #2
<class 'str'>
b'caf\xc3\xa9'
<class 'bytes'> #3
=====
café
café
#1에서는 place에 할당된 문자를 utf-8 방식으로 인코딩하고 있다. 그 후 place와 이를 인코딩한 바이트 문자열이 할당된 place_byte를 그 내용과 자료형을 각각 출력하고 있다. 실행결과를 통해 place와 place_byte가 서로 다른 자료형임을 알 수 있다.
한 편, #4에서 인코딩되어 바이트가 저장된 place_byte로부터 utf-8방식으로 디코딩하는 모습이다. 그 결과로 해당 유니코드 문자를 출력할 수 있게 된다. #5에서는 같은 바이트를 latin-1이라는 방식으로 디코딩하고 있다. 해당 인코딩 방식은 8비트를 쓰고 범위가 128(16진법으로 80)에서 255(16진법으로 FF)의 범위를 쓰고 있어 위 예제에서는 에러 없이 실행되었으나, 각 문자에 넘버링된 숫자들이 다르기에 전혀 다른 문자가 출력되었음을 알 수 있다.
외부 자료로부터 인코딩된 텍스트를 받아 이를 디코딩 할 때의 문제점은, 인코딩된 바이트 데이터 자체에는 인코딩 방식이 알려져 있지 않기 때문에 어떤 방식으로 이를 디코딩할지 알기가 어렵다는 것이다. (지금으로서는 여러 인코딩 방식으로 인코딩을 시도해보는 수밖에 없는 듯 싶다)
웬만해선 utf-8 인코딩을 사용하기를 권장되고 있다. 어디든지 지원이 되고 거의 모든 문자들을 표현할 수 있으며, 인코딩 및 디코딩 속도도 빠르기 때문이다.
References
[1] Bill Lubannovic, “Introducing Python” (O’REILLY, 2015)
[2] 문자 인코딩, 위키피디아
[3] ASCII, 위키피디아
[4] 아스키 코드, 나무위키
[5] 유니코드 평면, 위키백과
This content is licensed under
CC BY-NC 4.0
댓글남기기