股票API 日线、周线、月线与分钟级返回结构差异:一份统一解析代码
一、问题:同一套解析代码,换个周期就崩
写行情数据处理脚本时,一个很常见的起点是:先把日线拉下来,用 for row in data: 遍历,取 row["c"] 做后续处理。等需要换成周线、月线或者 5 分钟级,只把 URL 里的级别段换掉,脚本就报错了。
报错位置通常在两处。一是时间字段 t 从 2025-11-27 变成 2025-12-01 14:50:00,原来按 split("-") 取三段再拼日期的写法长度对不上;二是某个周期在指定区间内没有数据,接口照样返回 HTTP 200,但内容是一个空数组 [],脚本「跑通了」却一条记录都没有,而且不报任何错。
本文用几条可以直接点开的请求,把这些差异摊开看一遍,最后给一段把数组、空数组与时间格式统一处理的解析代码。重点只在数据获取与解析,不涉及任何行情判断。
二、先看结果:几条可以在线验证的返回
下面几条地址可以直接点开,浏览器会显示接口实际返回的 JSON。区别只在路径中的「分时级别」这一段:
- 日线:api.mairuiapi.com/hsstock/his...
- 周线:api.mairuiapi.com/hsstock/his...
- 月线:api.mairuiapi.com/hsstock/his...
- 5 分钟级:api.mairuiapi.com/hsstock/his...
紧跟一句说明:hsstock/history 是历史分时路径;000001.SZ 是「股票代码.市场」;d / w / m / 5 分别是分时级别;n 是不复权口径;最后一段是演示证书;st、et 是起止时间,lt=3 指定取最新 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}
再看 5 分钟级数组首条(原样):
json
{"t":"2025-12-01 14:50:00","o":11.67,"h":11.69,"l":11.67,"c":11.69,"v":14831.0,"a":17324939.0,"pc":11.68,"sf":0.0}
两条记录字段集合完全相同,都是 9 个短名字段,差的是 t 的格式和数值量级。也就是说,光看「字段名一样」是不够的,时间格式决定了整个解析入口该怎么写。
三、返回字段逐项说明
本次各级别返回的字段集合一致,都是 9 个。下表以日线首条与 5 分钟级首条为样例:
| 字段 | 实测类型 | 含义(接口文档定义) | 日线样例 | 5 分钟级样例 |
|---|---|---|---|---|
t |
str |
交易时间 | "2025-11-27" |
"2025-12-01 14:50:00" |
o |
float |
开盘价 | 11.66 |
11.67 |
h |
float |
最高价 | 11.71 |
11.69 |
l |
float |
最低价 | 11.58 |
11.67 |
c |
float |
收盘价 | 11.71 |
11.69 |
v |
float |
成交量 | 1011847.0 |
14831.0 |
a |
float |
成交额 | 1178308953.0 |
17324939.0 |
pc |
float |
前收盘价 | 11.69 |
11.68 |
sf |
数值 | 停牌标记:1 停牌、0 不停牌 | 0.0 |
0.0 |
有四点值得单独提醒:
t是字符串,不是时间戳 。日线及以上级别为yyyy-MM-dd,分钟级别为yyyy-MM-dd HH:mm:ss,两者都不要用同一套正则硬切。- 字段集合不随级别变化 。本次
d、w、m、5、15、30、60全部是这 9 个字段,所以字段校验函数可以写成一份,不必为每个周期各写一套。 sf在本次样例中恒为0.0,含义是「不停牌」。这是有值而不是空,别用if not value判断完整性。- 成交量与成交额的量级随周期变化 。日线
v是百万量级,5 分钟级是万量级;把它们拼进同一张表时,必须带上级别标签,否则后续按阈值过滤会把一半数据判成异常。
四、接口结构与级别参数
URL 结构如下:
text
https://api.mairuiapi.com/hsstock/history/{股票代码.市场}/{分时级别}/{除权方式}/{licence}?st=&et=<=
| 位置 | 本次取值 | 说明 |
|---|---|---|
| 股票代码.市场 | 000001.SZ |
六位代码加市场后缀 |
| 分时级别 | d / w / m / y / 5 / 15 / 30 / 60 |
见下表 |
| 除权方式 | n |
日线以上支持 n、f、b、fr、br;分钟级别按文档只用 n |
| licence | 演示证书 | 行为说明见第六节 |
st / et |
20250101 / 20251231 |
起止时间,YYYYMMDD 或 YYYYMMDDhhmmss |
lt |
3 |
取最新的 N 条 |
同一路径下只切换「分时级别」这一段,实测得到的返回差异:
| 级别 | HTTP | 返回条数 | 首条 t |
t 解析后 |
|---|---|---|---|---|
d 日线 |
200 | 3 | 2025-11-27 |
2025-11-27 00:00:00 |
w 周线 |
200 | 3 | 2025-11-16 |
2025-11-16 00:00:00 |
m 月线 |
200 | 3 | 2025-09-30 |
2025-09-30 00:00:00 |
y 年线 |
200 | 0 | 无 | 无 |
5 5 分钟 |
200 | 3 | 2025-12-01 14:50:00 |
2025-12-01 14:50:00 |
15 15 分钟 |
200 | 3 | 2025-12-01 14:30:00 |
2025-12-01 14:30:00 |
30 30 分钟 |
200 | 3 | 2025-12-01 14:00:00 |
2025-12-01 14:00:00 |
60 60 分钟 |
200 | 3 | 2025-12-01 11:30:00 |
2025-12-01 11:30:00 |
从表里能读出三条只关乎结构的结论:
- 日线及以上级别返回纯日期的
t,分钟级别返回带时分秒的t; y在本次区间内返回 HTTP 200 加空数组[],状态码成功但没有记录,这是最容易漏掉的一类情况;- 各级别字段集合一致,差异集中在时间字符串,所以归一化层只需要处理容器与时间格式两件事。
两点补充。其一,级别段写错(例如写成 m5)实测返回 HTTP 404 与一段纯文本,不是空数组,也不是 JSON 错误体;把它和 y 的空数组混为一类处理会漏判。其二,lt 指定的是条数,实测 lt=3 返回 3 条,不带 lt 时同一区间返回 50 条;行数一律以实际响应长度为准,不要写进校验规则。
五、代码实现:把形态与时间统一成行列表
下面这段用 Python 标准库,零第三方依赖,可以直接运行。它做三件事:把 dict / list / 空数组统一成 list[dict]、检查 9 个字段是否齐全、把 t 按两种格式解析成 datetime。
python
import json
from datetime import datetime
from urllib.request import Request, urlopen
LICENCE = "LICENCE-8003-6DCF-E0C78035A455"
BASE = ("https://api.mairuiapi.com/hsstock/history/000001.SZ/"
"{level}/n/" + LICENCE + "?st=20250101&et=20251231<=3")
LEVELS = ("d", "w", "m", "y", "5", "15", "30", "60")
FIELDS = ("t", "o", "h", "l", "c", "v", "a", "pc", "sf")
def fetch(level):
req = Request(BASE.format(level=level),
headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req, timeout=20) as resp:
return resp.status, json.loads(resp.read().decode("utf-8"))
def as_rows(payload):
"""把 dict / list / 空数组统一成 list[dict]。"""
if isinstance(payload, dict):
return [payload]
if isinstance(payload, list):
return payload
raise TypeError("unknown payload: %s" % type(payload).__name__)
def parse_time(value):
"""分钟级带时分秒,日线及以上只有日期。"""
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
return datetime.strptime(value, fmt)
except ValueError:
continue
raise ValueError("bad time field: %r" % value)
for level in LEVELS:
status, payload = fetch(level)
rows = as_rows(payload)
if rows:
missing = [f for f in FIELDS if f not in rows[0]]
t = rows[0]["t"]
parsed = parse_time(t).strftime("%Y-%m-%d %H:%M:%S")
else:
missing, t, parsed = [], "-", "-"
print("%-3s HTTP=%d rows=%-2d t=%-21s parsed=%s missing=%s" % (
level, status, len(rows), t, parsed, ",".join(missing) or "none"))
实测运行输出(原样):
text
d HTTP=200 rows=3 t=2025-11-27 parsed=2025-11-27 00:00:00 missing=none
w HTTP=200 rows=3 t=2025-11-16 parsed=2025-11-16 00:00:00 missing=none
m HTTP=200 rows=3 t=2025-09-30 parsed=2025-09-30 00:00:00 missing=none
y HTTP=200 rows=0 t=- parsed=- missing=none
5 HTTP=200 rows=3 t=2025-12-01 14:50:00 parsed=2025-12-01 14:50:00 missing=none
15 HTTP=200 rows=3 t=2025-12-01 14:30:00 parsed=2025-12-01 14:30:00 missing=none
30 HTTP=200 rows=3 t=2025-12-01 14:00:00 parsed=2025-12-01 14:00:00 missing=none
60 HTTP=200 rows=3 t=2025-12-01 11:30:00 parsed=2025-12-01 11:30:00 missing=none
as_rows() 放在所有解析逻辑的最前面,后面无论是算行数、取首末条还是落库,都只需要面对一种形态。parse_time() 用格式逐个尝试,能同时吃下 2025-11-27 和 2025-12-01 14:50:00,两种格式都失败时再抛错,比按字符串长度硬切更稳。字段检查用 FIELDS 元组做差集,一旦上游增减字段,输出里的 missing= 会立刻显示出来,而不是等到某处 KeyError 才发现问题。
y 那一行 rows=0 也要单独处理:空数组没有首条记录,代码里用 if rows: 分支跳过解析,避免写成 rows[0] 直接抛 IndexError。
六、演示证书的行为说明
关于示例中的证书:上面使用的是演示证书,服务端固定返回 000001 的样例数据,因此替换请求中的股票代码不会改变返回结果。若需要验证其他标的的真实历史数据,需要在请求中使用自己的 licence 参数。演示证书只用于确认接口连通与返回字段结构,不用于获取指定标的的数据。
这一点可以自己点开两条对照,除了股票代码外完全相同:
api.mairuiapi.com/hsstock/his...
api.mairuiapi.com/hsstock/his...
实测结果:两次返回逐字节相同。本次在 d、w、m、5 四个级别上做了同样的对照,结果都相同。换句话说,本文出现的所有数字都来自同一份固定样例,换代码不会带来新数据;第六节之外的字段与结构结论不受影响,但数值本身只能用于说明结构。
七、常见问题与踩坑
把单对象当成数组遍历。 虽然本次各级别都返回数组,但实时行情路径返回的是单个对象;两类路径混用同一段解析时,for row in data 会遍历字典的键。用 as_rows() 包一层,不要靠 try/except 兜。
只判断状态码。 y 返回 [] 且 HTTP 200,if resp.status == 200 之后直接循环会静默执行零次。行数校验必须独立于状态码。
级别段写错当成「无数据」。 m5 实测返回 404 与纯文本,和 y 的 200 空数组是两种结局。前者要按请求错误处理,后者按「成功但无数据」处理,不能合并成同一个分支。
t 的格式随周期变化。 2025-11-27 与 2025-12-01 14:50:00 需要分别解析;若统一按日期存入数据库,注意分钟级的时间部分会被丢掉,最好另存一列保留原始字符串。
不同周期的量级混进一张表。 日线 v 是百万量级,5 分钟级是万量级。落库时把级别写进表结构或元数据,避免后续把不同周期的数据拼在同一列做比较。
按请求参数推算行数。 lt=3 返回 3 条、不带 lt 返回 50 条,都是这次演示返回的表现,不是接口契约。校验函数应当只遍历传入的 rows,不对长度、日期范围做任何硬编码。
分钟级别沿用日线的除权参数。 接口文档说明分钟级别无除权数据、该段只用 n。把这个参数当成「所有周期通用」拼进 URL,得到的不是预期结构,排查时又容易先怀疑数据本身。
八、小结与数据来源说明
差异集中在三处:最外层容器(数组 / 空数组)、t 的时间格式、以及不同周期的数值量级。把这三处在入口处归一化,后面的落库、校验、可视化代码就不用为每个周期各写一套。本文那段 as_rows() + parse_time() 就是最小可用的归一化入口。
本文所有返回样例、字段表、级别对照与代码输出均来自对上述公开接口地址的实际请求,采集时间为 2026-11-12;分时级别与除权方式的取值以当期接口文档定义为准,演示返回的条数与数值是固定样例特征,不是接口契约。
本文只讨论接口返回结构与数据处理,不涉及任何行情判断或投资建议。