Skip to main content

텍스트 정제 실습

텍스트 정제(Text Cleaning)는 원시 텍스트에서 노이즈를 제거하고 일관된 형태로 변환하는 과정입니다. 이 문서에서는 실무에서 자주 마주하는 정제 작업을 Python 코드와 함께 단계별로 실습합니다.
1

유니코드 정규화

같은 글자가 다른 유니코드 코드 포인트를 가질 수 있습니다. 유니코드 정규화(Unicode Normalization)는 이런 불일치를 해결하는 첫 번째 단계입니다.왜 필요한가: “가”라는 글자는 유니코드에서 두 가지 방식으로 표현됩니다. NFC(조합형)는 하나의 코드 포인트로, NFD(분해형)는 초성+중성+종성으로 분해된 형태입니다. 이 두 표현은 눈에는 동일하지만 문자열 비교에서 다르게 취급됩니다.
import unicodedata

# 같아 보이지만 다른 유니코드 표현
nfc_text = "가나다"                    # NFC: 조합형
nfd_text = unicodedata.normalize("NFD", "가나다")  # NFD: 분해형

print(f"NFC 길이: {len(nfc_text)}")    # 3
print(f"NFD 길이: {len(nfd_text)}")    # 6 (각 글자가 초성+중성으로 분해)
print(f"동일 여부: {nfc_text == nfd_text}")  # False!

# NFC로 정규화하여 통일합니다
def normalize_unicode(text):
    """유니코드를 NFC 형태로 정규화합니다."""
    return unicodedata.normalize("NFC", text)

normalized = normalize_unicode(nfd_text)
print(f"정규화 후 동일 여부: {nfc_text == normalized}")  # True
# 전각/반각 문자 통일
def fullwidth_to_halfwidth(text):
    """전각 문자를 반각 문자로 변환합니다."""
    result = []
    for char in text:
        code = ord(char)
        # 전각 영숫자/기호 → 반각 (FF01~FF5E → 0021~007E)
        if 0xFF01 <= code <= 0xFF5E:
            result.append(chr(code - 0xFEE0))
        # 전각 공백 → 반각 공백
        elif code == 0x3000:
            result.append(' ')
        else:
            result.append(char)
    return ''.join(result)

text = "Hello World!"  # 전각 문자
print(fullwidth_to_halfwidth(text))    # "Hello World!"
한국어 텍스트는 반드시 NFC 정규화를 적용하세요. macOS의 파일 시스템은 NFD를 사용하므로, 파일에서 읽어온 한국어 텍스트가 다른 시스템과 다르게 처리될 수 있습니다.
2

대소문자 정규화

영문 텍스트가 포함된 경우, 대소문자 불일치가 동일한 단어를 다르게 취급하게 만듭니다.
def normalize_case(text, mode="lower"):
    """대소문자를 정규화합니다.

    Args:
        text: 입력 텍스트
        mode: 'lower'(소문자), 'upper'(대문자), 'preserve'(유지)
    """
    if mode == "lower":
        return text.lower()
    elif mode == "upper":
        return text.upper()
    return text

# 기본적으로 소문자로 통일합니다
text = "Natural Language Processing은 NLP라고도 합니다"
print(normalize_case(text))
# "natural language processing은 nlp라고도 합니다"
대소문자 정규화는 주의가 필요합니다. NER(개체명 인식)에서는 대문자가 중요한 단서가 됩니다. 예를 들어 “Apple”(회사)과 “apple”(과일)은 대소문자로 구분될 수 있습니다. 태스크에 따라 적용 여부를 판단하세요.
3

HTML 태그 및 마크업 제거

웹에서 수집한 텍스트에는 HTML 태그, CSS, JavaScript 코드가 포함되어 있을 수 있습니다.
import re
from html import unescape

