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则可能要求我们在后续请求中回传新的会话标识。

采集器与浏览器的核心差异在于:浏览器会自动维护Cookie、自动执行JavaScript、自动加载子资源;而采集器必须显式管理这些状态。因此,理解请求响应模型,就是理解“哪些状态需要我们自己维护”。

二、状态码:判断采集结果的第一信号

状态码是服务器对本次请求处理结果的标准化表达。在增量采集中,状态码不仅用于判断成功与失败,还直接影响重试策略、限速策略和增量判断逻辑。

状态码含义采集场景中的典型处理
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不是“随便填一个”,而是要与请求上下文匹配。

增量采集建议:首次请求先访问列表页获取Cookie与必要,再请求数据接口。这样比直接请求接口更接近真实用户行为,成功率更高。

四、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)
常见误区:把HTML当JSON解析,或把JSON当HTML解析。判断依据是响应头Content-Type和响应体首字符。若首字符是<,通常是HTML;若是{或[,通常是JSON。解析前先做类型判断,可避免大量无效异常。

五、案例复盘:一次公告增量采集的排障过程

某次采集金融公告时,程序始终返回403。排查发现:请求头缺少Referer,且UA为默认Python标识。补充Referer与真实UA后,状态码变为200,但返回内容为空列表。进一步检查发现,接口需要先访问列表页获取Cookie中的sessionId,而程序直接请求了接口。改为先请求列表页、再请求接口后,数据正常返回。最后,程序依据publishTime与本地已存最大时间比较,只入库新公告,实现了增量采集。

这个案例说明:状态码、Cookie、User-Agent和数据结构解析不是孤立知识点,而是一条完整的采集链路。任何一环缺失,都会表现为“拿不到数据”或“拿到错误数据”。

金融公告增量采集:HTTP请求响应链路 采集器 Session / Cookie User-Agent / Referer 请求报文 GET /api/notice Headers + Cookie 服务器 状态码 / Set-Cookie HTML 或 JSON 响应解析 Content-Type 判断 HTML / JSON 分流 状态码判断 200 / 304 / 403 / 429 重试 / 退避 / 跳过 增量判断 publishTime / noticeId 与本地最大时间比较 入库存储 仅写入新公告 更新采集水位线 核心:状态码定成败,Cookie/UA定身份,Content-Type定解析方式,时间/ID定增量

把上述链路固化为采集器的标准流程:建立会话并设置请求头,先访问列表页获取Cookie,再请求数据接口,依据状态码分流处理,依据Content-Type选择解析器,最后用时间或ID字段做增量过滤。这样,HTTP协议与网页结构就不再是抽象概念,而是可直接落地的采集工程基础。