Cada artículo sobre el "analizador HTML en Python más rápido" termina mencionando selectolax, y casi todos rematan con la misma idea: "mucho más rápido que BeautifulSoup". Y sí, eso es cierto. Lo que casi nunca se completa es qué pasa cuando comparas selectolax con lxml, porque ahí el "más rápido" viene con asterisco.
Así que lo medí como toca: selectolax (con sus dos backends) frente a lxml, BeautifulSoup usando html.parser y lxml, y parsel, en cinco tamaños de página, desde 1 KB hasta 10 MB. Cada medición se tomó como la mediana de tres ejecuciones independientes en procesos separados. selectolax superó con claridad a BeautifulSoup y empató con lxml en el trabajo total, pero perdió frente a lxml en la etapa pura de parseo. Todos los números de abajo son provisionales y salen de una sola máquina (macOS arm64, Python 3.14.2); los scripts están publicados, así que pruébalos en tu equipo antes de citarme.
Qué es realmente selectolax y qué no es
selectolax es un binding de Python para dos motores en C — Modest y Lexbor — que parsean HTML5 y permiten consultarlo con selectores CSS. No es un crawler, no es un navegador, no es un "scraper" en el sentido de hacer clic en un botón. Es la pieza que recibe un bloque de HTML después de que ya lo descargaste. La propia descripción del mantenedor lo resume así: "A fast HTML5 parser with CSS selectors, written in Cython, using Modest and Lexbor engines."
Hay dos backends, y la diferencia importa mucho más de lo que deja ver la documentación:
LexborHTMLParser(motor Lexbor) — el que el README recomienda usar a partir de 2024.HTMLParser(motor Modest) — el original, cuya librería C subyacente "ya no se mantiene", según ese mismo README.
Antes de hablar de velocidad, conviene tener algunos datos. En la instantánea del repositorio tomada el 2026-07-10, selectolax tenía 1,653 estrellas y la versión más reciente era v0.4.10 (mayo de 2026). En PyPI se indica compatibilidad con Python >=3.9,<3.15. La instalación es la parte menos dramática de esta revisión: pip install selectolax descargó un wheel precompilado de 2.3 MB para cp314 y funcionó de inmediato en Python 3.14 — sin bajar navegador, sin paso de doctor, sin compilación. Esa es la ventaja silenciosa de un parser puro frente a una herramienta apoyada en navegador: se importa y ya.
Hay un detalle de licencia que conviene dejar claro desde ya: el binding de Python es MIT, pero el wheel incluye los motores compilados, y esos traen sus propias licencias — Modest es LGPL-2.1, Lexbor es Apache-2.0. Así que decir "selectolax es MIT" es correcto para el código Python, pero incompleto para el binario que realmente distribuyes. Si tu equipo legal revisa componentes redistribuidos, esa es la distinción que hay que señalar.
La pregunta de la velocidad, respondida con números reales
La tarea que medí fue esta: parsear la cadena HTML, extraer todo el texto de <h3 class="title"> y sacar cada enlace href de <a>. La latencia se midió en milisegundos, como mediana de tres ejecuciones separadas; la variación entre corridas se mantuvo por debajo de ~5% para los parsers con backend en C en la mayoría de los tamaños. Antes de cronometrar cada celda, reduje la salida de cada parser a un hash de contenido, para detectar y excluir a cualquiera que hiciera menos trabajo en silencio. En estas páginas, los seis coincidieron en todos los tamaños, así que esta comparación sí es de peras con peras. Los datos completos están en bench_parse.json.

