Como Extrair Várias Páginas da Web com Python e Scrapeless
Senior Web Scraping Engineer
Resumo:
- A raspagem de várias páginas precisa de um registro de entrada e de uma fila limitada. Guarde o resultado de cada URL normalizado em vez de tratar um loop concluído como prova de cobertura.
- A deduplicação de URLs e a deduplicação de registros resolvem problemas diferentes. Uma entrada repetida não deve criar outra requisição, enquanto conteúdo repetido exige uma regra de identidade de registro.
- Uma página vazia precisa de evidência positiva da origem. Seletores ausentes, sozinhos, não comprovam que a fonte não contém registros.
- As verificações de exportação devem reabrir os arquivos. Compare identificadores de registros em CSV e JSONL e reconcilie páginas concluídas, vazias e com falha com a fila.
Raspar várias páginas transforma o trabalho de uma extração em um processo de coleta. O processo precisa saber o que foi solicitado para visitar, quais páginas foram aceitas e de onde os registros gravados vieram.
Aprender a extrair dados de várias páginas da web, portanto, começa com escopo e contabilidade. Um loop em Python é apenas o mecanismo de aquisição. Este tutorial constrói uma fila de URLs limitada, valida cada fonte, preserva os resultados das páginas, remove duplicatas de registros e verifica os arquivos exportados.
A demonstração pública usa Quotes to Scrape por meio de um caminho HTTP direto. O mesmo programa inclui um ramo de aquisição documentado do Scrapeless Web Unlocker, que exige a chave da sua conta e permanece um pré-requisito rotulado. Paginação interativa é um fluxo de trabalho de navegador separado quando a fonte precisar disso.
Pipeline em Resumo
O pipeline transforma uma lista de URLs aprovados em resultados de página reconciliados e arquivos de dados aceitos. Preserve esses artefatos separadamente para que uma falha não fique escondida dentro de uma exportação vazia.
| Stage | Operation | Artifact |
|---|---|---|
| Scope | Define permitted URLs and required fields | Input list and source contract |
| Queue | Normalize and deduplicate URL inputs | Bounded unique queue |
| Acquisition | Obtain the source HTML | Capture identity and response outcome |
| Extraction | Validate and parse each page | Accepted records or a failure reason |
| Deduplication | Compare record identities | Unique records with source links |
| Export | Write and reopen CSV and JSONL | Files and reconciliation summary |
Mantenha a operação de origem e o esquema de aplicação distintos. A mesma fonte pode produzir vários registros, nenhum registro ou uma resposta inutilizável, portanto, contagens de páginas e contagens de registros exigem nomes diferentes.
Escolha uma Lista de URLs ou Paginação da Fonte
Uma lista de URLs armazenada se encaixa em uma tarefa cujo escopo da fonte já é conhecido. A paginação se encaixa em uma tarefa que descobre a próxima página permitida a partir da própria navegação da fonte.
Para uma lista, mantenha as entradas originais e a fila normalizada. Para paginação, preserve a página atual, o próximo link ou interação observados, o conjunto visitado e o orçamento de páginas. Não presuma que um parâmetro arbitrário de número de página corresponda à navegação real do site.
A comparação de fluxos de trabalho de paginação explica as diferentes condições de avanço e parada. Este tutorial usa uma lista finita para que cada página tentada possa ser reconciliada diretamente com uma entrada.
Normalize URLs sem Apagar o Seu Significado
A normalização de URLs deve remover diferenças irrelevantes para a aquisição, ao mesmo tempo que retém parâmetros que afetam a fonte. Neste exemplo, um duplicado que difere apenas pelo fragmento é removido, enquanto o caminho e a query permanecem intactos.
Componentes de URI e resolução de referências distingue o fragmento do caminho e da query. Evite excluir parâmetros de query apenas porque duas URLs parecem semelhantes; eles podem selecionar registros ou filtros diferentes.
O programa também restringe entradas ao host de demonstração aprovado. Um coletor real deve usar seu manifesto de fontes em vez de aceitar destinos recém-descobertos arbitrariamente.
Pré-requisitos e Instalação
O exemplo precisa de Python com requests e beautifulsoup4 instalados no ambiente do projeto. O ambiente inspecionado usou Requests 2.34.2 e Beautiful Soup 4.15.0; registre as versões do seu próprio projeto ao reproduzi-lo.
O modo direto usa as páginas de demonstração pública e não precisa de credencial da API Scrapeless. O modo unlocker requer SCRAPELESS_API_KEY, acesso de conta ao Web Unlocker e permissão para coletar a fonte selecionada.
Web Unlocker é o produto de aquisição orientado à resposta. Sua configuração de renderização atual usa o endpoint de requisição v2 e campos aninhados jsRender. O branch da API solicita HTML, que alimenta o mesmo parser específico de origem que o modo direto.
Revise o escopo da fonte antes da execução. O exemplo é executado sequencialmente sobre uma lista curta e fixa; não é um crawler ilimitado nem um modelo para aumentar o volume de requisições sem permissão.
Execute o Coletor Completamente Limitado
O coletor remove duplicatas de suas URLs de entrada, registra o resultado de cada página enfileirada e exporta apenas os registros aceitos. Seu contrato de origem corresponde ao layout inspecionado de Quotes to Scrape.
A demonstração exporta nomes de autores, tags, hashes de registros e URLs de origem. Ela usa o texto da citação internamente para construir uma identidade de conteúdo, mas não reproduz as citações completas no conjunto de dados exportado.
Nota: O modo direto foi executado nas páginas públicas de demonstração. O branch
unlockerrequer uma chave de API Scrapeless real e acesso à conta e não foi executado. Seus campos de requisição foram verificados em relação à documentação atual; os resultados do modo direto não são relatados como desempenho do Web Unlocker.
python
import csv
import hashlib
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
import requests
from bs4 import BeautifulSoup
INPUT = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
"https://quotes.toscrape.com/page/2/#top",
"https://quotes.toscrape.com/page/99/",
"https://quotes.toscrape.com/missing/"
]
MODE = os.environ.get("FETCH_MODE", "direct")
if MODE not in {"direct", "unlocker"}:
raise ValueError("Unsupported acquisition mode")
def normalize(url):
parts = urlsplit(url)
if parts.scheme != "https" or parts.hostname != "quotes.toscrape.com":
raise ValueError("URL is outside the permitted demo scope")
return urlunsplit((parts.scheme, parts.netloc.lower(),
parts.path or "/", parts.query, ""))
queue = list(dict.fromkeys(normalize(url) for url in INPUT))
pages = []
records = {}
duplicate_records = 0
with requests.Session() as client:
for url in queue:
page = {"requested_url": url}
try:
if MODE == "unlocker":
response = client.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"]},
json={"actor": "unlocker.webunlocker",
"proxy": {"country": "ANY"},
"input": {"url": url, "jsRender": {
"enabled": True, "response": {"type": "html"}}}},
timeout=60
)
response.raise_for_status()
envelope = response.json()
html = envelope.get("data")
if envelope.get("code") != 200 or not isinstance(html, str):
raise ValueError("Expected a successful HTML envelope")
page["final_url"] = None
else:
response = client.get(url, timeout=30)
response.raise_for_status()
html = response.text
page["final_url"] = response.url
soup = BeautifulSoup(html, "html.parser")
heading = soup.select_one("h1 a")
grids = soup.select("div.container > div.row > div.col-md-8")
if not heading or heading.get_text(strip=True) != "Quotes to Scrape" or not grids:
raise ValueError("Source identity does not match the demo contract")
grid = grids[-1]
cards = grid.select("div.quote")
if not cards and (grid.select_one("ul.pager") is None or
"No quotes found!" not in grid.get_text(" ", strip=True)):
raise ValueError("Empty state is not confirmed")
extracted = []
for card in cards:
text_node = card.select_one("span.text")
author_node = card.select_one("small.author")
if not text_node or not author_node:
raise ValueError("Required source field missing")
text = text_node.get_text(strip=True)
author = author_node.get_text(strip=True)
if not text or not author:
raise ValueError("Required source field empty")
digest = hashlib.sha256((text + "\n" + author).encode()).hexdigest()
extracted.append({"record_id": digest, "author": author,
"tags": [x.get_text(strip=True) for x in card.select("a.tag")],
"source_urls": [url]})
for record in extracted:
digest = record["record_id"]
if digest in records:
duplicate_records += 1
records[digest]["source_urls"].append(url)
else:
records[digest] = record
page.update(status="completed" if cards else "empty", records=len(cards),
captured_at=datetime.now(timezone.utc).isoformat())
except (requests.RequestException, ValueError, KeyError) as error:
page.update(status="failed", reason=type(error).__name__)
pages.append(page)
rows = list(records.values())
with Path("records.jsonl").open("w", encoding="utf-8") as output:
for row in rows:
output.write(json.dumps(row, ensure_ascii=False) + "\n")
with Path("records.csv").open("w", encoding="utf-8", newline="") as output:
writer = csv.DictWriter(output, fieldnames=["record_id", "author", "tags", "source_urls"])
writer.writeheader()
for row in rows:
writer.writerow({**row, "tags": json.dumps(row["tags"], ensure_ascii=False),
"source_urls": json.dumps(row["source_urls"])})
with Path("records.csv").open(encoding="utf-8", newline="") as source:
csv_rows = list(csv.DictReader(source))
jsonl_rows = [json.loads(line) for line in Path("records.jsonl").read_text().splitlines()]
assert [r["record_id"] for r in csv_rows] == [r["record_id"] for r in jsonl_rows]
summary = {"mode": MODE, "input_urls": len(INPUT), "duplicate_inputs": len(INPUT)-len(queue),
"queued_pages": len(queue), "completed": sum(p["status"]=="completed" for p in pages),
"empty": sum(p["status"]=="empty" for p in pages),
"failed": sum(p["status"]=="failed" for p in pages),
"unique_records": len(rows), "duplicate_records": duplicate_records,
"csv_rows": len(csv_rows), "jsonl_rows": len(jsonl_rows)}
assert summary["completed"] + summary["empty"] + summary["failed"] == len(queue)
Path("pages.json").write_text(json.dumps(pages, indent=2), encoding="utf-8")
Path("summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
print(json.dumps(summary))
Salve o programa como multi_page.py e execute-o em um diretório de projeto. Sem substituição de modo, ele usa aquisição direta. Selecione FETCH_MODE=unlocker somente depois de concluir os pré-requisitos de credenciais e permissão de origem desse branch.
O programa mantém um resultado de página por URL enfileirada exclusiva. Ele verifica o cabeçalho de origem esperado antes de analisar os cards e requer a mensagem de nenhum resultado observado e o contêiner de navegação antes de aceitar uma página vazia.
Comece a fazer scraping com a Scrapeless
Turbine seu fluxo de trabalho de web scraping e automação com a Scrapeless!
Cadastre-se hoje e receba US$ 5 em crédito gratuito — sem necessidade de cartão de crédito.Solicite seu crédito gratuito agora no Painel da Scrapeless.
Extraia e Valide Cada Registro
A validação de registros deve exigir os campos que identificam dados de origem significativos. O exemplo exige tanto o texto da citação usado para o hash quanto o nome do autor observado antes de confirmar os registros de uma página.
Os seletores pertencem à fonte de demonstração inspecionada: div.quote, span.text, small.author e a.tag. O principal contêiner de dados é a coluna final de conteúdo correspondente, distinta da coluna de cabeçalho. Reavalie essas suposições quando a fonte mudar.
A semântica de resposta HTTP descreve resultados de protocolo, não a correção desses campos de origem. Uma resposta bem-sucedida ainda precisa satisfazer o contrato da aplicação.
O parser constrói os registros candidatos de uma página antes de adicioná-los ao conjunto de saída. Um campo obrigatório ausente rejeita essa página em vez de exportar silenciosamente uma captura parcial como completa.
Acompanhe Páginas Concluídas, Com Falha e Vazias
Os resultados das páginas devem explicar o que aconteceu com cada origem enfileirada. A demonstração distingue uma página de registros válida, uma página vazia confirmada e uma falha de HTTP ou de contrato de origem.
A execução limitada utilizou cinco URLs de entrada. Remover um duplicado apenas de fragmento deixou quatro páginas enfileiradas. A execução de aquisição direta produziu duas páginas concluídas, uma página vazia confirmada e uma página com falha, com 20 registros únicos gravados em ambos os arquivos.
| Observação | Resultado da execução | Interpretação |
|---|---|---|
| URLs brutas de entrada | 5 | Escopo original da tarefa |
| Entradas duplicadas removidas | 1 | Fonte normalizada repetida |
| Páginas exclusivas enfileiradas | 4 | Páginas com resultados exigidos |
| Páginas concluídas | 2 | Contrato de origem e registros aceitos |
| Páginas vazias confirmadas | 1 | Estado de nenhum resultado da origem observado |
| Páginas com falha | 1 | Caso deliberado de caminho indisponível |
| Registros únicos exportados | 20 | Registros aceitos após deduplicação de conteúdo |
Essas contagens descrevem a demonstração pública limitada, não um resultado garantido para outro site ou um benchmark de aquisição gerenciada. O caminho indisponível é incluído para testar a reconciliação de resultados; ele não é enviado repetidamente até desaparecer da lista de falhas.
Deduplicate Registros e Preserve a Linhagem da Origem
A deduplicação de registros compara uma identidade de conteúdo definida após a extração. O exemplo gera um hash combinando o texto da citação e o autor e preserva as URLs de origem quando a mesma identidade aparece novamente.
Esta é uma regra de identidade exata para o tipo de registro demonstrado. Um identificador de produto, revisão ou localidade pode precisar de uma chave diferente. Não use um hash de texto universal para combinar registros cujas diferenças são relevantes para a tarefa.
Mantenha a documentação de origem com os dados aceitos. A desduplicação deve preservar a relação com as capturas de origem em vez de apagar onde o conteúdo repetido foi observado.
Exportar CSV e JSONL e Depois Reconciliá-los
A verificação da exportação deve ler os artefatos após a serialização. O programa reabre o CSV e o JSONL e compara os identificadores de registro ordenados antes de relatar seu resumo.
A demonstração produziu 20 linhas CSV e 20 registros JSONL com identificadores correspondentes. summary.json mantém essas contagens, enquanto pages.json preserva os resultados individuais por página.
Citação de campos CSV lida com os detalhes de serialização. A validade do formato é separada da aceitação da origem, portanto, ambas as verificações são necessárias. O exemplo armazena campos com valores de lista como strings JSON dentro de células CSV e como arrays em JSONL.
As verificações de conservação são diretas: páginas concluídas mais vazias mais com falha são iguais ao tamanho único da fila, e os identificadores exportados coincidem entre os arquivos. Essas verificações não podem provar todos os fatos de origem, mas podem expor páginas descartadas ou artefatos inconsistentes.
Quando a Paginação Precisa de Interação com o Navegador
A paginação interativa precisa de um fluxo de trabalho com navegador quando o próximo estado da origem exige um clique ou outra ação de página. Renderizar um documento HTML retornado não é automaticamente uma sessão persistente de múltiplas etapas.
Escolha um fluxo de trabalho dedicado de Navegador de Agente para esse caso, inspecione o controle real de avanço e aguarde pelos novos registros necessários. Mantenha o mesmo orçamento de páginas, rastreamento de estados visitados e política de aceitação de registros em torno da aquisição via navegador.
Não misture os caminhos de forma invisível. Registre se a origem foi obtida por HTTP direto, por uma resposta de Web Unlocker ou por uma sessão de navegador interativa. Uma mudança na aquisição pode alterar o estado da página ou o escopo do conteúdo.
Tratar o Escopo da Origem com Responsabilidade
Uma tarefa de múltiplas páginas deve permanecer dentro da lista de origens aprovada e usar apenas os dados necessários para seu propósito. Revise os termos e as regras de exclusão de robots antes de habilitar um coletor mais amplo.
Mantenha um orçamento conservador por host. Este exemplo é sequencial; um piloto que use concorrência deve permanecer dentro de sua política de coleta permitida, como um limite de três workers por host em vez de um máximo de serviço presumido.
Preserve os resultados não resolvidos para revisão. Uma lista de falhas faz parte das evidências do trabalho concluído, não um incômodo a ser omitido do relatório final.
Conclusão
Raspar múltiplas páginas da web com Python requer uma fila limitada, aceitação específica da origem e reconciliação de arquivos. O processo de coleta só está completo quando cada entrada enfileirada tem um resultado e os artefatos escritos correspondem aos registros aceitos.
Use aquisição direta para fontes estáticas permitidas adequadas, o ramo documentado de Web Unlocker quando a tarefa precisar de seu caminho orientado à resposta, e um fluxo de trabalho separado de navegador para paginação interativa. Mantenha o livro-razão e o contrato de saída estáveis nessas escolhas.
Pronto para Construir um Trabalho de Coleta Reconciliado?
Planeje um fluxo de trabalho limitado com o Scrapeless e avalie os requisitos de aquisição em relação à precificação atual. Discuta filas e contratos de exportação no Telegram.
FAQ
P: A visibilidade pública estabelece permissão para coletar múltiplas páginas?
A visibilidade pública não estabelece permissão para toda coleta ou uso. Revise os termos da origem e os requisitos aplicáveis, e consulte assessoria jurídica qualificada quando a atividade pretendida for incerta.
P: Quando um coletor deve usar uma lista de URLs em vez de paginação?
Use uma lista de URLs quando o conjunto de origens permitidas for conhecido. Use paginação da origem quando a tarefa descobrir sua próxima página a partir de um link inspecionado ou interação.
P: Um resultado vazio de seletor é uma página vazia válida?
Um resultado vazio de seletor por si só é não resolvido. Exija a evidência positiva de estado vazio da origem e confirme a identidade da página antes de registrar um resultado vazio válido.
P: Como um trabalho pode mostrar que tratou cada URL de entrada?
Preserve as entradas brutas, a fila desduplicada e um resultado por origem enfileirada. Reconcilie os resultados concluídos, vazios e com falha com essa fila.
P: O ramo Web Unlocker precisa de um proxy configurado separadamente?
A solicitação gerenciada mostrada usa seu campo de país documentado. Credenciais de proxy alocadas separadamente são necessárias quando o seu próprio cliente usa o produto de proxy independente.
P: O que deve acontecer com um documento de desafio ou de acesso negado?
O coletor deve rejeitar conteúdo de origem inadequado e reter o motivo da aquisição. Um status HTTP de sucesso não torna um documento de desafio uma página de dados aceita.
P: O que deve mudar quando o markup de origem gira?
Reinspecione a identidade da página e os campos necessários, depois atualize os seletores específicos da origem e as verificações de estado vazio. Não trate markup alterado como prova de que a origem não possui registros.
P: Quanta simultaneidade o tutorial deve usar?
O coletor demonstrado é executado de forma sequencial. Um piloto permitido pode definir um limite conservador por host, como três workers, e então avaliar a operação antes de expandi-lo.
P: Este fluxo de trabalho precisa de um agente de IA?
A fila, a extração e as verificações de exportação em Python são executadas sem um agente de IA. Um agente pode consumir registros aceitos ou supervisionar uma tarefa em torno desses componentes determinísticos.
P: A URL solicitada deve ser armazenada como URL final ou canônica?
Armazene a URL solicitada separadamente da identidade final ou canônica observada. O modo direto registra a URL final da resposta; o branch de API mostrado deixa esse campo desconhecido em vez de inventar um resultado de redirecionamento de destino.
Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.



