spb/ka2 Public
ka2 — explorateur structuré du web québécois (édition légère, Groupe KA). Bot scraper+IA → graphe de connaissances. Claude Haiku + Firecrawl/Scrapfly.
Python 98.1%
Shell 1.9%
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3"""Cache robots.txt robuste.45`urllib.robotparser.RobotFileParser.read()` récupère le robots.txt avec le6User-Agent `Python-urllib`, souvent bloqué (403) par les serveurs, ce qui bascule7le parseur en `disallow_all=True` (faux négatif). On récupère donc le fichier nous-8mêmes via `requests` avec un vrai User-Agent, puis on le parse.9"""1011from __future__ import annotations1213import urllib.robotparser14from typing import Optional15from urllib.parse import urlparse1617import requests181920class RobotsCache:21 def __init__(self, user_agent: str, enabled: bool = True, timeout: int = 15):22 self.user_agent = user_agent23 self.enabled = enabled24 self.timeout = timeout25 self._cache: dict[str, Optional[urllib.robotparser.RobotFileParser]] = {}2627 def _load(self, root: str) -> Optional[urllib.robotparser.RobotFileParser]:28 if root in self._cache:29 return self._cache[root]30 rp: Optional[urllib.robotparser.RobotFileParser] = None31 try:32 r = requests.get(33 f"{root}/robots.txt",34 headers={"User-Agent": self.user_agent},35 timeout=self.timeout,36 )37 if r.status_code == 200 and r.text.strip():38 rp = urllib.robotparser.RobotFileParser()39 rp.parse(r.text.splitlines())40 # 4xx/5xx ou vide -> pas de règles connues -> on autorise (rp=None)41 except Exception:42 rp = None # robots injoignable -> on autorise, la politesse vient du rate-limit43 self._cache[root] = rp44 return rp4546 def allowed(self, url: str) -> bool:47 if not self.enabled:48 return True49 try:50 parts = urlparse(url)51 if not parts.scheme or not parts.netloc:52 return True53 root = f"{parts.scheme}://{parts.netloc}"54 rp = self._load(root)55 if rp is None:56 return True57 return rp.can_fetch(self.user_agent, url)58 except Exception:59 return True60