股票接口返回 404 与空数组的排查笔记:证书状态与级别参数

股票接口返回 404 与空数组的排查笔记:证书状态与级别参数

一、问题:HTTP 200 不等于拿到了数据

写数据管道时,最常见的错误处理是「看一眼状态码,200 就继续」。这套逻辑在股票数据接口上会漏掉两类情况:一类是 HTTP 层直接报错,客户端抛异常,容易发现;另一类是状态码 200、返回体却是空数组,程序一路跑到底,最后写出一个空文件或一张空表,等到下游发现时已经过了好几个环节。

还有一种更隐蔽的情况:同样是 404,有的返回带错误码的 JSON 对象,有的返回一段纯文本。若客户端无条件 json.loads,第二种会在解析那一行直接抛 ValueError,连错误信息都留不下来。

本文把四种真实返回放在一起对照------404 JSON 错误体、404 纯文本、200 空数组、200 正常数组------给出一段能区分它们的处理代码。全部结论来自实际请求,不涉及行情判断。

二、先看结果:四条可以在线验证的返回

第一条,一枚已失效的演示证书:

https://api.mairuiapi.com/hsrl/ssjy/000001/b997d4403688d5e66a

紧跟一句说明它是什么:hsrl/ssjy 是实时交易路径,000001 是六位股票代码,最后一段是证书串。点开返回的是 HTTP 404 与一个错误对象:

json 复制代码
{"code":404,"message":"Licence证书不存在","licence":"b997d4403688d5e66a"}

第二条 ,把分时级别从 y 换成无效写法 m5:

https://api.mairuiapi.com/hsstock/history/000001.SZ/m5/n/LICENCE-8003-6DCF-E0C78035A455?st=20250101&et=20251231&lt=3

点开返回的是 HTTP 404,但响应体不是 JSON,而是一段纯文本:

text 复制代码
404 page not found

第三条 ,分时级别用 y,在本次区间内没有记录:

https://api.mairuiapi.com/hsstock/history/000001.SZ/y/n/LICENCE-8003-6DCF-E0C78035A455?st=20250101&et=20251231&lt=3

点开返回的是 HTTP 200,但响应体只有:

json 复制代码
[]

第四条,正确的日线级别:

https://api.mairuiapi.com/hsstock/history/000001.SZ/d/n/LICENCE-8003-6DCF-E0C78035A455?st=20250101&et=20251231&lt=3

点开返回的是 HTTP 200 与一个 3 条记录的数组,首条是:

json 复制代码
{"t":"2025-11-27","o":11.66,"h":11.71,"l":11.58,"c":11.71,"v":1011847.0,"a":1178308953.0,"pc":11.69,"sf":0.0}

四条地址只有路径段不同,结果却分属四种完全不同的处理分支:抛异常并读错误体、抛异常但只有纯文本、成功但无数据、成功且有数据。把它们并排看一次,比读十行错误处理说明更直观。

三、返回字段逐项说明

404 JSON 错误体只有三个字段,全部是诊断信息:

字段 类型 实测值 说明
code int 404 与 HTTP 状态码一致
message str "Licence证书不存在" 明确指向证书段
licence str "b997d4403688d5e66a" 回显请求中的证书,便于定位是哪一枚

这个结构很有用:message 已经把问题定位到「证书」,所以排查时不需要去猜是不是代码、周期或者网络的问题。

404 纯文本 没有任何字段,长度 19 字节,内容就是 404 page not found。它不携带错误码,也不能被 JSON 解析器读取,只能由客户端按状态码先分流。

空数组长度为 0,同样是 HTTP 200。它不携带错误码,也不携带提示信息,因此只能由客户端自己判定「成功但无数据」。

正常数组 的 9 个字段分别是 t、o、h、l、c、v、a、pc、sf。其中 t 在日线级别是 2025-11-27 这种纯日期,分钟级别会带时分秒;sf 是停牌标记,实测以 0.0 返回。字段含义以接口文档定义为准。

四、接口结构与参数

两条路径的结构分别是:

text 复制代码
https://api.mairuiapi.com/hsrl/ssjy/{股票代码}/{licence}
https://api.mairuiapi.com/hsstock/history/{股票代码.市场}/{分时级别}/{除权方式}/{licence}?st=&et=&lt=

