English 中文 Español Deutsch 日本語 Português
Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек

Работаем с ZIP-архивами средствами MQL5 без использования сторонних библиотек

MetaTrader 5Примеры | 23 сентября 2015, 11:10
8 140 70
Vasiliy Sokolov
Vasiliy Sokolov

Оглавление


Введение


История вопроса

Однажды автора данной статьи привлекла интересная особенность функции CryptDecode, а именно — возможность распаковывать переданный ей ZIP-массив. Этот модификатор был введен разработчиками торговой платформы MetaTrader 5 для того, чтобы можно было распаковывать ответ некоторых серверов, используя стандартную функцию WebRequest. Однако из-за некоторых особенностей формата ZIP-файла, использовать ее напрямую было невозможно.

Требовалась дополнительная аутентификация: для распаковки архива необходимо знать его хеш-сумму до упаковки: Adler-32, которой, естественно, не было. Однако, при обсуждении этой проблемы, разработчики пошли навстречу и перегрузили CryptDecode и CryptEncode, ее зеркального близнеца, специальным флагом, игнорирующим хеш Adler32 при распаковке переданных данных. Для неискушенных в техническом плане пользователей это нововведение можно объяснить просто: благодаря ему стала возможной полнофункциональная работа с ZIP-архивами. Эта статья подробно описывает формат ZIP-файла, особенности хранения данных в нем и предлагает для работы с архивом удобный объектно-ориентированный класс CZip.


Для чего это нужно

Сжатие данных — одна из важнейших технологий, особенно распространенная в среде Web. Благодаря сжатию экономятся ресурсы, необходимые для передачи, хранения и обработки информации. Сжатие данных используется практически во всех областях коммуникаций и проникло практически во все задачи, с которыми мы сталкиваемся при работе на компьютере.

Не стала исключением и финансовая сфера: гигабайты тиковой истории, потоки котировок, включая слепки стаканов (Level2-данные), немыслимо хранить в сыром, несжатом виде. Многие серверы, в том числе предоставляющие аналитическую информацию, интересную для торговли, также хранят данных в ZIP-архивах. Раньше не представлялось возможным получать эти данные автоматически, используя штатные средства MQL5. Теперь ситуация изменилась.

С помощью функции WebRequest можно скачать ZIP-архив и на лету распаковать его данные прямо в оперативную память компьютера. Все эти возможности важны и обязательно будут востребованы многими трейдерами. Сжатие данных можно использовать даже для экономии оперативной памяти компьютера. О том, как это делается, посвящен раздел 3.2 данной статьи. Наконец, умение работать с ZIP-архивами открывает доступ к формированию документов типа Microsoft Office по стандарту Office Open XML, что, в свою очередь, сделает возможным создание простых файлов Excel или Word прямо из MQL5, также без использования сторонних DLL библиотек.

Как видно, применение ZIP-архивирования обширно, и класс, создаваемый нами, сослужит хорошую службу всем пользователям MetaTrader.

В первой главе данной статьи мы подробно опишем собственно формат ZIP-файла и поймем, из каких блоков данных он состоит. Данная глава будет интересна не только тем, кто изучает MQL, она также послужит хорошим образовательным материалом и тем, кто занимается изучением вопросов, связанных с архивированием и хранением данных. Вторая глава будет посвящена классам CZip, CZipFile и CZipDirectory — эти классы являются основными объектно-ориентированными элементами для работы с архивом. Третья глава описывает практические примеры, связанные с использованием архивирования. Четвертая глава является документацией к предлагаемым классам.

Итак, приступим к изучению этого самого распространенного типа архивирования.

 

Глава 1. Формат ZIP-файла и способ хранения данных в нем


1.1. Структура ZIP-файла

Формат ZIP был создан Филом Кацом в 1989 году и впервые был реализован в программе PKZIP для MS-DOS, выпущенной компанией PKWARE, основателем которой являлся Кац. Этот формат архива наиболее часто использует алгоритм сжатия данных DEFLATE. Наиболее распространенными программами для работы с этим форматом в среде Windows являются WinZip и WinRAR.

Важно понимать, что формат ZIP-архива развивался со временем и имеет несколько версий. В создании класса для работы с ZIP-архивом, мы будем опираться на официальную спецификацию формата версии 6.3.4, размещенную на сайте компании PKWARE по адресу: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT. Это последняя спецификация формата, датируемая 1 октября 2014 года. Сама спецификация формата достаточно обширна и включает описания множества нюансов.

В данной статье мы будем исходить из принципа наименьших усилий и создадим инструмент, который будет использовать лишь самые необходимые данные для успешной распаковки файлов и создания новых архивов. Это значит, что в определенной мере работа с ZIP-файлами будет ограничена — совместимость форматов не гарантируется, а значит, и не приходится говорить о полной "всеядности" архивов. Вполне возможно, что некоторые ZIP-архивы, созданные сторонними средствами, не смогут быть распакованы с помощью предложенного инструмента.

