Download backend/app/article_fetchers/generic.py from jackmouse/videoNote: direct link, hf CLI and curl.
- Browser
- Download file 4.31 kB
-
https://huggingface.co/spaces/jackmouse/videoNote/resolve/main/backend/app/article_fetchers/generic.py
- Command line
-
hf download hf://spaces/jackmouse/videoNote/backend/app/article_fetchers/generic.py
-
curl -L -o generic.py https://huggingface.co/spaces/jackmouse/videoNote/resolve/main/backend/app/article_fetchers/generic.py
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("通用网页暂不支持发布者订阅,请粘贴具体文章链接") | |