본문으로 건너뛰기
김신건의 로그

[Python] textwrap, locale, unicodedata: 텍스트 처리

· 수정 · 📖 약 1분 · 576자/단어 #python #textwrap #locale #unicodedata #stdlib #string
python textwrap, locale, unicodedata, string normalization, NFC, NFD

정의

텍스트 처리 표준 모듈 3개.

  • textwrap: 줄바꿈, 들여쓰기 정렬
  • locale: 지역화 (숫자/통화/날짜 포맷)
  • unicodedata: 유니코드 정규화·분류·검색

textwrap

긴 텍스트를 일정 폭으로 줄바꿈.

import textwrap

text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

print(textwrap.fill(text, width=40))
# Lorem ipsum dolor sit amet,
# consectetur adipiscing elit, sed do
# eiusmod tempor incididunt ut labore et
# dolore magna aliqua.

lines = textwrap.wrap(text, width=40)    # 리스트로
print(len(lines))

dedent, indent

from textwrap import dedent, indent

# dedent: 공통 들여쓰기 제거 (멀티라인 문자열 정리)
def helper():
    text = dedent("""
        Hello
            Indented
        World
    """).strip()
    return text

# indent: 모든 줄에 prefix
print(indent("line 1\nline 2", "    "))

dedent는 docstring/긴 문자열을 자연스럽게 들여쓰기로 작성하면서 결과는 깔끔하게.

shorten

textwrap.shorten("The quick brown fox jumps over the lazy dog", width=20)
# 'The quick brown [...]'

textwrap.shorten("...", width=20, placeholder="…")

긴 텍스트 잘라내기 (단어 경계 유지).

locale

OS 지역 설정 기반 포매팅.

import locale

# 시스템 기본
locale.setlocale(locale.LC_ALL, "")

# 또는 명시
locale.setlocale(locale.LC_ALL, "ko_KR.UTF-8")
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")

# 숫자
print(locale.format_string("%d", 1234567, grouping=True))   # 1,234,567

# 통화
print(locale.currency(1234.56, grouping=True))
# 한국: '₩1,234.56' 또는 '\u20a91,234'
# 미국: '$1,234.56'

# 날짜
import time
print(time.strftime("%c"))    # 로케일에 따른 표현

비교·정렬

import locale

words = ["zebra", "Apple", "banana", "Cherry"]

# 기본 (ASCII 순)
print(sorted(words))    # ['Apple', 'Cherry', 'banana', 'zebra']

# 로케일 인식
locale.setlocale(locale.LC_COLLATE, "en_US.UTF-8")
print(sorted(words, key=locale.strxfrm))    # ['Apple', 'banana', 'Cherry', 'zebra']

한글 가나다 순도 로케일 정렬로.

함정

  • 전역 상태라 멀티스레드에서 위험
  • 로케일 사용 가능 여부가 OS 의존
  • 대부분 사용자는 직접 포맷 (f"{n:,}")이 더 명시적
# 일반적으로 locale보다 명시 포맷
f"{1234567:,}"        # '1,234,567' (영문)
f"{1234.56:.2f}"      # '1234.56'

# 한국 원화는 보통 직접
def krw(n):
    return f"{n:,}원"

본격 i18n은 babel 라이브러리.

unicodedata

유니코드 문자의 속성과 정규화.

import unicodedata

print(unicodedata.name("A"))           # 'LATIN CAPITAL LETTER A'
print(unicodedata.name("한"))           # 'HANGUL SYLLABLE HAN'
print(unicodedata.name("😀"))            # 'GRINNING FACE'

print(unicodedata.category("A"))       # 'Lu' (대문자)
print(unicodedata.category("a"))       # 'Ll' (소문자)
print(unicodedata.category(" "))       # 'Zs' (공백)
print(unicodedata.category("1"))       # 'Nd' (숫자)
print(unicodedata.category("😀"))        # 'So' (기호)

카테고리 코드:

  • L=Letter (Lu/Ll/Lt/Lm/Lo)
  • N=Number (Nd/Nl/No)
  • P=Punctuation
  • S=Symbol
  • Z=Separator (Zs/Zl/Zp)
  • M=Mark (Mn/Mc/Me)
  • C=Control/format

정규화: NFC, NFD, NFKC, NFKD

같은 글자가 다른 코드포인트 조합으로 표현될 수 있다.

import unicodedata

s1 = "안"               # NFC: 한 코드포인트 U+C548
s2 = "\u110b\u1161\u11ab"   # NFD: 자모 분해 ㅇ + ㅏ + ㄴ
print(s1, s2)
print(len(s1), len(s2))     # 1, 3
print(s1 == s2)              # False (다른 코드포인트)

