单纯使用 MQL5 语言处理 ZIP 档案·进阶篇
🗜️

单纯使用 MQL5 语言处理 ZIP 档案·进阶篇

(2/3)· 跳过外部库依赖,用 MQL5 原生读写 ZIP 的底层结构与类设计全拆解

含代码示例偏理论 第 2/3 篇
不少交易者以为 MQL5 只能靠 CryptDecode 顺手解压 Web 回复,真要本地生成或改写 ZIP 就去找 DLL。其实平台内建的数组操作足够搭一套纯语言压缩流程,只是格式细节没人系统讲过。

◍ ZIP 中心目录与末尾记录的结构定义

在 MT5 里用 MQL5 直接解析 ZIP 包,核心是先吃透两个结构:中心目录项(CDR)和中心目录末尾记录(ECDR)。前者逐条描述压缩包内每个文件的元数据,后者锚定整张目录的边界与总量。 CDR 构造函数里写死了 header 值为 0x02014B50,made_ver 给 20、version 给 10,压缩方式默认 DEFLATE,其余 crc_32、comp_size、uncomp_size 等长度字段初始化为 0,等真正载入字节数组时再填。 ECDR 的 header 固定为 0x06054B50,靠它能在文件尾部反查中心目录的起始偏移 start_cd_offset 与总记录数 total_entries。注意原结构里 file_comment_lengtt 拼写漏了一个 h,编译不会报错但后续维护容易踩坑,建议改名。 开 MT5 新建脚本把下面结构贴进去,能直接编译通过,下一步就是写 LoadFromCharArray 按字节偏移逐个字段抠出来。

MQL5 / C++
class="kw">struct ZipCentralDirectory
{
  class="type">uint   header;
  class="type">class="kw">ushort made_ver;
  class="type">class="kw">ushort version;
  class="type">class="kw">ushort bit_flag;
  class="type">class="kw">ushort comp_method;
  class="type">class="kw">ushort last_mod_time;
  class="type">class="kw">ushort last_mod_date;
  class="type">uint   crc_32;
  class="type">uint   comp_size;
  class="type">uint   uncomp_size;
  class="type">class="kw">ushort filename_length;
  class="type">class="kw">ushort extrafield_length;
  class="type">class="kw">ushort file_comment_length;
  class="type">class="kw">ushort disk_number_start;
  class="type">class="kw">ushort internal_file_attr;
  class="type">class="kw">ushort external_file_attr;
  class="type">uint   offset_header;
  class="type">bool   LoadFromCharArray(class="type">uchar &array[]);
  class="type">int    ToCharArray(class="type">uchar &array[]);
  ZipCentralDirectory() : header(0x02014B50),
                         made_ver(class="num">20),
                         version(class="num">10),
                         bit_flag(class="num">0),
                         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),
                         file_comment_length(class="num">0),
                         disk_number_start(class="num">0),
                         internal_file_attr(class="num">0),
                         external_file_attr(class="num">0)
                         {;}
};
class="kw">struct ZipEndRecord
{
  class="type">uint   header;
  class="type">class="kw">ushort disk_number;
  class="type">class="kw">ushort disk_number_cd;
  class="type">class="kw">ushort total_entries_disk;
  class="type">class="kw">ushort total_entries;
  class="type">uint   size_central_dir;
  class="type">uint   start_cd_offset;
  class="type">class="kw">ushort file_comment_lengtt;
  class="type">class="kw">string FileComment(class="type">void);
  class="type">bool   LoadFromCharArray(class="type">uchar& array[]);
  class="type">int    ToCharArray(class="type">uchar &array[]);
  ZipEndRecord(class="type">void) : header(0x06054B50){;}
};

「在 MT5 里徒手拼一个 ZIP 容器」

想用 MQL5 直接生成可被 WinRAR 正常打开的 ZIP 包,核心不是调系统 API,而是先理解 CZip 这套对象层级:CZip 管档案整体,CZipFile 和 CZipFolder 分别管文件和目录,二者都继承自 CZipContent。要往档案里塞东西,得先 new 一个 CZipFile 对象,再交给 CZip 去存,文件夹则换成 CZipFolder 的实例。 压缩动作发生在创建 CZipFile 的那一刻,不是写盘时。构造函数内部走 AddFileArray,真正干活的是 CryptEncode 系统函数,数据统一收进 uchar 动态数组。也就是说,无论你是读磁盘文件还是 EA 运行时现造的数据,只要能转成 uchar[],就能进档案——这点决定了它对任意文件类型通吃。 时间日期是个坑:ZIP 格式沿用了 MS-DOS 的 16 位拆分存储,日期和时间各占一个 word,最早只认 1980-01-01。日期字节里 0–4 位表日、5–8 位表月、9–15 位表距 1980 的年数;时间字节里 0–4 位秒(±2 秒精度)、5–10 位分、11–15 位小时。MQL 的 datetime 必须靠位运算转成 DosDate/DosTime 才能写进中心目录结构 m_directory。 没有 CRC-32 校验和,第三方工具直接报损坏。CZipFile 里按经典 C 实现算了 32 位哈希,压缩前算一次,解压时重算比对。实测用这类类生成的档案在 WinRAR 里每个文件都有独立 CRC-32 码,匹配才放行。 读写靠三个方法配合:SaveZipToFile 落盘、ToCharArray 拼最终 ECDR 结构、ZipElementsToArray 按 ENUM_ZIP_PART 分别吐出本地头或中心目录的字节流。读回时不能直转,得先经 CSourceZip 中间容器判类型,再填回 CZipFile / CZipFolder——这套引用初始化只在 CZip 内部用,别在外面直接调。 下面这段是最小可运行样例:建空白档案、把字符串转 uchar 数组、生成 HelloWorld.txt 并挂进档案。复制到 MT5 脚本里跑完,磁盘上就会出现同名的 ZIP,用 WinRAR 打开不会报错。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用 &class="macro">#x27;HelloWorld&class="macro">#x27; 消息创建文件                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateHelloWorld()
  {
   CZip zip;                                                  class=class="str">"cmt">// 我们创建一个空白ZIP档案
   class="type">uchar content[];
   StringToCharArray("HelloWorld!",content,class="num">0,
                     StringLen("HelloWorld!"));               class=class="str">"cmt">// 我们把 "HelloWorld!" 短语写到字节数组中
   CZipFile* file = new CZipFile("HelloWorld.txt",content);   class=class="str">"cmt">// 我们使用 "HelloWorld.txt" 作为名称创建ZIP文件

ZIP 文件类的压缩与还原实现

在 MT5 里用 CZipFile 封装 ZIP 操作,核心是把字节数组直接交给 CryptEncode 做 ZIP 归档。下面这段 AddFileArray 展示了压缩全过程:先清空缓冲、置零尺寸,再用固定密钥 {1,0,0,0} 调用 CRYPT_ARCH_ZIP 算法。 [CODE]bool CZipFile::AddFileArray(uchar &file_src[]) { ResetLastError(); ArrayResize(m_file_puck,0); CompressedSize(0); UncompressedSize(0); CreateDateTime(TimeCurrent()); if(ArraySize(file_src)<1) { SetUserError(ZIP_ERROR_EMPTY_SOURCE); return false; } uchar key[]={1,0,0,0}; CryptEncode(CRYPT_ARCH_ZIP,file_src,key,m_file_puck); if(ArraySize(m_file_puck)<1) { SetUserError(ZIP_ERROR_BAD_PACK_ZIP); return false; } UncompressedSize(ArraySize(file_src)); CompressedSize(ArraySize(m_file_puck)); uint crc32=CRC32(file_src); m_header.crc_32=crc32; m_directory.crc_32=crc32; return true; }[/CODE] 逐行拆解:ResetLastError 清掉上一次错误码;ArrayResize 把压缩缓冲归零;两个 Size 函数先写 0,等密文出来再回填。空数组直接抛 ZIP_ERROR_EMPTY_SOURCE 并返回 false,避免后续崩溃。 密钥数组写死 {1,0,0,0} 是 MT5 ZIP 格式的内部约定,CryptEncode 用它能把明文压成 m_file_puck。压完若缓冲尺寸仍小于 1,说明打包失败。最后用 CRC32 算校验值,同时写进头部和目录结构。 解压侧对称:GetUnpackFile 里用同一把 key 跑 CryptDecode(CRYPT_ARCH_ZIP,...) 就能拿回原始字节。外汇与贵金属 EA 里若用这套存策略配置,需注意文件落盘有被读取的高风险,密钥仅为格式标识而非加密保护。

MQL5 / C++
class="type">bool CZipFile::AddFileArray(class="type">uchar &file_src[])
  {
  ResetLastError();
  ArrayResize(m_file_puck,class="num">0);
  CompressedSize(class="num">0);
  UncompressedSize(class="num">0);
  CreateDateTime(TimeCurrent());
  if(ArraySize(file_src)<class="num">1)
    {
    SetUserError(ZIP_ERROR_EMPTY_SOURCE);
    class="kw">return false;
    }
  class="type">uchar key[]={class="num">1,class="num">0,class="num">0,class="num">0};
  CryptEncode(CRYPT_ARCH_ZIP,file_src,key,m_file_puck);
  if(ArraySize(m_file_puck)<class="num">1)
    {
    SetUserError(ZIP_ERROR_BAD_PACK_ZIP);
    class="kw">return false;
    }
  UncompressedSize(ArraySize(file_src));
  CompressedSize(ArraySize(m_file_puck));
  class="type">uint crc32=CRC32(file_src);
  m_header.crc_32=crc32;
  m_directory.crc_32=crc32;
  class="kw">return true;
  }

◍ ZIP 时间戳的 DOS 与 MQL 互转细节

在 MT5 里处理 ZIP 归档时,文件头用的是 MS-DOS 日期时间格式,而 MQL5 内部走的是 datetime 结构。两者编码位宽不同,直接强转会丢精度,必须按位拆装。 DosDate 把 year 减去 1980 后左移 9 位,mon 左移 5 位,day 占低 5 位;DosTime 则把 hour 左移 11 位、min 左移 5 位、sec 除以 2 后占低 5 位。逆向的 MqlDate / MqlTime 用掩码 0x1F、0xE0、0xFE00 等把位段扒回来,再交给 StructToTime 合成 datetime。 注意 DosTime 里秒数只保留到偶数(sec/2),还原时乘 2,所以 1 秒内的奇偶差会抹平。如果你在 EA 里比对 ZIP 内文件修改时间,可能看到和实际落盘时间差 1 秒,这不是 bug,是 DOS 格式的天花板。 下面四个方法可直接塞进你自己的 CZipContent 类验证:开 MT5 新建脚本,#include 相关头文件后调用 DosDate(TimeCurrent()) 和 MqlDate() 对照输出,就能确认位运算没偏。

MQL5 / C++
class="type">class="kw">ushort CZipContent::DosDate(class="type">class="kw">datetime date)
  {
   class="type">class="kw">ushort dos_date=class="num">0;
   class="type">MqlDateTime time={class="num">0};
   TimeToStruct(date,time);
   if(time.year>class="num">1980)
     {
      dos_date = (class="type">class="kw">ushort)(time.year-class="num">1980);
      dos_date = dos_date << class="num">9;
     }
   class="type">class="kw">ushort mon=(class="type">class="kw">ushort)time.mon<<class="num">5;
   dos_date = dos_date | mon;
   dos_date = dos_date | (class="type">class="kw">ushort)time.day;
   class="kw">return dos_date;
  }
class="type">class="kw">datetime CZipContent::MqlDate(class="type">void)
  {
   class="type">MqlDateTime time={class="num">0};
   class="type">class="kw">ushort date=m_directory.last_mod_date;
   time.day = date & 0x1F;
   time.mon = date & 0xE0;
   time.year= class="num">1980+(date & 0xFE00);
   class="kw">return StructToTime(time);
  }
class="type">class="kw">ushort CZipContent::DosTime(class="type">class="kw">datetime time)
  {
   class="type">class="kw">ushort date=class="num">0;
   class="type">MqlDateTime mql_time={class="num">0};
   TimeToStruct(time,mql_time);
   date=(class="type">class="kw">ushort)mql_time.hour<<class="num">11;
   class="type">class="kw">ushort min=(class="type">class="kw">ushort)mql_time.min<<class="num">5;
   date = date | min;
   date = date | (class="type">class="kw">ushort)(mql_time.sec/class="num">2);
   class="kw">return date;
  }
class="type">class="kw">datetime CZipContent::MqlTime(class="type">void)
  {
   class="type">MqlDateTime time={class="num">0};
   class="type">class="kw">ushort date=m_directory.last_mod_time;
   time.sec = (date & 0x1F)*class="num">2;
   time.min = date & 0x7E0;
   time.hour= date & 0xF800;
   class="kw">return StructToTime(time);
  }

「CRC32与ZIP字节流的拼装细节」

在 MT5 里手写 ZIP 归档,绕不开 CRC32 校验。上面这段先用 256 长度的 uint 数组建表,多项式常数 0xEDB88320 是标准 CRC-32 反转形式,初始值取 0xFFFFFFFF,末位再异或一次得到最终校验值。 建表过程对每个 i 做 8 轮移位与条件异或:当 crc 最低位为 1 时右移一位再异或多项式,否则仅右移。这个查表法比逐位计算快一个数量级,处理几 MB 的报价缓存文件时差异明显。 ZipElementsToArray 负责把头段或目录段分别序列化成 uchar 数组。注意 ZIP_PART_HEADER 且类型为文件时,会调用 GetPackFile 把压缩体追加进 zip_element,totalSize 累加各段长度后一次性 ArrayResize,避免多次重分配。 ToCharArray 是总装入口:头段 + 目录段 + 末尾中央目录记录(ecdr)。ecdr.start_cd_offset 直接等于头段数组大小,size_central_dir 按 ZipCentralDirectory 结构体尺寸乘文件总数算,三个数组拼接后的 totalSize 就是完整 ZIP 字节流长度,可直接写盘或回传。

MQL5 / C++
class="type">uint crc_table[class="num">256];
ArrayInitialize(crc_table,class="num">0);
class="type">uint crc=class="num">0;
for(class="type">int i=class="num">0;i<class="num">256;i++)
  {
    crc=i;
    for(class="type">int j=class="num">0;j<class="num">8;j++)
      crc=(crc&class="num">1)>class="num">0 ?(crc>>class="num">1)^0xEDB88320 : crc>>class="num">1;
    crc_table[i]=crc;
  }
crc=0xFFFFFFFF;
class="type">int len=class="num">0,size=ArraySize(array);
while(len<size)
    crc=crc_table[(crc^array[len++])&0xFF]^(crc>>class="num">8);
class="kw">return crc ^ 0xFFFFFFFF;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回 ZIP 元素的uchar数组                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CZip::ZipElementsToArray(class="type">uchar &zip_elements[],ENUM_ZIP_PART part)
  {
   CArrayObj elements;
   class="type">int totalSize=class="num">0;
   for(class="type">int i=class="num">0;i<m_archive.Total(); i++)
     {
       class="type">uchar zip_element[];
       CZipContent *zipContent=m_archive.At(i);
       if(part==ZIP_PART_HEADER)
         zipContent.ToCharArrayHeader(zip_element);
       else if(part==ZIP_PART_DIRECTORY)
         zipContent.ToCharArrayDirectory(zip_element);
       if(part==ZIP_PART_HEADER && zipContent.ZipType()==ZIP_TYPE_FILE)
         {
           class="type">uchar pack[];
           CZipFile *file=zipContent;
           file.GetPackFile(pack);
           ArrayCopy(zip_element,pack,ArraySize(zip_element));
         }
       totalSize+=ArraySize(zip_element);
       elements.Add(new CCharArray(zip_element));
     }
   ArrayResize(zip_elements,totalSize);
   class="type">int offset= class="num">0;
   for(class="type">int i = class="num">0;i<elements.Total(); i++)
     {
       CCharArray *objArray=elements.At(i);
       class="type">uchar array[];
       objArray.GetArray(array);
       ArrayCopy(zip_elements,array,offset);
       offset+=ArraySize(array);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 以uchar数组方式生成ZIP档案.                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CZip::ToCharArray(class="type">uchar &zip_arch[])
  {
   class="type">uchar elements[],directories[],ecdr_array[];
   ZipElementsToArray(elements,ZIP_PART_HEADER);
   ZipElementsToArray(directories,ZIP_PART_DIRECTORY);
   ZipEndRecord ecdr;
   ecdr.total_entries_disk=(class="type">class="kw">ushort)m_archive.Total();
   ecdr.total_entries=(class="type">class="kw">ushort)m_archive.Total();
   ecdr.size_central_dir= class="kw">sizeof(ZipCentralDirectory)*m_archive.Total();
   ecdr.start_cd_offset = ArraySize(elements);
   ecdr.ToCharArray(ecdr_array);
   class="type">int totalSize=ArraySize(elements)+ArraySize(directories)+ArraySize(ecdr_array);

把归档字节流落盘并反向解析头结构

打包收尾时,先按总尺寸给 zip_arch 扩好容,再把元素、目录和结束描述符三段依次拷进去,顺序错一位整个包就废。 SaveZipToFile 负责把 ToCharArray 吐出的 uchar 数组以二进制写进磁盘,FileOpen 用 FILE_BIN|FILE_WRITE,句柄无效直接返 false,这一步不抛错但调用方得自己判返回值。 LoadHeader 从偏移处先截 sizeof(ZipLocalHeader) 字节还原本地头,再按 header.filename_length 截文件名、按 comp_size 截压缩体,返回新偏移;任何一段 ArrayCopy 长度对不上,就可能让后续解析整体偏移崩掉。 LoadDirectory 同理从给定 offset 截 sizeof(ZipCentralDirectory) 字节的中心目录结构,offset 管理全靠调用链自己累加,MT5 里跑这套时建议先打印各段 ArraySize 验证总长等于文件字节数。

MQL5 / C++
  ArrayResize(zip_arch,totalSize);
  ArrayCopy(zip_arch,elements,class="num">0);
  ArrayCopy(zip_arch,directories,ArraySize(elements));
  ArrayCopy(zip_arch,ecdr_array,ArraySize(elements)+ArraySize(directories));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把ZIP档案保存到文件 zip_name 中                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CZip::SaveZipToFile(class="type">class="kw">string zip_name,class="type">int file_common)
  {
   class="type">uchar zip[];
   ToCharArray(zip);
   class="type">int handle= FileOpen(zip_name,FILE_BIN|FILE_WRITE|file_common);
   if(handle == INVALID_HANDLE)class="kw">return false;
   FileWriteArray(handle,zip);
   FileClose(handle);
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从文件中载入本地文件头.                                                            |
class=class="str">"cmt">//| 返回:                                                                              |
class=class="str">"cmt">//| 返回本地文件头地址, 名称和zip内容.                                                 |
class=class="str">"cmt">//| 如果读取失败返回-class="num">1.                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CZip::LoadHeader(class="type">uchar &zip_array[],class="type">int offset,CSourceZip &zip)
  {
class=class="str">"cmt">//复制本地文件头
   class="type">uchar header[];
   ArrayCopy(header,zip_array,class="num">0,offset,class="kw">sizeof(ZipLocalHeader));
   if(!zip.header.LoadFromCharArray(header))class="kw">return -class="num">1;
   offset+=ArraySize(header);
   class="type">uchar name[];
class=class="str">"cmt">//复制文件名
   ArrayCopy(name,zip_array,class="num">0,offset,zip.header.filename_length);
   zip.header_file_name=CharArrayToString(name);
   offset+=ArraySize(name);
class=class="str">"cmt">//复制zip数组
   ArrayCopy(zip.zip_array,zip_array,class="num">0,offset,zip.header.comp_size);
   offset+=ArraySize(zip.zip_array);
   class="kw">return offset;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 从文件中载入中心目录.                                     |
class=class="str">"cmt">//| 返回:                                                                              |
class=class="str">"cmt">//| 返回中心目录地址和名称                                                                   |
class=class="str">"cmt">//| 如果读取失败返回-class="num">1.                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CZip::LoadDirectory(class="type">uchar &zip_array[],class="type">int offset,CSourceZip &zip)
  {
class=class="str">"cmt">//复制中心目录
   class="type">uchar directory[];
   ArrayCopy(directory,zip_array,class="num">0,offset,class="kw">sizeof(ZipCentralDirectory));
让小布替你跑这套压缩自检
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到;把重复劳动交给小布,你专注决策,比如定时把日志流压缩落内存这类活儿不用手写轮询。

常见问题

可用本地文件头签名 0x04034b50 与 ECDR 签名 0x06054b50 在字节数组里反向搜索,中心目录位置由 ECDR 内字段给出,偏移计算需注意小端序。
小布盯盘目前提供诊断与压缩状态展示,若你把自己的 CZip 封装成 EA 附属模块,把内存归档结果推到品种页,小布可读取并可视化,不必手动导出。
MS-DOS 格式把日期时间压成两个 16 位字,分辨率只到 2 秒且基准为 1980 年,转换时需移位拆分再映射至 datetime 的 1970 基准。
CryptEncode 侧重加解密与标准哈希,CRC-32 需按 ZIP 规范做反射多项式迭代,文内给出独立生成函数,挂到 CZipFile 写流前补字段。
多为 WebRequest 返回的数组未剥去 HTTP 头或长度截断,应先校验接收长度再送进 CreateFromCharArray,否则 ECDR 搜不到会报 ENUM_ZIP_ERROR 越界。