CODICT

6. Python으로 데이터 다루기(상) 본문

Programming/Python

6. Python으로 데이터 다루기(상)

Foxism 2020. 2. 29. 02:37

이번 게시글에서는 데이터에 대한 기술을 살펴봅니다. 내용의 대부분은 파이썬에 내장된 다음 데이터 타입입니다.

  • 문자열

    • 텍스트 데이터에 사용되는 유니코드(Unicode) 문자의 시퀀스

  • 바이트와 바이트 배열

    • 이진 데이터에 사용되는 8비트 정수의 시퀀스

 

1. 텍스트 문자열

 

텍스트는 대부분의 파이썬 프로그래머에게 가장 친숙한 데이터 타입일 것입니다. 파이썬 텍스트 문자열의 강력한 특징을 살펴보겠습니다.

 

1.1. 유니코드

 

지금까지 이 책의 모든 텍스트 예제는 평범하고 오래된 아스키(ASCII) 문자를 사용했습니다. 아스키코드는 1960년대에 정의되었습니다. 그때의 냉장고만한 컴퓨터는 단순한 계산을 잘 수행했습니다. 컴퓨터의 기본 저장 단위는 바이트(Byte)입니다. 바이틑 8비트에 256개의 고유한 값을 저장할 수 있습니다.

여러 가지 이유로 아스키코드는 7비트(128개의 고유한 값)만 사용합니다. 각 26개의 대/소문자, 10개의 숫자, 구두 문자, 공백 문자, 비인쇄 제어 코드(Nonprinting Control Code)로 구성되어 있습니다.

세상에는 아스키코드가 제공하는 것보다 더 많은 문자가 있습니다. 예를 들어 프랑스 식당에 독일의 핫도그는 있지만, 프랑스 카페에 옴라우트(ü)가 붙은 게뷔르츠트라미너(Gewürztraminer; 독일어는 움라우트가 있지만, 프랑스어는 움라우트가 없습니다.) 와인은 없습니다. 파이썬은 많은 문자와 기호를 지원하기 위해 많은 노력을 했습니다. 이 책에서 가끔 특이한 문자와 기호를 보게 될 것입니다. 다음은 그 중 일부 문자 코드입니다.

  • Latin-1 혹은 ISO 8859-1
  • Windows code page 1252

이들 각 문자는 모두 8비트를 사용합니다. 하지만 비유럽 국가의 언어는 8비트로 모든 문자를 표현할 수 없습니다. 유니코드(Unicode)는 전 세계 언어의 문자를 정의하기 위한 국제 표준 코드입니다. 유니코드는 수학 및 기타 분야의 기호들도 포함하고 있습니다.

유니코드는 플랫폼, 프로그램, 언어에 상관없이 문자마다 고유한 코드값을 제공한다.
-유니코드 협회(The Unicode Consortium)

유니코드 코드 차트(Unicode Code Chart) 페이지는 현재 정의된 모든 문자 집합과 이미지에 대한 링크를 제공합니다. 가장 최신 버전(7.0)은 113,021개의 문자를 정의합니다. 각각의 문자는 고유한 이름과 식별 번호가 있습니다. 문자들은 유니코드 평면(Plane)이라고 하는 8비트의 세트로 분할됩니다. 첫 번째 256 평면은 기본 다국어 평면입니다. 자세한 내용은 위키피디아 페이지의 유니코드 평면을 참고하세요.

 

파이썬 3 유니코드 문자열

파이썬 3 문자열은 바이트 배열이 아닌 유니코드 문자열입니다. 파이썬 3의 유니코드 문자열은 파이썬 2로부터의 가장 큰 변화입니다. 파이썬 3은 일반적인 바이트 문자열과 유니코드 문자를 구별합니다.

유니코드 식별자(ID) 혹은 문자에 대한 이름(Name)을 안다면, 이 문자를 파이썬 문자열에 사용할 수 있습니다. 여기 그 예시가 있습니다

  • 4자리 16진수와 그 앞에 \u는 유니코드의 기본 평면 256개 중 하나의 문자를 지정합니다. 첫 번째 두 숫자는 평면 번호입니다(00에서 FF까지). 다음의 두 숫자는 평면에 있는 문자의 인덱스입니다. 평면 00는 아스키코드고, 평면 안의 문자 위치는 아스키코드의 번호와 같습니다.

  • 높은 평면의 문자일수록 비트수가 더 필요합니다. 이에 대한 파이썬의 이스케이프 시퀀스(Escape Sequence)는 \U고, 8자리의 16진수를 사용합니다. 숫자에 남는 공간이 있다면 왼쪽에 0을 채웁니다.

  • 모든 문자는 표준 이름 \N{name}으로 지정할 수 있습니다. 유니코드 문자 이름 인덱스(The Unicode Character Name Index) 페이지에서 표준 이름 리스트를 참조합니다.

파이썬의 unicodedata 모듈은 유니코드 식별자와 이름으로 검색할 수 있는 함수를 제공합니다.

  • lookup()

    • 대/소문자를 구분하지 않는 인자를 취하고, 유니코드 문자를 반환합니다.

  • name()

    • 인자로 유니코드 문자를 취하고, 대문자 이름을 반환합니다.

파이썬 유니코드 문자를 취하는 테스트 함수를 작성해봅시다. 이름을 검색하고, 그 이름으로부터 유니코드 문자를 다시 검색합니다(원래 문자와 일치해야 합니다).

>>> def unicode_test(value):
	import unicodedata
	name = unicodedata.name(value)
	value2 = unicodedata.lookup(name)
	print('value = "%s", name = "%s", value2 = "%s"' % (value, name, value2))

먼저 아스키 문자를 넣어봅시다.

>>> unicode_test('A')
#value = "A", name = "LATIN CAPITAL LETTER A", value2 = "A"

아스키 문자 부호를 넣어봅시다.

>>> unicode_test('$')
#value = "$", name = "DOLLAR SIGN", value2 = "$"

유니코드 통화 문자를 넣어봅시다.

>>> unicode_test('\u00a2')
#value = "¢", name = "CENT SIGN", value2 = "¢"

다른 유니코드 통화 문자를 넣어봅시다.

>>> unicode_test('\u20ac')
#value = "€", name = "EURO SIGN", value2 = "€"

