Szybka odpowiedź
Jak przekonwertować stronę internetową do Markdown w Pythonie?
Wywołaj WebReaderAPI z requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...}) i swoim kluczem w nagłówku Authorization; odpowiedź JSON zawiera główną treść strony w markdown. Do wielu stron użyj httpx.AsyncClient z semaforem lub zadania wsadowego. Jedna strona kosztuje 1 kredyt, od 0,07 USD/1K stron według stanu na październik 2026. Bez KYC; płatność kryptowalutą.
Co muszę mieć zainstalowane?
Python 3.9 lub nowszy i jednego klienta HTTP. requests wystarcza do skryptów, które czytają po jednej stronie; httpx dodaje obsługę async do wielu stron równolegle. Żaden nasz pakiet nie jest potrzebny: API to zwykły HTTPS i JSON.
Trzymaj klucz poza kodem. Czytaj go ze zmiennej środowiskowej WEBREADER_API_KEY, jak w każdym przykładzie poniżej, i dawaj każdemu projektowi własny klucz, żeby móc unieważnić jeden bez ruszania pozostałych.
pip install requests httpx
export WEBREADER_API_KEY="your-key"Jak odczytać jedną stronę przez requests?
Wyślij url i dowolne opcje jako parametry zapytania, zgłoś wyjątek przy błędzie HTTP i odczytaj potrzebne pola. Przykład zachowuje główną treść, ogranicza ją do 20 000 znaków dla modelu i wypisuje, ile kosztowało wywołanie.
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])
Jak odczytać wiele stron naraz przez httpx?
Użyj jednego httpx.AsyncClient i asyncio.Semaphore ustawionego na wartość równą lub poniżej limitu współbieżności. Każde konto ma limit liczby stron czytanych jednocześnie; Twój dashboard go pokazuje, a support może go podnieść. Błędy jednej strony nie powinny zatrzymywać pozostałych, więc przykład zbiera je obok wyników zamiast zgłaszać wyjątek.
Jeśli przekroczysz limit, API odpowiada 429 concurrency_limit; nic nie jest rozliczane, ale strona nie zostaje odczytana. Trzymanie semafora poniżej limitu jest prostsze niż ponawianie.
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"])
Kiedy zamiast tego użyć zadania wsadowego?
Gdy masz listę do 100 URL-i i nie potrzebujesz każdej strony w momencie, gdy jest gotowa. Zadanie wsadowe działa po naszej stronie, więc Twoja skrypt nie trzyma otwartych połączeń i nie wykonuje logiki ponawiania. Uruchom je przez POST https://webreaderapi.com/api/v1/batch, a następnie odbierz wyniki zadania pętlą kursora pokazaną na stronie o zadaniach wsadowych i crawlingu.
Dla całej witryny uruchom zamiast tego crawling i pozwól mu podążać za sitemapą i linkami; ta sama strona zawiera pełną pętlę, która zapisuje jeden plik .md na stronę.
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"])
Jak podzielić Markdown na fragmenty do RAG?
Najpierw dziel po nagłówkach, potem po rozmiarze. Nagłówki wyznaczają miejsca zmiany tematu, więc fragment zaczynający się od nagłówka łatwiej dopasować retrieverowi. Zachowaj tytuł strony i ścieżkę nagłówków w metadanych każdego fragmentu: pomagają modelowi zacytować właściwą sekcję.
Poniższa funkcja nie wymaga dodatkowej biblioteki. Dzieli na nagłówkach od # do ###, a następnie dzieli każdą długą sekcję po pustych wierszach, aby zmieścić się w limicie znaków. Przechowuj final_url z każdym fragmentem, aby dwa adresy prowadzące do tej samej strony nie tworzyły duplikatów.
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
Ile kosztuje zadanie w Pythonie?
Licz strony, nie zapytania. 1 kredyt = 1 odczytana strona bez przeglądarki; 2 kredyty, gdy strona wymaga przeglądarki do wyrenderowania JavaScript. Zapytanie, które się nie powiedzie lub które zostanie odrzucone przez robots.txt witryny, nie jest obciążane opłatą. Odczyt 2 000 stron dokumentacji bez przeglądarki zużywa około 2 000 kredytów, co 10 000 stron (3,75 USD) pokrywa pięciokrotnie. Przedpłacone kredyty nigdy nie wygasają: niewykorzystane strony pozostają na Twoim koncie, dopóki ich nie wykorzystasz, więc pipeline uruchamiany raz w miesiącu zużywa tylko to, co odczytuje.
Python: najczęstsze pytania
Czy istnieje oficjalny pakiet Python?
Nie. Kod na tej stronie to wszystko, czego potrzebujesz: WebReaderAPI to zwykłe API HTTPS, a requests lub httpx komunikują się z nim bezpośrednio.
Jak obsłużyć stronę, której odmawia robots.txt?
Sprawdź kod błędu. blocked_by_robots i target_refused są ostateczne i darmowe; zapisz je i kontynuuj. Strona błędów zawiera pomocnik ponawiania, który ponawia tylko kody, które mogą się zmienić.
Czy mogę używać API z notatnika Jupyter?
Tak. Przepis requests działa bez zmian. W notatniku await read_many(urls) zastępuje asyncio.run, ponieważ notatnik już uruchamia pętlę zdarzeń.
Jakiego limitu czasu powinien używać mój klient?
Trochę większego niż samo API: domyślnie 30 sekund, maksymalnie 60, więc 75 sekund po stronie klienta zostawia zapas na wolną stronę i odpowiedź.