Каждый ZIP-архив — это бинарный файл, содержащий упорядоченную последовательность байтов. С другой стороны, в ZIP-архиве каждый файл имеет имя, атрибуты (например время модификации файла) и другие свойства, которые мы привыкли видеть в файловой системе любой операционной системы. Поэтому, помимо запакованных данных, каждый ZIP-архив хранит имя запакованного файла, его атрибуты и другую служебную информацию. Эта служебная информация располагается в строго определенном порядке и имеет регулярную структуру. Например, если в архиве содержатся два файла (File#1 и File#2), то архив будет иметь следующую схему:


Рис. 1. Схематичное представление ZIP-архива, содержащего два файла: File#1 и File#2

Позже мы разберем подробно каждый блок данной схемы, а сейчас дадим краткое описание этих блоков:

  • Local File Header — этот блок данных содержит основную информацию о запакованном файле: размер файла до и после упаковки, время модификации файла, контрольную сумму CRC-32 и локальный указатель на имя файла. Кроме того, этот блок содержит версию архиватора, необходимую для распаковки файла.
  • File Name последовательность байтов произвольной длины, образующая имя запакованного файла. При этом длина имени файла не должен превышать 65 536 символов.
  • File Data упакованное содержимое файла в виде байт-массива произвольной длины. Если файл пустой или представляет из себя каталог, то этот массив не используется, и сразу за именем файла или директории идет заголовок Local File Header, описывающий следующий файл.
  • Central Directory содержит расширенное представление данных в Local File Header. Помимо данных, содержащихся в Local File Header, содержит атрибуты файла, локальную ссылку на структуру Local File Header и некоторую другую, в большинстве случаев неиспользуемую информацию.
  • End of central directory record — эта структура представлена в каждом архиве единственным экземпляром и записывается в самый конец архива. Наиболее интересные данные, которые она содержит, это количество записей в архиве (или количество файлов и каталогов) и локальные ссылки на начало блока Central Directory.

Каждый блок данной схемы можно представить либо в виде регулярной структуры, либо в виде байт-массива произвольной длины. Каждую из структур, в свою очередь, можно описать с помощью одноименной конструкции программирования в MQL — структуры.

Структура всегда занимает строго определенное количество байтов, поэтому она не может содержать внутри себя массивы произвольной длины и строки. Однако она может содержать указатели на эти объекты. Именно поэтому названия файлов в архиве вынесены за пределы структуры, благодаря чему они могут иметь произвольную длину. То же относится к упакованным данным файлов — их размер является произвольным, поэтому и они содержатся за пределами структур. Таким образом, можно сказать, что ZIP-архив представляет из себя последовательность структур, строк и упакованных данных.

Формат ZIP-файла, помимо представленного выше, описывает дополнительную структуру, так называемый Data Descriptor. Эта структура используется только в том случае, если по каким-то причинам не удалось сформировать структуру Local File Header, и часть данных, необходимая для Local File Header, стала доступна уже после сжатия данных. На практике это очень экзотическая ситуация, поэтому данная структура практически никогда не используется, и в нашем классе для работы с архивами этот блок данных не поддерживается.

     Обратите внимание, что согласно формату ZIP-архива каждый файл сжимается независимо от остальных. С одной стороны, это позволяет локализовать возникновение ошибок: "битый" архив можно восстановить, удалив файл с неправильным содержимым и оставив все остальное содержимое архива без изменений. С другой стороны, при сжатии каждого файла отдельно снижается эффективность сжатия, в особенности если каждый из файлов занимает мало места.

 

1.2. Изучаем ZIP-файл в шестнадцатеричном редакторе

Теперь, вооружившись самыми необходимыми знаниями, мы можем посмотреть, что находится внутри типичного ZIP-архива. Для этого воспользуемся одним из шестнадцатиричных редакторов, WinHex. Если по каким-то причинам WinHex'а у вас нет, вы можете воспользоваться любым другим шестнадцатиричным редактором. Ведь мы помним, что любой архив — это бинарный файл, который можно открыть как простую последовательность байтов. Для эксперимента создадим простой ZIP-архив, содержащий внутри себя один-единственный текстовый файл с фразой "HelloWorld!" ("Привет Мир!"):

 

Рис. 2. Создание текстового файла в блокноте

Затем воспользуемся любым ZIP-архиватором и с его помощью создадим архив. В нашем примере таким архиватором будет WinRAR. В нем необходимо выбрать только что созданный файл и заархивировать его в формате ZIP:

 

Рис. 3. Создание архива с помощью архиватора WinRAR

После окончания архивирования на жестком диске компьютера в соответствующей директории появится новый файл "HelloWorld.zip". Первая особенность этого файла которая бросается в глаза — его размер, составляющий 135 байт, он гораздо больше первоначального размера исходного текстового файла в 11 байт. Это связано именно с тем, что помимо собственно упакованных данных, ZIP-архив содержит служебную информацию. Поэтому для небольших данных, занимающих всего несколько сот байтов, архивирование бессмысленно.

Теперь, когда у нас есть схема расположения данных, архив, состоящий из набора байтов, не покажется нам пугающим. Откроем его с помощью шестнадцатеричного редактора WinHex. На рисунке ниже представлен байтовый массив архива с условной подсветкой каждой области, описанной на схеме 1:


Рис. 4. Внутреннее содержимое ZIP-архива, содержащего файл HelloWorld.txt

 

Собственно фраза "HelloWorld!" содержится в диапазоне с 0x2B по 0x35 байт и занимает всего 11 байт. Обратите внимание, что алгоритм сжатия решил не сжимать исходную фразу, и в ZIP-архиве она присутствует в исходном виде. Это произошло потому, что сжатие такого короткого сообщения неэффективно, и сжатый массив может оказаться даже больше несжатого.

    ZIP-архив не во всех случаях содержит сжатые данные. Иногда данные в архиве располагаются в исходном, несжатом виде, даже если при создании архива было явно указано сжимать данные при архивировании. Эта ситуация  возникает в случае, когда размер данных незначителен, и сжатие данных неэффективно.

Благодаря рисунку 4 становится понятным, как разные блоки данных хранятся в получившемся архиве, и где именно содержатся данные файла. Теперь настало время подробно разобрать каждый из блоков данных в отдельности.

 

1.3. Структура Local File Header

Каждый ZIP-архив начинается со структуры Local File Header. Она содержит метаданные файла, следующего за ней в виде сжатого байтового массива. Каждая структура в архиве, согласно спецификации формата, имеет свой уникальный четырехбайтовый идентификатор. Не является исключением и эта структура, ее уникальный идентификатор равен 0x04034B50.

Важно учитывать, что x86-совместимые процессоры загружают данные из бинарных файлов в оперативную память в обратном порядке. Числа при этом располагаются наизнанку: последний байт занимает место первого и наоборот. Способ записи данных в файл определяется форматом самого файла и для файлов в формате ZIP также осуществляется в обратном порядке. Более подробно о порядке следования байтов можно почитать в одноименной статье на Wikipedia: "Порядок байтов". Для нас же это означает, что идентификатор структуры будет записан в виде числа 0x504B0304 (значения 0x04034B50, вывернутого наизнанку). Именно с этой последовательности байтов начинается любой ZIP-архив.

Поскольку структура — это строго определенная последовательность байтов, ее можно представить в виде аналогичной структуры на языке программирования MQL5. Описание структуры Local File Header на MQL5 будет следующим:

//+------------------------------------------------------------------+
//| Local file header based on specification 6.3.4:                  |
//| https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT,     |
//| 4.3.7                                                            |
//+------------------------------------------------------------------+
struct ZipLocalHeader
  {
   uint   header;                 // ZIP local header, always equals 0x04034b50
   ushort version;                // Minimum version for extracting
   ushort bit_flag;               // Bit flag
   ushort comp_method;            // Compression method (0 - uncompressed, 8 - deflate)
   ushort last_mod_time;          // File modification time
   ushort last_mod_date;          // File modification date
   uint   crc_32;                 // CRC-32 hash
   uint   comp_size;              // Compressed size
   uint   uncomp_size;            // Uncompressed size
   ushort filename_length;        // Length of the file name
   ushort extrafield_length;      // Length field with additional data
   bool   LoadFromCharArray(uchar& array[]);
   int    ToCharArray(uchar &array[]);
   ZipLocalHeader(): header(0x04034B50),
                     version(10),
                     bit_flag(2),
                     comp_method(DEFLATE),
                     last_mod_time(0),
                     last_mod_date(0),
                     crc_32(0),
                     comp_size(0),
                     uncomp_size(0),
                     filename_length(0),
                     extrafield_length(0)
                     {;}
  };

Данная структура используется для реальной работы с ZIP-архивами, поэтому помимо собственно полей данных, содержит дополнительные методы, позволяющие конвертировать структуру в набор байтов (байтовый массив uchar) и, наоборот, создавать структуру из набора байтов. Приведем содержимое методов ToCharArray и LoadFromCharArray, позволяющих делать такое преобразование:

//+------------------------------------------------------------------+
//|Private struct for convert LocalHeader to uchar array             |
//+------------------------------------------------------------------+
struct ZipLocalHeaderArray
  {
   uchar array[sizeof(ZipLocalHeader)];              // Size of ZipLocalHeader
  };
//+------------------------------------------------------------------+
//| Convert ZipHeader struct to uchar array.                         |
//| RETURN:                                                          |
//|   Numbers of copied elements.                                    |
//+------------------------------------------------------------------+
int ZipLocalHeader::ToCharArray(uchar &array[])
  {
   ZipLocalHeaderArray zarray=(ZipLocalHeaderArray)this;
   return ArrayCopy(array, zarray.array);
  }
//+------------------------------------------------------------------+
//| Init local header structure from char array                      |
//+------------------------------------------------------------------+
bool ZipLocalHeader::LoadFromCharArray(uchar &array[])
  {
   if(ArraySize(array)!=sizeof(ZipLocalHeader))
     {
      SetUserError(ZIP_ERROR_BAD_FORMAT_ZIP);
      return false;
     }
   ZipLocalHeaderArray zarray;
   ArrayCopy(zarray.array,array);
   this=(ZipLocalHeader)zarray;
   if(header!=ZIP_LOCAL_HEADER)
     {
      SetUserError(ZIP_ERROR_BAD_FORMAT_ZIP);
      return false;
     }
   return true;
  }

Опишем поля структуры  (перечислены в порядке следования):

  • header — уникальный идентификатор стркутуры, для File Local Header равен 0x04034B50;
  • version — минимальная версия для распаковки файла;
  • bit_flag — битовый флаг, имеет идентификатор 0x02;
  • comp_method — тип используемого сжатия. Как правило, всегда используется метод сжатия DEFLATE, этот тип сжатия имеет идентификатор 0x08.
  • last_mod_time — время последней модификации файла. Содержит часы, минуты и секунды модификации файла в формате MS-DOS. Этот формат описан на странице компании Microsoft.
  • last_mod_date — дата последней модификации файла. Содержит день месяца, номер месяца в году и год модификации файла в формате MS-DOS.
  • crc_32 — контрольная сумма CRC-32. Используется программами по работе с архивами для определения ошибок содержимого файла. Если это поле не заполнено, ZIP-архиватор откажется распаковывать упакованный файл, ссылаясь на испорченный файл.
  • comp_size — размер в байтах упакованных данных;
  • uncomp_size — размер в байтах исходных данных;
  • filename_length — длина имени файла;
  • extrafield_length — специальное поле для записи дополнительных атрибутов данных. Практически никогда не используется и равно нулю.

При сохранении этой структуры в архиве создается последовательность байтов, хранящая значения соответствующих полей этой структуры. Снова загрузим наш ZIP-архив с файлом HelloWorld.txt в шестнадцатеричный редактор и разберем байтовый массив этой структуры уже подробно:

 Рис. 5. Байт-схема структуры Local File Header в архиве HelloWorld.zip

Рис. 5. Байт-схема структуры Local File Header в архиве HelloWorld.zip

Из схемы видно, какие байты занимают те или иные поля структуры. Для проверки данных в ней обратим внимание на поле "File Name length", оно занимает два байта и равно значению 0x0D00. Вывернув это число наизнанку и переведя его в десятичный формат, мы получим значение 13 — именно столько символов занимает имя файла "HelloWorld.txt". То же самое можем проделать с полем, указывающим размер сжатых данных. Оно равно 0x0B000000, что соответствует 11 байтам. В самом деле, фраза "HelloWorld!" хранится в архиве в несжатом виде и занимает 11 байт.

Сразу после структуры идут сжатые данные, а затем начинается новая структура: Central Directory, ее мы опишем более подробно в следующем разделе.

 

1.4. Структура Central Directory

Структура Central Directory представляет собой расширенное представление данных, находящихся в Local File Header. По сути, для основной работы с ZIP-архивами достаточно данных из Local File Header. Тем не менее, использование структуры Central Directory является обязательным, и ее значения должны быть корректно заполнены. Эта структура имеет свой уникальный идентификатор 0x02014B50. На MQL5 ее представление будет следующим:

//+------------------------------------------------------------------+
//| Central directory structure                                      |
//+------------------------------------------------------------------+
struct ZipCentralDirectory
  {
   uint   header;                 // Central directory header, always equals 0x02014B50
   ushort made_ver;               // Version made by
   ushort version;                // Minimum version for extracting
   ushort bit_flag;               // Bit flag
   ushort comp_method;            // Compressed method (0 - uncompressed, 8 - deflate)
   ushort last_mod_time;          // File modification time
   ushort last_mod_date;          // File modification date
   uint   crc_32;                 // CRC32 hash
   uint   comp_size;              // Compressed size
   uint   uncomp_size;            // Uncompressed size
   ushort filename_length;        // Length of the file name
   ushort extrafield_length;      // Length field with additional data
   ushort file_comment_length;    // Length of comment file
   ushort disk_number_start;      // Disk number start
   ushort internal_file_attr;     // Internal file attributes
   uint   external_file_attr;     // External file attributes
   uint   offset_header;          // Relative offset of local header
   bool   LoadFromCharArray(uchar &array[]);
   int    ToCharArray(uchar &array[]);
   ZipCentralDirectory() : header(0x02014B50),
                           made_ver(20),
                           version(10),
                           bit_flag(0),
                           comp_method(DEFLATE),
                           last_mod_time(0),
                           last_mod_date(0),
                           crc_32(0),
                           comp_size(0),
                           uncomp_size(0),
                           filename_length(0),
                           extrafield_length(0),
                           file_comment_length(0),
                           disk_number_start(0),
                           internal_file_attr(0),
                           external_file_attr(0)
                           {;}
  };

Как видно, она содержит уже больше данных, однако большая часть из них дублирует данные Local File Header. Так же как и предыдущая структура, она содержит сервисные методы по конвертации своего содержимого в байт-массив и обратно.

Опишем ее поля:

  • header — уникальный идентификатор структуры, равен 0x02014B50;
  • made_ver — версия стандарта архивирования, используемого при архивировании;
  • version — минимальная версия стандарта для успешной распаковки файла;
  • bit_flag — битовый флаг, имеет идентификатор 0x02;
  • comp_method — тип используемого сжатия. Как правило, всегда используется метод сжатия DEFLATE, этот тип сжатия имеет идентификатор 0x08.
  • last_mod_time — время последней модификации файла. Содержит часы, минуты и секунды модификации файла в формате MS-DOS. Этот формат описан на странице компании Microsoft.
  • last_mod_date — дата последней модификации файла. Содержит день месяца, номер месяца в году и год модификации файла в формате MS-DOS.
  • crc_32 — контрольная сумма CRC-32. Используется программами по работе с архивами для определения ошибок содержимого файла. Если это поле не заполнено, ZIP-архиватор откажется распаковывать упакованный файл, ссылаясь на испорченный файл.
  • comp_size — размер в байтах упакованных данных;
  • uncomp_size — размер в байтах исходных данных;
  • filename_length — длина имени файла;
  • extrafield_length — специальное поле для записи дополнительных атрибутов данных. Практически никогда не используется и равно нулю.
  • file_comment_length — длина комментария к файлу;
  • disk_number_start — номер диска, в который записывается архив. Практически всегда равен нулю.
  • internal_file_attr — атрибуты файла в формате MS-DOS;
  • external_file_attr — расширенные атрибуты файла в формате MS-DOS;
  • offset_header — адрес, по которому располагается начало структуры Local File Header.

При сохранении этой структуры в архиве создается последовательность байтов, хранящая значения ее полей. Приведем схему расположения байтов этой структуры, подобно рисунку 5:

Рис. 6. Байт-схема структуры Central Directory в архиве HelloWorld.zip

Рис. 6. Байт-схема структуры Central Directory в архиве HelloWorld.zip

В отличие от Local File Header, структуры Central Directory идут последовательно друг за другом. Адрес начала первой из них указан в специальном завершающем блоке данных — структуре ECDR. Более подробно эту структуру мы опишем в следующем разделе.

 

1.5. Структура End of Central Directory Record (ECDR)

Структура End of Central Directory Record (или просто ECDR) завершает ZIP-файл. Ее уникальный идентификатор равен 0x06054B50. В каждом архиве она содержится в единственном экземпляре. ECDR хранит количество файлов и директорий, находящихся в архиве, а также адрес начала последовательности структур Central Directory и их суммарный размер. Помимо этого, блок данных хранит и другие сведения. Приведем полное описание ECDR на MQL5:

//+------------------------------------------------------------------+
//| End of central directory record structure                        |
//+------------------------------------------------------------------+
struct ZipEndRecord
  {
   uint   header;                // Header of end central directory record, always equals 0x06054b50
   ushort disk_number;           // Number of this disk
   ushort disk_number_cd;        // Number of the disk with the start of the central directory
   ushort total_entries_disk;    // Total number of entries in the central directory on this disk
   ushort total_entries;         // Total number of entries in the central directory
   uint   size_central_dir;      // Size of central directory
   uint   start_cd_offset;       // Starting disk number
   ushort file_comment_lengtt;   // File comment length
   string FileComment(void);       
   bool   LoadFromCharArray(uchar& array[]);
   int    ToCharArray(uchar &array[]);
   ZipEndRecord(void) : header(0x06054B50){;}
  };

Опишем поля этой структуры более подробно:

  • header — уникальный идентификатор структуры, равен 0x06054B50;
  • disk_number — номер диска;
  • disk_number_cd — номер диска, с которого начинается Central Directory;
  • total_entries_disk — всего записей в секции Central Directory (количество файлов и директорий);
  • total_entries — всего записей (количество файлов и директорий);
  • size_central_dir — размер секции Central Directory;
  • start_cd_offset — байт-адрес начала секции Central Directory;
  • file_comment_length — длина комментария к архиву.

При сохранении этой структуры в архиве создается последовательность байтов, хранящая значения ее полей. Приведем схему расположения байтов этой структуры:

Рис. 7. Байт-схема структуры ECDR

Рис. 7. Байт-схема структуры ECDR

Этот блок данных будет использован нами при определении количества элементов в массиве.


Глава 2. Обзор класса CZip и его алгоритмов


2.1. Структура упакованных файлов в архиве, классы CZipFile и CZipFolder

Итак, в первой главе мы подробно разобрали формат ZIP-архива. Мы рассмотрели, из каких типов данных он состоит, и описали эти типы в соответствующих структурах. Определив эти типы, мы реализуем высокоуровневый специализированный класс CZip, с помощью которого можно будет легко и просто совершать следующие действия с ZIP-архивом:

  • Создавать новый архив;
  • Открывать ранее созданный архив на жестком диске;
  • Загружать архив с удаленного сервера;
  • Добавлять новые файлы в архив;
  • Удалять файлы из архива;
  • Распаковывать архив как полностью, так и отдельные его файлы. 

Класс CZip возьмет на себя правильное заполнение необходимых структур в архиве, предоставляя нам привычный высокоуровневый интерфейс для работы с файловой коллекцией архива. Возможностей, предоставляемых классом, будет более чем достаточно для подавляющего большинства задач, связанных с архивированием.

Очевидно, что содержимое ZIP-архива можно разделить на папки и файлы. Оба типа содержимого имеют обширный набор свойств: имяразмератрибуты файлавремя создания и т.д. Некоторые из этих свойств являются общими как для папок, так и для файлов, а некоторые, как, например, упакованные данные, нет. Поэтому оптимальным решением для работы с архивом будет предоставление специальных служебных классов: CZipFile и CZipDirectory. Именно эти классы будут представлять файлы и папки соответственно. Условная классификация содержимого в архиве приведена на схеме ниже:


 Рис. 8. Условная классификация объектов в архиве

Таким образом, чтобы добавить файл в архив CZip, необходимо вначале создать объект типа CZipFile, а затем добавить этот объект-файл в архив. В качестве примера создадим текстовый файл "HelloWorld.txt", содержащий одноименный текст "HelloWorld!" и добавим его в архив:

//+------------------------------------------------------------------+
//| Create file with 'HelloWorld' message                            |
//+------------------------------------------------------------------+
void CreateHelloWorld()
  {
   CZip zip;                                                // создаем пустой ZIP-архив
   uchar content[];
   StringToCharArray("HelloWorld!",content,0,
                     StringLen("HelloWorld!"));             // записываем в байтовый массив фразу "HelloWorld!"
   CZipFile* file = new CZipFile("HelloWorld.txt",content); // создаем ZIP-файл с именем "HelloWorld.txt"
                                                            // содержащий байтовый массив "HelloWorld!"
   zip.AddFile(file);                                       // добавляем ZIP-файл в архив
   zip.SaveZipToFile("HellowWorld.zip",FILE_COMMON);        // сохраняем архив на диске под именем "HelloWorld.zip"
   printf("Size: "+(string)zip.Size());
  }

После выполнения этого кода на диске компьютера появится новый ZIP-архив, содержащий единственный текстовый файл "HelloWorld.txt" с одноименной фразой. Если бы мы захотели создать папку вместо файла, то вместо CZipFile нам необходимо было бы создать экземпляр класса CZipFolder. Для его создания достаточно было бы указать только имя. 

Как уже было сказано, классы CZipFile и CZipFolder имеют много общего. Поэтому оба класса наследуются от их общего прародителя — CZipContent. Этот класс содержит общие методы и данные для работы с содержимым архива.

 

2.2. Создание упакованных файлов с помощью CZipFile

Создание упакованного ZIP-файла тождественно созданию экземпляра CZipFile. Как известно, чтобы создать файл, необходимо указать его имя и содержимое. Поэтому конструктор CZipFile также требует явного указания соответствующих параметров:

//+------------------------------------------------------------------+
//| Create ZIP file from file array and name                         |
//+------------------------------------------------------------------+
CZipFile::CZipFile(string name,uchar &file_src[]) : CZipContent(ZIP_TYPE_FILE,name)
  {
   AddFileArray(file_src);
  }

В разделе 2.1 был показан вызов именно этого конструктора.

Кроме того, иногда требуется не создавать файл, а загрузить уже имеющийся файл с диска. Для этого случая в классе CZipFile существует второй конструктор, позволяющий создать ZIP-файл на основе обычного файла на жестком диске:

//+------------------------------------------------------------------+
//| Create ZIP file from file array and name                         |
//+------------------------------------------------------------------+
CZipFile::CZipFile(string path_file,int file_common) : CZipContent(ZIP_TYPE_FILE,"")
  {
   AddFile(path_file,file_common);
  }

Вся работа в этом конструкторе делегируется приватному методу AddFile. Алгоритм его работы следующий:

  1. Открывается указанный файл для чтения, его содержимое считывается в байт-массив.
  2. Полученный байт-массив упаковывается методом AddFileArray и хранится в специальном динамическом массиве типа uchar.

Метод AddFileArray является "сердцем" всей системы классов для работы с архивами. Ведь именно в этом методе находится самая важная системная функция — CryptEncode. Приведем исходный код этого метода:

//+------------------------------------------------------------------+
//| Add file array and zip it.                                       |
//+------------------------------------------------------------------+
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;
  }

