zhoujiaangyao
deploy videomemo backend to HF Space
6cfe55f
Raw
History Blame Contribute Delete
4.31 kB
from __future__ import annotations
import re
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from app.article_fetchers.base import ArticleContent, ArticleFetchError
from app.utils.url_parser import clean_url
def _clean_text(value: str) -> str:
return re.sub(r"[ \t\r\f\v]+", " ", value or "").strip()
def _normalize_body(value: str) -> str:
lines = [_clean_text(line) for line in (value or "").splitlines()]
return "\n".join(line for line in lines if line)
def _meta_content(soup: BeautifulSoup, *selectors: tuple[str, str]) -> str:
for attr, value in selectors:
node = soup.find("meta", attrs={attr: value})
if node:
content = _clean_text(node.get("content") or "")
if content:
return content
return ""
def _candidate_score(node) -> int:
text = _normalize_body(node.get_text("\n"))
paragraphs = node.find_all("p")
return len(text) + len(paragraphs) * 120
def parse_generic_article_html(html: str, url: str) -> ArticleContent:
soup = BeautifulSoup(html, "html.parser")
for tag in soup(["script", "style", "noscript", "svg", "canvas", "iframe"]):
tag.decompose()
for tag in soup(["nav", "header", "footer", "aside", "form"]):
tag.decompose()
title = (
_meta_content(soup, ("property", "og:title"), ("name", "twitter:title"))
or _clean_text(soup.title.get_text(" ")) if soup.title else ""
)
author = _meta_content(soup, ("name", "author"), ("property", "article:author"))
published_at = _meta_content(
soup,
("property", "article:published_time"),
("name", "publishdate"),
("name", "date"),
)
cover = _meta_content(soup, ("property", "og:image"), ("name", "twitter:image"))
candidates = []
for selector in ("article", "main", "[role='main']", "#content", ".content", ".article", ".post"):
candidates.extend(soup.select(selector))
if not candidates and soup.body:
candidates = [soup.body]
best = max(candidates, key=_candidate_score, default=None)
body = _normalize_body(best.get_text("\n")) if best else ""
if len(body) < 80:
description = _meta_content(soup, ("name", "description"), ("property", "og:description"))
body = description if len(description) > len(body) else body
if len(body) < 40:
raise ValueError("网页正文为空或过短,无法生成总结")
parsed = urlparse(url)
article_id = parsed.netloc + parsed.path
return ArticleContent(
platform="generic_web",
url=url,
article_id=article_id or url,
title=title or parsed.netloc or "网页文章",
author_name=author,
content_text=body,
image_urls=[cover] if cover else [],
cover_url=cover,
published_at=published_at,
raw_metadata={"source": "generic_web"},
)
class GenericArticleFetcher:
platform = "generic_web"
def fetch(self, url: str) -> ArticleContent:
clean = clean_url(url)
try:
response = requests.get(
clean,
timeout=12,
allow_redirects=True,
headers={
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
},
)
response.raise_for_status()
return parse_generic_article_html(response.text, response.url or clean)
except ValueError:
raise
except Exception as exc:
raise ArticleFetchError(f"网页文章抓取失败:{exc}") from exc
def search(self, keyword: str, limit: int = 20) -> list[ArticleContent]:
raise ArticleFetchError("通用网页暂不支持关键字查询,请粘贴具体文章链接")
def fetch_publisher(self, query: str, limit: int = 20) -> list[ArticleContent]:
raise ArticleFetchError("通用网页暂不支持发布者订阅,请粘贴具体文章链接")