def remove_html_tags(text):
    """HTML 태그를 제거하고 HTML 엔티티를 디코딩합니다."""
    # HTML 엔티티 디코딩 (&amp; → &, &lt; → <, 등)
    text = unescape(text)
    # HTML 태그 제거
    text = re.sub(r'<[^>]+>', '', text)
    # 연속 공백 정리
    text = re.sub(r'\s+', ' ', text).strip()
    return text

html_text = """
<div class="content">
    <h1>자연어 처리</h1>
    <p>NLP는 &amp; AI의 <b>핵심</b> 기술입니다.</p>
    <script>console.log("무시할 내용")</script>
</div>
"""
print(remove_html_tags(html_text))
# "자연어 처리 NLP는 & AI의 핵심 기술입니다. console.log("무시할 내용")"
# BeautifulSoup을 활용한 더 정밀한 HTML 처리
from bs4 import BeautifulSoup

def clean_html(html_text):
    """BeautifulSoup으로 HTML을 정밀하게 정제합니다."""
    soup = BeautifulSoup(html_text, "html.parser")

    # script, style 태그를 완전히 제거합니다
    for tag in soup(["script", "style", "meta", "link"]):
        tag.decompose()

    # 텍스트만 추출합니다
    text = soup.get_text(separator=" ")

    # 연속 공백과 줄바꿈을 정리합니다
    text = re.sub(r'\s+', ' ', text).strip()
    return text

print(clean_html(html_text))
# "자연어 처리 NLP는 & AI의 핵심 기술입니다."
4

특수문자 및 이모지 처리

텍스트에 포함된 특수문자와 이모지를 태스크에 맞게 처리합니다.
import re

def remove_special_characters(text, keep_korean=True, keep_english=True,
                               keep_numbers=True, keep_spaces=True,
                               additional_keep=""):
    """특수문자를 제거합니다. 유지할 문자 유형을 선택할 수 있습니다."""
    pattern_parts = []
    if keep_korean:
        pattern_parts.append("가-힣ㄱ-ㅎㅏ-ㅣ")
    if keep_english:
        pattern_parts.append("a-zA-Z")
    if keep_numbers:
        pattern_parts.append("0-9")
    if keep_spaces:
        pattern_parts.append(" ")
    if additional_keep:
        pattern_parts.append(re.escape(additional_keep))

    pattern = f"[^{''.join(pattern_parts)}]"
    text = re.sub(pattern, ' ', text)
    text = re.sub(r'\s+', ' ', text).strip()
    return text

text = "Hello! 안녕하세요~ NLP #공부 중입니다 😊👍 (2024년)"
print(remove_special_characters(text))
# "Hello 안녕하세요 NLP 공부 중입니다  2024년"

# 마침표와 쉼표를 유지하려면
print(remove_special_characters(text, additional_keep=".,"))
# 이모지 전용 처리
def remove_emojis(text):
    """이모지를 제거합니다."""
    emoji_pattern = re.compile(
        "["
        "\U0001F600-\U0001F64F"  # 얼굴 이모지
        "\U0001F300-\U0001F5FF"  # 기호 & 그림
        "\U0001F680-\U0001F6FF"  # 교통 & 지도
        "\U0001F1E0-\U0001F1FF"  # 국기
        "\U00002702-\U000027B0"  # 기타 기호
        "\U000024C2-\U0001F251"
        "]+",
        flags=re.UNICODE
    )
    return emoji_pattern.sub('', text)

text = "오늘 기분이 좋아요 😊🎉💕"
print(remove_emojis(text))
# "오늘 기분이 좋아요 "
5

불용어 제거

불용어(Stop Words)는 분석에 큰 도움이 되지 않는 고빈도 단어입니다. “은”, “는”, “이”, “가” 같은 조사와 “하다”, “있다” 같은 일반적인 동사가 해당합니다.
# 한국어 불용어 리스트 (기본)
KOREAN_STOPWORDS = {
    # 조사
    "이", "가", "은", "는", "을", "를", "에", "에서", "의", "와", "과",
    "도", "로", "으로", "만", "까지", "부터", "에게", "한테",
    # 보조 용언
    "하다", "있다", "되다", "않다", "없다", "같다",
    # 접속 부사
    "그리고", "그러나", "그래서", "하지만", "또한", "또는",
    # 대명사
    "나", "너", "우리", "그", "그녀", "그것", "이것", "저것",
    # 기타
    "것", "수", "등", "및", "더", "매우", "아주"
}

