# Página web a Markdown en Python

> Convierte páginas web a Markdown en Python con solicitudes concurrentes y fragmentación por encabezados para RAG. Desde 0,07 US$/1K páginas, sin KYC.

- URL: https://webreaderapi.com/es/docs/python/
- Última actualización: 2026-10-22
- Editor: WebReaderAPI (https://webreaderapi.com)

Cuatro recetas que cubren la mayoría de los proyectos en Python: leer una página con requests, leer muchas páginas en paralelo con httpx, pasar una lista a un trabajo por lotes y dividir el Markdown en fragmentos para un almacén vectorial.

**Respuesta rápida — ¿Cómo convierto una página web a Markdown en Python?**

Llama a **WebReaderAPI** con `requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...})` y tu clave en el header `Authorization`; la respuesta JSON contiene el contenido principal de la página en `markdown`. Para muchas páginas, usa `httpx.AsyncClient` con un semáforo o un trabajo por lotes. Una página cuesta 1 crédito, desde 0,07 US$/1K páginas a fecha de octubre de 2026. Sin KYC; paga con criptomonedas.

## ¿Qué necesito instalar?

Python 3.9 o posterior y un cliente HTTP. `requests` basta para scripts que leen una página a la vez; `httpx` añade soporte asíncrono para muchas páginas en paralelo. No hace falta ningún paquete nuestro: la API es HTTPS y JSON sin más.

Mantén tu clave fuera del código. Léela desde la variable de entorno `WEBREADER_API_KEY`, como hacen todos los ejemplos de abajo, y da a cada proyecto su propia clave para poder revocar una sin tocar las demás.

*Instalación*

```bash
pip install requests httpx
export WEBREADER_API_KEY="your-key"
```

## ¿Cómo leo una página con requests?

Envía `url` y las opciones que quieras como parámetros de consulta, lanza una excepción ante un error HTTP y lee los campos que necesites. El ejemplo conserva el contenido principal, lo limita a 20.000 caracteres para un modelo e imprime lo que costó la llamada.

*requests: una página*

```python
import os

import requests

API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}


def read(url, **options):
    resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
    if resp.status_code != 200:
        error = resp.json()["error"]
        raise RuntimeError(error["code"] + ": " + error["message"])
    return resp.json()


page = read("https://example.com/docs/getting-started", max_chars=20000)
print(page["title"], page["word_count"], "words", page["credits_used"], "credit(s)")
print(page["markdown"][:500])

```

## ¿Cómo leo muchas páginas a la vez con httpx?

Usa un `httpx.AsyncClient` y un `asyncio.Semaphore` fijado en tu límite de concurrencia o por debajo. Cada cuenta tiene un límite de cuántas páginas lee a la vez; tu panel lo muestra y el soporte puede ampliarlo. Los errores de una página no deberían detener a las demás, así que el ejemplo los recoge junto a los resultados en lugar de lanzar una excepción.

Si superas el límite, la API responde `429 concurrency_limit`; no se cobra nada, pero la página no se lee. Mantener el semáforo por debajo del límite es más sencillo que reintentar.

*httpx: hasta 10 páginas en vuelo*

```python
import asyncio
import os

import httpx

API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}


async def read_one(client, sem, url):
    async with sem:
        resp = await client.get(API, params={"url": url, "max_chars": 50000})
    body = resp.json()
    if resp.status_code == 200:
        return url, body, None
    return url, None, body["error"]["code"]


async def read_many(urls, limit=10):
    sem = asyncio.Semaphore(limit)
    async with httpx.AsyncClient(headers=HEADERS, timeout=75) as client:
        return await asyncio.gather(*(read_one(client, sem, u) for u in urls))


if __name__ == "__main__":
    urls = [f"https://example.com/blog/post-{n}" for n in range(1, 21)]
    for url, page, error in asyncio.run(read_many(urls)):
        print(url, "->", error or page["title"])

```

## ¿Cuándo debería usar mejor un trabajo por lotes?

Cuando tienes una lista de hasta 100 URL y no necesitas cada página en el momento en que está lista. Un trabajo por lotes se ejecuta en nuestro lado, así que tu script no mantiene conexiones abiertas ni implementa lógica de reintentos. Inícialo con `POST https://webreaderapi.com/api/v1/batch` y luego recoge los resultados del trabajo con el bucle de cursor que se muestra en la [página de lotes y rastreo](https://webreaderapi.com/es/docs/crawl/).

Para un sitio completo, inicia mejor un rastreo y deja que siga el sitemap y los enlaces; la misma página tiene un bucle completo que escribe un archivo `.md` por página.

*Iniciar un trabajo de batch*

```python
import os

import requests

urls = [f"https://example.com/changelog/{year}" for year in range(2020, 2027)]
resp = requests.post(
    "https://webreaderapi.com/api/v1/batch",
    headers={"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]},
    json={"urls": urls, "options": {"format": "markdown", "main_content": True}},
    timeout=30,
)
resp.raise_for_status()
print("job:", resp.json()["job_id"])

```

## ¿Cómo divido el Markdown en fragmentos para RAG?

Divide primero por encabezados y luego por tamaño. Los encabezados marcan dónde cambian los temas, así que un fragmento que empieza en un encabezado es más fácil de emparejar para un retriever. Conserva el título de la página y la ruta de encabezados en los metadatos de cada fragmento: ayudan al modelo a citar la sección correcta.

La función de abajo no necesita ninguna librería extra. Corta en los encabezados de `#` a `###` y luego divide cualquier sección larga por líneas en blanco para mantenerse por debajo de un presupuesto de caracteres. Guarda `final_url` con cada fragmento, para que dos direcciones que llevan a la misma página no creen duplicados.

*Fragmentar Markdown por encabezados*

```python
import re


def chunk_markdown(page, max_chars=2000):
    """Split a /read response into chunks with a heading path and the source URL."""
    chunks, path, buf = [], [], []

    def flush():
        text = "\n".join(buf).strip()
        while text:
            part = text[:max_chars]
            cut = part.rfind("\n\n")
            if len(text) > max_chars and cut > max_chars // 2:
                part = part[:cut]
            chunks.append({"text": part.strip(), "section": " > ".join(path), "title": page.get("title"), "source": page["final_url"]})
            text = text[len(part):].strip()
        buf.clear()

    for line in page["markdown"].splitlines():
        m = re.match(r"^(#{1,3}) (.+)", line)
        if m:
            flush()
            level = len(m.group(1))
            path[:] = path[: level - 1] + [m.group(2).strip()]
        buf.append(line)
    flush()
    return chunks

```

## ¿Cuánto cuesta un trabajo de Python?

Cuenta páginas, no peticiones. 1 crédito = 1 página leída sin navegador; 2 créditos cuando la página necesita un navegador para renderizar JavaScript. Una solicitud que falla, o que el robots.txt de un sitio rechaza, no se cobra. Leer 2,000 páginas de documentación sin navegador consume unos 2,000 créditos, que 10.000 páginas (3,75 US$) cubre cinco veces. Los créditos prepago nunca caducan: las páginas sin usar permanecen en tu cuenta hasta que las uses, así que un pipeline que se ejecuta una vez al mes usa solo lo que lee.

## Python: preguntas frecuentes

### ¿Existe un paquete oficial de Python?

No. El código de esta página es todo lo que necesitas: WebReaderAPI es una API HTTPS simple, y `requests` o `httpx` se comunican con ella directamente.

### ¿Cómo manejo una página que robots.txt rechaza?

Revisa el código de error. `blocked_by_robots` y `target_refused` son definitivos y gratuitos; regístralos y continúa. La [página de errores](https://webreaderapi.com/es/docs/errors/) tiene un helper de reintentos que solo reintenta los códigos que pueden cambiar.

### ¿Puedo usar la API desde un Jupyter notebook?

Sí. La receta de `requests` funciona tal cual. En un notebook, `await read_many(urls)` reemplaza a `asyncio.run`, porque el notebook ya ejecuta un event loop.

### ¿Qué timeout debería usar mi cliente?

Un poco más que el de la propia API: el predeterminado es de 30 segundos y el máximo de 60, así que 75 segundos en el lado del cliente dejan margen para un sitio lento y la respuesta.

---

WebReaderAPI: WebReaderAPI es una API de lectura web para agentes de IA que convierte cualquier página web pública en Markdown limpio, pagando con criptomonedas y sin subir ningún documento de identidad. Los créditos prepago desde 0,07 US$/1K páginas nunca caducan y se respeta robots.txt. Sin KYC: ni documento de identidad, ni selfie, ni documentos. Paga con criptomonedas.
