单纯使用 MQL5 语言处理 ZIP 档案·进阶篇
(2/3)· 跳过外部库依赖,用 MQL5 原生读写 ZIP 的底层结构与类设计全拆解
◍ 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 按字节偏移逐个字段抠出来。
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 打开不会报错。
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 里若用这套存策略配置,需注意文件落盘有被读取的高风险,密钥仅为格式标识而非加密保护。
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() 对照输出,就能确认位运算没偏。
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 字节流长度,可直接写盘或回传。
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 验证总长等于文件字节数。
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));