☰
Python解析同花顺day文件:定长二进制记录与复权处理实战
2026/10/23 21:06:55 网站建设 项目流程

简介:这份资源面向金融数据分析初学者与量化爱好者,聚焦同花顺日线二进制文件(.day)的解析方法,帮助读者掌握从原始字节流中提取股票历史行情的关键技能。压缩包共2个文件,包含1个day格式的示例数据文件与1个Python脚本,整体约4KB,其中day文件用于提供真实股票日线样本,py脚本则演示完整的解析流程。目前已有2258人学习下载,说明该主题在实战中具有较高关注度。通过阅读脚本,读者可以理解如何以二进制模式打开文件、按固定长度记录逐条读取、解析日期与开高低收及成交量成交额字段,并将结果转换为可分析的数据结构。代码仅约20行,简洁高效,适合作为二进制文件解析的入门范例,也可在此基础上扩展批量处理与错误处理逻辑,为后续股票数据分析打下基础。

1. 同花顺日线文件解析:为什么一个 20 行的 Python 脚本能撬动整套行情数据

很多人第一次拿到同花顺的日线数据目录时,会下意识觉得这东西得靠官方接口或者付费终端才能读。但真实情况恰恰相反:那些躺在history目录下、以股票代码命名、扩展名为.day的文件,本质上就是一段段定长二进制记录,没有任何加密,也没有任何压缩。你甚至不需要装同花顺客户端,只要知道每条记录多少字节、每个字段偏移在哪,用 Python 的struct模块二十行左右就能把开高低收、成交量、成交额全部拆出来。这件事值得搞懂,不是因为脚本短,而是因为它逼着你去理解一个更底层的问题:行情软件到底是怎么把「一天」这件事持久化到磁盘上的。搞懂了.day的布局,你再看任何一款行情软件的本地数据格式,都会有一种「原来都是同一套思路」的顿悟感。这也是为什么「python 20行 day文件」这个组合会被反复搜索——大家想要的不是代码,而是那个「为什么能这么简单」的答案。

2. 定长二进制记录:.day 文件为什么不用数据库也能被快速读取

2.1 从「一天一条记录」到「一条记录 32 字节」

要理解.day文件,先要接受一个设计前提:行情数据是典型的「按时间追加、按时间顺序读取」的场景。它几乎不需要随机更新历史某一天,也不需要复杂查询。这种访问模式决定了它没必要上数据库,直接用定长记录的顺序文件反而更快——因为读取第 N 天的数据,只需要把文件指针移到N * 记录长度的位置,读固定字节数即可,时间复杂度是 O(1)。

同花顺日线文件的记录长度是 32 字节,每条记录对应一个交易日。这个 32 不是随便定的,它是把当天所有关键行情字段塞进固定宽度后的结果。常见的字段布局是这样的:日期用 4 字节整数表示,格式为YYYYMMDD;开盘、最高、最低、收盘各占 4 字节,用整数存储,单位是「分」;成交量占 4 字节,单位是「股」;成交额占 4 字节,单位是「元」;最后还有 4 字节保留或用于其他标记。加起来正好 32 字节。

这里有个容易被忽略的细节:价格为什么用整数存「分」,而不是浮点数?因为浮点数在不同平台上的二进制表示可能有细微差异,而行情数据要求跨平台、跨版本读取结果完全一致。用整数存最小货币单位,是金融数据持久化的通用做法,读取时再除以 100 还原成元即可。这个取舍你在很多交易系统里都能看到。

2.2 字节序与 struct 的解包逻辑

知道了字段和宽度,接下来就是怎么把它们从字节流里「切」出来。Python 的struct模块就是干这个的。下面这段代码展示了读取单条记录的核心逻辑:

import struct # 记录格式:日期(4字节) + 开盘(4) + 最高(4) + 最低(4) + 收盘(4) + 成交量(4) + 成交额(4) + 保留(4) # 全部按小端无符号整数解析,价格单位为分 RECORD_FORMAT = '<IIIIIIII' RECORD_SIZE = struct.calcsize(RECORD_FORMAT) # 结果为 32 def parse_one_record(raw_bytes): fields = struct.unpack(RECORD_FORMAT, raw_bytes) date_int, open_p, high_p, low_p, close_p, volume, amount, _ = fields return { 'date': str(date_int), # 20240115 这样的整数转字符串 'open': open_p / 100.0, # 分转元 'high': high_p / 100.0, 'low': low_p / 100.0, 'close': close_p / 100.0, 'volume': volume, # 单位:股 'amount': amount, # 单位:元 }

<IIIIIIII里的<表示小端字节序,后面的八个I表示八个 4 字节无符号整数。struct.calcsize算出来是 32,和文件里每条记录的长度对得上,这就是验证格式假设是否正确的第一道关卡。如果你算出来不是 32,那说明字段数量或类型判断错了,后面读出来的全是乱码。

读取整个文件时,只要按 32 字节循环切片即可:

