빠른 답변
Python에서 웹페이지를 Markdown으로 변환하려면 어떻게 하나요?
Authorization 헤더에 requests.get("https://webreaderapi.com/api/v1/read", params={"url": ...})과 키를 담아 WebReaderAPI를 호출하세요. JSON 응답의 markdown에 페이지 본문이 담겨 있습니다. 여러 페이지에는 세마포어와 함께 httpx.AsyncClient을 사용하거나 배치 작업을 사용하세요. 2026년 10월 기준 US$0.07/1K 페이지부터 페이지 하나에 1크레딧입니다. KYC 없이, 암호화폐로 결제하세요.
무엇을 설치해야 하나요?
Python 3.9 이상과 HTTP 클라이언트 하나면 됩니다. 한 번에 한 페이지를 읽는 스크립트에는 requests이면 충분하고, httpx는 여러 페이지를 병렬로 처리하는 비동기 지원을 추가합니다. 저희 패키지는 필요 없습니다. API는 순수 HTTPS와 JSON이기 때문입니다.
키는 코드 밖에 두세요. 아래 모든 예제처럼 WEBREADER_API_KEY 환경 변수에서 읽고, 프로젝트마다 별도 키를 부여하여 다른 키는 건드리지 않고 하나만 폐기할 수 있게 하세요.
pip install requests httpx
export WEBREADER_API_KEY="your-key"requests로 한 페이지를 어떻게 읽나요?
url과 옵션을 검색 쿼리 파라미터로 보내고, HTTP 오류 시 예외를 발생시키고, 필요한 필드를 읽으세요. 예제는 본문을 유지하고, 모델을 위해 20,000자로 제한하며, 호출 비용을 출력합니다.
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])
httpx로 여러 페이지를 한 번에 어떻게 읽나요?
httpx.AsyncClient 하나와 동시성 한도 이하로 설정한 asyncio.Semaphore을 사용하세요. 모든 계정에는 동시에 읽는 페이지 수에 대한 한도가 있으며, 대시보드에 표시됩니다. 지원팀이 이를 높일 수 있습니다. 한 페이지의 오류가 다른 페이지를 멈추게 해서는 안 되므로, 예제는 예외를 발생시키는 대신 결과 옆에 오류를 모읍니다.
한도를 초과하면 API가 429 concurrency_limit으로 응답합니다. 청구는 되지 않지만 페이지는 읽히지 않습니다. 세마포어를 한도 아래로 유지하는 것이 재시도보다 간단합니다.
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"])
대신 배치 작업은 언제 사용해야 하나요?
최대 100개의 URL 목록이 있고 각 페이지가 준비되는 즉시 필요하지 않을 때 사용하세요. 배치 작업은 저희 쪽에서 실행되므로 스크립트가 연결을 열어둔 채로 있지 않고 재시도 로직도 수행하지 않습니다. POST https://webreaderapi.com/api/v1/batch로 시작한 다음, 배치 및 크롤링 페이지에 나온 커서 루프로 작업에서 결과를 수집하세요.
사이트 전체라면 대신 크롤링을 시작해 sitemap과 링크를 따라가게 하세요. 같은 페이지에 페이지마다 .md 파일 하나를 작성하는 완전한 루프가 있습니다.
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"])
RAG용으로 Markdown을 어떻게 청크로 나누나요?
먼저 제목을 기준으로 나누고, 그다음 크기를 기준으로 나누세요. 제목은 주제가 바뀌는 지점을 표시하므로 제목에서 시작하는 청크는 검색기가 매칭하기 더 쉽습니다. 각 청크의 메타데이터에 페이지 제목과 제목 경로를 유지하세요. 모델이 올바른 섹션을 인용하는 데 도움이 됩니다.
아래 함수는 별도 라이브러리가 필요 없습니다. #에서 ### 제목 기준으로 자른 다음, 긴 섹션은 빈 줄 기준으로 나누어 문자 예산 이내로 유지합니다. 각 청크와 함께 final_url를 저장해, 같은 페이지로 이어지는 두 주소가 중복을 만들지 않게 하세요.
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
Python 작업 비용은 얼마인가요?
요청이 아니라 페이지를 세세요. 1 크레딧 = 브라우저 없이 페이지 1회 읽기, 페이지가 JavaScript를 렌더링하는 데 브라우저가 필요하면 2 크레딧. 실패한 요청이나 사이트의 robots.txt가 거부한 요청은 과금되지 않습니다. 브라우저 없이 문서 페이지 2,000개를 읽으면 약 2,000 크레딧을 사용하며, 이는 10,000페이지(US$3.75)로 다섯 번 이상 충당됩니다. 선불 크레딧은 만료되지 않습니다. 사용하지 않은 페이지는 사용할 때까지 계정에 남아 있습니다이므로 한 달에 한 번 실행되는 파이프라인은 읽는 만큼만 사용합니다.
Python: 자주 묻는 질문
공식 Python 패키지가 있나요?
아니요. 이 페이지의 코드면 충분합니다. WebReaderAPI는 일반 HTTPS API이고, requests이나 httpx가 이를 직접 다룹니다.
robots.txt가 거부하는 페이지는 어떻게 처리하나요?
에러 코드를 확인하세요. blocked_by_robots과 target_refused는 최종 응답이며 무료입니다. 기록하고 계속 진행하세요. 에러 페이지에 상태가 바뀔 수 있는 코드만 재시도하는 재시도 헬퍼가 있습니다.
Jupyter 노트북에서 API를 사용할 수 있나요?
네. requests 레시피는 그대로 작동합니다. 노트북에서는 이미 이벤트 루프가 실행 중이므로 await read_many(urls)이 asyncio.run를 대체합니다.
클라이언트 타임아웃은 얼마로 설정해야 하나요?
API 자체보다 조금 더 길게: 기본값은 30초, 최댓값은 60초이므로 클라이언트 측을 75초로 두면 느린 사이트와 응답을 위한 여유가 생깁니다.