Групповые файловые операции

MetaQuotes | 15 июля, 2008

Введение

Прочитать или записать один файл не является проблемой. Даже если при этом придется использовать WinAPI, как это описано в статье Файловые операции через WinAPI. Но что делать, если мы не знаем точного имени файла, известно только его местонахождение в определенной папке и задано расширение? Можно вручную каждый раз вводить нужное имя в виде параметра, но если таких файлов будет пять, десять или более? Нужен способ групповой обработки однотипных файлов в указанной папке. Для этого хорошо подходят функции FindFirstFile(), FindNextFile() и FindClose(), входящие в библиотеку kernel32.dll.


Функция FindFirstFile()

Описание функции приводится в msdn по адресу http://msdn.microsoft.com/en-us/library/aa364418(VS.85).aspx.

HANDLE WINAPI FindFirstFile(
  __in   LPCTSTR lpFileName,
  __out  LPWIN32_FIND_DATA lpFindFileData
);

Из него следует, что данная функция возвращает указатель на найденный файл, удовлетворящий условиям поиска. Условие поиска задается в переменной lpFileName, которая содержит путь, по которому следует искать файл, и возможное имя этого файла. Удобство этой функции в том, что мы можем задать поиск по маске, например, найти файл по маске "C:\folder\*.txt". Функция вернет первый же файл, который находится в папке "C:\folder" и будет иметь расширение txt.

Возвращаемый результат этой функции имеет тип int в MQL4. Для передачи входного параметра подойдет тип string, осталось только разобраться с тем, что передавать в эту функкцию вторым параметром и как этот параметр потом обрабатывать. Импорт этой функции будет выглядеть примерно так:

#import "kernel32.dll"
int  FindFirstFileA(string path, .какой-то второй параметр);
#import

Здесь мы видим уже знакомую библиотеку kernel32.dll. Правда, имя функции записано не FindFirstFile(), а как FindFirstFileA(). Это объясняется тем, что многие функции в этой библиотеки имеют два варианта: для работы со строками в кодировке Unicode к имени функции добавляется буква "W" (FindFirstFileW), а для работы с кодировкой ANSI добавляется буква "A" (FindFirstFileA).

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

lpFindFileData [out] - A pointer to the WIN32_FIND_DATA structure that receives information about a found file or directory.

То есть, это указатель на некую структуру WIN32_FIND_DATA. В данном случае структура - это некая область в оперативной памяти компьютера, указатель(адрес) на которую и передается в функцию. Выделить память в MQL4 мы можем массивом данных, указатель задается спецификатором &, осталось только узнать размер необходимой памяти в байтах, указатель на которую мы передадим. Смотрим описание структуры.

typedef struct _WIN32_FIND_DATA {
  DWORD dwFileAttributes;
  FILETIME ftCreationTime;
  FILETIME ftLastAccessTime;
  FILETIME ftLastWriteTime;
  DWORD nFileSizeHigh;
  DWORD nFileSizeLow;
  DWORD dwReserved0;
  DWORD dwReserved1;
  TCHAR cFileName[MAX_PATH];
  TCHAR cAlternateFileName[14];
} WIN32_FIND_DATA, 

В MQL4 нет типов DWORD, TCHAR и FILETIME. Известно, что DWORD занимает 4 байта, как тип int в MQL4, TCHAR имеет внутренее представление в один байт. Чтобы подсчитать весь размер структуры WIN32_FIND_DATA в байтах, осталось только выяснить что такое FILETIME.

typedef struct _FILETIME {
  DWORD dwLowDateTime;
  DWORD dwHighDateTime;
} FILETIME

Оказывается, что FILETIME состоит из двух DWORD, а значит FILETIME - это 8 байт. Запишем все это в таблицу:

Тип Размер в байтах
DWORD 4
TCHAR 1
FILETIME 8

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

Тип Размер в байтах Примечание
dwFileAttributes 4 Атрибуты файла
ftCreationTime 8 Время создания файла или папки
ftLastAccessTime 8 Время последнего обращения к файлу
ftLastWriteTime 8 Время последнего сохранения файла
nFileSizeHigh 4 Максимальный размер в байтах
nFileSizeLow 4 Минимальный размер в байтах
dwReserved0 4 Обычно не определено и не используется
dwReserved1 4 Зарезервировано на будущее
cFileName[MAX_PATH] 260 (MAX_PATH = 260)
Имя файла
cAlternateFileName[14] 14 Альтернативное имя в формате 8.3

