Em 7 de outubro de 2026, a Shifter tem o maior pool de IPs dos EUA ativos, mais do que a Oxylabs e a Bright Data, por uma fração do custo.A Shifter agora tem o maior pool de IPs ativos dos EUA

Ver benchmarks

Extração de dados

Seu Scraper Está Lento e Provavelmente Não É o Proxy

Aceleramos um scraper rodando por proxies residenciais 7,5 vezes sem mexer no proxy. Como descobrir para onde o tempo realmente vai, e corrigir isso.

Matt Brown

8 de outubro de 2026 · 12 min de leitura

“Seus proxies estão lentos” é uma das mensagens mais comuns que recebemos de clientes. Às vezes é verdade. Mais frequentemente, quando analisamos o scraper junto com o cliente, a maior parte do tempo vai para coisas que o próprio scraper faz consigo mesmo: abrir uma nova conexão para cada página, fazer parsing com uma biblioteca lenta, esperar uma requisição de cada vez.

Para mostrar o quanto isso importa, pegamos um scraper simples, rodamos através dos nossos proxies residenciais e mudamos uma coisa de cada vez. As mesmas 120 páginas foram de 298 segundos para 40, uma aceleração de 7,5 vezes, sem mudar nada no proxy. Este guia percorre esse teste, a ferramenta que usamos para encontrar as partes lentas e um checklist para diagnosticar seu próprio scraper.

Principais conclusões

  • Meça antes de mudar qualquer coisa. Um cronômetro de estágio que registra quanto tempo a busca, o parsing e a escrita levam vai te dizer para onde o tempo vai, em minutos.
  • Reutilize conexões. Abrir uma nova conexão através de um proxy para cada requisição mais do que triplicou o tempo por busca no nosso teste. Uma Session resolveu isso.
  • O parsing pode custar tanto quanto a busca. Trocar o parser de HTML cortou o tempo de parsing pela metade.
  • A concorrência traz o maior ganho, e depois expõe o próximo gargalo. Oito workers reduziram o tempo de execução pela metade e mostraram que o parsing limitado por CPU tinha se tornado o limite.
  • Nem tudo que é lento vale a pena consertar. As escritas no banco de dados levaram 0,3 segundos de 298, então agrupá-las em lote não mudou nada.

O teste

Coletamos 120 artigos da Wikipédia em inglês através dos proxies residenciais da Shifter com saídas nos EUA, a partir de uma máquina na Europa. Os artigos da Wikipédia são páginas grandes, frequentemente com várias centenas de kilobytes de HTML. Para cada página, o scraper busca o HTML, faz o parsing com o Beautiful Soup para ler o título e contar os links, e escreve uma linha no SQLite.

Rodamos o mesmo job cinco vezes, mudando uma coisa de cada vez. O scraper se identificava com um User-Agent descritivo, o que a Wikipédia exige (seus servidores rejeitam o User-Agent padrão do Python), e repetia uma requisição que falhasse até duas vezes.

VersãoMudançaTempo totalBusca por páginaParsing por página
1. BaselineNova conexão por requisição, html.parser, um de cada vez298 s1.906 ms577 ms
2. Reutilização de conexõesUma requests.Session125 s568 ms473 ms
3. Parser mais rápidolxml em vez de html.parser76 s426 ms205 ms
4. Concorrência8 threads de worker40 s551 ms2.024 ms
5. Escritas em loteUma transação em vez de uma por linha42 s579 ms2.141 ms

Os tempos de busca através de saídas residenciais variam de execução para execução em cerca de cem milissegundos; as diferenças acima são muito maiores do que isso.

Primeiro passo: medir para onde o tempo vai

A ferramenta é um pequeno cronômetro que soma o tempo de relógio real por estágio, de forma segura entre threads:

import time
from collections import defaultdict
from contextlib import contextmanager
from threading import Lock


class StageTimer:
    """Adds up wall-clock time per pipeline stage, safely across threads."""

    def __init__(self):
        self.totals = defaultdict(float)
        self.counts = defaultdict(int)
        self._lock = Lock()

    @contextmanager
    def stage(self, name):
        start = time.perf_counter()
        try:
            yield
        finally:
            elapsed = time.perf_counter() - start
            with self._lock:
                self.totals[name] += elapsed
                self.counts[name] += 1

    def report(self, wall_seconds):
        busy = sum(self.totals.values())
        print(f"wall {wall_seconds:.1f}s, stage time {busy:.1f}s")
        for name, total in sorted(self.totals.items(), key=lambda kv: -kv[1]):
            print(f"{name:>8}: {total:7.1f}s  {100 * total / busy:5.1f}%  "
                  f"{1000 * total / self.counts[name]:7.1f} ms per call")

Envolva cada estágio do seu scraper com with timer.stage("fetch"):, with timer.stage("parse"): e assim por diante, e imprima o relatório no final. Isso custa quase nada para rodar e transforma “o scraper está lento” em uma resposta específica.

Aqui está o scraper de teste completo com o cronômetro implementado. A função run() recebe as quatro configurações que mudamos:

import os
import sqlite3
import time
from concurrent.futures import ThreadPoolExecutor

