brnews/fetcher.py — download HTTP com failover
Cliente HTTP que tenta várias "rotas" até conseguir um conteúdo válido. Existe porque muitos portais brasileiros bloqueiam (403/429/Cloudflare) os IPs de datacenter dos runners do GitHub Actions. Visão de usuário em Proxies e rotas; aqui, a visão de código.
Peças principais
load_proxies(explicit, files, env_vars, use_standard_env)
Junta proxies de CLI, arquivos (config/proxies.txt), variáveis
BRNEWS_PROXIES/PROXY_LIST/PROXIES e do ambiente padrão
(HTTPS_PROXY, …), normaliza cada entrada com normalize_proxy()
(esquemas aceitos: http, https, socks4, socks5, socks5h; host:porta
ganha http://) e deduplica preservando a ordem.
mask(url)
Substitui user:senha@ por ***@ — é aplicada sempre antes de registrar um
proxy em log, relatório ou JSONL.
ProxyPool
Rodízio thread-safe com punição temporária:
take(n)devolve aténproxies ativos, em round-robin a partir de um ponto aleatório por execução;report(proxy, ok): sucesso zera o contador; a 3ª falha põe o proxy em cooldown de 15 minutos (max_failures=3,cooldown=900);- só falhas de transporte (
ProxyError,ConnectionError,ConnectTimeout— a tuplaTRANSPORT_ERRORS) punem o proxy;HTTP 403do destino não.
Fetcher.get(url, validate=None, allow_markdown_mirrors=False, routes=None)
O coração do módulo. Para cada rota de route_order (ou do parâmetro routes, que
permite ao chamador restringir — ex.: só ["mirror", "proxy"] numa segunda
tentativa):
- monta os alvos da rota —
direct(1 alvo),proxy(atémax_proxies_per_urlproxies do pool),mirror(todos os espelhos compatíveis); - tenta cada alvo até
attempts_per_routevezes (espelhos: 1 vez só), com backoffbackoff * tentativa + jitter; - uma resposta só é aceita se: status < 400, corpo não vazio e o callback
validate(content, text)aprovar; - ao aceitar, preenche
FetchResultcomroute(direct/proxy/mirror),via(proxy ou nome do espelho, mascarado),is_markdown(espelho não-raw), tempo e tentativas — e esses valores chegam até os camposfetch_route/fetch_viado JSONL; - se tudo falhar, devolve
ok=Falsecom o último erro e a listatried(usada nos relatórios e anotações do CI).
Espelhos (DEFAULT_MIRRORS)
{"name": "allorigins", "template": "https://api.allorigins.win/raw?url={qurl}", "raw": True}
{"name": "codetabs", "template": "https://api.codetabs.com/v1/proxy/?quest={qurl}", "raw": True}
{"name": "cors-lol", "template": "https://api.cors.lol/?url={qurl}", "raw": True}
{"name": "jina", "template": "https://r.jina.ai/{url}", "raw": False}
{url}é a URL crua;{qurl}, percent-encoded;raw: False(r.jina.ai) devolve Markdown, não o corpo original — esse espelho só entra quando o chamador passaallow_markdown_mirrors=True, e o resultado é marcado comis_markdown=Truepara ser parseado porparse_markdown;- com
mirrors_through_proxy=True(modo--proxies-only), a requisição ao espelho também sai por proxy; sem proxy disponível, a rota é pulada (nunca vaza o IP local).
Anti-bloqueio nos headers e no ritmo
- User-Agent rotativo (
DEFAULT_USER_AGENTS): navegadores (Chrome/Safari) intercalados de propósito com leitores de RSS (Feedly, Inoreader, Tiny Tiny RSS) — se um navegador leva 403, a tentativa seguinte vai como leitor de RSS; Acceptpriorizandoapplication/rss+xml,Accept-Language: pt-BR,Refererapontando para a raiz do próprio host;_respect_host_delay: intervalo mínimo por host (min_interval_per_host, padrão 1s) compartilhado entre todas as threads;session.trust_env = False: os proxies do ambiente não são aplicados implicitamente pelorequests— só os controlados pelo pool;- uma
requests.Sessionpor thread (threading.local) para reaproveitar conexões sem compartilhar estado.
Contadores
Fetcher.route_counters acumula quantos downloads cada rota resolveu
(direct, proxy, mirror, failed). O valor aparece no .report.json
("routes"), no resumo do job e em data/index.jsonl.