크롤링으로 매물 데이터를 긁어모으면 항상 같은 벽에 부딪힌다. 리스트 페이지에서 제목, 가격, 본문 세 칸을 얻긴 했는데, 정작 내가 분석하고 싶은 값은 그 안에 자연어(natural language)로 뭉개져 있다는 것. “아이템 300개 일괄 50만원 (분할가능/현거래X)” 같은 문장에서 수량 300, 총액 500000, 조건들을 각각 뽑아내야 비로소 표가 된다. 오늘은 이 지저분한 텍스트를 구조화하는 정규식 레시피를 정리했다.

왜 곧바로 int()로 못 바꾸나?

처음엔 나도 순진했다. 가격 칸에서 숫자만 떼면 되지 않나? 그런데 실제 데이터를 열어보니 이런 것들이 섞여 있었다.

samples = [
    "골드 1억 5천 팝니다 개당 8원",   # 단위어 혼합
    "50만  일괄",                     # 만 단위 축약
    "500,000원 / 분할가능",           # 콤마 + 조건
    "5,0000원",                       # 오타(자릿수 붕괴)
    "가격은 쪽지주세요",              # 숫자 자체가 없음
]

int() 하나로는 절대 처리 못 한다. 그래서 나는 파싱을 한 방에 하지 않고, 아래처럼 파이프라인(pipeline)으로 쪼갰다.

flowchart LR
    A["① 원문<br/>제목+본문"] --> B["② 정규화<br/>공백·특수문자"]
    B --> C["③ 필드 추출<br/>수량·가격·조건"]
    C --> D["④ 단위 환산<br/>만·억 → 정수"]
    D --> E["⑤ 검증<br/>이상치·결측"]
    E --> F["⑥ 구조화 레코드"]
    classDef a fill:#e7f5ff,stroke:#1c7ed6,color:#10548f;
    classDef b fill:#fff3bf,stroke:#f08c00,color:#a5620a;
    classDef c fill:#d3f9d8,stroke:#2f9e44,color:#1a662a;
    class A,B a
    class C,D b
    class E,F c

단계를 나누면 각 정규식이 짧아지고, 실패했을 때 어느 칸에서 깨졌는지 바로 보인다. 이게 유지보수의 핵심이다.

정규화(normalize)는 왜 먼저 해야 하나?

추출 정규식을 아무리 잘 짜도, 입력이 들쭉날쭉하면 패턴이 폭발한다. 그래서 추출 전에 입력 모양부터 통일한다.

import re
 
def normalize(text: str) -> str:
    text = text.replace(" ", " ")          # 비가시 공백(NBSP) 제거
    text = re.sub(r"[//||]", " / ", text)      # 전각 구분자 통일
    text = re.sub(r"[,,]", "", text)            # 천단위 콤마 삭제
    text = re.sub(r"\s+", " ", text)             # 연속 공백 → 1칸
    return text.strip()

콤마를 먼저 지우는 이유는 뒤의 숫자 정규식을 \d+처럼 단순하게 유지하기 위해서다. 500,000을 그대로 두면 \d{1,3}(,\d{3})* 같은 괴물 패턴을 계속 끌고 다녀야 한다.

수량·가격·조건은 각각 어떤 패턴으로 잡나?

핵심 세 필드를 나눠서 잡는다. 아래 표가 내가 실제로 쓰는 패턴 모음이다.

필드정규식(요지)잡아내는 예주의점
수량(qty)(\d+)\s*(개|장|묶음)300개, 5묶음단위어 화이트리스트로 한정
가격(price)(\d+)\s*(억|천|만)?\s*원50만원, 1억원단위어를 캡처해 환산 단계로 넘김
개당가(unit)개당\s*(\d+)\s*원개당 8원총액과 구분 필수
조건(cond)(분할|일괄|택배|직거래|네고)일괄, 네고findall로 다중 수집
UNIT = {"억": 100_000_000, "천": 1_000, "만": 10_000, None: 1}
 
def parse_price(text: str):
    m = re.search(r"(\d+)\s*(||)?\s*", text)
    if not m:
        return None
    value = int(m.group(1)) * UNIT[m.group(2)]
    return value
 
