Saltar para o conteúdo
Comece grátis
Voltar ao blogue

Melhores práticas de web scraping: um guia do ciclo de vida com dicas e código funcional

Anda MiuțescuÚltima atualização em 33 min read
Melhores práticas de web scraping: um guia do ciclo de vida com dicas e código funcional
Resumo: As melhores práticas de web scraping são os hábitos que mantêm um scraper legal, educado, preciso e barato de executar. Escreva um contrato de dados, prefira APIs ou JSON a HTML, siga o robots.txt tal como a RFC 9309 o define, controle o ritmo por host, respeite o Retry-After e valide cada registo antes de o guardar. Este guia aplica-as a cada fase do ciclo de vida de um scraper, com excertos de Python funcionais.

As melhores práticas de web scraping são os hábitos que garantem que um scraper seja legal, respeitoso, preciso e económico de operar: planeie os dados de que necessita, respeite o ficheiro robots.txt e os termos do site, regule o ritmo das solicitações por servidor, lide com os erros 429 com um mecanismo de recuo, faça a análise de forma defensiva, valide cada registo, monitorize cada execução, verifique o progresso e acompanhe o custo por registo válido.

O guia abaixo aplica estas melhores práticas de web scraping a cada fase do ciclo de vida de um scraper, desde o plano inicial até à fatura mensal, com um pequeno trecho de código em Python sempre que o código tornar o conceito mais claro.

Os trechos de código requerem o Python 3.10 ou mais recente e duas bibliotecas:

pip install requests beautifulsoup4

Guarde cada trecho com o nome de ficheiro indicado acima e execute-o a partir dessa pasta, por exemplo, com python polite_fetch.py.

Fase

Objetivo

Prática-chave

Secção

Plano

Saber como é o «concluído»

Contrato de dados e ordem de origem

Planeamento

Legal e educado

Mantenha-se bem-vindo

RFC 9309 robots.txt, User-Agent honesto

Scraping legal e educado

Taxa

Não sobrecarregue

Limitação por anfitrião, Retry-After, backoff

Taxa de pedidos

Pedidos

Menos falhas

Tempos de espera, sessões, códigos 304

Hábitos de pedidos

Bloqueios

Recuperação com calma

Diagnosticar e, em seguida, escalar um nível

Bloqueios

Renderização

Pague apenas quando for necessário

JSON primeiro, navegador por último

Navegadores sem interface

Análise

Sobreviver a reformulações

Hooks estáveis, soluções alternativas, bytes recebidos

Analisadores

Qualidade dos dados

Confie no resultado

Esquema, motivos de rejeição, deduplicação

Qualidade dos dados

Monitorização

Aviso prévio

Métricas de execução, canários, códigos de saída

Monitorização

Escalar

Cresça sem caos

Frontier, pontos de verificação transacionais

Escalabilidade

Custo

Mantenha a rentabilidade

Custo por registo válido

Custo

Quais são as melhores práticas mais importantes em matéria de web scraping?

Se tiver de adotar apenas dez hábitos, que sejam estes: redija um contrato de dados, dê preferência a APIs e JSON em vez de HTML, cumpra corretamente o ficheiro robots.txt, identifique o seu bot, regule o ritmo de cada host, respeite o Retry-After, defina tempos de espera, valide os registos antes de os guardar, alerte em caso de páginas vazias ou com pouco conteúdo e grave pontos de verificação com os dados que descrevem.

Cada uma destas dez melhores práticas de web scraping tem uma ligação para a secção que a explica.

  1. Elabore um contrato de dados que defina campos, tipos, uma chave única e a atualidade dos dados antes de qualquer código existir.
  2. Dê preferência a uma API oficial ou ao próprio JSON da página em vez de analisar HTML.
  3. Aplique o ficheiro robots.txt da forma definida pela RFC 9309, e não da forma como a sua biblioteca o faz.
  4. Identifique o seu bot com um User-Agent honesto e um endereço de contacto.
  5. Regule o ritmo de cada host separadamente, começando devagar e adaptando-se ao servidor.
  6. Respeite o Retry-After e reduza exponencialmente a frequência com jitter.
  7. Defina tempos de espera para ligação e leitura em cada pedido.
  8. Valide os registos antes de os guardar e registe os motivos de cada rejeição.
  9. Alerte em caso de páginas vazias ou com pouco conteúdo, e não apenas em caso de falhas.
  10. Confirme os pontos de verificação juntamente com os dados que descrevem, para que uma falha nunca deixe uma página parcialmente guardada.

Como se deve planear um projeto de web scraping?

Planeie a partir dos dados. Anote os campos, tipos, atualização e volume de que necessita e, em seguida, escolha a fonte fiável mais económica. Classifique as técnicas de web scraping por custo: primeiro uma API oficial, depois o JSON que a página já carrega, seguidamente os mapas do site para descoberta, depois HTML estático e, apenas como último recurso, um navegador headless. Defina o orçamento antes de escrever código.

Se ainda estiver a decidir se deve ou não fazer o scraping, comece pelas perguntas a responder antes de escrever um scraper.

Escreve primeiro um contrato de dados

Um contrato de dados é uma tabela sucinta que especifica exatamente o que o scraper deve fornecer. Aqui está um exemplo para um monitor de preços de livros:

Campo

Tipo

Obrigatório

Regra

url

cadeia de caracteres (absoluta)

sim

chave única

título

cadeia de caracteres

sim

não vazio

preço

decimal

sim

> 0

moeda

código ISO 4217

sim

por exemplo, GBP

em_estoque

booleano

sim

a partir do texto de disponibilidade

classificação

inteiro 1–5

não

nulo permitido

data_da_recuperação

carimbo de data/hora UTC

sim

definido pelo programa de recolha

O monitor necessita de dados atualizados diariamente, a uma taxa de cerca de 1 000 linhas por execução.

O analisador cumpre este requisito, a validação assegura o seu cumprimento e o monitorização emite um alerta quando uma execução fica aquém do esperado.

Escolha a fonte fiável mais económica

Percorra esta lista e pare na primeira fonte que tenha todos os campos do seu contrato:

  1. Uma API oficial. Quando um site disponibiliza uma, extrair dados de uma API é melhor do que analisar HTML: obtém nomes de campos estáveis, limites documentados e termos de utilização explícitos.
  2. O JSON que a página já carrega. Abre o DevTools, filtra o separador «Rede» por «Fetch/XHR» e atualiza a página. Muitos sites obtêm os seus dados como JSON antes de os apresentar no ecrã.
  3. Sitemaps, para descoberta. Utilize os sitemaps XML do site para descobrir URLs, em vez de rastrear todas as ligações. As respetivas lastmod datas também indicam quais as páginas que foram alteradas desde a sua última execução.
  4. HTML estático. Obtenha e analise a página quando os dados estiverem no código-fonte.
  5. Uma página renderizada. Execute um navegador sem interface apenas quando os dados não existirem em mais nenhum outro local.

Cada passo a mais implica mais pedidos, mais código de análise e mais falhas quando o site muda.

Eis a etapa 2 na prática: a versão em JavaScript do quotes.toscrape.com carrega as suas cotações a partir de um endpoint JSON paginado, pelo que nunca se lida com o HTML.

json_endpoint.py

"""Prefer the JSON a page already loads over parsing its HTML."""
import requests