한 가지 문제가 있다면, 텍스트를 표시하는 데 사용하는 글꼴에 한계가 있습니다. 모든 글꼴은 모든 유니코드에 대한 이미지를 가지고 있지 않으며, 이것을 일부 플레이스홀더(Placeholder) 문자로 표시할 것입니다. 예를 들어 딩벳 글꼴(Dingbat Font)의 SNOWMAN에 대한 유니코드 기호는 다음과 같습니다.

>>> unicode_test('\u2603')
#value = "☃", name = "SNOWMAN", value2 = "☃"

문자열에 단어 café를 저장한다고 하자. 한 가지 방법은 파일 혹은 웹사이트에서 이 단어를 찾아서 복사/붙여넣기하고, 잘 저장되길 기도하는 것입니다.

>>> place = 'café'
>>> place
#'café'

필자가 UTF_8 인코딩(다음 절에서 볼 것입니다) 형식의 소스를 복사/붙여넣기했기 때문에 제대로 작동했습니다.

마지막 é 문자는 어떻게 지정할 수 있을가요? E에 대한 문자 인덱스를 찾으면, E WITH ACUTE, LATIN SMALL LETTER 이름은 00E9 값을 가집니다. name()과 lookup() 함수로 확인해봅시다. 먼저 유니코드로 문자 이름을 얻습니다.

>>> unicodedata.name('\u00e9')
#'LATIN SMALL LETTER E WITH ACUTE'

다음에는 이름으로 코드를 얻습니다.

>>> unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
#Traceback (most recent call last):
#  File "<pyshell#346>", line 1, in <module>
#    unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
#KeyError: "undefined character name 'E WITH ACUTE, LATIN SMALL LETTER'"

참고로, 유니코드 문자 이름 인덱스 페이지(Unicode Character Name Index)에 등록된 문자 이름은 잘 정리되어 있습니다. 이들을 파이썬이 사용하는 실제 유니코드 이름으로 변환하려면 콤마를 지우고, 콤마 이후에 나오는 이름을 앞으로 옮겨야 합니다. 따라서 E WITH ACUTE. LATIN SMALL LETTER는 LATIN SMALL LETTER E WITH ACUTE로 바궈야 합니다.

>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
#'é'

이제 코드와 이름으로 문자열 café를 저장할 수 있습니다.

>>> place = 'caf\u00e9'
>>> place
#'café'
>>> place = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> place
#'café'

이전 코드에서는 é를 직접 삽입했습니다. 그리고 다음과 같이 문자열에 추가하는 것도 가능합니다.

>>> u_umlaut = '\N{LATIN SMALL LETTER U WITH DIAERESIS}'
>>> u_umlaut
#'ü'
>>> drink = 'Gew' + u_umlaut + 'rztraminer'
>>> print('Now I can finally have my', drink, 'in a', place)
#Now I can finally have my Gewürztraminer in a café

문자열 len 함수는 유니코드의 바이트가 아닌 문자수를 셉니다.

>>> len('$')
#1
>>> len('\U0001f47b')
#1

 

UTF-8 인코딩과 디코딩

파이썬에서 일반 문자열을 처리할 때는 각 유니코드 문자를 저장하는 방법에 대해 걱정하지 않아도 됩니다.

그러나 외부 데이터를 교환할 때는 다음 과정이 필요합니다.

  • 문자열을 바이트로 인코딩
  • 바이트를 문자열로 디코딩

유니코드에 64,000 미만의 문자가 있다면 2바이트로 된 각 유니코드 문자의 식별자를 저장할 수 있습니다. 그러나 불행하게도 문자가 더 많습니다. 3 또는 4바이트의 식별자를 인코딩할 수 있지만, 텍스트 문자열에 대한 메모리 및 디스크 저장 공간이 3 ~ 4배 증가합니다. 그래서 유닉스 개발자 켄 톰슨(Ken Thompson)과 롭 파이크(Rob Pike)는 뉴저지 식당의 식탁에서 UTF-8 동적 인코딩 형식을 하룻밤에 설계했습니다. 유니코드 한 문자당 1 ~ 4 바이트를 사용합니다.

  • 1바이트: 아스키코드
  • 2바이트: 키릴(Cyrillic) 문자를 제외한 대부분의 파생된 라틴어
  • 3바이트: 기본 다국어 평면의 나머지
  • 4바이트: 아시아 언어 및 기호를 포함한 나머지

UTF-8은 파이썬, 리눅스, HTML의 표준 텍스트 인코딩입니다. UTF-8은 빠르고 완전하고 잘 동작합니다. 코드에 UTF-8 인코딩 방식을 사용하면 다양한 인코딩 방식을 시도하는 것보다 인생이 한결 편해질 것입니다.

참고로, 웹 페이지와 같은 다른 소스로부터 복사/붙여넣기하여 문자열을 작성하는 경우, 소스가 UTF-8 형식으로 인코딩되었는지 확인해야 합니다. 일반적으로 문자열을 복사할 때 Latin-1 혹은 Windows 1252 형식으로 인코딩된 텍스트를 쉽게 볼 수 있습니다. 이들은 나중에 바이트 시퀀스가 유효하지 않다는 예외를 발생시킵니다.

 

인코딩

문자열을 바이트로 인코딩해봅시다. 문자열 encode() 함수의 첫 번째 인자는 인코딩 이름입니다. 다음의 표에서 선택할 수 있습니다.

'ascii' 7비트의 아스키코드
'utf-8' 8비트 가변 길이 인코딩 형식, 거의 대부분의 문자 지원
'latin-1' ISO8859-1로도 알려짐
'cp-1252' 윈도우 인코딩 형식
'unicode-escape' 파이썬 유니코드 리터럴 형식, \uxxxx 또는 \Uxxxxxxxx

어떤 것도 UTF-8로 인코딩할 수 있습니다. 유니코드 문자 '\u2603'을 snowman에 할당해봅시다.

>>> snowman = '\u2603'

snowman은 한 문자의 파이썬 유니코드 문자열입니다. 내부적으로 몇 바이트가 저장되어야 하는지 신경 쓸 필요가 없습니다.

>>> len(snowman)
#1

유니코드 문자를 바이트 시퀀스로 인코딩해봅시다.

>>> ds = snowman.encode('utf-8')

앞서 언급햇듯이 UTF-8은 가변 길이 인코딩입니다. 이 경우 snowman 유니코드 문자를 인코딩하기 위해 3바이트를 사용합니다.

>>> len(ds)
#3
>>> ds
#b'\xe2\x98\x83'

ds는 바이트 변수기 때문에 len()은 숫자 3을 반환합니다.