import requests
from bs4 import BeautifulSoup

from stagetimer import StageTimer
from urls import URLS

user, password = os.environ["PROXY_USER"], os.environ["PROXY_PASS"]
PROXY = f"http://{user}-country-us:{password}@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
HEADERS = {"User-Agent": "ExampleScraper/1.0 (+https://example.com/bot)"}  # name your scraper


def scrape(url, session, parser, timer):
    with timer.stage("fetch"):
        for attempt in range(3):
            try:
                if session is None:               # a new connection for every request
                    response = requests.get(url, headers=HEADERS, proxies=PROXIES, timeout=20)
                else:                             # reuse connections through one Session
                    response = session.get(url, timeout=20)
                response.raise_for_status()
                break
            except requests.RequestException:
                if attempt == 2:
                    raise
        html = response.text
    with timer.stage("parse"):
        soup = BeautifulSoup(html, parser)
        title = soup.find("h1").get_text(strip=True)
        links = len(soup.select("a[href]"))
    return url, title, links


def run(reuse=False, parser="html.parser", workers=1, batch_writes=False):
    timer = StageTimer()
    session = None
    if reuse:
        session = requests.Session()
        session.proxies = PROXIES
        session.headers.update(HEADERS)
        adapter = requests.adapters.HTTPAdapter(pool_connections=workers, pool_maxsize=workers)
        session.mount("https://", adapter)
    db = sqlite3.connect("pages.db", check_same_thread=False)
    db.execute("CREATE TABLE IF NOT EXISTS pages (url TEXT, title TEXT, links INTEGER)")
    db.execute("DELETE FROM pages")
    db.commit()

    start = time.perf_counter()
    with ThreadPoolExecutor(workers) as pool:
        rows = pool.map(lambda u: scrape(u, session, parser, timer), URLS)
        if batch_writes:
            rows = list(rows)
            with timer.stage("write"):
                db.executemany("INSERT INTO pages VALUES (?, ?, ?)", rows)
                db.commit()
        else:
            for row in rows:
                with timer.stage("write"):
                    db.execute("INSERT INTO pages VALUES (?, ?, ?)", row)
                    db.commit()                   # one transaction per row
    wall = time.perf_counter() - start
    timer.report(wall)
    return wall


if __name__ == "__main__":
    steps = [
        ("baseline", {}),
        ("reuse connections", {"reuse": True}),
        ("faster parser", {"reuse": True, "parser": "lxml"}),
        ("8 workers", {"reuse": True, "parser": "lxml", "workers": 8}),
        ("batch writes", {"reuse": True, "parser": "lxml", "workers": 8, "batch_writes": True}),
    ]
    for name, config in steps:
        print(f"\n== {name}")
        run(**config)

urls.py contém a lista de 120 URLs de artigos. Rodar o código imprimiu:

== baseline
wall 298.2s, stage time 298.1s
   fetch:   228.7s   76.7%   1905.7 ms per call
   parse:    69.2s   23.2%    576.6 ms per call
   write:     0.3s    0.1%      2.2 ms per call

== reuse connections
wall 125.0s, stage time 125.2s
   fetch:    68.2s   54.5%    568.3 ms per call
   parse:    56.7s   45.3%    472.7 ms per call
   write:     0.3s    0.2%      2.1 ms per call

== faster parser
wall 75.7s, stage time 75.9s
   fetch:    51.1s   67.4%    425.8 ms per call
   parse:    24.6s   32.4%    204.8 ms per call
   write:     0.2s    0.3%      1.6 ms per call

== 8 workers
wall 39.7s, stage time 309.8s
   parse:   242.8s   78.4%   2023.6 ms per call
   fetch:    66.1s   21.3%    551.0 ms per call
   write:     0.8s    0.3%      6.9 ms per call

== batch writes
wall 42.1s, stage time 326.4s
   parse:   256.9s   78.7%   2141.0 ms per call
   fetch:    69.5s   21.3%    579.0 ms per call
   write:     0.0s    0.0%      1.8 ms per call

O que cada mudança fez

Reutilizar conexões cortou o tempo de busca em 70%. No baseline, cada chamada a requests.get() abre uma conexão completamente nova. Através de um proxy, isso significa uma conexão TCP com o proxy, uma requisição de túnel e um handshake TLS completo com o alvo através da saída, antes que um único byte da página chegue. Uma requests.Session mantém as conexões abertas e as reutiliza, então a maioria das requisições pula toda essa configuração. O tempo de busca por página caiu de 1,9 segundos para 0,57. Quase toda biblioteca HTTP tem um equivalente: um objeto cliente que você cria uma vez e compartilha.

Um parser mais rápido cortou o tempo de parsing pela metade. O Beautiful Soup pode usar vários parsers. O html.parser embutido é Python puro; o lxml é escrito em C. A troca cortou o tempo de parsing de 473 para 205 milissegundos por página nessas páginas grandes, com um único argumento alterado. Se você só precisa de alguns campos, selecioná-los diretamente com lxml ou extrair o JSON embutido é ainda mais rápido.

