이 글의 배경이 되는 이야기는 8화: “정제수, 설탕, 산도조절제(구연산)“에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 성분표 텍스트에서 개별 원재료를 추출하는 4단계 전략
- 구분자가 5종류일 때 왜 단순한 분리가 안 되는지
- “원재료가 아닌 것”을 걸러내는 필터링의 범위
문제: 긴 문자열 하나, 규칙 없음
API에서 받아온 원재료 데이터는 이런 형태입니다:
정제수, 밀가루(밀:미국산, 호주산), 설탕, L-글루타민산나트륨(향미증진제),
코코아버터 5.2%, 합성향료(바닐린)
사람이 읽으면 원재료 목록이 보입니다. 하지만 프로그램에게 이건 그냥 글자들의 나열입니다. 이 문자열에서 “정제수”, “밀가루”, “설탕” 같은 개별 원재료명을 뽑아내야 합니다.
쉼표로 나누면 될 것 같지만, 실제 데이터에서는 그게 통하지 않았습니다.
구분자 5종류
98만 건의 원재료 텍스트를 살펴보니, 원재료를 구분하는 기호가 5종류였습니다.
| 구분자 | 예시 | 비고 |
|---|---|---|
쉼표 , | 정제수, 설탕, 소금 | 가장 흔함 |
가운뎃점 · | 정제수·설탕·소금 | 일부 제조사 |
슬래시 / | 정제수/설탕/소금 | 간혹 사용 |
세미콜론 ; | 정제수; 설탕; 소금 | 드물지만 존재 |
줄바꿈 \n | 정제수(줄바꿈)설탕 | 포장지 텍스트 그대로 입력 |
같은 제품 안에서도 구분자가 섞여 나오는 경우가 있었습니다. “정제수, 설탕·과당/올리고당” — 이런 식으로요.
그래서 이 5가지를 모두 구분자로 인식하되, 괄호 안에 있는 구분자는 무시해야 했습니다.
왜 단순 분리가 안 되는가
밀가루(밀:미국산, 호주산)
이 텍스트를 쉼표로 나누면 “밀가루(밀:미국산”과 “호주산)“으로 잘립니다. 괄호가 깨집니다.
괄호 안의 쉼표는 원재료를 구분하는 것이 아니라, 원산지를 나열하는 것입니다. 구분자와 구분자가 아닌 것을 구별하려면, 지금 괄호 안에 있는지 밖에 있는지를 추적해야 합니다.
이 괄호 추적이 파싱의 핵심이고, 다음 글에서 더 깊이 다루겠습니다.
4단계 전략
이 문제를 풀기 위해 파싱을 4단계로 나눴습니다.
1단계: 분해 — 괄호를 보존하면서 주요 구분자로 분리
글자를 하나씩 읽으면서 괄호의 깊이를 추적합니다. 깊이가 0일 때만 구분자로 인식하고 잘라냅니다.
입력: "정제수, 밀가루(밀:미국산, 호주산), 설탕"
깊이 0 → "정제수" (쉼표에서 자름)
깊이 0→1→0 → "밀가루(밀:미국산, 호주산)" (괄호 안 쉼표는 무시)
깊이 0 → "설탕"
2단계: 분리 — 괄호 안쪽과 바깥쪽을 나누기
1단계에서 나온 각 조각을 다시 분석합니다. “밀가루(밀:미국산, 호주산)“에서 괄호 바깥의 “밀가루”와 괄호 안의 “밀:미국산, 호주산”을 분리합니다.
괄호 안의 내용은 다시 구분자로 쪼개고, 콜론이 있으면 콜론 앞뒤도 분리합니다.
3단계: 필터 — 원재료가 아닌 것 제거
2단계까지 거치면 “밀가루”, “밀”, “미국산”, “호주산”, “설탕” 같은 조각들이 나옵니다. 이 중 “미국산”, “호주산”은 원재료가 아니라 원산지 정보입니다.
이런 것들을 걸러내는 패턴 목록을 만들었습니다. (이 필터링 자체가 하나의 주제여서, skip_patterns 설계기에서 별도로 다룹니다.)
4단계: 정제 — 최종 정리
앞뒤 공백 제거, 마침표 제거, 1글자 항목 제외(오매칭 소지), 50글자 초과 항목 제외, 중복 제거.
1글자 필터는 의도적인 선택입니다. “물”이나 “밀”처럼 정당한 1글자 원재료가 있지만, 괄호 분해 과정에서 발생하는 의미 없는 1글자 파편(“s”, “p” 등)이 더 많았습니다. 정확한 것 몇 개를 놓치더라도, 잘못된 것 수백 개를 걸러내는 쪽을 택했습니다.
갈림길: 파싱 순서
처음에는 “먼저 전부 쪼개고, 나중에 걸러내자”는 방식이었습니다. 그런데 이렇게 하면 사전에 괄호 포함 이름으로 등록된 원재료까지 쪼개버리는 문제가 생겼습니다.
| 방식 | 동작 | 문제 |
|---|---|---|
| 무조건 분해 우선 | 모든 괄호를 열어서 내용물 분리 | ”합성향료(바닐린)“이 “합성향료”와 “바닐린”으로 분리됨 |
| 무조건 보존 우선 | 괄호를 아예 건드리지 않음 | 하위 원재료를 놓침 |
| 사전 먼저, 분해는 나중에 | 사전에 있으면 통째로 유지, 없으면 분해 | 초기 로딩 비용 발생 |
세 번째를 선택했습니다.
원재료 표준 사전을 미리 로드하고, 괄호가 포함된 이름이 사전에 통째로 있으면 쪼개지 않습니다. 사전에 없는 경우에만 괄호를 열어서 안쪽을 분석합니다.
결과
| 항목 | 수치 |
|---|---|
| 파싱 대상 | 98만 건의 원재료 원문 |
| 인식하는 구분자 | 5종류 (쉼표, 가운뎃점, 슬래시, 세미콜론, 줄바꿈) |
| 괄호 유형 | 4종류 ( (), [], {}, 【】 ) |
| 파싱 단계 | 4단계 (분해→분리→필터→정제) |
한계
파싱은 100% 정확하지 않습니다. 제조사가 자유롭게 입력한 텍스트를 규칙 기반으로 처리하는 것이라, 예외는 계속 나타납니다. 100건을 돌리면 새 패턴이 발견되고, 그걸 처리하면 또 다른 패턴이 나오는 반복이었습니다.
1글자 원재료를 전부 제외합니다. “물”, “밀” 같은 정당한 원재료를 놓치는 대가입니다. 현재로서는 이 trade-off를 감수하고 있습니다.
한 줄 교훈
비정형 텍스트 파싱에서 가장 중요한 것은 “쪼개는 것”이 아니라, “쪼개면 안 되는 것을 쪼개지 않는 것”입니다.
다음 글: 괄호 깊이 3단계 파싱기 만들기