3. Python核心实现:字典存储快照、增量数据解析、合并算法实现
好,咱们直接进入正题。前面聊了那么多理论,现在该动手了。
我个人习惯用Python来做行情数据的处理,原因很简单:开发快,调试方便,而且字典(dict)这种数据结构简直就是为快照设计的。你想想看,行情快照本质上就是一堆键值对——股票代码对应价格、数量、时间戳。这不就是字典吗?
3.1 用字典存储快照——为什么选它?
我在项目中遇到过好几次这样的场景:刚开始用列表存快照,结果每次查找某个股票的数据都要遍历一遍,数据量一上来,延迟直接爆炸。后来换成字典,查找复杂度从O(n)降到了O(1),效果立竿见影。
快照字典的结构其实很简单:
# 快照字典的基本结构
snapshot = {
"timestamp": 1628000000000, # 快照时间戳
"bids": { # 买单
"600519": {"price": 2050.0, "volume": 100},
"000858": {"price": 198.5, "volume": 200}
},
"asks": { # 卖单
"600519": {"price": 2051.0, "volume": 150},
"000858": {"price": 199.0, "volume": 180}
}
}
嗯,这里要注意:快照字典的key一定要用字符串。我见过有人用整数存股票代码,结果遇到"000001"这种代码,整数直接变成1,数据全乱了。血的教训。
核心原则:快照字典的key必须是不可变类型(字符串、元组),千万别用列表或自定义对象。
3.2 增量数据解析——别被格式吓到
增量数据长什么样?说白了就是告诉你「刚才那个快照里,哪些东西变了」。常见的格式有两种:
| 格式类型 | 特点 | 解析难度 |
|---|---|---|
| JSON增量 | 字段名清晰,可读性好 | 低 |
| 二进制增量 | 体积小,速度快 | 高 |
我曾经接手过一个项目,增量数据是二进制格式的,文档还写得不清不楚。当时我花了整整两天才把解析逻辑调通。所以这里我建议:如果条件允许,优先用JSON格式做增量,等系统稳定了再考虑二进制优化。
来看一个典型的增量数据解析代码:
def parse_incremental(data: dict) -> dict:
"""
解析增量数据
返回格式:{"action": "update/delete", "key": "600519", "value": {...}}
"""
result = {}
# 解析操作类型
action = data.get("action", "update")
# 解析股票代码
code = data.get("code", "")
if not code:
raise ValueError("增量数据缺少股票代码")
# 解析具体字段
if action == "update":
result = {
"action": "update",
"key": code,
"value": {
"price": data.get("price"),
"volume": data.get("volume"),
"side": data.get("side") # "bid" 或 "ask"
}
}
elif action == "delete":
result = {
"action": "delete",
"key": code,
"value": None
}
return result
小技巧:解析增量数据时,记得加一个版本号字段。我遇到过好几次因为数据顺序错乱导致合并结果不对,加了版本号之后,直接按版本号排序再合并,问题就解决了。
3.3 合并算法实现——核心中的核心
好了,快照有了,增量也解析好了,接下来就是合并。这个算法其实不复杂,但细节决定成败。
合并的核心逻辑就三步:
- 定位:在快照字典中找到要修改的股票
- 应用:根据增量操作类型(增/删/改)更新字典
- 校验:检查合并后的数据是否合理
直接上代码,这是我个人比较喜欢的实现方式:
def merge_snapshot(snapshot: dict, incremental: dict) -> dict:
"""
将增量数据合并到快照中
"""
# 深拷贝快照,避免修改原始数据
merged = copy.deepcopy(snapshot)
action = incremental["action"]
code = incremental["key"]
value = incremental["value"]
if action == "update":
# 更新操作:直接覆盖或新增
side = value.pop("side", "bid")
if side == "bid":
merged["bids"][code] = value
else:
merged["asks"][code] = value
elif action == "delete":
# 删除操作:从字典中移除
merged["bids"].pop(code, None)
merged["asks"].pop(code, None)
# 更新快照时间戳
merged["timestamp"] = incremental.get("timestamp", merged["timestamp"])
return merged
注意:合并时一定要做深拷贝!我刚开始做的时候图省事直接赋值,结果增量数据把原始快照也给改了,排查了半天才发现是引用传递的问题。
3.4 完整流程——一张图看懂
说了这么多,咱们用一张流程图把整个逻辑串起来:
这张图把整个流程讲得很清楚了。从初始快照开始,每来一条增量数据,就解析、判断、合并,最后输出新的快照。循环往复,这就是行情数据实时更新的本质。
3.5 避坑指南——我踩过的那些坑
最后分享几个实战中容易翻车的地方:
- 并发问题:多线程环境下,快照字典的读写要加锁。我曾经因为没加锁,导致两个线程同时修改同一个key,数据直接乱掉。
- 内存泄漏:快照字典会越来越大吗?会的。如果某些股票退市了,记得从字典里删掉,不然内存迟早爆掉。
- 时间戳对齐:增量数据的时间戳可能比快照还早。这种情况我一般直接丢弃,因为旧数据没有意义。
总结一下:字典存快照、解析增量、合并算法,这三步是行情数据处理的基础。代码量不大,但细节很多。多写几个单元测试,把边界情况都覆盖到,上线之后能省不少心。