파이썬 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