-
Notifications
You must be signed in to change notification settings - Fork 0
crawling
siggu edited this page Nov 27, 2025
·
2 revisions
→ 링크 수집 → HTML 파싱 → LLM 구조화 → 품질 평가
| 방식 | 적용 대상 | 선택 이유 |
|---|---|---|
| requests + BeautifulSoup | 정적 페이지 | ✅ 모든 보건소 사이트 해당 |
| Selenium | 동적 페이지 | ❌ 해당 없음 |
수집하려는 모든 보건소 웹사이트는 정적 페이지로 구성되어 있어
requests와BeautifulSoup사용
- 각 보건소마다 웹사이트 구조가 상이
-
gnb,lnb,sub-menu등 메뉴 구조 제각각 - 하나의 선택자 규칙으로 모든 보건소 커버 불가능
각 보건소마다 개별 크롤링 규칙 정의
CRAWL_RULES = [
{
"name": "동작구 건강관리청 LNB",
"domain": "dongjak",
"main_selector": ".left-area .left-mdp1 > li > a",
"sub_selector": [
".left-mdp1 > li.on > ul > li > a",
".tab-list li a",
],
},
{
"name": "서울시 임신출산정보센터",
"domain": "seoul-agi",
"single_page": True,
"menu_container": "#content-menu",
"main_selector": ".menu-item > .menu-link",
"sub_selector": ".menu-sub-item > a.menu-sub-link",
},
]TAB_SELECTORS = [
".tabmenu ul li a", # 강남구
".tab-list li a", # 동작구
".tab-nav ul li a", # 강북구
".tab_panel ul li button", # 성북구
".content-tab ul li a", # 양천구
]| 설정 항목 | 설명 |
|---|---|
| HTTP 설정 | 각 보건소별 요청 헤더 |
| 쿠키 설정 | 세션 유지를 위한 쿠키 |
| SSL 설정 | 인증서 검증 옵션 |
크롤링을 위한 초기 항목(링크) 수집 단계에서 아래와 같은 문제가 발생했습니다.
문제:
-
dep1(모자보건) →dep2(임신준비 지원) →dep3의 링크가 모두 동일 - 나중 링크 무시 → 제목은
dep1, 내용은dep3로 수집되는 불일치 발생
depth_scores 기반 제목 선택 전략
DISTRICT_CONFIGS: Dict[str, Dict[str, Any]] = {
"은평구": {
"strategy_class": EPStrategy,
"filter_text": "사업안내",
"output_dir": "app/crawling/output/은평구",
"max_workers": MAX_WORKERS_DEFAULT,
"depth_scores": {2: 5000, 3: 10000, 4: 20000}, # 구체성 점수
},
"용산구": {
"strategy_class": YongsanStrategy,
"filter_text": None,
"output_dir": "app/crawling/output/용산구",
"max_workers": MAX_WORKERS_DEFAULT,
"depth_scores": {1: 5000, 2: 10000},
},
}| 핵심 아이디어 | 설명 |
|---|---|
| depth_scores | depth가 증가할수록 점수 증가 |
| 최종 제목 선택 | 가장 높은 depth의 제목 사용 |
보건소에서 제공하는 정보들 중에 취지에 맞는 정보도 있었지만 그렇지 않은 불필요한 정보들도 존재했습니다.
![]() |
![]() |
|---|---|
| 중구 - 보건사업 > 위생 > 공중위생 업종의 정의 | 서초구 - 생애주기건강관리 > 건강도시서초 > 담배연기 제로 서초 > 금연 FAQ > 흡연과태료 의견제출 및 이의제기 안내 |
키워드 블랙리스트 기반 필터링
KEYWORD_FILTER = {
"blacklist": [
# 비건강/의료 관련 키워드
"교육", "캠페인", "건강도시", "조사",
"동물", "사진", "기관정보", "자료실", "정의",
]
}| 단계 | 작업 | 방식 |
|---|---|---|
| 1 | 초기 항목(링크) 수집 | 순차 처리 |
| 2 | 초기 항목 저장 | 선택 사항 |
| 3 | 항목 처리 및 구조화 | ❌ 순차 처리 |
| 4 | 결과 저장 | - |
| 5 | 요약 출력 | - |
| 6 | 완료 후 처리 | - |
수집된 링크에 대해서 하나씩 항목 처리 및 구조화(process_items_for_workflow)를 진행하고 있었기 때문에 매우 느린 속도를 보였습니다.
| 측정 항목 | 값 |
|---|---|
| 처리 항목 수 | 48개 |
| 워크플로우 전체 | 288.85초 |
| 평균 페이지 처리 | 4.78초 |
문제: 최대 160개 항목 보건소는 20분 이상 소요
ThreadPoolExecutor 기반 병렬 처리
concurrent.futures.ThreadPoolExecutor을 이용하여 링크들을 병렬로 처리하도록 변경했습니다.
| 측정 항목 | Before | After | 개선율 |
|---|---|---|---|
| 워크플로우 전체 | 288.85초 | 81.18초 | 3.5배 |
| 평균 페이지 처리 | 4.78초 | 4.91초 | 유사 |
원본 텍스트에서 지원 대상과 지원 내용을 요약하여 사용자에게 제공
원본 HTML (raw_text)
↓
텍스트 전처리
↓
정제된 텍스트 (최대 20만 자)
↓
1단계: 요약 생성
↓
구조화된 데이터 (title, support_target, support_content)
↓
2단계: 품질 평가
↓
최종 데이터 + 품질 점수 (eval_target, eval_content)
본문 HTML에서 불필요한 요소들은 제외하고 메인 컨텐츠를 찾아 정책 정보만 추출하게 합니다.
| 카테고리 | 선택자 |
|---|---|
| 네비게이션 | nav, header, footer, .sidebar, .menu |
| 광고 | .ad, .advertisement |
| 스크립트 | script, style, noscript |
| 팝업 | .cookie-banner, .popup |
for selector in [
"main", "#content", "#main", ".content",
".main-content", ".contentArea", ".content-area",
"article", ".article", "[role='main']",
]:
content_area = soup_copy.select_one(selector)
if content_area:
break정책 정보가 테이블(표) 형태로 제공되는 경우가 많아, LLM이 조금이라도 이해하기 쉽도록 테이블을 별도로 추출하여 [표 시작] ~ [표 끝] 형태로 포맷팅합니다.
for table in content_area.find_all("table"):
table_lines = ["[표 시작]"]
# 헤더 추출
headers = [th.get_text(strip=True) for th in table.find_all("th")]
if headers:
table_lines.append(" | ".join(headers))
table_lines.append("-" * len(" | ".join(headers)))
# 행 추출
for row in table.find_all("tr"):
cells = [cell.get_text(strip=True) for cell in row.find_all(["td", "th"])]
if cells:
table_lines.append(" | ".join(cells))
table_lines.append("[표 끝]\n")| 항목 | 내용 |
|---|---|
| 목적 | 비정형 텍스트에서 구조화된 정보 추출 |
| 입력 | 정제된 HTML 텍스트 (최대 20만 자), 제목 힌트 |
| 출력 | title, support_target, support_content |
class _LLMSummary(BaseModel):
title: Optional[str] = None
support_target: str
support_content: str| 규칙 | 설명 |
|---|---|
| 필드 분리 | support_target ↔ support_content 분리 |
| 정보 정확성 | 원문에 없는 조건/수치 생성 금지 |
| 대상 정보 없음 | support_target='정보 없음' |
| 항목 | 내용 |
|---|---|
| 목적 | 요약문의 품질 점수 부여 |
| 입력 | 요약된 지원 대상, 요약된 지원 내용 |
| 출력 | eval_target (1 |
class _LLMEval(BaseModel):
eval_target: int = Field(ge=1, le=10, description="지원 대상 품질")
eval_content: int = Field(ge=0, le=10, description="지원 내용 품질")| 점수 | 기준 |
|---|---|
| 1 | 정보 없음 |
| 2 | 일반 거주 조건 (지역주민/서울시민) |
| 3 | 단일 정성 조건 (청년/노인/장애/임산부) |
| 4 | 거주 조건 + 단일 정성 조건 |
| 5 | 단일 정량 조건 (소득/병명/기간) |
| 6 | 정성 조건 2개 |
| 7 | 정량 + 정성 복합조건 |
| 8 | 다중 복합조건 or 정성 조건 4개 |
| 9 | 명시적 행정기준 포함 복합조건 |
| 10 | 예외 조항 포함 복합조건 |
| 점수 | 기준 |
|---|---|
| 0 | 지원내용 없음 |
| 2 | 모호한 서술 ('지원합니다' 등) |
| 4 | 항목은 있으나 금액/기간/횟수 미기재 |
| 6 | 금액/기간/횟수 중 1개 이상 구체 정보 포함 |
| 8 | 지원항목 다수 + 절차/제외조건 등 복수 요소 |
| 10 | 금액/횟수/기간/절차/예외 모두 구체적으로 명시 |

