网页转 JSON API 怎么接:从 Prompt 到可校验的结构化数据
把网页内容变成 JSON,真正困难的部分通常不是“得到一段看起来像 JSON 的文本”,而是让字段含义、数据类型、缺失值、来源和更新规则都能被程序稳定处理。
语义化获取站点 JSON 结构内容 API 可以根据网页 URL 和自然语言 Prompt 提取自定义结构。它适合商品列表、文章索引、表格内容、站点研究和自动化数据准备等场景,但接口返回成功并不代表所有字段都已经过事实验证。生产接入仍需要本地 Schema 校验、来源证据和版本控制。
先选对接口:固定文章结构还是自定义字段
GuGuData 同时提供文章抽取和网页转 JSON 两类能力,它们解决的问题不同。
| 需求 | 更适合的接口 | 原因 |
|---|---|---|
| 提取标题、正文、作者、摘要和发布时间 | article-extract |
返回相对固定的文章结构 |
| 从商品页提取名称、价格、库存状态 | url2json |
字段由 Prompt 按业务需要定义 |
| 从列表页提取多条链接和标签 | url2json |
可以描述列表范围和每项字段 |
| 把正文送入知识库 | 先 article-extract |
正文识别比任意字段抽取更明确 |
| 对文章进一步提取人物、机构或事件 | 两步组合 | 先得到正文,再按下游模型或规则处理 |
不要只因为两个接口都返回 JSON 就把它们当成同一种能力。article-extract 的结构主要由接口定义;url2json 的结构更多由 Prompt 和目标页面共同决定。
请求前先把 Prompt 写成数据契约
“提取这个网页的重要信息”很容易得到可读结果,却不适合直接入库。一个可执行的 Prompt 至少应说明以下内容:
| 契约项 | 应写清楚的内容 | 示例 |
|---|---|---|
| 提取范围 | 从页面哪个区域、提取多少项 | 只提取产品列表中的前 20 项 |
| 字段名称 | 使用稳定、唯一的键名 | name、price、productUrl |
| 字段类型 | 字符串、数字、布尔值、数组或对象 | price 为字符串,保留货币符号 |
| 缺失值 | 页面没有字段时如何表示 | 使用 null,不要猜测 |
| 文本规则 | 是否去除空白、是否保留原文 | 标题去除首尾空白,不翻译 |
| 链接规则 | 相对链接如何处理 | 返回页面中可见的原始链接 |
| 输出外壳 | 单对象还是数组 | 返回 { "products": [] } |
例如,商品列表页可以使用这样的 Prompt:
从页面的产品列表中提取最多 20 项,返回 JSON 对象:
{
"products": [
{
"name": "string",
"price": "string|null",
"availability": "string|null",
"productUrl": "string|null"
}
]
}
要求:
1. 只使用页面明确出现的信息;
2. 缺失字段返回 null,不推断;
3. 保持页面原始语言;
4. 不添加上述结构之外的字段。
这里的 JSON 示例是对输出的自然语言约束,不等同于服务端强制执行的 JSON Schema。调用方仍要在收到响应后自行验证。
从 Prompt 到可入库 JSON 的数据流