Желтым цветом указано конфигурирование функции CryptEncode c последующем архивированием байт-массива. Таким образом, можно сделать вывод, что упаковка файла происходит в момент создания объекта CZipFile, а не в момент создания или сохранения самого ZIP-архива, как можно было бы подумать. Благодаря этому свойству все данные, переданные в класс CZip, автоматически сжимаются, а значит, требуют для своего хранения меньше оперативной памяти.

Обратите внимание, что во всех случаях в качестве данных используется беззнаковый байт-массив uchar. В самом деле, все данные, которыми мы оперируем на компьютере, можно представить как некую последовательность байтов. Поэтому для создания действительно универсального контейнера сжатых данных, чем CZipFile и является по своей сути, был выбран беззнаковый массив uchar. 

   Пользователю необходимо самостоятельно сконвертировать данные для хранения в архиве в беззнаковый массив uchar[], который, в свою очередь, также необходимо передать по ссылке в качестве содержимого файла для класса CZipFile. Благодаря этой особенности в ZIP-архиве может располагаться абсолютно любой тип файлов, как загруженный с диска, так и созданный в процессе работы MQL-программы.

Распаковка данных является более тривиальной задачей. Для распаковки данных в исходный байт массив file_array используется метод GetUnpackFile, который по сути является методом-обложкой для системной функции CryptDecode:

//+------------------------------------------------------------------+
//| Get unpack file.                                                 |
//+------------------------------------------------------------------+
void CZipFile::GetUnpackFile(uchar &file_array[])
  {
   uchar key[]={1,0,0,0};
   CryptDecode(CRYPT_ARCH_ZIP,m_file_puck,key,file_array);
  }

  

2.3. Вспоминаем MS-DOS. Формат времени и даты в ZIP-архиве

Формат хранения данных ZIP создавался в конце 80-ых годов прошлого века для платформы MS-DOS, "правопреемником" которой стала Windows. В то время ресурсы для хранения данных были ограничены, поэтому дату и время ОС MS-DOS хранила отдельно: два байта (или машинное слово для 16-разрядных процессоров того времени) выделялось для даты и два байта для времени. Причем самая ранняя дата, которая могла быть представлена этим форматом, была 1 января 1980 года (01.01.1980). Минуты, часы, дни, месяцы и годы занимали определенные диапазоны битов в машинном слове, и для того чтобы извлечь или записать их, по-прежнему приходится прибегать к битовым операциям.

Спецификация этого формата указана на сайте Microsoft по следующей ссылке: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx.

Приведем формат хранения даты в соответствующем двухбайтовом поле:

№ битовОписание
 0-4 День месяца (0-31)
 5-8 Номер месяца (1 — январь, 2 — февраль и т.д.)
 9-15 Номер года начиная с 1980

Таблица 1. Формат хранения даты в двухбайтовом поле

Аналогично, приведем формат хранения времени в соответствующем двухбайтовом поле:

№ битовОписание
 0-4 Секунды (точностью хранения +/-  2 секунды)
 5-10 Минуты (0-59)
 11-15 Часы в 24-часовом формате

 Таблица 2. Формат хранения времени в двухбайтовом поле

Зная спецификацию данного формата и умея работать с битовыми операциями, можно написать соответствующие функции, конвертирующие дату и время в формате MQL в формат MS-DOS. Можно также написать обратные процедуры. Такие методы конвертации общие как для папок, представленных CZipFolder, так и для файлов, представленных CZipFile. Задавая таким образом дату и время для них в привычном MQL формате, мы "за кулисами" конвертируем этот тип данных в формат MS-DOS. Таким конвертированием занимаются методы DosDate, DosTime, MqlDate и MqlTime. Приведем их исходный код.

Конвертация даты формата MQL в формат даты MS-DOS:

//+---------------------------------------------------------------------------------+
//| Get data in MS-DOS format. See specification on:                                |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx |
//+---------------------------------------------------------------------------------+
ushort CZipContent::RjyDosDate(datetime date)
  {
   ushort dos_date=0;
   MqlDateTime time={0};
   TimeToStruct(date,time);
   if(time.year>1980)
     {
      dos_date = (ushort)(time.year-1980);
      dos_date = dos_date << 9;
     }
   ushort mon=(ushort)time.mon<<5;
   dos_date = dos_date | mon;
   dos_date = dos_date | (ushort)time.day;
   return dos_date;
  }

Конвертация даты в формате MS-DOS в формат MQL:

//+---------------------------------------------------------------------------------+
//| Get data in MQL format. See specification on:                                   |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx |
//+---------------------------------------------------------------------------------+
datetime CZipContent::MqlDate(void)
  {
   MqlDateTime time={0};
   ushort date=m_directory.last_mod_date;
   time.day = date & 0x1F;
   time.mon = date & 0xE0;
   time.year= 1980+(date & 0xFE00);
   return StructToTime(time);
  }

Конвертация времени формата MS-DOS в формат времени MQL:

//+---------------------------------------------------------------------------------+
//| Get Time in MS-DOS format. See specification on:                                |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx |
//+---------------------------------------------------------------------------------+
ushort CZipContent::DosTime(datetime time)
  {
   ushort date=0;
   MqlDateTime mql_time={0};
   TimeToStruct(time,mql_time);
   date=(ushort)mql_time.hour<<11;
   ushort min=(ushort)mql_time.min<<5;
   date = date | min;
   date = date | (ushort)(mql_time.sec/2);
   return date;
  }

