비-텍스트 파일 처리하기
텍스트 처리는 프로그래머가 하는 가장 흔한 일입니다. 그러나 가끔은 날 이진 데이터도 처리할 필요가 있습니다. 이를 VBScript나 JavaScript로 처리하는 일은 아주 드뭅니다. 그래서 여기에서는 파이썬으로 다루는 법만 살펴보겠습니다.
이진 파일을 열고 닫기
텍스트 파일과 이진 파일 사이의 핵심적인 차이점은 텍스트 파일이 이진 데이터의 옥텟(octets)으로 구성된다는 것입니다. 즉, 한 바이트가 한 문자를 나타내는 바이트로 구성됩니다. 파일 끝은 특별한 바이트 패턴으로 표식이 되는데, 일반적으로 eof(end of file)라고 합니다. 이진 파일에는 임의의 이진 데이터가 들어 있으며 그리하여 파일의 끝을 식별할 특정한 값을 사용할 수 없습니다. 그래서 이런 파일을 읽으려면 다른 연산 모드가 요구됩니다. 결과적으로 파이썬으로 (또는 실제로 다른 어떤 언어이든) 이진 파일을 열 때 이진 모드로 열린다는 것을 지정해야 합니다. 그렇지 않으면 파이썬이 처음으로 eof 문자를 발견하는 곳에서 데이터가 잘릴 위험을 감수해야 합니다. 파이썬으로 이렇게 하는 방법은 'b'를 모드 매개변수에 덧붙이는 것입니다. 다음과 같이:
binfile = file("aBinaryFile.bin","rb")
텍스트 파일을 여는 것과의 유일한 차이점은 "rb"라는 모드 값입니다. 다른 모드도 역시 무엇이든 사용할 수 있습니다. 그냥 'b'를 덧붙이면 됩니다: 쓰려면 "wb"를, 덧붙이려면 "ab"를 사용합니다.
이진 파일을 닫는 일은 텍스트 파일과 다르지 않습니다. 그냥 열린 파일 객체에 close() 메쏘드를 호출하면 됩니다:
binfile.close()
파일이 이진 모드로 열렸기 때문에 파이썬에게 추가 정보를 줄 필요가 없습니다. 파이썬은 파일을 올바르게 닫는 법을 압니다.
데이터 표현과 저장법
이진 파일 안에서 데이터에 접근하는 법을 연구하기 전에 먼저 데이터가 컴퓨터에서 어떻게 표현되고 저장되는지 생각해 볼 필요가 있습니다. 모든 데이터는 일련의 이진 자리수로, 즉 비트의 연속열로 저장됩니다. 이런 비트들은 8 개나 16개로 무리지어져서 각각 바이트(bytes) 또는 워드(words)라고 불리웁니다. (4 비트로 구성된 그룹은 종종 니블(nibble)이라고 부릅니다!). 바이트는 256개의 비트 패턴 중의 하나가 되며 값이 0-255으로 주어집니다.
프로그램에서 조작할 정보인 문자열 숫자 등등은 모두 바이트 연속열로 변환되어야 합니다. 그리하여 문자열에서 사용되는 문자들은 각각 특정한 바이트 패턴이 할당되어야 합니다. 원래는 여러 인코딩(encodings)이 있었지만, 아주 흔히 사용되는 인코딩은 ASCII(정보 교환용 미국 표준 코드)입니다. 불행하게도 순수한 ASCII는 오직 128 개의 값만을 충족하며 이는 다른 언어에는 충분하지 못합니다. 유니코드(Unicode)라는 새로운 인코딩 표준이 세워졌습니다. 유니코드는 바이트 대신에 워드를 사용하여 문자를 표현합니다. 65000개가 넘는 문자를 충족합니다. 유니코드의 하위세트로서 UTF-8은 초기의 ASCII 코딩과 밀접하게 상응하는데 그리하여 유효한 ASCII 파일이라면 당연히 유효한 UTF-8 파일입니다. 물론 반대로는 언제나 그런 것은 아닙니다. 파이썬은 기본으로 ASCII를 지원하며 문자열의 앞에 u를 두면 파이썬에게 문자열을 유니코드로 취급하라고 알려줄 수 있습니다. (물론 파이썬에게 어느 인코딩을 사용중인지 알려줄 필요도 있습니다. 그렇지 않으면 파이썬은 혼란을 일으킵니다.)
같은 방식으로 숫자는 이진 코딩으로 변환할 필요가 있습니다. 작은 정수라면 바이트 값을 직접적으로 사용해도 충분하지만, 255를 넘어가는 숫자라면 (또는 음수나 분수라면) 추가로 조치가 필요합니다. 시간이 지나면서 숫치 데이터를 위하여 다양한 표준 코딩이 출현하였습니다. 대부분의 프로그래밍 언어와 운영 체제는 이런 코딩을 사용합니다. 예를 들어 IEEE(미국 전기 전자 협회)는 부동소수점수에 대하여 수 많은 코딩을 정의하였습니다.
이 모든 것의 요점은 이진 파일을 읽을 때 프로그램을 위하여 날 비트 패턴을 올바른 유형의 데이터로 번역해야 한다는 것입니다. 원래 문자열로 작성된 바이트 스트림을 부동소수점수 집합으로 번역하는 것도 가능합니다. 물론 원래 의미는 잃어 버리지만 비트 패턴은 어느 쪽이든 표현할 수 있습니다. 그래서 이진 데이터를 읽을 때 그것을 올바른 데이터 유형으로 변환하는 것이 아주 중요합니다.
Struct 모듈
이진 데이터를 인코드/디코드 하기 위해 파이썬은 structure를 약칭한 struct라는 모듈을 제공합니다. struct 모듈은 혼합 데이터를 인쇄하는데 사용한 적이 있는 형식화 문자열과 아주 비슷하게 작동합니다. 읽을 데이터를 표현하는 문자열을 바이트 스트림에 적용하여 번역을 시도합니다. struct를 이용하면 데이터 집합을 쓰기용 바이트 스트림으로 변환하거나, 이진 파일로 (심지어 통신 회선으로도!) 변환할 수 있습니다.
다양한 변환 포맷 코드가 많이 있지만 여기에서는 정수와 문자열 코드만 다루어 보겠습니다. (다른 것들은 struct 모듈에 대한 파이썬 문서를 참고하시면 됩니다.) 정수와 문자열을 위한 코드는 각각 i와 s입니다. struct 형식화 문자열은 코드의 연속열로 구성됩니다. 숫자를 앞에 두어 필요한 항목의 개수를 표시합니다. 예외는 s 코드인데 앞에 배치된 숫자가 문자열의 길이를 뜻합니다. 예를 들어 4s는 4 개의 문자를 가진 문자열을 뜻합니다 (4 개의 문자열이 아니라 4 개의 문자임에 주목하세요!).
위의 주소록 프로그램에서 주소를 이진 데이터로 자세하게 작성하고 싶다고 가정해 봅시다. 도로 번호는 정수로 하고 나머지는 문자열로 하고 싶습니다. (이는 실용적으로 나쁜 생각입니다. 왜냐하면 도로 "번호"에 종종 문자가 포함될 수 있기 때문입니다!). 형식화 문자열을 다음과 같이 보일 것입니다:
'i34s' # 주소에 34 개의 문자가 있다고 간주한다!
주소의 길이가 다른 문제에 대처하기 위해 함수를 작성하여 이진 문자열을 다음과 같이 만들면 됩니다:
def formatAddress(address):
# split 함수는 문자열을 '단어' 리스트로 분해한다.
fields = address.split()
number = int(fields[0])
rest = ' '.join(fields[1:])
format = "i%ds" % len(rest) # 형식화 문자열을 만든다.
return struct.pack(format, number, rest)
그래서 문자열 메쏘드를 - split() - (다음 주제에서 더 자세하게 배웁니다!) 사용해서 주소 문자열을 부분별로 갈랐고, 첫 부분을 번호로 추출하였습니다. 다음으로 또다른 문자열 메쏘드 join을 사용하여 나머지 필드를 다시 하나로 결합하였습니다. 문자열의 길이는 struct 형식화 문자열에서 필요한 숫자입니다. 휴!
formatAddress()는 주소의 이진 표현이 담긴 바이트 연속열을 돌려줍니다. 이제 이진 데이터를 확보하였으므로 어떻게 그것을 이진 파일로 쓸 수 있는지 그리고 다시 어떻게 읽어 들일 수 있는지 알아봅시다.
Struct 모듈을 이용하여 읽고 & 쓰기
위에서 정의한 formatAddress() 함수를 이용하여 주소가 단 한 줄 들어 있는 이진 파일을 만들어 봅시다. 파일을 읽기용으로 'wb' 모드에서 열 필요가 있습니다. 데이터를 인코드하고, 그것을 파일에 쓴 다음 파일을 닫습니다. 시험해 봅시다:
import struct
f = file('address.bin','wb')
data = "10 Some St, Anytown, 0171 234 8765"
bindata = formatAddress(data)
print "Binary data before saving: ", repr(bindata)
f.write(bindata)
f.close()
노트패드에서 address.bin을 열어보면 데이터가 실제로 이진 포맷인지 점검할 수 있습니다. 문자는 읽을 수 있겠지만 숫자는 10 진수처럼 보이지 않습니다! 사실 숫자는 사라지고 안 보입니다! 이진 파일을 읽을 수 있는 편집기가 있다면 (예, vim 또는 emacs) 그리고 그런 편집기를 사용하여 address.bin을 열어 보면 파일의 처음에 4 바이트가 보일 것입니다. 이 중에 첫 바이트는 새줄문자처럼 보일 것이고 나머지는 0입니다. 이제 알고보면 그냥 우연하게도 새줄문자의 숫치 값이 10입니다! 파이썬을 사용하여 그를 보여줄 수 있습니다:
>>> ord('\n')
10
ord 함수는 그냥 주어진 문자의 ASCII 값을 돌려줍니다. 그래서 첫 4 바이트는 십진수로 10,0,0,0입니다. (또는 십육진수로 0xA,0x0,0x0,0x0입니다).
32 비트 컴퓨터에서 정수는 4 바이트를 차지합니다. 그래서 정수 값 '10'은 struct 모듈에 의하여 4 바이트 이진 연속열 10,0,0,0으로 변환됩니다. 이제 인텔 마이크로-프로세서에서 바이트 연속열은 LSB(least significant byte)를 먼저 배정하므로, 거꾸로 읽어서 진짜 "이진" 값을 돌려줍니다: 0,0,0,10.
이는 정수 값 10이 4 개의 십진 바이트로 표현된 것입니다. 나머지 데이터는 기본적으로 원래의 텍스트 문자열이며 그래서 그의 보통 문자 형식으로 나타납니다.
노트패드 안에서 그 파일을 저장하지 않도록 주의하세요. 왜냐하면 노트패드가 이진 파일을 적재할 수는 있지만 이진 파일로 저장하지는 못하기 때문입니다. 노트패드는 이진 데이터를 텍스트로 바꾸려고 시도할 것이고 그 과정에서 데이터가 부패할 수 있습니다! 여기에서 파일 확장자 .bin이 순수하게 편의를 위한 것일 뿐이라는 사실을 지적할 가치가 있습니다. 파일이 이진 형식인지 텍스트 형식인지 전혀 아무 함의도 없습니다. 어떤 운영체제는 파일을 열기 위해 확장자를 이용하여 어떤 프로그램을 사용할지 결정합니다. 그러나 그냥 파일의 이름을 바꾸면 확장자를 바꿀 수 있습니다. 내용은 바뀌지 않습니다. 원래 무엇이었든지 텍스트이든 이진이든 여전히 그대로일 것입니다. (이를 증명할 수 있습니다. 윈도우즈에서 텍스트 파일의 이름을 .exe로 바꾸면 윈도우즈는 그 파일을 실행파일로 취급하지만, 실행하려고 하면 텍스트가 실제로는 실행 이진 코드가 아니기 때문에 에러가 일어납니다! 이제 다시 이름을 .txt로 바꾸면 그 파일은 노트패드에서 정확하게 예전과 똑 같이 열릴 것이고, 그 내용은 전혀 변하지 않습니다. - 실제로 파일이 .exe로 이름이 바뀌어 있는 동안에도 노트패드에서 텍스트를 열 수도 있었습니다. 그래도 여전히 잘 작동할 것입니다!)
이진 데이터를 다시 읽어 들이려면 파일을 'rb' 모드로 열고, 데이터를 바이트 연속열로 읽어 들이고, 그 파일을 닫고 마지막으로 struct 형식화 문자열을 이용하여 데이터를 언팩할 필요가 있습니다. 문제는: 그 형식화 문자열의 모습을 어떻게 구별할까? 일반적으로 이진 포맷은 그의 파일 정의로부터 찾을 필요가 있습니다. (이런 정보를 제공하는 여러 웹 사이트가 있습니다 - 예를 들어 어도비(Adobe)사는 자시의 공통 PDF 이진 포맷을 공개합니다). 이 경우 포맷이 반드시 formatAddress()에서 만든 것과 같으며, 즉 'iNs'라는 것을 알고 있습니다. 여기에서 N은 변수 번호입니다. 어떻게 N의 값을 결정하는가?
struct 모듈은 각 데이터 유형의 크기를 돌려주는 도움자 함수를 제공합니다. 그래서 파이썬 프롬프트를 기동하고 실험해 보면 각 데이터 유형에 대하여 얼마나 많은 바이트를 돌려받는지 알 수 있습니다:
>>> import struct
>>> print struct.calcsize('i')
4
>>> print struct.calcsize('s')
1
좋습니다. 데이터가 숫자에 대하여 4 바이트로 구성되고 문자에 대하여 1 바이트이군요. 그래서 N은 데이터의 길이에 4를 뺀 값입니다. 그를 이용하여 파일을 읽어 봅시다: 이진 데이터 파일에 관한 언급은 이 정도면 충분합니다. 적어도 이 주제에 관하여 할 수 있는 만큼은 다 언급하였습니다. 보시다시피 이진 데이터를 사용하려면 여러 복잡한 일들이 관련됩니다. 아주 좋은 이유가 없는 한 권장하지 않습니다. 그러나 적어도 이진 파일을 읽을 필요가 있다면, 그렇게 할 수 있습니다 (물론 무엇보다도 그 데이터가 무엇을 표현하는지 알고 있다면 말입니다!)
import struct
f = file('address.bin','rb')
data = f.read()
f.close()
fmtString = "i%ds" % (len(data) - 4)
number, rest = struct.unpack(fmtString, data)
address = ' '.join((str(number),rest))
print "Address after restoring data:", address
파일에 무작위로 접근하는 법
파일 처리에서 살펴볼 마지막 측면은 무작위 접근입니다. 이는 그 사이에 있는 데이터를 전혀 읽지 않고서 파일의 특정 부분으로 직접 이동하는 것을 의미합니다. 어떤 프로그래밍 언어는 이를 아주 빠르게 처리해 줄 특별히 인덱스된 파일 유형을 제공합니다. 그러나 대부분의 언어는 보통의 순차 파일 접근법 위에 구축되어 있습니다. 이런 접근법은 지금까지도 사용 중입니다.
커서의 개념이 사용됩니다. 커서는 파일 안에서의 현재 위치를 표식합니다. 문자그대로 파일의 처음에서 얼마나 많은 바이트가 떨어져 있는지 나타냅니다. 이 커서를 현재 위치에 상대적으로 또는 파일의 처음에 상대적으로 이동시킬 수 있습니다. 또한 파일에게 커서가 현재 어디에 위치하고 있는 알려주도록(tell) 명령할 수 있습니다.
줄길이를 고정시키면 (필요하면 데이터 문자열에 공간문자나 기타 문자를 추가해서) 특정 줄의 처음으로 점프할 수 있습니다. 줄의 개수에 줄 길이를 곱하면 됩니다. 이 때문에 파일에 있는 데이터에 무작위로 접근하는 듯한 인상을 줍니다.
어디에 있는가?
파일에서 어디에 있는지 결정하기 위해 파일의 tell() 메쏘드를 사용할 수 있습니다. 예를 들어 파일을 열고 세 줄을 읽어 들인다면, 파일에게 현재 위치가 처음보다 얼마나 떨어져 있는지 물어볼 수 있습니다.
예제를 하나 살펴봅시다. 먼저 길이는 모두 같고 5 줄의 텍스트를 가진 파일을 만들겠습니다. (꼭 길이가 같아야할 필요는 없지만 그래야 좀 더 삶이 편안해집니다!). 다음 세 줄을 읽고 어디에 있는지 물어보겠습니다. 다음으로 처음으로 되돌아가, 한 줄을 읽고 세 번째 줄로 점프한 다음 그 줄을 인쇄하고, 두 번째 줄로 점프하겠습니다. 다음과 같이:
# 20 개의 문자로 구성된 5 줄을 만든다 (+ \n)
testfile = open('testfile.txt','w')
for i in range(5):
testfile.write(str(i) * 20 + '\n')
testfile.close()
# 3 줄을 읽고 어디에 위치하고 있는지 물어본다.
testfile = open('testfile.txt','r')
for line in range(4):
print testfile.readline().strip()
position = testfile.tell()
print "At position: ", position, "bytes"
# 다시 처음으로 돌아간다
testfile.seek(0)
print testfile.readline().strip()
testfile.seek(2*22)
print testfile.readline().strip()
testfile.close()
seek() 함수를 사용하여 커서를 이동시킨 것에 주목하세요. 기본 연산은 여기에서 보여준 것처럼 커서를 지정된 바이트 개수만큼 이동시키는 것이지만, 추가로 인자를 제공하면 인덱스 메쏘드의 행위를 바꿀 수 있습니다. 또한 첫 tell() 함수에 의하여 인쇄된 값이 플랫폼의 새줄문자의 길이에 따라 다르다는 것을 주목하세요. 본인의 윈도우즈 XP PC에서는 69가 인쇄되었는데 새줄문자 연속열이 길이가 3바이트라는 것을 나타냅니다. 그러나 이는 플랫폼마다 다른 값이므로 그리고 코드가 이식성이 있기를 원하므로 한 줄을 읽고 난후 다시 tell()을 사용하여 각 줄이 실제로 얼마나 긴지 알아냅니다. 플랫폼 종속적 문제를 다룰 때 이런 종류의 "책략(cunning ploys)"이 종종 필요합니다!
댓글