Na maioria dos pipelines de scraping, a página é descartada assim que os dados são extraídos dela. O parser lê o HTML, grava uma linha, e a resposta desaparece. Isso funciona até o dia em que se descobre que o extrator estava errado havia uma semana, ou que um novo campo é necessário para o trimestre passado, ou que alguém pergunta o que a página realmente dizia. Nesse ponto, o único caminho de volta é buscar tudo de novo, e as páginas já mudaram nesse meio tempo.
Guardar a resposta bruta resolve os três problemas, e existe um formato padrão construído para isso: WARC, o Web ARChive format usado por arquivos da web e pelo Common Crawl. Este guia cobre o que armazenar, o código para armazenar e o custo em um teste real.
Principais conclusões
- Armazene cada resposta como recebida, antes de fazer o parsing. Reextrair de respostas armazenadas é barato; recuperar o histórico é impossível.
- WARC é o container padrão: um formato aberto com registros de request, response e metadata, legível por muitas ferramentas já existentes.
- Solicite respostas compactadas e as armazene ainda compactadas. No nosso teste com 40 páginas, isso manteve 20,6 MB de HTML em apenas 3,4 MB em trânsito e 3,5 MB em disco.
- Reproduzir todas as 40 páginas a partir do arquivo levou menos de 0,2 segundos, contra 6 a 10 segundos para buscá-las.
- Registre como cada arquivo foi coletado, incluindo o país de saída, para que páginas arquivadas possam ser comparadas de forma justa mais tarde.
Por que manter a resposta bruta
Três situações surgem em quase todo projeto de coleta de longa duração.
Extratores quebram silenciosamente. Um site muda sua marcação, o parser continua rodando, e um campo silenciosamente fica vazio ou errado. O monitoramento de schema drift detecta isso, mas só depois que algumas linhas ruins já foram gravadas. Com as respostas brutas em disco, você corrige o extrator e o executa novamente sobre os dias afetados. Sem elas, esses dias estão perdidos.
As perguntas mudam. Seis meses depois, alguém precisa de um campo que ninguém extraiu: uma observação de frete, uma avaliação de vendedor, um selo. Se as páginas estão arquivadas, é um job em lote. Se não, começa hoje e não tem histórico.
Evidências precisam do original. Quando os dados coletados sustentam uma decisão, uma reclamação ou uma disputa, a página como foi servida tem mais peso do que uma linha derivada dela, o que é a razão pela qual evidências prontas para uso judicial começam com capturas preservadas.
Isso também muda como você pode trabalhar com extração. Experimentar uma nova abordagem, como a extração baseada em modelo comparada com seletores, torna-se um experimento offline sobre páginas armazenadas, em vez de um novo crawl.
O que é o WARC
WARC é um formato de container para capturas web, mantido pelo International Internet Preservation Consortium e padronizado como ISO 28500. Um arquivo WARC é uma sequência de registros, cada um com um pequeno bloco de cabeçalhos de texto seguido de conteúdo. Os tipos de registro que importam para scraping são:
| Tipo de registro | O que contém |
|---|---|
warcinfo | Como o arquivo foi feito: software, operador, notas de coleta |
request | A requisição HTTP como enviada, incluindo cabeçalhos como Accept-Language |
response | A resposta HTTP como recebida: linha de status, cabeçalhos e corpo |
metadata | Qualquer outra informação sobre uma captura, vinculada a ela pelo ID do registro |
revisit | Um ponteiro para uma captura idêntica anterior, usado para evitar armazenar duplicatas |
Cada registro carrega um digest de seu conteúdo, de modo que um arquivo pode ser verificado quanto a corrupção. A especificação recomenda compactar cada registro separadamente com gzip, o que mantém os arquivos pequenos e ainda permite que um leitor pule diretamente para um registro. O Common Crawl, por exemplo, distribui seus dados de crawl como arquivos WARC.
A vantagem prática sobre um formato caseiro é o ferramental. Arquivos escritos conforme o padrão podem ser indexados, validados, reproduzidos e pesquisados com ferramentas open-source já existentes, por pessoas que nunca viram seu código.
O código
A biblioteca Python warcio, do projeto Webrecorder, lê e escreve WARC. A função abaixo busca uma página com requests, grava a requisição e a resposta em um arquivo WARC aberto, e mantém o corpo exatamente como o servidor o enviou:
from io import BytesIO
from urllib.parse import urlsplit
import requests
from warcio.archiveiterator import ArchiveIterator
from warcio.statusandheaders import StatusAndHeaders
from warcio.warcwriter import WARCWriter
def fetch_and_archive(session, url, writer, **kwargs):
"""Fetch url and write the request and the response, byte for byte as received, to a WARC file."""
response = session.get(url, stream=True, **kwargs)
# Read the body undecoded, so gzip or br responses are stored exactly as the server sent them.
raw = response.raw.read(decode_content=False)
sent = response.request
path = urlsplit(sent.url)
request_line = f"{sent.method} {path.path or '/'}{'?' + path.query if path.query else ''} HTTP/1.1"
request_headers = [("Host", path.netloc)] + list(sent.headers.items())
request = writer.create_warc_record(
sent.url, "request", payload=BytesIO(b""),
http_headers=StatusAndHeaders(request_line, request_headers, is_http_request=True))
# urllib3 has already removed any chunked framing, so drop the header that describes it.
headers = [(k, v) for k, v in response.raw.headers.items() if k.lower() != "transfer-encoding"]
status = f"{response.status_code} {response.reason}"
record = writer.create_warc_record(
response.url, "response", payload=BytesIO(raw),
http_headers=StatusAndHeaders(status, headers, protocol="HTTP/1.1"))
request.rec_headers.add_header("WARC-Concurrent-To", record.rec_headers.get_header("WARC-Record-ID"))
writer.write_record(request)
writer.write_record(record)
return response.status_code, raw
def replay(path):
"""Yield (url, status, decoded body) for every response in a WARC file, with no network access."""
with open(path, "rb") as stream:
for record in ArchiveIterator(stream):
if record.rec_type == "response":
status = int(record.http_headers.get_statuscode())
yield record.rec_headers.get_header("WARC-Target-URI"), status, record.content_stream().read()
E usando-a através de um proxy, com um registro warcinfo que indica de onde as páginas foram coletadas:
import os
import requests
from warcio.warcwriter import WARCWriter
from archive import fetch_and_archive, replay
proxy = f"http://{os.environ['SHIFTER_PROXY_USER']}-country-de:{os.environ['SHIFTER_PROXY_PASS']}@p.shifter.io:443"
session = requests.Session()
session.proxies = {"http": proxy, "https": proxy}
session.headers["Accept-Encoding"] = "gzip"
# Identify your collector; some sites refuse the library's default User-Agent.
session.headers["User-Agent"] = "ExampleArchiver/1.0 (+https://example.com/bot)"
urls = ["https://en.wikipedia.org/wiki/Web_archiving", "https://en.wikipedia.org/wiki/Web_crawler"]
with open("crawl-2026-10-01.warc.gz", "wb") as output:
writer = WARCWriter(output, gzip=True)
# One warcinfo record per file says how and from where the pages were collected.
writer.write_record(writer.create_warcinfo_record(
"crawl-2026-10-01.warc.gz", {"software": "warcio", "description": "exit country: de"}))
for url in urls:
fetch_and_archive(session, url, writer, timeout=30)
# Months later, with no network access: re-run a new extractor over the same bytes.
for url, status, body in replay("crawl-2026-10-01.warc.gz"):
print(status, url, len(body))
Três detalhes nesse código surgiram ao testá-lo.
- Leia o corpo sem decodificar. O requests normalmente descompacta as respostas para você. Ler o stream bruto mantém os bytes como foram enviados, o que é ao mesmo tempo mais fiel e muito menor. O warcio os descompacta novamente na reprodução.
- Descarte o cabeçalho de chunked. Um quarto das nossas respostas chegou com transfer encoding chunked, que a biblioteca HTTP já havia desempacotado. Armazenar o cabeçalho com o corpo já desempacotado deixa um registro que se descreve de forma errada. O warcio por acaso lidou bem com isso; outros leitores podem não lidar.
- Envie um User-Agent real. Nossa primeira execução do exemplo foi recusada com HTTP 403, porque o site rejeita o User-Agent padrão da biblioteca HTTP. Identifique seu coletor com honestidade.
Mais um detalhe encontrado no código-fonte da biblioteca, e não durante os testes: se você aceita respostas compactadas com Brotli (br), instale o pacote brotli. Sem ele, o warcio não consegue decodificar esses corpos na reprodução e retorna os bytes ainda compactados sem gerar erro.
Qual foi o custo
Arquivamos 40 artigos da Wikipédia em inglês em 1 de outubro de 2026 através de uma saída residencial na Alemanha, duas vezes: uma aceitando respostas compactadas com gzip, e outra solicitando respostas não compactadas.
| Respostas compactadas | Respostas não compactadas | |
|---|---|---|
| Páginas | 40 | 40 |
| Transferido | 3,43 MB | 20,6 MB |
| HTML após decodificação | 20,6 MB | 20,6 MB |
| Arquivo WARC em disco | 3,54 MB | 3,51 MB |
| Tempo para buscar | 6 a 10 s | 9 a 12 s |
| Tempo para reproduzir todas as 40 | 0,19 s | 0,18 s |
Cada página reproduzida foi byte a byte idêntica à que foi buscada, verificado por hash SHA-256, e o warcio check validou os digests dos 81 registros do arquivo.
Duas coisas se destacam. Primeiro, o armazenamento é barato: o arquivo ficou cerca de 3% maior do que os bytes compactados transferidos, sendo a diferença os registros de request e os cabeçalhos. Segundo, o armazenamento foi o mesmo nos dois casos, porque o arquivo WARC é compactado de qualquer forma; o que mudou foi a largura de banda. Solicitar respostas não compactadas custou seis vezes mais transferência para um arquivo idêntico. Em coletas cobradas por largura de banda, essa é a diferença que aparece na fatura, como explica em mais detalhes como reduzir custos de largura de banda de proxy.
Regras práticas
- Arquive antes de fazer o parsing. Grave o registro WARC primeiro, depois extraia. Se o extrator travar, a página ainda é mantida.
- Gire os arquivos por tamanho ou tempo. A especificação WARC recomenda 1 GB como tamanho-alvo prático por arquivo. Nomeie os arquivos com a data e a coleta, e nunca anexe a um arquivo que outro processo esteja escrevendo.
- Registre o ponto de vista. Uma página buscada da Alemanha e outra buscada dos Estados Unidos podem diferir em idioma, preço e conteúdo. Coloque o país de saída e as configurações de coleta no registro
warcinfo, como argumentado em o caso para registrar onde os dados foram observados. - Indexe o que você guarda. Um pequeno índice de URL, data, arquivo e offset permite extrair uma captura de meio de terabytes sem ler tudo. A ferramenta de linha de comando
indexdo warcio produz um. - Defina uma política de retenção. Páginas brutas podem conter dados pessoais. Decida por quanto tempo mantê-las, restrinja quem pode lê-las e exclua conforme um cronograma.
- Ignore duplicatas de forma deliberada. Quando uma página não mudou desde a última captura, um registro
revisitpode apontar para a cópia anterior em vez de armazená-la novamente, o que combina bem com a detecção de mudanças.
Conclusão
O parsing é a parte de um pipeline de scraping com maior chance de estar errada e maior chance de mudar, portanto não deveria ser o único registro do que foi coletado. Armazene a resposta bruta em WARC antes de fazer o parsing, solicite respostas compactadas e as mantenha compactadas, e registre de onde cada arquivo foi coletado.
No nosso teste, isso custou aproximadamente 3,5 MB de disco para 40 páginas e transformou um crawl que levava segundos em uma reprodução que levou uma fração de um segundo. Na próxima vez que um extrator quebrar, ou uma nova pergunta surgir sobre o mês passado, a resposta é um job em lote, e não uma semana perdida.
Fontes e referências
- International Internet Preservation Consortium, The WARC Format 1.1.
- Webrecorder, warcio, versão 1.8.1, usada no código e no teste acima.
- Common Crawl, Get started, sobre seu uso do formato WARC.
- Arquivo de teste com 40 páginas coletadas pela Shifter em 1 de outubro de 2026, usando o código acima.