Volver al blog

Raspado web con Polars: De página en vivo a DataFrame rápido

James Thompson
James Thompson

Scraping and Proxy Management Expert

22-Jul-2026

Resumen:

  • Polars es una biblioteca de DataFrame rápida y respaldada por Arrow, y se combina con Scrapeless para convertir una página en vivo en un marco tipado y consultable.
  • Polars no tiene cliente HTTP ni analizador HTML, por lo que los pasos de obtención y extracción provienen de Scrapeless y un analizador; Polars toma el control una vez que existen los registros.
  • Construye un marco a partir de una lista de diccionarios con pl.DataFrame(records), y el esquema se tipa a partir de la primera fila cuando coercitas los valores durante la extracción.
  • La API de expresiones reemplaza la indexación encadenada estilo pandas: pl.col("price_gbp").mean() dentro de group_by(...).agg(...) se lee y se ejecuta de manera clara.
  • El camino perezoso, df.lazy()...collect(), permite que Polars planifique y optimice toda la consulta antes de que toque los datos, que es donde se adelanta en conjuntos más grandes.
  • Comienza con el plan gratuito de Scrapeless y dirige el paso de obtención a tu propio catálogo.

Polars sigue apareciendo en trabajos de datos porque es rápido y su API es agradable, pero casi todos los tutoriales le entregan un archivo Parquet que ya existe. El web scraping es la situación opuesta. Los datos son en vivo, son HTML y no tienen tipo hasta que los tipas. Esta guía toma un catálogo público de libros de una URL a un DataFrame de Polars que puedes agrupar y agregar, y es honesta sobre la única cosa que Polars no hará por ti: obtener la página.

La obtención proviene de la API Universal de Scraping de Scrapeless, que devuelve HTML renderizado para una URL pública. Un analizador convierte ese HTML en registros. Polars hace el resto, y lo hace rápido.

Lo que Polars añade a un flujo de trabajo de scraping

Polars es una biblioteca de DataFrame construida sobre el formato de memoria columnar Apache Arrow, que es lo que hace que sus columnas tipadas y agregaciones agrupadas sean rápidas. Para los datos raspados, el beneficio es concreto: una vez que tus registros están en un marco, limpiarlos y resumirlos es cuestión de unas pocas expresiones en lugar de un bucle escrito a mano, y las columnas mantienen sus tipos. Polars se basa en el formato columnar de Apache Arrow para su distribución en memoria, por lo que una columna de precios es una columna numérica real, no una lista de cadenas que vuelves a analizar en cada operación.

Instalación

Polars maneja el marco; un analizador maneja el HTML. Instala ambos.

bash Copy
pip install polars lxml

Configura tu clave API de Scrapeless una vez en la terminal. Usa la clave real en tiempo de ejecución y mantén el marcador fuera de tu fuente.

bash Copy
export SCRAPELESS_API_KEY="sk_tu_clave_aqui"

Obtener, Extraer y Construir un Marco

La primera etapa obtiene la página, extrae un registro por producto y construye el DataFrame. Dado que Polars no puede obtener o analizar HTML, esta etapa es donde Scrapeless y el analizador hacen su trabajo; Polars toma los registros terminados.

python Copy
import json
import os
import urllib.request

import polars as pl
from lxml import html as lxhtml

API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"Uno": 1, "Dos": 2, "Tres": 3, "Cuatro": 4, "Cinco": 5}