url = "https://quotes.toscrape.com/api/quotes"
page, quotes = 1, []
while True:
    data = requests.get(url, params={"page": page}, timeout=(5, 30),
                        headers={"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}).json()
    quotes += [(q["text"], q["author"]["name"]) for q in data["quotes"]]
    if not data["has_next"]:
        break
    page += 1
print(len(quotes), "quotes from", page, "JSON pages; first:", quotes[0][1])
  • /api/quotes?page=N é a mesma solicitação que a página faz no navegador, encontrada no separador «Rede».
  • has_next é a condição de paragem, pelo que nunca terá de adivinhar a última página nem ir buscar páginas vazias.
  • Não há analisador de HTML, porque os campos chegam com nome e tipo definidos.

Calcular o orçamento antes de escrever o código

Um orçamento aproximado mostra se o plano é sequer viável: páginas × frequência ÷ taxa permitida = duração da execução.

Digamos que precise de 10 000 páginas de produtos por dia e que o site tolere uma solicitação a cada dois segundos. Isso equivale a 10 000 × 2 = 20 000 segundos, ou cerca de 5,6 horas de rastreamento por dia. Se isso não for viável, precisa de menos páginas, de um rastreamento incremental ou de uma fonte com mais registos por pedido. Rastrear mais rapidamente não é uma opção, porque é o site que define a taxa.

Guarde as respostas em bruto durante alguns dias

Armazene o HTML ou JSON bruto que obtiver durante alguns dias, comprimido e indexado por URL e hora de obtenção. Um seletor avariado ou um novo campo significa, então, reanalisar as cópias armazenadas, em vez de rastrear o site novamente.

Extraia apenas dados públicos para os quais tenha um motivo legítimo de recolher, leia os termos do site e mantenha-se afastado de áreas que exijam início de sessão, a menos que tenha autorização para tal. Obtenha o ficheiro robots.txt e aplique-o conforme definido pela RFC 9309, identifique o seu bot com um URL de contacto, evite dados pessoais de que não necessita e nunca se faça passar por um rastreador de um motor de busca.

Como funciona, na prática, a correspondência do ficheiro robots.txt?

O ficheiro robots.txt é uma norma formal desde 2022, quando a IETF publicou a RFC 9309, o Protocolo de Exclusão de Robôs. As regras que importam:

  • Encontre primeiro o seu grupo. Os grupos começam com User-agent linhas. O seu rastreador segue o grupo que nomeia o seu token de produto, e o * grupo aplica-se apenas se nenhum outro o fizer.
  • A correspondência mais longa prevalece. Quando várias regras correspondem a um caminho, a mais específica (aquela com mais caracteres) é a que prevalece.
  • A permissão decide em caso de empate. Se um Allow e uma Disallow regra corresponderem com comprimento igual, o caminho é permitido.
  • Dois caracteres especiais. * corresponde a qualquer sequência de caracteres e $ marca o fim do caminho, pelo que Disallow: /*.pdf$ bloqueia todas as URLs cujo caminho termine em .pdf.
  • Os códigos de estado alteram o significado. Um erro 4xx no robots.txt significa que um rastreador pode aceder a tudo. Um erro 5xx ou um pedido falhado significa que deve assumir uma proibição total.
  • O armazenamento em cache tem um limite. Não utilize uma cópia em cache por mais de 24 horas.
  • O «crawl-delay» não faz parte da norma e o Google ignora-o. Respeite-o na mesma quando estiver presente, uma vez que indica o que o proprietário do site pretende.

O RFC também indica que estas regras não constituem uma forma de autorização de acesso: o ficheiro é um pedido publicado pelo proprietário do site, não um bloqueio.

Por que razão o seu analisador de robots.txt pode não estar em conformidade com a norma

O `urllib.robotparser` do Python é a escolha óbvia para scrapers em Python, mas verifica as regras pela ordem em que aparecem no ficheiro e pára na primeira correspondência, em vez de escolher a mais longa. Além disso, trata os caminhos das regras como prefixos simples, pelo que * e $ não têm qualquer significado especial.

O trecho de código abaixo executa um pequeno ficheiro robots.txt utilizando ambas as abordagens e apresenta os resultados lado a lado. O «polite fetcher» importa posteriormente o seu parse_robots() e rfc9309_allowed() .

robots_match.py

"""robots.txt per RFC 9309: pick the group for your user agent, then the
longest matching rule wins, Allow wins ties, '*' matches any characters and
'$' anchors the end of the path."""
import re
import urllib.robotparser


def parse_robots(text, user_agent):
    """Return (rules, crawl_delay) for the group that applies to user_agent."""
    groups, current, in_agent_lines = [], None, False
    for raw in text.splitlines():
        line = raw.split("#", 1)[0].strip()
        if ":" not in line:
            continue
        key, value = (part.strip() for part in line.split(":", 1))
        key = key.lower()
        if key == "user-agent":
            if not in_agent_lines:  # a new group starts
                current = {"agents": [], "rules": [], "delay": None}
                groups.append(current)
            current["agents"].append(value.lower())
            in_agent_lines = True
            continue
        in_agent_lines = False
        if current is None:
            continue
        if key in ("allow", "disallow"):
            current["rules"].append((key, value))
        elif key == "crawl-delay":  # not in RFC 9309, but a clear signal
            try:
                current["delay"] = float(value)
            except ValueError:
                pass
    token = user_agent.split("/")[0].lower()  # product token, e.g. "acme-price-monitor"
    matched = [g for g in groups if token in g["agents"]] or \
              [g for g in groups if "*" in g["agents"]]
    rules = [rule for g in matched for rule in g["rules"]]
    delay = next((g["delay"] for g in matched if g["delay"] is not None), None)
    return rules, delay


def _pattern(rule_path):
    regex = re.escape(rule_path).replace(r"\*", ".*")
    if regex.endswith(r"\$"):
        regex = regex[:-2] + "$"
    return re.compile(regex)


def rfc9309_allowed(rules, path):
    """rules: list of ("allow"|"disallow", path) for the group that applies to you."""
    best_len, verdict = -1, True  # no matching rule means allowed
    for kind, rule_path in rules:
        if not rule_path:
            continue  # an empty Disallow allows everything
        if _pattern(rule_path).match(path):
            length = len(rule_path)
            if length > best_len or (length == best_len and kind == "allow"):
                best_len, verdict = length, kind == "allow"
    return verdict


if __name__ == "__main__":
    robots_txt = """User-agent: *
Disallow: /catalogue/
Allow: /catalogue/category/
Disallow: /*.pdf$

User-agent: acme-price-monitor
Disallow: /checkout/
Crawl-delay: 2
"""
    stdlib = urllib.robotparser.RobotFileParser()
    stdlib.parse(robots_txt.splitlines())
    rules, _ = parse_robots(robots_txt, "mybot/1.0")
    for path in ["/catalogue/category/books_1/index.html", "/catalogue/item_1.html",
                 "/files/report.pdf", "/files/report.pdf?v=2", "/index.html"]:
        print(f"{path:42} RFC 9309: {str(rfc9309_allowed(rules, path)):5}  "
              f"urllib.robotparser: {stdlib.can_fetch('mybot', path)}")
    rules, delay = parse_robots(robots_txt, "acme-price-monitor/1.2 (+https://example.com/bot)")
    print("acme-price-monitor group:", rules, "crawl-delay:", delay)
    print("/catalogue/item_1.html for acme-price-monitor:", rfc9309_allowed(rules, "/catalogue/item_1.html"))

O que a comparação revela:

  • /files/report.pdf é permitido pelo urllib.robotparser, apesar de Disallow: /*.pdf$ o proíba ao abrigo da norma, porque o analisador da biblioteca padrão lê o * literalmente.
  • /catalogue/category/books_1/index.html é bloqueado por urllib.robotparser, porque Disallow: /catalogue/ aparece primeiro no ficheiro. De acordo com a norma, o mais longo Allow: /catalogue/category/ prevalece e a página é permitida.
  • acme-price-monitor tem o seu próprio grupo, que substitui o * grupo na totalidade. Para esse bot, /catalogue/item_1.html é permitido com um atraso de rastreamento de dois segundos, porque nenhuma das * regras se aplicam.

Utilize um comparador em conformidade com as normas, ou, pelo menos, teste o seu analisador em relação às regras específicas de que depende.

Termos de serviço, inícios de sessão e dados pessoais

Algumas regras abrangem a maioria das situações:

  • Leia primeiro os termos. Iniciar sessão significa, normalmente, aceitá-los, e muitos proíbem o acesso automatizado; por isso, trate as áreas autenticadas como proibidas sem autorização.
  • Recolha o mínimo de dados pessoais, ou nenhum. Ignore nomes, e-mails ou detalhes de perfil que o seu contrato de dados não mencione; as leis de proteção de dados, como o RGPD, aplicam-se aos dados pessoais, quer os tenha recolhido ou não.
  • Não contorne barreiras de acesso pago, inícios de sessão ou CAPTCHAs. Estes indicam que o proprietário pretende limitar o acesso.

Estes são hábitos de trabalho, não aconselhamento jurídico. Para obter detalhes jurídicos, consulte o nosso guia sobre se o web scraping é legal.

Identifique o seu scraper de forma honesta

Utilize um User-Agent estável que indique o nome do seu produto, a sua versão e uma forma de o contactar:

acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)

Um proprietário de site que detete tráfego invulgar proveniente dessa string pode contactá-lo em vez de bloquear um intervalo de IP, e os autores do ficheiro robots.txt obtêm um token do produto para direcionar.

Nunca envie um User-Agent «Googlebot». O Google documenta como os proprietários de sites podem verificar os seus rastreadores através de pesquisas DNS reversas e diretas ou dos seus intervalos de IP publicados; assim, um User-Agent falso falha na verificação e parece enganador quando detetado.

A que velocidade pode fazer o scraping sem sobrecarregar um site?

Não existe uma taxa segura universal. Começa com uma ou duas solicitações simultâneas por host e uma pausa de cerca de um segundo; depois, deixa que o servidor defina o ritmo: abranda quando a latência aumentar, pára e espera quando receberes um erro 429 ou 503, respeita o Retry-After e diminui exponencialmente a frequência em caso de jitter.

Defina o orçamento por host, não por scraper

Dez trabalhadores que façam uma pausa «educada» podem, mesmo assim, enviar dez pedidos paralelos para um domínio. Controle o ritmo por onde cada pedido a um host passa: um objeto «fetcher» ou um limitador partilhado entre máquinas.

Deixe que o servidor defina o ritmo

O aumento da latência significa que o servidor está a trabalhar mais, por isso aumente o atraso antes que os erros comecem. Programe rastreios de grande dimensão para as horas de menor movimento do site, normalmente à noite, no fuso horário do seu mercado principal.

Lide corretamente com os códigos 429 e Retry-After

O código 429 Too Many Requests (RFC 6585) significa que enviou demasiadas solicitações num determinado período de tempo, e um 503 significa que o servidor está sobrecarregado ou em manutenção. Ambos podem incluir o cabeçalho «Retry-After», e a Semântica HTTP (RFC 9110) permite dois formatos para o seu valor:

Retry-After: 120
Retry-After: Fri, 09 Oct 2026 07:30:00 GMT

O primeiro é em segundos e o segundo numa data HTTP; um scraper que apenas processe um deles irá falhar ou ignorar o cabeçalho quando se deparar com o outro. Trate o valor como um tempo mínimo de espera.

Aumente o intervalo de forma exponencial, com jitter

Quando não houver Retry-After, ou se uma nova tentativa falhar novamente, aguarde mais tempo a cada vez:

wait = min(cap, base × 2^attempt) × random(0.5 to 1.5)

Utilize o maior valor entre esse tempo de espera e qualquer valor de Retry-After. O fator aleatório é o jitter: sem ele, os workers que falharam em simultâneo tentam novamente em simultâneo e atingem o servidor numa onda sincronizada.

Refaça apenas o que é possível repetir

Refaça tentativas para respostas 429, 500, 502, 503 e 504 e tempos de espera de rede em pedidos GET idempotentes. Não tente novamente um 404, uma vez que a página não vai voltar, nem insista num 403, uma vez que a mesma solicitação recebe a mesma resposta. Limite as tentativas e registe cada desistência para que os URLs bloqueados apareçam nas suas métricas.

Um fetcher educado numa única classe

A PoliteFetcher classe abaixo combina esta secção com a seguinte, importando parse_robots() e rfc9309_allowed() do fragmento de código do robots.txt; por isso, mantém ambos os ficheiros numa única pasta.

polite_fetch.py

"""A polite, resilient fetcher: robots.txt, per-host pacing, timeouts,
retries with backoff and jitter, Retry-After, and conditional requests."""
import email.utils
import random
import time
from urllib.parse import urlsplit

import requests

from robots_match import parse_robots, rfc9309_allowed  # Snippet 2

USER_AGENT = "acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)"
RETRYABLE = {429, 500, 502, 503, 504}
ROBOTS_TTL = 24 * 3600  # RFC 9309: don't trust a cached copy for more than 24 h


class PoliteFetcher:
    def __init__(self, min_delay=1.0, max_retries=4, timeout=(5, 30)):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = USER_AGENT
        self.min_delay, self.max_retries, self.timeout = min_delay, max_retries, timeout
        self.robots, self.last_hit, self.validators = {}, {}, {}

    def _robots(self, host):
        cached = self.robots.get(host)
        if cached and time.monotonic() - cached[0] < ROBOTS_TTL:
            return cached[1], cached[2]
        try:
            resp = self.session.get(host + "/robots.txt", timeout=self.timeout)
        except requests.RequestException:
            resp = None
        if resp is None or resp.status_code >= 500:
            rules, delay = [("disallow", "/")], None  # unreachable: assume full disallow
        elif resp.status_code >= 400:
            rules, delay = [], None  # unavailable (e.g. 404): no restrictions
        else:
            rules, delay = parse_robots(resp.text, USER_AGENT)
        self.robots[host] = (time.monotonic(), rules, delay)
        return rules, delay

    def allowed(self, url):
        parts = urlsplit(url)
        rules, _ = self._robots(f"{parts.scheme}://{parts.netloc}")
        path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
        return rfc9309_allowed(rules, path)

    def _pace(self, host):
        _, crawl_delay = self._robots(host)
        delay = max(self.min_delay, crawl_delay or 0)
        wait = self.last_hit.get(host, 0) + delay - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        self.last_hit[host] = time.monotonic()

    @staticmethod
    def _retry_after(value):
        if not value:
            return None
        if value.isdigit():
            return int(value)
        when = email.utils.parsedate_to_datetime(value)  # HTTP-date form
        return max(0, when.timestamp() - time.time())

    def get(self, url):
        if not self.allowed(url):
            raise PermissionError(f"robots.txt disallows {url}")
        parts = urlsplit(url)
        host = f"{parts.scheme}://{parts.netloc}"
        headers = {}
        etag, modified = self.validators.get(url, (None, None))
        if etag:
            headers["If-None-Match"] = etag
        if modified:
            headers["If-Modified-Since"] = modified
        for attempt in range(self.max_retries + 1):
            self._pace(host)
            try:
                resp = self.session.get(url, headers=headers, timeout=self.timeout)
            except (requests.ConnectionError, requests.Timeout):
                resp = None
            if resp is not None and resp.status_code not in RETRYABLE:
                if resp.status_code == 200:
                    self.validators[url] = (resp.headers.get("ETag"),
                                            resp.headers.get("Last-Modified"))
                return resp  # 200, 304, or a non-retryable error for the caller
            if attempt == self.max_retries:
                break
            backoff = min(60, 2 ** attempt) * random.uniform(0.5, 1.5)
            server_hint = self._retry_after(resp.headers.get("Retry-After")) if resp is not None else None
            time.sleep(max(backoff, server_hint or 0))
        raise RuntimeError(f"gave up on {url} after {self.max_retries + 1} attempts")


if __name__ == "__main__":
    f = PoliteFetcher(min_delay=1.0, max_retries=2)
    for url in ["https://books.toscrape.com/", "https://httpbin.org/etag/abc123",
                "https://httpbin.org/etag/abc123", "https://httpbin.org/status/404"]:
        r = f.get(url)
        print(r.status_code, len(r.content), "bytes", url)
    for url in ["https://httpbin.org/deny", "https://httpbin.org/status/503"]:
        try:
            f.get(url)
        except (PermissionError, RuntimeError) as exc:
            print("expected:", exc)

As linhas principais:

  • _robots() recupera o ficheiro robots.txt com o seu próprio User-Agent, trata um código 4xx como «sem restrições» e um código 5xx ou erro de rede como «não aceder», e volta a recuperar o ficheiro assim que a cópia em cache tiver 24 horas.
  • _pace() aplica o atraso por host e utiliza o Crawl-delay do site quando este for superior ao seu próprio valor mínimo.
  • RETRYABLE limita as tentativas de repetição a limites de taxa e erros de servidor. Um código 404 é devolvido diretamente ao chamador.
  • _retry_after() lida tanto com o formato em segundos como com o formato de data HTTP, e o ciclo fica em espera pelo tempo que for mais longo, seja o intervalo de espera ou a indicação do servidor.

A demonstração na parte inferior obtém um 304 numa solicitação repetida, devolve um 404 sem repetir a tentativa, rejeita um caminho proibido por robots antes de o enviar e desiste perante um 503 permanente após atingir o limite de tentativas.

Que hábitos de solicitação evitam a maioria das falhas de scraping?

A maioria das falhas resulta de pedidos mal elaborados, e não de defesas inteligentes. Defina sempre tempos de espera para ligação e leitura, reutilize uma sessão por anfitrião, envie um conjunto de cabeçalhos consistente e honesto, aceite compressão, ignore imagens e tipos de letra de que não necessita, normalize os URLs para evitar duplicados e utilize pedidos condicionais para que as páginas inalteradas sejam devolvidas como um 304 vazio.

  • Defina tempos de espera separados para ligação e leitura em cada pedido, por exemplo, 5 segundos e 30 segundos. No requests, uma chamada sem um limite de tempo pode ficar à espera indefinidamente e bloquear um trabalhador.
  • Utilize um requests.Session por host. Isso reutiliza as ligações, evitando handshakes TLS repetidos e aliviando a carga no servidor.
  • Envie o mesmo conjunto de cabeçalhos realista e honesto em cada pedido, com um User-Agent que o identifique. O nosso guia de cabeçalhos HTTP para web scraping aborda quais os cabeçalhos que importam e em que ordem.
  • Mantenha a compressão ativada. requests Solicita gzip por predefinição; se criar os cabeçalhos manualmente, mantenha Accept-Encoding, uma vez que o HTML comprimido ocupa uma fração do tamanho na rede.
  • Limpe os URLs antes de estes entrarem na sua fila: remova os parâmetros de rastreamento e #fragments, siga rel="canonical" ligações e elimine duplicados.
  • Ignore os pixels de rastreamento e as visualizações duplicadas de paginação; recupere apenas o que vai analisar.

Armazene páginas em cache e utilize pedidos condicionais

Quando uma resposta contiver um ETag ou Last-Modified , guarde-o e reenvie-o na próxima vez como If-None-Match ou If-Modified-Since. Com estas solicitações condicionais, uma página inalterada recebe um 304 Not Modified sem corpo, o que poupa largura de banda para si e trabalho para o site.

PoliteFetcher.get() Já faz isto: o seu validators dicionário armazena o ETag e Last-Modified das últimas 200 respostas e reenvia-as na próxima vez. Um código de estado 304 é devolvido tal como está, pelo que o seu código reutiliza a cópia armazenada.

O que deve fazer quando um site começa a bloquear o seu scraper?

Diagnostique antes de escalar o problema. Analise o código de resposta (403, 429, 503 ou um 200 que apresenta um CAPTCHA ou uma página vazia), verifique primeiro a sua própria taxa de acesso e os cabeçalhos e, em seguida, avance passo a passo: abrande o ritmo, corrija os cabeçalhos, adicione proxies testados, renderize a página e, por fim, encaminhe os alvos mais difíceis para uma API de scraping gerida.

Código de erro

Causa provável

Primeira resposta

429 Demasiadas solicitações

Limite de taxa

Respeitar o Retry-After; reduzir a simultaneidade por anfitrião

503 com Retry-After

Sobrecarga ou manutenção

Aguarde o tempo indicado; reduza a frequência

403 Proibido

IP bloqueado, incompatibilidade de cabeçalho ou é necessário iniciar sessão

Verifique os cabeçalhos e os termos; não insista com tentativas repetidas

200 com um desafio ou página CAPTCHA

Defesa contra bots

Trate-o como uma falha na validação; abrande o ritmo e, em seguida, escale

200 com um shell de dados vazio

Conteúdo renderizado em JavaScript

Procure a chamada JSON; renderize apenas se necessário

Aumento dos tempos de espera

Sobrecarga do servidor ou limitação de tráfego

Aumente os atrasos; reduza a simultaneidade

Não tem a certeza do que significa um código? Consulte a nossa lista de erros de proxy e de estado HTTP.

Os bloqueios «suaves» são os mais perigosos: uma resposta 200 que apresenta uma página de desafio ou ausência de dados parece um sucesso, e apenas as verificações de dados apresentadas mais adiante neste guia conseguem detetá-la.

Os sites bloqueiam os scrapers porque o tráfego automatizado intenso sobrecarrega os servidores e pode prejudicar o negócio. A lista de ameaças automatizadas da OWASP identifica o scraping como uma ameaça específica, a OAT-011, e a maioria das defesas tem como alvo o volume e os padrões de abuso; por isso, o tráfego educado e bem identificado ativa menos dessas defesas.

Escalem nesta ordem:

  1. Reduzir a velocidade e diminuir a simultaneidade no host afetado.
  2. Corrija os seus cabeçalhos para que sejam consistentes e completos.
  3. Adicione proxies, começando por escolher o tipo de proxy adequado para o alvo. Teste os seus proxies antes de aumentar a escala e, em seguida, distribua a carga com proxies rotativos.
  4. Renderize a página num navegador sem interface gráfica se os dados só aparecerem após a execução do JavaScript.
  5. Envie os alvos mais difíceis para uma API de scraping gerida.

Para identificação por impressão digital, CAPTCHAs e navegadores invisíveis, utilize o nosso guia para fazer scraping sem ser bloqueado.

Quando é que realmente precisa de um navegador sem interface gráfica?

Apenas quando os dados não estiverem no HTML nem numa chamada JSON efetuada pela página. Compare o código-fonte da página com o que o DevTools mostra; se os dados chegarem através de um pedido XHR, aceda diretamente a esse ponto final. Renderize apenas as páginas que realmente necessitem de JavaScript, bloqueie imagens e tipos de letra e reutilize contextos de navegador, pois a renderização é muito mais lenta e dispendiosa.

Um navegador headless executa o JavaScript da página sem uma janela visível: a forma mais completa de obter uma página, e a mais dispendiosa. Dois testes rápidos permitem-lhe saber se precisa de um:

  • Compare a visualização do código-fonte com o painel «Elementos». Procure no código-fonte da página (Ctrl+U ou Cmd+Option+U) um valor que pretenda, como um preço. Se estiver lá, basta uma solicitação HTTP simples; se apenas o painel «Elementos» do DevTools o mostrar, foi o JavaScript que o criou.
  • Verifique o separador «Rede» para ver se existe uma solicitação Fetch/XHR que transporte os dados, como no exemplo anterior do endpoint JSON, e chame-a diretamente.

Se precisar de renderização, envie apenas os URLs que falharem nesses testes para o navegador e bloqueie imagens, tipos de letra e multimédia. Com o Playwright ou uma ferramenta semelhante, execute um único processo do navegador com um contexto novo por tarefa, em vez de um novo navegador por página.

Os utilizadores do Scrapy podem renderizar JavaScript apenas para as solicitações que necessitem disso e manter o resto do rastreio no rápido descarregador HTTP do Scrapy.

Como se escrevem analisadores que resistem a alterações de layout?

Ancore os seletores a elementos que raramente mudam (IDs, atributos de dados, estrutura semântica, JSON-LD incorporado) em vez de classes de estilo, mantenha uma lista ordenada de seletores de recurso por campo, descodifique os bytes brutos corretamente e faça com que a falta de campos obrigatórios resulte num erro evidente. Um analisador que devolve silenciosamente espaços em branco é pior do que um que entra em falha.

Escolha seletores que resistam a uma reformulação

Classes de estilo como .text-lg.font-bold mudam sempre que um designer altera a página. Os IDs e data-* atributos, itemprop microdados, etiquetas semânticas e blocos JSON-LD alteram-se com muito menos frequência, porque outro código depende deles. Mantenha também os seletores curtos, uma vez que um caminho completo de <body> deixa de funcionar quando qualquer div envolvente é movida.

Os seletores CSS são mais fáceis de ler, enquanto o XPath permite selecionar por texto e percorrer a árvore. A comparação entre o XPath e os seletores CSS aborda as vantagens e desvantagens.

Mantenha opções alternativas ordenadas para cada campo

Atribua a cada campo uma lista de seletores, começando pelo mais específico, e utilize o primeiro que corresponder. O SELECTORS dicionário no trecho de código abaixo funciona desta forma.

Decodifique bytes, não texto adivinhado

Esteja atento ao bug de codificação do «£». Se um servidor enviar Content-Type: text/html sem especificar o conjunto de caracteres, requests recorre à ISO-8859-1 ao construir resp.text, pelo que uma página em UTF-8 é descodificada incorretamente, o «£» transforma-se em «Â£» e todos os preços não são analisados. Em vez disso, passa resp.contentos bytes em bruto ao seu analisador; o BeautifulSoup irá ler o próprio <meta charset>.

Teste em páginas guardadas

Guarde algumas páginas reais como exemplos de teste. Quando um seletor mudar, volte a analisar as respostas em bruto armazenadas e compare os resultados antes e depois.

Este trecho de código transforma as fichas de livros numa página do catálogo books.toscrape.com em registos tipados, valida cada um deles e imprime um relatório de qualidade; em seguida, fornece ao analisador uma ficha com uma classe de preço renomeada para mostrar que uma alteração no layout foi detetada.

parse_validate.py

"""Parse defensively, normalize, validate, and report data quality."""
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# Ordered fallbacks: the first selector that matches wins.
SELECTORS = {
    "title": ["h3 a[title]", "h3 a"],
    "price": ["p.price_color", "[itemprop=price]"],
    "availability": ["p.availability", ".instock"],
    "url": ["h3 a[href]"],
}
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}


@dataclass
class Book:
    url: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    rating: int | None
    fetched_at: str  # provenance: when this record was fetched (UTC)


def first(card, field):
    for css in SELECTORS[field]:
        node = card.select_one(css)
        if node is not None:
            return node
    return None


def parse_card(card, page_url, fetched_at=""):
    errors = []
    link, title_node = first(card, "url"), first(card, "title")
    price_node, stock_node = first(card, "price"), first(card, "availability")
    title = (title_node.get("title") or title_node.get_text(strip=True)) if title_node else ""
    if not title:
        errors.append("title missing")
    raw_price = price_node.get_text(strip=True) if price_node else ""
    currency = "GBP" if "£" in raw_price else "UNKNOWN"
    try:
        price = Decimal(raw_price.replace("£", "").replace(",", ""))
        if price <= 0:
            errors.append("price not positive")
    except InvalidOperation:
        price = Decimal("NaN")
        errors.append(f"price unparseable: {raw_price!r}")
    rating_node = card.select_one("p.star-rating")
    rating = next((RATINGS[c] for c in (rating_node.get("class", []) if rating_node else [])
                   if c in RATINGS), None)
    if link is None:
        errors.append("url missing")
    book = Book(url=urljoin(page_url, link["href"]) if link else "", title=title, price=price,
                currency=currency, in_stock="in stock" in (stock_node.get_text(" ", strip=True).lower()
                                                          if stock_node else ""),
                rating=rating, fetched_at=fetched_at)
    return book, errors


def scrape(page_url):
    resp = requests.get(page_url, timeout=(5, 30),
                        headers={"User-Agent": "acme-catalogue-bot/0.1 (+mailto:data@example.com)"})
    resp.raise_for_status()
    fetched_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
    # Pass bytes, not resp.text: with no charset in Content-Type, requests guesses
    # ISO-8859-1 and "£" becomes "£". BeautifulSoup reads the page's <meta charset>.
    soup = BeautifulSoup(resp.content, "html.parser")
    cards = soup.select("article.product_pod")
    seen, good, rejected = set(), [], []
    for card in cards:
        book, errors = parse_card(card, page_url, fetched_at)
        if book.url in seen:
            errors.append("duplicate url")
        seen.add(book.url)
        (rejected if errors else good).append((book, errors))
    report = {
        "page": page_url,
        "cards_found": len(cards),
        "valid": len(good),
        "rejected": len(rejected),
        "missing_rating": sum(1 for b, _ in good if b.rating is None),
        "sample": {k: str(v) for k, v in asdict(good[0][0]).items()} if good else None,
    }
    return good, rejected, report


if __name__ == "__main__":
    _, rejected, report = scrape("https://books.toscrape.com/catalogue/page-1.html")
    print(json.dumps(report, indent=2))
    for book, errors in rejected:
        print("REJECTED", book.url, errors)
    # Simulate a layout change: the price class is renamed.
    html = '<article class="product_pod"><h3><a href="x.html" title="T">T</a></h3><p class="cost">£1.00</p></article>'
    card = BeautifulSoup(html, "html.parser").select_one("article")
    print("after layout change:", parse_card(card, "https://books.toscrape.com/")[1])
  • first() percorre a lista de alternativas de cada campo, para que uma classe renomeada não esvazie uma coluna.
  • BeautifulSoup(resp.content, ...) passa bytes, o que evita o erro de codificação acima referido.
  • parse_card() recolhe erros em vez de os levantar, para que um cartão incorreto não interrompa a página e cada rejeição tenha um motivo.
  • A alteração de layout simulada devolve «preço não analisável» em vez de um preço em branco, que é o sinal que pretende nos seus registos.

Encontrará mais informações sobre como transformar páginas em bruto em campos no nosso guia de análise de dados.

Como é que verifica a qualidade dos dados extraídos?

Trate cada registo como não fiável até que passe num esquema: campos obrigatórios presentes, tipos corretos, intervalos razoáveis e uma chave única. Utilize o tipo «Decimal» para valores monetários, normalize moedas e datas, elimine duplicados com base na chave, armazene o URL de origem e a hora de recolha, e gere um relatório por execução com valores válidos, rejeitados e em falta.

A qualidade dos dados é onde os scrapers falham silenciosamente: a recolha é bem-sucedida, o analisador é executado e valores errados aparecem nos relatórios durante semanas. Estas melhores práticas de recolha de dados impedem isso:

  • Defina o esquema no código. A Book dataclass no fragmento de código de análise é o contrato de dados em Python; o pydantic adiciona mais verificações, mas uma dataclass não necessita de dependências adicionais.
  • Rejeite em vez de forçar. Quando um preço não for analisado, rejeite o registo indicando o motivo, em vez de armazenar um zero; os motivos de rejeição são o seu primeiro aviso de uma alteração no layout.
  • Armazene valores monetários como Decimal, nunca como um float, uma vez que os floats não conseguem representar a maioria dos preços com exatidão. Normalize as moedas para códigos ISO 4217 e os carimbos de data/hora para UTC.
  • Fique atento a picos nas taxas de valores nulos ao nível dos campos em relação à última execução.
  • Elimine duplicados com base no URL canónico ou numa chave de negócio, como um SKU. As duplicatas significam normalmente um ciclo de paginação ou o mesmo artigo listado em duas categorias.
  • Atribua a cada linha um source_url e fetched_at, para que qualquer valor possa ser rastreado até à sua página e momento.
  • Produza um relatório de qualidade por execução: cartões encontrados, válidos, rejeitados e com campos opcionais em falta. O trecho de análise apresenta exatamente isso, e a secção seguinte monitoriza-o.

Verifique

Regra de exemplo

Deteta

Campos obrigatórios

título e preço presentes

Alterações no layout

Tipos

O preço é interpretado como decimal

Erros de codificação, seletor incorreto

Intervalos

0 < preço < 10 000

Confusões entre unidades ou moedas

Exclusividade

uma linha por URL

Loops de paginação, duplicados

Taxa de valores nulos

Valores nulos nas classificações não superiores a 5 pontos acima da última execução

Falhas parciais

Proveniência

source_url e fetched_at definidos

Registos não rastreáveis

Como monitorizar um web scraper em produção?

Acompanhe algumas métricas de execução e configure alertas em caso de alterações, não apenas em caso de falhas: taxa de sucesso por código de estado, registos por página, páginas vazias, taxas de campos nulos, novas tentativas, latência, atualização e custo por registo válido. Configure o trabalho para que termine com um valor diferente de zero quando os limiares forem ultrapassados, para que seja o seu agendador, e não um cliente, a detetar o problema em primeiro lugar.

Um scraper que entra em falha é fácil de detectar; um que recolhe silenciosamente a informação errada, não. Considere os limiares abaixo como pontos de partida para afinação, não como valores de referência.

Métrica

Por que é importante

Exemplo de alerta

Taxa de sucesso por código de estado

Deteta bloqueios e interrupções

A percentagem de códigos 2xx desce abaixo dos 95%

Registos por página

Deteta alterações no layout

Abaixo de 50% da mediana de 7 dias

Páginas vazias

Erros silenciosos

Qualquer página com 0 registos

Taxa de campos nulos

Falha parcial

Aumento de 10 pontos em relação à última execução

Número de tentativas por pedido

Limitação

Média superior a 0,3

Latência p95

Carga do servidor

Duplica em relação ao valor de referência, pelo que se verifica um abrandamento

Atualização

Execuções falhadas

Dados mais antigos do que o seu objetivo

Custo por registo válido

Desvio orçamental

Aumento de 25% em relação à semana anterior

Dois hábitos tornam estas métricas mais úteis:

  • Recupere algumas URLs «canário» com valores conhecidos, como um produto com um título fixo, em cada execução. Um «canário» errado significa que o analisador falhou, não os dados.
  • Alerte com base em variações, em vez de números fixos. Uma queda no número de registos por página em relação à mediana de 7 dias é um melhor indicador de alteração no layout do que qualquer contagem absoluta.

O trecho de rastreamento na secção seguinte termina com uma health() função que assinala erros de recuperação, páginas vazias e páginas com pouco conteúdo, e depois termina com o estado 1. Agendadores como o cron, o systemd, o Airflow e os executores de CI tratam isso como uma falha, pelo que o alerta provém de ferramentas que já executa.

Para agendamento, entrega e repetição, veja como criar tarefas de scraping que possa recuperar.

Como é que se pode escalar o scraping da Web sem o comprometer?

Escale a gestão de registos antes dos trabalhadores, porque técnicas avançadas de scraping da Web, como o rastreamento distribuído, dependem disso. Mantenha uma fronteira de URLs deduplicadas, confirme os registos de cada página e o seu sinalizador «concluído» numa única transação, escreva de forma idempotente para que as reexecuções não dupliquem linhas, imponha limites de taxa por domínio em todos os trabalhadores e rastreie de forma incremental utilizando mapas do site e pedidos condicionais, em vez de voltar a recuperar tudo.

As melhores práticas para o web scraping escalável dizem respeito, principalmente, ao estado, porque um maior número de trabalhadores multiplica quaisquer erros na forma como se acompanha o progresso.

  • Utilize uma «fronteira», não uma lista: uma fila de URLs deduplicadas que permite que um rastreamento descubra novas páginas à medida que avança. Não conhece as «fronteiras»? Comece com o rastreamento da Web em Python.
  • Torne os pontos de verificação transacionais. Confirme os registos de uma página, os links descobertos e o sinalizador «concluído» numa única transação, para que, em caso de falha, todo o trabalho dessa página seja mantido ou nenhum seja perdido.
  • Escreve de forma idempotente. As chaves primárias ou os «upserts» fazem com que uma gravação duplicada seja uma operação nula, pelo que as repetições e novas tentativas são seguras.
  • Retome em vez de reiniciar. A execução seguinte continua a partir do primeiro URL inacabado.

Para ver como funciona, rastreie o site quotes.toscrape.com com o fragmento de código abaixo, que mantém uma fronteira SQLite e confirma o trabalho de cada página numa única transação. Execute python resumable_crawl.py 3 para parar após três páginas e, em seguida, execute python resumable_crawl.py novamente: a segunda execução retoma onde a primeira parou, sem linhas duplicadas.

resumable_crawl.py

"""Resumable pagination crawl with a SQLite checkpoint and a run health check."""
import json
import sqlite3
import sys
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

START = "https://quotes.toscrape.com/page/1/"
HEADERS = {"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}


def open_db(path="crawl.db"):
    db = sqlite3.connect(path)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS frontier (url TEXT PRIMARY KEY, done INTEGER DEFAULT 0);
        CREATE TABLE IF NOT EXISTS quotes (text TEXT, author TEXT, tags TEXT,
                                           page TEXT, PRIMARY KEY (text, author));
    """)
    db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)", (START,))
    db.commit()
    return db


def crawl(db, max_pages):
    stats = {"pages": 0, "errors": 0, "rows": 0, "empty_pages": 0, "rejected": 0}
    while stats["pages"] < max_pages:
        row = db.execute("SELECT url FROM frontier WHERE done = 0 LIMIT 1").fetchone()
        if row is None:
            break  # frontier exhausted
        url = row[0]
        try:
            resp = requests.get(url, headers=HEADERS, timeout=(5, 30))
            resp.raise_for_status()
        except requests.RequestException:
            stats["errors"] += 1
            break  # leave it undone; the next run resumes here
        soup = BeautifulSoup(resp.content, "html.parser")
        quotes = soup.select("div.quote")
        stats["empty_pages"] += not quotes
        with db:  # rows, link discovery and the checkpoint commit together
            for q in quotes:
                text, author = q.select_one("span.text"), q.select_one("small.author")
                if text is None or author is None:
                    stats["rejected"] += 1  # malformed card: reject it, don't coerce
                    continue
                cur = db.execute(
                    "INSERT OR IGNORE INTO quotes VALUES (?, ?, ?, ?)",
                    (text.get_text(strip=True), author.get_text(strip=True),
                     ",".join(t.get_text(strip=True) for t in q.select("a.tag")), url))
                stats["rows"] += cur.rowcount
            nxt = soup.select_one("li.next a[href]")
            if nxt:
                db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)",
                           (urljoin(url, nxt["href"]),))
            db.execute("UPDATE frontier SET done = 1 WHERE url = ?", (url,))
        stats["pages"] += 1
        time.sleep(1.0)  # pace requests to one host
    return stats


def health(stats, db, min_rows_per_page=5):
    problems = []
    if stats["errors"]:
        problems.append(f"{stats['errors']} fetch errors")
    if stats["empty_pages"]:
        problems.append(f"{stats['empty_pages']} pages had no records (layout change?)")
    if stats["rejected"]:
        problems.append(f"{stats['rejected']} cards missing text or author")
    if stats["pages"] and stats["rows"] / stats["pages"] < min_rows_per_page:
        problems.append("rows per page below threshold")
    total = db.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
    pending = db.execute("SELECT COUNT(*) FROM frontier WHERE done = 0").fetchone()[0]
    return {**stats, "total_rows": total, "pending_urls": pending, "problems": problems}


if __name__ == "__main__":
    max_pages = int(sys.argv[1]) if len(sys.argv) > 1 else 100
    db = open_db()
    report = health(crawl(db, max_pages), db)
    print(json.dumps(report))
    sys.exit(1 if report["problems"] else 0)
  • with db: agrupa as citações inseridas, o link para a página seguinte recém-encontrado e a done = 1 atualização numa única transação.
  • INSERT OR IGNORE A verificação das chaves primárias torna cada gravação idempotente, tanto para os URLs de fronteira como para as citações.
  • Uma recuperação falhada deixa a respetiva URL por processar e interrompe a execução, pelo que a execução seguinte tenta novamente essa recuperação em primeiro lugar.
  • Os cartões em que falte uma citação ou um autor são ignorados e contabilizados, em vez de serem guardados parcialmente vazios.
  • health() transforma os contadores da execução numa lista de problemas e sys.exit(1) torna esses problemas visíveis para quem quer que seja que agende a tarefa.

Assim que a contabilidade estiver sólida, mais três práticas aumentam a capacidade de forma segura:

  • Partilhe limites de taxa por domínio. Ao longo de várias máquinas, o controlo de ritmo local já não protege o site; por isso, mantenha um orçamento por domínio num local partilhado, como um «token bucket» no Redis.
  • Faça o rastreamento de forma incremental. Utilize datas do mapa do site lastmod para enfileirar apenas URLs novas ou alteradas e utilize pedidos condicionais para transformar páginas inalteradas em respostas 304 vazias.
  • Escale horizontalmente por último. Adicione trabalhadores apenas depois de os limites por anfitrião serem aplicados centralmente; assim, mais trabalhadores significam mais anfitriões em paralelo, e não mais pressão sobre um único.

Como mantém os custos do web scraping sob controlo?

Meça o custo por registo válido, não o custo por pedido. A maior parte das poupanças advém de se fazer menos requisições: utilize a cache e pedidos condicionais, dê preferência a endpoints JSON, faça a renderização apenas quando necessário, limite as tentativas de repetição, utilize proxies apenas para os hosts que deles necessitem e faça o rastreio de forma incremental. Em seguida, compare esse valor de forma honesta com uma API de scraping gerida antes de desenvolver mais.

A fórmula é simples:

cost per valid record = (infrastructure + proxies + engineering hours) ÷ valid records delivered

As horas de engenharia devem figurar no numerador, uma vez que um scraper que necessita de um programador todas as semanas não é barato. O denominador conta apenas os registos que passaram na validação.

Alavanca

Efeito

Ver

Pedidos condicionais e armazenamento em cache

As páginas inalteradas devolvem um 304 vazio

Hábitos de pedido

Endpoints JSON em vez de HTML

Menos pedidos, análise mais simples

Planeamento

Renderizar apenas quando necessário

Evita custos do navegador em páginas estáticas

Navegadores sem interface gráfica

Limites de tentativas e backoff

Impede «tempestades» de tentativas

Taxa de pedidos

Proxies apenas quando necessário

Pagar por IPs apenas nos hosts que bloqueiam

Bloqueios

Rastreios incrementais

Recuperar apenas URLs novas ou alteradas

Escalabilidade

Validação antes do armazenamento

Não pague para armazenar ou enviar registos incorretos

Qualidade dos dados

Desenvolver ou adquirir

Criar internamente faz sentido para alguns alvos estáveis e com pouca proteção. Quando os proxies, as tentativas de repetição, a renderização e os bloqueios custam mais tempo de engenharia do que o valor dos dados, compare o custo por registo válido entre as opções na nossa compilação de web scrapers e APIs de scraping. Se preferir não gerir essa infraestrutura de todo, a Scraper API da WebScrapingAPI é uma API de scraping gerida que trata de proxies, novas tentativas e renderização opcional em JavaScript. A renderização em JavaScript e a segmentação geográfica não têm custos adicionais nesta API, e os pedidos que falham não são faturados.

Que dicas populares sobre web scraping deve ignorar?

Ignore conselhos que trocam acesso a curto prazo por risco: falsificar um User-Agent do Googlebot, alternar User-Agents em cada pedido com cabeçalhos incompatíveis, movimentos «aleatórios» do rato programados por defeito, atrasos fixos que ignoram sinais do servidor, repetir indefinidamente cada erro e extrair tudo «por via das dúvidas». Cada uma destas práticas acrescenta custos, riscos ou dados de má qualidade.

«Utilize um User-Agent do Googlebot para que os sites o deixem entrar.» Os proprietários dos sites podem desmascarar um Googlebot falso através de uma pesquisa de DNS. Utilize um User-Agent honesto com detalhes de contacto, conforme descrito na secção de identificação.

«Alterne os User-Agents em cada pedido.» Um User-Agent de navegador com cabeçalhos que nenhum navegador real enviaria destaca-se mais do que uma identidade consistente; por isso, mantenha um conjunto de cabeçalhos coerente por sessão.

«Adicione movimentos e cliques aleatórios do rato para parecer humano.» Para a maioria dos alvos, isto aumenta a complexidade e torna todas as páginas mais lentas sem resolver o problema habitual, o volume de pedidos; por isso, ajuste primeiro a sua taxa e os seus cabeçalhos.

«Aguarde 5 a 10 segundos entre pedidos e estará seguro.» Um atraso fixo é demasiado lento para um site que está a funcionar bem e demasiado rápido para um que devolve erros 429. Utilize um ritmo por anfitrião que se adapte à latência e ao Retry-After.

«Tenta novamente até funcionar.» Tentativas ilimitadas transformam uma falha num «tempestade de tentativas». Tenta novamente apenas os erros que permitem repetição, recua com jitter e limita o número de tentativas.

«Extraia tudo, por via das dúvidas.» Páginas adicionais consomem largura de banda, armazenamento e tempo de servidor, e campos adicionais aumentam a sua exposição se contiverem dados pessoais. Recolha apenas o que estiver listado no seu contrato de dados.

«O `crawl-delay` é uma norma que toda a gente segue.» Não faz parte da RFC 9309 e o Google ignora-o. Respeite-o quando estiver presente, mas não espere que todos os rastreadores ou analisadores o interpretem da mesma forma.

«A minha biblioteca trata do robots.txt por mim.» requests e o BeautifulSoup não recuperam nem aplicam o robots.txt de todo. O Scrapy consegue fazê-lo, através da sua ROBOTSTXT_OBEY configuração, que os projetos criados com scrapy startproject ativar. Verifique as suas definições e teste o comparador em relação às regras de que depende.

Lista de verificação das melhores práticas de web scraping

Antes de um scraper entrar em produção, confirme se possui um contrato de dados por escrito, uma verificação do ficheiro robots.txt em conformidade com as normas, um User-Agent válido, limitação de pedidos por host com suporte a Retry-After, tempos limite, pedidos condicionais, seletores de recurso alternativo, validação de esquemas, alertas de integridade da execução, pontos de verificação transacionais e um custo por registo válido monitorizado.

Cada linha tem um teste «concluído quando», para que possa determinar se um scraper está pronto para produção.

Fase

Verificação

Concluído quando

Plano

Contrato de dados redigido

Campos, tipos, chave e atualização acordados

Aspetos legais

Termos e ficheiro robots.txt revistos

Matching em conformidade com as normas no caminho de obtenção

Identidade

User-Agent genuíno

Nome, versão e URL de contacto incluídos

Taxa

Limitação de tráfego por anfitrião

Retry-After respeitado; backoff com jitter; limite de tentativas definido

Pedidos

Tempos de espera e sessões

Tempos de espera de ligação e leitura definidos em todos os casos

Armazenamento em cache

Pedidos condicionais

ETag e Last-Modified armazenados e enviados

Análise

Seletores de fallback

Bytes passados ao analisador; fixtures nos testes

Qualidade

Validação do esquema

Motivos de rejeição registados; deduplicação por chave

Monitorização

Estado da execução

Saída diferente de zero em caso de violação do limiar; canários implementados

Escalabilidade

Pontos de verificação

Confirmação de dados e progresso numa única transação

Custo

Economia por unidade

Custo por registo válido, monitorizado semanalmente

A começar do zero? Escolha entre os nossos melhores cursos de web scraping.

FAQ

O que são as melhores práticas de web scraping?

As melhores práticas de web scraping são os hábitos que garantem que os scrapers sejam legais, respeitosos, precisos e económicos. Incluem recolher apenas os dados de que necessita, seguir o ficheiro robots.txt e os termos do site, identificar o seu bot, regular o ritmo de cada host, respeitar o Retry-After, validar registos, monitorizar execuções, verificar o progresso e acompanhar o custo por registo válido.

Quantas solicitações por segundo são seguras ao fazer web scraping num site?

Não existe um número que seja seguro em todas as situações. Comece com uma ou duas solicitações simultâneas por host, com intervalos de cerca de um segundo, e depois adapte-se: abrande à medida que os tempos de resposta aumentam, faça uma pausa em caso de respostas 429 ou 503, respeite o Retry-After e execute rastreamentos de grande dimensão fora dos horários de pico.

Tenho de seguir o ficheiro robots.txt ao fazer scraping?

Tecnicamente, nada o impede, mas o robots.txt é a regra publicada pelo proprietário para clientes automatizados, e ignorá-lo pode resultar em bloqueios. Aplique-o conforme definido pela RFC 9309, utilizando o próprio grupo do seu bot, se existir, e leia também os termos do site.

O robotparser do Python segue a norma do ficheiro robots.txt?

Não totalmente. O urllib.robotparser do Python aplica as regras pela ordem em que aparecem no ficheiro, em vez de escolher a correspondência mais longa, pelo que pode permitir URLs que um site proíbe ou bloquear URLs que este permite. Além disso, ignora os caracteres curinga * e $ definidos na RFC 9309.

Que User-Agent devo utilizar para web scraping?

Utilize um User-Agent honesto e estável que identifique o seu bot e forneça um URL de contacto ou um e-mail, para que os proprietários dos sites possam contactá-lo em vez de o bloquearem. Nunca se faça passar pelo Googlebot, porque o Google publica instruções sobre como verificar os seus rastreadores.

Como deve um scraper lidar com uma resposta 429 Too Many Requests?

Pare de enviar pedidos para esse host, verifique o valor de «Retry-After» e aguarde pelo menos esse tempo; o valor é expresso em segundos ou numa data HTTP. Se não houver esse cabeçalho, reduza a frequência de forma exponencial com jitter, limite o número de tentativas e diminua a simultaneidade por host antes de retomar.

Como posso evitar que o meu scraper deixe de funcionar quando um site altera o seu layout?

Não é possível impedir reformulações, mas é possível lidar com elas: vincule os seletores a pontos de referência estáveis, como IDs, atributos de dados ou JSON-LD, mantenha alternativas por campo, sinalize claramente a falta de campos obrigatórios e alerte quando o número de registos por página diminuir.

Como posso verificar a qualidade dos dados extraídos?

Valide cada registo em relação a um esquema antes de o armazenar: campos obrigatórios, tipos corretos, intervalos razoáveis e uma chave única. Utilize o tipo «Decimal» para preços, elimine duplicados com base na chave, registe os motivos de rejeição e compare as taxas de valores nulos e o número de linhas entre execuções.

Preciso de um navegador headless para a extração de dados da Web?

Normalmente, não. Se os dados estiverem no código-fonte da página ou numa solicitação JSON no separador «Rede», recupere-os diretamente. Utilize um navegador sem interface apenas para páginas que criem o seu conteúdo com JavaScript e bloqueiem imagens e tipos de letra.

Como posso tornar o web scraping mais rápido sem ser bloqueado?

Recupere menos em vez de insistir mais: pontos de extremidade JSON em vez de páginas renderizadas, pedidos condicionais para que páginas inalteradas devolvam um 304, rastreios incrementais orientados por mapa do site e reutilização de ligações. Em seguida, paralelize entre servidores, e não dentro de um único, com limites por servidor definidos.

É permitido extrair dados que estão protegidos por um login?

Tenha cuidado. Iniciar sessão implica, normalmente, aceitar os termos do site, muitos dos quais proíbem o acesso automatizado; por isso, trate as áreas autenticadas como fora dos limites sem autorização ou um contrato. Com autorização, guarde as credenciais num gestor de segredos, limite os dados pessoais e respeite os limites de taxa normais.

Quando devo utilizar uma API de extração de dados da Web em vez de criar o meu próprio scraper?

Crie um scraper interno quando os alvos forem poucos, estáveis e pouco protegidos. Utilize uma API de scraping gerida quando os proxies, as tentativas de repetição, a renderização e os bloqueios consumirem mais tempo de engenharia do que o valor dos dados, ou quando precisar de muitos países, e compare o custo por registo válido, incluindo as horas de engenharia.

Dedique o seu tempo aos dados, não à infraestrutura

Todas as práticas acima são tarefas que pode manter internamente e, para muitas equipas, essa é a decisão certa. Quando não for, pode experimentar a API de scraping com 5 000 pedidos gratuitos e deixar que ela trate de proxies, tentativas de repetição e renderização opcional em JavaScript, execute páginas dinâmicas através da API do navegador com um único pedido REST, adicione proxies residenciais com pagamento à medida a partir de 8,50 $ por GB, ou obter registos estruturados de 116 fontes suportadas com a API de Dados.

Sobre o autor

Anda Miuțescu, Redator de conteúdos técnicos @ WebScrapingAPI

Anda Miuțescu

Redator de conteúdos técnicos

Anda Miuțescu é redatora de conteúdos técnicos na WebScrapingAPI, criando conteúdos claros e úteis que ajudam os programadores a compreender o produto e as suas funcionalidades.

Tutorial de Python do Nodriver: Extração discreta de dados da Web — Do primeiro script à produção
A ciência da extração de dados da Web

Tutorial de Python do Nodriver: Extração discreta de dados da Web — Do primeiro script à produção

TL;DR: Resumo: O Nodriver é uma biblioteca Python assíncrona que controla o Chromium através do Chrome DevTools Protocol, sem Selenium nem o binário chromedriver. Utilize-o quando um navegador furtivo auto-hospedado, mais próximo de uma sessão real do Chrome, é melhor do que uma stack WebDriver, mas saiba que, por si só, não elimina riscos de IP, TLS ou comportamento. Este tutorial vai de um primeiro script à produção em contentores e a uma Browser API alojada.

Suciu Dan15 min read
Ler artigo
Como lidar com URLs de redirecionamento do Google /goto e tokens CAES num scraper de SERP
A ciência da extração de dados da Web

Como lidar com URLs de redirecionamento do Google /goto e tokens CAES num scraper de SERP

TL;DR: Resumo: Os URLs /goto do Google com um token CAES, e os wrappers /url mais antigos, são camadas de redirecionamento, não destinos, por isso um scraper de SERP que trata cada href como final estraga o acompanhamento de posições e as bases de links. Prefira uma API de SERP estruturada quando devolve os links de destino. Quando tiver de analisar HTML, classifique cada href, elimine redirecionamentos duplicados e resolva-os com um GET simples. Não desencripte o CAES.

Suciu Dan13 min read
Ler artigo
Cabeçalhos de resposta HTTP no cURL: Cada flag, técnica e receita de scripting
A ciência da extração de dados da Web

Cabeçalhos de resposta HTTP no cURL: Cada flag, técnica e receita de scripting

TL;DR: O cURL esconde os cabeçalhos de resposta por padrão. Use -i para ver os cabeçalhos junto com o corpo, -I para uma solicitação HEAD que retorna apenas cabeçalhos, -v para depuração completa de solicitação/resposta e -D para salvar cabeçalhos em um arquivo. Para scripts modernos, o cURL 7.83+ permite extrair cabeçalhos individuais ou despejar todos eles como JSON com a opção -w write-out.

Suciu Dan12 min read
Ler artigo

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.