def read_day_file(path): records = [] with open(path, 'rb') as f: while True: chunk = f.read(RECORD_SIZE) if len(chunk) < RECORD_SIZE: break # 文件末尾不足一条记录,正常结束 records.append(parse_one_record(chunk)) return records

这段逻辑里有个关键判断:len(chunk) < RECORD_SIZE时退出。因为定长文件在极端情况下可能被截断,如果不做这个判断,struct.unpack会直接抛异常。这是实际解析时最常见的翻车点之一。

2.3 为什么不用 pandas 直接 read_csv 那套思路

有人会问,既然都是结构化数据,为什么不直接当成 CSV 读?因为.day是二进制,不是文本。文本格式每条记录长度不固定,解析器需要逐字符扫描分隔符;而二进制定长记录可以直接用偏移量定位,读取速度差一个数量级。对于动辄几千只股票、每只几百到几千个交易日的数据量,这个差异在批量处理时非常明显。这也是为什么行情软件普遍选择二进制定长格式,而不是文本格式。

3. 日期编码与复权处理:解析出来之后数据为什么还对不上

3.1 日期字段的整数编码与边界情况

日期存成20240115这样的整数,好处是排序和比较都直接按数值来,不需要转成日期对象。但解析时要注意两点:一是这个整数没有前导零,20240105存进去就是20240105,读出来也是这个数,转字符串时不会变成202415;二是早期数据可能存在日期格式不一致的情况,比如某些老文件用YYYYMMDD但年份只有两位。实际处理时,稳妥的做法是先转成字符串,再按长度判断格式:

def normalize_date(date_int): s = str(date_int) if len(s) == 8: return f"{s[:4]}-{s[4:6]}-{s[6:]}" elif len(s) == 6: # 两位年份的情况,按 2000 年后处理 return f"20{s[:2]}-{s[2:4]}-{s[4:]}" else: raise ValueError(f"无法识别的日期编码: {date_int}")

这个函数看起来简单,但它解决的是实际解析中最容易埋雷的地方。很多人在自己电脑上跑通了,换一批数据就报错,原因往往就是日期字段的编码假设太死。

3.2 复权:为什么解析出的价格和行情软件显示的不一样

这是.day文件解析里最大的认知门槛。你从文件里读出来的开盘、收盘价,是不复权的原始价格。而行情软件默认显示的是前复权价格。两者在除权除息日之后会出现明显差异。举个例子,某只股票在 2024 年 6 月 10 日每 10 股送 10 股,那么 6 月 10 日之后的不复权价格会直接腰斩,但前复权价格会把历史价格按比例下调,使得 K 线看起来连续。

.day文件本身通常只存不复权数据,复权因子需要另外从财务数据或除权除息记录里计算。这意味着,如果你只解析.day文件,得到的是一套「真实成交价」,而不是「可比价格」。做回测时如果用不复权价格,遇到除权日会产生虚假的巨额亏损或收益,这是量化新手最常踩的坑之一。

常见的处理思路是:解析出不复权数据后,再根据除权除息事件表计算复权因子,对历史价格做调整。复权因子的计算逻辑大致是:

def adjust_factor(prev_close, split_ratio, dividend): # prev_close: 除权前一日收盘价 # split_ratio: 送转比例,如 10 送 10 则为 2.0 # dividend: 每股现金分红 return (prev_close * split_ratio + dividend) / prev_close

这个因子用来调整除权日之前的所有价格。具体实现时还要区分前复权、后复权、定点复权,不同场景用的不一样。回测一般用前复权,因为要保证最新价格和实际盘面一致;而做长期收益分析时,后复权更能反映真实累计收益。

3.3 成交额与成交量的单位陷阱

成交量字段存的是「股」,但行情软件上显示的往往是「手」,1 手等于 100 股。成交额存的是「元」,但有些数据源会存成「千元」或「万元」。如果你解析出来的成交额和软件上差 100 倍或 10000 倍,先别怀疑代码,去核对一下单位定义。同花顺的.day文件里,成交额通常是元,成交量是股,但不同版本或不同市场(比如指数和个股)可能有差异。稳妥的做法是拿一只你熟悉的股票,取某一天的解析结果和软件显示值做交叉验证,确认单位后再批量处理。

4. 批量解析与性能:几千个 .day 文件怎么在几秒内读完

4.1 用内存映射替代逐次 read

单个.day文件通常只有几十到几百 KB,逐次read完全够用。但当你需要批量处理几千只股票时,频繁的系统调用和文件打开关闭会成为瓶颈。一个常见的优化是用mmap把文件映射到内存,然后直接按偏移量切片:

import mmap import struct def read_day_file_mmap(path): with open(path, 'rb') as f: with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm: n = len(mm) // RECORD_SIZE records = [] for i in range(n): offset = i * RECORD_SIZE chunk = mm[offset:offset + RECORD_SIZE] records.append(parse_one_record(chunk)) return records

mmap的好处是把文件 I/O 交给操作系统按页调度,避免了用户态和内核态之间反复拷贝。对于批量读取场景,这个差异在数据量大时能省下可观的等待时间。不过要注意,mmap在文件被其他进程写入时行为不确定,所以只适合读取静态的历史数据文件。