这条链路把“抽取成功”和“可以入库”明确分开:URL2JSON API 先根据公开网页与版本化 Prompt 生成候选 JSON,本地 Schema 再检查字段类型、必填项、数量上限和业务规则。通过校验的结果进入版本化数据集,失败结果进入隔离队列,不覆盖旧版本。缺失字段保持 null,而不是为了通过校验自动补猜测值。
调用中文接口
中文接口为:
POST https://api.gugudata.com/websitetools/url2json
参数包括 appkey、url 和 prompt。当前中文文档使用查询参数传递这些值,因此要注意反向代理、APM 和访问日志可能记录完整查询字符串。真实 AppKey 应只由受控服务端持有,并在日志层进行脱敏。
下面使用 Python 标准库发送请求,同时校验 HTTP 状态、JSON 格式、业务状态和 Data 类型:
import json
import os
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
ENDPOINT = "https://api.gugudata.com/websitetools/url2json"
class UrlToJsonError(RuntimeError):
def __init__(self, message: str, http_status: int | None = None):
super().__init__(message)
self.http_status = http_status
def extract_webpage(url: str, prompt: str) -> object:
query = urlencode(
{
"appkey": os.environ["GUGUDATA_APPKEY"],
"url": url,
"prompt": prompt,
}
)
request = Request(f"{ENDPOINT}?{query}", method="POST")
try:
with urlopen(request, timeout=60) as response:
http_status = response.status
payload = json.load(response)
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise UrlToJsonError(detail or str(exc), exc.code) from exc
except (URLError, TimeoutError, json.JSONDecodeError) as exc:
raise UrlToJsonError(str(exc)) from exc
if http_status != 200:
raise UrlToJsonError("Unexpected HTTP status", http_status)
data_status = payload.get("DataStatus", {})
if int(data_status.get("StatusCode", 0)) != 100:
description = data_status.get("StatusDescription", "Extraction failed")
raise UrlToJsonError(description)
data = payload.get("Data")
if not isinstance(data, (dict, list)):
raise UrlToJsonError("Data is not a JSON object or array")
return data
调用时只传公开且有权处理的网页:
prompt = """
Extract at most 20 products as
{"products":[{"name":"string","price":"string|null"}]}.
Use null for missing fields and do not infer values.
""".strip()
data = extract_webpage("https://example.com/products", prompt)
不要在示例或日志中打印完整请求 URL,因为它包含 AppKey、目标 URL 和 Prompt。生产环境可以记录请求 ID、目标域名哈希、Prompt 版本、HTTP 状态和业务状态,但应避免记录凭证和完整采集内容。
英文接口的请求结构不同
英文接口为:
POST https://api.gugudata.io/v1/websitetools/url2json
当前 OpenAPI 契约要求 appkey 位于查询参数,url 和 prompt 位于 JSON 请求体:
curl -X POST "https://api.gugudata.io/v1/websitetools/url2json?appkey=YOUR_APPKEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/products",
"prompt": "Extract product names and prices. Use null for missing values."
}'
英文接口的响应包装字段使用小驼峰,例如 dataStatus.statusCode 和 data;中文接口使用 DataStatus.StatusCode 和 Data。两套接口的成功码也不同,不能共用一条写死字段大小写和成功码的校验逻辑。
返回 JSON 后必须再做本地 Schema 校验
假设业务期望的数据结构是:
{
"products": [
{
"name": "Example Product",
"price": "¥99",
"availability": null,
"productUrl": "https://example.com/products/1"
}
]
}
最低限度的校验应覆盖:
- 顶层必须是对象;
products必须是数组;- 每一项必须是对象;
name必须是非空字符串;- 可空字段只能是指定类型或
null; - 数量不能超过 Prompt 约定的上限;
- URL 字段只能接受业务允许的协议和域名范围;
- 未声明字段是拒绝、保留还是隔离,必须有明确策略。
不依赖第三方库时,可以先做一层窄校验:
from urllib.parse import urlparse
def validate_products(data: object, max_items: int = 20) -> list[dict]:
if not isinstance(data, dict):
raise ValueError("Expected a JSON object")
products = data.get("products")
if not isinstance(products, list):
raise ValueError("products must be an array")
if len(products) > max_items:
raise ValueError("products exceeds the configured limit")
validated = []
for index, item in enumerate(products):
if not isinstance(item, dict):
raise ValueError(f"products[{index}] must be an object")
name = item.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError(f"products[{index}].name is invalid")
product_url = item.get("productUrl")
if product_url is not None:
if not isinstance(product_url, str):
raise ValueError(f"products[{index}].productUrl is invalid")
parsed = urlparse(product_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError(f"products[{index}].productUrl is invalid")
validated.append(item)
return validated
正式项目可以使用 JSON Schema、Pydantic、Zod 或团队现有的契约框架,但不要为了“提高成功率”而把必填字段、类型和数量上限全部放宽。验证失败的数据应进入隔离队列,而不是直接覆盖线上记录。
为 Prompt 和结果建立版本
网页结构、Prompt 和模型行为都会变化。只保存最终 JSON,会让后续很难回答“这个字段为什么变了”。建议至少记录:
| 字段 | 用途 |
|---|---|
sourceUrl |
原始来源 |
canonicalUrl |
规范化后的来源标识 |
promptVersion |
生成该结果的 Prompt 版本 |
schemaVersion |
本地校验规则版本 |
fetchedAt |
本次采集时间 |
requestId |
对应服务端请求,便于排查 |
contentHash |
规范化结果哈希,用于变化检测 |
validationStatus |
passed、rejected 或 needs_review |
rawPayloadRef |
受控原始响应引用,而不是公开日志内容 |
更新时不要仅按 URL 执行覆盖。更稳妥的流程是:
- 获取新结果;
- 验证响应包装和业务状态;
- 执行本地 Schema 校验;
- 规范化字段并计算哈希;
- 与上一版本比较;
- 无变化时只更新检查时间;
- 有变化时创建新版本,并保留差异和来源;
- 关键字段变化进入人工复核或业务规则判断。
列表页需要稳定主键和去重策略
从网页提取多条记录时,标题通常不是稳定主键。商品可能改名,文章标题可能修订,同名记录也可能同时存在。
主键优先级可以设计为:
- 页面明确提供的业务 ID;
- 规范化后的详情页 URL;
- 多个稳定字段组成的复合键;
- 最后才考虑标题等易变文本。
去重还要区分三种情况:
| 情况 | 建议处理 |
|---|---|
| 主键相同、内容哈希相同 | 记录本次检查,不创建内容版本 |
| 主键相同、内容哈希不同 | 创建新版本并记录字段差异 |
| 没有稳定主键 | 标记 needs_review,不要自动合并 |
错误处理要分清 HTTP 与业务状态
中文接口在 JSON 包装中使用 DataStatus.StatusCode。当前本地事实文档列出的状态包括参数错误、频率限制、账号或 AppKey 问题、配额限制和提取失败。英文接口则通过 HTTP 400、401、403、429、500、503 表达请求级状态,并在成功响应中提供 dataStatus。
处理策略可以分为三类:
| 类型 | 示例 | 是否重试 |
|---|---|---|
| 请求不可恢复 | 参数缺失、URL 格式错误、Schema 不匹配 | 修正输入,不自动重试 |
| 权限或配额 | AppKey 错误、无权限、配额耗尽 | 停止并核查账号状态 |
| 暂时性失败 | 频率限制、上游不可用、明确的服务端暂时错误 | 指数退避并设置总次数上限 |
网页提取失败不一定是 AI 服务故障,也可能是目标网页无法访问、只返回脚本壳、加载超时、页面要求登录,或者 Prompt 与页面内容不匹配。重试前应先分类原因,避免对同一不可恢复目标持续请求。
安全边界不能只依赖接口
调用方仍需负责自己的采集合规与数据安全:
- 只处理公开且有权使用的页面;
- 不绕过登录、验证码、付费墙或目标站点访问控制;
- 不把 Cookie、Authorization、浏览器存储或客户会话交给提取链路;
- 对目标域名、允许协议、重定向和结果中的 URL 再做业务侧校验;
- 对 Prompt 做长度和模板限制,避免把不受控用户文本直接变成系统级指令;
- 对结果中的 HTML、链接和富文本执行输出编码或净化;
- 对个人信息、联系方式和敏感字段设置最小化采集与保留策略。
Prompt 注入也需要单独考虑。目标网页中的文字可能包含“忽略之前要求”等内容。调用方不应把网页输出直接当成下一阶段 Agent 的可信指令;提取结果只能作为数据,进入工具调用或自动决策前必须通过固定 Schema、权限和业务规则。
Demo 能证明什么,不能证明什么
2026 年 8 月 25 日(Asia/Shanghai)核验时:
- 中文 Demo 返回 HTTP 200、
DataStatus.StatusCode=100,并按固定 Prompt 返回Data.products; - 英文 Demo 返回 HTTP 200、
dataStatus.statusCode=200、status=SUCCESS,响应头明确标记为缓存 Demo; - 两个 Demo 都返回了非空结构化数据。
这些证据可以证明当前固定样例链路、包装字段和结果形状可读,但不能证明:
- 任意第三方网页都能访问或成功渲染;
- 任意 Prompt 都严格遵守字段与类型要求;
- 生产 AppKey、配额、并发和长时间稳定性已经验收;
- 提取字段与来源页面在事实层面完全一致;
- 缓存 Demo 代表实时抓取结果。
正式上线前,应使用自有 AppKey、已授权测试网页和固定 Prompt 完成小范围冒烟,并人工对照来源页面核验关键字段。
上线前检查清单
- 明确选择
url2json,而不是更适合固定文章结构的article-extract - Prompt 写明范围、字段、类型、缺失值、数量上限和输出外壳
- AppKey 只保存在服务端,不进入代码、前端或普通访问日志
- 目标 URL 属于公开且有权处理的网页
- 同时检查 HTTP 状态和对应域名的业务状态字段
- 对
Data执行本地 Schema 和数量限制校验 - Schema 失败结果进入隔离,不覆盖旧数据
- 保存 Prompt 版本、Schema 版本、请求 ID、来源和采集时间
- 使用稳定主键和内容哈希区分重复与更新
- 对结果中的 URL、HTML 和敏感字段再次执行安全策略
- 为 429 或暂时性错误设置有上限的退避,不重试参数和权限错误
- 用真实 AppKey 和自有测试页完成生产冒烟,不把 Demo 当成生产验收
网页转 JSON 的价值,是让同一套采集链路适配不同页面和字段需求;它的工程边界,是输出结构仍然需要调用方验证。把 Prompt 当成可版本化的数据契约,把 Schema 校验、来源证据和失败隔离放在入库之前,才能让“能提取”变成“可长期维护”。