파이썬 3에서 한글 print 포맷 정렬하기

CJK Format

현재 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

참고문헌

이 칸을 비워 두세요.