파이썬 바이트 표현 (bytes, bytearray)
개념정리
- 코드 포인트(code point): 각 유니코드 문자에는 고유한 코드포인트(숫자)가 할당되어있습니다. 이 유니코드 하나하나에 매핑되는 숫자를 코드포인트라고합니다. 1:1로 매핑되어있고, 코드포인트를 어떤 인코딩 방식을 하냐에 따라, 문자열이 달라질 수 있습니다.
- 인코딩: 코드포인트를 바이트 시퀀스로 변환하는 규식을 일컫습니다.
- 디코딩: 바이트 시퀀스를 코드포인트로 역변환 하는 규칙을 일컫습니다
- 유니코드(Unicode): 모든 문자를 컴퓨터에서 일관되게 표현하고 다루기 위한 국제 표준입니다. 모든 문자열은 코드포인트를 이용하여, 1:1 매핑되어있습니다. 그리고 유니코드로 표현되는 경우 "U+"라는 접두사를 사용합니다.
코드포인트: 문자와 1:1로 매핑하는 숫자
코드포인트는 문자와 1:1로 매핑되는 숫자입니다. 예를 들어, `U+1f62e`와 같이 생긴 숫자가 코드포인트입니다. 유니코드의 코드포인트는 유니코드 표준으로 "U+"라는 접두사를 붙여서 4자리에서 6자리의 16진수로 표현합니다. `U+` 뒤에 1f62e은 각각이 16문자 문자열입니다. 이 문자는 😮의 이모지를 뜻합니다.
이 코드포인트를 UTF-8로 인코딩한다면, f0 9f 98 ae가 됩니다.
>>> code_point = int("1f62e", 16) # "U+1f62e"은 16진수로 표현합니다.
>>> print(code_point)
128558
>>> chr(code_point)
😮
>>> chr(code_point).encode("utf-8")
b'\xf0\x9f\x98\xae'
UTF-8은 가변 길이 인코딩 방식으로, 각 유니코드 코드 포인트를 1바이트에서 4바이트까지 다양한 길이로 인코딩합니다. 아래에 간단한 UTF-8 인코딩 테이블을 제시하겠습니다:
유니코드 범위 (16진수)UTF-8 바이트 시퀀스 (이진수)
U+0000 - U+007F | 0xxxxxxx |
U+0080 - U+07FF | 110xxxxx 10xxxxxx |
U+0800 - U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 - U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
여기서 "x"는 유니코드 코드 포인트의 비트를 나타냅니다. UTF-8에서 첫 번째 바이트는 0, 110, 1110, 또는 11110으로 시작하며, 다음 바이트는 항상 10으로 시작합니다. 이 테이블을 사용하여 유니코드 문자를 UTF-8로 인코딩할 수 있습니다.
이제 "b'\xf0\x9f\x98\xae'"을 디코딩 과정을 설명해 보겠습니다. UTF-8에서 각 문자의 첫 번째 바이트를 보고 이 바이트가 어떤 범위에 속하는지에 따라 그 문자가 몇 바이트인지 결정됩니다. 그런 다음 각 바이트에서 필요한 비트를 추출하여 이를 유니코드 코드 포인트로 변환합니다.
UTF-8 디코딩의 과정은 다음과 같습니다:
- 첫 번째 바이트를 확인합니다. => '\xf0'이며, 이는 이진수로 '1111 0000'을 나타냅니다. 이 바이트는 4바이트 문자의 첫 번째 바이트를 나타냅니다.
- 첫 번째 바이트를 보고 문자의 길이를 결정합니다. => '11110000'은 네 번째 범위에 속하므로 이 문자는 총 4바이트로 구성됩니다.
- 첫 번째 바이트의 마지막 4비트를 제외한 나머지 비트는 유니코드 코드 포인트를 나타냅니다. 여기서는 '1110'으로 시작하므로 이후 3바이트의 유니코드 코드 포인트가 함께 사용됩니다.
- 다음 바이트부터는 모두 '10'으로 시작해야 합니다. 여기서는 '\x9f', '\x98', '\xae'가 모두 이 조건을 충족합니다.
- 각 바이트에서 첫 번째 바이트 이후의 유니코드 코드 포인트를 나타내는 비트를 추출합니다. 이 비트는 모두 '10xxxxxx' 형식입니다. 따라서 각 바이트의 마지막 6비트를 추출하여 이를 합칩니다.
- 마지막으로 이진수를 십진수로 변환한 다음 이를 유니코드 코드 포인트로 해석합니다.
- 유니코드 코드 포인트를 유니코드 문자로 변환합니다.
또 다른 파이썬에서의 예시를 들어보겠습니다. 안녕하세요의 문자열이 있는 경우 `ord()`함수를 이용해서 10진법으로 표현할 수 있습니다. hex은 정수를 입력받아 16진법으로 표현합니다. 그렇기에, 이 10진법으로 표현한 것을 다시 16진법으로 표현하기위해서 `hex()`함수를 이용해서 변환합니다. 그리고 출력하면 아래와 같이 얻을 수 있습니다. 그리고 각 10진법으로 표현하면 숫자로만 열거됩니다.
>>> text = "안녕하세요"
>>> code_points = [hex(ord(char)) for char in text]
>>> print(code_points)
['0xc548', '0xb155', '0xd558', '0xc138', '0xc694']
code_points = [ord(c) for c in text]
>>> print(code_points)
[50504, 45397, 54616, 49464, 50836]
`0x`은 파이썬에서 16진법을 표현하는 방식입니다. "안"에 해당하는 문자는 10진법으로 50504이며, 이를 16진법으로 표현하면 "C548"입니다.
"안녕하세요"를 utf-8로 인코딩하면 아래와 같습니다. b로 시작하는 것은 바이트 리터럴을 나타냅니다. 파이썬에서 바이트 리터럴은 바이트 문자열을 나타내며, 이는 일련의 8비트 바이트로 구성된 바이트 시퀀스입니다.
인코딩을 하고하면, b''로 표현되는데 어떤문자는 문자 그대로 표현되고, 어떤 경우는 16진법으로 표현됩니다. 이 차이는 무엇일까요?
아래의 예시를 하나 보겠습니다. apple은 utf-8의 경우는 b'apple이 출력됩니다. b''여도 16진수로 표현되는 경우가 있고, 그렇지 않은 경우가 있는데요. 아스키문자(공백부터 물결표(~))까지는 아스키 문자 그대로 출력이 가능한 경우는 그대로 출력됩니다. 그렇지 않은 경우는 16진수로 표현되기 때문입니다. 아스키코드 확인(URL)
>>> text ="안녕하세요"
>>> text.encode("utf-8")
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
>>> text2 = 'apple'
>>> text2.encode('utf-8')
b'apple'
>>> text2.encode('utf-8')[0]
97
>>> text2.encode('ascii')[0]
97
아래의 아스키코드를 보면 Dec, Hx, Oct, Char가 있습니다. Dec은 Decimal(10진법)이며, Hx은 Heximal(16진법)입니다. Oct은 Octal으로 8진법입니다. apple을 utf-8로 인코딩한다음에 표현해보면, 97이나오고, 97에해당한는 문자(char)은 'a'입니다. 마찬가지로, 이를 ascii로 인코딩해도 97이나옵니다.
UTF-8와 ASCII는 대부분의 영문 알파벳 및 일부 특수 문자에 대해서는 동일한 방식으로 인코딩됩니다. ASCII는 UTF-8의 서브셋이며, UTF-8은 ASCII를 포함하고 있기 때문에 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩이 동일한 결과를 생성합니다.
따라서 'utf-8'과 'ascii' 인코딩을 사용하여 같은 문자열을 인코딩할 경우, 영문 알파벳과 일부 특수 문자에 대해서는 두 인코딩 결과가 동일할 수 있습니다. 이 경우에는 두 결과가 모두 97로 동일하게 나타나게 됩니다.
파이썬에서의 자료형: bytes, bytearray
- bytes:
- bytes는 불변(immutable)한 바이트 시퀀스입니다. 즉, 한 번 생성되면 내용을 변경할 수 없습니다.
- 바이트 객체는 0부터 255까지의 숫자로 이루어진 값들의 불변된 시퀀스입니다.
- 주로 파일 입출력, 네트워크 통신 등에서 사용됩니다.
- 예를 들어, b'hello'는 바이트 리터럴로, 문자열 'hello'를 바이트로 표현한 것입니다.
- bytearray:
- bytearray는 변경 가능한(mutable)한 바이트 시퀀스입니다. 즉, 내용을 변경할 수 있습니다.
- 0부터 255까지 숫자로 이루어진 값들의 시퀀스입니다.
- 바이트 배열은 bytes와 유사하지만, 변경 가능하고 가변성이 있습니다.
- bytearray는 리스트와 유사하게 인덱싱 및 슬라이싱을 통해 요소를 수정하거나 추가할 수 있습니다.
- 주로 이진 데이터를 변경하거나 조작할 때 사용됩니다.
- 예를 들어, bytearray(b'hello')는 바이트 배열을 생성하고 초기값으로 문자열 'hello'를 사용한 것입니다.
간단히 말해, bytes는 불변하고 bytearray는 변경 가능한 바이트 시퀀스를 나타냅니다. 때에 따라서는 데이터를 수정해야 하는 경우 bytearray를 사용하고, 데이터를 변경할 필요가 없는 경우에는 bytes를 사용하는 것이 좋습니다.
In [23]: s = "안녕하세요"
In [24]: len(s)
Out[24]: 5
In [25]: s.encode('utf8')
Out[25]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
In [26]: encoded_s = s.encode('utf8')
In [27]: type(encoded_s)
Out[27]: bytes
In [28]: bytes("안녕하세요", encoding='utf-8')
Out[28]: b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
바이트(bytes)은 바이트어레이(bytearray)은 슬라이싱 및 인덱싱에 따라 아래와 같은 결과를 보입니다.
- bytes의 슬라이싱: bytes
- bytes의 인덱싱: int
- bytearray의 슬라이싱: bytearray
- bytearray의 인덱싱: int
>>> s = "안녕하세요"
>>> bytes(s, encoding='utf-8')
b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
>>> bytes(s, encoding='utf-8')[0]
236
>>> bytes(s, encoding='utf-8')[:1]
b'\xec'
>>> bytearray(s, encoding='utf-8')
bytearray(b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94')
>>> bytearray(s, encoding='utf-8')[:1]
bytearray(b'\xec')
>>> bytearray(s, encoding='utf-8')[0]
236
Struct module
파이썬 내장 모듈인 `struct` 모듈은 바이트로 전환하거나, 바이트를 문자열로 변경할 때, 주로 사용됩니다. 포맷을 지정하는 등의 역할도 가능합니다. `struct`모듈은 주로 아래와 같은 기능을 합니다.
- 패킹(Packing):
- struct.pack(format, v1, v2, ...) 함수를 사용하여 파이썬 데이터를 이진 데이터로 변환합니다.
- 이진 데이터는 바이트 문자열 또는 바이트 배열로 반환됩니다.
- 주어진 형식(format)에 따라 데이터가 패킹됩니다. 예를 들어, I는 4바이트의 부호 없는 정수를 나타내는 형식입니다.
- 언패킹(Unpacking):
- struct.unpack(format, buffer) 함수를 사용하여 이진 데이터를 파이썬 데이터 타입으로 변환합니다.
- 주어진 형식에 따라 바이트 데이터가 해체됩니다.
- 이 함수는 바이트 문자열 또는 바이트 배열을 받아서 주어진 형식에 따라 데이터를 언패킹합니다.
- 형식 지정자(Format Specifiers):
- 패킹 및 언패킹 작업에 사용되는 형식 지정자는 바이트 순서, 데이터 유형 및 크기를 정의합니다.
- 예를 들어, I는 부호 없는 4바이트 정수를 나타내며, f는 4바이트 부동 소수점을 나타냅니다.
Memory view
메모리뷰는 바이트 시퀀스를 생성/저장하는게 아니라(복사X), 공유메모리 방식으로 버퍼데이터를 조작할 때 주로 사용됩니다. 복사가 아니기에 직접 값을 변경은 불가능합니다.
메모리는 메모리에 대한 접근을 보다 효율적으로 다루는 데 사용됩니다. 아래는 memoryview의 간단한 예시입니다.
# 바이트 데이터로부터 memoryview 생성
>>> data = b'hello world'
>>> mv = memoryview(data)
# memoryview의 내용 확인
>>> print(mv) # <memory at 0x7fbba30d2080>>
# memoryview를 이용하여 데이터 접근
>>> print(mv[0]) # 104 (b'h'의 ASCII 코드 값)
>>> print(mv[1:5]) # <memory at 0x7fbba98234c0>>
>>> print(bytes(mv[1:5])) # b'ello' (슬라이싱된 바이트 데이터)
# memoryview를 이용하여 데이터 수정
>>> mv[6] = 66 # ASCII 코드 66은 'B' (원본 데이터가 수정불가, TypeError 발생)
예시: GIF포맷처리
GIF은 움짤로 주로 사용되는데요. GIF포맷을 struct을 이용해서 언패킹해보겠습니다.
GIF포맷은 아래와같이 포맷이 지정되어있습니다.
- 0번부터 3바이트는 GIF을 의미하며,
- 3번부터 3개의 바이트는 87a, 89a GIf 버전을 의미합니다.
- offset 6부터 2바이트: logicla width
- offset 8부터 2바이트: logical height
이를 파이썬으로 접근하면 아래와 같습니다. fmt의 "<"은 리틀엔디언, "3s"은 3개의 바이트, "H"은 하나의 정수를 의미합니다.
>>> import struct
>>>fmt = "<3s3sHH"
>>>with open("200w.gif", "rb") as fh:
...: img = memoryview(fh.read())
...:
>>> header = img[:10]
>>> header
<memory at 0x7f8b757bfac0>
>>> bytes(header)
b'GIF89a\xc8\x00\xff\x00'
>>> struct.unpack(fmt, header)
(b'GIF', b'89a', 200, 255)
- <: 리틀 엔디안(Endian)을 나타냅니다. 리틀 엔디안은 낮은 주소부터 데이터를 읽음을 의미합니다. (파일 형식에 따라 다를 수 있으며, 여기서는 일반적으로 사용되는 리틀 엔디안을 가정합니다.)
- 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 형식을 나타내는 문자열입니다. "GIF"을 읽어왔습니다.
- 3s: 3바이트 문자열을 의미합니다. 여기서는 GIF 파일 버전을 나타내는 문자열입니다. "89a"을 읽어왔습니다.
- HH: 각각 2바이트(16비트)의 부호 없는(short) 정수를 나타냅니다. 여기서는 GIF 이미지의 가로 및 세로 크기를 나타내는데 사용됩니다.
문제: tiff파일에서 바이트 조직
이 사이트에서 다운로드받은 tiff파일의 이미지 헤더를 다음을 참조하여(스펙), 이 파일 포맷에서 다음을 구하세요.
- 바이트 오더: 리틀엔디언, 빅엔디언
- tiff 파일 여부:
- IFD 시작 오프셋:
이중 모드 스트링 / 바이트 API
바이트로 정규표현식을 만들면 "\d"와 같은 글자나 "\w"은 아스키문자만 매칭되지만 str로 이 패턴을 만들면 아스키문자외에 유니코드나 숫자도 매칭이된다.
import re
# bytes로 정규표현식 패턴 컴파일하기
pattern_bytes = re.compile(rb'\d')
# bytes 대상으로 매칭 시도하고 모든 매칭 결과 찾기
binary_data = b'123 abc 456'
matches_bytes = pattern_bytes.findall(binary_data)
print("Matches with bytes regex pattern:", matches_bytes)
Matches with bytes regex pattern: [b'1', b'2', b'3', b'4', b'5', b'6']
import re
# str로 정규표현식 패턴 컴파일하기
pattern_str = re.compile(r'\d')
# str 대상으로 매칭 시도하고 모든 매칭 결과 찾기
text = '123 abc 456'
matches_str = pattern_str.findall(text)
print("Matches with str regex pattern:", matches_str)
Matches with str regex pattern: ['1', '2', '3', '4', '5', '6']