# Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai """Cache robots.txt robuste. `urllib.robotparser.RobotFileParser.read()` récupère le robots.txt avec le User-Agent `Python-urllib`, souvent bloqué (403) par les serveurs, ce qui bascule le parseur en `disallow_all=True` (faux négatif). On récupère donc le fichier nous- mêmes via `requests` avec un vrai User-Agent, puis on le parse. """ from __future__ import annotations import urllib.robotparser from typing import Optional from urllib.parse import urlparse import requests class RobotsCache: def __init__(self, user_agent: str, enabled: bool = True, timeout: int = 15): self.user_agent = user_agent self.enabled = enabled self.timeout = timeout self._cache: dict[str, Optional[urllib.robotparser.RobotFileParser]] = {} def _load(self, root: str) -> Optional[urllib.robotparser.RobotFileParser]: if root in self._cache: return self._cache[root] rp: Optional[urllib.robotparser.RobotFileParser] = None try: r = requests.get( f"{root}/robots.txt", headers={"User-Agent": self.user_agent}, timeout=self.timeout, ) if r.status_code == 200 and r.text.strip(): rp = urllib.robotparser.RobotFileParser() rp.parse(r.text.splitlines()) # 4xx/5xx ou vide -> pas de règles connues -> on autorise (rp=None) except Exception: rp = None # robots injoignable -> on autorise, la politesse vient du rate-limit self._cache[root] = rp return rp def allowed(self, url: str) -> bool: if not self.enabled: return True try: parts = urlparse(url) if not parts.scheme or not parts.netloc: return True root = f"{parts.scheme}://{parts.netloc}" rp = self._load(root) if rp is None: return True return rp.can_fetch(self.user_agent, url) except Exception: return True