UTF-8 이외의 다른 인코딩도 사용할 수 있습니다. 하지만 유니코드 문자열을 인코딩할 수 없다면 에러를 얻게 됩니다. 예를 들어 아스키 인코딩을 사용할 때, 유니코드 문자가 유효한 아스키 문자가 아닌 경우 실패합니다.

>>> ds = snowman.encode('ascii')
#Traceback (most recent call last):
#  File "<pyshell#364>", line 1, in <module>
#    ds = snowman.encode('ascii')
#UnicodeEncodeError: 'ascii' codec can't encode character '\u2603' in position 0: ordinal not in range(128)

encode() 함수는 인코딩 예외를 피하기 위해 두 번째 인자를 취합니다. 이전 예제에서는 두 번째 인자를 지정하지 않았기 때문에 기본값인 'strict'가 저장되었습니다. 이는 아스키코드가 아닌 문자가 나타났을 때 UnicodeEncodeError를 발생시킵니다. 'ignore'를 사용하여 알 수 없는 문자를 인코딩하지 않도록 해봅시다.

>>> snowman.encode('ascii', 'ignore')
#b''

'replace'는 알 수 없는 문자를 ?로 대체합니다.

>>> snowman.encode('ascii', 'replace')
#b'?'

'backslashreplace'는 유니코드 이스케이프(Unicode-Escape)처럼 파이썬 유니코드 문자의 문자열을 만듭니다.

>>> snowman.encode('ascii', 'backslashreplace')
#b'\\u2603'

'xmlcharrefreplace'는 유니코드 이스케이프 시퀀스를 출력할 수 있는 문자열로 만듭니다.

>>> snowman.encode('ascii', 'xmlcharrefreplace')
#b'&#9731;'

 

디코딩

바이트 문자열을 유니코드 문자열로 디코딩해봅시다. 외부 소스(파일, 데이터베이스, 웹사이트, 네트워크 API 등)에서 텍스트를 얻을 때마다 그것은 바이트 문자열로 인코딩되어 있습니다. 이 소스에서 실제로 사용된 인코딩을 알기 위해, 인코딩 과정을 거꾸로 하여 유니코드 문자열을 얻을 수 있습니다.

문제는 바이트 문자열이 어떻게 인코딩되엇는지 말해주지 않는다는 것입니다. 앞서 웹사이트의 텍스트를 복사/붙여넣기했을 때 일어날 수 있는 위험 상황을 설명했습니다. 예를 들어 웹사이트 텍스트의 문자를 아스키 문자로 예상했는데, 이상한 다른 문자로 되어 있는 경우입니다.

'café'의 유니코드 문자열을 생성해봅시다.

>>> place = 'caf\u00e9'
>>> place
#'café'
>>> type(place)
#<class 'str'>

이것을 UTF-8 형식의 place_bytes라는 바이트 변수로 인코딩합니다.

>>> place_bytes = place.encode('utf-8')
>>> place_bytes
#b'caf\xc3\xa9'
>>> type(place_bytes)
#<class 'bytes'>

place_bytes는 5바이트로 되어 있습니다. 첫 3바이트는 UTF-8과 똑같이 표현되는 아스키문자입니다. 그리고 마지막 2바이트에서 'é'를 인코딩했습니다. 이제 바이트 문자열을 유니코드 문자열로 디코딩해봅시다.

>>> place2 = place_bytes.decode('utf-8')
>>> place2
#'café'

'café'를 UTF-8로 인코딩한 후, UTF-8을 인코딩했습니다. 다른 인코딩 방식으로 디코딩하면 무슨 일이 일어날까요?

>>> place3 = place_bytes.decode('ascii')
#Traceback (most recent call last):
#  File "<pyshell#15>", line 1, in <module>
#    place3 = place_bytes.decode('ascii')
#UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)

아스키 디코더는 예외를 던집니다. 0xc3바이트 값은 아스키코드에 유효하지 않기 때문입니다. 아스키코드는 128(16진수 80)에서 255(16진수 FF) 사이에 있는 값의 일부 8비트 문자 셋의 인코딩이 유효하지만, UTF-8과는 다릅니다.

>>> place4 = place_bytes.decode('latin-1')
>>> place4
#'café'
>>> place5 = place_bytes.decode('windows-1252')
>>> place5
#'café'

ㅓㅜㅑ;;;

이 코드의 교훈은 가능하면 UTF-8을 사용하라는 것입니다. UTF-8은 모든 유니코드 문자를 표현할 수 있고, 어디에서나 지원합니다. 그리고 빠르게 디코딩과 인코딩을 수행합니다.

 

참고 사이트

유니코드에 대해 좀 더 알고 싶다면 다음 링크를 참고하세요.

 

1.2. 포맷

 

지금까지는 텍스트 포매팅을 거의 사용하지 않았습니다. 2장에서는 문자열을 정렬(Alignment)하는 함수와 간단한 print() 문을 사용하거나 인터프리터가 값을 표시하도록 놔뒀습니다. 이번 절에서는 데이터값을 문자열에 끼워 넣는(Interpolate) 방법을 배웁니다. 다시 말해 다양한 포맷(Format)을 사용하여 문자열 안에 값을 넣습니다. 포맷은 보고서와 그 외에 정리된 출력물을 만들기 위해 사용합니다.

파이썬에서는 옛 스타일새로운 스타일의 포매팅 문자열이 있습니다. 두 스타일은 파이썬 2와 3에서 지원합니다(새로운 스타일은 파이썬 2.6 이상에서 지원). 간단한 옛 스타일에서 시작해봅시다.

 

옛 스타일: %

문자열 포매팅의 옛 스타일은 string % data 형식입니다. 문자열(String) 안에 끼워 넣을 데이터(Data)를 표시하는 형식은 보간 시퀀스(Interpolation Sequence)입니다. 다음의 표는 %와 데이터 타입을 나타내는 아주 간단한 보간 시퀀스를 보여줍니다.

%s

문자열

%d 10진 정수
%x 16진 정수
%o 8진 정수
%f 10진 부동소수점수
%e 지수로 나타낸 부동소수점수
%g 10진 부동소수점수 혹은 지수로 나타낸 부동소수점수
%% 리터럴 %

다음은 정수에 대한 간단한 코드입니다.

>>> '%s' % 42
#'42'
>>> '%d' % 42
#'42'
>>> '%x' % 42
#'2a'
>>> '%o' % 42
#'52'

다음은 부동소수점수에 대한 간단한 코드입니다.

