문자열 인코딩
: 2진법을 사용하는 컴퓨터가 인간의 언어를 일정한 규칙에 따라 2진수로 변환하는 방식 # 2진수를 10진수로 처리
print('10진수(01000001)={0}'.format(0b01000001))
# 2진수를 16진수로 처리
print('16진수(01000001)={0}'.format(hex(0b01000001)))
# 2진수를 문자로 처리
print('16진수(01000001)={0}'.format(chr(0b01000001)))
10진수(01000001)=65
6진수(01000001)=0x41
16진수(01000001)=A
아스키코드
는 처음으로 표준을 정립한 문자열 인코딩 방식#ascii.py
def print_text(text, encoding_type):
byte_data = text.encode(encoding_type)
hex_data_as_str = ' '.join("{0}".format(hex(c)) for c in byte_data)
int_data_as_str = ' '.join("{0}".format(int(c)) for c in byte_data)
print('\'' + text + '\' 문자열 길이: {0}'.format(len(text)))
print('\'' + text + '\' 전체 문자를 표현하는 데 사용한 바이트 수: {0} 바이트'.format(len(byte_data)))
print('\'' + text + '\' 16진수 값 : {0}'.format(hex_data_as_str))
print('\'' + text + '\' 10진수 값 : {0}'.format(int_data_as_str))
print_text("Hello", 'ascii')
def print_text(text, encoding_type):
byte_data = text.encode(encoding_type)
hex_data_as_str = ' '.join("{0}".format(hex(c)) for c in byte_data)
int_data_as_str = ' '.join("{0}".format(int(c)) for c in byte_data)
print('\'' + text + '\' 문자열 길이: {0}'.format(len(text)))
print('\'' + text + '\' 전체 문자를 표현하는 데 사용한 바이트 수: {0} 바이트'.format(len(byte_data)))
print('\'' + text + '\' 16진수 값 : {0}'.format(hex_data_as_str))
print('\'' + text + '\' 10진수 값 : {0}'.format(int_data_as_str))
print_text('Hello', 'euc-kr')
print_text('안녕하세요', 'euc-kr')
영문자 "Hello"를 출력할 떄 아스키 코드와 동일하게 5바이트를 사용했지만, 한글 "안녕하세요"를 출력하기 위해서는 10바이트를 사용했습니다. EUC-KR로 아스키 코드 영역에 있는 글자를 표현할 때는 1바이트를 사용하지만, 한글 문자를 표현할 때는 2바이트를 사용하기 때문입니다.
이처럼 문자열 인코딩에서는 실제 문자열 길이가 버퍼 길이와 다른 경우가 많습니다. 실제 문자열 길이는 사람 눈에 보이는 문자 길이에 해당하고, 버퍼 길이는 컴퓨터가 문자를 표현하는 데 사용한 바이트 수를 의미합니다. 여기서 버퍼는 메모리에 할당된 공간을 뜻합니다.(예를 들어 변수를 선언해 숫자나 문자열 값을 넣거나, 새로운 객체를 생성하는 행위 등 모두 버퍼가 필요함). 그래서 실제 문자열 길이와 컴퓨터가 할당하는 버퍼 크기는 항상 다를 수 있다는 점
개발 환경에서는 실제 문자열 길이와 컴퓨터가 할당하는 버퍼 크기를 동일하게 취급해 생기는 버그가 생각보다 많습니다. 문자열을 취급할 때는 어떤 문자열 인코딩 방식을 쓰는지 반드시 알아야 하고, 가능한 같은 문자열 인코딩을 사용하는 것이 좋음.
보통 일반적인 문자는 3바이트 내로 처리되며, 4바이트 영역에는 이모지같은 문자가 있습니다.
고대 문자 같은 것을 사용하지 않는 한 5바이트 이상을 쓰는 경우는 거의 없습니다.
def print_text(text, encoding_type):
byte_data = text.encode(encoding_type)
hex_data_as_str = ' '.join("{0}".format(hex(c)) for c in byte_data)
int_data_as_str = ' '.join("{0}".format(int(c)) for c in byte_data)
print('\'' + text + '\' 문자열 길이: {0}'.format(len(text)))
print('\'' + text + '\' 전체 문자를 표현하는 데 사용한 바이트 수: {0} 바이트'.format(len(byte_data)))
print('\'' + text + '\' 16진수 값 : {0}'.format(hex_data_as_str))
print('\'' + text + '\' 10진수 값 : {0}'.format(int_data_as_str))
print_text('Hello', 'utf8')
print_text('안녕하세요', 'utf8')
'Hello' 문자열 길이: 5
'Hello' 전체 문자를 표현하는 데 사용한 바이트 수: 5 바이트
'Hello' 16진수 값 : 0x48 0x65 0x6c 0x6c 0x6f
'Hello' 10진수 값 : 72 101 108 108 111
'안녕하세요' 문자열 길이: 5
'안녕하세요' 전체 문자를 표현하는 데 사용한 바이트 수: 15 바이트
'안녕하세요' 16진수 값 : 0xec 0x95 0x88 0xeb 0x85 0x95 0xed 0x95 0x98 0xec 0x84 0xb8 0xec 0x9a 0x94
'안녕하세요' 10진수 값 : 236 149 136 235 133 149 237 149 152 236 132 184 236 154 148
"Hello"를 출력할 때는 아스키 코드와 동일하게 5바이트를 사용합니다. 하지만 "안녕하세요"를 출력할 떄는 15바이트를 사용했습니다.
def print_text(text, encoding_type):
byte_data = text.encode(encoding_type)
hex_data_as_str = ' '.join("{0}".format(hex(c)) for c in byte_data)
int_data_as_str = ' '.join("{0}".format(int(c)) for c in byte_data)
print('\'' + text + '\' 문자열 길이: {0}'.format(len(text)))
print('\'' + text + '\' 전체 문자를 표현하는 데 사용한 바이트 수: {0} 바이트'.format(len(byte_data)))
print('\'' + text + '\' 16진수 값 : {0}'.format(hex_data_as_str))
print('\'' + text + '\' 10진수 값 : {0}'.format(int_data_as_str))
print_text('Hello', 'utf-16')
print_text('안녕하세요', 'utf-16')
'Hello' 문자열 길이: 5
'Hello' 전체 문자를 표현하는 데 사용한 바이트 수: 12 바이트
'Hello' 16진수 값 : 0xff 0xfe 0x48 0x0 0x65 0x0 0x6c 0x0 0x6c 0x0 0x6f 0x0
'Hello' 10진수 값 : 255 254 72 0 101 0 108 0 108 0 111 0
'안녕하세요' 문자열 길이: 5
'안녕하세요' 전체 문자를 표현하는 데 사용한 바이트 수: 12 바이트
'안녕하세요' 16진수 값 : 0xff 0xfe 0x48 0xc5 0x55 0xb1 0x58 0xd5 0x38 0xc1 0x94 0xc6
'안녕하세요' 10진수 값 : 255 254 72 197 85 177 88 213 56 193 148 198
UTF-16, UTF-32는 바이트 순서 표시(byte order mark) | BOM을 사용합니다. BOM은 문자열 가장 맨 앞 2바이트에 0xFEFF(유니코드로 U+FEFF)로 표기하여 사용한다는 것을 의미합니다. 또한 0xFE와 0xFF 중 어떤 문자가 먼저 오는지에 따라 리틀 엔디언 (LE)과 빅 엔디언(BE)으로 나뉩니다. 그래서 두 방식에 따라 문자열 인코딩 시 바이트 데이터를 조합하는 순서가 바뀌게 됩니다.
BOM을 이용하여 바이트 표현 순서를 정하는 이유는, CPU 설계에 따라 바이트 값을 처리하는 순서가 다르기 때문입니다. 같은 0xFEFE를 CPU가 읽을 때 리틀 엔디언 방식은 0xFF 다음 0xFE을 읽으며, 빅 엔디언 방식은 0xFE 다음 0xFF를 읽음.
종류 | 0x1234표현 | 0x12345678표현 |
---|---|---|
빅엔디언 | 12 34 | 12 34 56 78 |
리틀 엔디언 | 34 12 | 78 56 34 12 |
빅 엔디언은 문자를 구성하는 바이트 두 개 [12], [34]중 큰 단위인 [12]가 먼저 나옵니다. 반대로 리틀 엔디언은[12], [34]중 작은 단위인 [34]가 먼저 나오는 겁니다.
위 코드, UTF16은 0xFF
가 먼저 나왔으므로 리틀 엔디언(UTF-16-LE) 인코딩으로 볼 수 있음. 리틀 엔디언은 뒷자리부터 읽으므로 0x48, 0xC5, oxC548로 읽습니다. 오늘날 대부분의 개인 컴퓨터는 리틀 엔디언 방식을 사용함.
0xfe 0xff 0xc5 0x48 0xb1 0x55 0xd5 0x58 0xc1 0x38 0xc6 0x94
이전 [코드 1-5] 출력 결과를 살펴 봅시다. UTF-16은 특수한 경우를 제외하고는 모두 2바이트를 사용하니 2바이트로 끊어 읽으면 됩니다. 'Hello'문자열을 구성하는 각 문자를 표현하는 데 사용하는 값은 아래와 같습니다.
문자 | 16진수 |
---|---|
H | 0x48 0x00 |
e | 0x65 0x00 |
l | 0x6c 0x00 |
o | 0x6f 0x00 |
이 코드를 실행한 컴퓨터는 리틀 엔디언 방식을 사용하므로 거꾸로 조합하면 각각 0x48, 0x65, 0x6C, 0x6F입니다. 이 값들은 실제 UTF-16 코드 표에서 사용하는 값으로 'H'문자열을 예를 들면 다으모가 같습니다.
| H -> LATIN CAPITAL LETTER H(U+0048)