从curl到工程封装:网页正文提取API接入全指南

背景与适用场景

在信息聚合、内容分析、知识库构建等任务中,常常需要从杂乱的新闻页面、公众号文章或博客中剥离导航、广告、侧栏与评论区,仅保留正文主体。若手动编写XPath/CSS选择器,每换一个网站就要重新适配,维护维护复杂度极高。

网页正文提取API基于文本密度算法,可自动识别并抽取网页的核心内容,同时返回标题、发布时间、配图列表与预估阅读时长。本文将以该API为例,演示从最直接的curl命令行调用到生产级Python封装的完整链路,让读者掌握一种可复用的“先验证、再封装、后集成”的API接入范式。

接口能力边界

在投入封装之前,有必要明确该API的适用边界:

  • 输入:一个合法的公开网页URL(HTTP或HTTPS)。
  • 输出:结构化JSON,包含title(标题)、content(正文HTML/纯文本)、publish_time(发布时间)、word_count(字数)、reading_time(预估阅读时长)、images(配图URL数组)、image_count(配图数量)。
  • 限制:QPS为5次/秒;单次请求仅处理一个URL;仅支持可公开访问的页面,无法处理需要登录认证或频繁触发反爬机制的站点。
  • 不承诺:素材中未提及SLA、可用性百分比、处理成功率或处理速度保证,生产环境中需自行设计降级与重试逻辑。

鉴权方式与请求参数

鉴权

该API使用HTTP Header 鉴权,请求时必须在X-API-Key头中传入有效的API密钥。密钥通常由平台提供,本文不涉及申请流程,假设读者已持有密钥。

Query 参数

参数类型必填说明
urlstring需要提取正文的目标网页URL,需URL编码

请求方法

GET

请求地址

https://v1.apizero.cn/api/content-extract

快速体验——curl 示例

将API密钥设置为环境变量(或直接替换字符串),即可通过一行命令验证接口是否正常工作:

export APIZERO_API_KEY="your-api-key-here" curl -sS \ -X GET \ -H "X-API-Key: $APIZERO_API_KEY" \ "https://v1.apizero.cn/api/content-extract?url=https://apizero.cn"

说明

  • -sS使curl静默输出并显示错误信息;
  • -H添加请求头;
  • URL参数中url的值已被自动编码(curl不会对已有URL再次编码,但建议目标URL本身不含空格等特殊字符);
  • 返回结果为标准JSON,可使用管道| jq .进行格式化。

如果一切正常,你将看到类似如下结构的响应(具体内容依目标页面而异):

{ "code": 0, "msg": "成功", "data": { "title": "Apizero - 聚合API平台", "content": "...(正文HTML或纯文本)...", "publish_time": "2024-01-15", "word_count": 2300, "reading_time": "5分钟", "image_count": 3, "images": [ "https://..." ] } }

返回值结构详解

接口始终返回一个顶层对象,字段如下:

字段类型说明
codeint业务状态码,0表示成功,非0表示异常
msgstringcode对应的中文描述
dataobject/null成功时包含提取结果,失败时为null

data 对象字段

字段类型说明
titlestring网页标题(head中的title标签或文章中h标签)
contentstring提取后的正文,格式为HTML标签包裹的文本(部分场景可能为纯文本)
publish_timestring页面标注的发布时间,格式为YYYY-MM-DD;若无法识别则为空字符串
word_countint正文汉字与英文单词的总计字数(近似值)
reading_timestring基于平均阅读速度(约300字/分钟)推算的阅读时长,如"5分钟"
imagesarray[string]正文内图片的完整URL列表(不含装饰性图标)
image_countintimages数组长度

注意content字段可能包含HTML标签,如<p><img>等,需要渲染时可直接内嵌,如需纯文本可自行用正则或HTML解析器去除标签。

错误处理与常见问题

HTTP 状态码与业务状态码

HTTP状态业务码(code)msg含义典型原因
2000成功-
40010001参数缺失或格式错误未提供url或URL无效
40110002API Key 无效或缺失请求头中未携带X-API-Key或密钥错误
42910003请求过于频繁超过QPS 5次/秒限制
50020001服务内部错误目标网页无法访问或解析失败

常见排查切入点

  1. curl返回401:检查X-API-Key是否拼写正确,密钥是否过期或被盗用。
  2. curl返回400:确认URL参数名是否为url,且值已正确URL编码(如含有&符号时)。
  3. 返回code=0但content为空:可能目标页面为纯图片、PDF或需JavaScript渲染的单页应用(SPA),文本密度算法无法提取有效正文。此时建议降级为直接抓取页面描述或放弃提取。
  4. 响应缓慢或超时:目标网页响应慢或服务器连接超时,建议设置客户端超时时间为10-15秒,并考虑将URL排队异步处理。

工程化封装建议

curl验证到集成到项目,通常需要以下几个步骤:

1. 定义配置与异常

在Python中,首先创建一个封装类:

import requests from typing import Optional, Dict, Any class ContentExtractError(Exception): pass class ContentExtractClient: BASE_URL = "https://v1.apizero.cn/api/content-extract" def __init__(self, api_key: str, timeout: int = 10): self.api_key = api_key self.timeout = timeout self.session = requests.Session() self.session.headers.update({"X-API-Key": api_key}) def extract(self, url: str) -> Dict[str, Any]: """提取网页正文,返回data字段字典""" resp = self.session.get( self.BASE_URL, params={"url": url}, timeout=self.timeout ) if resp.status_code != 200: raise ContentExtractError(f"HTTP {resp.status_code}: {resp.text}") body = resp.json() if body.get("code") != 0: raise ContentExtractError(f"业务错误 [{body['code']}]: {body.get('msg')}") return body["data"]

2. 添加重试与降级

考虑到网络波动和限流,可用tenacity库实现指数退避重试:

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class ContentExtractClient: # ... 上述代码 ... @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout, ContentExtractError)) ) def extract_with_retry(self, url: str) -> Dict[str, Any]: return self.extract(url)

注意:对429限流错误可特别处理,例如增加延时后重试。

3. 异步并发控制

如果需要批量处理多个URL,应使用信号量控制并发数不超过QPS(5)。以下使用asyncio+aiohttp示例:

import asyncio import aiohttp class AsyncContentExtractClient: def __init__(self, api_key: str, max_concurrent: int = 5): self.api_key = api_key self.semaphore = asyncio.Semaphore(max_concurrent) async def extract_one(self, session: aiohttp.ClientSession, url: str) -> dict: async with self.semaphore: headers = {"X-API-Key": self.api_key} params = {"url": url} async with session.get(self.BASE_URL, headers=headers, params=params) as resp: body = await resp.json() if body.get("code") != 0: raise ContentExtractError(...) return body["data"] async def extract_many(self, urls: list[str]) -> list[dict]: async with aiohttp.ClientSession() as session: tasks = [self.extract_one(session, url) for url in urls] return await asyncio.gather(*tasks, return_exceptions=True)

4. 缓存与幂等考虑

对于相同URL的重复提取,可考虑本地缓存(如functools.lru_cache或 Redis),避免消耗API配额。注意缓存键应包含URL(保留原始字符顺序)。

参考文档

  • 网页正文提取 API 官方文档
  • 原始 Markdown 文档