파이썬 3에서 한글 print 포맷 정렬하기
현재 GRASS GIS를 한글화하면서 발생하는 문제 중의 하나이다. C에서 한글 printf 포맷 정렬하기도 같은 문제이다.
파이썬 3에서 한글 한 글자의 길이가 1로 계산된다. 실제 터미널에서 차지하는 공간은 2이기 때문에 영문자와 정렬이 어긋나게 된다.
>>> print(len('가'))
1 # 2가 아닌 1이 출력된다!
>>> print('%-10s|' % '123456789')
123456789 |
>>> print('%-10s|' % '1234567가') # 위의 명령과 입력의 폭은 똑같지만 출력의 폭은 한 칸 더 길다.
1234567가 |
이 문제는 한글 문자열의 길이가 길어질 수록 더 두드러진다.
>>> print('%-10s|%-10s|' % ('ab', 'cd'))
ab |cd |
>>> print('%-10s|%-10s|' % ('가나다라', '마바사아'))
가나다라 |마바사아 |
이 문서에서 같은 문제를 다루고 있다. 이 문서에 링크된 원문은 호스팅 서비스가 종료되었다. 다음과 같은 해결책을 제시하고 있다.
import unicodedata
def preformat_cjk(string, width, align='<', fill=' '):
count = (width - sum(1 + (unicodedata.east_asian_width(c) in "WF")
for c in string))
return {
'>': lambda s: fill * count + s,
'<': lambda s: s + fill * count,
'^': lambda s: fill * (count / 2)
+ s
+ fill * (count / 2 + count % 2)
}[align](string)
다음과 같이 사용한다.
>>> print('%-10s|%-10s|' % ('ab', 'cd'))
ab |cd |
>>> print('%s|%s|' % (preformat_cjk('가나다라', 10), preformat_cjk('마바사 아', 10)))
가나다라 |마바사아 |
본 HTML 문서에서는 정렬이 어긋나 보일 수 있다. 이건 브라우저의 고정폭 글꼴 문제인 것 같다. 이건 또 어떻게 해결하지?
아무튼 대충 힌트는 얻은 것 같은데 이 방법은 사용하기가 번거롭고 CJK가 아닌 언어를 사용하는 개발자들에게는 권하기가 민망한 방법이다.
여기에 CJK 문자열의 길이를 계산하는 함수들의 예가 많이 있다. 라이선스들이 있어서 GNU와 호환되는지 잘 모르겠고 그냥 혼자 만들어 보면 다음과 같다.
import unicodedata
def width(s):
return sum([1 + (unicodedata.east_asian_width(c) in 'WF') for c in s])
테스트해 보자.
>>> print(width('xyz'))
3
>>> print(width('가나다'))
6
>>> print(width('가나다xyz'))
9
이제 최종 해결책을 살펴 보자.
# (C) 2020 GPL by Huidae Cho
import re
import unicodedata
def wide_count(s):
return sum(unicodedata.east_asian_width(c) in 'WF' for c in s)
def f(fmt, *args):
matches = []
# https://docs.python.org/3/library/stdtypes.html#old-string-formatting
for m in re.finditer('%([#0 +-]*)([0-9]*)(\.[0-9]*)?([hlL]?[diouxXeEfFgGcrsa%])', fmt):
matches.append(m)
if len(matches) != len(args):
raise Exception('The numbers of format specifiers and arguments do not match')
i = len(args) - 1
for m in reversed(matches):
f = m.group(1)
w = m.group(2)
p = m.group(3) or ''
c = m.group(4)
print(f, w, p, c)
if c == 's' and w:
w = str(int(w) - wide_count(args[i]))
fmt = ''.join((fmt[:m.start()], '%', f, w, p, c, fmt[m.end():]))
i -= 1
return fmt % args
def printf(fmt, *args):
print(f(fmt, *args), end='')
wide_count()는 CJK 글자가 몇 개 있는지 세는 함수로서 주함수인 f()에서 호출된다. f() 함수를 어떻게 쓰는지 살펴 보자.
>>> print('%10s|%10s|' % ('a', 'b')) # 기존의 방법
a| b|
>>> print('%10s|%10s|' % ('가나', '다라'))
가나| 다라|
>>> print(f('%10s|%10s|', 'a', 'b')) # 새로운 방법
a| b|
>>> print(f('%10s|%10s|', '가나', '다라'))
가나| 다라|
새로운 방법은 파이썬 3의 f-문자열 문법을 살짝 닮은 이름이다. 일부러 함수의 이름을 짧게 지었다. print()와 f() 함수를 함께 사용하고 싶은 경우를 위해 단축 함수인 printf()도 정의했다. 함수의 이름이 C의 printf()와 같아서 새줄을 출력하지 않게 end=''를 추가했다.
>>> printf('%10s|%10s|\n', 'a', 'b') # printf()를 사용한 방법
a| b|
>>> printf('%10s|%10s|\n', '가나', '다라')
가나| 다라|
GitHub에 CJK Format이라는 저장소를 만들었고 PyPI에 올려 뒀다. 다음과 같이 pip3으로 설치할 수 있다.
pip3 install --user cjkformat