>>> '%s' % 7.03
#'7.03'
>>> '%f' % 7.03
#'7.030000'
>>> '%e' % 7.03
#'7.030000e+00'
>>> '%g' % 7.03
#'7.03'

다음은 정수와 리터럴 %에 대한 간단한 코드입니다.

>>> '%d%%' % 100
#'100%'

다음은 정수와 문자열에 대한 간단한 코드입니다.

>>> actor = 'Richard Gere'
>>> cat = 'Chester'
>>> weight = 28
>>> "My wife's favorite actor is %s" % actor
#"My wife's favorite actor is Richard Gere"
>>> "Our cat %s weighs %s pounds" % (cat, weight)
#'Our cat Chester weighs 28 pounds'

문자열 내의 %s는 다른 문자열을 끼워 넣는 것을 의미합니다. 문자열 안의 % 수는 % 뒤의 데이터 항목의 수와 일치해야 합니다. actor와 같은 단일 데이터 항목은 % 바로 뒤에 입력합니다. 여러 데이터 항목은 (cat, weight)와 같이 튜플로 묶어야 합니다.

심지어 weight는 정수인데도 불구하고, 문자열 안의 %s는 그것을 문자열로 변환합니다.

최소 및 최대 길이 조절과 정렬 및 문자를 채우기 위해 $와 타입 지정자 사이에 다른 값을 추가할 수 있다.

변수에 정수 n, 부동소수점수 f, 문자열 s를 정의해봅시다.

>>> n = 42
>>> f =7.03
>>> s = 'string cheese'

먼저 기본 포맷으로 출력해봅시다.

>>> '%d %f %s' % (n, f, s)
#'42 7.030000 string cheese'

각 변수에 최소 10자의 필드를 설정하고, 오른쪽으로 정렬합니다. 사용하지 않는 왼쪽 공간을 공백으로 채웁니다.

>>> '%10d %10f %10s' % (n, f, s)
#'        42   7.030000 string cheese'

각 필드 길이는 같게 지정하고, 왼쪽으로 정렬합니다.

>>> '%-10d %-10f %-10s' % (n, f, s)
#'42         7.030000   string cheese'

이번에도 각 필드 길이를 같게 지정합니다. 하지만 최대 문자 길이기 4며, 오른쪽으로 정렬합니다. 이 설정은 문자열을 잘라내고, 소수점 이후의 숫자 길이를 4로 제한합니다.

>>> '%10.4d %10.4f %10.4s' % (n, f, s)
#'      0042     7.0300       stri'

이전 예제를 오른쪽으로 정렬합니다.

>>> '%.4d %.4f %.4s' % (n, f, s)
#'0042 7.0300 stri'

하드코딩하지 않고, 인자로 필드 길이를 지정합니다.

>>> '%*.*d %*.*f %*.*s' % (10, 4, n, 10, 4, f, 10, 4, s)
#'      0042     7.0300       stri'

 

새로운 스타일의 포매팅: {}와 format

옛 스타일의 포매팅은 아직도 지원됩니다. 버전 2.7까지만 지원될 예정인 파이썬 2는 옛 스타일의 포매팅을 계속 지원할 것입니다. 만약 파이선 3을 사용한다면 새로운 스타일의 포매팅을 사용하는 것을 추천합니다.

다음은 간단한 코드입니다.

>>> '{} {} {}'.format(n, f, s)
#'42 7.03 string cheese'

옛 스타일의 인자는 문자열에서 %가 나타난 순서대로 데이터를 제공합니다. 새로운 스타일에서는 아래와 같이 순서를 지정할 수 있습니다. 

>>> '{2} {0} {1}'.format(f, s, n)
#'42 7.03 string cheese'

0은 첫 번째 인자인 부동소수점수  f, 1은 문자열 s, 2는 마지막 인자인 정수 n을 참조합니다.

인자는 딕셔너리 혹은 이름을 지정한 인자가 될 수 있습니다. 그리고 지정자는 그들의 이름을 포함할 수 있습니다.

>>> '{n} {f} {s}'.format(n = 42, f = 7.03, s = 'string cheese')
#'42 7.03 string cheese'

다음과 같이 세 값을 딕셔너리에 넣어봅시다.

>>> d = {'n': 42, 'f': 7.03, 's': 'string cheese'}

다음 코드에서 {0}은 딕셔너리 전체인 반면 {1}은 딕셔너리 다음에 오는 문자열 'other'입니다.

>>> '{0[n]} {0[f]} {0[s]} {1}'.format(d, 'other')
#'42 7.03 string cheese other'

이 코드들은 모두 기본 포맷을 사용하여 인자를 출력했습니다. 옛 스타일은 문자열 내의 % 다음에 타입 지정자(Type Specifier)를 입력합니다. 그러나 새로운 스타일에서는 : 다음에 타입 지정자를 입력합니다. 먼저 위치 인자를 살펴봅시다.

>>> '{0:d} {1:f} {2:s}'.format(n, f, s)
#'42 7.030000 string cheese'

이번에는 같은 값을 사용하지만, 인자에 이름을 지정합니다.

>>> '{n:d} {f:f} {s:s}'.format(n = 42, f = 7.03, s = 'string cheese')
#'42 7.030000 string cheese'

다른 옵션(최소 필드 길이, 최대 문자 길이, 정렬 등) 또한 지원됩니다.

다음 코드는 최소 필드 길이가 10이고, 오른쪽으로 정렬(기본값)한 것입니다.

>>> '{0:10d} {1:10f} {2:10s}'.format(n, f, s)
#'        42   7.030000 string cheese'

이전 코드와 같으나, > 기호는 오른쪽 정렬에 대해 더 명확하게 해줍니다.

>>> '{0:>10d} {1:>10f} {2:>10s}'.format(n, f, s)
#'        42   7.030000 string cheese'

최소 필드 길이가 10이고, 왼쪽 정렬해봅시다.

>>> '{0:<10d} {1:<10f} {2:<10s}'.format(n, f, s)
#'42         7.030000   string cheese'

최소 필드 길이가 10이고, 중앙 정렬해봅시다.

>>> '{0:^10d} {1:^10f} {2:^10s}'.format(n, f, s)
#'    42      7.030000  string cheese'

(소수점 이후의) 정밀(Precision)값은 옛 스타일과 같이 소수부 숫자의 자릿수와 문자열의 최대 문자수를 의미합니다. 하지만 새로운 스타일에서는 이것을 정수에 사용할 수 없습니다.

