股票API 日线、周线、月线与分钟级返回结构差异:一份统一解析代码

股票API 日线、周线、月线与分钟级返回结构差异:一份统一解析代码

一、问题:同一套解析代码,换个周期就崩

写行情数据处理脚本时,一个很常见的起点是:先把日线拉下来,用 for row in data: 遍历,取 row["c"] 做后续处理。等需要换成周线、月线或者 5 分钟级,只把 URL 里的级别段换掉,脚本就报错了。

报错位置通常在两处。一是时间字段 t 从 2025-11-27 变成 2025-12-01 14:50:00,原来按 split("-") 取三段再拼日期的写法长度对不上;二是某个周期在指定区间内没有数据,接口照样返回 HTTP 200,但内容是一个空数组 [],脚本「跑通了」却一条记录都没有,而且不报任何错。

本文用几条可以直接点开的请求,把这些差异摊开看一遍,最后给一段把数组、空数组与时间格式统一处理的解析代码。重点只在数据获取与解析,不涉及任何行情判断。

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

下面几条地址可以直接点开,浏览器会显示接口实际返回的 JSON。区别只在路径中的「分时级别」这一段:

紧跟一句说明: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

有四点值得单独提醒:

  1. t 是字符串,不是时间戳 。日线及以上级别为 yyyy-MM-dd,分钟级别为 yyyy-MM-dd HH:mm:ss,两者都不要用同一套正则硬切。
  2. 字段集合不随级别变化 。本次 d、w、m、5、15、30、60 全部是这 9 个字段,所以字段校验函数可以写成一份,不必为每个周期各写一套。
  3. sf 在本次样例中恒为 0.0 ,含义是「不停牌」。这是有值而不是空,别用 if not value 判断完整性。
  4. 成交量与成交额的量级随周期变化 。日线 v 是百万量级,5 分钟级是万量级;把它们拼进同一张表时,必须带上级别标签,否则后续按阈值过滤会把一半数据判成异常。

四、接口结构与级别参数

URL 结构如下:

text 复制代码
https://api.mairuiapi.com/hsstock/history/{股票代码.市场}/{分时级别}/{除权方式}/{licence}?st=&et=&lt=
位置 本次取值 说明
股票代码.市场 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

从表里能读出三条只关乎结构的结论:

  1. 日线及以上级别返回纯日期的 t,分钟级别返回带时分秒的 t;
  2. y 在本次区间内返回 HTTP 200 加空数组 [],状态码成功但没有记录,这是最容易漏掉的一类情况;
  3. 各级别字段集合一致,差异集中在时间字符串,所以归一化层只需要处理容器与时间格式两件事。

两点补充。其一,级别段写错(例如写成 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&lt=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;分时级别与除权方式的取值以当期接口文档定义为准,演示返回的条数与数值是固定样例特征,不是接口契约。

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

相关推荐
这个DBA有点耶10 分钟前
MCP隔离层+行级语义审计+输出校验:AI访问数据库的安全边界拆解
数据库·人工智能·ai编程
IT_陈寒18 分钟前
Vue的watch把我坑惨了,原来要这样用才靠谱
前端·人工智能·后端
`流年づ25 分钟前
策略梯度(Policy Gradient)
人工智能
疯华整猫39 分钟前
删掉 LeakCanary 那天,我用 Trae 自己撸了一套:最难的不是分析堆,是决定什么时候 dump
android·人工智能·性能优化
慢慢来不要急1 小时前
Anthropic 弃用 Git 自建源码控制系统 · 11-11
人工智能·开发工具
用户别睡觉1 小时前
有时候放弃也是一种收获
人工智能·github
shengjk11 小时前
从一条 Bash 命令看懂 LLM Agent:为什么大模型会执行命令?
人工智能
Yuzhiyuxia2 小时前
【AI Agent面试系列】AI Agent可观测、评测、安全相关的知识点
人工智能·安全·面试
dozenyaoyida2 小时前
AI与大模型新闻日报 | 2026-11-11
人工智能·chatgpt·大模型·新闻