Grande parte do nosso trabalho (clientes de metadados de media, enriquecimento de catálogos, arquivo) depende de ler páginas web públicas de forma fiável. O problema raramente são os dados. É que grande parte da infraestrutura de deteção de bots foi afinada contra ataques automatizados e acaba por classificar incorretamente qualquer cliente não-navegador bem-comportado como um deles. Isso acontece em dois eixos distintos:
- “O que és tu?” A Cloudflare e afins assinalam pedidos não pelo que
pedes, mas pelo aspeto que tens à saída: o teu handshake TLS, a tua impressão
digital JA3 (um resumo de como o teu handshake TLS está construído, que difere
entre um navegador e uma biblioteca HTTP simples mesmo quando ambos pedem a
mesma página), se consegues executar um desafio JavaScript. Um handshake simples
do
requestsnão se parece nada com o de um navegador, pelo que fica preso em verificações destinadas a abuso automatizado, mesmo quando o próprio tráfego é benigno. - “Quem és tu?” A reputação de IP e os limites de taxa ignoram por completo a tua impressão digital. Contam quantos pedidos vêm de um único endereço, o que pode penalizar um único cliente bem-comportado tão facilmente como um abusivo.
Os dois eixos são ortogonais, por isso respondemos-lhes com duas pequenas
bibliotecas que se empilham de forma limpa: o unblock_requests responde a o
que és tu, e o anon_requests responde a quem és tu. Ambas são substitutos
diretos de uma sessão requests no código do dia a dia. Este artigo é
especificamente sobre essa camada de transporte, a parte dos bytes à saída, e não
sobre o parsing ou o pipeline que assenta por cima.
Âmbito, dito sem rodeios: estes transportes destinam-se apenas a páginas
públicas e não autenticadas. Respeitam o robots.txt e qualquer crawl-delay
declarado (vê o nosso artigo sobre robots.txt e sitemaps
para saber como verificamos isso antes de escrever um scraper), e todo o cliente
construído sobre eles é mantido a volumes de pedidos baixos, pelo que uma origem
alvo nunca vê carga significativa vinda de nós. Isto não é uma cláusula de
isenção acrescentada depois. É uma restrição de engenharia real sobre como estas
sessões são usadas, porque um cliente resiliente que também é desconsiderado
anula o seu próprio propósito.
A restrição de design: manter a forma do requests
As sessões do unblock_requests fazem subclasse de requests.Session e apenas
sobrepõem o request(). Tudo o resto (.get(), .post(), cookies, cabeçalhos,
semântica de context manager) é herdado, pelo que qualquer código tipado contra
requests.Session as aceita sem alterações. As sessões do anon_requests envolvem
em vez de fazer subclasse. Expõem os mesmos métodos de verbo e a mesma interface
de context manager, mas reconstroem a sua sessão interna a cada rotação:
from unblock_requests import CloudflareSession # alias: Session
import requests
s = CloudflareSession(flaresolverr_url="http://your-flaresolverr-host:8191")
html = s.get("https://www.progarchives.com/artist.asp?id=1").text
assert isinstance(s, requests.Session) # True
Está aqui, numa linha, toda a postura ética e de engenharia: não estamos a automatizar um navegador-como-utilizador, estamos a construir um cliente HTTP resiliente para dados que já são públicos. Nenhum navegador com interface aparece no ecrã de ninguém, e nada no caminho crítico precisa de um display.
Camada um: o unblock_requests e os seus transportes
O unblock_requests torna um cliente Python simples interoperável com
verificações de deteção de bots afinadas para navegadores. Escolhes um transporte
com o argumento mode= (ou a variável de ambiente UNBLOCK_REQUESTS_TRANSPORT;
argumentos explícitos ganham sempre). Os quatro principais:
| Modo | O que faz |
|---|---|
curl_cffi (predefinição) | Impersonação TLS/JA3 do Chrome via curl_cffi. Passa como um handshake com forma de navegador na maioria das redes sem infraestrutura extra. |
requests | requests simples, sem impersonação. |
flaresolverr | Encaminha através de um navegador headless FlareSolverr que resolve o desafio JS: dados em direto. |
wayback | Lê o snapshot mais recente do Internet Archive: desatualizado, mas não precisa de nada. |
A predefinição, curl_cffi, é a vitória barata. A maioria dos veredictos “és um
bot” resume-se a uma discrepância de impressão digital TLS: o requests de origem
(via OpenSSL) faz um handshake em nada parecido com o do Chrome. O curl_cffi
impersona uma build real do Chrome (impersonate="chrome" por predefinição), pelo
que o handshake e o JA3 alinham e a verificação simplesmente passa. Nenhum
JavaScript executado, nenhum navegador arrancado.
Quando um site escala para um verdadeiro desafio JS interativo, o curl_cffi não
chega. Algo tem de executar o desafio. É aí que entra o modo flaresolverr: uma
instância de FlareSolverr que
alojas tu próprio faz a resolução num navegador headless fora do teu processo, e
o unblock_requests limita-se a fazer POST para ela e a extrair o HTML resolvido da
resposta. Definir flaresolverr_url seleciona este modo automaticamente:
CloudflareSession(flaresolverr_url="http://host:8191") # solve live
CloudflareSession(mode="wayback") # force archive
CloudflareSession(flaresolverr_url="http://host:8191",
wayback_fallback=True) # live, archive on failure
Degradação graciosa para o arquivo
A infraestrutura tem dias maus: o FlareSolverr está em baixo, o site está
inacessível, o desafio é neste momento insolúvel. Em vez de falhar todo o trabalho,
a sessão pode recorrer à Wayback Machine. A deteção de desafios é heurística:
um pequeno auxiliar is_challenge() fareja a primeira parte do corpo à procura dos
marcadores reveladores de um interstício da Cloudflare (“just a moment”,
challenge-platform, cf_chl_opt, cf-mitigated). Perante um GET bloqueado, se o
wayback_fallback estiver ativo, a sessão resolve o snapshot mais recente via API
de disponibilidade do archive.org e devolve os seus bytes em bruto (a forma em
bruto …id_/, sem barra de ferramentas nem reescrita de links). O archive.org não
está protegido pela Cloudflare, por isso o requests simples chega lá.
Duas notas de implementação que vale a pena conhecer: nos modos wayback e
flaresolverr o resultado é uma requests.Response sintetizada mas genuína,
construída a partir do HTML obtido, pelo que stream=, adaptadores personalizados
e connection pooling não se aplicam aí, ao passo que os modos requests/curl_cffi
são totalmente nativos. E o recurso ao arquivo só dispara para GETs. Nunca
reproduzimos silenciosamente um pedido mutante a partir de um arquivo.
Camada dois: o anon_requests e a rotação de IP
O problema ortogonal é a reputação de IP. Mesmo um handshake com forma de
navegador perfeito pode apanhar limite de taxa se todos os pedidos vierem de um
único endereço. As heurísticas baseadas em volume olham para o endereço, não
para a impressão digital. O anon_requests distribui a carga por vários
endereços com RotatingProxySession (proxies públicos extraídos, validação
opcional, SOCKS5/HTTP) e RotatingTorSession (circuitos Tor rotativos), pelo
que um cliente de baixo volume nunca é confundido com um a martelar um site a
partir de um único IP. Cada pedido sai por uma saída nova, e os proxies mortos
são retirados por rotação em caso de falha de ligação.
from anon_requests import RotatingProxySession, ProxyType
with RotatingProxySession(proxy_type=ProxyType.SOCKS5, validate=True) as s:
print(s.get("https://ipecho.net/plain", timeout=5).text) # a new IP each time
A composição: carga distribuída e um handshake compatível de uma só vez
Estas duas bibliotecas foram desenhadas para se empilhar em vez de se sobrepor. As
sessões do anon_requests aceitam um session_factory, qualquer callable que
devolva uma requests.Session, com predefinição requests.Session. As definições
de rotação e proxy são aplicadas ao que quer que essa factory devolva. Assim,
injetas uma CloudflareSession como factory e obténs ambos os comportamentos num só
objeto:
from anon_requests import RotatingProxySession
from unblock_requests import CloudflareSession
session = RotatingProxySession(
session_factory=lambda: CloudflareSession(flaresolverr_url="http://host:8191"),
)
session.get(url) # spreads load across IPs *and* uses a browser-compatible handshake
O proxy rotativo flui através de todos os transportes, incluindo para dentro do
FlareSolverr, que conduz o seu navegador headless através do campo proxy do pedido
de resolução. Assim, o pedido inteiro (handshake, resolução do desafio e IP de
saída) mantém-se consistente de ponta a ponta, o que é o
comportamento correto para um cliente que não está a tentar parecer mais do que
um único visitante.
Porquê esta forma
Manter cada preocupação na sua própria subclasse fina de requests.Session
significa que quem chama escolhe apenas o que precisa (só impersonação TLS, a stack
completa de rotação-mais-resolução, ou qualquer coisa pelo meio) trocando um
construtor, não reescrevendo o seu código HTTP. A ferramenta cara e pesada (um
navegador a sério) fica fora do processo no FlareSolverr e só é invocada quando um
desafio JS genuinamente o exige. O caso comum é um handshake impersonado e barato. E
quando a web em direto se recusa, o arquivo responde.
A junção do session_factory é onde acontece a composição, e é o que mantém as
bibliotecas extensíveis de forma independente: acrescenta um modo de transporte ao
unblock_requests e o anon_requests compõe-no de borla. Acesso resiliente a
dados públicos, feito de forma limpa.
Ambas são FOSS e auto-alojáveis:
unblock_requests e
anon_requests.
Estes transportes alimentam todos os nossos scrapers de bases de dados de música. Para reconhecimento de um site antes de construíres qualquer scraper, vê o sitemapper e o artigo sobre robots.txt e sitemaps.