Resposta rápida
Como converto uma página web em Markdown em Python?
Chame WebReaderAPI com requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...}) e sua chave no header Authorization; a resposta JSON traz o conteúdo principal da página em markdown. Para muitas páginas, use httpx.AsyncClient com um semáforo ou um job em lote. Uma página custa 1 crédito, a partir de US$ 0,07/1K páginas em outubro de 2026. Sem KYC; pague com cripto.
O que preciso ter instalado?
Python 3.9 ou superior e um cliente HTTP. requests basta para scripts que leem uma página por vez; httpx adiciona suporte assíncrono para muitas páginas em paralelo. Nenhum pacote nosso é necessário: a API é HTTPS e JSON puros.
Mantenha sua chave fora do código. Leia-a da variável de ambiente WEBREADER_API_KEY, como todos os exemplos abaixo fazem, e dê a cada projeto sua própria chave para revogar uma sem tocar nas outras.
pip install requests httpx
export WEBREADER_API_KEY="your-key"Como leio uma página com requests?
Envie url e quaisquer opções como parâmetros de consulta, dispare em caso de erro HTTP e leia os campos de que precisa. O exemplo mantém o conteúdo principal, limita a 20.000 caracteres para um modelo e imprime quanto custou a chamada.
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])
Como leio muitas páginas de uma vez com httpx?
Use um httpx.AsyncClient e um asyncio.Semaphore definido no seu limite de concorrência ou abaixo dele. Cada conta tem um limite de quantas páginas lê ao mesmo tempo; seu dashboard mostra esse limite, e o suporte pode aumentá-lo. Erros de uma página não devem parar as outras, então o exemplo os coleta ao lado dos resultados em vez de disparar.
Se você passar do limite, a API responde 429 concurrency_limit; nada é cobrado, mas a página não é lida. Manter o semáforo abaixo do limite é mais simples do que tentar de novo.
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 devo usar um job em lote?
Quando você tem uma lista de até 100 URLs e não precisa de cada página no momento em que ela fica pronta. Um job em lote roda do nosso lado, então seu script não mantém conexões abertas nem faz lógica de retry. Inicie com POST https://webreaderapi.com/api/v1/batch e depois colete os resultados do job com o loop de cursor mostrado na página de lote e rastreamento.
Para um site inteiro, inicie um rastreamento (crawl) e deixe que ele siga o sitemap e os links; a mesma página tem um loop completo que grava um arquivo .md por página.
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"])
Como divido o Markdown em chunks para RAG?
Divida primeiro pelos títulos, depois pelo tamanho. Os títulos marcam onde os assuntos mudam, então um chunk que começa em um título é mais fácil para o retriever casar. Mantenha o título da página e o caminho de títulos nos metadados de cada chunk: eles ajudam o modelo a citar a seção certa.
A função abaixo não precisa de nenhuma biblioteca extra. Ela corta nos títulos de # a ### e depois divide qualquer seção longa em linhas em branco para ficar dentro de um limite de caracteres. Armazene final_url com cada chunk, para que dois endereços que levam à mesma página não gerem duplicatas.
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 custa um job em Python?
Conte páginas, não requisições. 1 crédito = 1 página lida sem navegador; 2 créditos quando a página precisa de um navegador para renderizar JavaScript. Uma requisição que falha, ou que o robots.txt de um site recusa, não é cobrada. Ler 2.000 páginas de documentação sem navegador usa cerca de 2.000 créditos, o que 10.000 páginas (US$ 3,75) cobre cinco vezes. Os créditos pré-pagos nunca expiram: as páginas não usadas ficam na sua conta até você usá-las, então um pipeline que roda uma vez por mês usa apenas o que lê.
Python: perguntas frequentes
Existe um pacote oficial de Python?
Não. O código nesta página é tudo o que você precisa: WebReaderAPI é uma API HTTPS simples, e requests ou httpx falam com ela diretamente.
Como trato uma página que o robots.txt recusa?
Verifique o código de erro. blocked_by_robots e target_refused são definitivos e gratuitos; registre-os e continue. A página de erros tem um helper de retry que só repete os códigos que podem mudar.
Posso usar a API a partir de um notebook Jupyter?
Sim. A receita com requests funciona como está. Em um notebook, await read_many(urls) substitui asyncio.run, porque o notebook já roda um event loop.
Qual timeout meu cliente deve usar?
Um pouco mais que o da própria API: o padrão é 30 segundos e o máximo 60, então 75 segundos do lado do cliente deixa margem para um site lento e a resposta.