>>> '{0:>10.4d} {1:>10.4f} {2:10.4s}'.format(n, f, s)
#Traceback (most recent call last):
#  File "<pyshell#55>", line 1, in <module>
#    '{0:>10.4d} {1:>10.4f} {2:10.4s}'.format(n, f, s)
#ValueError: Precision not allowed in integer format specifier
>>> '{0:>10d} {1:>10.4f} {2:10.4s}'.format(n, f, s)
#'        42     7.0300 stri 

마지막으로 문자를 채워 넣어 봅시다. : 이후에, 그리고 정렬(<, >, ^) 혹은 길이 지정자 이전에 채워 넣고 싶은 문자를 입력하면 됩니다.

>>> '{0:!^20s}'.format('BIG SALE')
#'!!!!!!BIG SALE!!!!!!'

 

1.3. 정규표현식

 

이전 게시글에서 간단한 문자열 연산을 다뤘습니다. 그리고 커맨드 라인의 파일 항목에서 파일 이름이 .py 로 끝나는 *.py와 같은 와일드카드(Wildcard) 패턴을 한번쯤 써봤을 것입니다.

복잡한 패턴 매칭의 정규표현식(Regular Expression)을 사용할 때가 왔습니다. 정규표현식은 임포트할 수 있는 표준 모듈 re로 제공합니다. 원하는 문자열 패턴을 정의하여 소스 문자열과 일치하는지 비교합니다. 간단한 코드를 살펴봅시다.

>>> result = re.match('You', 'Young Frankenstein')

'You'는 패턴이고, 'Young Frankenstein'은 확인하고자 하는 문자열 소스입니다. .match()는 소스패턴의 일치 여부를 확인합니다.

좀 더 복잡한 방법으로, 나중에 패턴 확인을 빠르게 하기 위해 패턴을 먼저 컴파일할 수 있습니다.

>>> youpattern = re.compile('You')

그러고 나서 컴파일된 패턴으로 패턴의 일치 여부를 확인할 수 있습니다.

>>> result = youpattern.match('Young Frankenstein')

match()가 패턴과 소스를 비교하는 유일한 방법은 아닙니다. 다른 메소드를 살펴봅시다.

  • search()는 첫 번째 일치하는 객체를 반환합니다.
  • findall()은 중첩에 상관없이 모두 일치하는 문자열 리스트를 반환합니다.
  • split()은 패턴에 맞게 소스를 쪼갠 후 문자열 조각의 리스트를 반환합니다.
  • sub()는 대체 인자를 하나 더 받아서, 패턴과 일치하는 모든 소스 부분을 대체 인자로 변경합니다.

 

시작부터 일치하는 패턴 찾기: match()

'Young Frankenstein' 문자열은 'You' 단어로 시작하는가? 여기에 그에 대한 코드와 코멘트가 있습니다.

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('You', source) # match는 소스의 시작부터 패턴이 일치하는지 확인합니다.
>>> if m: # match는 객체를 반환합니다. 무엇이 일치하는지 보기 위해 다음 작업을 수행합니다.
	print(m.group())

	
#You
>>> m = re.match('^You', source) # 문자열이 You로 시작하는지 확인합니다.
>>> if m:
	print(m.group())

	
#You

'Frank'는 어떨까요?

>>> m = re.match('Frank', source)
>>> if m:
	print(m.group())

이번에는 match()가 아무것도 반환하지 않아서 print 문이 실행되지 않았습니다. 이전에도 언급했듯이 match()는 패턴이 소스의 처음에 있는 경우에만 작동합니다. 반면에 search()는 패턴이 아무데나 있어도 작동합니다.

>>> m = re.search('Frank', source)
>>> if m:
	print(m.group())

	
#Frank

패턴을 바꿔봅시다.

>>> m = re.match('.*Frank', source)
>>> if m:
	print(m.group())

	
#Young Frank

다음은 바뀐 패턴에 대한 간단한 설명입니다.

  • .은 한 문자를 의미합니다.
  • *는 이전 패턴이 여러 개 올 수 있다는 것을 의미합니다. 그러므로 .*는 0회 이상의 문자가 올 수 있다는 것을 의미합니다.
  • Frank는 포함되어야 할 문구를 의미합니다.

match()는 .*Frank와 일치하는 문자열 'Young Frank'를 반환합니다.

 

첫 번째 일치하는 패턴 찾기: search()

.* 와일드카드 없이 'Young Frankenstein' 소스 문자열에서 'Frank' 패턴을 찾기 위해 search()를 사용할 수 있습니다.

>>> m = re.search('Frank', source)
>>> if m: # search는 객체를 반환합니다.
	print(m.group())

	
#Frank

 

일치하는 모든 패턴 찾기: findall()

이전 예제들은 매칭되는 패턴 하나만을 찾았습니다. 그렇다면 문자열에 'n'이 몇 개 있는지 알 수 있을까요?

>>> m = re.findall('n', source)
>>> m # findall은 리스트를 반환합니다.
#['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
#Found 4 matches

'n' 다음에 어떤 문자가 오는지 알아봅시다.

>>> m = re.findall('n.', source)
>>> m
#['ng', 'nk', 'ns']

마지막 'n'은 위 패턴에 포함되지 않습니다. 'n' 이후의 문자는 선택적이 되도록 ?을 추가합니다(.은 한 문자를 의미하고, ?는 0 또는 1회를 의미합니다. 그러므로 .?은 하나의 문자가 0 또는 1회 올 수 있다는 뜻입니다).

>>> m = re.findall('n.?', source)
>>> m
#['ng', 'nk', 'ns', 'n']

 

패턴으로 나누기: split()

다음 코드에서는 간단한 문자열 대신 패턴으로 문자열으 리스트로 나눕니다(일반 문자열에서 split() 메소드의 사용).

>>> m = re.split('n', source)
>>> m # split은 리스트를 반환합니다.
#['You', 'g Fra', 'ke', 'stei', '']

 

일치하는 패턴 대체하기: sub()

sub() 메소드는 문자열 replace() 메소드와 비슷하지만, 리터럴 문자열이 아닌 패턴을 사용합니다. 

>>> m = re.sub('n', '?', source)
>>> m # sub는 문자열을 반환합니다.
#'You?g Fra?ke?stei?'

 

패턴: 특수 문자

