Python解析同花顺day文件:定长二进制记录与复权处理实战 简介这份资源面向金融数据分析初学者与量化爱好者聚焦同花顺日线二进制文件.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 f20{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, accessmmap.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 recordsmmap的好处是把文件 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 fhistory/{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:ichunk_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 股、港股、指数生成的文件字段布局可能有细微差异。我一般会先写一个探测脚本读前几条记录打印出每个字段的原始值和已知行情做比对确认格式假设成立后再批量跑。这个习惯帮我省掉了很多次「跑了几千个文件才发现格式不对」的返工。从那以后我每次拿到一批新的二进制行情文件都会先强制走一遍「单文件、单记录、逐字段人工核对」的流程确认无误再写批量逻辑。希望这份解析思路和踩坑记录能帮你在处理同花顺日线数据时少走几个弯路。本文还有配套的精品资源点击获取