Spaces:
Running
Running
| import logging | |
| import re | |
| import time | |
| from datetime import datetime | |
| from urllib.parse import urljoin, urlparse | |
| import requests | |
| from bs4 import BeautifulSoup | |
| from .models import RedditPost | |
| logger = logging.getLogger(__name__) | |
| HTML_SITES = [ | |
| { | |
| "name": "Webmanagercenter", | |
| "url": "https://www.webmanagercenter.com/", | |
| "selectors": [".entry-title a", "h3 a", "h2 a"], | |
| "domain": "webmanagercenter.com", | |
| }, | |
| { | |
| "name": "Directinfo", | |
| "url": "https://directinfo.webmanagercenter.com/", | |
| "selectors": [".entry-title a", "h3 a", "h2 a"], | |
| "domain": "directinfo.webmanagercenter.com", | |
| }, | |
| { | |
| "name": "Tuniscope", | |
| "url": "https://www.tuniscope.com/", | |
| "selectors": [".entry-title a", "h3 a", "h2 a", ".article-title a"], | |
| "domain": "tuniscope.com", | |
| }, | |
| ] | |
| class HTMLSiteScraper: | |
| def __init__(self, config): | |
| self.config = config | |
| self.session = requests.Session() | |
| self.session.headers.update({ | |
| "User-Agent": "Mozilla/5.0 (compatible; newsapp/1.0)", | |
| }) | |
| def fetch_posts(self) -> list[RedditPost]: | |
| seen_urls = set() | |
| posts = [] | |
| for site in HTML_SITES: | |
| logger.info("Scraping HTML: %s (%s)", site["name"], site["url"]) | |
| try: | |
| resp = self.session.get(site["url"], timeout=15) | |
| resp.raise_for_status() | |
| soup = BeautifulSoup(resp.text, "html.parser") | |
| links = self._find_article_links(soup, site) | |
| logger.info(" %s → %d links found", site["name"], len(links)) | |
| for a_tag in links[: self.config.posts_per_subreddit]: | |
| title = a_tag.get_text(strip=True) | |
| href = a_tag.get("href", "") | |
| if not title or not href: | |
| continue | |
| full_url = urljoin(site["url"], href) | |
| if full_url in seen_urls: | |
| continue | |
| seen_urls.add(full_url) | |
| domain = site.get("domain") or urlparse(full_url).netloc | |
| published, published_iso = self._extract_date(a_tag) | |
| image_url = self._extract_image(a_tag, site["url"]) | |
| post = RedditPost( | |
| id=full_url, | |
| title=title, | |
| url=full_url, | |
| subreddit=domain, | |
| score=0, | |
| num_comments=0, | |
| source_domain=domain, | |
| image_url=image_url, | |
| published=published, | |
| published_iso=published_iso, | |
| ) | |
| posts.append(post) | |
| except Exception as exc: | |
| logger.warning("Failed HTML scrape %s: %s", site["name"], exc) | |
| time.sleep(1.0) | |
| return posts | |
| def _extract_date(a_tag) -> tuple[str, str]: | |
| for parent_tag in ("div", "article", "li", "section"): | |
| parent = a_tag.find_parent(parent_tag) | |
| if not parent: | |
| continue | |
| time_tag = parent.find("time") | |
| if time_tag: | |
| raw = time_tag.get("datetime") or time_tag.get_text(strip=True) | |
| if raw: | |
| parsed = _parse_date_str(raw) | |
| if parsed: | |
| return parsed | |
| date_el = parent.find(["span", "div"], class_=re.compile(r"date|time", re.I)) | |
| if date_el: | |
| raw = date_el.get("datetime") or date_el.get_text(strip=True) | |
| if raw: | |
| parsed = _parse_date_str(raw) | |
| if parsed: | |
| return parsed | |
| return ("", "") | |
| def _extract_image(a_tag, base_url: str) -> str: | |
| for ancestor in a_tag.parents: | |
| if ancestor.name not in ("div", "article", "li", "section"): | |
| continue | |
| img = ancestor.find("img") | |
| if not img: | |
| continue | |
| for attr in ("data-src", "data-lazy-src", "src", "data-srcset", "srcset"): | |
| val = img.get(attr, "") | |
| if val: | |
| if attr in ("data-srcset", "srcset"): | |
| val = val.split(",")[0].strip().split(" ")[0] | |
| return urljoin(base_url, val) | |
| return "" | |
| def _find_article_links(soup: BeautifulSoup, site: dict) -> list: | |
| for selector in site["selectors"]: | |
| found = soup.select(selector) | |
| if found: | |
| return found | |
| return [] | |
| _DATE_FORMATS = [ | |
| "%Y-%m-%dT%H:%M:%S%z", | |
| "%Y-%m-%dT%H:%M:%S", | |
| "%Y-%m-%d", | |
| "%d %b %Y", | |
| "%d %B %Y", | |
| "%b %d, %Y", | |
| "%B %d, %Y", | |
| "%d/%m/%Y", | |
| "%m/%d/%Y", | |
| ] | |
| def _parse_date_str(raw: str) -> tuple[str, str] | None: | |
| raw = raw.strip() | |
| if not raw: | |
| return None | |
| for fmt in _DATE_FORMATS: | |
| try: | |
| dt = datetime.strptime(raw, fmt) | |
| if dt.tzinfo: | |
| display = dt.strftime("%d %b %Y") | |
| iso = dt.strftime("%Y-%m-%d") | |
| else: | |
| display = dt.strftime("%d %b %Y") | |
| iso = dt.strftime("%Y-%m-%d") | |
| return (display, iso) | |
| except ValueError: | |
| continue | |
| try: | |
| dt = datetime.fromisoformat(raw) | |
| display = dt.strftime("%d %b %Y") | |
| iso = dt.strftime("%Y-%m-%d") | |
| return (display, iso) | |
| except (ValueError, TypeError): | |
| pass | |
| return None | |