from typing import Optional from urllib.parse import urljoin import httpx from common.security import is_safe_url DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 bangwen/1.0" MAX_REDIRECTS = 5 class PoliteFetcher: def __init__(self, timeout: float = 15.0, verify: bool | str = True): self.headers = { "User-Agent": DEFAULT_USER_AGENT, "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", } self.client = httpx.Client( headers=self.headers, timeout=timeout, follow_redirects=False, verify=verify, ) def get_text( self, url: str, allowed_domains: Optional[list[str]] = None, start_host: Optional[str] = None, ) -> str: current_url = url for _ in range(MAX_REDIRECTS): if not is_safe_url(current_url, allowed_domains, start_host): raise ValueError(f"Unsafe request URL blocked: {current_url}") resp = self.client.get(current_url) if resp.is_redirect and "location" in resp.headers: next_url = urljoin(current_url, resp.headers["location"]) current_url = next_url continue resp.raise_for_status() content_type = resp.headers.get("content-type", "").lower() if ( "gbk" in content_type or "gb2312" in content_type or "gb18030" in content_type ): return resp.content.decode("gb18030", errors="replace") try: return resp.text except UnicodeDecodeError: return resp.content.decode("gb18030", errors="replace") raise httpx.TooManyRedirects( f"Exceeded max redirects ({MAX_REDIRECTS}) for {url}" ) def close(self): self.client.close()