PyQuery coloca una API al estilo jQuery sobre lxml. En cinco tamaños de página, desde 1 KB hasta 10 MB, todas las medianas mostradas quedaron por debajo de las de lxml en bruto, incluida una diferencia del 1,5% en el tamaño más grande. El benchmark no demuestra que el wrapper sea más rápido; simplemente no encontró una diferencia lo bastante grande como para cambiar esta decisión entre seleccionar y leer.
También se mantuvo dentro de unos pocos puntos porcentuales de selectolax desde 10 KB en adelante en las medianas mostradas. Sin un margen de equivalencia definido de antemano, eso es un resultado cercano, no un empate estadístico.
Qué es PyQuery
PyQuery es una biblioteca de Python que te da la API de selectores y encadenamiento de jQuery sobre un árbol de documentos lxml. Versión probada: 2.1.0, licencia BSD, 2.380 estrellas en GitHub, 59 incidencias abiertas, último push el 2026-07-27.
Referencia oficial: documentación de PyQuery.

from pyquery import PyQuery as pq
d = pq(html)
titles = [e.text_content() for e in d("h3.title")]
hrefs = [e.get("href") for e in d("a")]
Los elementos que devuelve son elementos de lxml, así que todo lo que ya sabes hacer con lxml sigue funcionando. Esa es la idea: PyQuery es una capa de comodidad, no un parser. pip install pyquery instala 3 paquetes — lxml, cssselect y el propio PyQuery — y ocupa 20,1 MiB, casi todo correspondiente a las extensiones compiladas de lxml.
Si has usado cheerio en Node, aquí la idea general de la API es la misma en Python. Los dos selectores probados aquí funcionaron en ambos; la prueba no demuestra paridad total del lenguaje de selectores entre cssselect y cheerio.
La medición
Esta base de investigación ya contaba con un benchmark de parsers con una propiedad que la mayoría no tiene: una puerta de paridad que compara el hash del contenido extraído — títulos ordenados más hrefs ordenados — contra un parser de referencia, de modo que una biblioteca no pueda “hacer trampa” omitiendo trabajo y aun así marcar un tiempo rápido. Cinco tamaños de página, 50 iteraciones, tres ejecuciones independientes.
Añadir PyQuery a este entorno exigió dos cosas.
Volver a ejecutar la referencia. selectolax se ejecutó de nuevo en el mismo proceso. El hash del contenido coincidió en 5 de 5 tamaños y su p50 quedó entre 0,989× y 1,079× de la cifra publicada; es decir, se trata de la misma máquina y el mismo banco de pruebas.
Ejecutar también lxml en el mismo proceso. El benchmark publicado registra la máquina y la versión de Python, pero no las versiones de las bibliotecas, así que su fila de lxml podría haber provenido de una versión distinta de lxml a la que PyQuery envuelve aquí. Comparar a través de esa brecha habría sido comparar dos versiones de lxml y llamarlo “coste del wrapper”. Ejecutar lxml junto a PyQuery elimina la duda — ambos son lxml 6.1.1 en este entorno virtual.
| Tamaño de página | selectolax | PyQuery | lxml | PyQuery vs lxml |
|---|---|---|---|---|
| 1 KB | 0.0286 ms | 0.0456 ms | 0.0508 ms | 0.90× |
| 10 KB | 0.1725 ms | 0.1728 ms | 0.1802 ms | 0.96× |
| 100 KB | 1.4855 ms | 1.4093 ms | 1.4145 ms | 1.00× |
| 1 MB | 14.97 ms | 14.96 ms | 15.03 ms | 1.00× |
| 10 MB | 158.10 ms | 162.86 ms | 165.25 ms | 0.99× |
p50 en milisegundos, mediana de tres ejecuciones, todo en un solo proceso. parser-bench.json. Los tres hashes de contenido coincidieron con la referencia en todos los tamaños.
El coste del wrapper que no aparece

