Cómo extraer múltiples páginas web con Python y Scrapeless
Senior Web Scraping Engineer
TL;DR:
- El scraping de múltiples páginas necesita un libro mayor de entrada y una cola acotada. Conserva el resultado de cada URL normalizada en lugar de tratar un bucle terminado como prueba de cobertura.
- La desduplicación de URL y la desduplicación de registros resuelven problemas diferentes. Una entrada repetida no debería crear otra solicitud, mientras que el contenido repetido necesita una regla de identidad de registro.
- Una página vacía necesita evidencia positiva de origen. Los selectores faltantes por sí solos no demuestran que la fuente no contenga registros.
- Las comprobaciones de exportación deben reabrir los archivos. Compara los identificadores de registro en CSV y JSONL, y reconcilia las páginas completadas, vacías y fallidas con la cola.
Raspar varias páginas cambia el trabajo de una extracción a un proceso de recopilación. El proceso necesita saber qué se le pidió visitar, qué páginas aceptó y de dónde proceden los registros escritos.
Aprender a extraer datos de múltiples páginas web por lo tanto comienza con el alcance y la contabilidad. Un bucle de Python es solo el mecanismo de adquisición. Este tutorial construye una cola de URL acotada, valida cada fuente, conserva los resultados de las páginas, elimina los duplicados de los registros y verifica los archivos exportados.
La demostración pública utiliza Quotes to Scrape a través de una ruta HTTP directa. El mismo programa incluye una rama de adquisición documentada de Scrapeless Web Unlocker, que requiere la clave de tu cuenta y sigue siendo un requisito previo etiquetado. La paginación interactiva es un flujo de trabajo de navegador separado cuando la fuente lo necesita.
Pipeline de un Vistazo
El pipeline convierte una lista de URL aprobadas en resultados de página reconciliados y archivos de datos aceptados. Conserva esos artefactos por separado para que un fallo no quede oculto dentro de una exportación vacía.
| 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 |
Mantén la operación de origen y el esquema de la aplicación diferenciados. La misma fuente puede producir varios registros, ningún registro o una respuesta inutilizable, de modo que los recuentos de páginas y los recuentos de registros requieren nombres distintos.
Elige una Lista de URL o la Paginación de la Fuente
Una lista de URL almacenada se ajusta a una tarea cuyo alcance de fuente ya es conocido. La paginación se ajusta a una tarea que descubre la siguiente página permitida a partir de la propia navegación de la fuente.
Para una lista, conserva las entradas originales y la cola normalizada. Para la paginación, conserva la página actual, el siguiente enlace u otra interacción observada, el conjunto de páginas visitadas y el presupuesto de páginas. No asumas que un parámetro arbitrario de número de página coincide con la navegación real del sitio.
La comparación de flujos de trabajo de paginación explica las diferentes condiciones de avance y detención. Este tutorial utiliza una lista finita para que cada página intentada pueda reconciliarse directamente con una entrada.
Normaliza las URL sin Borrar su Significado
La normalización de URL debe eliminar diferencias irrelevantes para la adquisición y al mismo tiempo conservar los parámetros que afectan a la fuente. En este ejemplo, se elimina un duplicado que solo difiere en el fragmento, mientras que la ruta y la consulta permanecen intactas.
Componentes de URI y resolución de referencias distingue el fragmento de la ruta y la consulta. Evita eliminar parámetros de consulta solo porque dos URL parezcan similares; pueden seleccionar distintos registros o filtros.
El programa también restringe las entradas al host de demostración aprobado. Un recolector real debería usar su manifiesto de origen en lugar de aceptar destinos nuevos descubiertos de forma arbitraria.
Requisitos Previos e Instalación
El ejemplo necesita Python con requests y beautifulsoup4 instalados en el entorno del proyecto. El entorno inspeccionado utilizó Requests 2.34.2 y Beautiful Soup 4.15.0; registra las versiones de tu propio proyecto al reproducirlo.
El modo directo utiliza las páginas públicas de demostración y no necesita credenciales de la API de Scrapeless. El modo unlocker requiere SCRAPELESS_API_KEY, acceso de cuenta a Web Unlocker y permiso para recopilar la fuente seleccionada.
Web Unlocker es el producto de adquisición orientado a la respuesta. Su configuración de renderizado actual utiliza el endpoint de solicitudes v2 y campos jsRender anidados. La rama de la API solicita HTML, que alimenta el mismo parser específico de la fuente que el modo directo.
Revisa el alcance de la fuente antes de la ejecución. El ejemplo se ejecuta secuencialmente sobre una lista corta y fija; no es un crawler ilimitado ni una plantilla para incrementar el volumen de solicitudes sin permiso.
Ejecutar el recopilador acotado completo
El recopilador elimina duplicados en sus URLs de entrada, registra el resultado de cada página en cola y exporta solo los registros aceptados. Su contrato de origen coincide con el diseño inspeccionado de Quotes to Scrape.
La demostración exporta nombres de autores, etiquetas, hashes de registros y URLs de origen. Usa el texto de la cita internamente para construir una identidad de contenido, pero no reproduce las citas completas en el conjunto de datos exportado.
Nota: El modo directo se ejecutó contra las páginas públicas de demostración. La rama
unlockerrequiere una clave de API real de Scrapeless y acceso a la cuenta y no se ejecutó. Sus campos de solicitud se comprobaron contra la documentación actual; los resultados del modo directo no se informan como rendimiento de 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))
Guarda el programa como multi_page.py y ejecútalo en un directorio de proyecto. Sin sobrescritura de modo utiliza adquisición directa. Selecciona FETCH_MODE=unlocker solo después de completar los requisitos previos de credenciales y permisos de origen de esa rama.
El programa conserva un resultado de página por cada URL en cola única. Comprueba el encabezado de origen esperado antes de analizar las tarjetas y requiere el mensaje de “sin resultados” observado y el contenedor de navegación antes de aceptar una página vacía.
Comienza a hacer scraping con Scrapeless
Potencia tu flujo de trabajo de web scraping y automatización con Scrapeless.
Regístrate hoy y obtén 5 USD en crédito gratis, sin necesidad de tarjeta de crédito.Reclama tu crédito gratuito ahora en el Panel de Scrapeless.
Extraer y validar cada registro
La validación de registros debe exigir los campos que identifican datos significativos de la fuente. El ejemplo requiere tanto el texto de la cita usado para el hash como el nombre de autor observado antes de confirmar los registros de una página.
Los selectores pertenecen a la fuente de demostración inspeccionada: div.quote, span.text, small.author y a.tag. El contenedor principal de datos es la última columna de contenido coincidente, distinta de la columna de encabezado. Revisa de nuevo estas suposiciones cuando cambie la fuente.
La semántica de respuesta HTTP describe resultados de protocolo, no la corrección de estos campos de origen. Una respuesta exitosa aún tiene que satisfacer el contrato de la aplicación.
El parser construye los registros candidatos de una página antes de agregarlos al conjunto de salida. Falta de un campo obligatorio hace que se rechace esa página en lugar de exportar silenciosamente una captura parcial como si fuera completa.
Rastrear páginas completadas, fallidas y vacías
Los resultados de las páginas deben explicar qué sucedió con cada fuente en cola. La demostración distingue una página de registros válida, una página vacía confirmada y un fallo de HTTP o de contrato de origen.
La ejecución acotada utilizó cinco URLs de entrada. Eliminar un duplicado que solo difería en el fragmento dejó cuatro páginas en cola. La ejecución de adquisición directa produjo dos páginas completadas, una página vacía confirmada y una página fallida, con 20 registros únicos escritos en ambos archivos.
| Observación | Resultado de la ejecución | Interpretación |
|---|---|---|
| URLs crudas de entrada | 5 | Alcance de la tarea original |
| Entradas duplicadas eliminadas | 1 | Fuente normalizada repetida |
| Páginas únicas en cola | 4 | Páginas con resultados requeridos |
| Páginas completadas | 2 | Contrato de origen y registros aceptados |
| Páginas vacías confirmadas | 1 | Estado de “sin resultados” de la fuente observado |
| Páginas fallidas | 1 | Caso deliberado de ruta no disponible |
| Registros únicos exportados | 20 | Registros aceptados después de la deduplicación de contenido |
Estos conteos describen la demostración pública acotada, no un resultado garantizado para otro sitio ni un benchmark de adquisición gestionada. La ruta no disponible se incluye para probar la conciliación de resultados; no se envía repetidamente hasta que desaparece de la lista de fallos.
Deduplicar registros y preservar la procedencia de la fuente
La deduplicación de registros compara una identidad de contenido definida después de la extracción. El ejemplo genera un hash del texto de la cita y el autor juntos y conserva las URLs de origen cuando la misma identidad vuelve a aparecer.
Esta es una regla de identidad exacta para el tipo de registro demostrado. Es posible que un identificador de producto, revisión o configuración regional necesite una clave diferente. No utilices un único hash de texto universal para combinar registros cuyas diferencias sean importantes para la tarea.
Mantén la documentación de origen junto con los datos aceptados. La desduplicación debe preservar la relación con las capturas de origen en lugar de borrar dónde se observó contenido repetido.
Exportar CSV y JSONL, luego conciliarlos
La verificación de exportación debe leer los artefactos después de la serialización. El programa vuelve a abrir CSV y JSONL y compara los identificadores de registros ordenados antes de informar su resumen.
La demostración produjo 20 filas CSV y 20 registros JSONL con identificadores coincidentes. summary.json conserva esos conteos, mientras que pages.json preserva los resultados de cada página individual.
El entrecomillado de campos CSV gestiona los detalles de serialización. La validez del formato es independiente de la aceptación de origen, por lo que se requieren ambas comprobaciones. El ejemplo almacena campos con valores de lista como strings JSON dentro de celdas CSV y como arrays en JSONL.
Las comprobaciones de conservación son directas: páginas completadas más vacías más fallidas equivalen al tamaño único de la cola, y los identificadores exportados coinciden entre los archivos. Estas comprobaciones no pueden probar todos los hechos de origen, pero pueden exponer páginas omitidas o artefactos inconsistentes.
Cuando la paginación necesita interacción con el navegador
La paginación interactiva necesita un flujo de trabajo con navegador cuando el siguiente estado de origen requiere un clic u otra acción de página. Renderizar un documento HTML devuelto no es automáticamente una sesión persistente de varios pasos.
Elige un flujo de trabajo dedicado de Agent Browser para ese caso, inspecciona el control siguiente real y espera los nuevos registros requeridos. Mantén el mismo presupuesto de páginas, seguimiento de estados visitados y política de aceptación de registros alrededor de la adquisición mediante navegador.
No mezcles los caminos de forma invisible. Registra si el origen se obtuvo mediante HTTP directo, una respuesta de Web Unlocker o una sesión de navegador interactivo. Un cambio en la adquisición puede cambiar el estado de la página o el alcance del contenido.
Gestiona el alcance de origen de forma responsable
Una tarea de varias páginas debe permanecer dentro de la lista de fuentes aprobadas y usar solo los datos necesarios para su propósito. Revisa los términos y las reglas de exclusión de robots antes de habilitar un recolector más amplio.
Mantén un presupuesto conservador por host. Este ejemplo es secuencial; un piloto que use concurrencia debe mantenerse dentro de su política de recopilación permitida, como un límite de tres workers por host en lugar de un máximo de servicio asumido.
Conserva los resultados no resueltos para su revisión. Una lista de fallos forma parte de la evidencia del trabajo completado, no es una molestia que pueda omitirse del informe final.
Conclusión
Extraer varias páginas web con Python requiere una cola acotada, aceptación específica de origen y conciliación de archivos. El proceso de recopilación solo está completo cuando cada entrada en la cola tiene un resultado y los artefactos escritos coinciden con los registros aceptados.
Usa adquisición directa para fuentes estáticas permitidas adecuadas, la rama documentada de Web Unlocker cuando la tarea necesite su ruta orientada a respuestas y un flujo de trabajo de navegador separado para paginación interactiva. Mantén el registro y el contrato de salida estables en todas esas opciones.
¿Listo para crear un trabajo de recopilación conciliado?
Planifica un flujo de trabajo acotado con Scrapeless y evalúa los requisitos de adquisición en función de la tarificación actual. Habla sobre colas y contratos de exportación en Telegram.
Preguntas frecuentes
P: ¿La visibilidad pública establece permiso para recopilar varias páginas?
La visibilidad pública no establece permiso para toda recopilación o uso. Revisa los términos de origen y los requisitos aplicables, y consulta a un asesor calificado cuando la actividad prevista sea incierta.
P: ¿Cuándo debería un recopilador usar una lista de URL en lugar de paginación?
Usa una lista de URL cuando el conjunto de fuentes permitidas sea conocido. Usa la paginación de origen cuando la tarea descubra su página siguiente a partir de un enlace o interacción inspeccionados.
P: ¿Un resultado vacío de selector es una página vacía válida?
Un resultado vacío de selector por sí solo está sin resolver. Exige la evidencia positiva del estado vacío por parte del origen y confirma la identidad de la página antes de registrar un resultado vacío válido.
P: ¿Cómo puede un trabajo demostrar que manejó cada URL de entrada?
Conserva las entradas sin procesar, la cola desduplicada y un resultado por cada origen en cola. Concilia los resultados completados, vacíos y fallidos con esa cola.
P: ¿La rama de Web Unlocker necesita un proxy configurado por separado?
La solicitud gestionada mostrada utiliza su campo de país documentado. En cambio, se necesitan credenciales de proxy asignadas por separado cuando tu propio cliente usa el producto de proxy independiente.
P: ¿Qué debería suceder con un documento de desafío o de acceso denegado?
El colector debe rechazar contenido de origen inadecuado y conservar el motivo de adquisición. Un estado de éxito HTTP no convierte un documento de desafío en una página de datos aceptada.
P: ¿Qué debería cambiar cuando rota el marcado de origen?
Vuelve a inspeccionar la identidad de la página y los campos requeridos, luego actualiza los selectores específicos del origen y las comprobaciones de estado vacío. No trates el marcado cambiado como prueba de que la fuente no tiene registros.
P: ¿Cuánta concurrencia debería usar el tutorial?
El colector demostrado se ejecuta de forma secuencial. Un piloto autorizado puede fijar un límite conservador por host, como tres trabajadores, y luego evaluar la operación antes de ampliarlo.
P: ¿Este flujo de trabajo necesita un agente de IA?
La cola de Python, la extracción y las comprobaciones de exportación se ejecutan sin un agente de IA. Un agente puede consumir registros aceptados o supervisar una tarea alrededor de esos componentes deterministas.
P: ¿Se debe almacenar la URL solicitada como URL final o canónica?
Almacena la URL solicitada por separado de la identidad final u canónica observada. El modo directo registra la URL final de la respuesta; la rama de API mostrada deja ese campo como desconocido en lugar de inventar un resultado de redirección de destino.
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.



