jb-marker / app /core /article_fetch.py
Doss-8b-instruct's picture
Upload folder using huggingface_hub
35a6f44 verified
Raw History Blame Contribute Delete
2.75 kB
"""리서치 인용 URL에서 기사 본문을 best-effort로 스크랩한다(실모드용).
브레인스토밍 Stage A의 web_search 인용은 url/title/snippet(한 줄)만 준다. 파일 트리의
research 산출물이 한 줄짜리로 빈약해 보이지 않도록, 가능하면 원문 기사 본문을 긁어 채운다.
의존: httpx(이미 백엔드 의존성). 표준 라이브러리 HTMLParser로 태그를 제거(추가 의존 없음).
실패(타임아웃·비-HTML·네트워크)는 빈 문자열을 돌려 호출부가 snippet으로 폴백하게 한다.
mock 경로는 fixture가 body를 직접 제공하므로 이 함수를 타지 않는다(결정성 보존).
"""
from __future__ import annotations
import logging
import re
from html.parser import HTMLParser
logger = logging.getLogger(__name__)
# 본문에서 제외할 비가시/구조 태그.
_SKIP_TAGS = {"script", "style", "noscript", "head", "nav", "footer", "header", "svg"}
_BLOCK_TAGS = {"p", "div", "section", "article", "li", "br", "h1", "h2", "h3", "h4", "tr"}
_MAX_CHARS = 6000 # 산출물이 비대해지지 않도록 본문 상한.
class _TextExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__()
self._skip = 0
self.parts: list[str] = []
def handle_starttag(self, tag, attrs):
if tag in _SKIP_TAGS:
self._skip += 1
elif tag in _BLOCK_TAGS:
self.parts.append("\n")
def handle_endtag(self, tag):
if tag in _SKIP_TAGS and self._skip:
self._skip -= 1
elif tag in _BLOCK_TAGS:
self.parts.append("\n")
def handle_data(self, data):
if not self._skip and data.strip():
self.parts.append(data)
def _html_to_text(html: str) -> str:
p = _TextExtractor()
try:
p.feed(html)
except Exception:
return ""
text = "".join(p.parts)
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\n\s*\n\s*\n+", "\n\n", text) # 빈 줄 3개+ → 2개
return text.strip()
def fetch_article_text(url: str, *, timeout: float = 8.0) -> str:
"""url의 기사 본문 텍스트(태그 제거)를 반환. 실패 시 빈 문자열."""
if not url or not url.startswith("http"):
return ""
try:
import httpx
r = httpx.get(url, timeout=timeout, follow_redirects=True,
headers={"User-Agent": "Mozilla/5.0 (compatible; JBMarker/1.0)"})
if r.status_code != 200 or "html" not in r.headers.get("content-type", "").lower():
return ""
text = _html_to_text(r.text)
return text[:_MAX_CHARS] if text else ""
except Exception:
logger.debug("article fetch 실패: %s", url, exc_info=True)
return ""