由此可以把四种结果对应到不同的路径段:

现象 出问题的段 客户端表现
404 + Licence证书不存在 证书段 urlopen 抛 HTTPError,捕获后可读到 JSON 错误体
404 + 纯文本 分时级别段 urlopen 抛 HTTPError,响应体不是 JSON
200 + [] 分时级别与区间组合 无异常,返回体为空数组
200 + 数组 全部正确 正常解析

分时级别按接口文档支持 5、15、30、60、d、w、m、y;本次 y 在 20250101 到 20251231 区间内没有可返回的记录,m5 不在取值表里。除权方式日线以上支持 n、f、b、fr、br,分钟级别按文档只用 n。

一个实际结论:404 是请求本身的问题,重试同一个请求不会改变结果;而 200 空数组是「本次条件下没有记录」,也不是瞬时故障。把两种情况的处置方式写反,要么产生大量无效重试,要么把空结果当成一次正常采集写进库。

五、代码实现:把四种结果分开处理

下面这段代码只依赖标准库,重点在 HTTPError 的捕获、「非 JSON 响应体」的显式判断,以及「空数组」的单独归类。

python 复制代码
import json
from urllib.request import Request, urlopen
from urllib.error import HTTPError

LICENCE = "LICENCE-8003-6DCF-E0C78035A455"
DEAD_LICENCE = "b997d4403688d5e66a"
Q = "?st=20250101&et=20251231&lt=3"
HIS = "https://api.mairuiapi.com/hsstock/history/000001.SZ/"


def fetch(url):
    """统一返回 (status, text),把 HTTPError 也转成可判断的结构。"""
    req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
    try:
        with urlopen(req, timeout=20) as resp:
            return resp.status, resp.read().decode("utf-8")
    except HTTPError as e:
        return e.code, e.read().decode("utf-8")


def classify(status, text):
    try:
        payload = json.loads(text)
    except ValueError:
        payload = None
    if status >= 400 and payload is None:
        return "ERROR_TEXT", "404 非 JSON 纯文本"
    if status >= 400:
        return "ERROR_JSON", "HTTP %d / %s" % (
            status, payload.get("message", ""))
    if isinstance(payload, list) and len(payload) == 0:
        return "EMPTY", "200 但返回空数组"
    if isinstance(payload, list):
        return "ROWS", "%d 条" % len(payload)
    if isinstance(payload, dict):
        return "ONE", "单对象,%d 字段" % len(payload)
    return "UNKNOWN", type(payload).__name__


cases = (
    ("失效演示证", "https://api.mairuiapi.com/hsrl/ssjy/000001/" + DEAD_LICENCE),
    ("年线区间无数据", HIS + "y/n/" + LICENCE + Q),
    ("无效级别 m5", HIS + "m5/n/" + LICENCE + Q),
    ("正常日线", HIS + "d/n/" + LICENCE + Q),
)

for label, url in cases:
    status, text = fetch(url)
    kind, note = classify(status, text)
    print("%-16s %-10s %s" % (label, kind, note))

实测运行输出(原样):

text 复制代码
失效演示证            ERROR_JSON HTTP 404 / Licence证书不存在
年线区间无数据          EMPTY      200 但返回空数组
无效级别 m5          ERROR_TEXT 404 非 JSON 纯文本
正常日线             ROWS       3 条

关键在 classify() 的两个分支:status >= 400 先分流,并且区分「响应体能不能解析成 JSON」;200 + 空数组 再单独归为一类,与「200 + 有数据」区分开。这样上游可以决定是告警、跳过还是继续解析,而不会把空结果当成一次正常采集写进库。

还有一点容易忽略:fetch() 里 HTTPError 必须调用 e.read() 才能拿到响应体。若不读取,日志里只剩一个状态码,错误体里的 message 就丢了。

六、演示证书的行为说明

用当前有效的演示证书做一组对照,只把股票代码从 000001.SZ 换成 600519.SH:

实测两次返回逐字节相同 ;把级别换成 w、m、5 重复对照,结果同样逐字节相同。

示例使用的是演示证书,服务端固定返回 000001 的样例数据,因此替换请求中的股票代码不会改变返回结果。演示证书的作用只有两个:确认接口连通、看清返回字段结构。若需要验证其他标的的真实历史数据,需要在请求中使用自己的 licence 参数。