Итого размер структуры равен: 4 + 8 + 8 + 8 + 4 + 4 + 4 +4 + 260 +14 = 318 байт.


Как видно из рисунка, имя файла начинается с 45 по счету байта, предудыщие 44 байта содержат всякую вспомогательную информацию. Необходимо передать в функцию FindFirstFile() в качестве второго параметра по ссылке какую-то структуру средствами языка MQL4, которая имела бы размер 318 байт. Наиболее удобным будет использовать для этого массив типа int, который имел бы размер не менее требуемого. Делим 318 на 4 (внутреннее представление типа int - 4 байта), получаем 79.5, округляем до ближайшего верхнего целого - значит нам нужен массив с 80 элементами.

Сам импорт функции будет выглядеть теперь так:

#import "kernel32.dll"
int  FindFirstFileA(string path, int & answer[]);
#import

Здесь используется вариант функции с буквой A в конце имени FindFirstFileA() для ANSI - кодировки. Массив answer передается по ссылке и служит для заполнения структурой WIN32_FIND_DATA. Пример вызова:

   int win32_DATA[80];
   int handle = FindFirstFileA(TerminalPath() + "\experts\*.mq4",win32_DATA);

Функции FindNextFileA() и FindClose()

Функция FindNextFileA() получает в качестве первого параметра handle файла, полученного предварительно функцией FindFirstFileA() или другим более ранним вызовом FindNextFileA(). Второй параметр точно такой же. Функция FindClose() просто закрывает поиск. Поэтому полный вид импорта данных функций будет выглядеть таким образом:

#import "kernel32.dll"
int  FindFirstFileA(string path, int & answer[]);
bool FindNextFileA(int handle, int & answer[]);
bool FindClose(int handle);
#import

Осталось только научиться извлекать имя файла, записанного в массив answer[].

Получение имени файла

Имя файла содержится в массиве начиная с 45-го и заканчивая 304 байтом включительно. Тип int содержит 4 байта, поэтому каждый элемент массива содержит как бы 4 символа, если представлять весь массив наполненным символами. Поэтому, чтобы обратиться к первому символу в имение файла, нам необходимо пропустить 44/4 = 11 элементов массива answer[]. Имя файла находится в цепочке из 65(260/4=65) элементов массива, начиная с элемента answer[11] (индексация начинается с нуля) и заканчивая элементом answer[76].

Таким образом, мы можем получать из массива answer[] имя файла блоками по 4 символа в каждом. Число int представляет из себя последовательность из 32 бит, эти 32 бита представляют из себя 4 блока по 8 бит в каждом.

Самый младший байт находится справа, самый старший байт находится слева. Биты пронумерованы в порядке возрастания, то есть, биты с 1-по по 8-ой составляют из себя младший байт. Мы можем извлечь нужные нам байты с помощью битовых операций. Чтобы получить значение младшего байта, нам необходимо заполнить нулевыми значениями все биты с 9-го по 32. Делается это логической операцией И.

int a = 234565;
int b = a & 0x000000FF;

Здесь 0x000000FF - это 32-битовое целое число, у которого все разряды начиная с 9-го имеют нулевое значение, а разряды с первого по восьмой имеют значение единица. Таким образом, полученное число b будет содержать только один (младший) байт от числа a. Полученный байт (код символа) мы превратим в односимвольную строку с помощью функции CharToStr().

Первый символ мы получили, как получить следующий? Очень просто, производим побитовый сдвиг вправо на 8 бит и второй бит становится на место младшего бита. Затем применяем уже знакомую операцию логического И.

int a = 234565;
int b = (a >>8) & 0x000000FF;

Как вы уже догадались, третий по старшинству байт мы получим сдивгом на 16 бит, а самый старший сдвигом на 24 бита. Таким образом, из одного элемента массива типа int мы можем извлечь 4 символа. Вот как выглядит получение 4-х первых символов в имени файла из массива answer[]:

   string text="";
   
   int pos = 11;
   int curr = answer[pos];
      {
      text = text + CharToStr(curr & 0x000000FF)
         +CharToStr(curr >> 8 & 0x000000FF)
         +CharToStr(curr >> 16 & 0x000000FF)
         +CharToStr(curr >> 24 & 0x000000FF);
      }
    Print("text = ", text);

Создадим отдельную функцию, которая возвращает текстовую строку из переданного массива с именем buffer.

