Python - struct 모듈을 사용하여 Binary 읽고/쓰기

출처

struct 모듈

struct 모듈은 pack, unpack, calcsize 등의 기능이 있으며, 이것들을 사용하여 정수, 부동 소수점 숫자, 문자열(을 encode 메소드에서 인코딩 한 것)을 bytes 객체로 변환하거나 반대로 bytes 객체에서 이것을 빼낸다.

bytes 객체로 변환하려면 “서식 지정 문자”라는 문자를 조합하여 이것들과 실제로 변환 할 값을 pack 함수에 넘기면 된다. 서식 지정 문자와 데이터를 unpack 함수에 전달하여 바이너리에서 읽어 낸 bytes 객체에서 필요한 데이터를 꺼낼 수있다.

서식 지정 문자에는 아래과 같은 것이 있다(일부이므로 더 자세한 것은 python 문서를 보기 바란다).

서식 지정    설명	크기(바이트 수)  
c              문자               1  
b              부호있는 정수   1  
B              부호없는 정수   1  
h              부호있는 정수   2  
H              부호없는 정수   2  
i              부호있는 정수   4  
I              부호없는 정수   4  
l              부호있는 정수   4  
L              부호없는 정수   4  
f              부동 소수점       4  
d              부동 소수점       8  
s              문자열             -  
<              리틀 엔디안 지정 -  
>              빅 엔디안 지정  -  
  
  
예를 들어, 정수를 1 바이트의 부호 있는 정수로 bytes 객체로 변환한다면, 형식 문자열은 *b'를 지정하여 변환하고자 하는 값과 함께 pack 함수에 전달한다.   
```
from struct import pack, unpack, calcsize

data = pack('b', 100)
print(data)
print(f"100 == b'{chr(100)}'")
```
  
문자 ''d''의 ASCII 값은 10 진수 "100"이므로 100을 pack 함수에 전달하여 bytes 객체로 변환하면 "b'd'"가 되는 것에 주의하자. 반대로 얻은 bytes 객체를 정수로 변환하려면 아래처럼 한다.  
```
result = unpack('b', data)
print(result)
```
  
결과를 보면 알 수 있듯이, 얻어진 데이터는 튜플에 포함 되는 것에 주의하자. 여기에서는 형식 문자를 문자 하나 밖에 사용하지 않았기 때문에 얻어진 값도 하나 뿐이지만, 여러 형식 문자열을 지정하면 그 수만큼 데이터가 얻어진다.  
  
  
  
## struct 모듈을 사용하여 바이너리 파일을 읽고 쓰기
struct 모듈을 사용하여 이진 파일에 데이터를 쓰고, 이것을 다시 읽어 보자.  
우선, 여기에서는 튜플을 요소로 하는 리스트를 만들어 둔다.  
```
mydata = [(1, 'FOO', 1023), (2, 'BAR', 80), (3, 'BAZ', 4000)]
```
  
요소가 되는 튜플에는 ID, 이름, 데이터가 담겨 있다. 이 때 이름의 길이가 같게 되어 있는 것에 주의하자. struct 모듈은 (문자 포함)이와 같은 정형 포맷의 데이터를 쉽게 bytes 문자열로 변환 할 수 있다. 여기에서는 이들을 다음과 같은 구성으로 bytes 객체로 대체하기로 한다.  
- ID: 4 바이트의 정수
- 이름: 3 문자열
- 데이터: 4 바이트의 정수
   
이것들을 서식 지정 문자로 표현하면 "l3sll"가된다.  "s" 앞에 있는 "3"은 이것이 3 문자로 구성된 문자열임을 의미한다.  
  
예를 들어, 목록의 첫 번째 요소를 bytes 객체로 변환한다면 다음과 같다(문자열이 포함 되어 있기 때문에, encode 메소드에서 bytes 타입으로 한번 변환해야 하고, 깔끔하게 쓸 수는 없다).  
```
result = pack('l3sl', mydata[0][0], mydata[0][1].encode(), mydata[0][2])
print(result)
```
3개의 데이터가 하나의 bytes 객체로 팩된다.  
이를테면 이런 식으로 리스트의 각 요소(튜플)를 bytes 객체로 팩하고, 이것을 write 메소드에서 이진 파일에 기록하면 된다는 것이다. 실제 코드는 다음과 같다.  
```
myfile = open('mydata.data', 'wb')

for item in mydata:
    result = pack('l3sl', item[0], item[1].encode(), item[2])
    myfile.write(result)

myfile.close()
```
  
이번에는 쓴 데이터를 읽고, 원래의 데이터로 복원 할 수 있는지 확인 해보자. 이 때 튜플 하나 당 데이터 크기가 필요하다. 이를 확인하 데 사용할 수 있는 것이, calcsize 함수이다. 이것에 서식 지정 문자를 주면 이 데이터의 크기 알 수 있다.  
```
size = calcsize('l3sl')
myfile = open('mydata.data', 'rb')

content = myfile.read(size)
while content:
    restored_data = unpack('l3sl', content)
    print(restored_data)
    content = myfile.read(size)

myfile.close()
```
  
여기에서는 calcsize 함수에서 조사한 데이터 크기마다 read 메소드에서 파일의 내용을 읽고, 이것을 unpack 함수에 전달하여 데이터를 추출하여 화면에 표시하고 있다. 이것이 끝나면 다시 데이터 크기만큼 파일에서 로드하고 파일이 비워 질 때까지 이것을 계속한다.  
  
  
  
## struct 모듈을 사용하여 GIF 파일에서 읽어들이는 코드
필요한 데이터는 다음과 같다.  
- GIF 시그니처는 "GIF" 후에 "87a" 또는 "89a"가 계속
- 그런 다음 2 바이트로 화면의 가로 폭과 세로 폭이 있다
  
이것을 서식 지정 문자로하면 "6shh"가 된다. 이것을 알면 함수는 즉시 쓸 수 있다. 실제 코드는 다음과 같다.  
```
def get_dimension_with_struct(filename):
    myfile = open(filename, 'rb')
    content = myfile.read(10)
    myfile.close()
    (spec, width, height) = unpack('6shh', content)
    print(f'this file is {spec.decode()}, size: {width} x {height}')
```
  
여기에서 첫 번째 10 바이트를 먼저 읽고, 이것을 위의 형식 문자열과 함께 unpack 함수에 전달(바이트 순서를 규정하고 있지 않기 때문에, 환경에 따라서는 잘못된 값으로 변환될도 있다).  
  
unpack 함수는 bytes 오브젝트화 된 "GIF + 사양"과 가로, 세로 폭을 튜플에 모아서 반환 하므로, 3개의 변수 spec, width, height에 대입하도록 한다. 뒤는 이것들을 화면에 표시 할 뿐이다.  
  
struct 모듈을 사용하면 이처럼 비교적 쉽게 정형 데이터를 bytes 객체로 변환하고, 이것을 이진 파일에 기록하거나 바이너리 파일에서 읽을 수 있다. 그러나 특정 포맷을 가진 데이터를 읽고 쓰려면, pickle 모듈 등보다 편리한 모듈도 준비되어 있다.   

이 글은 2020-02-19에 작성되었습니다.