| import re |
| from urllib.parse import urljoin, urlparse |
| from bs4 import BeautifulSoup |
| from typing import Dict, Any, List, Set |
|
|
| class PageParser: |
| def __init__(self, domain: str, scheme: str = "https"): |
| self.domain = domain.lower() |
| self.scheme = scheme |
|
|
| def clean_text(self, text: str) -> str: |
| """Cleans formatting, extra spaces, and redundant newlines from text.""" |
| if not text: |
| return "" |
| |
| text = re.sub(r'\s+', ' ', text) |
| return text.strip() |
|
|
| def parse(self, html_content: str, current_url: str) -> Dict[str, Any]: |
| """Extracts structured text data and meta information from the HTML.""" |
| |
| soup = BeautifulSoup(html_content, "lxml") |
| |
| |
| for element in soup(["script", "style", "iframe", "noscript"]): |
| element.decompose() |
|
|
| |
| title = soup.title.string.strip() if soup.title else "" |
| |
| meta_desc = "" |
| meta_desc_tag = soup.find("meta", attrs={"name": "description"}) or soup.find("meta", attrs={"property": "og:description"}) |
| if meta_desc_tag: |
| meta_desc = meta_desc_tag.get("content", "").strip() |
|
|
| meta_keys = "" |
| meta_keys_tag = soup.find("meta", attrs={"name": "keywords"}) |
| if meta_keys_tag: |
| meta_keys = meta_keys_tag.get("content", "").strip() |
|
|
| |
| h1s = [self.clean_text(h.get_text()) for h in soup.find_all("h1") if h.get_text()] |
| h1_headers = " | ".join(h1s) |
|
|
| |
| |
| main_content = soup.find("main") or soup.find("article") or soup.find("body") |
| if main_content: |
| text_content = self.clean_text(main_content.get_text(separator=" ")) |
| else: |
| text_content = self.clean_text(soup.get_text(separator=" ")) |
|
|
| return { |
| "title": title, |
| "meta_description": meta_desc, |
| "meta_keywords": meta_keys, |
| "h1_headers": h1_headers, |
| "text_content": text_content, |
| } |
|
|
| def extract_links(self, html_content: str, current_url: str) -> Set[str]: |
| """Discovers and normalizes all internal/same-domain links from page anchor tags.""" |
| soup = BeautifulSoup(html_content, "lxml") |
| links = set() |
| |
| for anchor in soup.find_all("a", href=True): |
| href = anchor["href"].strip() |
| |
| |
| if not href or href.startswith(("#", "mailto:", "javascript:", "tel:")): |
| continue |
| |
| |
| absolute_url = urljoin(current_url, href) |
| |
| |
| parsed = urlparse(absolute_url) |
| |
| |
| if parsed.netloc.lower() == self.domain: |
| |
| normalized_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}" |
| |
| if parsed.query: |
| normalized_url += f"?{parsed.query}" |
| links.add(normalized_url) |
| |
| return links |
|
|