SPB Git

spb/trouve-ka Public

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

Python 76.8% TypeScript 15.7% SQL 3.9% Shell 1.4% CSS 1.3% Dockerfile 0.7%
3.3 KB · 85 lines python
Raw Blame History
1# Trouve-KA — gestion robots.txt2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Cache robots.txt conforme aux standards (Protego, le parseur de Scrapy).67Les règles sont persistées en base (domains.robots_body) et rafraîchies8après 24 h. Un robots.txt introuvable (404) = tout permis; une erreur9serveur = prudence, on refuse temporairement.10"""1112from datetime import UTC, datetime, timedelta1314import httpx15from protego import Protego1617from trouveka.logging import get_logger1819log = get_logger("crawler.robots")2021ROBOTS_TTL = timedelta(hours=24)22_MAX_ROBOTS_BYTES = 512_000232425class RobotsCache:26    def __init__(self, db, client: httpx.AsyncClient, user_agent: str):27        self._db = db28        self._client = client29        self._ua = user_agent30        self._parsers: dict[int, tuple[Protego | None, datetime]] = {}3132    async def _fetch_robots(self, scheme_host: str) -> tuple[str | None, str]:33        """Retourne (body, status) : ok | not_found | error."""34        try:35            resp = await self._client.get(36                f"{scheme_host}/robots.txt",37                headers={"User-Agent": self._ua},38                timeout=15,39                follow_redirects=True,40            )41        except httpx.HTTPError:42            return None, "error"43        if resp.status_code == 200:44            return resp.text[:_MAX_ROBOTS_BYTES], "ok"45        if 400 <= resp.status_code < 500:46            return None, "not_found"  # 4xx = pas de robots → tout permis (standard)47        return None, "error"4849    async def get_parser(self, domain_id: int, scheme_host: str) -> tuple[Protego | None, str, int | None]:50        """Parseur robots pour un domaine. Retourne (parser|None, status, crawl_delay_ms)."""51        cached = self._parsers.get(domain_id)52        now = datetime.now(UTC)53        if cached and now - cached[1] < ROBOTS_TTL:54            parser = cached[0]55            return parser, "ok" if parser else "not_found", None5657        row = await self._db.get_domain_by_id(domain_id)58        if row and row["robots_fetched_at"] and now - row["robots_fetched_at"] < ROBOTS_TTL:59            body, status = row["robots_body"], row["robots_status"]60        else:61            body, status = await self._fetch_robots(scheme_host)62            delay_ms = None63            if body:64                delay = Protego.parse(body).crawl_delay(self._ua)65                delay_ms = int(delay * 1000) if delay else None66            await self._db.save_robots(domain_id, body, status, delay_ms)6768        parser = Protego.parse(body) if body else None69        self._parsers[domain_id] = (parser, now)70        if len(self._parsers) > 5000:  # borne mémoire du cache local71            self._parsers.pop(next(iter(self._parsers)))72        return parser, status, row["crawl_delay_ms"] if row else None7374    async def allowed(self, domain_id: int, url: str, scheme_host: str) -> tuple[bool, float | None]:75        """(autorisé, crawl_delay_secondes). Erreur robots = refus temporaire (prudence)."""76        parser, status, delay_ms = await self.get_parser(domain_id, scheme_host)77        if status == "error":78            return False, None79        if parser is None:80            return True, None81        delay = parser.crawl_delay(self._ua)82        if delay is None and delay_ms:83            delay = delay_ms / 100084        return parser.can_fetch(url, self._ua), delay85