Notice
Recent Posts
Recent Comments
반응형
관리 메뉴

HM소프트

크롤러 데이터 검증·검색·스케줄러·데스크톱앱화 — 공개 데이터 크롤러 (4) 본문

기술 개발일지

크롤러 데이터 검증·검색·스케줄러·데스크톱앱화 — 공개 데이터 크롤러 (4)

HM소프트 2026. 6. 11. 17:06
반응형

오번역을 막는 번역 필요 여부 가드와 스레드 로컬 번역기 격리, 429 분기 백오프, 원문·번역 칸을 모두 훑는 다필드 LIKE 검색과 이스케이프, 화면과 일치하는 내보내기, 국가별 시각 분산과 직렬화 락 스케줄러, APScheduler 잠자기 방어, UNIQUE 마이그레이션, Electron 포트 폴백·트레이 상주까지 데스크톱 앱화 기록.

 

4편: 검색·번역·스케줄러·데스크톱

시리즈 마지막이다. 1편 파이프라인, 2편 PDF 파싱, 3편 강건성에 이어, 이번엔 사용자가 실제로 만지는 부분과 운영 자동화를 한 번에 다룬다.

번역 — "되게"보다 "틀리지 않게"

수집한 외국어 원문은 googletrans로 번역했다. 네트워크 작업이라 실패가 잦아, 재시도와 백오프를 넣었다.

def translate_with_retry(text, retries=3):
    for i in range(retries):
        try:
            return translator.translate(text, dest="ko").text
        except Exception:
            time.sleep(2 ** i)   # 1s, 2s, 4s 백오프
    return text                  # 끝내 실패하면 원문 유지

그런데 실무에서 진짜 문제는 번역이 안 되는 것이 아니라 엉뚱하게 되는 것이었다. 대표적으로 짧은 영문 브랜드명이 일반 단어로 오역됐다(예: 짧은 영문 토큰을 뜻 있는 단어로 바꿔버림). 고유명사가 번역되면 데이터가 오염된다.

그래서 번역 차단 가드를 넣었다. 짧은 영문 토큰·브랜드명으로 판단되는 값은 번역 대상에서 빼고 원형을 유지했다.

def should_skip_translation(s: str) -> bool:
    t = s.strip()
    if len(t) <= 3 and t.isascii():      # 짧은 영문 약어/브랜드
        return True
    if t.isupper() and t.isascii():       # 대문자 약어
        return True
    return False

19개 필드 검색

검색은 19개 항목을 대상으로 한다. 처음엔 제목 위주였는데, 고객 피드백을 받아 실제로 찾고 싶어 하는 항목들을 키워드 검색에 포함시켰다. 결함 원인(defect_cause), 영향(consequence), 조치(remedy), 부품(component), 리콜 번호(recall_number) 등이다.

검색은 선택된 필터(기간·국가 등) 조건과 결합되며, 결과는 화면 표시와 내보내기에서 동일하게 적용된다.

데이터 정합성: DB UNIQUE 마이그레이션

자동 수집을 매일 돌리면 같은 데이터가 반복 수집된다. 3편에서 "중복은 시각이 아닌 고유 키로 판정"한다고 했는데, 그 고유 키에 DB UNIQUE 제약을 걸어 중복 저장을 원천 차단했다. 기존 DB에는 마이그레이션으로 적용했고, 개발 중 데이터를 갈아엎을 수 있도록 reset 엔드포인트도 뒀다.

저장은 INSERT ... ON CONFLICT로 처리해, 새 데이터는 넣고 기존 데이터는 갱신하는 upsert가 되게 했다.

내보내기 — 필터를 그대로 반영

CSV/Excel 내보내기에서 중요한 건, 화면에서 좁혀 본 결과와 내려받은 파일이 같아야 한다는 점이다. 그래서 export가 현재 적용된 필터 조건을 그대로 반영하게 했다. 번역본이 export에서 누락되던 버그도 함께 잡았다.

자동 스케줄러 — APScheduler

운영은 매일 자동으로 돌아야 한다. APScheduler로 일일 수집을 걸되, 핵심은 국가별로 수집 시각을 분산한 것이다. 모든 나라를 같은 시각에 한꺼번에 긁으면 부하가 몰리고, 한 곳이 막히면 줄줄이 밀린다.

scheduler = BackgroundScheduler(timezone="UTC")
for i, country in enumerate(COUNTRIES):
    scheduler.add_job(
        crawl_country, "cron", args=[country],
        hour=2, minute=i * 10,     # 나라마다 10분씩 어긋나게
    )

데스크톱 앱으로 마무리 — Electron + React

상시 도는 도구라 데스크톱 경험을 챙겼다. 백엔드(FastAPI)는 로컬에서 돌고, 그 위에 Electron + React로 5개 화면과 트레이를 올렸다.

  • 트레이 상주 + 알림: 백그라운드로 돌며 수집 결과를 트레이 알림으로.
  • 윈도우 자동 시작: 부팅 시 자동 실행.
  • 즉시 수집: 스케줄을 기다리지 않고 바로 한 번 돌리는 버튼.
  • 반응형 UI, 날짜 포맷, 전각 정규화(3편), 에러 처리 정리.

프론트엔드는 API 클라이언트로 백엔드와만 통신한다. "수집·파싱·번역은 백엔드, 화면은 프론트"라는 경계를 끝까지 지켜, 한 대의 PC에서 도는 자족적인 앱이 됐다.

시리즈를 마치며

네 편을 관통한 교훈은 "크롤링의 본질은 HTTP가 아니라 데이터의 비일관성과 싸우는 일"이라는 점이다.

  • 1편: 다양한 소스를 추상 클래스로 흡수
  • 2편: 복잡한 포맷은 흐름이 아닌 구조로 파싱
  • 3편: 환경 차이를 명시적으로 못박아 재현 안 되는 버그 제거
  • 4편: 번역은 틀리지 않게, 중복은 키로 차단, 운영은 분산 스케줄로

수집 대상이 늘어도 베이스 크롤러 하나만 더 얹으면 되고, 데이터 품질을 지키는 장치들이 파이프라인에 녹아 있어 확장이 두렵지 않은 구조가 된 점이 가장 만족스러웠다.

반응형