Конвертация времени формата MS-DOS в формат времени MQL:

//+---------------------------------------------------------------------------------+
//| Get data in MQL format. See specification on:                                   |
//| https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx |
//+---------------------------------------------------------------------------------+
datetime CZipContent::MqlTime(void)
  {
   MqlDateTime time={0};
   ushort date=m_directory.last_mod_time;
   time.sec = (date & 0x1F)*2;
   time.min = date & 0x7E0;
   time.hour= date & 0xF800;
   return StructToTime(time);
  }

Данные методы используют внутренние переменные для хранения даты и времени: m_directory.last_mod_time и m_directory.last_mod_date, где m_directory — структура типа Central Directory.

 

2.4. Генерация контрольной суммы CRC-32

Интересной особенностью формата ZIP-архива является хранение, помимо служебных данных, специальной информации для восстановления, которая в некоторых случаях помогает восстановить поврежденные данные. Для того чтобы понять, являются ли полученные данные целыми или поврежденными, в ZIP-архиве содержится специальное дополнительное поле, хранящее двухбайтовое значение специального хеша CRC-32. Это контрольная сумма, которая рассчитывается для данных до упаковки. Архиватор, распаковав данных из архива, заново рассчитывает эту контрольную сумму, и если она не совпадает, считает, что данные повреждены и не могут быть предоставлены пользователю.

Таким образом, нашему классу CZip необходимо иметь свой собственный алгоритм расчета CRC-32. В противном случае архивы, созданные нашим классом, откажутся читать сторонние средства для работы с ними, например WinRAR выдаст ошибку-предупреждение о поврежденных данных:


Рис. 9. Предупреждение архиватора WinRAR о повреждении данных файла "HelloWorld.txt"

 

Поскольку контрольная сумма CRC-32 требуется только для файлов, метод, рассчитывающий эту сумму, представлен лишь в классе CZipFile. Метод реализован на основе примера на языке программирования Си, приведенного в статье по адресу: https://ru.wikibooks.org/wiki/Реализации_алгоритмов/Циклический_избыточный_код:

//+------------------------------------------------------------------+
//| Return CRC-32 sum on source data 'array'                         |
//+------------------------------------------------------------------+
uint CZipFile::CRC32(uchar &array[])
  {
   uint crc_table[256];
   ArrayInitialize(crc_table,0);
   uint crc=0;
   for(int i=0; i<256; i++)
     {
      crc=i;
      for(int j=0; j<8; j++)
         crc=(crc  &1)>0 ?(crc>>1)^0xEDB88320 : crc>>1;
      crc_table[i]=crc;
     }
   crc=0xFFFFFFFF;
   int len=0,size=ArraySize(array);
   while(len<size)
      crc=crc_table[(crc^array[len++])  &0xFF]^(crc>>8);
   return crc ^ 0xFFFFFFFF;
  }

Чтобы убедиться в правильности работы метода, достаточно открыть архив, созданный с помощью CZip, в архиваторе WinRAR. Каждый файл будет иметь свой уникальный код CRC-32:


Рис. 10. Контрольная сумма CRC-32 в окне архиватора WinRAR

Файлы с корректным CRC-32 хешем архиватор распаковывает в штатном режиме, без появления соответствующих предупреждающих сообщений.

 

2.5. Чтение и запись архива 

Последнее, что мы разберем, будут методы для чтения и записи самого ZIP-архива. Очевидно, что если у нас есть коллекция, например CArrayObj, состоящая из элементов CZipFile и CZipFolder, задача по формированию самого архива будет тривиальной. Достаточно каждый элемент сконвертировать в байт-последовательность и записать ее в файл. Этими задачами занимаются следующие методы:

  • SaveZipToFile — открывает указанный файл и записывает в него сгенерированный байт-массив архива.
  • ToCharArray — создает соответствующую байтовую структуру архива. Генерирует завершающую структуру ECDR.
  • ZipElementsToArray — преобразует элемент типа CZipContent в последовательность байтов.

Единственная сложность состоит в том, что каждый элемент архива, представленный типом CZipContent, хранится в двух разных частях файла, в структурах Local File Header и Central Directory. Поэтому необходимо использовать специальный вызов метода ZipElementsToArray, который в зависимости от переданного ему модификатора ENUM_ZIP_PART выдает либо байтовый массив типа Local File Header, либо Central Directory. 

Теперь, учитывая эту особенность, нам должно быть понятно содержимое этих трех методов, исходный код которых представлен ниже:

//+------------------------------------------------------------------+
//| Return uchar array with ZIP elements                             |
//+------------------------------------------------------------------+
void CZip::ZipElementsToArray(uchar &zip_elements[],ENUM_ZIP_PART part)
  {
   CArrayObj elements;
   int totalSize=0;
   for(int i=0; i<m_archive.Total(); i++)
     {
      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)
        {
         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);
   int offset= 0;
   for(int i = 0; i<elements.Total(); i++)
     {
      CCharArray *objArray=elements.At(i);
      uchar array[];
      objArray.GetArray(array);
      ArrayCopy(zip_elements,array,offset);
      offset+=ArraySize(array);
     }
  }
//+------------------------------------------------------------------+
//| Generate ZIP archive as uchar array.                             |
//+------------------------------------------------------------------+
void CZip::ToCharArray(uchar &zip_arch[])
  {
   uchar elements[],directories[],ecdr_array[];
   ZipElementsToArray(elements,ZIP_PART_HEADER);
   ZipElementsToArray(directories,ZIP_PART_DIRECTORY);
   ZipEndRecord ecdr;
   ecdr.total_entries_disk=(ushort)m_archive.Total();
   ecdr.total_entries=(ushort)m_archive.Total();
   ecdr.size_central_dir= sizeof(ZipCentralDirectory)*m_archive.Total();
   ecdr.start_cd_offset = ArraySize(elements);
   ecdr.ToCharArray(ecdr_array);
   int totalSize=ArraySize(elements)+ArraySize(directories)+ArraySize(ecdr_array);
   ArrayResize(zip_arch,totalSize);
   ArrayCopy(zip_arch,elements,0);
   ArrayCopy(zip_arch,directories,ArraySize(elements));
   ArrayCopy(zip_arch,ecdr_array,ArraySize(elements)+ArraySize(directories));
  }
//+------------------------------------------------------------------+
//| Save ZIP archive in file zip_name                                |
//+------------------------------------------------------------------+
bool CZip::SaveZipToFile(string zip_name,int file_common)
  {
   uchar zip[];
   ToCharArray(zip);
   int handle= FileOpen(zip_name,FILE_BIN|FILE_WRITE|file_common);
   if(handle == INVALID_HANDLE)return false;
   FileWriteArray(handle,zip);
   FileClose(handle);
   return true;
  }

Загрузка архива также обладает некоторыми нюансами. Очевидно, что загрузка архива — операция, обратная сохранению. Если при сохранении архива элементы типа CZipContent преобразуются в байтовую последовательность, то при загрузке архива байтовая последовательность преобразуется в элементы типа CZipContent. И снова, из-за того, что каждый элемент в архиве хранится в двух разных частях файла — Local File Header и Central Directory, элемент CZipContent за одно чтение данных создать невозможно.

Необходимо использовать промежуточный класс-контейнер CSourceZip, в который вначале последовательно добавляются нужные элементы, а затем на его основе формируется нужный тип данных — CZipFile или CZipFolder. Именно поэтому эти два класса имеют дополнительный конструктор, принимающий в качестве ссылочного параметра указатель на элемент типа CSourceZip. Данный вид инициализации, как и сам класс CSourceZip, создавался исключительно для служебного использования классом CZip, и его не рекомендуется использовать в явном виде.

За саму загрузку ответственны три метода класса CZip:

  • LoadZipFromFile — открывает указанный файл и читает его содержимое в байт-массив.
  • LoadHeader — загружает по предложенному адресу из байтового массива архива структуру Local File.
  • LoadDirectory — загружает по предложенному адресу из байтового массива архива структуру Central Directory.

Итак, приведем исходный код этих методов:

//+------------------------------------------------------------------+
//| Load Local Header with name file by offset array.                |
//| RETURN:                                                          |
//| Return address after local header, name and zip content.         |
//| Return -1 if read failed.                                        |
//+------------------------------------------------------------------+
int CZip::LoadHeader(uchar &zip_array[],int offset,CSourceZip &zip)
  {
//Copy local header
   uchar header[];
   ArrayCopy(header,zip_array,0,offset,sizeof(ZipLocalHeader));
   if(!zip.header.LoadFromCharArray(header))return -1;
   offset+=ArraySize(header);
   uchar name[];
//Copy header file name
   ArrayCopy(name,zip_array,0,offset,zip.header.filename_length);
   zip.header_file_name=CharArrayToString(name);
   offset+=ArraySize(name);
//Copy zip array
   ArrayCopy(zip.zip_array,zip_array,0,offset,zip.header.comp_size);
   offset+=ArraySize(zip.zip_array);
   return offset;
  }
//+------------------------------------------------------------------+
//| Load Central Directory with name file by offset array.           |
//| RETURN:                                                          |
//| Return adress after CD and name.                                 |
//| Return -1 if read failed.                                        |
//+------------------------------------------------------------------+
int CZip::LoadDirectory(uchar &zip_array[],int offset,CSourceZip &zip)
  {
//Copy central directory
   uchar directory[];
   ArrayCopy(directory,zip_array,0,offset,sizeof(ZipCentralDirectory));
   if(!zip.directory.LoadFromCharArray(directory))return -1;
   offset+=ArraySize(directory);
   uchar name[];
//Copy directory file name
   ArrayCopy(name,zip_array,0,offset,zip.directory.filename_length);
   zip.directory_file_name=CharArrayToString(name);
   offset+=ArraySize(name);
   return offset;
  }
//+------------------------------------------------------------------+
//| Load ZIP archive from HDD file.                                  |
//+------------------------------------------------------------------+
bool CZip::LoadZipFromFile(string full_path,int file_common)
  {
   uchar zip_array[];
   ZipEndRecord ecdr;
   if(!LoadZipFile(full_path, file_common, zip_array))return false;
   if(!TakeECDR(zip_array, ecdr))return false;
   CSourceZip sources[];
   ArrayResize(sources,ecdr.total_entries);
   int offset=0;
   int entries=ecdr.total_entries;
   for(int entry=0; entry<ecdr.total_entries; entry++)
      offset=LoadHeader(zip_array,offset,sources[entry]);
   for(int entry=0; entry<ecdr.total_entries; entry++)
      offset=LoadDirectory(zip_array,offset,sources[entry]);
   for(int entry=0; entry<ecdr.total_entries; entry++)
     {
      bool is_folder=sources[entry].header.bit_flag==3;
      CZipContent *content=NULL;
      if(is_folder)
         content=new CZipDirectory(sources[entry]);
      else
         content=new CZipFile(sources[entry]);
      m_archive.Add(content);
     }
   return true;
  }


Глава 3. Примеры использования класса CZip, измерение производительности

В предыдущей главе мы рассмотрели класс CZip и собственно формат данных ZIP-архива. Теперь, зная устройство архива, а также общие принципы работы с классом CZip, мы можем перейти к рассмотрению практических задач, связанных с архивированием. В данной главе подробно рассмотрены три разносторонних примера, которые наиболее полно охватывают весь спектр задач этого класса.


3.1. Создание ZIP-архива с котировками по всем выбранным символам