대부분의 정규표현식 설명은 정규표현식을 정의하는 방법에 대한 세부사항으로 시작합니다. 하지만 정규표현식은 한 번에 많은 세부사항을 머릿속에 넣을 수 있는 그렇게 작은 언어가 아닙니다. 정규표현식은 아주 많은 문장 부호를 사용합니다.

match(), search(), findall(), sub() 메소드에서 사용할 수 있는 정규표현식의 세부사항을 살펴봅시다. 이 메소드에서는 아래의 패턴을 적용할 수 있습니다.

기초부터 살펴봅시다.

  • 리터럴은 모든 비특수 문자와 일치한다.
  • \n을 제외한 하나의 문자: .
  • 0회 이상: *
  • 0 또는 1회: ?

다음의 표에 특수 문자를 나타냈습니다.

패턴 문자
\d 숫자
\D 비숫자
\w 알파벳 문자
\W 비알파벳 문자
\s 공백 문자
\S 비공백 문자
\b 단어 경계(\w와 \W 또는 \W와 \w 사이의 경계)
\B 비단어 경계

string 모듈은 테스트에 사용할 수 있는 문자열 상수가 미리 정의되어 있습니다. 알파벳 대/소문자, 숫자, 공백 문자, 구두점을 포함한 100가지 아스키 문자가 포함된 printable을 사용해봅시다.

>>> import string
>>> printable = string.printable
>>> len(printable)
#100
>>> printable[:50]
#'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
#'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

printable에서 숫자는 무엇일까요?

>>> re.findall('\d', printable)
#['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

숫자와 문자, 언더스코어는 무엇일까요?

>>> re.findall('\w', printable)
#['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_']

공백 문자는 무엇일까요?

>>> re.findall('\s', printable)
#[' ', '\t', '\n', '\r', '\x0b', '\x0c']

정규표현식은 아스키코드 외에 다른 것도 사용할 수 있습니다. \d는 아스키 문자 '0'에서 '9'까지 뿐만 아니라 유니코드가 정의하는 숫자도 될 수 있습니다. 아스키코드 문자가 아닌 두 글자를 아래와 같이 추가해봅시다(유니코드 문자는 FileFormat.info를 참고하세요).

테스트 조건은 다음과 같습니다.

  • 3개의 아스키 문자
  • \w와 일치하지 않는 3개의 구두점 기호
  • 유니코드 LATIN SMALL LETTER E WITH CIRCUMFLEX(\u00ea)
  • 유니코드 LATIN SMALL LETTER E WITH BREVE(\u0115)
>>> x = 'abc' + '-/*' + '\u00ea' + '\u0115'

예상한 대로 이 패턴은 문자만 찾습니다.

>>> re.findall('\w', x)
#['a', 'b', 'c', 'ê', 'ĕ']

 

패턴: 지정자

정규표현식에 다음의 표에 나타낸 패턴 지정자(Pattern Specifier)를 사용할 수 있습니다.

표에서 expr과 이탤릭체로 된 단어는 유효한 정규표현식을 의미합니다. 여기서 expr은 표현식(Expression)을, prev는 이전 토큰(Previous Token)을, next는 다음 토큰(Next Token)을 의미합니다.

패턴 일치
abc 리터럴 abc
( expr ) expr
expr1 | expr2 expr1 또는 expr2
. \n을 제외한 모든 문자
^ 소스 문자열의 시작
$ 소스 문자열의 끝
prev ? 0 또는 1회의 prev
prev* 0회 이상의 최대 prev
prev*? 0회 이상의 최소 prev
prev+ 1회 이상의 최대 prev
prev+? 1회 이상의 최소 prev
prev {m} m회의 prev
prev {m, n} m에서 n회의 최대 prev
prev {m, n}? m에서 n회의 최소 prev
[abc] a 또는 b 또는 c (a | b | c와 같음)
[^abc] (a 또는 b 또는 c)가 아님
prev (?=next) 뒤에 next가 오면 prev
prev (?!next) 뒤에 next가 오지 않으면 prev
(?<=prev) next 전에 prev가 오면 next
(?<!prev) next 전에 prev가 오지 않으면 next

패턴 지정자에 대한 코드를 살펴봅시다. 먼저 소스 문자열을 정의합니다.

>>> source = '''I wish I may, I wish I might
Have a dish of fish tonight.'''

소스에서 wish를 모두 찾습니다.

>>> re.findall('wish',  source)
#['wish', 'wish']

소스에서 wish 또는 fish를 모두 찾습니다.

>>> re.findall('wish | fish', source)
#['wish ', 'wish ', ' fish']

소스가 wish로 시작하는지 찾습니다.

>>> re.findall('^wish', source)
#[]

소스가 I wish로 시작하는지 찾습니다.

>>> re.findall('^I wish', source)
#['I wish']

소스가 fish로 끝나는지 찾습니다.

>>> re.findall('fish$', source)
#[]

소스가 fish tonight.으로 끝나는지 찾습니다.

>>> re.findall('fish tonight.$', source)
#['fish tonight.']

문자 ^와 $는 앵커(Anchor)라고 부릅니다. ^는 검색 문자열의 시작 위치에, $는 검색 문자열의 마지막 위치에 고정합니다. 그리고 .$는 가장 마지막에 있는 한 문자와 .을 매칭합니다. 더 정확하게 하려면 문자 그대로 매칭하기 위해 .에 이스케이프 문자를 붙여야합니다.

>>> re.findall('fish tonight\.$', source)
#['fish tonight.']

w 또는 f 다음에 ish가 오는 단어를 찾습니다.

>>> re.findall('[wf]ish', source)
#['wish', 'wish', 'fish']

w, s, h가 하나 이상인 단어를 찾습니다.

>>> re.findall('[wsh]+', source)
#['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']

ght 다음에 비알파벳 문자가 나오는 단어를 찾습니다.

>>> re.findall('ght\W', source)
#['gh

wish 이전에 나오는 I를 찾습니다.

>>> re.findall('I (?=wish)', source)
#['I ', 'I ']

I 다음에 나오는 wish를 찾습니다.

>>> re.findall('(?<=I) wish', source)
#[' wish', ' wish']

정규표현식 패턴이 파이썬 문자열 규칙과 충돌하는 몇 가지 경우가 잇습니다. 다음 패턴은 fish로 시작하는 단어를 찾아야 합니다.

>>> re.findall('\bfish', source)
#[]