| Página | selectolax (Lexbor) | selectolax (Modest) | lxml | parsel | BS (lxml) | BS (html.parser) |
|---|---|---|---|---|---|---|
| 1 KB | 0.027 | 0.029 | 0.036 | 0.044 | 0.281 | 0.323 |
| 10 KB | 0.160 | 0.171 | 0.166 | 0.228 | 1.705 | 2.049 |
| 100 KB | 1.464 | 1.565 | 1.423 | 2.020 | 16.5 | 20.6 |
| 1 MB | 14.901 | 16.9 | 14.177 | 20.9 | 181.9 | 232.6 |
| 10 MB | 159.9 | 247.9 | 172.9 | 231.9 | 2261.6 | 2788.7 |
Frente a BeautifulSoup: unas 12-17x, y el rumor se queda corto
Si conviertes eso en ratios, selectolax-Lexbor resulta unas 12x más rápido que BeautifulSoup(html.parser) en una página de 1 KB, subiendo a unas 17x en 10 MB, y unas 10-14x más rápido que BeautifulSoup(lxml) en el mismo rango. La cifra que suele circular en internet —"selectolax es unas 4-5 veces más rápido que BeautifulSoup"— se queda corta frente a html.parser y solo se acerca a la realidad cuando BeautifulSoup usa lxml. El múltiplo real depende de qué versión de BeautifulSoup uses y de cuánto extraigas por página.
Eso también encaja con el benchmark del propio README, que sugiere una ventaja de 25.5x sobre BeautifulSoup(html.parser). Ninguna de las dos cifras es falsa. La tarea del README (título, enlaces, scripts y metadatos de páginas pequeñas) extrae menos contenido en páginas chicas, lo que hace pesar más la sobrecarga por parseo de BeautifulSoup. En términos prácticos, el resultado es este: selectolax es aproximadamente 10-15x más rápido que BeautifulSoup en trabajo real de parseo y extracción, con una ventaja mayor en páginas pequeñas y en extracciones ligeras.
Si tu cuello de botella actual es una montaña de código con BeautifulSoup procesando páginas, esta migración se paga sola. Ese caso no genera debate. El siguiente sí.
Frente a lxml: empate, y lxml gana la parte que casi todos olvidan aislar
Mira las filas de 100 KB y 1 MB. Lexbor y lxml están dentro de ~5% uno del otro, sus bandas por corrida se superponen, y con mi metodología eso es empate: no hay ganador, no hay "más rápido". El único punto en que selectolax se despega de verdad es la página de 10 MB (159.9 ms frente a 172.9 ms, una diferencia de 8.1% con intervalos que no se solapan). Así que, en la tarea completa, selectolax iguala a lxml y solo lo supera en los documentos más grandes.

Luego separé la construcción del árbol de la consulta CSS, y el resultado cambia de forma que muchas reseñas no detectan. En el parseo puro, sin ninguna consulta, lxml fue consistentemente ~33-34% más rápido que selectolax-Lexbor en esta máquina: 77.9 ms frente a 116.6 ms en la página de 10 MB. En la tarea completa, ambos convergen de todos modos, y mi hipótesis de trabajo —no algo que haya demostrado con un experimento de atribución— es que, en estas páginas, la consulta CSS ocupa una porción pequeña del tiempo total, por lo que la ventaja de lxml en el parseo se diluye hasta que los totales se igualan.
Esta es probablemente la afirmación más atacable de toda la revisión, y quiero ser transparente sobre por qué. Va contra la sabiduría habitual, y el único benchmark publicado que encontré que aísla solo parseo —aows.jpt.sh— reporta lo contrario, con selectolax unas 4 veces más rápido. Por eso lo acoté: el resultado es de una sola plataforma (macOS arm64, Python 3.14, wheels cp314 precompilados — no probé Linux x86_64 ni compilación desde código fuente), se verificó en cuatro tamaños de página y se mantuvo en todos, y además se revalidó con dos APIs distintas de lxml para descartar un artefacto de API. Ambas APIs de lxml superaron a selectolax-Lexbor en todos los tamaños. No presento "lxml parsea más rápido" como verdad cerrada; lo presento como lo que arrojó mi prueba, con el script adjunto, frente a la mayoría de los números publicados. Pruébalo en tu equipo.
Un matiz más: al consultar 100,000 <a> en una página plana, lxml y selectolax-Modest empatan (33.30 ms frente a 34.19 ms, con bandas superpuestas), mientras que selectolax-Lexbor queda ~15% por detrás de ambos. Lo que comparten los tres motores en C es que son 5-7x más rápidos que parsel o BeautifulSoup en selecciones masivas, donde el modelo de un objeto Python por nodo es lo que realmente frena. Así que tampoco se sostiene que "selectolax es el más rápido en selección CSS masiva": Modest apenas empata con lxml, y Lexbor pierde frente a él.
La conclusión que sí defendería es esta: la ventaja de selectolax sobre lxml no es una superioridad amplia en velocidad total. Solo gana en la página más grande. Su valor está en otras cosas: ergonomía de la API, comportamiento ante entradas malas y CSS moderno. Ahí entra el resto de la revisión.
Memoria y arranque en frío: clasifica por RSS, no por tu profiler
La memoria es el punto en que tengo que corregir mis propios números anteriores, y esa corrección es justamente el aprendizaje. Medido como delta de RSS en la página de 10 MB con tracemalloc desactivado, BeautifulSoup usa alrededor de 1.5-1.8x más memoria que selectolax o lxml — el rango va desde 1.51x (BS-lxml con 218.4 MB frente a Lexbor con 144.6 MB) hasta 1.75x en el extremo superior. selectolax y lxml comparten el escalón de menor consumo; lxml es el más liviano por RSS.

