ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

通达信本地数据解析:Python读取dat与blk文件实战

通达信本地数据解析:Python读取dat与blk文件实战 做量化分析和数据整理的人迟早会遇到这么一件事通达信里攒了一堆自选股和板块数据但软件本身不提供批量导出接口你想用 Python 拉出日线、按板块做统计只能去读它本地落盘的文件。dat和blk就是绕不开的两个文件类型前者是行情序列比如日线数据后者是板块定义比如你自己建的“半导体”“白酒”这类分组。这篇文章我把解析这两个文件的过程完整写出来附可运行的 Python 源码适合想折腾本地行情数据、又不想过度依赖第三方接口的读者。整套代码我控制在只用 Python 标准库struct加pathlib就够了不需要装 pandas、numpy 也能跑。你只要能跑 Python 3.6 以上就行。我用这种方法做过板块批量统计、自选股跨设备备份、把日线数据灌进自己的小数据库实测下来很稳。重要的是读完你会发现通达信本地文件没有想象中神秘也就是二进制格式和文本格式的区别而已。1. 为什么非要啃本地文件先弄清 dat、blk 的底细1.1 一个能做的事提前说清楚举个具体场景。我建了一个“光伏设备”板块里面有十几只股票想统计这些股票最近半年的平均涨幅。如果用通达信自带功能得一只一只翻如果去问行情接口又要处理限频、鉴权、字段对不上这些破事。但换个思路板块名单其实就在.blk文件里每只股票的日线数据就在vipdoc目录下的.dat文件里两个文件都读一遍用 Python 几秒钟就能算完。这就是本地解析的典型用途不需要联网不需要接口 token只要通达信自己还在正常下载行情你就能拿到它所有历史数据。对于个人研究来说这个数据量级完全够用。比如一张存了十年的日线 .dat 文件记录数也就两千多条每条 32 字节整个文件不到 100KB解析起来几乎是无感的。1.2 通达信的数据目录长什么样要解析文件第一步是找到它们。通达信安装目录下有这些关键路径vipdoc\sh\lday\sh600000.dat上海市场日线数据文件名是市场前缀加股票代码vipdoc\sz\lday\sz000001.dat深圳市场日线数据vipdoc\bj\lday\bj430047.dat北交所市场日线数据T0002\blocknew\*.blk用户自定义板块比如自选股、你自己建的分类T0002\block\*.blk系统板块通常是通达信自带的行业、概念板块这里有个容易混淆的点dat文件不是按股票代码简单堆在一起的它按照沪深北分目录存放文件名自带市场前缀。blk文件则是纯文本里面记录的是 6 位股票代码和市场前缀没关系。所以解析时你会发现一个天然的分工blk告诉你有哪些股票dat告诉你这些股票的行情两者通过 6 位代码做关联。1.3 本地解析和现成第三方库怎么选网上能搜到 pytdx、mootdx 这类专门解析通达信数据的库它们功能确实强大能直接读本地文件甚至模拟客户端协议。但我的建议是如果只是个人研究先自己写个几十行的解析器比直接上库更有价值。原因有三点。第一通达信本地文件格式很固定自己解析花不了多少时间还能彻底搞懂数据结构第二第三方库为了兼容各种版本往往会包含大量你不需要的代码出了问题排查更麻烦第三本地解析没有任何接口限制代码拿过来就能跑也不会因为库版本更新导致接口变化。当然如果你的目标是做类似“通达信实时行情”这种高频应用那用现成库没毛病但如果是做日线级别的数据整理和统计自己动手更可控。2. 动手前的关键准备格式、编码、代码段规则2.1 日线 dat 文件的 32 字节格式通达信日线.dat文件最经典的结构是定长记录每条记录固定 32 字节小端字节序。字段排列顺序是这样的偏移字节数类型说明04uint32日期整数格式 YYYYMMDD如 2023122944float32开盘价84float32最高价124float32最低价164float32收盘价204float32成交额单位元244uint32成交量单位股284uint32保留字段部分版本可能存涨跌额或前收用 Python 的struct模块来解格式串是IfffffII表示小端I是无符号 32 位整数f是单精度浮点数。这个格式串算下来正好 4 5×4 2×4 32 字节。这里要提醒一下我见过网上有些文章把成交额和成交量顺序写反了。如果你解析出来的数据里volume字段带小数或者amount是个看起来像手数的整数那基本可以断定顺序不对。后面第 4 章我会专门讲怎么验证。2.2 blk 板块文件其实是纯文本blk文件没有二进制那么复杂本质就是文本文件每行记录一个股票代码。但因为它可能是不同通达信版本、不同电脑导出的实际格式有点随缘最常见一行一个 6 位数字比如600000带名称可能是600000 浦发银行或者600000|浦发银行带前缀有些版本会直接写成SH600000之类的全格式代码空行、注释行偶尔出现空行直接跳过即可另外编码方面老版本多用 GBK新版本可能是 UTF-8所以解析时不能用固定的open(file, encodingutf-8)最好先读二进制字节再用gbk解码如果出现乱码再尝试utf-8。最保险的做法是根本不依赖解码后的文字内容而是用正则表达式提取每一行里连续出现的 6 位数字这样哪怕一行里混着中文名字也能正确拿到代码。2.3 6 位代码与沪深北市场的对应拿到 6 位代码后还需要判断它属于哪个市场才能定位到vipdoc下对应的dat文件。规则并不复杂按第一位判断第一位是6上海主板、科创板前缀sh第一位是5上海基金前缀sh第一位是0、1、2、3深圳主板、中小板、创业板、深圳基金前缀sz第一位是4、8北交所、新三板前缀bj第一位是9上海 B 股前缀sh这个规则覆盖了 95% 以上的情况。有个特例是上证指数代码是999999在通达信本地文件里它也存在vipdoc\sh\lday\sh999999.dat但它的成交量和成交额含义跟个股不完全一样做统计时要注意过滤。2.4 环境准备与目标目录确认代码只依赖标准库所以环境准备非常简单。如果你电脑上还没有 Python装一个 3.8 以上的版本就行安装时记得勾选“Add Python to PATH”这个步骤能省掉后面一堆路径问题。不需要配虚拟环境不需要 pip install 任何东西。目录方面你需要先找到通达信安装目录。常见的有C:\new_tdx、D:\new_tdx、D:\zd_zsone这些。拿不准的话在通达信图标上右键查看“打开文件所在位置”得到的文件夹就是安装目录。脚本运行时会让输入这个路径也可以直接把路径写在代码开头的常量里。3. 源码实现从解析单文件到批量扫板块3.1 解析 dat 文件的核心函数我先把解析日线dat文件的函数单独拎出来。这个函数接受一个文件路径返回列表列表里每个元素是一天的行情字典。import struct from pathlib import Path # 单条日线记录 32 字节 # 格式: 日期(uint32) 开/高/低/收/成交额(5*float32) 成交量(uint32) 保留(uint32) DAY_STRUCT struct.Struct(IfffffII) DAY_SIZE DAY_STRUCT.size # 32 def parse_day_file(path): 解析通达信日线 .dat 文件 返回按时间升序排列的记录列表 path Path(path) raw path.read_bytes() records [] # 用总字节数整除单条记录大小得到记录条数 # 比 while 循环更简洁也避免越界 count len(raw) // DAY_SIZE for i in range(count): offset i * DAY_SIZE date_int, open_, high, low, close, amount, volume, reserved \ DAY_STRUCT.unpack_from(raw, offset) # 日期字段是整数比如 20231229 # 直接整除和取余比 strftime 更稳也避免字符串解析开销 year date_int // 10000 month date_int // 100 % 100 day date_int % 100 records.append({ date: f{year:04d}-{month:02d}-{day:02d}, open: round(open_, 3), high: round(high, 3), low: round(low, 3), close: round(close, 3), amount: round(amount, 2), volume: volume, reserved: reserved, }) if len(raw) % DAY_SIZE ! 0: print(f警告: {path.name} 数据尾部有 {len(raw) % DAY_SIZE} 字节多余内容) return records这个实现里有几个细节值得说。unpack_from比unpack好用它可以指定偏移量不需要反复切割字节串。日期字段直接用整除和取余拆成年月日不经过字符串中间层一方面性能好另一方面避免遇到异常日期值比如 0时strptime直接抛异常。reserved字段我没有丢弃调试的时候可以把它打印出来看是不是存了前收或者涨跌额。3.2 解析 blk 文件的核心函数blk文件解析看起来简单但要做得够健壮需要考虑编码和多种行格式。我的做法是先读二进制再用gbk解码如果出现替换字符就改用utf-8对每一行内容用正则提取第一个 6 位连续数字同时去重保序。import re def parse_blk_file(path): 解析通达信板块 .blk 文件 返回 6 位股票代码列表保持原文件顺序并去重 path Path(path) raw path.read_bytes() # 旧版本 .blk 可能是二进制格式直接跳过 if b\x00 in raw: print(f提示: {path.name} 看起来是二进制板块文件跳过文本解析) return [] # 优先 GBK兜底 UTF-8 text raw.decode(gbk, errorsignore) if \ufffd in text: text raw.decode(utf-8, errorsignore) codes [] seen set() for line in text.splitlines(): line line.strip() m re.search(r(\d{6}), line) if not m: continue code m.group(1) # 过滤明显不是股票的代码比如 000001 是平安银行但有时会混入重复 if code not in seen: seen.add(code) codes.append(code) return codes用正则而不是line.split()的好处是600000|浦发银行、600000 浦发银行、SH600000这三种格式统统能处理。\d{6}会匹配到字符串里第一个连续的 6 位数字正好是股票代码本身。如果一行里写的是SH600000匹配结果也是600000不会出错。3.3 把板块和行情串起来的完整示例有了这两个核心函数接下来就是组装。补全市场前缀、定位日线文件、批量扫描板块目录这些逻辑我放到一个完整脚本里。这个脚本运行后会扫描通达信安装目录下的两个板块文件夹列出板块里的股票再读取每只股票日线数据的最后一条记录打印出最新收盘价。def normalize_code(code): 将 6 位数字代码补全为带市场前缀的代码 例如 600000 - sh600000000001 - sz000001 if len(code) ! 6 or not code.isdigit(): raise ValueError(f无效股票代码: {code}) # 按首位数字判断所属市场 if code[0] in (6, 5, 9): return fsh{code} if code[0] in (0, 1, 2, 3): return fsz{code} if code[0] in (4, 8): return fbj{code} return code def find_day_file(code, tdx_root): 在通达信安装目录中定位日线 .dat 文件 prefix normalize_code(code) market prefix[:2] return Path(tdx_root) / vipdoc / market / lday / f{prefix}.dat def main(): import sys # Windows 终端可能默认 GBK强制 stdout 用 UTF-8避免打印中文乱码 try: sys.stdout.reconfigure(encodingutf-8) except AttributeError: pass tdx_root input(请输入通达信安装目录例如 D:\\new_tdx: ).strip().strip() root Path(tdx_root) if not root.exists(): print(目录不存在请检查路径) return # 同时扫用户自定义板块和系统板块 block_dirs [ root / T0002 / blocknew, root / T0002 / block, ] for block_dir in block_dirs: if not block_dir.exists(): continue print(f\n 扫描目录: {block_dir} ) for blk in sorted(block_dir.glob(*.blk)): print(f\n--- 板块: {blk.stem} ---) codes parse_blk_file(blk) if not codes: print( 没有解析到股票代码) continue print(f 共 {len(codes)} 只股票) for code in codes[:5]: day_file find_day_file(code, root) if not day_file.exists(): print(f {code}: 未找到本地日线文件) continue records parse_day_file(day_file) if not records: print(f {code}: 日线数据为空) continue last records[-1] print( f {code} 最新日期 {last[date]} f收盘 {last[close]} 成交量 {last[volume]} ) if __name__ __main__: main()运行这段代码控制台会输出类似这样的结果--- 板块: 自选股 --- 共 8 只股票 600000 最新日期 2024-12-29 收盘 7.21 成交量 40120000 000001 最新日期 2024-12-29 收盘 9.55 成交量 38160000 300750 最新日期 2024-12-29 收盘 152.3 成交量 21560000这只是一个起点。拿到 records 列表后你可以继续算涨跌幅、算均线、统计区间最高最低或者把整个板块的数据聚合成一个 DataFrame 做进一步分析。代码的扩展点就在records这个列表上。3.4 顺手加一个 CSV 导出能力板块扫描只读最后一条记录很多时候不够用。大多数情况你需要把某只股票的完整日线导出来或者把整个板块所有股票的日线汇总成表格。加一个 CSV 导出函数是最实用的扩展。import csv def day_records_to_csv(records, csv_path): 将日线记录列表导出为 CSV 注意用 utf-8-sig这样 Excel 打开不会乱码 if not records: return with open(csv_path, w, newline, encodingutf-8-sig) as f: fieldnames [date, open, high, low, close, amount, volume] writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(records)我见过不少人栽在这个小细节上用默认的utf-8写 CSV用 Excel 打开全是乱码。加-sig后缀写出来是带 BOM 的 UTF-8Excel 能正确识别。另外newline也别忘了否则 Windows 上每一行后面会多一个空行。如果要导出整个板块的数据可以先建一个汇总字典key 是股票代码value 是该股票的 records 列表最后循环写文件即可。4. 跑起来踩过的坑常见问题与排查实录4.1 文件被占用或权限不对最先容易遇到的坑是通达信还开着的时候去读dat文件Windows 可能提示文件被占用。实际上通达信对日线lday目录下的文件并没有长期独占锁大多数情况下只读打开没问题。但如果你遇到PermissionError优先检查是不是通达信正在实时写入这个文件尤其是盘中的行情数据文件。解决思路是先捕获异常给出友好提示。代码里可以这样try: raw path.read_bytes() except PermissionError: print(f文件被占用稍后重试: {path}) return []如果数据目录在系统盘且权限受限会碰到Access is denied。这时候要用管理员身份运行终端或者用os.chmod调整目录权限。但这属于 Windows 常见问题不是通达信特有不再展开。4.2 成交量与成交额字段顺序的兼容问题我在第 2 章说过网上流传的格式有两种说法差别在偏移 20 到 27 这两个字段谁先谁后。如果你解析出来的数据不对劲最典型的现象是成交量字段出现123456.78这种小数或者成交额字段小得离谱。验证方法很简单拿一只你熟悉的股票对照通达信界面里最近一天的收盘价和成交量看解析结果是否对得上。我的经验是绝大多数通达信版本都是“成交额在前、成交量在后”也就是我代码里的amount(20-23) volume(24-27)。但也有一些修改版或者旧数据文件存在对调遇到这种情况把结构体格式串改成IfffffII里最后两个字段的位置换一下就行也就是把amount和volume的解包顺序互换。4.3 数据不是复权的回测前要处理本地dat文件里的价格是“不复权”的原始价格。什么意思如果某只股票中途经历过分红送股那么在除权除息日之后价格会出现一个人为的跳空缺口。从原始价格序列看前后两天的收盘价可能直接从 20 元跳到 15 元但这不代表股价真的跌了 25%。做策略回测或者技术指标计算时这种缺口会严重扭曲结果。所以读出来的数据不能直接用于计算涨跌幅必须先做复权处理。常见的做法有两种前复权以最新价格为准把历史价格按比例调整后复权以上市首日价格为准把之后的价格往上调。具体算法不复杂但需要除权除息数据这是一块独立的工作。我的建议是先用不复权数据看看形态没问题真正跑回测之前必须处理复权。4.4 板块文件编码乱码与二进制旧格式如果你解析blk文件后打印板块名是乱码多半是编码判断出了问题。我代码里的策略是先gbk后utf-8基本覆盖了大多数情况。但如果通达信版本非常老blk文件可能是真正的二进制格式每行不是文本正则匹配出来就会是错的。怎么判断是二进制格式文本.blk文件里基本不会出现\x00字节所以代码里我直接用b\x00 in raw做快速判断。碰到二进制旧格式我建议直接用通达信软件打开该板块重新另存一份一般就能转成文本格式了。这种老数据文件现在已经很少见但兼容一下总没坏处。4.5 常见问题速查表问题现象可能原因解决方法读取 dat 文件报 PermissionError通达信正在写入该文件捕获异常后等待重试或先关闭软件成交量字段出现小数成交额与成交量顺序解析反了交换结构体中最后两个字段日期字段为 0000-00-00文件不完整或文件头损坏检查文件大小是否为 32 的倍数blk 板块名乱码编码不是 UTF-8强制用 GBK 解码找不到某只股票的日线文件市场前缀判断错误或未下载该股检查 normalize_code 前缀确认 vipdoc 目录数据涨跌幅看起来不对不复权价格导致的跳空缺口回测前先做复权处理4.6 一个额外提醒保留字段别急着忽略我解析dat文件时特意把reserved也保留在字典里因为它不是完全没用。在部分通达信版本里这个字段存的是当日的涨跌额、前收盘价甚至可能是复权因子。如果你发现这个字段总是有值可以尝试把它当作前收价用收盘 - 前收/ 前收来计算涨跌幅会比你用上一根K线收盘价计算更准确。我在一个老版本数据里就遇到过这种用法当时是因为某只股票停牌几天后复牌前一根 K 线不是前收盘价用reserved字段反而能算出官方口径的涨跌幅。当然这个字段在不同版本里含义不一样不要在生产代码里依赖它但调试时可以多看一眼。我个人在实际操作中的体会是解析本地行情文件这件事难点从来不在代码量而在于你愿不愿意先把二进制格式吃透。dat和blk两个文件搞明白之后通达信在你眼里就不再是一个黑盒你可以随时把它的本地数据变成自己数据库里的一张表。最后再分享一个小技巧拿到一个陌生dat文件先别急着写循环用十六进制编辑器打开看前 32 个字节能直观确认字段边界这个习惯帮我省了不少排查时间。
返回列表