单纯使用 MQL5 语言处理 ZIP 档案(基础篇)
用 MQL5 直接拆 ZIP 档案
MT5 终端自 build 1431 起在 MQL5 标准库引入了 Zip 压缩支持,意味着交易者不必借助外部脚本或第三方解压工具,就能在 EA 或指标里直接读取打包的历史数据或配置集。
早期社区示例由 Vasiliy Sokolov 在 2016-02-22 发布,原帖浏览量 2713、收藏 93,说明这类底层文件操作需求虽小众但稳定。
实际做法是在代码里调用 #include <ZipFile.mqh>,用 CZipFile 打开 .zip 后逐条 GetNextFile 读取内存流,再转成字符串或二进制喂给指标缓冲。外汇与贵金属行情数据常被券商分包成 zip,直接解包能省掉手动导出的步骤,但自动化读写文件涉及终端沙盒权限,误操作有覆盖本地数据的潜在风险。
「这套 ZIP 处理教程到底讲什么」
这一节把后续六章的骨架直接摊开:从 ZIP 文件格式与十六进制结构(本地文件头、中心目录、ECDR)讲起,再到 MQL5 里 CZip / CZipFile / CZipFolder 类的设计与压缩算法,最后落到远程下载解包、内存压缩与服务数据落地的实测。 对交易者来说,重点不在理论,而在第三章——用 MQL5 把多品种报价或日志压进内存、从 mql5 社区服务器拉档案解包,这几段代码能直接省掉本地 IO 的延迟。 目录里还列了 CZipContent、CZipFile、CZip 三个类的完整方法清单,以及 ENUM_ZIP_ERROR 的扩展错误取法。外汇与贵金属自动化跑这套时,压缩失败常因路径权限或 DOS 时间格式错位,高风险环境里建议先小样本验证再上实盘。
◍ MQL5 里直接拆 ZIP 到底图什么
最早吸引本文作者的是 CryptDecode 的一个隐蔽能力:它能解压传进来的 ZIP 字节数组。这个接口本是 MT5 开发组为解析多服务器 WebRequest 回包而留的,但 ZIP 格式本身有个坑——用 Adler-32 方式压缩的包,解压前必须知道哈希和,而实际拿到的流里并没有这东西。 后来在相关讨论里,开发组给 CryptDecode / CryptEncode 加了重载开关,允许解压时跳过 Adler-32 校验。这对普通用户意味着:ZIP 的完整功能在 MQL5 里终于能用了,不必再外挂 DLL。 为什么交易者要在意?订单历史、报价流、二级深度数据动辄以 GB 计,原始存储根本不现实;不少数据服务器只发 ZIP。过去 MQL5 标准库没法自动取这类数据,现在用 WebRequest 拉回 ZIP 后能当场解压,省传输也省内存。 更实用的一点:ZIP 是 Office Open XML 的容器,能操作 ZIP 就意味着可以用纯 MQL5 写出 Excel / Word 文件,不依赖第三方库。本文后续会先拆 ZIP 格式与数据块定位,再给 CZip / CZipFile / CZipDirectory 三个类,最后跑实战例子与文档。
拆开 ZIP 档案的字节骨架
ZIP 是 Phil Katz 在 1989 年设计的二进制容器,核心靠 DEFLATE 压缩,但档案本身远不止压缩流:它用本地文件头、中心目录、ECDR 三块固定结构串起文件名与数据。我们做 MT5 工具时只实现 6.3.4 规格的必需字段,第三方生成的某些变体档案可能解不开,这点要心里有数。 拿一个只装了 "HelloWorld!" 文本(源文件 11 字节)的 HelloWorld.zip 说事:用 WinRAR 压完体积 135 字节,服务信息比数据本身还重,所以几百字节的小文件归档没意义。在 WinHex 里看,0x35 到 0x2B 区间原样躺着那 11 字节——短到压缩不划算,算法直接存了明文。 本地文件头签名真实值是 0x04034B50,x86 小端写入变 0x504B0304,任何 ZIP 都从这起步。MQL5 里用 struct 描它,固定字节、文件名与数据放结构外。图 5 里 filename_length 两字节 0x0D00,反转为 13,正好 "HelloWorld.txt" 字符数;comp_size 0x0B000000 即 11 字节,印证未压缩存放。 中心目录签名 0x02014B50,是本地头的扩展,多了 offset_header 等字段,顺序连续,起点由 ECDR 记录。ECDR 签名 0x06054B50 永远在档尾,存总记录数与中心目录偏移,我们靠它定位数组元素数。外汇贵金属相关的 EA 打包分发若用自写 ZIP 类,先拿 WinHex 对照这三块偏移,能少踩很多坑。
「在 MT5 里手动拼一个 ZIP 本地文件头」
做本地日志归档或离线打包时,MT5 的 File 函数不直接给压缩接口,得自己按 PKWARE 的 APPNOTE 规范(4.3.7 节)把本地文件头结构写出来。下面这段结构就是照着 6.3.4 规格落的,头部魔数固定为 0x04034b50,少写一位 MT5 读回来就会错位。 结构体里 version 默认填 10,bit_flag 填 2,comp_method 用 DEFLATE 常量(即 8),这几个值对应「最小解压版本 1.0 / 使用 DEFLATE 压缩」的常规配置。crc_32、comp_size、uncomp_size 初始全 0,等真实数据算完再回填,否则校验必失败。 ZipLocalHeaderArray 只是个内存镜像壳,用 uchar array[sizeof(ZipLocalHeader)] 把结构按字节铺平,方便后面 ToCharArray 直接丢给文件流。开 MT5 新建一个 include 把这段贴进去,编译能看到 sizeof(ZipLocalHeader) 在 x64 下通常是 30 字节(不含方法体),可以顺手验证对齐有没有被编译器插空。 外汇与贵金属 EA 跑这套本地压缩属于高频 IO 操作,磁盘写失败或头错位都可能让历史 tick 丢失,实盘前务必在策略测试器用极小样本跑通再上真仓,相关操作风险偏高。
class="kw">struct ZipLocalHeader { class="type">uint header; class=class="str">"cmt">// ZIP 本地文件头, 永远等于 0x04034b50 class="type">class="kw">ushort version; class=class="str">"cmt">// 解压的最小版本号 class="type">class="kw">ushort bit_flag; class=class="str">"cmt">// 位标志 class="type">class="kw">ushort comp_method; class=class="str">"cmt">// 压缩方法 (class="num">0 - 未压缩, class="num">8 - 紧缩) class="type">class="kw">ushort last_mod_time; class=class="str">"cmt">// 文件修改时间 class="type">class="kw">ushort last_mod_date; class=class="str">"cmt">// 文件修改日期 class="type">uint crc_32; class=class="str">"cmt">// CRC-class="num">32 哈希值 class="type">uint comp_size; class=class="str">"cmt">// 压缩大小 class="type">uint uncomp_size; class=class="str">"cmt">// 未压缩大小 class="type">class="kw">ushort filename_length; class=class="str">"cmt">// 文件名长度 class="type">class="kw">ushort extrafield_length;class=class="str">"cmt">// 额外数据的长度栏位 class="type">bool LoadFromCharArray(class="type">uchar& array[]); class="type">int ToCharArray(class="type">uchar &array[]); ZipLocalHeader(): header(0x04034B50), version(class="num">10), bit_flag(class="num">2), comp_method(DEFLATE), last_mod_time(class="num">0), last_mod_date(class="num">0), crc_32(class="num">0), comp_size(class="num">0), uncomp_size(class="num">0), filename_length(class="num">0), extrafield_length(class="num">0) {;} }; class="kw">struct ZipLocalHeaderArray { class="type">uchar array[class="kw">sizeof(ZipLocalHeader)]; class=class="str">"cmt">// ZipLocalHeader的大小 };
◍ 把 ZIP 头结构塞回字符数组与中心目录定义
ZipLocalHeader 的 ToCharArray 方法直接把自身强转为 ZipLocalHeaderArray,再调用 ArrayCopy 把内部字节数组复制出去,返回的是复制元素的数量。这一招在 MT5 里跑通的前提是结构体的内存布局与字节数组严格对齐,否则复制出来的长度会和你预期不符。 LoadFromCharArray 先做一道防线:用 ArraySize(array)!=sizeof(ZipLocalHeader) 核对字节数,不等就抛 ZIP_ERROR_BAD_FORMAT_ZIP 并返回 false。随后反向 ArrayCopy 进临时结构,再强转回 this,最后校验 header 成员是否等于 ZIP_LOCAL_HEADER,任何一步不对都直接报错退出。 中心目录结构 ZipCentralDirectory 用固定头常量 0x02014B50 做标识,字段覆盖了压缩方法(0 为存储、8 为 deflate)、CRC32、压缩与未压缩大小、文件名与附加字段长度等。你在写自定义解压模块时,这几个 ushort 长度字段直接决定了后面要跳多少字节读文件名。 下面这段结构定义贴进 MT5 的 MQ5 文件就能编译,配合上面的两个方法可以搭出一个最小可用的 ZIP 本地头读写原型,外汇与贵金属历史数据批量打包前先拿小样本验证字节对齐。
class=class="str">"cmt">//| 返回: | class=class="str">"cmt">//| 复制元素的数量. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int ZipLocalHeader::ToCharArray(class="type">uchar &array[]) { ZipLocalHeaderArray zarray=(ZipLocalHeaderArray)this; class="kw">return ArrayCopy(array, zarray.array); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从字符数组获得LocalHeader结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ZipLocalHeader::LoadFromCharArray(class="type">uchar &array[]) { if(ArraySize(array)!=class="kw">sizeof(ZipLocalHeader)) { SetUserError(ZIP_ERROR_BAD_FORMAT_ZIP); class="kw">return false; } ZipLocalHeaderArray zarray; ArrayCopy(zarray.array,array); this=(ZipLocalHeader)zarray; if(header!=ZIP_LOCAL_HEADER) { SetUserError(ZIP_ERROR_BAD_FORMAT_ZIP); class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 中心目录(Central directory)结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct ZipCentralDirectory { class="type">uint header; class=class="str">"cmt">// 中心目录头, 永远等于 0x02014B50 class="type">class="kw">ushort made_ver; class=class="str">"cmt">// 创建版本 class="type">class="kw">ushort version; class=class="str">"cmt">// 解压的最小版本号 class="type">class="kw">ushort bit_flag; class=class="str">"cmt">// 位标志 class="type">class="kw">ushort comp_method; class=class="str">"cmt">// 压缩方法 (class="num">0 - 未压缩, class="num">8 - deflate) class="type">class="kw">ushort last_mod_time; class=class="str">"cmt">// 文件修改时间 class="type">class="kw">ushort last_mod_date; class=class="str">"cmt">// 文件修改日期 class="type">uint crc_32; class=class="str">"cmt">// CRC32 哈希 class="type">uint comp_size; class=class="str">"cmt">// 压缩大小 class="type">uint uncomp_size; class=class="str">"cmt">// 未压缩大小 class="type">class="kw">ushort filename_length; class=class="str">"cmt">// 文件名长度 class="type">class="kw">ushort extrafield_length; class=class="str">"cmt">// 额外数据的长度栏位 class="type">class="kw">ushort file_comment_length; class=class="str">"cmt">// 文件注释长度 class="type">class="kw">ushort disk_number_start; class=class="str">"cmt">// 磁盘起始编号 class="type">class="kw">ushort internal_file_attr; class=class="str">"cmt">// 内部文件属性 class="type">uint external_file_attr; class=class="str">"cmt">// 外部文件属性