Web Scraping con pyquery: Análisis HTML al estilo de jQuery en Python
Senior Web Scraping Engineer
Resumen:
- pyquery te ofrece la API de jQuery en Python. Si conoces
$("div.quote").find("small.author").text(), ya conoces pyquery. - Lo que pyquery no hace es obtener. Envuelve
lxmlpara el análisis y la selección; no tiene cliente HTTP y no ejecuta JavaScript. - La diferencia se muestra en un script. Un GET simple en una página de demostración con JavaScript renderizado le da a pyquery 0 nodos de cita; la misma URL a través de la API Universal de Scraping Scrapeless con
js_renderle da todas las 10. - La extracción es real en esta guía. Una ejecución en vivo utilizó
.items(),.find()y.text()para obtener las 10 citas con autores y etiquetas desde el HTML renderizado. - Las dos capas se mantienen separadas. Scrapeless obtiene y renderiza; pyquery selecciona. Ninguno interfiere en el trabajo del otro.
- Libre para comenzar del lado de la obtención. Crea tu clave API de Scrapeless en app.scrapeless.com.
Qué es pyquery, y qué no es
pyquery es una biblioteca de Python que pone una API al estilo de jQuery sobre lxml. Envuelves el marcado en un objeto PyQuery — convencionalmente llamado d — y luego seleccionas y recorres con las mismas llamadas que ya usa un desarrollador de front-end: d("selector"), .find(), .eq(), .text(), .attr(), .items(). Para cualquiera que venga del navegador, es el camino más corto desde "sé cómo consultar el DOM" hasta "puedo extraer esto en Python", y dado que se basa en lxml, la selección subyacente es rápida.
Es una biblioteca de análisis y selección, nada más. pyquery no tiene cliente HTTP, no mantiene sesiones, y no ejecuta JavaScript. Dale una cadena y construirá un documento consultable; pídele que obtenga una URL y, aunque técnicamente puede hacerlo, utiliza una solicitud simple sin renderizado, que es la herramienta incorrecta para cualquier página construida por scripts. Así que cada configuración de "scraping web con pyquery" consiste en dos capas: algo que devuelve HTML renderizado fielmente, y pyquery que selecciona de él. Esta guía utiliza la API Universal de Scraping de Scrapeless para la primera capa. El más amplio tutorial de scraping web en Python cubre el ecosistema circundante.
Instalación
pyquery y requests son toda la cadena de herramientas. La versión contra la que se escribió esta guía es pyquery 2.0.1:
bash
pip install "pyquery==2.0.1" requests
Mantén tu clave en el entorno, nunca en el código fuente:
bash
export SCRAPELESS_API_KEY="sk_tu_clave_scrapeless"
Obtén HTML que vale la pena analizar
La calidad de la selección está limitada por la fidelidad de la obtención, así que empieza allí. En una página renderizada con JavaScript, el marcado que recibe un cliente HTTP simple no es el marcado que ve un lector — el contenido llega solo después de que los scripts construyen el DOM, un ciclo de vida definido por la especificación de scripting HTML. Un script cuenta la diferencia a través de pyquery mismo:
python
# fidelity.py — lo que pyquery ve: GET simple vs renderizado del lado del servidor
import os
import requests
from pyquery import PyQuery as pq
URL = "https://quotes.toscrape.com/js/"
plain = requests.get(URL, timeout=60).text
print("caracteres GET simples:", len(plain), "| nodos de cita:", pq(plain, parser="html")("div.quote").length)
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
headers={"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]},
json={"actor": "unlocker.webunlocker", "input": {"url": URL, "js_render": True}},
timeout=120,
)
resp.raise_for_status()
rendered = resp.json().get("data", "")
print("caracteres renderizados:", len(rendered), "| nodos de cita:", pq(rendered, parser="html")("div.quote").length)
La ejecución imprime 0 nodos de cita para la obtención simple y 10 para la renderizada:
text
caracteres GET simples: 5806 | nodos de cita: 0
caracteres renderizados: 8940 | nodos de cita: 10
El renderizado, desbloqueo y enrutamiento de proxy ocurren del lado del servidor en esa única POST — la API Universal de Scraping es la capa de obtención, y el marcado renderizado es de lo que pyquery selecciona.
Extraer con la API de jQuery
Con HTML real en mano, pyquery realiza la extracción de la manera en que lo haría jQuery. .items() convierte una selección en un iterador de objetos PyQuery, .find() limita un sub-selector a cada uno, y .text() lee el texto — los selectores siguen la especificación de Selectores de W3C, la misma sintaxis que utiliza jQuery:
python
# extract.py — obtener la página renderizada, luego seleccionar con la API de jQuery
import os
import requests
from pyquery import PyQuery as pq
resp = requests.post(
"https://api.scrapeless.com/api/v2/unlocker/request",
text
headers={"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]},
json={"actor": "unlocker.webunlocker", "input": {"url": "https://quotes.toscrape.com/js/", "js_render": True}},
timeout=120,
)
resp.raise_for_status()
d = pq(resp.json().get("data", ""), parser="html")
records = []
for quote in d("div.quote").items():
records.append({
"text": quote.find("span.text").text(),
"author": quote.find("small.author").text(),
"tags": [pq(tag).text() for tag in quote.find("a.tag").items()],
})
print("registros:", len(records))
print("primer autor:", records[0]["author"])
print("primeras etiquetas:", records[0]["tags"])
La ejecución en vivo seleccionó todos los 10 registros, etiquetas y todo:
text
registros: 10
primer autor: Albert Einstein
primeras etiquetas: ['cambio', 'profundos-pensamientos', 'pensar', 'mundo']
Ese es todo el scraper: un POST para obtener y renderizar, un objeto PyQuery para consultar. El iterador .items() es la idiomática de pyquery que vale la pena recordar: es lo que convierte una selección en objetos por registro en los que puedes .find(), el análogo directo de .each() de jQuery.
Obtén tu clave API en el plan gratuito: app.scrapeless.com
Patrones avanzados
- Siempre itera con
.items(). Recorrer una selecciónPyQuerydirectamente produce elementos lxml en bruto, no objetosPyQuery, por lo que.find()se rompe..items()te da objetos envueltos con la API completa en cada uno. - Lee atributos con
.attr().quote.find("a::attr(href)")no es sintaxis de pyquery; usaquote.find("a").attr("href"), exactamente como en jQuery. - Pasa
parser="html"para páginas reales. Selecciona el analizador HTML indulgente de lxml, que maneja el marcado malformado que envían los sitios reales; el predeterminado puede ser más estricto de lo que quieres. - Encadena, no reconsulta.
d("div.quote").eq(0).find("small.author")limita cada paso al último, lo que es más rápido y se acerca a cómo ya sabes que jQuery lee.
Solución de problemas
- Cero nodos de una página que puedes ver en un navegador. El contenido se renderiza con JavaScript y tu solicitud devolvió el HTML pre-renderizado. Cuenta un selector conocido como lo hace el primer script; un documento casi vacío es un problema de fetch, que se soluciona con
js_render, no un problema de selector. .find()planteaAttributeError. Iteraste la selección directamente en lugar de usar.items(), así que obtuviste un elemento lxml sin procesar. Cambia el bucle afor x in sel.items():..text()devuelve todo concatenado. pyquery une el texto descendente. Limita el alcance con un selector más específico, o lee un solo nodo con.eq(0).text().- La codificación parece incorrecta. Pasa el texto de la respuesta primero a pyquery con
parser="html"; lxml lee la codificación declarada del documento cuando el analizador es el HTML.
Conclusión
pyquery se gana su lugar como la capa de selección que habla jQuery: las mismas llamadas .find(), .text(), y .items() que conoces del navegador, sobre un árbol lxml rápido. La capa que decide si cualquiera de eso es posible es la obtención: el conteo de 0 contra 10 del primer script lo resuelve, y una POST renderizada por el servidor cierra la brecha. Conéctalos y las diez citas de la página de demostración llegan como registros limpios, seleccionados de la manera que ya piensas.
Crea una cuenta gratuita en Scrapeless para obtener una clave API, y la documentación para desarrolladores cubre los parámetros de unlocker.webunlocker. Consulta precios de Scrapeless cuando planees un trabajo recurrente.
FAQ
P: ¿Puede pyquery raspar sitios web por sí mismo?
No realmente. pyquery puede extraer una URL, pero lo hace con una solicitud simple y sin renderización de JavaScript, por lo que en una página moderna selecciona de un marcado vacío. Trátalo como un analizador: combínalo con una capa de obtención — aquí la API de Scrapeless Universal Scraping, que renderiza la página del lado del servidor — y pyquery maneja la selección de HTML devuelto.
P: ¿Es pyquery lo mismo que jQuery?
Copia la API de jQuery en Python — d("selector"), .find(), .text(), .attr(), .items() — pero se ejecuta del lado del servidor en lxml, no en un navegador, por lo que selecciona de un marcado estático y no ejecuta scripts ni maneja eventos. La sintaxis de selección se transfiere; el tiempo de ejecución no.
P: ¿pyquery o BeautifulSoup?
Preferencia. pyquery se lee como jQuery y es una combinación natural si vienes del trabajo de front-end; BeautifulSoup tiene una API más pythonica. Ambas analizan el mismo HTML, y ambas necesitan una capa de obtención separada para páginas renderizadas por JavaScript.
P: ¿Maneja pyquery páginas renderizadas por JavaScript?
No por sí solo; nunca ejecuta scripts. Si el contenido se carga después del HTML inicial, una simple petición le entrega a pyquery un documento vacío. Obtén la página a través de la API de Scrapeless con `js_render` primero, luego selecciona del HTML renderizado, como hace esta guía.
**P: ¿Es legal el scraping con pyquery?**
La biblioteca de selección no cambia las reglas de recopilación. Obtén solo páginas públicas, respeta los términos del sitio y las directivas de robots estandarizadas por <a href="https://datatracker.ietf.org/doc/html/rfc9309" rel="nofollow"><strong>el Protocolo de Exclusión de Robots</strong></a>, mantén los volúmenes limitados y maneja cualquier dato personal según las leyes que te apliquen.
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.



