저는 URL을 데이터로 변환하는 유료 호출 기반 API인 WebExtract를 개발하고 있습니다. 이 API는 두 개의 주요 엔드포인트(endpoint)를 가지고 있습니다. GET /v1/markdown은 깔끔한 마크다운(Markdown)을 반환하며, GET /v1/extract는 동일한 마크다운에 고정 스키마 필드(product, tables, metadata)가 추가된 데이터를 반환합니다. 본 게시물에서는 하나의 제품 페이지와 하나의 테이블 페이지를 모두 이 두 엔드포인트를 통해 실행하여 에이전트(agent)가 실제로 어떤 데이터를 받게 되는지 보여줍니다. 아래의 모든 스니펫은 수동으로 작성된 것이 아니라 examples/compare 폴더에 저장된 API 응답에서 복사한 것입니다.
파일 출처에 대한 한 가지 참고 사항: 이 파일들은 2026년 10월 25일 (UTC)에 베이스 세폴리아(Base Sepolia) 샌드박스 경로( /sandbox/v1/..., 테스트 USDC로 결제)를 통해 캡처되었습니다. 해당 샌드박스는 라이브 /v1/... 경로와 동일한 추출 코드를 실행합니다.
페이지 1: 제품 페이지
이 페이지는 스크래핑을 위해 구축된 연습 사이트인 https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html입니다. 페이지 자체에도 '여기에 표시된 가격과 평점은 무작위로 할당되었으며 실제 의미가 없다'고 명시되어 있습니다.
마크다운 (The Markdown)
/v1/markdown은 1567자, 261 단어를 반환했습니다. 끝 부분은 다음과 같습니다:
## Product Information
| UPC | a897fe39b1053632 |
...
이것은 페이지를 읽기만 하면 되는 모델에게 좋은 입력입니다. 짧고, 가격, 재고, 테이블 정보가 모두 포함되어 있습니다. 하지만 이 테이블을 보세요. HTML 테이블에는 헤더 행(header row)이 없고, 마크다운 테이블에는 헤더가 필요하기 때문에 첫 번째 데이터 행(UPC)이 헤더 위치에 놓이게 됩니다. 모델은 아마도 이를 올바르게 읽어낼 것입니다. 파이프(|)를 기준으로 분할하고 첫 번째 행을 열 이름으로 처리하는 프로그램은 그렇지 못할 것입니다.
구조화된 JSON (The structured JSON)
/v1/extract는 동일한 마크다운(두 문자열은 동일함)에 추가 필드를 더하여 반환했습니다. 여기 product 객체에 대한 jq 선택 결과가 있습니다:
jq '.product | {name, price, currency, availability, rating, source}' out/product.extract.json
{
"name": "A Light in the Attic",
"price": 51.77,
...
price는 숫자(number)이고, currency는 코드(code)이며, availability는 고정 값(fixed value)입니다. 따라서 코드를 사용하여 정규 표현식(regex) 없이 이들을 비교하거나 저장할 수 있습니다.
가장 중요한 필드는 source입니다. 이 페이지에는 JSON-LD, OpenGraph 제품 태그 또는 마이크로데이터가 없기 때문에 가격은 페이지 휴리스틱(page heuristics)을 통해 추출되었습니다. 응답에서는 이를 숨기는 대신 명시합니다:
"evidence": "first [class*=price] element; same amount in table row \"Price (excl. tax)\""
[
"heuristic_price_low_confidence",
"currency_inferred_from_symbol"
...
이것은 product.price_detail.prices[0].evidence와 product.price_detail.warnings에 해당합니다. evidence는 숫자가 어디서 왔는지(그리고 제품 테이블에서 같은 금액을 보여주는지) 알려줍니다. warnings는 가격이 휴리스틱으로 추출되었고 통화가 '£' 기호로부터 추론되었다는 것을 알려줍니다. 만약 에이전트가 가격에 따라 행동하려고 한다면, 반드시 먼저 price_detail.warnings를 확인해야 합니다. 저희는 이 경고(warning)들을 의도적으로 유지하는데, 비록 숫자가 정확하더라도 다른 페이지에서는 휴리스틱이 틀릴 수 있기 때문입니다.
테이블은 다음과 같이 반환됩니다:
["col_1","col_2"]
[["UPC","a897fe39b1053632"],["Product Type","Books"],["Price (excl. tax)","£51.77"]]
{"row_count":7,"column_count":2,"consistent_width":true}
이것들은 tables[0].headers, 처음 세 개의 rows, 그리고 checks입니다. 페이지에 헤더 행(header row)이 없기 때문에, 헤더는 플레이스홀더(col_1, col_2)이며 UPC는 데이터 행으로 남아 있습니다. checks 객체는 사용하기 전에 구조를 확인할 수 있게 해줍니다: 7개 행, 2개 열, 모든 행의 너비가 동일함.
페이지 2: 테이블 페이지
두 번째 페이지는 https://webscraper.io/test-sites/tables이며, 두 개의 표준 HTML 테이블을 가진 연습용 페이지입니다. /v1/markdown은 508자, 124단어를 반환했고, 여기서는 마크다운(Markdown) 테이블이 괜찮아 보입니다. 왜냐하면 이 페이지는 실제 <thead> 행을 가지고 있기 때문입니다.
차이점은 /v1/extract가 추가하는 내용입니다. 다음은 tables[0]의 원문 그대로입니다:
{
"index": 0,
"caption": null,
...
마크다운(Markdown) 방식에서는 두 개의 테이블이 서로 아무것도 없이 연속적으로 배치됩니다. 반면, JSON 방식에서는 이들이 각각 tables[0]과 tables[1]로 분리되며, 각자 고유한 헤더와 검사 항목을 갖기 때문에 어느 것이 끝이고 다음 것이 시작인지 구분할 필요가 없습니다.
이것은 제품 페이지(product page)가 아니므로 product는 단순히 null입니다. 추측하는 내용은 전혀 없습니다. 응답에는 그 이유도 설명됩니다:
{
"reason": "no_product_markup",
"detail": "no JSON-LD Product/ProductGroup, product:price meta, schema.org microdata, or price+title+cart heuristic match"
...
}
이것이 바로 product_unresolved 필드입니다. 이유와 함께 null인 것이 비어 보이는 객체보다 처리하기가 더 쉽습니다.
두 페이지 모두 metadata 객체를 받게 됩니다. 테이블 페이지의 예시는 다음과 같습니다:
{
"title": "Table Playground | Web Scraper Test Sites",
"language": "en",
...
}
전체 객체에는 또한 description, authors, published_at, modified_at, site_name, 그리고 image 필드가 포함됩니다. 페이지가 제공하지 않는 필드는 채워지지 않고 null이거나 비어 있습니다.
에이전트는 어느 것을 사용해야 할까요?
- 모델이 단순히 페이지 내용을 읽을 필요만 있을 때: 요약하거나, 내용에 대한 질문에 답하거나, 인용할 때
/v1/markdown을 사용하세요. 호출당 비용은 $0.002입니다.
- 코드에서 필드가 필요한 경우: 가격 비교, 테이블을 데이터프레임(dataframe)으로 로드, 재고 여부 확인 등을 할 때는
/v1/extract를 사용하세요. 호출당 비용은 $0.005입니다. 이 방식에는 마크다운도 포함되어 있으므로 두 가지를 모두 호출할 필요가 없습니다.
알아두어야 할 몇 가지 제한 사항이 있습니다: product는 JSON-LD, OpenGraph, microdata 또는 최후의 수단으로 페이지 휴리스틱(page heuristics)에서 가져옵니다. product.rating은 페이지에 JSON-LD나 별점 마크업이 있는 경우에만 채워집니다. 이미지 URL은 페이지가 작성한 대로(../../media/...와 같이) 유지되므로, final_url을 기준으로 이를 해결해야 할 수도 있습니다.
직접 시도해 보기
REST의 무료 등급(free tier)은 옵트인 방식입니다: 가입할 필요 없이 호출하는 IP 주소당 매일 20회의 무료 호출이 제공됩니다. 헤더에 X-Billing: free를 추가하세요.
또는 클라이언트가 헤더를 설정할 수 없는 경우 쿼리 매개변수 ?free=1을 사용할 수 있습니다:
curl -G "https://api.webextract.dev/v1/markdown" --data-urlencode "free=1" \
--data-urlencode "url=https://webscraper.io/test-sites/tables"
무료 할당량은 호출자 IP 주소(caller IP address)별로 계산됩니다. 요청 사이에 아웃고잉 IP가 변경되는 경우(프록시 또는 NAT 뒤에 있는 클라우드 호스트의 경우), 각 주소마다 별도로 호출 횟수가 계산되므로 X-Free-Remaining 값은 오르내릴 수 있습니다. 실패한 REST 가져오기(REST fetches)는 요금이 부과되지 않습니다. 무료 옵트인, 키 또는 결제 없이 엔드포인트에 접근하면 HTTP 402 응답 코드와 함께 x402 결제 요구 사항이 반환됩니다. 이후에는 USDC로 호출당 비용을 지불하거나 선불 크레딧을 사용할 수 있습니다.
해당 폴더에는 네 가지의 단일 파일 클라이언트가 포함되어 있습니다: compare_rest.py (두 엔드포인트를 나란히 비교), langchain_tool.py, mcp_client.py (https://api.webextract.dev/mcp를 대상으로 하며, 도구는 page_markdown과 extract_url임) 및 x402_client.py입니다. x402 클라이언트는 테스트 USDC와 함께 Base Sepolia 샌드박스에서만 실행되며 메인넷(mainnet)은 거부합니다. LangChain 도구들은 모델 기반 에이전트 내부가 아닌 직접 호출하여 테스트되었습니다.
만약 사용자가 자신의 페이지를 두 엔드포인트 모두로 돌려보고 JSON에 오류가 발생한다면 알려주시면 감사하겠습니다. 잘못된 가격과 깨진 표는 우리가 가장 찾고 싶은 버그들입니다.
고지 사항: 저는 WebExtract 팀 소속이며, WebExtract는 저희 자체 제품입니다. 이 게시물은 AI의 도움을 받아 작성되었습니다. JSON 및 마크다운 발췌 내용은 저장소(repository)에 있는 저장된 API 응답에서 복사했으며, 발행 전에 해당 파일들과 비교하여 확인했습니다.