Na maioria dos setups de scraping, a configuração de proxy é escolhida uma vez, na fase de design, e depois mantida. Residencial com segmentação por cidade para este site, proxies ISP para aquele outro, datacenter onde funciona. A escolha costuma ser feita testando algumas opções durante uma tarde, e raramente é revista até que algo quebre.
Essa é uma decisão tomada sob incerteza, repetida milhares de vezes por dia, com feedback a cada requisição. É exatamente o formato de problema que os estatísticos chamam de multi-armed bandit, e existem algoritmos bem compreendidos para isso. Este guia explica o enquadramento, apresenta uma implementação curta e testada, e mostra o que ela fez em uma simulação na qual a melhor opção mudou na metade do caminho.
Principais conclusões
- Cada configuração de proxy que você poderia usar para um alvo é um “braço”; cada requisição é um “pull”; uma resposta boa é uma recompensa. O objetivo é o menor custo por resposta boa, não a maior taxa de sucesso a qualquer preço.
- O Thompson sampling lida com a troca entre usar a melhor opção conhecida e testar as outras, com poucas linhas de código e sem cronograma de ajuste.
- Os alvos mudam, então evidências antigas devem perder peso. Um fator de desconto dá ao seletor uma memória de aproximadamente os últimos mil resultados.
- Em nossa simulação, o Thompson sampling com desconto ficou a 2% de uma estratégia que conhecia as taxas reais de antemão, enquanto uma escolha fixa “segura” custou 25% a mais por resposta boa, e manter o vencedor inicial custou 57% a mais.
- Conte como sucesso apenas respostas genuinamente boas, e limite quanto de exploração um alvo sensível vê.
O enquadramento
Imagine uma fileira de caça-níqueis, cada um pagando com uma probabilidade desconhecida. Cada puxada ensina algo sobre uma máquina, e cada puxada gasta aprendendo sobre uma máquina ruim é uma puxada que não foi gasta em uma boa. Essa tensão entre explorar o que você já sabe e investigar o que você não sabe é o problema do multi-armed bandit.
Para a seleção de proxy, o mapeamento é direto:
| Termo do bandit | No scraping |
|---|---|
| Braço | Uma configuração de proxy para um alvo: tipo de proxy, segmentação, configuração de sessão |
| Pull | Uma requisição |
| Recompensa | Uma resposta que contém os dados desejados |
| Custo de um pull | Banda, retentativas e tempo gasto naquela requisição |
| Não estacionariedade | O alvo mudando suas defesas, ou a reputação de um pool mudando |
Duas características tornam a versão para scraping mais difícil do que a do livro-texto. Os braços custam quantias diferentes, então o melhor braço é aquele com o menor custo por sucesso, não a maior taxa de sucesso. E as taxas mudam: uma configuração que funciona hoje pode ser bloqueada amanhã, o que é o padrão por trás do contágio de sub-rede e o motivo para construir um índice de saúde do alvo.
O seletor
O Thompson sampling mantém uma distribuição de probabilidade sobre a taxa de sucesso de cada braço, uma distribuição Beta construída a partir de seus sucessos e falhas. Antes de cada requisição, ele sorteia uma taxa plausível para cada braço e escolhe o braço que parece mais barato por sucesso sob esses sorteios. Braços com pouca evidência têm distribuições amplas, então ocasionalmente sorteiam uma taxa alta e acabam sendo testados. Braços com muita evidência sorteiam valores próximos de sua taxa real. A exploração diminui por conta própria conforme a evidência se acumula.
A versão abaixo adiciona duas coisas: custo por tentativa, e um desconto que decai a evidência antiga em direção ao prior, para que o seletor consiga perceber mudanças.
import random
class ProxySelector:
"""Pick a proxy configuration per request with Thompson sampling, minimising cost per successful response.
Each configuration keeps a Beta(successes, failures) belief about its success rate. A discount below 1
lets old outcomes fade, so the selector notices when a configuration that used to work starts failing.
"""
def __init__(self, costs, discount=0.999, prior=(1.0, 1.0), rng=None):
self.costs = dict(costs) # configuration -> cost per attempt
self.discount = discount
self.prior = prior
self.wins = {arm: prior[0] for arm in self.costs}
self.losses = {arm: prior[1] for arm in self.costs}
self.rng = rng or random.Random()
def choose(self):
"""Sample a plausible success rate for each configuration and take the cheapest per expected success."""
def sampled_cost_per_success(arm):
rate = self.rng.betavariate(self.wins[arm], self.losses[arm])
return self.costs[arm] / max(rate, 1e-9)
return min(self.costs, key=sampled_cost_per_success)
def update(self, arm, success):
"""Record one outcome. Every belief decays towards the prior first, so recent results count most."""
for a in self.costs:
self.wins[a] = self.prior[0] + self.discount * (self.wins[a] - self.prior[0])
self.losses[a] = self.prior[1] + self.discount * (self.losses[a] - self.prior[1])
if success:
self.wins[arm] += 1
else:
self.losses[arm] += 1
No uso prático, chame choose() antes de cada requisição, envie a requisição através da configuração escolhida, e chame update() informando se a resposta foi boa. Um desconto de 0.999 dá uma memória efetiva de aproximadamente mil resultados; 0.995 dá aproximadamente duzentos.
A simulação
Para ver como o seletor se comporta quando a resposta muda, simulamos um alvo com quatro configurações. As taxas de sucesso e os custos abaixo são premissas escolhidas para tornar as trocas visíveis, não medições de qualquer provedor ou rede:
| Configuração | Taxa de sucesso assumida | Custo por tentativa assumido | Custo por sucesso |
|---|---|---|---|
| Datacenter | 25% | 0.5 | 2.00 |
| ISP | 80%, caindo para 30% após a requisição 5.000 | 0.9 | 1.13, depois 3.00 |
| Residencial, segmentação por país | 92% | 1.3 | 1.41 |
| Residencial, segmentação por cidade | 95% | 1.6 | 1.68 |
Os custos são unidades relativas que cobrem banda e a sobrecarga de uma tentativa. Nas primeiras 5.000 requisições, o ISP é a forma mais barata de obter uma resposta boa; depois o alvo começa a bloqueá-lo, e o residencial com segmentação por país se torna a melhor escolha. Cada estratégia fez 20.000 requisições, e repetimos cada estratégia 100 vezes com sementes aleatórias diferentes.
| Estratégia | Custo por resposta boa | Contra a visão retrospectiva perfeita | Respostas boas | Requisições para se adaptar após a mudança |
|---|---|---|---|---|
| Visão retrospectiva perfeita (conhece as taxas reais) | 1.348 | linha de base | 17.806 | 0 |
| Thompson sampling, desconto 0.999 | 1.375 | +2.0% | 16.694 | cerca de 800 |
| Thompson sampling, desconto 0.995 | 1.389 | +3.0% | 15.704 | cerca de 1.075 |
| Thompson sampling, sem desconto | 1.428 | +5.9% | 15.933 | cerca de 3.500 |
| Epsilon-greedy, 10% de exploração | 1.441 | +6.9% | 15.848 | cerca de 2.800 |
| Thompson sampling, desconto 0.98 | 1.443 | +7.0% | 13.834 | nunca se estabilizou |
| Sempre residencial, segmentação por cidade | 1.684 | +24.9% | 19.000 | não aplicável |
| Round robin entre as quatro | 1.689 | +25.3% | 12.731 | não aplicável |
| Sempre ISP, o vencedor inicial | 2.118 | +57.1% | 8.501 | não aplicável |
Os números são médias de 100 execuções; para cada estratégia, os 90% centrais das execuções ficaram dentro de menos de 2.5% de sua média. “Requisições para se adaptar” é o número mediano de requisições após a mudança até que 90% de uma janela de 200 requisições fosse direcionado para a nova melhor configuração.
O que os resultados dizem
Escolhas fixas são caras nos dois sentidos. Usar sempre a configuração mais confiável entregou o maior número de respostas boas, mas custou 25% a mais por cada uma. Usar sempre a configuração que venceu no início foi a pior estratégia de todas depois que o alvo mudou, a 57% acima da linha de base.
Aprender não basta sem esquecer. O Thompson sampling sem desconto levou cerca de 3.500 requisições após a mudança para parar de confiar na evidência que havia acumulado sobre o antigo vencedor. Um desconto de 0.999 reduziu isso para cerca de 800.
Esquecer rápido demais é um problema por si só. Com um desconto de 0.98, o seletor lembrava apenas cerca de 50 resultados, continuava retestando as opções ruins e nunca se estabilizou. A faixa útil depende do tráfego: a memória deve cobrir requisições suficientes para distinguir as configurações, e nada além disso.
O objetivo determina a resposta. O bandit minimizou o custo por resposta boa. Se o que importa é a maior quantidade de dados independentemente do custo, ou um prazo, a recompensa precisa dizer isso; caso contrário, o seletor vai otimizar a coisa errada de forma muito eficiente.
Usando isso em tráfego real
- Defina sucesso de forma rigorosa. Um HTTP 200 com uma página de bloqueio ou resultados vazios é uma falha. Alimente o seletor com as mesmas verificações que você usa para a taxa de falha silenciosa, ou ele vai aprender a preferir configurações que falham silenciosamente.
- Execute um seletor por alvo. As taxas de sucesso variam por site, então um seletor compartilhado faz uma média que apaga exatamente as diferenças que você quer que ele aprenda.
- Limite a exploração em alvos sensíveis. Cada requisição exploratória através de uma configuração ruim é uma requisição que um site pode contar contra você. Remova configurações claramente inadequadas em vez de deixar o seletor redescobrir isso sozinho.
- Mantenha as sessões coerentes. Escolha a configuração por sessão, não por requisição, para tudo que depende de estado, como logins ou carrinhos de compra, conforme explica sessões fixas versus rotativas.
- Use custos reais. Em proxies cobrados por banda, o custo por tentativa depende do peso da página e das retentativas; os números por trás do custo por registro limpo são os insumos corretos.
- Mantenha um piso de balanceamento de carga. Um bandit decide qual configuração usar, não como distribuir as requisições dentro dela; isso ainda é tarefa da arquitetura de rotação, concorrência e retentativa.
Conclusão
Escolher uma configuração de proxy uma vez e mantê-la é uma aposta de que nem seus custos nem o alvo vão mudar. Tratar cada requisição como um pequeno experimento, com Thompson sampling e uma memória sensata, transforma essa aposta em uma medição que continua se atualizando.
Em nossa simulação, isso ficou a 2% de conhecer a resposta de antemão, se adaptou em cerca de 800 requisições quando a melhor opção mudou, e evitou o sobrecusto de 25% a 57% das estratégias fixas. As taxas e os custos eram premissas; o código é curto o suficiente para ser executado com os seus próprios dados.
Fontes e referências
- Daniel Russo e colegas, A Tutorial on Thompson Sampling, 2017.
- Simulação executada pela Shifter em 2 de outubro de 2026 usando o código acima; as taxas de sucesso e os custos são premissas, não medições de qualquer rede.