En una versión anterior reporté "~3x", y ese número estaba mal por una razón útil: lo medí con tracemalloc activo, y el registro por asignación de tracemalloc casi duplica el RSS aparente del parser que más asigna. Así que una advertencia para cualquiera que mida memoria de parsers: clasifica por RSS con el profiler apagado. Ordenar parsers por el pico de tracemalloc descoloca especialmente a los motores en C: hizo que selectolax-Lexbor pareciera más pesado que Modest cuando, según el RSS real, están bastante cerca. BeautifulSoup sí es realmente el más pesado aquí; solo que no lo es por el margen de 3x que mostraba un instrumento contaminado.
El arranque en frío importa menos, pero importa: selectolax tarda unos 14 ms en importarse, aproximadamente al nivel de lxml y ~2.3x más rápido que bs4 o parsel. Si estás distribuyendo una CLI o una función serverless donde el tiempo de importación cuenta en cada ejecución, ese margen merece atención.
Cobertura de selectores CSS: sólida, con un par de agujeros reales
La cobertura CSS se probó con una matriz de 41 casos, cada selector validado contra un fixture con respuestas conocidas, más una ronda deliberada de pruebas para forzar fallos en el motor Lexbor. Cada caso se ejecutó en un subproceso distinto, algo que resultó necesario porque uno de ellos tumba por completo el intérprete. Los resultados:

| Motor | PASS | WRONG | UNSUPPORTED | PROCESS_ABORT |
|---|---|---|---|---|
| soupsieve | 41 | 0 | 0 | 0 |
| selectolax Lexbor | 39 | 0 | 2 | 0 |
| lxml (cssselect) | 37 | 1 | 3 | 0 |
| parsel (cssselect) | 37 | 1 | 3 | 0 |
| selectolax Modest | 35 | 3 | 2 | 1 |
Cuando entran selectores hostiles, Lexbor no es el ganador absoluto — lo es soupsieve, con un limpio 41/41 frente a 39/41 de Lexbor. Los dos fallos de Lexbor son :lang(en) y :dir(rtl), que rechaza con error de parseo. Aparte de eso, acierta todo, incluyendo :has(), :is(), :where() y atributos sin distinción de mayúsculas/minúsculas.
Donde Lexbor sí destaca es frente al stack cssselect. El selector estrella del README — div > :nth-child(2n+1):not(:has(a)) — devuelve el conjunto correcto en ambos motores de selectolax y en soupsieve, pero el conjunto incorrecto en lxml y parsel, sin lanzar error. Un scraper que copie ese selector a Scrapy o parsel obtendrá resultados erróneos en silencio. Para ser preciso con el contexto: cssselect parsea :has() desde la versión 1.2.0 (2022), y yo probé la 1.4.0, así que aquí no hablamos de "no soportado", sino de "soportado pero evaluado mal en la combinación". Ese comportamiento silenciosamente incorrecto en este compuesto concreto no aparece en el tracker de cssselect, que registra las limitaciones de :has() como errores lanzados. Lexbor también maneja el atributo con indicador de mayúsculas/minúsculas [data-role="LEAD" i], que cssselect rechaza por completo.
Aun así, hay dos vacíos que sí determinan migraciones. selectolax no soporta XPath — ninguno de los dos backends expone xpath() — y tampoco tiene pseudo-elementos ::text / ::attr(), porque eso es una extensión de parsel/Scrapy, no CSS real. Si tus scrapers actuales dependen de XPath, ese es el muro más grande que vas a encontrar; no estarías cambiando de biblioteca, estarías reescribiendo selectores. Por otro lado, Lexbor trae :lexbor-contains("text" i), un pseudo-clase para coincidencia de texto sin distinguir mayúsculas/minúsculas que ni lxml, ni parsel, ni CSS estándar ofrecen, y funciona tal como lo documentan.
Robustez con HTML sucio, que es donde selectolax se gana el sueldo
En scraping real le das basura a un parser y esperas que no se derrumbe. Probé 18 entradas adversariales, y esta es la categoría donde el caso de selectolax frente a lxml es más fuerte.
Si le das a lxml.html.fromstring una cadena vacía o solo espacios, lanza ParserError("Document is empty"). Ambos motores de selectolax devuelven un árbol vacío válido en su lugar. Para un scraper que recorre una lista de URLs y algunas respuestas vuelven en blanco, eso significa un try/except menos envolviendo todo. selectolax también procesó 100,000 elementos sin desbordar la pila.
El anidamiento profundo fue el punto de mayor contraste. Con 1,000 y 5,000 niveles de <div> anidados, lxml descarta silenciosamente el contenido más profundo mientras selectolax lo conserva. libxml2 limita la profundidad de parseo a unas 256 capas y truncará el árbol sin avisar, así que el texto más profundo simplemente queda inaccesible. Ambos motores de selectolax devuelven el árbol completo. Es la imagen opuesta del problema con <template> que verás enseguida: allí Lexbor pierde contenido que los demás conservan; aquí lxml pierde contenido que selectolax conserva.
No todo fue victoria. El backend Modest aborta por completo el intérprete de Python con un SIGABRT cuando encuentra :dir() — no es una excepción que puedas capturar, es un cierre brutal del proceso. Eso es una advertencia real de robustez para cualquiera que siga usando el backend heredado, y precisamente el tipo de cosa que pasa inadvertida hasta que tumba un trabajo de producción a las 3 a.m.
Dos trampas de pérdida silenciosa de datos que debes conocer antes de lanzar a producción
Ninguna de estas es un hallazgo nuevo — ambas están documentadas aguas arriba — pero las dos cuestan datos reales, sin avisar, y ninguna destaca mucho en el README.
Lexbor omite los <a> dentro de <template>
En la página de MDN en vivo que probé, selectolax-Lexbor encontró 497 enlaces mientras que lxml, ambos backends de BeautifulSoup e incluso el propio backend Modest de selectolax encontraron 508. Los once enlaces que faltaban eran un selector de idioma y un enlace de discusiones dentro de elementos <template> (la página usa componentes web de Lit).