PyQuery quedó igual o por debajo de lxml en bruto en todas las medianas mostradas. Eso no es prueba de que un wrapper haga más rápido el parseo. Tres medianas de ejecución y ningún margen de equivalencia predefinido apuntan a una conclusión más estrecha: en este fixture no apareció una sobrecarga de selectores relevante para la decisión.
En 10 MB, las tres ejecuciones de PyQuery fueron 162.86, 163.17 y 161.13 ms; las de lxml fueron 169.46, 165.25 y 164.18. Los rangos son cercanos, pero no se solapan. En 1 MB, las dos medianas difieren en 0,5%. Estas ejecuciones pequeñas respaldan una decisión práctica, no una afirmación de equivalencia estadística.

El mecanismo es bastante simple: pq(html) construye un árbol lxml una sola vez, d("h3.title") compila el selector CSS mediante cssselect igual que hace tree.cssselect(), y los elementos devueltos son elementos de lxml. En esta ruta caliente medida, PyQuery apenas añade trabajo. El recorrido, la manipulación, las consultas repetidas, la importación y la memoria quedan fuera de esta afirmación sobre el tiempo de selección.
Resultados muy cercanos desde 10 KB en adelante
El hallazgo más útil es la primera columna.
Desde 10 KB en adelante, la diferencia entre el más rápido y el más lento entre selectolax, lxml y PyQuery fue de 4,5% en 10 KB, 5,4% en 100 KB, 0,5% en 1 MB, y 4,5% en 10 MB. Esta ejecución no fue una prueba de equivalencia; la conclusión práctica es que estas diferencias no cambiarían la mayoría de las decisiones de parser para esta carga de trabajo.
selectolax sí es realmente más rápido en 1 KB — 0.0286 ms frente a 0.0456 y 0.0508 — pero esa fila no sirve. Entre los tres parsers, la dispersión en ese tamaño es de 77,6%, y las tres ejecuciones de selectolax oscilaron entre 0.0267 y 0.0404 ms. A 28 microsegundos, el temporizador y el planificador dominan. Yo no pondría un ranking ahí.
Para esta carga de trabajo de seleccionar y leer, elige entre estos tres según la API y los hechos medidos de dependencias, no por una supuesta jerarquía de velocidad. PyQuery no mostró una penalización relevante frente a lxml. selectolax usa una pila de parser distinta, pero este artículo no midió su huella instalada, cobertura de wheels ni requisitos de compilación con la misma base.
A modo de contraste, el benchmark publicado colocó otras dos opciones de Python en el mismo fixture de 10 MB, y ahí sí se ven diferencias reales:
| Parser (10 MB) | p50 |
|---|---|
| selectolax (lexbor) | 159.93 ms |
| lxml | 172.93 ms |
| parsel | 231.85 ms |
| selectolax (modest) | 247.95 ms |
| BeautifulSoup + lxml | 2,261.56 ms |
| BeautifulSoup + html.parser | 2,788.75 ms |
Cifras publicadas desde bench_parse.json.
Las filas históricas del benchmark sitúan a BeautifulSoup más de un orden de magnitud por encima de las medianas de los parsers más rápidos en este fixture. Esas filas no se volvieron a ejecutar con el par PyQuery/lxml del proceso actual, así que sirven como contexto, no como multiplicador controlado para el veredicto principal.
La fila almacenada de Cheerio fue 2.927,89 ms (2927.8857 en parser-bench.json) con hashes del contenido extraído coincidentes. Ese resultado entre runtimes también depende de Node, de las versiones de los paquetes y de los controles históricos de la ejecución; no debe leerse como un multiplicador aislado de velocidad de la biblioteca.
La realidad del entorno
| Biblioteca | Paquetes | Disco | Licencia | Estrellas | Último push |
|---|---|---|---|---|---|
| PyQuery | 3 | 20.1 MiB | BSD | 2,380 | 2026-07-27 |
| cheerio (Node) | 22 (npm) | 9.0 MiB | MIT | 30,449 | 2026-08-11 |
Referencia oficial: PyQuery en PyPI.
Tres paquetes es una huella de dependencias limpia, y dos de ellos — lxml y cssselect — son cosas que muchísimos proyectos de scraping en Python ya utilizan. En ese caso, el coste marginal de PyQuery son apenas unas decenas de kilobytes.
Los 20,1 MiB corresponden a las extensiones compiladas de lxml, no a PyQuery. Es el mismo coste de unos 20 MiB que pagas si usas lxml directamente.
La biblioteca tiene licencia BSD. En la instantánea fechada tenía 59 incidencias abiertas y un push tres semanas antes de las pruebas; esas observaciones por sí solas no demuestran calidad de mantenimiento ni compatibilidad futura.
Memoria, y qué le hace el HTML roto
Dos cosas que todos los análisis de este lote marcaban como no probadas, ahora medidas.
El contexto más amplio de la prueba de estrés está en la comparativa de memoria y HTML malformado entre diez bibliotecas.
Memoria residente máxima, mediante /usr/bin/time -l, un proceso nuevo por celda — el piso de importación es lo que cuesta la biblioteca cargada e inactiva, y los picos incluyen el documento.
| Biblioteca | Runtime | Piso de importación | Pico 226 KB | Pico 10 MB |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json. Las líneas base de Python y Node no son comparables entre sí; el intérprete está dentro de ambas.
PyQuery es más ligero que resiliparse en el documento grande — 172.5 MiB frente a 225.1 — pese a tener un piso de importación más alto. El árbol de lxml es compacto, y la mayor parte del piso de 30,3 MiB de PyQuery se debe a cargar lxml, no a algo que PyQuery haga por sí mismo.
HTML roto. Doce documentos, cada uno rompiendo exactamente una cosa — etiquetas sin cerrar, elementos en línea mal anidados, atributos sin comillas con espacios, cierres sobrantes, ausencia total de <html>, atributos duplicados, un documento truncado a mitad de etiqueta, entidades inválidas, un <script> sin cerrar, una declaración de charset engañosa, un comentario que contiene marcado, y 600 niveles de anidación — más dos controles bien formados con tamaños equivalentes, porque “no devolvió nada” solo dice algo sobre lo malformado si la biblioteca tampoco calla en un documento limpio del mismo tamaño.
pyquery lanzó excepción en 0 de 14 y no devolvió nada en 1, recuperando 10/22 sentinelas en los fixtures rotos (malformed-results.json). Un fixture queda excluido de ese conteo: según HTML5, todo lo que sigue a un <script> sin cerrar sí es contenido del script, así que perderlo ahí es correcto y recuperarlo sería la desviación. Sin los resultados de sentinelas de las alternativas directas a su lado, 10/22 es una observación de robustez, no un ranking de selección de parser.
Pros y contras
A favor. Sintaxis jQuery, familiar para cualquiera que haya escrito JavaScript de front-end o haya usado cheerio. En este fixture no apareció una sobrecarga de selectores relevante frente a lxml en bruto. Solo 3 paquetes, y 2 de ellos probablemente ya están en tu árbol. Devuelve elementos lxml, así que las técnicas de lxml siguen disponibles. BSD. Los hashes de contenido coincidieron con la referencia en los cinco tamaños.
En contra. 20,1 MiB, por lxml. 2.380 estrellas implican una comunidad mucho menor que las 30.449 de cheerio — menos ejemplos prácticos cuando algo raro ocurre. Es una capa de comodidad, así que cualquier cosa que lxml no pueda hacer, tampoco la puede hacer. Y si esperabas que la API jQuery te diera rendimiento, no: te da ergonomía, y el parser de debajo es el que hace el trabajo.
Quién debería usarlo y quién no
Usa PyQuery si tú o tu equipo preferís selectores estilo jQuery en Python. La construcción, las dos selecciones y las lecturas medidas no mostraron penalización relevante frente a lxml; otras operaciones de PyQuery no se cronometraron.
Usa lxml directamente si prefieres XPath o quieres un paquete menos. Esta ejecución no mostró una razón de velocidad en selectores para elegir entre ambos.
Evalúa selectolax si su API de parser y su pila de dependencias encajan con tu proyecto. La fila de 1 KB está explícitamente sin ranking, y este artículo no respalda una afirmación de “dependencia más pequeña”.
En Node, cheerio es la forma equivalente de API. Las filas cruzadas almacenadas fueron más lentas aquí, pero las diferencias de runtime y de ejecución histórica impiden una conclusión limpia solo a nivel de biblioteca.
Dónde encaja una API gestionada
PyQuery analiza HTML que ya tienes. No descarga, no renderiza JavaScript ni se ocupa de una capa anti-bot — ningún parser de esta comparación lo hace, y en muchos objetivos reales esa es la mitad más difícil.
Nota del autor: Thunderbit es nuestra opción gestionada para obtención, renderizado y extracción a partir de una URL. No se comparó con PyQuery en este análisis. La frontera relevante es si ya tienes el HTML y quieres selectores locales, o si quieres que la adquisición y extracción de la página funcione como servicio.
La idea honesta: si ya tienes el HTML y conoces tus selectores, PyQuery es gratis y agradable. Si los selectores se rompen una y otra vez, o estás obteniendo datos a escala, entonces estás comprando otra cosa.
Para un panorama más amplio, nuestro resumen de APIs de web scraping cubre opciones alojadas y el artículo pilar de scrapers de código abierto cubre las autogestionadas. Si la salida parseada alimenta a un modelo, convertir HTML a Markdown en Python es donde se empieza a perder fidelidad.
Prueba Thunderbit para extraer datos web
¿Deberías usar PyQuery?
Sí, si quieres sintaxis tipo jQuery en Python y la ruta medida de selección y lectura representa tu carga de trabajo.
El benchmark no encontró una sobrecarga de selectores relevante frente a lxml, manteniendo la paridad del hash de contenido. No demostró coste cero para la biblioteca en su conjunto.
En cinco tamaños, las tres medianas de los parsers de Python estuvieron lo bastante cerca como para que el ajuste a la API probablemente pese más en esta tarea. Define un margen de equivalencia y vuelve a ejecutar las alternativas exactas antes de convertir ese juicio en un ranking más amplio de parsers.
Prueba Thunderbit para extraer datos web Get Started Free
Preguntas frecuentes
¿PyQuery ralentiza lxml?
En esta ejecución no apareció una sobrecarga de selectores relevante para la decisión. En cinco tamaños de página, sus medianas quedaron igual o por debajo de lxml en bruto, usando ambos lxml 6.1.1 en el mismo proceso. En 10 MB, los rangos cercanos no se solaparon: PyQuery 161.13–163.17 ms y lxml 164.18–169.46 ms. pq(html) construye un árbol lxml, y los selectores probados se compilan mediante cssselect.
¿selectolax es más rápido que PyQuery? Su mediana en 1 KB fue menor, pero esa fila no se clasifica porque la variación domina a escala de microsegundos. A partir de 10 KB, las diferencias de medianas fueron del 0,5% al 5,4%. Eso es cercano para esta carga de trabajo, no una prueba de equivalencia ni de rangos solapados en todos los casos.
¿Por qué volver a ejecutar lxml en vez de citar la cifra publicada? Porque el benchmark publicado registra la máquina y la versión de Python, pero no las versiones de las bibliotecas. Su fila de lxml podría haber venido de una versión distinta a la que PyQuery envuelve hoy, y una brecha de versión habría parecido un coste del wrapper que no existe. Ejecutar ambos en un solo proceso con lxml 6.1.1 elimina la ambigüedad.
¿Cómo se compara con cheerio? Misma idea general de API, ecosistema distinto. Los dos selectores probados aquí funcionaron en ambos y los hashes de contenido coincidieron en los cinco tamaños; eso no demuestra compatibilidad total de selectores. Los tiempos almacenados de cheerio fueron más lentos, pero las diferencias entre runtime y ejecución histórica impiden afirmar un multiplicador solo de biblioteca.
¿Qué no se probó aquí? La memoria se midió como RSS máximo para solo importación, un documento de 226 KB y un documento de 10 MB. El HTML malformado se probó con 12 documentos rotos más dos controles equivalentes. Aún no se han probado el rendimiento de manipulación y recorrido de PyQuery, la caché de consultas repetidas, la obtención de URL, la concurrencia y cargas de trabajo reales representativas de sitios web. La medición de 1 KB sigue sin clasificar.


