Raspado Web con Parsel: Selectores CSS y XPath en Python
Senior Web Scraping Engineer
Resumen:
- parsel selecciona datos de HTML usando tanto CSS como XPath, sobre el mismo documento — es el motor de selección que Scrapy utiliza, disponible como una biblioteca independiente.
- Lo que parsel no hace es obtener. No tiene cliente HTTP y no ejecuta JavaScript; le entregas el marcado y construye un árbol consultable.
- La diferencia se muestra en un script. Un GET simple en una página de demostración renderizada por JavaScript le da a parsel 0 nodos de cita; la misma URL obtenida a través de la API de Scraping Universal de Scrapeless con
js_renderle da todas las 10. - CSS y XPath, lado a lado. Una ejecución en vivo obtuvo los mismos 10 autores de dos maneras —
sel.css("small.author::text")ysel.xpath("//small[@class='author']/text()")— del HTML renderizado. - Las dos capas permanecen separadas. Scrapeless obtiene y renderiza; parsel selecciona. Ninguna de las dos se adentra en el trabajo de la otra.
- Libre para comenzar en el lado de la obtención. Crea tu clave API de Scrapeless en app.scrapeless.com.
Qué es parsel, y qué no es
parsel es una biblioteca de Python para extraer datos de HTML y XML usando selectores CSS y expresiones XPath. Es la capa de selección sobre la que Scrapy está construido, empaquetada para que puedas usarla en cualquier lugar, y envuelve lxml debajo, de modo que ambos lenguajes de selección se ejecutan contra el mismo árbol de análisis rápido. La razón para elegirla sobre un analizador solo de CSS es XPath: cuando un campo se define por su posición, su texto, o su relación con un hermano en lugar de una clase, XPath lo expresa y CSS no puede.
Es una biblioteca de selección y nada más. parsel no tiene cliente HTTP, no mantiene sesión y no ejecuta JavaScript. Dale una cadena y construye un Selector que puedes consultar; pídele que obtenga una URL y no hay método para eso. Así que cada configuración de "raspado web con parsel" es de dos capas: algo que devuelve HTML fiel, y parsel que selecciona de él. Esta guía utiliza la API de Scraping Universal de Scrapeless para la primera capa, porque un cliente HTTP simple devuelve el marcado pre-renderizado en cualquier página que construya su contenido con JavaScript. El más amplio tutorial de raspado web en Python cubre el ecosistema circundante.
Instalar
parsel y requests son toda la cadena de herramientas. La versión contra la que se escribió esta guía es parsel 1.11.0:
bash
pip install "parsel==1.11.0" requests
Mantén tu clave en el entorno, nunca en el origen:
bash
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
Obtén HTML que vale la pena analizar
La calidad de la selección está determinada por la fidelidad de la obtención, así que comienza allí. En una página renderizada por JavaScript, el marcado que recibe un simple cliente HTTP 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 parsel mismo:
python
# fidelity.py — lo que parsel ve: GET simple vs renderización del lado del servidor
import os
import requests
from parsel import Selector
URL = "https://quotes.toscrape.com/js/"
plain = requests.get(URL, timeout=60).text
print("caracteres GET simples:", len(plain), "| nodos de cita:", len(Selector(text=plain).css("div.quote")))
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:", len(Selector(text=rendered).css("div.quote")))
La ejecución imprime 0 nodos de cita para la obtención simple y 10 para la rendida:
text
caracteres GET simples: 5806 | nodos de cita: 0
caracteres renderizados: 8940 | nodos de cita: 10
La renderización, desbloqueo y enrutamiento de proxy ocurren todos del lado del servidor en ese POST — la API de Scraping Universal es la capa de obtención, y el marcado renderizado es de lo que parsel debería seleccionar.
Extraer con CSS y XPath
Con HTML real en mano, parsel realiza la extracción. css y xpath devuelven ambos un SelectorList; get devuelve la primera coincidencia y getall devuelve todas las coincidencias. El pseudo-elemento ::text y el nodo text() test tanto extraen texto, por lo que puedes leer el mismo campo de cualquier manera — CSS sigue la especificación de selectores W3C y XPath sigue la especificación de XPath W3C:
python
# extract.py — obtener la página renderizada, luego seleccionar con CSS y XPath
import os
import requests
from parsel import Selector
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": "https://quotes.toscrape.com/js/", "js_render": True}},
timeout=120,
)
resp.raise_for_status()
sel = Selector(text=resp.json().get("data", ""))
css_nodes = sel.css("div.quote")
xpath_authors = sel.xpath("//div[@class='quote']//small[@class='author']/text()").getall()
print("nodos css de citas:", len(css_nodes))
print("autores xpath:", len(xpath_authors))
records = []
for quote in css_nodes:
records.append({
"texto": quote.css("span.text::text").get(),
"autor": quote.xpath(".//small[@class='author']/text()").get(),
"etiquetas": quote.css("a.tag::text").getall(),
})
print("primer autor:", records[0]["autor"])
print("primeras etiquetas:", records[0]["etiquetas"])
La ejecución en vivo seleccionó los 10 registros, con CSS y XPath devolviendo los mismos autores:
```text
nodos css de citas: 10
autores xpath: 10
primer autor: Albert Einstein
primeras etiquetas: ['cambio', 'profundas-reflexiones', 'pensando', 'mundo']
Ese es el scraper completo: un POST para fetch y render, un Selector para consultar. Mezclando CSS para los campos fáciles y XPath para los posicionales — quote.xpath("...") se ejecuta en relación con cada nodo — es el patrón que mantiene un scraper legible a medida que las páginas se vuelven complicadas.
Obtén tu clave API en el plan gratuito: app.scrapeless.com
Patrones avanzados
- Recurrir a XPath cuando CSS se queda corto. Seleccionar por texto (
//a[contains(text(), "Siguiente")]), por posición ((//tr)[2]), o por un eje (following-sibling::td) es territorio de XPath; CSS no tiene equivalente. - Encadenar selectores en relación con un nodo.
quote.css(...)yquote.xpath(".//...")ambos se limitan a ese nodo — el.inicial en el XPath lo mantiene relativo, lo que es la diferencia entre "autores dentro de esta cita" y "todos los autores en la página". - Usar
get(default="")para evitarNone.sel.css("span.missing::text").get(default="")devuelve una cadena vacía en lugar deNone, lo que evita que un bucle sobre registros desiguales se rompa en una página extraña. - Extraer atributos con
::attr()o@.sel.css("a::attr(href)")ysel.xpath("//a/@href")ambos leen un atributo; usa el lenguaje que el resto del selector ya esté usando.
Solución de problemas
- Cero nodos de una página que puedes ver en un navegador. El contenido es renderizado por JavaScript y tu fetch devolvió el HTML pre-renderizado. Cuenta un selector conocido como lo hace el primer script; un árbol casi vacío es un problema de fetch, que se soluciona con
js_render, no un problema de selector. get()devuelveNone. El selector no coincidió con nada. Verifica el marcado renderizado, confirma la clase o la ruta y pasa undefaultpara que el código descendente no se caiga por la falta.- XPath devuelve elementos cuando querías texto. Agrega
/text()a la ruta o.get()en un selector CSS::text; una ruta de elemento desnudo devuelve el nodo, no su cadena. - XPath relativo agarra toda la página. Una ruta que comienza con
//es absoluta incluso cuando se llama a un nodo. Prefíjalo con.—.//small— para limitarlo al elemento actual.
Conclusión
parsel gana su lugar como la capa de selección que habla ambos dialectos: CSS para los casos comunes, XPath para aquellos que CSS no puede alcanzar, sobre un árbol respaldado por lxml. La capa que decide si alguno de eso es posible es el fetch — el conteo 0 frente a 10 del primer script lo establece — y un POST renderizado por el servidor cierra la brecha. Conecta los dos y las diez citas de la página de demostración llegan como registros limpios, seleccionados de la manera que mejor se lee.
Crea una cuenta gratuita en Scrapeless para obtener una clave API, y la documentación del desarrollador cubre los parámetros de unlocker.webunlocker. Verifica los precios de Scrapeless cuando planees un trabajo recurrente.
Preguntas frecuentes
P: ¿Puede parsel raspar sitios web por sí mismo?
No. parsel selecciona datos del HTML que ya tienes; no tiene cliente HTTP y no ejecuta JavaScript. Empáréjalo con una capa de fetch — aquí, la API de Raspado Universal de Scrapeless, que renderiza la página en el lado del servidor — y parsel maneja la extracción del marcado devuelto.
P: ¿Cuál es la diferencia entre parsel y los selectores de Scrapy?
Ninguno, en la práctica: parsel es el motor de selección que Scrapy utiliza, lanzado como una biblioteca independiente. Si has usado response.css o response.xpath en una araña de Scrapy, eso es parsel. Usarlo directamente te permite mantener la API de selección sin adoptar todo el marco.
P: ¿Debería usar CSS o XPath con parsel?
Ambos, según sea necesario. CSS es más corto para la selección basada en clases y etiquetas; XPath maneja la selección por texto, posición o eje, que CSS no puede expresar. Parsel ejecuta ambos sobre el mismo árbol, así que mezclalos por campo.
P: ¿Parsel maneja páginas renderizadas con JavaScript?
No por sí solo: nunca ejecuta scripts. Si el contenido se carga después del HTML inicial, una obtención sencilla le da a parsel un árbol vacío. Obtén la página a través de la API Scrapeless con js_render primero, luego selecciona del HTML renderizado, como lo hace esta guía.
P: ¿Es legal hacer scraping con parsel?
La biblioteca de selectores no cambia las reglas de recolección. Obtén solo páginas públicas, respeta los términos del sitio y las directrices de robots estandarizadas por el Protocolo de Exclusión de Robots, mantén los volúmenes limitados y maneja cualquier dato personal bajo las leyes que te sean aplicables.
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.