La causa es legítima: según la especificación HTML5, el contenido de <template> se analiza en un fragmento separado e inerte, no en el DOM normal, y Lexbor sigue eso al pie de la letra — tree.css("a") no desciende al contenido del template. lxml, ambos backends de BeautifulSoup y Modest aplanan ese contenido dentro del árbol principal, por eso encuentran esos enlaces. Se trata de un problema abierto documentado (selectolax#146, con la causa en el motor en lexbor#170), y ambas lecturas son defendibles — Lexbor es, probablemente, la más fiel a la especificación. Pero un desarrollador que use el backend recomendado puede perder esos datos en silencio, sin ningún error. También vale decir lo contrario: los otros parsers te muestran contenido inerte de template que un navegador nunca renderiza, así que pueden darte datos fantasma que un usuario no vería. La salida segura para esa página en particular es el backend Modest, o directamente otra librería.
Los bytes no UTF-8 corrompen .text() sin avisar
Si le pasas a selectolax bytes que no son UTF-8 válido, el parseo se completa y la corrupción aparece más tarde, peor que un fallo claro. Con "<p>café éè</p>".encode("latin-1"), .text() en Lexbor devuelve caracteres de reemplazo, .text() en Modest descarta silenciosamente los bytes problemáticos, y ambos motores solo lanzan UnicodeDecodeError cuando accedes a .html. El binding decodifica con UTF-8 estricto al leer de vuelta, no al parsear. Esto está relacionado con un problema conocido de selectolax sobre la strictness de codificación y decodificación.
La solución es una línea y conviene memorizarla: decodifica tú mismo los bytes antes de pasarlos — LexborHTMLParser(resp.content.decode("latin-1")) — y ambos motores devolverán 'café éè' correctamente. En la práctica, pásale siempre a selectolax un str, nunca bytes no UTF-8 en bruto. El README no lo deja claro.
Dimensiones de producción (una sola observación, así que tómalo como señal)
Los siguientes resultados los medí una sola vez, no en tres corridas, así que los marco como señales y no como números cerrados.
El escalado con hilos es el dato más interesante. Al parsear una página de 1 MB 48 veces repartidas en cuatro hilos, selectolax mostró una aceleración de ~3.5-3.9x en tiempo real — la firma empírica de una biblioteca que libera el GIL durante el parseo en C — mientras que BeautifulSoup(lxml) se volvió varias veces más lento en hilos, la firma de trabajo serializado por el GIL. lxml quedó en un punto intermedio y no concluyente. Para la era de free-threading hacia la que se dirige Python, que selectolax paralelice el parseo entre hilos cuando BeautifulSoup no lo hace es una ventaja real, aunque provisional. Es un solo conteo de hilos en un solo tamaño de página, y el mecanismo es una hipótesis, no algo que confirmara instrumentando el código C.
Sobre fugas: tras 2,000 iteraciones de parsear-extraer-soltar en 1 MB, ninguno de los tres parsers mostró el ascenso lineal de RSS típico de una fuga — cada uno se estabilizó en una banda acotada de memoria. Confío en ese resultado precisamente porque pasé por el mismo instrumento un caso de calibración con fuga conocida, y subió hasta +198 MB como se esperaba, lo que demuestra que el instrumento sí podía detectar una fuga y simplemente no encontró ninguna en los parsers. Y un manejador de nodo mantenido vivo después de que su árbol dueño saliera de alcance siguió siendo usable, sin segfault. Todo eso es una sola observación; nada de esto fue un soak de varias horas.
Dónde encaja selectolax y dónde deja paso a otra capa
Todo lo anterior trata de una sola tarea: convertir HTML que ya tienes en datos estructurados, rápido. selectolax es muy bueno en eso. Lo que deliberadamente no hace es descargar la página, renderizar JavaScript, rotar proxies, resolver CAPTCHAs o decidir qué elementos quieres. Todo eso sigue siendo tu código. selectolax es la capa de parseo y no pretende ser más que eso.
Ahí es donde un servicio administrado de extracción se sitúa por encima de un parser en vez de reemplazarlo. Si prefieres no construir y mantener tú mismo toda la pila de descarga, renderizado, anti-bot y extracción, Thunderbit lo expone como API, servidor MCP y CLI — POST /distill convierte una página en Markdown limpio y POST /extract devuelve JSON estructurado que encaja con un esquema, con renderizado JS y anti-bot resueltos por ti. Es otra capa del problema: usarías selectolax cuando ya tienes el HTML y quieres velocidad de parseo bajo tu control, y algo como la API, el servidor MCP o la CLI de Thunderbit cuando quieres que se encarguen de la descarga y la extracción y solo te devuelvan datos estructurados. No es un reemplazo: es otro nivel en la misma pila.
Prueba Thunderbit para extraer datos web
Pros, contras y quién debería usarlo de verdad
Dónde gana selectolax:
- Entre 12 y 17x más rápido que BeautifulSoup en tareas realistas de parseo y extracción, estable a lo largo de tres órdenes de magnitud en tamaño de página.
- Memoria contenida (el nivel de lxml, ~1.5-1.8x más liviano que BeautifulSoup) y una importación de ~14 ms.
- Se comporta bien con entradas que rompen lxml: vacías, solo espacios y anidamiento patológicamente profundo.
- CSS moderno, incluyendo
:has(),:is(),:where(), atributos sin distinguir mayúsculas/minúsculas y el:lexbor-contains()exclusivo de Lexbor. - Un DOM seguro ante
None: los elementos faltantes devuelvenNoneo[]en lugar de lanzar errores, y además puedes modificar y reserializar el árbol. - Mantenimiento activo (v0.4.10, mediados de 2026) e instalación trivial.
Dónde no gana:
- No es más rápido que lxml de forma amplia: empata en la tarea completa y pierde la etapa de parseo puro en mi prueba.
- Sin XPath y sin
::text/::attr()— una barrera fuerte de migración para scrapers basados en XPath. - Dos trampas de pérdida silenciosa de datos: contenido dentro de
<template>en Lexbor y bytes no UTF-8 a través de.text(). - El backend Modest es heredado y puede hacer SIGABRT con
:dir(). - Todos los números aquí provienen de una sola plataforma (macOS arm64, Python 3.14) y son provisionales.
¿Deberías usar selectolax? Sí, si quieres velocidad de parseo del nivel de lxml con una API más amigable y segura frente a None, y un comportamiento mucho mejor ante HTML vacío o mal formado, siempre que te sientas cómodo trabajando solo con CSS. Si tu base de código depende de XPath, el coste de reescritura es real y deberías valorarlo con honestidad. Y si estás persiguiendo "el parser único más rápido", la respuesta correcta de esta prueba es que selectolax y lxml están tan cerca que el desempate está en ergonomía y robustez, no en velocidad bruta. De hecho, esa es una mejor razón para elegir una herramienta.
Prueba Thunderbit para extraer datos web Get Started Free
Preguntas frecuentes
¿selectolax es más rápido que BeautifulSoup?
Sí, claramente: aproximadamente 12-17x más rápido que BeautifulSoup(html.parser) y 10-14x más rápido que BeautifulSoup(lxml) en una tarea real de parseo y extracción, manteniéndose estable desde páginas de 1 KB hasta 10 MB (macOS arm64, Python 3.14). La cifra que suele citarse de "4-5x" subestima la diferencia frente a html.parser.
¿selectolax es más rápido que lxml? No de forma general. En la tarea completa de parseo y extracción empatan en 100 KB y 1 MB, y selectolax solo gana en la página de 10 MB. En parseo puro, sin consulta, lxml fue en realidad ~33-34% más rápido en mi máquina — un resultado que va contra el consenso y que dejo acotado como específico de una sola plataforma, así que conviene verificarlo en tu propio hardware.
¿Debería usar el backend Lexbor o Modest?
Lexbor, casi siempre: es el motor mantenido y completo que recomienda el README, con mejor cobertura CSS. La excepción es una página que esconda contenido dentro de <template>, donde el comportamiento fiel a la especificación de Lexbor omite ese contenido y Modest, por casualidad, lo conserva. Modest también tiene bordes muy duros, incluido el cierre brutal del intérprete con :dir().
¿selectolax soporta XPath?
No. Ninguno de los dos backends expone un método xpath(); selectolax es solo CSS. Si tus scrapers dependen de XPath, migrar implica reescribir selectores, que es el mayor coste de pasar desde una pila basada en lxml o parsel.
¿Por qué mi salida de selectolax sale corrupta o faltan elementos?
Suelen ser dos causas. Si el texto vuelve con caracteres de reemplazo o acentos perdidos, probablemente pasaste bytes no UTF-8 en bruto: decodifícalos primero a str (resp.content.decode("latin-1")) antes de parsear. Si faltan enlaces o elementos en un sitio moderno, puede que estén dentro de etiquetas <template> a las que el backend Lexbor no desciende; usa Modest u otro parser para esa página.