그런데 위 코드는 왜 실행되지 않을까요? 2장에서 본 것처럼 파이썬은 문자열에 대해 몇 가지 특별한 이스케이프 문자를 사용합니다. 예를 들어 파이썬 문자열에서 \b는 백스페이스(Backspace)를 의미하지만, 정규표현식에서는 단어의 시작 부분을 의미합니다. 정규표현식의 패턴을 입력하기 전에 항상 문자 r(Raw String)을 입력합시다. 그러면 파이썬의 이스케이프 문자를 사용할 수 없게 되므로 실수로 이스케이프 문자를 사용하여 충돌이 일어나는 것을 피할 수 있게 됩니다. 다음 코드를 봅시다.

>>> re.findall(r'\bfish', source)
#['fish']

 

패턴: 매칭 결과 저장하기

match() 또는 search()를 사용할 때 모든 매칭은 m.group()과 같이 객체 m으로부터 결과를 반환합니다. 만약 패턴을 괄호로 둘러싸는 경우, 매칭은 그 괄호만의 그룹으로 저장됩니다. 그리고 다음과 같이 m.groups()를 사용하여 그룹의 튜플을 출력합니다.

>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
#'a dish of fish'
>>> m.groups()
#('a dish', 'fish')

만약(?P< name > expr) 패턴을 사용한다면, 표현식(expr)이 매칭되고, 그룹 이름(name)의 매칭 내용이 저장됩니다.

>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
#'a dish of fish'
>>> m.groups()
#('a dish', 'fish')
>>> m.group('DISH')
#'a dish'
>>> m.group('FISH')
#'fish'

 

2. 이진 데이터

 

텍스트 데이터는 다루기 힘들 수 있지만, 이진 데이터는 꽤 재미있을 것입니다. 이진 데이터를 다루기 위해서는 엔디안(Endian; 컴퓨터 프로세서가 데이터를 바이트로 나누는 방법)과 정수에 대한 사인 비트(Sign Bit)같은 개념을 알아야 합니다. 또한 데이터를 추출하거나 변경하는 바이너라 파일 형식과 네트워크 패킷을 배워야 할 수도 있습니다. 이번 절에서는 이진 데이터에 대한 기초를 살펴봅니다.

 

2.1. 바이트와 바이트 배열

 

파이썬 3은 다음 두 가지 타입으로 0 ~ 255 범위에서 사용할 수 있는 8비트 정수으 시퀀스를 소개했습니다.

  • 바이트(Byte)는 바이트의 튜플처럼 불변(Immutable)합니다.
  • 바이트 배열(Bytearray)은 바이트의 리스트처럼 변경 가능(Mutable)합니다.

먼저 리스트의 변수를 blist, 바이트 변수를 the_bytes, 바이트 배열 변수를 the_byte_array라고 합시다.

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
#b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
#bytearray(b'\x01\x02\x03\xff')

참고로, 바이트 값은 b로 시작하고, 그 다음에 인용 부호가 따라옵니다. 인용 부호 안에는 \x02 또는 아스키 문자와 같은 16진수 시퀀스가 옵니다. 파이썬은 16진수 시퀀스나 아스키 문자를 작은 정수로 변환합니다. 또한 아스키 문자처럼 유효한 아스키 인코딩의 바이트 배열을 보여줍니다.

>>> b'\x61'
#b'a'
>>> b'\x01abc\xff'
#b'\x01abc\xff'

다음 코드는 바이트 변수가 불변한다는 것을 보여줍니다.

>>> the_bytes[1] = 127
#Traceback (most recent call last):
#  File "<pyshell#25>", line 1, in <module>
#    the_bytes[1] = 127
#TypeError: 'bytes' object does not support item assignment

그러나 바이트 배열 변수는 변경 가능합니다.

>>> the_byte_array = bytearray(blist)
>>> the_byte_array
#bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
#bytearray(b'\x01\x7f\x03\xff')

다음 코드의 각 라인은 0에서 255까지, 256개의 결과를 생성합니다.

>>> the_bytes = bytes(range(0, 256))
>>> the_byte_array = bytearray(range(0, 256))

바이트 혹은 바이트 배열 데이터를 출력할 때, 파이썬은 출력할 수 없는 바이트에 대해서는 \x~~를 사용하고, 출력할 수 있는 바이트에 대해서는 아스키코드 값을 사용합니다(\x0a 대신 \n을 사용하는 것처럼, 일부 아스키코드 값은 일반적인 이스케이프 문자를 사용합니다). 다음은 the_bytes의 출력 결과입니다(수동으로 한 줄에 16바이트를 표시했습니다).

>>> the_bytes
#b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f
#\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
#!"#$%&\'()*+,-./
#0123456789:;<=>?
#@ABCDEFGHIJKLMNO
#PQRSTUVWXYZ[\\]^_
#`abcdefghijklmno
#pqrstuvwxyz{|}~\x7f
#\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f
#\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f
#\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf
#\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf
#\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
#\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
#\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef
#\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

이것은 문자가 아닌 바이트(작은 정수)이기 때문에 혼란스러울 수 있습니다.

 

2.2. 이진 데이터 변환하기: struct

 

파이썬에는 텍스트를 다루기 위한 많은 도구가 있습니다. 이진 데이터에 대한 도구는 그렇게 많이 알려지지 않았습니다. 파이썬 표준 라이브러리는 C와 C++의 구조체(Struct)와 유사한, 데이터를 처리하는 struct 모듈이 있습니다. struct를 사용하면 이진 데이터를 파이썬 데이터 구조로 바꾸거나 파이썬 데이터 구조를 이진 데이터로 바꿀 수 있습니다.

이것이 PNG 파일에서 데이터를 어떻게 처리하는지 살펴봅시다(GIF와 JPEG와 같은 일반적인 이미지 형식에서도 볼 수 있습니다). PNG 데이터에서 이미지의 가로와 세로의 길이를 추출하는 프로그램을 작성해봅시다.

예제에서는 오라일리의 로고인 안경원숭이를 사용합니다.

오라일리 안경원숭이

이 이미지의 PNG파일은 위키피디아에서 구할 수 있습니다. 파일을 ㅇ릭는 방법은 추후에 작성할 게시글을 통해 알려드리도록 하겠습니다. 그래서 저는 파일을 내려받은 후, 파일값을 바이트로 출력하기 위해 작은 프로그램을 작성했습니다. 그리고 첫 30바이트의 값을 data라는 바이트 변수에 할당했습니다(PNG 형식 사양은 가로와 세로의 길이가 첫 24바이트 내에 저장됩니다). 그래서 지금은 더 많은 정보를 필요로 하지 않습니다.

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
       b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
	width, height = struct.unpack('>LL', data[16:24])
	print('valid PNG, width', width, 'height', height)
    else:
	print('Not a valid PNG')

	