4.2 多进程并行与 GIL 的取舍

Python 的全局解释器锁决定了多线程在 CPU 密集型任务上无法真正并行。解析.day文件是典型的 CPU 密集型操作(大量struct.unpack和循环),所以用多进程比多线程更合适。常见的做法是把股票代码列表切成若干份,每份交给一个进程处理:

from multiprocessing import Pool def parse_batch(codes): result = {} for code in codes: path = f"history/{code}.day" result[code] = read_day_file(path) return result if __name__ == '__main__': all_codes = [...] # 股票代码列表 chunk_size = len(all_codes) // 4 chunks = [all_codes[i:i+chunk_size] for i in range(0, len(all_codes), chunk_size)] with Pool(4) as pool: results = pool.map(parse_batch, chunks)

进程数一般设成 CPU 核心数,不要盲目开太多,否则进程切换的开销会抵消并行收益。另外,每个进程返回的数据量如果很大,进程间通信的序列化成本也会上升,这时候可以考虑让每个进程直接把结果写到独立的临时文件,主进程只负责汇总。

4.3 解析结果的存储选择

解析出来的数据如果只是临时用,直接放内存里的字典或列表就行。但如果要反复查询,建议转成 Parquet 或 SQLite。Parquet 的列式存储对行情数据的按列读取非常友好,压缩率也高;SQLite 则胜在零配置、单文件、支持 SQL 查询。选择哪个取决于你的下游用途:做回测框架通常偏好 Parquet,做交互式查询偏好 SQLite。不建议存回 CSV,因为浮点精度和日期格式在文本往返中容易出问题。

5. 避坑与排查:解析 .day 文件时最常见的五类翻车

5.1 现象:读出来的价格是天文数字或接近零

原因通常是字节序搞反了。.day文件是小端存储,如果你用了>大端格式去解,四个字节会被完全错误地重组,得到一个毫无意义的巨大整数。解决方法是确认struct格式字符串以<开头,并且用一只已知价格的股票做验证。

5.2 现象:文件读到一半抛 struct.error

原因是文件末尾有不足 32 字节的残留数据,可能是写入过程中断或文件被截断。解决方法是在循环里判断len(chunk) < RECORD_SIZE时直接 break,而不是无条件 unpack。如果残留数据很多,说明文件本身损坏,需要重新获取。

5.3 现象:日期解析出来是 1970 年或 1900 年

原因是把日期字段当成了 Unix 时间戳去转换。.day里的日期是YYYYMMDD整数,不是时间戳。不要用datetime.fromtimestamp,直接按字符串切分即可。另一个可能是字段偏移算错了,把成交量当成了日期。

5.4 现象:成交量和软件显示差 100 倍

原因是单位没统一。文件里是股,软件显示是手。如果你要和软件对齐,记得除以 100。成交额同理,确认是元还是千元。这个坑在跨数据源对比时尤其常见。

5.5 现象:批量解析时内存暴涨

原因是把所有股票的所有历史数据一次性加载到内存。几千只股票乘以几千个交易日,数据量很容易到 GB 级别。解决方法是分批处理,或者解析完一只就落盘一只,不要全部攒在内存里。用生成器逐条 yield 而不是返回完整列表,也能显著降低内存峰值。

6. 从定长记录到数据管线:解析只是起点,不是终点

把.day文件解析出来,本质上只是把磁盘上的字节变成了内存里的结构化字段。真正决定这套数据有没有用的,是解析之后你怎么组织它。我自己的习惯是,解析层只负责「字节到字典」这一件事,不做任何复权、不做任何单位转换之外的加工。复权、对齐、填充缺失交易日,全部放到独立的处理层。这样做的原因是,解析层的正确性可以用「和软件显示值逐字段对比」来验证,而处理层的逻辑会随策略变化,混在一起会让排查变得极其困难。

另一个值得思考的取舍是:要不要自己解析,还是直接用现成的数据接口。自己解析的优势是零依赖、离线可用、数据完全可控;劣势是复权因子、财务数据、停牌标记这些周边信息需要自己维护。如果你的需求只是读历史 K 线做简单分析,自己解析.day文件是性价比最高的路径;如果你需要完整的财务和事件数据,那解析.day只是整个数据管线里很小的一环。

还有一个容易被忽视的点:.day文件的格式并非铁板一块。不同版本的同花顺客户端、不同市场(A 股、港股、指数)生成的文件,字段布局可能有细微差异。我一般会先写一个探测脚本,读前几条记录,打印出每个字段的原始值,和已知行情做比对,确认格式假设成立后再批量跑。这个习惯帮我省掉了很多次「跑了几千个文件才发现格式不对」的返工。

从那以后,我每次拿到一批新的二进制行情文件,都会先强制走一遍「单文件、单记录、逐字段人工核对」的流程,确认无误再写批量逻辑。希望这份解析思路和踩坑记录,能帮你在处理同花顺日线数据时少走几个弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询