포스트

[한국 전통주 RAG] 2편: 데이터 크롤링과 파싱

[한국 전통주 RAG] 2편: 데이터 크롤링과 파싱

한국 전통주 RAG 프로젝트 2편: 데이터 크롤링과 파싱

RAG 시스템을 만들려면 데이터 확보가 가장 먼저다.

나는 우연한 계기로 잘 정리된 데이터를 찾았으니 반은 온 셈이다.

이번 편에서는 내가 어떻게 한국술 고문헌 DB에서 데이터를 긁어왔는지, 그리고 그 데이터를 어떤 방식으로 파싱했는지 정리해보려고 한다.


1. 처음엔 막막했던 구조

사이트 메인에 들어갔을 때는 검색창 하나만 있었다.

“특정 키워드로 검색을 하고 그 결과를 크롤링 해야 하나?” 싶었지만, 무작정 키워드를 넣어 긁는 건 비효율적이라 포기했다.


2. 상세 주방문 페이지 발견

대신 “상세 주방문” 페이지를 찾아냈다.

여기서는 셀렉트 박스로 문헌을 고르면, 그 문헌에 수록된 술 정보가 한 번에 표시되는 구조였다.

셀레니움으로 간편하게 셀렉트 박스 값들을 순회하며, 크롤링을 진행 가능하다는 것을 이전에 배워둔 적이 있기 때문에 이 페이지를 활용하면 되겠다는 생각이 들었다.

그래서 실제로 셀레니움을 이용해 각 문헌을 선택 → 결과 페이지 HTML을 저장하는 방식으로 전체 데이터를 수집했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from selenium import webdriver
from selenium.webdriver.support.ui import Select

def get_book_options(driver):
    sel = Select(driver.find_element("name", "book"))
    return [
        {"value": opt.get_attribute("value"), "text": opt.text}
        for opt in sel.options
        if opt.get_attribute("value") not in ("", "@")
    ]

def save_current_html(driver, label, suffix):
    fname = f"{label}__{suffix}.html"
    with open(fname, "w", encoding="utf-8") as f:
        f.write(driver.page_source)
    return fname

3. 파싱을 하며 생긴 고민

결론적으로 셀렉트 박스를 순회하며 각 서적에 포함된 레시피들을 담은 HTML은 성공적으로 크롤링했다.

HTML을 모았으니 나니 이제 파싱을 해야 했다.

그런데 데이터를 까보니 생각보다 단순하지 않았다.

앞서 말했듯 한국술 고문헌 DB에는 술 이름과 레시피뿐 아니라 다양한 정보가 있다.

  • 고려시대 군인들에게 술을 공급하던 관청 기록
  • 앵무조개로 만든 술잔 같은 물품 정보
  • “신 술 고치는 법” 같은 양조 방법

즉, 모든 데이터가 술은 아니다.

이걸 어떻게 /누룩/기타로 나눌지 고민이 컸다.

카테고리만 보자니 분류가 굉장히 많고 분명 술 레시피지만 카테고리 안에 누룩이 포함되는 경우가 많았다.

이름만으로 구분하기엔 엣지 케이스(예: 구주불비법 / 법주)가 있었고,

그러다 중요한 규칙을 발견했다.

카테고리의 첫 번째 값이 항상 가장 큰 분류(발효주/과하주/혼성주 같은 주류 or 누룩/기타)를 의미한다.

그래서 parse_koreansool.py에서는 첫 번째 카테고리를 기준으로 사이트 메인에 있던 분류 기준인 sool, nuruk, other 세 가지로 정리했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LEVEL1_TO_MAIN = {
    "발효주": "sool",
    "증류주": "sool",
    "과하주": "sool",
    "혼성주": "sool",
    "누룩":   "nuruk",
    "기타":   "other",
}

def derive_main_category(liq, hanja, source_categories):
    level1 = source_categories[0] if source_categories else None
    if level1 in LEVEL1_TO_MAIN:
        return LEVEL1_TO_MAIN[level1]
    return "other"

코드에서는 레벨1 카테고리 즉, 가장 첫번째 카테고리를 기준으로 잡아 데이터를 분류하고 나머지를 저장한다.

이를 통해 메인 페이지에 있던 (술 2,876, 누룩 193, 기타 450) 카테고리에 해당하는 데이터들을 100%의 정확도로 성공적으로 분류할수 있었다.


4. 결과 구조

최종적으로 각 레코드는 대략 다음과 같은 JSON 구조를 가진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
  "doc_id": "string",
  "book": "string",
  "title": "string",
  "liq": "string",
  "hanja": "string|null",
  "description": "string|null",
  "main_category": "sool",
  "entry_type": "sool",
  "text": "string|null",
  "metadata": {
    "layout": "string|null",
    "page_title": "string|null",
    "order": "number|null",
    "source_categories": ["string", "..."],
    "category_levels": {
      "level1": "string",
      "level2": "string|null",
      "level3": "string|null",
      "level4": "string|null"
    },
    "external_links": {
      "문헌 정보": "string",
      "상세 주방문": "string"
    },
    "grid_headers": ["string", "..."],
    "steps": [
      {
        "단계": "string",
        "일": "number|null",
        "발효": "string|boolean|null",
        "멥쌀": "number|null",
        "찹쌀": "number|null",
        "물": "number|null",
        "가공": "string|null",
        "누룩": "number|null",
        "누룩형태": "string|null",
        "메모": "string|null",
        "...": "..."
      }
    ],
    "memo_free": "string|null",
    "original": "string",
    "translation": "string",
    "year_guess": "number|null",
    "is_beverage": "boolean"
  }
}

어차피 이후에 전처리를 한번 할 예정이기에 HTML에서 가능한 많은 데이터를 추출하고자 했다.


5. 정리

  • 크롤링 단계: crawler.py로 셀렉트 박스를 순회하며 문헌별 HTML 저장
  • 파싱 단계: parse_koreansool.py로 1분류 카테고리 기반 분류 (sool/nuruk/other)
  • 산출물: 구조화된 JSON 데이터셋

전체 크롤링 & 파싱 로직은 GitHub repo에 정리해 두었다.


이제 기본 데이터셋이 확보되었으니, 다음 편에서는 이 데이터 중 필요한 데이터만 고르고 텍스트를 RAG 학습에 맞게 전처리하는 과정을 다뤄보려 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.