//+------------------------------------------------------------------+
//|  считать текст из буфера                                         |
//+------------------------------------------------------------------+ 
string bufferToString(int buffer[])
   {
   string text="";
   
   int pos = 10;
   for (int i=0; i<64; i++)
      {
      pos++;
      int curr = buffer[pos];
      text = text + CharToStr(curr & 0x000000FF)
         +CharToStr(curr >> 8 & 0x000000FF)
         +CharToStr(curr >> 16 & 0x000000FF)
         +CharToStr(curr >> 24 & 0x000000FF);
      }
   return (text);
   }

Задача по получению имени файла из структуры решена.

Получение списка всех экспертов с исходным кодом

Простой скрипт для демонстрации возможностей описанных функций:

//+------------------------------------------------------------------+
//|                                                CheckFindFile.mq4 |
//|                      Copyright © 2008, MetaQuotes Software Corp. |
//|                                       https://www.metaquotes.net/ |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2008, MetaQuotes Software Corp."
#property link      "https://www.metaquotes.net/"
 
#property show_inputs
 
#import "kernel32.dll"
int  FindFirstFileA(string path, int& answer[]);
bool FindNextFileA(int handle, int& answer[]);
bool FindClose(int handle);
#import
 
 
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   int win32_DATA[79];
   
   int handle = FindFirstFileA(TerminalPath() + "\experts\*.mq4",win32_DATA);
   Print(bufferToString(win32_DATA));
   ArrayInitialize(win32_DATA,0);
 
   while (FindNextFileA(handle,win32_DATA))
      {
      Print(bufferToString(win32_DATA));
      ArrayInitialize(win32_DATA,0);
      }
 
   if (handle>0) FindClose(handle);
   
//----
   return(0);
  }
  
//+------------------------------------------------------------------+
//|  считать текст из буфера                                         |
//+------------------------------------------------------------------+ 
string bufferToString(int buffer[])
   {
   string text="";
   
   int pos = 10;
   for (int i=0; i<64; i++)
      {
      pos++;
      int curr = buffer[pos];
      text = text + CharToStr(curr & 0x000000FF)
         +CharToStr(curr >> 8 & 0x000000FF)
         +CharToStr(curr >> 16 & 0x000000FF)
         +CharToStr(curr >> 24 & 0x000000FF);
      }
   return (text);
   }  
//+------------------------------------------------------------------+

Здесь после вызова функций FindFirstFileA() и FindNextFileA() производится перевод массива win32_DATA (структуры WIN32_FIND_DATA) в "пустое" состояние, а именно - все элементы массива заполняются нулями:

   ArrayInitialize(win32_DATA,0);

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


Пример реализации:Резервное копирование исходных кодов

Один из простых примеров для демонстрации практических возможностей - это создание резервных копий исходных кодов в специально отведенной директории. Для этого нам нужно объединить рассмотренные в этой статье функции с функциями из статьи Файловые операции через WinAPI. Получится вот такой простой скрипт backup.mq4. Полный код вы можете посмотреть в приложенном файле, здесь же только функция start(), которая использует все описанные в двух статьях функции:

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   string expert[1000]; // должно хватить 
   string EAname="";    // имя эксперта
   int EAcounter = 0;   // счетчик экспертов   
   int win32_DATA[80];  
   
   int handle = FindFirstFileA(TerminalPath() + "\experts\*.mq4",win32_DATA);
   EAname = bufferToString(win32_DATA);
   expert[0] = EAname;
   ArrayInitialize(win32_DATA,0);
   
   int i=1;
   while (FindNextFileA(handle,win32_DATA))
      {
      EAname = bufferToString(win32_DATA);
      expert[i] = EAname;
      ArrayInitialize(win32_DATA,0);
      i++;
      if (i>=1000) ArrayResize(expert,2000); // теперь то точно хватит
      }
 
   ArrayResize(expert, i);
   int size = i;
   
   if (handle>0) FindClose(handle);
 
   for (i = 0 ; i < size; i++) 
      {
      Print(i,":  ",expert[i]);   
      string backupPathName = backup_folder + "experts\\" + expert[i];
      string originalName = TerminalPath() + "\\experts\\" + expert[i];
      string buffer=ReadFile(originalName);
      WriteFile(backupPathName,buffer);   
      }
   if (size > 0 ) Print("There are ",size," files were copied to folder "+backup_folder+"experts\\");   
//----
   return(0);
  }
//+------------------------------------------------------------------+


Заключение

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