def parse_conditions(text: str):
    return re.findall(r"(분할|일괄|현거래|택배|직거래|네고)", text)

억|천|만캡처 그룹으로 잡아 딕셔너리로 환산하는 게 포인트다. “50만원”을 문자열 그대로 두면 나중에 정렬·합산이 안 된다. 숫자로 바꿔야 데이터가 된다.

여러 값이 섞여 나올 때 어느 걸 믿나?

여기서 진짜 트러블슈팅이 시작된다. “1억 5천”처럼 복합 단위(억+천)가 붙어 나오면 위 정규식은 앞의 1억만 잡고 5천을 흘린다. 나는 이런 케이스를 세 갈래로 나눠 처리한다.

flowchart TD
    A["가격 후보<br/>추출 결과"] --> B{"단위어<br/>몇 개?"}
    B -->|"0개"| C["① 결측 처리<br/>price=null"]
    B -->|"1개"| D["② 단일 환산<br/>정상 경로"]
    B -->|"2개 이상"| E["③ 복합 합산<br/>억+천+만 누적"]
    E --> F["④ 이상치 검증<br/>개당가×수량 대조"]
    D --> F
    classDef a fill:#e7f5ff,stroke:#1c7ed6,color:#10548f;
    classDef b fill:#ffe3e3,stroke:#e03131,color:#a11515;
    classDef c fill:#d3f9d8,stroke:#2f9e44,color:#1a662a;
    class A,B a
    class C,E b
    class D,F c

복합 단위는 finditer로 모든 (숫자, 단위) 쌍을 훑어 누적한다.

def parse_price_multi(text: str):
    total, found = 0, False
    for m in re.finditer(r"(\d+)\s*(||)", text):
        total += int(m.group(1)) * UNIT[m.group(2)]
        found = True
    return total if found else None
 
# "1억 5천" -> 100_000_000 + 5_000 ... 어? 5천이 5000?

바로 이 지점에서 데이터 검증이 필요하다. “5천”이 5,000원인지 5,000만원(=천만)인지는 문맥마다 다르다. 매물 카테고리(고가재/저가재)별 가격 밴드(band)로 검증해, 밴드를 벗어나면 자동으로 플래그를 세우고 사람이 확인하도록 큐에 넣는다. 정규식만으로 100%를 노리면 반드시 조용히 틀린다.

마지막 검증: 개당가 × 수량 ≈ 총액인가?

내가 가장 애용하는 자기검증 트릭이다. 수량·개당가·총액 세 값이 다 잡혔다면, 이들은 서로 곱셈 관계여야 한다. 어긋나면 어딘가 파싱이 틀린 것이다.

def cross_check(qty, unit_price, total, tol=0.15):
    if not (qty and unit_price and total):
        return "insufficient"
    expected = qty * unit_price
    diff = abs(expected - total) / total
    return "ok" if diff <= tol else "mismatch"

mismatch가 뜬 레코드만 따로 모아 보면, 대개 오타(자릿수 붕괴)나 “개당”과 “일괄”을 헷갈린 케이스다. 전체를 손으로 볼 필요 없이 의심 레코드만 걸러내는 이 필터 하나로 검수 시간이 크게 줄었다.

정리하면

원칙이유
파싱을 파이프라인으로 쪼개라실패 지점이 눈에 보인다
추출 전에 정규화하라패턴 폭발을 막는다
단위어는 캡처해 정수로 환산하라정렬·합산 가능한 데이터가 된다
곱셈 관계로 자기검증하라조용한 오류를 잡는다

정규식은 만능이 아니다. “대부분을 자동으로 잡고, 의심스러운 소수를 사람에게 넘기는” 구조를 설계하는 게 실전 파싱의 전부다. 완벽한 하나의 정규식보다, 틀렸을 때 그 사실을 알려주는 파이프라인이 낫다.

본문의 데이터·수치·매물 텍스트는 모두 합성·더미 예시이며, 특정 서비스나 실거래를 나타내지 않는다. 투자권유나 회계판단이 아니다.