Первая задача, которую часто требуется решать, это сохранение ранее полученных данных. Часто данные получаются в самом терминале MetaTrader. Такими данными могут быть последовательность накопленных тиков или котировки в формате OHLCV. Мы рассмотрим задачу, когда котировки требуется сохранить в специальные CSV-файлы, формат которых будет следующим:

Date;Time;Open;High;Low;Close;Volume
31.08.2015;16:48;1.11767;1.12620;1.11692;1.12020;87230

Это текстовый формат данных. Он часто используется для передачи данных между разными системами статистического анализа. Формат текстовых файлов, к сожалению, обладает большой избыточностью хранения данных, так как в каждом байте используется лишь очень ограниченное количество знаков. Как правило, это знаки пунктуации, цифры, заглавные и строчные буквы алфавита. Кроме того, многие значения в этом формате часто повторяются, например, дата или цена открытия, как правило, одна и та же для большого массива данных. Поэтому сжатие этого типа данных должно быть эффективным. 

Итак, давайте напишем скрипт, загружающий необходимые данные из терминала. Его алгоритм будет следующим:

  • Последовательно выбираются инструменты, размещенные в окне Market Watch.
  • По каждому выбранному инструменту запрашиваются котировки для каждого из 21 таймфрейма.
  • Котировки выбранного таймфрейма конвертируются в массив CSV строк.
  • Массив CSV строк конвертируется в байтовый массив.
  • Создается ZIP-файл (CZipFile), содержащий байтовый массив котировок, после чего он добавляется в архив.
  • После создания всех файлов котировок архив CZip сохраняется на диске компьютера в файл Quotes.zip.

Исходный код скрипта, выполняющий эти действия, представлен ниже:

//+------------------------------------------------------------------+
//|                                                     ZipTask1.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Zip\Zip.mqh>
CZip Zip;      // Create empty ZIP archive.
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CopyBarsToCSV();
  }
//+------------------------------------------------------------------+
//| Create ZIP with quotes from market watch                         |
//+------------------------------------------------------------------+
void CopyBarsToCSV(void)
  {
   bool MarketWatch=true;
   for(int i=0; i<SymbolsTotal(MarketWatch); i++)
     {
      string symbol=SymbolName(i,MarketWatch);
      printf("Save quotes "+symbol+"...");
      for(int t=1; t<22; t++)
        {
         ENUM_TIMEFRAMES tf=TimeframeAt(t);
         MqlRates rates[];
         CopyRates(symbol,tf,0,100,rates);
         string csv_lines[];
         uchar src_array[];
         RatesToCSV(rates,csv_lines);
         LinesToCharArray(csv_lines,src_array);
         string name_arch=GenName(symbol,tf);
         CZipFile *file=new CZipFile(name_arch,src_array);
         Zip.AddFile(file);
        }
     }
   Zip.SaveZipToFile("Quotes.zip",FILE_COMMON);
  }

ENUM_TIMEFRAMES TimeframeAt(int index)
  {
   switch(index)
     {
      case 1: return PERIOD_M1;
      case 2: return PERIOD_M2;
      case 3: return PERIOD_M3;
      case 4: return PERIOD_M4;
      case 5: return PERIOD_M5;
      case 6: return PERIOD_M6;
      case 7: return PERIOD_M10;
      case 8: return PERIOD_M12;
      case 9: return PERIOD_M15;
      case 10: return PERIOD_M20;
      case 11: return PERIOD_M30;
      case 12: return PERIOD_H1;
      case 13: return PERIOD_H2;
      case 14: return PERIOD_H3;
      case 15: return PERIOD_H4;
      case 16: return PERIOD_H6;
      case 17: return PERIOD_H8;
      case 18: return PERIOD_H12;
      case 19: return PERIOD_D1;
      case 20: return PERIOD_W1;
      case 21: return PERIOD_MN1;
     }
   return PERIOD_CURRENT;
  }

void RatesToCSV(MqlRates &rates[],string &csv_lines[])
  {
   string t=";";
   ArrayResize(csv_lines,ArraySize(rates));
   for(int i=0; i<ArraySize(rates); i++)
     {
      csv_lines[i] =  TimeToString(rates[i].time,TIME_DATE|TIME_MINUTES)+ t;
      csv_lines[i]+= DoubleToString(rates[i].open,5) + t +
                    DoubleToString(rates[i].high, 5) + t +
                    DoubleToString(rates[i].low, 5) + t +
                    DoubleToString(rates[i].close, 5) + t +
                    (string)rates[i].tick_volume+t+"\n";
     }
  }

void LinesToCharArray(string &csv_lines[],uchar &src_array[])
  {
   int size=0;
   for(int i=0; i<ArraySize(csv_lines); i++)
      size+=StringLen(csv_lines[i]);
   ArrayResize(src_array,size);
   size=0;
   for(int i=0; i<ArraySize(csv_lines); i++)
     {
      uchar array[];
      StringToCharArray(csv_lines[i],array);
      ArrayCopy(src_array,array,size,0,WHOLE_ARRAY);
      size+=ArraySize(array);
     }
  }

string GenName(string symbol,ENUM_TIMEFRAMES tf)
  {
   string stf=EnumToString(tf);
   string period=StringSubstr(stf,6);
   string name=symbol+"\\"+symbol+period+".csv";
   return name;
  }
//+------------------------------------------------------------------+

Загрузка данных может занимать существенное время, поэтому в окне Market Watch были выбраны только четыре символа. Кроме того, мы загрузим лишь сто последних известных баров. Это тоже должно сократить время выполнения скрипта. После его выполнения в папке общих файлов MetaTrader появился архив Quotes.zip. Его содержимое можно увидеть в любой программе по работе с архивами, например WinRAR:

 

Рис. 11. Сохраненные файлы котировок, просматриваемые в архиваторе WinRAR

Созданный архив сжат втрое по сравнению с исходным размером. Об этом нам сообщает сам архиватор WinRAR:

 

Рис. 12. Степень сжатия сгенерированного архива в информационном окне WinRAR
 

Это хороший результат сжатия. Однако еще лучшего коэффициента сжатия удалось бы добиться на больших и немногочисленных файлах.

Пример скрипта, создающий котировки и сохраняющий их в zip архив прикреплен к данной статье под именем ZipTask1.mq5 и находится в папке Scripts. 

 

3.2. Загрузка архива с удаленного сервера на примере сайта MQL5.com

Следующая рассматриваемая задача будет сетевой. Наш пример продемонстрирует, как можно загружать ZIP-архивы с удаленных серверов. В качестве примера мы произведем загрузку индикатора Alligator, находящегося в базе исходных кодов Code Base по адресу https://www.mql5.com/ru/code/9:

 

Для каждого опубликованного в Code Base индикатора, советника, скрипта или библиотеки существует архивная версия, где все исходные коды продукта упакованы в единый архив. Именно эту архивную версию мы скачаем и распакуем на локальном компьютере. Но прежде чем это сделать, необходимо поставить разрешение на доступ к mql5.com, для чего в окне Сервис --> Настройки --> Советники необходимо вписать адрес "https://www.mql5.com" в список разрешенных серверов.

Класс CZip обладает собственным методом для загрузки архивов с интернет-ресурсов. Но вместо его использования давайте напишем собственный скрипт, совершающий такую загрузку:

//+------------------------------------------------------------------+
//|                                                     ZipTask2.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Zip\Zip.mqh>

CZip Zip;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   string cookie,headers;
   string mql_url="https://www.mql5.com/ru/code/download/9";
   int timeout=5000;
   uchar data[],zip_array[];
   if(!WebRequest("GET",mql_url,cookie,NULL,timeout,data,0,zip_array,headers))
     {
      printf("Unable to download ZIP archive from "+mql_url+". Check request and permissions EA.");
      return;
     }
   if(!Zip.CreateFromCharArray(zip_array))
     {
      printf("Loaded bad ZIP archive. Check results array.");
      return;
     }
   printf("Archive successfully loaded. Total files: "+(string)Zip.TotalElements());
   Zip.UnpackZipArchive("Alligator",FILE_COMMON);
  }
//+------------------------------------------------------------------+

Как видите, исходный код скрипта довольно простой. Вначале вызывается WebRequest с адресом удаленного ZIP-архива. WebRequest закачивает байт-массив архива в результирующий массив zip_array, после чего он загружается в класс CZip посредством метода CreateFromCharArray. Этот метод позволяет создать архив прямо из последовательности байтов, что бывает иногда необходимым при внутренней работе с архивами.

Помимо метода CreateFromCharArray, CZip содержит специальный метод LoadZipFromUrl для загрузки архивов по интернет-ссылке. Он работает примерно так же, как и наш предыдущий скрипт. Приведем его исходный код:

//+------------------------------------------------------------------+
//| Load ZIP archive from url                                        |
//+------------------------------------------------------------------+
bool CZip::LoadZipFromUrl(string url)
  {
   string cookie,headers;
   int timeout=5000;
   uchar data[],zip_array[];
   if(!WebRequest("GET",url,cookie,NULL,timeout,data,0,zip_array,headers))
     {
      SetUserError(ZIP_ERROR_BAD_URL);
      return false;
     }
   return CreateFromCharArray(zip_array);
  }

Результат работы этого метода будет аналогичным: через некоторое время будет создан ZIP-архив, содержимое которого будет скачано с удаленного сервера.

Пример скрипта, загружающий архив из CodeBase прикреплен к данной статье под именем ZipTask2.mq5 и находится в папке Scripts. 

 

3.3. Сжатие служебных данных программы в оперативной памяти компьютера

Сжатие внутренних данных программы в оперативной памяти компьютера — нетривиальный способ использования архивирования. Этот способ можно использовать, когда обрабатываемых данных слишком много для размещения в оперативной памяти. Однако при использовании данного подхода общая производительность программы снизится, ведь потребуются дополнительные действия по архивированию/разархивированию служебных структур или данных.

Предположим, что MQL-программе необходимо хранить коллекцию исторических ордеров. Каждый ордер будет описываться специальной структурой Order, которая будет содержать все его свойства: идентификатор, тип ордера, время исполнения, объем и т.д. Опишем данную структуру:

//+------------------------------------------------------------------+
//| History order                                                    |
//+------------------------------------------------------------------+
struct Order
  {
private:
   uchar m_comment[32];
   uchar m_symbol[32];
public:
   ulong ticket;                          // Ticket order
   datetime time_setup;                   // Time setup order
   ENUM_ORDER_TYPE type;                  // Type order
   ENUM_ORDER_STATE state;                // State order
   datetime time_exp;                     // Expiration time
   datetime time_done;                    // Time done or canceled order
   long time_setup_msc;                   // Time setup in msc
   long time_done_msc;                    // Time done in msc
   ENUM_ORDER_TYPE_FILLING filling;       // Type filling
   ENUM_ORDER_TYPE_TIME type_time;        // Type living time
   ulong magic;                           // Magic of order
   ulong position_id;                     // ID position
   double vol_init;                       // Volume init
   double vol_curr;                       // Volume current
   double price_open;                     // Price open
   double sl;                             // Stop-Loss level
   double tp;                             // Take-Profit level
   double price_current;                  // Price current
   double price_stop_limit;               // price stop limit
   string Comment(void);
   string Symbol(void);
   void Comment(string comment);
   void Symbol(string symbol);
   void ToCharArray(uchar& array[]);
   void InitByTicket(ulong ticket);
  };