A concorrência reduziu o tempo total pela metade e deslocou o gargalo. A maior parte de uma busca é espera pela rede, e threads podem esperar em paralelo. Com oito workers, a execução caiu de 76 para 40 segundos. Mas observe a coluna de parsing: 205 milissegundos por página se tornaram mais de 2 segundos. No CPython, as threads compartilham um único lock do interpretador, então trabalho intensivo em CPU, como o parsing, não roda em paralelo; oito threads entram na fila para o mesmo núcleo. O cronômetro de estágio torna isso visível: o parsing foi de um terço do tempo de estágio para quase 80%. Nesse ponto, o scraper está limitado pela CPU, e o próximo passo é fazer o parsing em processos separados ou fazer parsing de menos coisas.

Agrupar as escritas em lote não mudou nada, porque as escritas nunca foram o problema: 0,3 segundos de 298 no baseline. A pequena diferença entre as versões 4 e 5 é ruído de execução. Esse é o ponto de medir primeiro. Agrupar escritas de banco de dados em lote é um bom conselho em geral, e teria sido esforço desperdiçado aqui.

Um checklist para um scraper lento

O que você observaCausa provávelO que verificar
Toda requisição é lenta, a CPU está ociosaUma requisição de cada vezAdicione concorrência; a maioria dos scrapers passa o tempo esperando
A primeira requisição para cada host é muito mais lenta que as outrasConfiguração de conexãoReutilize uma sessão ou cliente; verifique se o keep-alive não está desativado
A CPU está em 100% em um núcleoParsing ou outro trabalho limitado por CPUUse um parser mais rápido, extraia menos, ou mova o parsing para processos
O throughput cai conforme a concorrência aumentaO alvo está limitando você, ou sua própria máquina está saturadaObserve os códigos de status e tempos de resposta por alvo; veja nosso guia sobre planejamento de volume de requisições e concorrência
Requisições ocasionais levam de 20 a 30 segundosTimeouts e retentativasDefina timeouts explícitos, limite as retentativas e registre-as; veja retentativa e backoff para web scrapers
O scraper fica mais lento ao longo das horasFilas crescendo, pressão de memória, buffers sem limiteAdicione contrapressão; veja contrapressão e controle de fluxo
As páginas são lentas e pesadasRenderizar um navegador quando o HTML seria suficienteBusque o HTML diretamente, ou bloqueie imagens, mídia e fontes

Quando é realmente o proxy

Às vezes o proxy é o gargalo, e vale a pena saber como identificar isso.

  • Compare com um baseline direto. Cronometre as mesmas requisições sem o proxy, a partir da mesma máquina. Se ambos estiverem lentos, o problema é o alvo ou seu código.
  • Observe o tempo de busca depois da reutilização de conexões. Se as buscas ainda estiverem lentas com uma sessão reutilizada e o alvo for rápido diretamente, a rota através do proxy está adicionando o tempo.
  • Verifique seu direcionamento. Filtros estreitos, como uma cidade específica, deixam menos saídas para escolher. Saídas distantes do alvo adicionam tempo de ida e volta a cada requisição.
  • Observe os percentis, não as médias. Algumas saídas muito lentas podem puxar uma média para cima enquanto a maioria das requisições está bem.

Nosso guia sobre redução de latência com proxies residenciais cobre as configurações do lado do proxy em detalhe. Se você mediu e os números apontam para o proxy, envie-os para nossa equipe de suporte: cronometragens de estágio como as acima tornam um relatório de proxy lento muito mais rápido de investigar.

Perguntas frequentes

Por que meu scraper está lento com proxies?

As causas mais comuns estão no próprio scraper: abrir uma nova conexão para cada requisição, buscar uma página de cada vez e parsing lento. No nosso teste, corrigir isso tornou o mesmo job 7,5 vezes mais rápido sem mudar o proxy.

A reutilização de conexão funciona através de um proxy?

Sim. Uma requests.Session com proxies configurados mantém as conexões abertas, o que economiza a configuração de conexão e TLS a cada requisição. No nosso teste, isso cortou o tempo de busca por página de 1,9 segundos para 0,57.

Quantas threads um scraper deve usar?

O suficiente para manter a rede ocupada, dentro do que o alvo e seu plano permitem. Meça conforme você aumenta: quando o tempo de parsing por página ou as taxas de erro começam a subir, você encontrou o limite. Trabalho limitado por CPU, como o parsing, não acelera com mais threads no CPython.

O BeautifulSoup é lento?

O Beautiful Soup em si é uma camada de conveniência; a maior parte de sua velocidade depende do parser por baixo. Com lxml em vez do html.parser embutido, o parsing foi mais que duas vezes mais rápido no nosso teste.

Conclusão

Antes de culpar o proxy, meça o scraper. Um cronômetro de estágio mostra em minutos se o tempo vai para busca, parsing ou escrita. No nosso teste, reutilizar conexões, trocar parsers e adicionar concorrência tornaram o mesmo job 7,5 vezes mais rápido com o mesmo proxy. Corrija primeiro o maior estágio, meça de novo e pare quando o tempo restante realmente for a rede.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar