单纯使用 MQL5 语言处理 ZIP 档案(基础篇)
📘

单纯使用 MQL5 语言处理 ZIP 档案(基础篇)

第 1/3 篇

用 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 丢失,实盘前务必在策略测试器用极小样本跑通再上真仓,相关操作风险偏高。

MQL5 / C++
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 本地头读写原型,外汇与贵金属历史数据批量打包前先拿小样本验证字节对齐。

MQL5 / C++
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">// 外部文件属性

常见问题

可以,用MQL5的FileOpen/FileRead把ZIP当二进制读,按本地文件头偏移自己解析,不用外部解压工具。
本地文件头偏移26处2字节是文件名长度,28处2字节是额外字段长度,其后紧跟文件名,再往后就是压缩数据起点。
可以,小布能按你给的头结构规则批量扫ZIP字节流,提取各文件命名并整理成可读列表,省去手写解析循环。
自己拼头能精确控制读取偏移,避免异步解压阻塞EA tick,回测时内存占用也更可控。
会导致定位不到对应本地文件头,读出来的压缩数据错位或直接报文件损坏,必须按字节严格对齐。