//+------------------------------------------------------------------+
//| Init by ticket                                                   |
//+------------------------------------------------------------------+
void Order::InitByTicket(ulong id)
  {
   this.ticket= id;
   time_setup =(datetime)HistoryOrderGetInteger(ticket,ORDER_TIME_SETUP);
   type=(ENUM_ORDER_TYPE)HistoryOrderGetInteger(ticket,ORDER_TYPE);
   state=(ENUM_ORDER_STATE)HistoryOrderGetInteger(ticket,ORDER_STATE);
   time_exp=(datetime)HistoryOrderGetInteger(ticket,ORDER_TIME_EXPIRATION);
   time_done=(datetime)HistoryOrderGetInteger(ticket,ORDER_TIME_DONE);
   time_setup_msc= HistoryOrderGetInteger(ticket,ORDER_TIME_SETUP_MSC);
   time_done_msc = HistoryOrderGetInteger(ticket,ORDER_TIME_DONE_MSC);
   filling=(ENUM_ORDER_TYPE_FILLING)HistoryOrderGetInteger(ticket,ORDER_TYPE_FILLING);
   type_time=(ENUM_ORDER_TYPE_TIME)HistoryOrderGetInteger(ticket,ORDER_TYPE_TIME);
   magic=HistoryOrderGetInteger(ticket,ORDER_MAGIC);
   position_id=HistoryOrderGetInteger(ticket,ORDER_POSITION_ID);
   vol_init = HistoryOrderGetDouble(ticket, ORDER_VOLUME_INITIAL);
   vol_curr = HistoryOrderGetDouble(ticket, ORDER_VOLUME_CURRENT);
   price_open=HistoryOrderGetDouble(ticket,ORDER_PRICE_OPEN);
   price_current=HistoryOrderGetDouble(ticket,ORDER_PRICE_CURRENT);
   sl = HistoryOrderGetDouble(ticket, ORDER_SL);
   tp = HistoryOrderGetDouble(ticket, ORDER_TP);
   price_stop_limit=HistoryOrderGetDouble(ticket,ORDER_PRICE_STOPLIMIT);
   this.Symbol(HistoryOrderGetString(ticket, ORDER_SYMBOL));
   this.Comment(HistoryOrderGetString(ticket, ORDER_COMMENT));
  }
//+------------------------------------------------------------------+
//| Return comment of order                                          |
//+------------------------------------------------------------------+
string Order::Comment(void)
  {
   return CharArrayToString(m_comment);
  }
//+------------------------------------------------------------------+
//| Return symbol of order                                           |
//+------------------------------------------------------------------+
string Order::Symbol(void)
  {
   return "";
  }
//+------------------------------------------------------------------+
//| Set comment order                                                |
//+------------------------------------------------------------------+
void Order::Comment(string comment)
  {
   string s=StringSubstr(comment,0,32);
   StringToCharArray(s,m_comment);
  }
//+------------------------------------------------------------------+
//| Set symbol order                                                 |
//+------------------------------------------------------------------+
void Order::Symbol(string symbol)
  {
   string s=StringSubstr(symbol,0,32);
   StringToCharArray(s,m_symbol);
  }
//+------------------------------------------------------------------+
//| Converter for uchar array.                                       |
//+------------------------------------------------------------------+
struct OrderArray
  {
   uchar array[sizeof(Order)];
  };
//+------------------------------------------------------------------+
//| Convert order structure to uchar array                           |
//+------------------------------------------------------------------+
void Order::ToCharArray(uchar &array[])
  {
   OrderArray src_array;
   src_array=(OrderArray)this;
   ArrayCopy(array,src_array.array);
  }

Вызов оператора sizeof показывает, что данная структура занимает 200 байт. Таким образом, хранение коллекции исторических ордеров занимает количество байт, рассчитанное по формуле: sizeof(Order) * количество исторических ордеров. Следовательно, для коллекции, насчитывающей 1000 исторических ордеров, потребуется выделение памяти 200 * 1000 = 200 000 байт  или почти 200 Кбайт. Это немного по современным меркам, однако в случае, когда размер коллекции будет превышать десятки тысяч элементов, объем занимаемой памяти будет существенен.

Тем не менее, можно разработать специальный контейнер для хранения этих ордеров, который бы позволял сжимать их содержимое.  Этот контейнер, помимо обычных методов добавления и удаления новых элементов Order, также будет содержать методы Pack и Unpack, сжимающие содержимое структур типа Order. Приведем исходный код данного контейнера:

//+------------------------------------------------------------------+
//| Container of orders.                                             |
//+------------------------------------------------------------------+
class COrderList
  {
private:
   CArrayObj m_orders;
   uchar m_packed[];
public:
   bool AddOrder(Order& order);
   int TotalOrders(void);
   void At(int index, Order& order);
   bool DeleteAt(int index);
   void Pack(void);
   void Unpack(void);
   bool IsPacked();
   int Size();
  };
//+------------------------------------------------------------------+
//| Return packed status.                                            |
//+------------------------------------------------------------------+
bool COrderList::IsPacked(void)
  {
   return (ArraySize(m_packed) > 0);
  }
//+------------------------------------------------------------------+
//| Add new order.                                                   |
//+------------------------------------------------------------------+
bool COrderList::AddOrder(Order &order)
  {
   if(IsPacked())
      Unpack();
   COrderObj *o=new COrderObj();
   o.order=order;
   return m_orders.Add(o);
  }
//+------------------------------------------------------------------+
//| Return order at index.                                           |
//+------------------------------------------------------------------+
void COrderList::At(int index,Order &order)
  {
   if(IsPacked())
      Unpack();
   COrderObj *o=m_orders.At(index);
   order=o.order;
  }
//+------------------------------------------------------------------+
//| Return total orders.                                             |
//+------------------------------------------------------------------+
int COrderList::TotalOrders(void)
  {
   if(IsPacked())
      Unpack();
   return m_orders.Total();
  }
//+------------------------------------------------------------------+
//| Delete order by index.                                           |
//+------------------------------------------------------------------+
bool COrderList::DeleteAt(int index)
  {
   if(IsPacked())
      Unpack();
   return m_orders.Delete(index);
  }
//+------------------------------------------------------------------+
//| Return packed status.                                            |
//+------------------------------------------------------------------+
void COrderList::Unpack(void)
  {
   if(!IsPacked())return;
   uchar unpack[];
   uchar key[]={1,0,0,0};
   CryptDecode(CRYPT_ARCH_ZIP,m_packed,key,unpack);
   int size=ArraySize(unpack);
   m_orders.Clear();
   for(int offset=0; offset<size; offset+=sizeof(Order))
     {
      OrderArray o;
      ArrayCopy(o.array,unpack,0,offset,sizeof(Order));
      COrderObj *orderObj=new COrderObj();
      orderObj.order=(Order)o;
      m_orders.Add(orderObj);
     }
   ArrayResize(m_packed,0);
  }
//+------------------------------------------------------------------+
//| Return packed status.                                            |
//+------------------------------------------------------------------+
void COrderList::Pack(void)
  {
   if(IsPacked())return;
   int size=m_orders.Total()*sizeof(Order);
   uchar array[];
   ArrayResize(array,size);
   for(int i=0,offset=0; i<m_orders.Total(); i++,offset+=sizeof(Order))
     {
      COrderObj *orderObj=m_orders.At(i);
      OrderArray o;
      o=(OrderArray)orderObj.order;
      ArrayCopy(array,o.array,0,offset);
     }
   uchar key[]={1,0,0,0};
   CryptEncode(CRYPT_ARCH_ZIP,array,key,m_packed);
   m_orders.Clear();
  }
//+------------------------------------------------------------------+
//| Return orders size.                                              |
//+------------------------------------------------------------------+
int COrderList::Size(void)
  {
   if(IsPacked())
      return ArraySize(m_packed);
   return m_orders.Total()*sizeof(Order);
  }

 Идея в том, что пользователь может добавлять новые элементы в контейнер, а также, в случае необходимости, сжимать его содержимое прямо в оперативной памяти компьютера. Посмотрим, как это работает. Напишем демонстрационный скрипт:

//+------------------------------------------------------------------+
//|                                                     ZipTask3.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Orders.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   OrderList list;
   HistorySelect(0,TimeCurrent());
   int total = HistoryOrdersTotal();
   for(int i = 0; i < total; i++)
     {
      ulong ticket=HistoryOrderGetTicket(i);
      Order order;
      order.InitByTicket(ticket);
      list.AddOrder(order);
     }
   int unpack_size=list.Size();
   uint tiks=GetTickCount();
   list.Pack();
   uint time_tiks= GetTickCount()-tiks;
   int pack_size = list.Size();
   string per=DoubleToString((double)pack_size/(double)unpack_size*100.0,2);
   string message="Unpack size: "+(string)unpack_size+"bytes; "+
                  "Pack size: "+(string)pack_size+" bytes ("+per+" percent compressed. "+
                  "Pack execute msc: "+(string) time_tiks;
   printf(message);
   int totals=list.TotalOrders();
   if(list.TotalOrders()>0)
     {
      Order first;
      list.At(0,first);
      printf("First id ticket: "+(string)first.ticket);
     }
  }
//+------------------------------------------------------------------+

Момент сжатия коллекции в нем выделен желтым маркером. Запущенный на одном из счетов, насчитывающем 858 исторических ордеров, данный скрипт выдал следующие результаты:

2015.09.01 15:47:31.182 ZipTask3 (SBRF-9.15,H1) Unpack size: 171600 bytes; Pack size: 1521 bytes (0.89 percent compressed. Pack execute microsec.: 2534

Как видно, размер неупакованной коллекции составил 171 600 байт. После упаковки размер коллекции был равен всего 1 521 байт. Т.е. степень компрессии превысила сто раз! Это объясняется тем, что многие поля структуры содержат похожие данные. Также многие поля имеют пустые значения, под которые, тем не менее, выделяется память.   

Чтобы убедиться, что сжатие действительно работает корректно, достаточно выбрать любой ордер из коллекции и распечатать его свойство. Нами был выбран первый ордер и его уникальный идентификатор. После упаковки идентификатор ордера отобразился верно:

2015.09.01 15:47:31.182 ZipTask3 (SBRF-9.15,H1) First id ticket: 10279280

Описываемый алгоритм в случае обращения к своей коллекции распаковывает ранее упакованные данные. Это снижает производительность. Поэтому сжимать данные нужно только после того, как работа по их формированию завершена. Для лучшей степени сжатия все данные собираются в единый массив, а затем упаковываются. Для распаковки происходит обратный процесс. 

Интересно, что упаковка 858 элементов занимает около 2.5 миллисекунд на достаточно мощном компьютере. Распаковка этих же данных происходит еще быстрее и занимает порядка 0.9 миллисекунды. Таким образом, на один цикл упаковки/распаковки массива, состоящего из тысячи элементов, тратится примерно 3.5-4.0 миллисекунды. При этом достигается более чем стократная экономия памяти. Такие характеристики смотрятся достаточно впечатляюще, чтобы использовать ZIP-сжатие для организации больших массивов данных.

Пример скрипта, сжимающего данные в оперативной памяти, прикреплен к данной статье под именем ZipTask3.mq5 и находится в папке Scripts. Для его работы также необходим файл Orders.mqh, располагающийся в папке Include.

 

Глава 4. Документация к классам для работы с ZIP-архивами

 

4.1. Документация к классу CZipContent 

В данной главе описаны методы и перечисления, используемые в классах для работы с ZIP-архивами. На уровне пользователей класс CZipContent не используется напрямую, однако все его открытые методы делегируются классам CZipFile и CZipFolder, таким образом, свойства и методы, описанные в нем, распространяются также и на эти классы. 

Метод ZipType()

Метод ZipType возвращает тип текущего элемента в архиве. Типов элементов, хранимых в архиве, два: папка (директория) и файл. Тип папки представлен классом CZipDirectory, тип файла представлен классом CZipFile. Более подробно о типах ZIP-архива можно прочитать в разделе 2.1 текущей главы: "Структура упакованных файлов в архиве, классы CZipFile и CZipFolder".

ENUM_ZIP_TYPE   ZipType(void);

Возвращаемое значение

Возвращает перечисление ENUM_ZIP_TYPE, описывающее, к какому типу принадлежит текущий экземпляр CZipContent.

 

Метод Name(void)

Возвращает название папки или файла в архиве.
string  Name(void);

Возвращаемое значение

Имя файла или папки.

 

Метод Name(string name) 

Устанавливает название текущей папки или файла в архиве. Используется в случаях, когда требуется изменить имя текущей папки или файла.

void  Name(string name);

Параметры:

  • [in] name — новое имя папки или файла. Имя должно быть уникальным и не иметь совпадений среди других имен папок и файлов в архиве.


Метод CreateDateTime(datetime date_time)

Устанавливает новую дату изменения папки или файла в архиве.

void  CreateDateTime(datetime date_time);

Параметры:

  • [in] date_time — дата и время, которое требуется установить для текущей папки или файла.

Примечание: 

Дата и время конвертируются в формат MS-DOS и хранятся во внутренних структурах типа ZipLocalHeader и ZipCentralDirectory. Подробнее о способах конвертирования и представления этого формата описано в разделе 2.3 данной статьи: "Вспоминаем MS-DOS. Формат времени и даты в ZIP-архиве".

 

Метод CreateDateTime(void)

Возвращает дату и время изменения текущей папки или файла.

datetime  CreateDateTime(void);

Возвращаемое значение

Дата и время изменения текущей папки или файла. 

 

Метод CompressedSize()

Возвращает размер упакованных данных в файле. Для директорий размер упакованных данных всегда равен нулю.

uint  CompressedSize(void);

Возвращаемое значение 

Размер упакованных данных в байтах.

 

Метод UncompressedSize()

Возвращает размер исходных, неупакованных данных в файле. Для директорий размер неупакованных данных всегда равен нулю.

uint  UncompressedSize(void);

Возвращаемое значение 

Размер исходных данных в байтах.

 

Метод TotalSize()

Возвращает суммарный размер элемента архива. Каждый файл или директория в архиве, помимо своего названия и содержимого (для файлов), хранит дополнительные служебные структуры, размер которых также учитывается при расчете суммарного размера элемента архива.

int TotalSize(void);

Возвращаемое значение 

Суммарный размер текущего элемента в архиве, учитывающий дополнительные служебные данные.

 

Метод FileNameLength()

Возвращает длину имени директории или файла, выраженную в количестве используемых символов.

ushort FileNameLength(void);

Возвращаемое значение 

Длина имени директории или файла, выраженная в количестве используемых символов.

 

Метод UnpackOnDisk()

Распаковывает содержимое элемента и сохраняет его в файле с именем элемента на жестком диске компьютера. Если распаковывается директория, то вместо файла создается соответствующая папка.

bool UnpackOnDisk(string folder, int file_common);

 Параметры

  • [in] folder — имя корневой папки, в которую требуется распаковать текущую папку или файл. Если элемент требуется распаковать без создания папки архива, то это значение необходимо оставить пустым, равным "".
  • [in] file_common — этот модификатор указывает, в какой секции файловой системы программ MetaTrader необходимо распаковать элемент. Установите этот параметр равным FILE_COMMON, если хотите выполнить распаковку в общий файловый раздел всех терминалов MetaTrader 5.

Возвращаемое значение

Возвращает истину, если распаковка файла или папки на жесткий диск прошла успешно. Возвращает ложь в противном случае. 

 

4.2. Документация к классу CZipFile

Класс CZipFile наследуется от CZipContent и используется для хранения файлов в архиве. CZipFile хранит содержимое файла только в упакованном виде. Это значит, что при передаче ему файла для хранения происходит автоматическая упаковка его содержимого. Распаковка файла также происходит в автоматическом режиме при вызове метода GetUnpackFile. Помимо ряда поддерживаемых методов CZipContent, CZipFile также поддерживает специальные методы для работы с файлами. Далее приведены описания этих методов.


Метод AddFile()

Добавляет файл с жесткого диска в текущий элемент CZipFile. Для того чтобы добавить файл в архив типа CZip, вначале необходимо создать экземпляр класса CZipFile, указав при создании название файла и путь к нему. После того как экземпяляр класса будет создан, его необходимо будет добавить в класс CZip, вызвав соответствующий метод.  Фактическая упаковка переданного содержимого происходит в момент его добавления (вызова данного метода).

bool AddFile(string full_path, int file_common);

 Параметры

  • [in] full_path — полное имя файла, включая путь к нему относительно центрального каталога файлов программ MQL.
  • [in] file_common — этот модификатор указывает, в какой секции файловой системы программ MetaTrader необходимо распаковать элемент. Установите этот параметр равным FILE_COMMON, если хотите выполнить распаковку в общий файловый раздел всех терминалов MetaTrader 5.

Возвращаемое значение

Возвращает истину, если добавление файл прошло успешно. Возвращает ложь в противном случае. 

 

Метод AddFileArray()

Добавляет в качестве содержимого CZipFile массив байтов типа uchar. Данный метод используется в случае динамического создания содержимого файла. Фактическая упаковка переданного содержимого происходит в момент его добавления (вызова данного метода).

bool AddFileArray(uchar& file_src[]);

Параметры

  • [in] file_src — байтовый массив, который необходимо добавить.

Возвращаемое значение

Возвращает истину, если добавление файл прошло успешно. Возвращает ложь в противном случае.

 

Метод GetPackFile()

Возвращает упакованное содержимое файла.

void GetPackFile(uchar& file_array[]);

 Параметры

  • [out] file_array — байтовый массив, в который требуется принять упакованное содержимое файла.


Метод GetUnpackFile()

Возвращает распакованное содержимое файла. Содержимое распаковывается в момент обращения к методу.

void GetUnpackFile(uchar& file_array[]);

 Параметры

  • [out] file_array — байтовый массив, в который требуется принять распакованное содержимое файла.

 

4.3. Документация к классу CZip

Класс CZip реализует основную работу с архивами типа ZIP. Класс представляет собой обобщенный архив ZIP, в который могут быть добавлены ZIP-элементы двух типов: элементы, представляющие папку (CZipDirectory), и элементы, представляющие  ZIP-файлы (CZipFile). Помимо прочего, класс CZip позволяет загружать уже существующие архивы, как с жесткого диска компьютера, так и в виде байт-последовательности. 

 

Метод ToCharArray()

Конвертирует содержимое ZIP-архива в байтовую последовательность типа uchar.

void ToCharArray(uchar& zip_arch[]);

Параметры

  • [out] zip_arch — байтовый массив, в который требуется принять содержимое ZIP-архива.


Метод CreateFromCharArray()

Загружает ZIP-архив из байтовой последовательности.

bool CreateFromCharArray(uchar& zip_arch[]);

 Параметры

  • [out] zip_arch — байтовый массив, из которого требуется загрузить содержимое ZIP-архива.

Возвращаемое значение

Истина, если создание архива из байтовой последовательности прошло успешно, и ложь в противном случае.

 

Метод SaveZipToFile()

Сохраняет текущий ZIP-архив с его содержимым в указанный файл.

bool SaveZipToFile(string zip_name, int file_common);

  Параметры

  • [in] zip_name — полное имя файла, включая путь к нему относительно центрального каталога файлов программ MQL.
  • [in] file_common — этот модификатор указывает, в какой секции файловой системы программ MetaTrader необходимо распаковать элемент. Установите этот параметр равным FILE_COMMON, если хотите выполнить распаковку в общий файловый раздел всех терминалов MetaTrader 5.

Возвращаемое значение

Истина, если сохранение архива в файл прошла успешно, и ложь в противном случае.

 

Метод LoadZipFromFile()

Загружает содержимое архива из файла на жестком диске компьютера.

bool LoadZipFromFile(string full_path, int file_common);

 Параметры

  • [in] full_path — полное имя файла, включая путь к нему относительно центрального каталога файлов программ MQL.
  • [in] file_common — этот модификатор указывает, в какой секции файловой системы программ MetaTrader необходимо распаковать элемент. Установите этот параметр равным FILE_COMMON, если хотите выполнить распаковку в общий файловый раздел всех терминалов MetaTrader 5.

Возвращаемое значение

Истина, если загрузка архива из файла прошла успешно, и ложь в противном случае.

 

Метод LoadZipFromUrl()

Загружает содержимое архива по интернет-ссылке url. Для корректной работы данного метода необходимо выставить разрешение для доступа к запрашиваемому ресурсу. Более подробно работа этого метода описана в разделе 3.2 данной статьи: "Загрузка архива с удаленного сервера на примере сайта MQL5.com"

bool LoadZipFromUrl(string url);

 Параметры

  • [in] url — ссылка на архив.

 

Метод UnpackZipArchive()

Распаковывает все файлы и директории текущего архива в предложенный каталог.

bool UnpackZipArchive(string folder, int file_common);

  Параметры

  • [in] folder — папка, в которую необходимо распаковать текущий архив. Если папку для архива создавать не требуется, в качестве параметра необходимо передать пустое значение "".
  • [in] file_common — этот модификатор указывает, в какой секции файловой системы программ MetaTrader необходимо распаковать элемент. Установите этот параметр равным FILE_COMMON, если хотите выполнить распаковку в общий файловый раздел всех терминалов MetaTrader 5.

Возвращаемое значение

Истина, если распаковка архива прошла успешно, и ложь в противном случае.

 

Метод Size()

Возвращает размер архива в байтах.

int Size(void);

Возвращаемое значение

Размер архива в байтах.

 

Метод TotalElements()

Возвращает количество элементов в архиве. Элементом архива может быть директория либо запакованный файл.

int TotalElements(void);

Возвращаемое значение

Количество элементов в архиве.

 

Метод AddFile()

Добавляет новый ZIP-файл в текущий архив. Файл должен быть представлен в виде CZipFile и создан предварительно, до добавления в архив.

bool AddFile(CZipFile* file);

Параметры

  • [in] file — ZIP-файл, который требуется добавить в архив.

Возвращаемое значение

Истина, если добавление в архив прошло успешно, и ложь в противном случае. 

 

Метод DeleteFile()

Удаляет файл типа CZipFile по индексу index из архива.

bool DeleteFile(int index);

Параметры

  • [in] index — индекс файла, который требуется удалить из архива.

Возвращаемое значение

Истина, если удаление файла из архива прошло успешно. Ложь в противном случае.

 

Метод ElementAt()

Получает элемент типа CZipFile, размещенный по индексу index.

CZipContent*   ElementAt(int index)const;

 Параметры

  • [in] index — индекс файла, который требуется получить из архива.

Возвращаемое значение

Элемент типа CZipFile, который располагается по индексу index.

 

4.4. Структура ENUM_ZIP_ERROR и получение расширенной информации об ошибках

В процессе работы с классами CZip, CZipFile и CZipDirectory могут возникать различные ошибки, например ошибка, возникающая при попытке доступа к несуществующему файлу и т.д. Большинство методов, представленных в этих классах, возвращают соответствующий флаг типа bool, сигнализирующий об успешности совершения операции. В случае возвращения отрицательного значения (false) можно получить дополнительную информацию о причинах сбоя. Причинами сбоя могут быть как стандартные, системные ошибки, так и специфические ошибки, возникающие в процессе работы с ZIP-архивом. Для передачи специфических ошибок используется механизм передачи пользовательских ошибок с помощью функции SetUserError. Коды пользовательских ошибок задаются перечислением ENUM_ZIP_ERROR:

Перечисление ENUM_ZIP_ERROR

ЗначениеОписание
 ZIP_ERROR_EMPTY_SOURCE Файл, переданный для упаковки, пустой.
 ZIP_ERROR_BAD_PACK_ZIP Ошибка внутреннего упаковщика/распаковщика.
 ZIP_ERROR_BAD_FORMAT_ZIP Переданный формат ZIP-файла не соответствует стандарту либо испорчен.
 ZIP_ERROR_NAME_ALREADY_EXITS  Имя, под которым пользователь пытается сохранить файл, уже используется в архиве.
 ZIP_ERROR_BAD_URL Переданная ссылка не ссылается на ZIP-архив, либо доступ к указанному интернет-ресурсу запрещен  настройками  терминала.

При получении пользовательской ошибки необходимо явно привести ее к перечислению ENUM_ZIP_ERROR и обработать соответствующим образом. Пример работы с ошибками, возникающими в ходе работы с классами архивирования, представлен ниже в виде скрипта:

//+------------------------------------------------------------------+
//|                                                     ZipError.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Zip\Zip.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CZip Zip;
   bool res=Zip.LoadZipFromFile("TestZip.zip",FILE_COMMON);
   if(!res)
     {
      uint last_error=GetLastError();
      if(last_error<ERR_USER_ERROR_FIRST)
         printf("Возникла системная ошибка при загрузке архива. Номер ошибки: "+(string)last_error);
      else
        {
         ENUM_ZIP_ERROR error=(ENUM_ZIP_ERROR)(last_error-ERR_USER_ERROR_FIRST);
         printf("Возникла ошибка обработки архива в момент его загрузки: "+EnumToString(error));
        }
     }
  }
