Extraer datos de HTML en Python con Respondo por Scrapeless
Expert in Web Scraping Technologies
TL;DR:
- Respondo es la biblioteca de Python de código abierto de Scrapeless para convertir HTML y JSON en registros. Funciona en las páginas y respuestas de API que ya tienes, y no tiene dependencias en tiempo de ejecución.
- Instala la versión 0.6 desde GitHub. El paquete
respondoen PyPI aún está en 0.4.0, que es anterior a las recetas de campo, los ayudantes de JSON Lines y el modo por lotes. - Una receta de campo asigna cada columna a un selector. Las recetas pueden leer un atributo en lugar de texto y pueden vivir en un archivo JSON al lado de tu código.
- La salida CSV es segura para hojas de cálculo por defecto. Los valores que comienzan como fórmulas reciben un prefijo de apóstrofe, y los números permanecen numéricos.
- Respondo no obtiene ni renderiza páginas. La API Universal Scraping de Scrapeless recopila el HTML, y Respondo lo convierte en filas.
- Prueba el paso de colección en el plan gratuito de Scrapeless y extrae tu primera página en pocos minutos.
Para extraer datos de HTML en Python, necesitas dos cosas: el HTML en sí, y el código que convierte etiquetas en campos que puedes usar. La segunda parte tiende a convertirse en bucles únicos y código CSV que difiere para cada sitio.
Respondo empaqueta esa segunda parte. Describes cada campo una vez, como un selector más un atributo opcional, y Respondo devuelve una lista de diccionarios que puedes escribir en CSV o JSON Lines, desde Python o desde la línea de comandos. Esta guía construye un pequeño catálogo de libros a partir de un sitio de práctica pública, luego empareja Respondo con Scrapeless para el paso de colección.
Qué es Respondo
Respondo es un kit de herramientas de extracción local mantenido bajo la organización de GitHub de Scrapeless y publicado bajo la licencia MIT. Su repositorio fuente en GitHub describe la división en una línea: recopilar con Scrapeless, luego extraer, transformar y exportar con Respondo.
La biblioteca funciona con el contenido que ya tienes. Sus funciones HTML se basan en el módulo html.parser de la biblioteca estándar, por lo que el entorno instalado no tiene nada más que Respondo en sí. La versión 0.6 cubre cuatro tipos de trabajo:
- registros repetidos de HTML, utilizando selectores de estilo CSS y recetas de campo reutilizables;
- consultas, aplanamiento, proyección y parches de fusión en documentos JSON;
- resúmenes de páginas, feeds y sitemaps;
- un comando
respondocon 21 modos, incluyendo el procesamiento por lotes de una carpeta completa.
Respondo no obtiene URL ni lanza navegadores, y no contiene ningún cliente API de Scrapeless. Si aún estás decidiendo qué implica el análisis, nuestra visión general de qué es el análisis de datos cubre los conceptos.
Instalar Respondo 0.6
Instala Respondo directamente desde GitHub, fijado a un commit. Necesita Python 3.9 o superior:
bash
python -m pip install "git+https://github.com/scrapeless-ai/respondo@be376d112ecf681011a079e809acae46b7e1ff59"
respondo --version
text
respondo 0.6.0
El pin de commit mantiene tu entorno en el código exacto con el que se probó esta guía. Un simple pip install respondo instala la versión 0.4.0 desde PyPI, y las funciones utilizadas a continuación no están en ella. Después de la instalación, pip list muestra solo respondo y pip.
Definir una Receta de Campo
Una receta de campo es un diccionario que asigna cada columna de salida a una regla para encontrarla dentro de un elemento repetido. Una cadena sencilla es un selector cuyo texto se convierte en el valor. Un diccionario añade opciones:
selectorencuentra el elemento dentro del elemento actual.attrlee un atributo comohrefotitleen lugar del texto.required: Truegenera un error cuando un elemento no tiene coincidencia.many: Truedevuelve una lista, ydefaultestablece un valor predeterminado para un valor faltante.
Los selectores cubren etiquetas, clases, IDs, pruebas de atributos, combinadores de descendientes e hijos, y grupos por comas. Ese es un subconjunto deliberado de la especificación de Selectores de W3C: las pseudoclases como :nth-child y los combinadores de hermanos son rechazados con un ValueError en lugar de ser ignorados.
Las recetas también pueden vivir en un archivo JSON, lo que mantiene los selectores fuera de tu código y permite que la línea de comandos los reutilice. Guarda esto como book-fields.json:
json
{
"title": {"selector": "h3 a", "attr": "title", "required": true},
"price": ".price_color",
"availability": ".availability",
"url": {"selector": "h3 a", "attr": "href"}
}
El campo title lee intencionadamente el atributo title del enlace. El texto del enlace visible en el sitio de práctica está acortado para nombres largos, mientras que el atributo contiene el título completo.
Extraer Registros y Escribir CSV
extract_records toma el HTML, un selector para el elemento repetido y la receta, y devuelve un diccionario por elemento. Este ejemplo utiliza dos elementos copiados de la categoría de misterio del sitio de práctica:
python
from respondo import extract_records, normalize_url, records_to_csv
PAGE_URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
html = """
<article class="product_pod">
<h3><a href="../../../sharp-objects_997/index.html" title="Sharp Objects">Sharp Objects</a></h3>
<p class="price_color">£47.82</p>
<p class="instock availability"><i class="icon-ok"></i> In stock</p>
</article>
<article class="product_pod">
<h3><a href="../../../in-a-dark-dark-wood_963/index.html" title="In a Dark, Dark Wood">In a Dark, Dark ...</a></h3>
<p class="price_color">£19.63</p>
<p class="instock availability"><i class="icon-ok"></i> In stock</p>
</article>
"""
books = extract_records(html, "article.product_pod", {
"title": {"selector": "h3 a", "attr": "title", "required": True},
"price": ".price_color",
"availability": ".availability",
"url": {"selector": "h3 a", "attr": "href"},
})
for book in books:
book["url"] = normalize_url(book["url"], base=PAGE_URL)
print(records_to_csv(books), end="")
text
title,price,availability,url
Sharp Objects,£47.82,In stock,https://books.toscrape.com/catalogue/sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,https://books.toscrape.com/catalogue/in-a-dark-dark-wood_963/index.html
Tres detalles son manejados para ti. El texto de .availability regresa recortado, sin el elemento de ícono. El href relativo se convierte en una URL absoluta, resuelta contra la dirección de la página de la manera que las reglas de resolución de referencias de la especificación URI describen. Y el título que contiene una coma se cita en el CSV.
records_to_csv también protege contra la inyección de fórmulas en hojas de cálculo, un riesgo que la entrada de OWASP sobre inyección CSV describe. Una cadena que comienza con =, +, -, @, un tabulador o un salto de línea recibe un apóstrofe al frente, por lo que =HYPERLINK(1) se escribe como '=HYPERLINK(1), mientras que un número real como -5 permanece como está. Pasa escape_formulas=False solo cuando el archivo nunca llega a una hoja de cálculo.
Ir más lejos: Resúmenes de Página, Líneas JSON y la CLI
Los registros son una salida. El mismo paquete también resume páginas completas y maneja Líneas JSON, y su comando respondo ejecuta la extracción de registros desde la terminal, para un archivo o una carpeta completa.
Resumir una página completa
extract_page devuelve el título, texto, metadatos, encabezados, enlaces, imágenes y tablas de un documento en una sola llamada. Ejecútalo en una copia guardada de la página de la categoría de misterio:
python
from respondo import extract_page
with open("mystery-page-1.html", encoding="utf-8") as handle:
page = extract_page(
handle.read(),
base="https://books.toscrape.com/catalogue/category/books/mystery_3/index.html",
)
print(sorted(page))
print(page["headings"][:2])
print(len(page["links"]), "links,", len(page["images"]), "images")
text
['headings', 'images', 'links', 'meta', 'tables', 'text', 'title']
[{'level': 1, 'id': '', 'text': 'Mystery'}, {'level': 3, 'id': '', 'text': 'Sharp Objects'}]
95 links, 20 images
El texto, encabezados y enlaces omiten scripts, estilos y el encabezado del documento, y los enlaces relativos se resuelven contra base.
Consultar Líneas JSON
jsonl_dumps escribe registros como Líneas JSON, un objeto compacto por línea, y iter_jsonl los lee de forma perezosa. json_query luego extrae valores con un pequeño lenguaje de ruta que siempre devuelve una lista:
python
from respondo import iter_jsonl, json_query
with open("mystery-books.jsonl", encoding="utf-8") as handle:
books = list(iter_jsonl(handle))
print(len(books), "records")
print(json_query(books, "$[*].title")[:3])
print(json_query(books, "[-1].price"))
text
20 records
['Sharp Objects', 'In a Dark, Dark Wood', 'The Past Never Ends']
['£20.89']
La sintaxis de ruta cubre claves de punto, claves entre comillas, índices negativos y comodines *. No tiene filtros ni descenso recursivo, y una ruta que no coincide con nada devuelve una lista vacía.
Ejecutar la misma receta desde la línea de comandos
El comando respondo lee un archivo local y escribe JSON por defecto, o CSV y Líneas JSON con --format:
bash
respondo records mystery-page-1.html --selector article.product_pod --fields book-fields.json --format csv | head -4
text
title,price,availability,url
Sharp Objects,£47.82,In stock,../../../sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,../../../in-a-dark-dark-wood_963/index.html
The Past Never Ends,£56.50,In stock,../../../the-past-never-ends_942/index.html
En modo records, las URL permanecen exactamente como aparecen en la página, incluso cuando se pasa --base. Resuélvelas en Python con normalize_url cuando necesites enlaces absolutos.
Procesar una carpeta de páginas guardadas
El modo por lotes ejecuta una receta en cada archivo coincidente en un directorio, en orden de nombre de archivo, y escribe una fila de salida por archivo:
bash
respondo records responses/ --batch --pattern '*.html' \
--selector article.product_pod --fields book-fields.json \
--format jsonl --output results.jsonl
python -c "import json; [print(row['source'], row['status'], len(row['result'])) for row in map(json.loads, open('results.jsonl'))]"
text
mystery-page-1.html ok 20
mystery-page-2.html ok 12
Cada fila lleva source, status, result y error, por lo que un archivo ilegible no detiene el resto. Las dos páginas contienen todos los 32 libros en la categoría. El modo por lotes nunca sobrescribe: ejecutar el mismo comando nuevamente se detiene con respondo: batch output exists y estado de salida 1, y una ruta de salida dentro de la carpeta de entrada es rechazada como insegura.
Dónde se Detiene Respondo: Captura la Página Con Scrapeless
Respondo analiza lo que se le da y nada más. No descarga páginas ni ejecuta JavaScript, por lo que sus selectores solo ven el HTML que reciben. Para ese paso, la API de extracción universal de Scrapeless obtiene una URL y devuelve la página, por lo que las dos mitades permanecen separadas: la clave de API pertenece a la llamada de colección, y la extracción se ejecuta localmente.
¿Configurando esto ahora? El plan gratuito de Scrapeless cubre tus primeras solicitudes.
Este script recoge la página de la categoría de misterio en vivo a través del actor unlocker.webunlocker, luego ejecuta la misma receta en ella. Lee tu clave de la variable de entorno SCRAPELESS_API_KEY:
python
import json
import os
import urllib.request
from respondo import extract_records, jsonl_dumps, normalize_url, records_to_csv
PAGE_URL = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
BOOK_FIELDS = {
"title": {"selector": "h3 a", "attr": "title", "required": True},
"price": ".price_color",
"availability": ".availability",
"rating": {"selector": "p.star-rating", "attr": "class"},
"url": {"selector": "h3 a", "attr": "href"},
}
def fetch_html(url):
payload = {"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET", "js_render": False}}
request = urllib.request.Request(
"https://api.scrapeless.com/api/v2/unlocker/request",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]},
)
with urllib.request.urlopen(request, timeout=120) as response:
body = json.load(response)
if body.get("code") != 200:
raise RuntimeError(f"Scrapeless returned code {body.get('code')}")
return body["data"]
html = fetch_html(PAGE_URL)
books = extract_records(html, "article.product_pod", BOOK_FIELDS)
for book in books:
book["url"] = normalize_url(book["url"], base=PAGE_URL)
book["rating"] = book["rating"].split()[-1]
print(len(books), "books")
print(records_to_csv(books[:3]), end="")
with open("mystery-books.jsonl", "w", encoding="utf-8") as handle:
handle.write(jsonl_dumps(books))
text
20 books
title,price,availability,rating,url
Sharp Objects,£47.82,In stock,Four,https://books.toscrape.com/catalogue/sharp-objects_997/index.html
"In a Dark, Dark Wood",£19.63,In stock,One,https://books.toscrape.com/catalogue/in-a-dark-dark-wood_963/index.html
The Past Never Ends,£56.50,In stock,Four,https://books.toscrape.com/catalogue/the-past-never-ends_942/index.html
La API envuelve la página en un sobre JSON, {"code": 200, "data": "<html>…"}, y urlopen genera una HTTPError para un fallo HTTP antes de que se lea el sobre. La calificación proviene de la lista de clases de p.star-rating, cuya última clase indica el número de estrellas. La guía de inicio rápido de la API de extracción universal enumera las otras opciones de solicitud, como el país del proxy y el manejo de redirecciones.
Solución de Problemas
| Lo que ves | Causa | Solución |
|---|---|---|
ValueError: required field has no matches |
Un elemento carece de un campo marcado required |
Verifica el selector contra la página, o elimina required y usa default |
ValueError: unsupported selector syntax |
El selector utiliza una pseudo-clase como :nth-child |
Seleccionar por clase, ID o atributo en su lugar |
ValueError: expected a tag, class, ID or attribute selector |
El selector utiliza + o ~ |
Usa combinadores de descendientes o hijos |
| Una columna está vacía para cada fila | El contenido se agrega mediante JavaScript después de cargar | Solicita la página con js_render habilitado |
| URLs relativas en la salida de CLI | El modo records mantiene los valores de los atributos como están |
Resuélvelas con normalize_url en Python |
| Un apóstrofo antes de algunos valores CSV | El escape de fórmulas está activado por defecto | Mantenlo, o pasa escape_formulas=False para consumidores confiables |
respondo: batch output exists |
El archivo de salida ya está allí | Elige un nuevo nombre de archivo |
respondo: batch unsafe output path |
El archivo de salida está dentro de la carpeta de entrada | Escribe los resultados en otro lugar |
Para las páginas que construyen su contenido en el navegador, renderizar páginas con la API Universal Scraping explica las opciones, y la documentación de JS Render lista los parámetros. Revisa precios para ver cuánto cuestan las solicitudes renderizadas.
Conclusión
Respondo convierte la mitad de extracción de un trabajo de scraping en configuración: un selector para el ítem repetido y una receta para sus campos. Desde allí, extract_records devuelve diccionarios que records_to_csv o jsonl_dumps convierten en archivos. El comando respondo ejecuta la misma receta en una carpeta y reporta un resultado para cada página.
Mantén las dos mitades separadas. Instala 0.6 desde GitHub, recopila páginas con la API Universal Scraping, y deja que Respondo trabaje en lo que regresa, sin una conexión de red o credenciales propias.
¿Listo para alimentar a Respondo con páginas reales? Comienza con el plan gratuito de Scrapeless y recopila tu primera página.
FAQ
P: ¿Qué es Respondo?
Respondo es una biblioteca de Python de código abierto de Scrapeless que extrae, transforma y exporta datos de HTML y JSON que ya tienes. No tiene dependencias en tiempo de ejecución y se ejecuta completamente en tu máquina.
P: ¿Cómo instalo Respondo 0.6?
Instálalo desde GitHub con python -m pip install "git+https://github.com/scrapeless-ai/respondo@be376d112ecf681011a079e809acae46b7e1ff59". El paquete de PyPI está en 0.4.0 y carece de las características de esta guía.
P: ¿Puede Respondo descargar páginas web?
No. Respondo solo analiza el contenido que le pasas. Usa la API Universal Scraping de Scrapeless, u otra fuente de HTML, para el paso de descarga.
P: ¿Cómo extraigo datos de HTML a CSV en Python con Respondo?
Llama a extract_records con el HTML, un selector para el ítem repetido y una receta de campo, luego pasa el resultado a records_to_csv. Desde la terminal, respondo records page.html --selector … --fields recipe.json --format csv hace lo mismo.
P: ¿Qué selectores CSS admite Respondo?
Etiquetas, clases, IDs, pruebas de atributos, combinadores de descendientes e hijos, y grupos por coma. Las pseudo-clases y combinadores de hermanos generan un ValueError.
P: ¿Por qué mi CSV tiene un apóstrofo delante de algunos valores?
Respondo antepone cadenas que comienzan con =, +, -, @, un tabulador o un salto de línea, para que las hojas de cálculo no las ejecuten como fórmulas. Los números se dejan sin cambios, y escape_formulas=False desactiva el prefijo.
P: ¿Maneja Respondo páginas renderizadas con JavaScript?
Respondo analiza el HTML que recibe y no ejecuta scripts. Obtén tales páginas con la renderización de JavaScript habilitada en la API Universal Scraping, luego pasa el HTML renderizado a Respondo.
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.



