Senza KYC — niente documento, niente selfie, niente documenti. Paga in criptovalute: BTC, USDT, USDC, ETH, SOL, LTC
Lingua: Italiano
Ottieni una chiave API

Docs · Python

Web page to Markdown in Python

Quattro ricette che coprono la maggior parte dei progetti Python: leggi una pagina con requests, leggi molte pagine in parallelo con httpx, passa una lista a un job batch e dividi il Markdown in chunk per un vector store.

Ultimo aggiornamento: · 5 min di lettura

Risposta rapida

Come converto una pagina web in Markdown in Python?

Chiama WebReaderAPI con requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...}) e la tua chiave nell'header Authorization; la risposta JSON contiene il contenuto principale della pagina in markdown. Per molte pagine, usa httpx.AsyncClient con un semaforo o un job batch. Una pagina costa 1 credito, da 0,07 USD/1K pagine a ottobre 2026. Senza KYC; paghi in criptovalute.

Cosa devo installare?

Python 3.9 o successivo e un client HTTP. requests basta per gli script che leggono una pagina alla volta; httpx aggiunge il supporto async per molte pagine in parallelo. Non serve nessun nostro pacchetto: l'API è semplice HTTPS e JSON.

Tieni la chiave fuori dal codice. Leggila dalla variabile d'ambiente WEBREADER_API_KEY, come fa ogni esempio qui sotto, e dai a ogni progetto la sua chiave, così puoi revocarne una senza toccare le altre.

Installazione
pip install requests httpx
export WEBREADER_API_KEY="your-key"

Come leggo una pagina con requests?

Invia url e le opzioni come parametri di query, solleva un'eccezione in caso di errore HTTP e leggi i campi che ti servono. L'esempio mantiene il contenuto principale, lo limita a 20.000 caratteri per un modello e stampa quanto è costata la chiamata.

requests: una pagina
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])

Come leggo molte pagine contemporaneamente con httpx?

Usa un solo httpx.AsyncClient e un asyncio.Semaphore impostato pari o sotto il tuo limite di concorrenza. Ogni account ha un limite di pagine lette contemporaneamente; la tua dashboard lo mostra e il supporto può aumentarlo. Gli errori di una pagina non devono fermare le altre, quindi l'esempio li raccoglie accanto ai risultati invece di sollevare un'eccezione.

Se superi il limite, l'API risponde 429 concurrency_limit; non viene addebitato nulla, ma la pagina non viene letta. Tenere il semaforo sotto il limite è più semplice che riprovare.

httpx: fino a 10 pagine in volo
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"])

Quando dovrei usare invece un batch job?

Quando hai una lista di fino a 100 URL e non ti serve ogni pagina nel momento in cui è pronta. Un batch job gira dalla nostra parte, quindi il tuo script non tiene connessioni aperte e non gestisce logiche di retry. Avvialo con POST https://webreaderapi.com/api/v1/batch, poi raccogli i risultati del job con il ciclo a cursore mostrato sulla pagina batch e crawl.

Per un intero sito, avvia invece un crawl e lascia che segua la sitemap e i link; la stessa pagina ha un ciclo completo che scrive un file .md per pagina.

Avviare un job batch
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"])

Come divido il Markdown in chunk per il RAG?

Prima dividi per heading, poi per dimensione. Gli heading segnano dove cambiano gli argomenti, quindi un chunk che inizia con un heading è più facile da far combaciare per un retriever. Mantieni il titolo della pagina e il percorso degli heading nei metadata di ogni chunk: aiutano il modello a citare la sezione giusta.

La funzione qui sotto non richiede librerie aggiuntive. Taglia sugli heading da # a ###, poi divide qualsiasi sezione lunga sulle righe vuote per restare sotto un limite di caratteri. Memorizza final_url con ogni chunk, così due indirizzi che portano alla stessa pagina non creano duplicati.

Dividi il Markdown per heading
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

Quanto costa un job Python?

Conta le pagine, non le richieste. 1 credito = 1 pagina letta senza browser; 2 crediti quando la pagina richiede un browser per eseguire JavaScript. Una richiesta che fallisce, o che il robots.txt di un sito rifiuta, non viene addebitata. Leggere 2.000 pagine di documentazione senza browser usa circa 2.000 crediti, che 10.000 pagine (3,75 USD) copre cinque volte. I crediti prepagati non scadono mai: le pagine non utilizzate restano sul tuo account finché non le usi, quindi una pipeline che gira una volta al mese usa solo ciò che legge.

Python: domande frequenti

Esiste un pacchetto Python ufficiale?

No. Il codice su questa pagina è tutto ciò che ti serve: WebReaderAPI è una semplice API HTTPS, e requests o httpx la parlano direttamente.

Come gestisco una pagina che robots.txt rifiuta?

Controlla il codice di errore. blocked_by_robots e target_refused sono definitivi e gratuiti; registrali e continua. La pagina degli errori ha un helper di retry che ritenta solo i codici che possono cambiare.

Posso usare l'API da un notebook Jupyter?

Sì. La ricetta requests funziona così com'è. In un notebook, await read_many(urls) sostituisce asyncio.run, perché il notebook ha già un event loop in esecuzione.

Quale timeout dovrebbe usare il mio client?

Un po' più di quello dell'API: il default è 30 secondi e il massimo 60, quindi 75 secondi lato client lasciano spazio a un sito lento e alla risposta.

Da 0,07 USD/1K pagine · senza KYC

Ottieni una chiave API