//+------------------------------------------------------------------+

 

4.5. Описание файлов, приложенных к статье

Ниже приведено краткое описание файлов, приложенных к статье:

  • Zip\Zip.mqh — содержит основной класс для работы с архивами CZip.
  • Zip\ZipContent.mqh — содержит базовый класс CZipContent для основных классов элементов архива: CZipFile и CZipDirectory.
  • Zip\ZipFile.mqh — содержит класс для работы с ZIP-файлами архива.
  • Zip\ZipDirectory.mqh — содержит класс для работы с ZIP-папками архива.
  • Zip\ZipHeader.mqh — в файле даны описания структур File Local Header, Central Directory и End Central Directory Record.
  • Zip\ZipDefines.mqh — перечислены определения, константы и коды ошибок, используемые при работе с классами по архивированию.
  • Dictionary.mqh — вспомогательный класс, обеспечивающий контроль уникальности имен файлов и директорий, добавляемых в архив. Алгоритм работы этого класса подробно описан в статье "Рецепты MQL5 - Реализуем ассоциативный массив или словарь для быстрого доступа к данным". 

Все файлы, приведенные в статье, необходимо разместить относительно внутреннего каталога <каталог_данных_терминала>\MQL5\Include. Для начала работы с классом в проект необходимо включить файл Zip\Zip.mqh. Опишем в качестве примера скрипт, создающий ZIP-архив и записывающий в него текстовый файл с сообщением "test":

//+------------------------------------------------------------------+
//|                                                          Zip.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Zip\Zip.mqh>                                   // Включаем все необходимые классы для работы с ZIP-архивом
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   CZip Zip;                                             // Создаем пустой ZIP-архив
   uchar array[];                                        // Создаем пустой байтовый массив array
   StringToCharArray("test", array);                     // Конвертируем сообщение "test" в байтовый массив array
   CZipFile* file = new CZipFile("test.txt", array);     // Создаем новый ZIP-файл 'file', на основе массива array с именем "test.txt"
   Zip.AddFile(file);                                    // Добавляем созданный ZIP-файл 'file' в ZIP-архив
   Zip.SaveZipToFile("Test.zip", FILE_COMMON);           // Сохраняем ZIP-архив на диске под именем "Test.zip". 
  }
//+------------------------------------------------------------------+

После его выполнения на жестком диске компьютера в центральном каталоге файлов для MetaTrader 5 появится новый ZIP-архив с именем Test.zip, содержащий один текстовой файл с надписью "test". 

    Архив, прилагаемый к данной статье, создан с помощью MQL-архиватора CZip, который здесь описан.                                                                                                                                                                                                                                                         


Заключение

Мы подробно рассмотрели структуру ZIP-архива и создали классы, реализующие работу с этим типом архивов. Этот формат архивирования был разработан еще в конце 80-ых годов прошлого века, но это не мешает ему оставаться самым популярным форматом сжатия данных. Набор данных классов может оказать разработчику торговых систем неоценимую пользу. С его помощью можно экономно хранить собираемые данные, будь то тиковая история или другая торговая информация. Часто различная аналитическая информация также предоставляется в сжатом виде. В этом случае умение работать с такой информацией даже в сжатом виде также может оказаться полезным.

Описываемые классы скрывают многие технические аспекты работы с архивами, предоставляя на пользовательском уровне простые и понятные методы работы с ними, сходные с теми операциями, что привыкли совершать пользователи с программами архивирования: добавление и извлечение файлов из архива, создание новых и загрузка уже существующих архивов, в том числе размещенных на удаленных сторонних серверах. Эти возможности полностью решают большинство задач, связанных с архивированием, и делают программирование в среде MetaTrader еще проще и функциональнее. 

Прикрепленные файлы |
MQL5.zip (19.75 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (70)
Forester
Forester | 22 февр. 2023 в 16:23

Это ветка обсуждения указанной вами статьи. Увидел только возможность выгрузить ZIP в папку на диск. А хотелось бы сразу в строку.

Forester
Forester | 22 февр. 2023 в 20:33

Если кому нужно будет вот готовый скрипт для чтения архива в строку.

Архив из статьи не компилируется.  папку ZIP и Dictionary.mqh скачать тут https://www.mql5.com/en/code/27955 - они рабочие

#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"

int is_common=1;

#include <Zip\Zip.mqh>

CZip Zip;

void OnStart(){
   string filenames[]; if(FileSelectDialog("Выберите файлы для загрузки", "CSV/", "CSV files (*.csv)|*.csv;*.zip|ZIP files (*.zip)|*.zip|All files (*.*)|*.*", FSD_ALLOW_MULTISELECT|(is_common?FSD_COMMON_FOLDER:0), filenames, "data.txt")>0){int total=ArraySize(filenames);for(int i=0; i<total; i++){Print(i, ": ", filenames[i]);}} else{Print("Files not selected");}

   bool res=Zip.LoadZipFromFile(filenames[0],FILE_COMMON);//считать 0й файл из выбранных
   if(!res) {
      uint last_error=GetLastError();
      if(last_error<ERR_USER_ERROR_FIRST){printf("Возникла системная ошибка при загрузке архива. Номер ошибки: "+(string)last_error);}
      else {  ENUM_ZIP_ERROR error=(ENUM_ZIP_ERROR)(last_error-ERR_USER_ERROR_FIRST); printf("Возникла ошибка обработки архива в момент его загрузки: "+EnumToString(error));}
      return;
   }
   
   CZipFile* content = Zip.ElementAt(0);//считать в архиве файл номер 0
   uchar file_content[];
   content.GetUnpackFile(file_content);// распаквать его в массив file_content

   string  s = CharArrayToString(file_content,0,-1,CP_UTF8);//CP_ACP
   
   string sa[];
   int k=StringSplit(s,'\n',sa);//--- разобьем строку на подстроки
   for(int i=0;i<k;i++){
      PrintFormat("result[%d]=\"%s\"",i,sa[i]);
   }
}
Forester
Forester | 22 февр. 2023 в 20:38
А вообще, хорошо бы добавить функции архивации в сам терминал. Судя по обсуждению, файлы регулярно перестают компилироваться.
Ivan Titov
Ivan Titov | 21 мар. 2024 в 13:27
Планируется ли добавление функции распаковки строки, сжатой алгоритмом GZIP?
Vasiliy Sokolov
Vasiliy Sokolov | 21 мар. 2024 в 14:44
Ivan Titov #:
Планируется ли добавление функции распаковки строки, сжатой алгоритмом GZIP?

Краткий ответ: нет, не планируется, так как это совсем другой формат не совместимый с zip. Для работы с GZip нужна отдельная, аналогичная этой, библиотека. Далее моя цитата из соседней ветки:

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

WebSocket и GZIP

Vasiliy Sokolov, 2024.03.21 14:35

Стоит различать форматы хранения данных от алгоритмов сжатия.

Zip - это формат хранения нескольких файлов в одном, наподобии формата TAR. Сами файлы в формате zip обычнно сжимаются алгоритмом сжатия deflate, но могут не сжиматся им вовсе, а просто хранится в этом формате, в таком случае формат позволяет просто объединить несколько файлов в один архив (файл) без сжатия.

GZip это формат хранения сжатых данных алгоритмом deflate в одном файле.

Если Вам выдали GZip - формат zip вам не подойдет. Вам нужно знать формат gzip и распоковать его с помощью системной функции CryptEncode(CRYPT_ARCH_ZIP). Это не сложно сделать самостоятельно, без привлечения сторонних библиотек, так как алгоритм сжатия там один и тот же, deflate (в MQL он имеет не очень удачный идентификатор CRYPT_ARCH_ZIP). Жаль что нет специальной статьи "Работаем с GZip архивом средствами MQL5". В целом, это не задача системной функции, а задача специальной MQL бибилотеки, оборачивающий deflate вокруг формата gzip.


Применение нечеткой логики в трейдинге средствами MQL4 Применение нечеткой логики в трейдинге средствами MQL4
В данной статье предлагаются примеры применения теории нечетких множеств в трейдинге средствами MQL4. Описывается разработка индикатора и советника с использованием библиотеки FuzzyNet для MQL4.
Индикатор для построения графика "шпинделей" (веретён) Индикатор для построения графика "шпинделей" (веретён)
Статья рассматривает построение графика "шпинделей" (spindles) или, как их еще называют, "веретён", его использование в торговых стратегиях и советниках. Вначале обсудим появление графика, его построение и связь с графиком японских свечей. Далее проанализируем реализацию индикатора в программном коде на языке MQL5. Протестируем основанный на индикаторе эксперт и сформулируем торговую стратегию.
Оценка и выбор переменных для моделей машинного обучения Оценка и выбор переменных для моделей машинного обучения
В статье будут рассмотрены особенности выбора, предподготовки и оценки входных переменных (предикторов) для использования в моделях машинного обучения. Будут рассмотрены новые подходы и возможности по глубокому анализу предикторов, их влияние на возможное переобучение моделей. От результата этого этапа работы во многом зависит общий результат использования моделей. Будут рассмотрены два пакета, предлагающие новый и оригинальный подход к выбору предикторов.
Использование утверждений (assertions) при разработке программ на MQL5 Использование утверждений (assertions) при разработке программ на MQL5
В данной статье рассматриваются утверждения (assertions) в рамках языка MQL5. Даются два примера реализации механизма утверждений, а также приводятся общие рекомендации по применению утверждений.