def fetch_html(url: str) -> str:
    payload = json.dumps(
        {"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
    ).encode()
    request = urllib.request.Request(
        API_URL,
        data=payload,
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=90) as response:
        return json.loads(response.read())["data"]


dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
    records.append(
        {
            "title": card.cssselect("h3 a")[0].get("title"),
            "price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
            "rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
            "in_stock": "En stock" in card.cssselect("p.instock.availability")[0].text_content(),
        }
    )

df = pl.DataFrame(records)
print(f"registros extraídos: {df.height}")
print("esquema:", dict(df.schema))

La extracción utiliza el estándar de Selectores CSS a través de cssselect de lxml, y coerciona cada valor mientras lo lee: el precio se convierte en un float, la palabra de calificación se convierte en un entero y la disponibilidad se convierte en un booleano. Esa coerción es el hábito importante. Construye los registros con tipos de Python reales y Polars infiere un esquema real a partir de ellos.

text Copy
registros extraídos: 20
esquema: {'title': String, 'price_gbp': Float64, 'rating': Int64, 'in_stock': Boolean}

Veinte tarjetas de producto en la primera página se convierten en un marco con un esquema tipado: String, Float64, Int64 y Boolean. Ninguna columna queda como texto que luego tenga que volver a analizarse.

Transformar con Expresiones

Polars reemplaza la indexación encadenada con una API de expresiones, y esta es la parte que vale la pena aprender bien. Una expresión como pl.col("price_gbp").mean() describe un cálculo que Polars ejecuta de manera eficiente, y las expresiones se componen dentro de filter, group_by y agg.

python Copy
summary = (
    df.lazy()
    .filter(pl.col("in_stock"))
    .group_by("rating")
    .agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
    .sort("rating")
    .collect()
)
print(summary)

df.lazy() convierte el marco ansioso en un plan de consulta, la cadena describe el trabajo, y .collect() lo ejecuta. En este pequeño conjunto, la diferencia es invisible, pero el camino perezoso permite a Polars analizar toda la consulta y omitir trabajo antes de leer un solo valor, que es la razón por la cual se escala. Los grupos de agregados agrupan los libros en stock por calificación estelar y reportan el conteo y el precio medio por grupo.

text Copy
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ ---    ┆ ---   ┆ ---       │
│ i64    ┆ u32   ┆ f64       │
╞════════╪═══════╪═══════════╡
│ 1      ┆ 6     ┆ 40.02     │
│ 2      ┆ 3     ┆ 36.83     │
│ 3      ┆ 3     ┆ 42.32     │
│ 4      ┆ 4     ┆ 31.1      │
│ 5      ┆ 4     ┆ 39.75     │
└────────┴───────┴───────────┘

El Script Completo

Junta las etapas, agrega una expresión más para encontrar el libro de cinco estrellas más barato y almacena el marco como Parquet.

python Copy
import json
import os
import urllib.request

import polars as pl
from lxml import html as lxhtml

API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
RATINGS = {"Uno": 1, "Dos": 2, "Tres": 3, "Cuatro": 4, "Cinco": 5}


def fetch_html(url: str) -> str:
    payload = json.dumps(
        {"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
    ).encode()
    request = urllib.request.Request(
        API_URL,
        data=payload,
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=90) as response:
        return json.loads(response.read())["data"]


dom = lxhtml.fromstring(fetch_html("https://books.toscrape.com/"))
records = []
for card in dom.cssselect("article.product_pod"):
    records.append(
        {
            "title": card.cssselect("h3 a")[0].get("title"),
            "price_gbp": float(card.cssselect("p.price_color")[0].text_content().strip().lstrip("£")),
            "rating": RATINGS[card.cssselect("p.star-rating")[0].get("class").split()[-1]],
            "in_stock": "En stock" in card.cssselect("p.instock.availability")[0].text_content(),
        }
    )

df = pl.DataFrame(records)
print(f"registros extraídos: {df.height} | columnas: {df.columns}")

summary = (
    df.lazy()
    .filter(pl.col("in_stock"))
    .group_by("rating")
    .agg(pl.len().alias("books"), pl.col("price_gbp").mean().round(2).alias("avg_price"))
    .sort("rating")
    .collect()
)
print(summary)

cheapest = df.filter(pl.col("rating") == 5).sort("price_gbp").select("title", "price_gbp").head(1)
print("más barato de 5 estrellas:", cheapest.to_dicts())

df.write_parquet("books.parquet")
print(f"bytes de parquet: {os.path.getsize('books.parquet')}")

La ejecución informa cada paso, terminando con un archivo Parquet que mantiene el esquema para el siguiente lector.

text Copy
registros extraídos: 20 | columnas: ['title', 'price_gbp', 'rating', 'in_stock']
shape: (5, 3)
┌────────┬───────┬───────────┐
│ rating ┆ books ┆ avg_price │
│ ---    ┆ ---   ┆ ---       │
│ i64    ┆ u32   ┆ f64       │
╞════════╪═══════╪═══════════╡
│ 1      ┆ 6     ┆ 40.02     │
│ 2      ┆ 3     ┆ 36.83     │
│ 3      ┆ 3     ┆ 42.32     │
│ 4      ┆ 4     ┆ 31.1      │
│ 5      ┆ 4     ┆ 39.75     │
└────────┴───────┴───────────┘
más barato de 5 estrellas: [{'title': 'Set Me Free', 'price_gbp': 17.46}]
bytes de parquet: 2233

write_parquet almacena las columnas tipadas en el formato de archivo Apache Parquet, por lo que el siguiente proceso lee price_gbp nuevamente como un flotante sin re-coerción. La guía del usuario de Polars documenta la API completa de expresiones y perezosa cuando superas este ejemplo.

Lo Que Polars No Hará

Polars es un motor de dataframe, y ese es todo el límite: no tiene cliente HTTP ni parser HTML. No solicitará una URL, no renderizará JavaScript y no convertirá las etiquetas <article> en filas. Eso es intencional, y es por eso que este flujo de trabajo utiliza dos herramientas por delante. Scrapeless recupera la página, y un parser lee el marcado en registros. Si necesitas un tratamiento más profundo del paso de parsing, la guía sobre usar BeautifulSoup para web scraping cubre la extracción de HTML en Python, y los mismos registros se alimentan directamente en pl.DataFrame.

Cuando un objetivo renderiza su contenido con JavaScript, la primera solicitud devuelve un contenedor vacío y el bucle de extracción no encuentra tarjetas. Establece js_render en True en la solicitud, para que Scrapeless devuelva la página después de que se ejecute su script, y déjalo en False cuando el marcado ya esté renderizado por el servidor, como es este catálogo.

Antes de ampliar el rastreo más allá de la primera página, lee el robots.txt y los términos del objetivo. El Protocolo de Exclusión de Robots indica qué rutas pide un sitio a los clientes automatizados que eviten, y mantenerse dentro de él mantiene el flujo de trabajo sostenible. Mantén el volumen acotado y los datos públicos, como lo hace este ejemplo.

¿Listo para intentar con tus propios datos? Crea una cuenta gratuita en Scrapeless y cambia la URL y los selectores en la etapa de obtención.

Conclusión

Polars convierte los registros extraídos en un marco tipado y consultable con muy poco código, siempre que algo más obtenga la página primero. Scrapeless obtiene el HTML, un parser construye los registros con tipos reales, y Polars agrupa, agrega y los almacena a través de sus APIs de expresión y perezosa. Comienza desde el script completo, intercambia la URL y los selectores por tu propio objetivo, y busca el camino perezoso a medida que tus datos crecen.

Comienza con el plan gratuito de Scrapeless para obtener tus propias páginas, y consulta los precios de Scrapeless cuando planees un trabajo recurrente.

FAQ

Q: ¿Polars extrae páginas web por sí solo?

No. Polars es una biblioteca de DataFrame sin cliente HTTP y sin parser HTML, por lo que no puede solicitar una URL ni leer el marcado. Combínalo con una capa de obtención como Scrapeless y un parser como lxml o BeautifulSoup; Polars se encarga una vez que los registros existen.

Q: ¿Por qué elegir Polars sobre pandas para datos extraídos?

Polars está construido sobre el formato columnar Apache Arrow y ofrece un motor de consulta perezoso, por lo que las agregaciones agrupadas y los filtros en grandes marcos son rápidos, y la API de expresión es consistente. Una compensación es relevante para la extracción: pandas tiene read_html para tablas, mientras que Polars espera que tú construyas el marco a partir de registros, lo que se adapta mejor a páginas de estilo tarjeta que a las solo de tablas.

Q: ¿Cuál es la diferencia entre ansioso y perezoso en Polars?

Un DataFrame ansioso ejecuta cada operación de inmediato, mientras que df.lazy() construye un plan de consulta que solo se ejecuta cuando llamas a .collect(). El camino perezoso permite a Polars optimizar toda la consulta y omitir trabajo innecesario, por lo que se escala mejor en conjuntos de datos grandes, aunque el resultado sea idéntico en los pequeños.

Q: ¿Por qué mis números extraídos aparecen como cadenas en Polars?

Los valores extraídos llegan como texto, y si los pasas a pl.DataFrame sin cambios, el tipo de columna es String. Coerciona cada valor durante la extracción, ya que el ejemplo convierte el precio a float y la palabra de calificación a un entero, de modo que el esquema esté tipado desde la primera fila y ninguna columna necesite reanálisis más tarde.

Q: ¿Cómo construir un DataFrame de Polars a partir de una lista de registros extraídos?

Recoge cada registro como un diccionario con claves consistentes y pasa la lista a pl.DataFrame(registros). Polars infiere el esquema de los valores, por lo que los valores tipados en los diccionarios producen un marco tipado, listo para la API de expresión.

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.

Artículos más populares

Catalogar