def remove_stopwords(tokens, stopwords=None):
    """토큰 리스트에서 불용어를 제거합니다."""
    if stopwords is None:
        stopwords = KOREAN_STOPWORDS
    return [token for token in tokens if token not in stopwords]

# Mecab과 함께 사용
from konlpy.tag import Mecab
mecab = Mecab()

text = "나는 오늘 학교에서 자연어 처리를 공부했습니다"
tokens = mecab.morphs(text)
print(f"원본 토큰: {tokens}")
# ['나', '는', '오늘', '학교', '에서', '자연어', '처리', '를', '공부', '했', '습니다']

filtered = remove_stopwords(tokens)
print(f"불용어 제거: {filtered}")
# ['오늘', '학교', '자연어', '처리', '공부', '했', '습니다']
불용어 제거는 태스크에 따라 신중하게 적용해야 합니다. 감성 분석에서 “않다”, “없다”는 부정의 핵심 단서이므로 제거하면 안 됩니다. Transformer 모델을 사용할 때는 불용어 제거가 오히려 성능을 저하시킬 수 있습니다.
6

반복 문자 정규화

소셜 미디어나 리뷰 데이터에서 자주 나타나는 반복 문자를 정규화합니다.
import re

def normalize_repeated_chars(text, max_repeat=2):
    """연속 반복 문자를 최대 max_repeat 개로 제한합니다."""
    pattern = re.compile(r'(.)\1{' + str(max_repeat) + r',}')
    return pattern.sub(r'\1' * max_repeat, text)

text = "진짜 맛있어요ㅋㅋㅋㅋㅋㅋㅋ 완전 좋아요!!!!!"
print(normalize_repeated_chars(text))
# "진짜 맛있어요ㅋㅋ 완전 좋아요!!"

text2 = "ㅠㅠㅠㅠ 너무 슬퍼요ㅜㅜㅜㅜㅜ"
print(normalize_repeated_chars(text2))
# "ㅠㅠ 너무 슬퍼요ㅜㅜ"
7

통합 정제 파이프라인 구축

지금까지의 단계를 하나의 파이프라인으로 통합합니다.
import re
import unicodedata
from html import unescape

class TextCleaner:
    """텍스트 정제 파이프라인을 관리합니다."""

    def __init__(self, config=None):
        """정제 설정을 초기화합니다.

        Args:
            config: 정제 옵션 딕셔너리. None이면 기본값을 사용합니다.
        """
        self.config = config or {
            "unicode_normalize": True,
            "remove_html": True,
            "lowercase": False,
            "remove_special_chars": True,
            "normalize_repeats": True,
            "max_repeat": 2,
            "remove_emojis": True,
            "strip_whitespace": True
        }

    def clean(self, text):
        """설정에 따라 텍스트를 정제합니다."""
        if not text or not isinstance(text, str):
            return ""

        if self.config.get("unicode_normalize"):
            text = unicodedata.normalize("NFC", text)

        if self.config.get("remove_html"):
            text = unescape(text)
            text = re.sub(r'<[^>]+>', ' ', text)

        if self.config.get("lowercase"):
            text = text.lower()

        if self.config.get("remove_emojis"):
            text = re.sub(
                r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF'
                r'\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF]+',
                '', text
            )

        if self.config.get("normalize_repeats"):
            max_r = self.config.get("max_repeat", 2)
            text = re.sub(
                r'(.)\1{' + str(max_r) + r',}',
                r'\1' * max_r, text
            )

        if self.config.get("remove_special_chars"):
            text = re.sub(r'[^\w\s가-힣ㄱ-ㅎㅏ-ㅣ.,!?]', ' ', text)

        if self.config.get("strip_whitespace"):
            text = re.sub(r'\s+', ' ', text).strip()

        return text

    def clean_batch(self, texts):
        """텍스트 리스트를 일괄 정제합니다."""
        return [self.clean(text) for text in texts]

