SPB Git

spb/ka4 Public

ka4 — explorateur structuré du web québécois (édition agressive, Groupe KA). Crawling concurrent, Claude Sonnet+Haiku, Firecrawl/Scrapfly, graphe de connaissances.

Python 97.8% Shell 2.2%
2.1 KB · 60 lines python
Raw Blame History
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