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

개요

최근 사내 서버 비용 관리 대시보드를 만들면서 겪었던 문제를 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): 오타, 약어, 순서 변경 등 “비정형 텍스트” 비교에 최강.

관련 글

지식 공유2분 읽기

[Python] Redis 캐싱으로 API 성능 향상시켜보기

Redis란 무엇인가? Redis가 왜 필요할까? 상황을 가정해봅시다. 데이터베이스 쿼리: 100ms 소요 하루 API 요청: 10,000회 총 대기 시간: 1,000초 => 16분 이 때 만약 Redis를 도입한다면? 첫 요청: 1...

Python