# 사용 예시
cleaner = TextCleaner()

raw_texts = [
    "<p>완전 좋아요ㅋㅋㅋㅋㅋㅋ 😊👍</p>",
    "이 제품은 &amp; 최고입니다!!!!!!",
    " Hello 자연어 처리~~ ##NLP"
]

for raw in raw_texts:
    cleaned = cleaner.clean(raw)
    print(f"원본: {raw}")
    print(f"정제: {cleaned}")
    print()
8

데이터 품질 관리

정제 후에는 데이터 품질을 검사하여 문제가 없는지 확인합니다.
def check_data_quality(texts):
    """텍스트 데이터의 품질을 검사합니다."""
    stats = {
        "total": len(texts),
        "empty": 0,          # 빈 텍스트
        "too_short": 0,      # 너무 짧은 텍스트 (5자 미만)
        "too_long": 0,       # 너무 긴 텍스트 (5000자 초과)
        "duplicates": 0,     # 중복 텍스트
        "high_special": 0,   # 특수문자 비율이 높은 텍스트
    }

    seen = set()
    for text in texts:
        if not text:
            stats["empty"] += 1
            continue

        if len(text) < 5:
            stats["too_short"] += 1

        if len(text) > 5000:
            stats["too_long"] += 1

        if text in seen:
            stats["duplicates"] += 1
        seen.add(text)

        # 특수문자 비율 계산
        special_ratio = sum(
            1 for c in text if not c.isalnum() and c != ' '
        ) / len(text)
        if special_ratio > 0.3:
            stats["high_special"] += 1

    # 품질 보고서를 출력합니다
    print("=== 데이터 품질 보고서 ===")
    print(f"전체 데이터: {stats['total']}건")
    print(f"빈 텍스트: {stats['empty']}건 ({stats['empty']/stats['total']*100:.1f}%)")
    print(f"짧은 텍스트: {stats['too_short']}건")
    print(f"긴 텍스트: {stats['too_long']}건")
    print(f"중복: {stats['duplicates']}건")
    print(f"특수문자 과다: {stats['high_special']}건")

    return stats
네, 일반적으로 유니코드 정규화 → HTML 제거 → 특수문자 처리 → 정규화 순서를 따릅니다. HTML 엔티티(&amp;)를 먼저 디코딩해야 이후 단계에서 올바른 문자를 처리할 수 있습니다. 불용어 제거는 토큰화 이후에 적용합니다.
Transformer의 토크나이저는 특수문자와 노이즈를 어느 정도 처리할 수 있지만, HTML 태그, 불필요한 코드, 인코딩 오류 등 명백한 노이즈는 제거하는 것이 좋습니다. 다만, 불용어 제거나 형태소 분석 같은 언어학적 전처리는 Transformer 모델에서는 생략해도 되는 경우가 많습니다.
기본적인 패턴만 알면 대부분의 정제 작업을 수행할 수 있습니다. .(아무 문자), *(0회 이상), +(1회 이상), [](문자 클래스), \s(공백), \d(숫자), \w(단어 문자)만 익히면 됩니다. Python의 re 모듈 공식 문서를 참고하세요.
정규 표현식을 미리 컴파일(re.compile())하고, 리스트 컴프리헨션 대신 제너레이터를 활용하세요. 데이터가 매우 크다면 multiprocessing이나 pandasapply()를 사용한 병렬 처리를 고려하세요. 정제 결과를 캐싱하면 반복 작업을 줄일 수 있습니다.

다음 문서