顺带说明本文第一条地址为什么会 404:那是一枚早期公开过的演示证书,实测已返回「Licence证书不存在」。公开演示证书会轮换,写在文章或示例代码里的串需要定期复测,不能假定长期有效;本文第一条地址是刻意保留的反例,用于展示错误体结构,不是可用的体验入口。

七、常见问题与踩坑

只判断状态码。 if resp.status == 200 之后直接 for row in payload,遇到 [] 会静默执行零次循环。行数校验必须独立于状态码。

无条件解析 JSON。 无效级别返回的是纯文本 404,json.loads 会抛 ValueError。先按状态码分流,再用 try/except ValueError 兜住非 JSON 响应体。

不捕获 HTTPError。 用 urllib 时 404 会抛异常,若不捕获,错误体里的 message、licence 就读不到。用 requests 时则相反------404 不抛异常,需要主动判断 status_code,两套客户端行为不同。

对 404 做重试。 证书状态与无效路径段都不是瞬时故障,重试不会改变结果,只会放大请求量。正确处置是修正请求参数并告警。

把「无数据」和「失败」混成同一个结局。 采集任务通常要区分三种结局:成功且有数据、成功但无数据、请求失败。第二种不一定需要重试;若把前两者都记为失败,会产生大量无效重试;都记为成功,又会让空表静默通过。建议在任务结果里保留独立的 row_count 字段,让调度层据此决定下一步动作。

沿用旧文章里的证书串。 公开演示证书会轮换,复制旧代码前应当把 URL 逐条实测一次,确认返回 200 且结构符合预期。

把错误体里的证书串原样写进日志。 licence 字段回显的是请求里的证书值,用于定位是哪一枚出了问题;日志里保留错误码与 message 就足够排查,没必要把整串完整落盘。

把空数组写成空表还提交。 落库前加一道「本次采集行数大于 0」的检查,并保留上一次成功结果,避免一次空返回把已有数据覆盖掉。

只在开发时验证过一次就上线。 演示证书会轮换,级别参数也可能调整。上线前把文中每条 URL 逐条实测一遍;错误分支也不要只靠模拟数据测试,用一条确实会 404 的地址跑通一次,才能确认 HTTPError 捕获与错误体解析真的生效。

八、小结与数据来源说明

四类结果对应四套处置:404 JSON 错误体要读 message 定位到证书段并停止重试;404 纯文本要先按状态码分流、不做 JSON 解析;200 空数组要独立归类为「无数据」;200 正常数组才进入解析链。把「状态码正确」与「确实拿到数据」拆成两个判断,是这一层最值得补的一行代码。

本文所有返回样例、错误体、状态码与代码输出均来自对上述公开接口地址的实际请求,采集时间为 2026-11-12。第一条 404 地址是本文刻意保留的反例,用于展示错误体结构,不是可用的体验入口;分时级别与除权方式的取值以当期接口文档为准。

本文只讨论接口返回结构与错误处理,不涉及任何行情判断或投资建议。

相关推荐
nice_lcj52013 分钟前
MySQL 存储引擎入门:InnoDB 和 MyISAM 到底怎么选
java·数据库·mysql·面试·innodb·存储引擎·myisam
蒸蒸yyyyzwd18 分钟前
从邓明学习的后端笔记
笔记·学习
文丑颜不良啊29 分钟前
LeetCode2861~2880题目解析
java·leetcode
2601_9679801134 分钟前
世界上最好的Python编辑器是什么?我投PyCharm一票
ide·python·pycharm·数据科学·jupyter notebook
岑梓铭39 分钟前
《考研408数据结构》第八章(排序)复习笔记
数据结构·笔记
装不满的克莱因瓶40 分钟前
设计模式之策略模式核心概念和应用场景
java·后端·设计模式·架构·系统架构·策略模式
晴天的雨.9921 小时前
【C++】继承篇
java·开发语言·c++
SomeOtherTime1 小时前
第 1 讲:内存布局与所有权解剖——栈帧、胖指针与“移动/拷贝”在汇编层面的真实面目
java·开发语言·汇编
wuyk5551 小时前
Spider-第09讲-三格式落盘CSV-Excel-JSON
爬虫·python·json·excel