Profile picture

[Python] 정규표현식으로 안되는 데이터 RapidFuzz로 매핑하기

JaehyoJJAng2025년 12월 28일

개요

최근 사내 서버 비용 관리 대시보드를 만들면서 겪었던 문제를 Fuzz Matching 라이브러리인 rapidfuzz를 통해 해결한 경험을 공유하려고 합니다.


데이터 정규화 난제 ...

사내 인프라팀에서 관리하는 DB와, 자산팀에서 제공하는 가격 API를 연동하여

이번 달 입고된 서버의 총 가격 을 계산해야 하는 상황입니다.


그런데 데이터가 심상치 않습니다..

  • DB (엔지니어 입력값): 총무팀으로부터 전달된 서버 스펙 정보가 저장됩니다. (약어와 축약어가 난무합니다.)
    • ssd wd 1T*2 (웨스턴 디지털 SSD 1테라 2개)
    • intel nic 10g (인텔 10G 네트워크 카드)
  • API (시스템 표준값): 제조사 풀네임과 대괄호 등이 포함된 정형화된 데이터입니다.
    • [SSD]Western Digital Blue 1T
    • [10G NIC]Intel X550-T2

문제점

단순히 if db_name == api_name:으로는 절대 매칭되지 않습니다.


정규표현식(re)을 쓰자니 wd가 western digital인지, 순서가 섞여있는지 모든 케이스를 예외처리하다가 밤을 새울 것 같습니다.


이때 알게된 것이 바로 유사도 검사(Fuzz Matching) 입니다.




RapidFuzz

파이썬에는 FuzzyWuzzy라는 유명한 라이브러리가 있지만, 속도 이슈와 라이선스 문제(GPL)가 있었습니다.

이를 개선하여 MIT 라이센스이면서, C++로 작성되어 엄창나게 빠른 라이브러리가 바로 RapidFuzz입니다.


설치

pip install rapidfuzz

[실습] 유사도 측정해보기

먼저 간단한 문자열 비교부터 해보겠습니다.

from rapidfuzz import fuzz

# Case 1: 단순 비율 비교 (Ratio)
# wd와 Western Digital은 글자 길이가 너무 달라서 점수가 낮게 나옵니다.
s1 = "wd ssd 1t"
s2 = "Western Digital SSD 1T"
print(f"Ratio: {fuzz.ratio(s1, s2)}")
# 결과: Ratio: 19.354838709677423 (매칭 실패로 간주될 확률 높음)

# Case 2: 부분 집합 비율 비교 (Token Set Ratio)
# 이 방식은 문자열을 공백 기준으로 쪼개고(Token), 교집합을 찾아 비교함
# 순서가 바뀌어도, 중간에 다른 단어가 껴도 매칭이 잘된다는 특징이 있음
print(f"Token Set Ratio: {fuzz.token_set_ratio(s1=s1,s2=s2)}")
# 결과: 25.80645161290323 (여전히 낮음 ... 이유는 'wd' != 'Western Digital'이기 때문임.)

[실전 구현] 전처리 + 매칭 로직

이제 실제 프로젝트에 적용했던 방식을 알아보겠습니다.

동의어 사전(Synonym Dict) 을 이용하여 약어를 풀네임으로 치환한 뒤, rapidfuzz를 돌리는 방식입니다.

# 동의어 사전: 실무에서 자주 쓰는 약어를 정의
SYNONYMS = {
    "wd": "western digital",
    "nic": "nic", # 그대로 유지
    "dell": "dell",
    "ent": "enterprise"
}

def normalize_text(text):
    """텍스트를 소문자로 변환하고, 동의어를 치환합니다."""
    text = text.lower()
    for key, value in SYNONYMS.items():
        # 단어 경계(\b)를 기준으로 치환 (wd -> western digital)
        text = re.sub(r'\b' + re.escape(key) + r'\b', value, text)
    return text

def find_best_match(user_input, catalog_list):
    """
    RapidFuzz를 이용해 가장 적합한 모델을 찾습니다.
    """
    # 1. 입력값 정규화
    clean_input = normalize_text(user_input)
    
    # 2. 비교 대상(Catalog)의 이름들만 추출하여 리스트 생성
    # (실제로는 catalog 로딩 시 미리 정규화해두면 더 빠릅니다)
    choices = {idx: normalize_text(item["name"]) for idx, item in enumerate(catalog_list)}
    
    # 3. extractOne: 가장 유사한 하나를 추출
    # scorer=fuzz.token_set_ratio: 순서가 섞여도 잘 찾음
    result = process.extractOne(clean_input, choices, scorer=fuzz.token_set_ratio)
    
    if result:
        match_text, score, key = result
        
        # 임계값(Threshold) 설정: 50점 이상만 매칭으로 인정
        if score >= 50:
            matched_item = catalog_list[key]
            return matched_item, score
            
    return None, 0

테스트 및 결과

작성한 로직을 돌려볼게요.

print(f"{'Input Data':<20} | {'Matched Name':<30} | {'Score':<5} | {'Price'}")
print("-" * 75)

for inp in db_inputs:
    matched_item, score = find_best_match(inp, api_catalog)
    
    if matched_item:
        print(f"{inp:<20} | {matched_item['name']:<30} | {score:<5.1f} | {matched_item['price']:,}원")
    else:
        print(f"{inp:<20} | {'(매칭 실패)':<30} | {score:<5.1f} | -")

실행 결과

---------------------------------------------------------------------------
ssd wd 1T            | [SSD]Western Digital Blue 1T   | 84.0  | 100,000원
samsung 500g ssd     | [SSD]Samsung 870 EVO 500G      | 63.4  | 80,000원
intel 10g nic        | [10G NIC]Intel X550-T2         | 57.1  | 250,000원
hpe DL20 Gen11       | (매칭 실패)                        | 0.0   | -

마무리

이번 프로젝트를 통해 "이미 있는 도구를 잘 쓰자" 는 교훈을 얻었습니다.

  • 정규식: 패턴이 명확할 때 좋음 (이메일, 전화번호).
  • Fuzzy Matching (RapidFuzz): 오타, 약어, 순서 변경 등 "비정형 텍스트" 비교에 최강.

Loading script...