자동화 slecs

크롤러 파싱 안정성 개선

목차

크롤러 파싱 안정성 작업을 한 번에 네 군데 손봤다.


배경: 파서는 항상 "정상 케이스"에 맞춰져 있다

콘텐츠 크롤링 파이프라인을 운영하다 보면 어느 순간 반드시 이 문제를 만나게 된다. HTML 소스는 생각보다 훨씬 지저분하다. 이론적으로는 Well-formed XML에 가까운 구조여야 하지만, 실제 퍼블리셔들이 뱉는 마크업은 닫히지 않은 태그, 중첩 오류, 푸터 콘텐츠가 본문 안으로 스며드는 현상이 일상이다.

이번에 crawler_common.py, migrate_content_absolutize.py, publish.py 세 파일을 동시에 건드린 건 그 증상이 파이프라인 전 구간에 걸쳐 나타났기 때문이다. 단순히 파서 하나 고치면 끝나는 문제가 아니었음.


작업 내용

copy_right_wrap 푸터 마커 처리

copy_right_wrap 같은 클래스명은 전형적인 저작권 표기 푸터 래퍼다. 문제는 일부 소스에서 이 블록이 본문 cut 경계보다 앞에 등장하거나, 본문 DOM 트리 안에 인라인으로 끼어들어 오는 경우가 있다는 것. 크롤러가 이걸 "본문의 일부"로 판단하면 저작권 고지, 편집자 서명, 광고 문구 같은 쓰레기가 고스란히 추출 콘텐츠에 들어간다.

이번 수정은 copy_right_wrap를 명시적 푸터 마커로 등록해서, 이 블록을 만나는 순간 본문 추출을 중단하도록 경계를 박은 것. 사소해 보이지만 이런 마커 목록을 관리하는 게 크롤러 품질의 핵심이다.

cut 위치 closing tag 보정

본문 cut 지점이 태그 중간에 걸리는 경우가 있다. 예를 들어 <p> 안에서 cut이 일어나면 이후 HTML이 </p> 없이 잘린 상태로 넘어온다. 다운스트림에서 이걸 렌더링하거나 다시 파싱할 때 레이아웃이 무너지는 원인이 된다.

# before: 단순 슬라이싱
content = raw_html[:cut_index]

# after: cut 후 closing tag 유무 검사 + 보정
content = raw_html[:cut_index]
content = close_open_tags(content)  # 열린 태그 자동 닫음

close_open_tags 류의 유틸을 만들어두면 이후 비슷한 케이스에서 재사용이 가능하다. crawler_common.py에 이 로직을 모은 것도 그 이유.

깨진 태그 자동 닫음

BeautifulSoup의 html.parserlxml은 관대하게 파싱하지만, 그 결과물을 다시 직렬화할 때 예상치 못한 구조가 튀어나오기도 한다. 특히 <br>, <img>, <input> 같은 void element가 아닌 블록 태그들이 닫히지 않은 채 들어오는 경우, 명시적으로 닫아주는 보정 로직을 추가했다.

이건 migrate 쪽 migrate_content_absolutize.py에서도 같이 적용됐는데, 콘텐츠 이관 작업 중에 기존 저장된 HTML이 이미 깨진 상태로 DB에 들어가 있는 케이스가 있어서다. 파이프라인 신규 유입뿐 아니라 레거시 데이터도 커버해야 했음.

page 2/3 fallback

케이스 기존 동작 변경 후 동작
단일 페이지 정상 추출 동일
2페이지 이상, page 1 정상 정상 추출 동일
2/3페이지 파싱 실패 에러 or 빈 콘텐츠 fallback으로 빈 문자열 처리 후 계속 진행
2/3페이지 없음 에러 fallback으로 무시

멀티페이지 아티클에서 2, 3페이지 취득에 실패하면 전체 크롤링 작업 자체가 죽어버리는 구조였다. 이건 너무 가혹한 실패 정책이다. 1페이지만이라도 살려서 넘기는 게 "아무것도 없는 것"보다 낫다는 판단으로 fallback을 추가했다.


회고

이번 작업의 핵심은 "실패 범위를 좁히는 것"이었다. 파이프라인 전체를 죽이는 에러를 국소화하고, 불완전하더라도 다음 단계로 넘길 수 있는 데이터를 최대한 살리는 방향. 크롤러는 외부 HTML이라는 통제 불가능한 입력을 다루는 시스템이기 때문에, "모든 입력이 정상"이라고 가정하는 순간 운영 중에 반드시 터진다.

publish.py가 같이 수정된 것도 이 맥락이다. 파싱 결과가 보정된 이후 발행 단계에서의 렌더링 흐름도 같이 조율해야 했고, 세 파일이 사실상 한 흐름을 이루고 있었기 때문에 한 커밋에서 같이 처리했다.

팀원들에게도 이 케이스를 공유할 때 강조한 부분이 있다. 크롤러 버그는 대부분 "이런 HTML이 올 수 있다"는 가정을 추가할 때마다 발생한다. 마커 목록, closing tag 보정 유틸, fallback 정책 — 이런 것들을 crawler_common.py에 모아두는 이유는 다음 사람이 같은 삽질을 반복하지 않기 위해서다. 공통 모듈이 누적될수록 파서의 방어 수준도 같이 올라간다.

다음은 이 마커 목록을 설정 파일로 분리해서 코드 수정 없이 관리할 수 있게 할 차례다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.