Spaces:
Sleeping
Sleeping
| """ | |
| Web Scraper Tool - Crawl nội dung từ URL. | |
| Hỗ trợ nhiều chiến lược scraping cho báo Việt Nam. | |
| """ | |
| import requests | |
| from bs4 import BeautifulSoup # type: ignore[import-untyped] | |
| from langchain_core.tools import tool # type: ignore[import-untyped] | |
| from utils.logger import get_logger, log_agent_step | |
| logger = get_logger("Tools.WebScraper") | |
| HEADERS = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/122.0.0.0 Safari/537.36" | |
| ), | |
| "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", | |
| "Accept-Language": "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7", | |
| "Accept-Encoding": "gzip, deflate, br", | |
| "Connection": "keep-alive", | |
| "Upgrade-Insecure-Requests": "1", | |
| } | |
| REMOVE_TAGS = [ | |
| "script", "style", "nav", "footer", "header", | |
| "aside", "iframe", "noscript", "form", "button", | |
| "figure", # ảnh caption thường là noise | |
| ] | |
| # Các selector đặc trưng của báo Việt Nam | |
| VIET_ARTICLE_SELECTORS = [ | |
| # VnExpress | |
| {"class": "fck_detail"}, | |
| {"class": "article-body"}, | |
| # Tuổi Trẻ | |
| {"class": "detail-content"}, | |
| {"class": "content-detail"}, | |
| # Thanh Niên | |
| {"class": "detail-content-body"}, | |
| # Dân Trí | |
| {"class": "singular-content"}, | |
| {"class": "news-content"}, | |
| # Generic | |
| {"itemprop": "articleBody"}, | |
| {"class": "article__body"}, | |
| {"class": "post-content"}, | |
| {"class": "entry-content"}, | |
| {"id": "article-body"}, | |
| {"id": "content"}, | |
| ] | |
| def _extract_content(soup: BeautifulSoup) -> str: | |
| """ | |
| Trích xuất nội dung sạch từ HTML. | |
| Thử nhiều selector theo thứ tự ưu tiên. | |
| """ | |
| from bs4 import Tag # type: ignore[import-untyped] | |
| # Xóa noise trước | |
| for tag_name in REMOVE_TAGS: | |
| for tag in soup.find_all(tag_name): | |
| if isinstance(tag, Tag): | |
| tag.decompose() | |
| # Thử các selector đặc trưng của báo Việt Nam | |
| main_content: Tag | None = None | |
| for selector in VIET_ARTICLE_SELECTORS: | |
| found = soup.find(attrs=selector) # type: ignore[arg-type] | |
| if isinstance(found, Tag): | |
| main_content = found | |
| break | |
| # Fallback: article → main → body | |
| if main_content is None: | |
| for fallback in ("article", "main", "body"): | |
| found = soup.find(fallback) | |
| if isinstance(found, Tag): | |
| main_content = found | |
| break | |
| if main_content is None: | |
| return "" | |
| # Thu thập text từ các đoạn | |
| paragraphs = main_content.find_all(["p", "h1", "h2", "h3", "h4", "li"]) | |
| text_parts = [] | |
| for p in paragraphs: | |
| if not isinstance(p, Tag): | |
| continue | |
| text = p.get_text(separator=" ", strip=True) | |
| if text and len(text) > 30: # Bỏ qua text quá ngắn | |
| text_parts.append(text) | |
| content = "\n\n".join(text_parts) | |
| # Nếu vẫn rỗng, lấy toàn bộ text của main_content | |
| if not content: | |
| content = main_content.get_text(separator="\n", strip=True) | |
| return content | |
| def web_scrape(url: str) -> dict: | |
| """ | |
| Crawl và trích xuất nội dung text từ một URL. | |
| Hỗ trợ các trang báo Việt Nam (VnExpress, Tuổi Trẻ, Thanh Niên, Dân Trí...). | |
| Tự động xử lý encoding tiếng Việt và loại bỏ HTML/scripts/ads. | |
| Args: | |
| url: URL cần crawl nội dung | |
| Returns: | |
| Dict chứa: url, title, content (clean text), success (bool) | |
| """ | |
| log_agent_step(logger, "WebScraper", "Scraping", f"URL: {url}") | |
| try: | |
| response = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True) | |
| response.raise_for_status() | |
| # VnExpress và nhiều báo Việt Nam dùng UTF-8 | |
| if response.encoding and response.encoding.lower() in ("iso-8859-1", "latin-1"): | |
| # Sai encoding, thử detect lại | |
| response.encoding = response.apparent_encoding or "utf-8" | |
| else: | |
| response.encoding = response.encoding or "utf-8" | |
| soup = BeautifulSoup(response.text, "html.parser") | |
| # Lấy title | |
| title = "" | |
| if soup.title and soup.title.string: | |
| title = soup.title.string.strip() | |
| # Trích xuất nội dung | |
| content = _extract_content(soup) | |
| # Giới hạn 10000 ký tự — đủ context để LLM suy luận sâu | |
| if len(content) > 10000: | |
| content = content[:10000] + "\n\n[... nội dung đã được cắt ngắn ...]" | |
| log_agent_step( | |
| logger, "WebScraper", "Done", | |
| f"Title: {title[:80]} | Content: {len(content)} chars" | |
| ) | |
| return {"url": url, "title": title, "content": content, "success": True} | |
| except requests.exceptions.Timeout: | |
| logger.warning(f"[WebScraper] Timeout: {url}") | |
| return {"url": url, "title": "", "content": "", "success": False, "error": "Timeout"} | |
| except requests.exceptions.RequestException as e: | |
| logger.warning(f"[WebScraper] Error: {url} — {e}") | |
| return {"url": url, "title": "", "content": "", "success": False, "error": str(e)} | |
| except Exception as e: | |
| logger.error(f"[WebScraper] Unexpected error: {e}") | |
| return {"url": url, "title": "", "content": "", "success": False, "error": str(e)} | |