#valid PNG, width 154 height 141

다음은 코드에 대한 설명입니다.

  • data는 PNG 파일의 첫 30바이트를 포함합니다. 페이지에 맞추기 위해 두 개의 바이트 문자열을 _로 결합하고, 라인을 유지하는 문자를 사용했습니다.
  • valid_png_header는 유효한 PNG 파일의 시작을 표시하는 8바이트의 시퀀스를 포함합니다.
  • width는 16 ~20바이트에서 추출되었고, height는 21 ~ 24바이트에서 추출되었습니다.

unpack()에서 >LL은 입력한 바이트 시퀀스를 해석하고, 파이썬의 데이터 형식으로 만들어주는 형식 문자열(Format String)입니다. 그 의미를 살펴봅시다.

  • >는 정수가 빅엔디안(Big-Endian)형식으로 저장되었다는 것을 의미합니다.
  • 각각의 L은 4바이트의 부호 없는 긴 정수(Unsigned Long Integer)를 지정합니다.

각 4바이트 값을 직접 볼 수 있습니다.

>>> data[16:20]
#b'\x00\x00\x00\x9a'
>>> data[20:24]
#b'\x00\x00\x00\x8d'

빅엔디안 정수는 왼쪽에서부터 최상위 바이트가 저장됩니다. 가로와 세로의 각 길이는 255 이하이므로, 그들은 각 시퀀스의 마지막 바이트와 일치합니다. 이러한 16진수 값이 예상한 10진수 값과 맞는지 확인할 수 있습니다.

>>> 0x9a
#154
>>> 0x8d
#141

또한 struct 모듈의 pack() 함수로 파이썬 데이터를 바이트로 변환할 수 있습니다.

>>> import struct
>>> struct.pack('>L', 154)
#b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
#b'\x00\x00\x00\x8d'

다음의 표는 pakc()과 unpack()에 대한 형식 지정자(Format Specifier)를 보여줍니다. 먼저 형식 문자열의 엔디안 지정자(Endian Specifier)를 살펴봅시다.

지정자 바이트 순서
< 리틀엔디안
> 빅엔디안

다음은 형식 지정자입니다.

지정자 설명 바이트
x 1 바이트 건너뜀 1
b 부호 있는 바이트 1
B 부호 없는 바이트 1
h 부호 있는 짧은 정수 2
H 부호 없는 짧은 정수 2
i 부호 있는 정수 4
I 부호 없는 정수 4
l 부호 있는 긴 정수 4
L 부호 없는 긴 정수 4
Q 부호 없는 아주 긴 정수 8
f 단정도 부동소수점수 4
d 배정도 부동소수점수 8
p 문자수(count)와 문자 1 + count
s 문자 count

타입 지정자는 엔디안 문자를 따릅니다. 어떤 지정자는 문자수를 가리키는 숫자가 선행될 수 있습니다. 에를 들어, 5B는 BBBBB와 같습니다.

그러므로 >LL 대신 count(문자수)를 선행하여 >2L로 지정할 수 있습니다.

>>> struct.unpack('>2L', data[16:24])
#(154, 141)

원하는 바이트를 바로 얻기 위해 슬라이스 data[16:24]를 사용했습니다. 또한 다음과 같이 x 지정자를 사용하여 필요 없는 부분을 건너뛸 수 있습니다.

>>> struct.unpack('>16x2L6x', data)
#(154, 141)

다음은 위 코드에 대한 설명입니다.

  • 빅엔디안 정수 형식 사용함(>)
  • 16바이트를 건너뜀(16x)
  • 두 개의 부호 없는 긴 정수(Unsigned Long Integer)의 8바이트를 읽음(2L)
  • 마지막 6바이트를 건너뜀(6x)

 

2.3. 바이트 / 문자열 변환하기: binascii()

 

표준 binascii 모듈은 이진 데이터와 다양한 문자열 표현(16진수, 64진수, uuencoded 등)을 서로 변환할 수 있는 함수를 제공합니다. 다음 콛는 아스키코드의 혼합과 바이트 변수를 보여주기 위해 사용했던 \x ~~ 이스케이프 대신 16진수의 시퀀스인 8바이트의 PNG 헤더를 출력합니다.

>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
#b'89504e470d0a1a0a'

그 반대도 가능합니다.

>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
#b'\x89PNG\r\n\x1a\n'

 

2.4. 비트 연산자

 

파이썬은 C언어와 유사한 비트단위 정수 연산을 제공합니다. 다음의 표에 이들을 요약한 내용과 정수 a(10진수 5, 2진수 0b0101)와 b(10진수 1, 2진수 0b0001)를 사용한 연산 예제가 있습니다.

연산자 설명 예제 10진수 결과 2진수 결과
& AND a & b 1 0b0001
| OR a | b 5 0b0101
^ 배타적(Exclusive) OR a ^ b 4 0b0100
~ NOT ~a -6 정수 크기에 따라 다름
<< 비트 왼쪽 이동 a << 1 10 0b1010
>> 비트 오른쪽 이동 a >> 1 2 0b0010

이러한 연산자는 3장의 집합(Set) 연산자처럼 동작합니다. & 연산자는 두 인자의 비트가 모두 1일 때 1을 반환합니다. | 연산자는 둘 중 하나의 비트라도 1일 때 1을 반환합니다. ^ 연산자는 두 인자의 비트가 서로 다를 때 1을 반환합니다. ~ 연산자는 1은 0으로 0은 1로 비트를 반전시킵니다. 이것은 또한 부호를 반전시킵니다. 모든 현대 컴퓨터에 사용되는 2의 보수 연산에서 최상위 비트는 부호(1 = 음수)를 나타내기 때문입니다. <<와 >> 연산자는 왼쪽 또는 오른쪽으로 비트를 이동시킵니다. 한 비트 왼쪽 이동은 2를 곱한 것과 같고, 오른쪽 이동은 2로 나눈 것과 같습니다.

'Programming > Python' 카테고리의 다른 글

8. Python으로 웹 살펴보기  (0) 2020.03.13
7. Python으로 데이터 다루기(하)  (0) 2020.03.11
5. Python의 객체와 클래스  (0) 2020.02.06
4. Python의 모듈, 패키지, 프로그램  (0) 2020.02.04
3. Python의 코드 구조  (0) 2019.12.02