4. HTTP协议与网页结构
在金融新闻与公告的增量采集中,我们面对的不是一个静态文件,而是一套持续运转的Web交互系统。每一次采集,本质上都是一次或多次HTTP请求与响应的往返。理解这套协议与网页结构,决定了采集程序能否稳定拿到数据、能否正确识别“有新内容”、能否在遇到反爬时快速定位问题。本节围绕请求响应模型、状态码、Cookie、User-Agent以及HTML与JSON两类数据结构展开,目标是让你能读懂一次采集请求的完整生命周期。
一、请求响应模型:一次采集的完整往返
HTTP是典型的客户端—服务器请求响应模型。采集器作为客户端,向目标服务器发送一个请求报文,服务器处理后返回响应报文。请求报文由请求行、请求头、空行和请求体组成;响应报文由状态行、响应头、空行和响应体组成。对于金融公告采集,最常见的请求是GET(获取列表页或详情页)和POST(提交查询条件、获取分页数据)。
请求头中承载了大量影响采集结果的关键信息。例如Host决定虚拟主机路由,Accept声明可接受的响应类型,Accept-Encoding影响是否返回压缩内容,Referer常被用于防盗链校验,Cookie携带会话身份,User-Agent标识客户端类型。响应头中的Content-Type决定我们该用HTML解析器还是JSON解析器,Set-Cookie则可能要求我们在后续请求中回传新的会话标识。
二、状态码:判断采集结果的第一信号
状态码是服务器对本次请求处理结果的标准化表达。在增量采集中,状态码不仅用于判断成功与失败,还直接影响重试策略、限速策略和增量判断逻辑。
| 状态码 | 含义 | 采集场景中的典型处理 |
|---|---|---|
| 200 | 请求成功 | 正常解析响应体,进入数据抽取流程 |
| 301/302 | 永久/临时重定向 | 跟随Location,注意Cookie与Referer是否需同步更新 |
| 304 | 资源未修改 | 配合If-Modified-Since或ETag实现增量,无需重复解析 |
| 401/403 | 未授权/禁止访问 | 检查Cookie、、User-Agent及请求频率 |
| 404 | 资源不存在 | 公告可能已下架,记录并跳过,避免无限重试 |
| 429 | 请求过多 | 触发限速,需退避重试并降低并发 |
| 500/502/503 | 服务器错误 | 多为临时故障,采用指数退避重试 |
需要特别注意的是,很多金融数据接口在业务层面失败时仍返回200,错误信息藏在JSON体的code或success字段中。因此状态码只是第一层判断,响应体内部还需要二次校验。
三、Cookie与User-Agent:会话与身份的维护
Cookie是服务器存储在客户端、并在后续请求中回传的小型状态数据。金融网站常用Cookie维持登录态、记录访问轨迹、实施反爬挑战。采集器需要像浏览器一样,在首次响应中读取Set-Cookie,并在后续请求的Cookie头中回传。使用requests.Session或httpx.Client可以自动完成这一过程。
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Referer": "https://www.example-finance.com/notice/list"
})
resp = session.get("https://www.example-finance.com/api/notice?page=1")
print(resp.status_code)
print(session.cookies.get_dict())
User-Agent用于标识客户端身份。默认的Python UA极易被识别为爬虫并直接拒绝。合理的做法是使用真实浏览器UA,并保持同一会话内UA一致。若目标站点对UA与Cookie、Referer的组合做一致性校验,随意伪造反而会触发风控。因此,UA不是“随便填一个”,而是要与请求上下文匹配。
四、HTML与JSON数据结构解析
金融新闻与公告的响应体主要有两类:HTML和JSON。列表页、详情页多为HTML,而分页数据、公告查询接口多为JSON。两者解析思路不同。
HTML是半结构化文档,依赖标签层级和属性定位。常用解析方式包括CSS选择器和XPath。例如公告列表通常位于ul.notice-list > li中,标题在a标签内,发布时间在span.time中。解析时要关注编码、动态渲染和结构变化。
from bs4 import BeautifulSoup
html = resp.text
soup = BeautifulSoup(html, "html.parser")
for li in soup.select("ul.notice-list > li"):
title = li.select_one("a").get_text(strip=True)
link = li.select_one("a")["href"]
date = li.select_one("span.time").get_text(strip=True)
print(date, title, link)
JSON是结构化数据,解析更直接,但字段嵌套和类型变化需要防御性处理。金融接口常见结构为data.list、data.total、data.pageNum。增量采集时,我们通常依据publishTime或noticeId判断是否为新数据。
import json
payload = resp.json()
items = payload.get("data", {}).get("list", [])
for item in items:
notice_id = item.get("noticeId")
title = item.get("title")
publish_time = item.get("publishTime")
print(notice_id, publish_time, title)
Content-Type和响应体首字符。若首字符是<,通常是HTML;若是{或[,通常是JSON。解析前先做类型判断,可避免大量无效异常。五、案例复盘:一次公告增量采集的排障过程
某次采集金融公告时,程序始终返回403。排查发现:请求头缺少Referer,且UA为默认Python标识。补充Referer与真实UA后,状态码变为200,但返回内容为空列表。进一步检查发现,接口需要先访问列表页获取Cookie中的sessionId,而程序直接请求了接口。改为先请求列表页、再请求接口后,数据正常返回。最后,程序依据publishTime与本地已存最大时间比较,只入库新公告,实现了增量采集。
这个案例说明:状态码、Cookie、User-Agent和数据结构解析不是孤立知识点,而是一条完整的采集链路。任何一环缺失,都会表现为“拿不到数据”或“拿到错误数据”。
把上述链路固化为采集器的标准流程:建立会话并设置请求头,先访问列表页获取Cookie,再请求数据接口,依据状态码分流处理,依据Content-Type选择解析器,最后用时间或ID字段做增量过滤。这样,HTTP协议与网页结构就不再是抽象概念,而是可直接落地的采集工程基础。