# 정규화
nfc1 = unicodedata.normalize("NFC", s1)
nfc2 = unicodedata.normalize("NFC", s2)
print(nfc1 == nfc2)          # True

NFC vs NFD vs NFKC vs NFKD

C (Composed)D (Decomposed)
일반NFC: 결합형NFD: 분해형
호환NFKC: 호환 + 결합NFKD: 호환 + 분해
  • NFC: ㄱㅏ → 가. 기본 권장. JSON·DB 저장에.
  • NFD: 가 → ㄱㅏ. macOS 파일시스템 기본 (HFS+).
  • NFKC: 호환 문자 통합. ① → 1, ㉠ → 가. 검색 정규화에.
  • NFKD: KC의 분해.

실용: 사용자 입력 정규화

def normalize_korean(s):
    """가급적 NFC로."""
    return unicodedata.normalize("NFC", s)

# DB 저장 전, 비교 전 적용
input_name = normalize_korean(form_data["name"])

macOS 파일명을 그대로 받으면 NFD라 다른 표현. 항상 정규화.

accent 제거 (transliteration)

import unicodedata

def strip_accents(s):
    nfd = unicodedata.normalize("NFD", s)
    return "".join(c for c in nfd if unicodedata.category(c) != "Mn")

print(strip_accents("café"))       # 'cafe'
print(strip_accents("résumé"))     # 'resume'
print(strip_accents("naïve"))      # 'naive'

검색 인덱스, slug 생성에 흔히 사용.

슬러그 생성

import unicodedata
import re

def slugify(s):
    s = unicodedata.normalize("NFKD", s)
    s = s.encode("ascii", "ignore").decode("ascii")    # 비ASCII 제거
    s = re.sub(r"[^\w\s-]", "", s).strip().lower()
    return re.sub(r"[-\s]+", "-", s)

print(slugify("Hello, World!"))         # 'hello-world'
print(slugify("Café résumé"))            # 'cafe-resume'
print(slugify("한글 제목"))               # '' (한글은 제거됨)

한글까지 보존하려면 python-slugify 라이브러리.

pip install python-slugify

자주 보는 패턴

한국어 텍스트 분리 (음절/자모)

import unicodedata

def to_jamo(text):
    """음절 → 자모 분해."""
    return unicodedata.normalize("NFD", text)

def from_jamo(text):
    """자모 → 음절 결합."""
    return unicodedata.normalize("NFC", text)

print(list(to_jamo("한글")))    # ['ㅎ', 'ㅏ', 'ㄴ', 'ㄱ', 'ㅡ', 'ㄹ']
print(from_jamo("ㅎㅏㄴㄱㅡㄹ"))  # '한글'

이모지 검사

import unicodedata

def has_emoji(s):
    return any(unicodedata.category(c).startswith("So") for c in s)

print(has_emoji("hello 😀"))     # True
print(has_emoji("hello"))        # False

복잡한 이모지(ZWJ, skin tone)는 emoji 라이브러리.

텍스트 폭 (CJK 고려)

import unicodedata

def display_width(s):
    """터미널 표시 폭 추정."""
    w = 0
    for c in s:
        if unicodedata.east_asian_width(c) in ("W", "F"):
            w += 2
        else:
            w += 1
    return w

print(display_width("hello"))         # 5
print(display_width("안녕하세요"))     # 10 (한글 = 2칸)
print(display_width("hello 안녕"))    # 11

CLI 출력 폭 정렬, 터미널 UI에 활용.

Codepoint 검색

import unicodedata

print(unicodedata.lookup("BLACK STAR"))    # '★'
print(unicodedata.lookup("CHECK MARK"))    # '✓'

# 반대로
print(unicodedata.name("★"))               # 'BLACK STAR'

\N{NAME} 이스케이프와 같은 메커니즘.

s = "\N{BLACK STAR}"    # '★'

함정

1. 표면적으로 같은 문자

"ㅏ" == "ᅡ"      # False (다른 코드포인트)
"K" == "К"      # False (라틴 K vs 키릴 К)

특히 보안: 도메인 이름 phishing(paypal.com vs pаypal.com)에 활용. 검증에 NFKC 정규화.

2. textwrap의 CJK 폭 무시

import textwrap
print(textwrap.fill("한글한글한글한글한글한글한글", width=10))
# CJK가 2칸이지만 1칸으로 계산 → 폭 초과

CJK 안전 wrapping은 wcwidth 또는 직접 구현 필요.

3. locale 멀티스레드

import locale, threading

def worker():
    locale.setlocale(locale.LC_ALL, "...")    # 전역! 다른 스레드 영향

스레드 안전성 약함. 가능하면 명시 포맷 사용.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기