Operaciones con grupos de archivos

MetaQuotes | 20 abril, 2016

Introducción

No es un problema leer o escribir un archivo. Incluso si usamos para ello WinAPI, como se describe en Operaciones con archivos a través de WinAPI. Pero, ¿qué debemos hacer si no sabes cuál es el nombre exacto del archivo, sino solo su ubicación en una cierta carpeta y su extensión? Por supuesto, podemos introducir el nombre necesario manualmente, como parámetro, pero ¿qué debemos hacer si hay diez o más de estos archivos? Necesitamos un método de procesamiento agrupado de los archivos del mismo tipo en la carpeta en cuestión. Esta tarea puede realizarse con eficacia mediante las funciones FindFirstFile(), FindNextFile() y FindClose() incluidas en kernel32.dll.


Función FindFirstFile()

La descripción de la función se da en msdn en: http://msdn.microsoft.com/en-us/library/aa364418(VS.85).aspx.

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

De esta descripción se deduce que la función devuelve el descriptor del archivo encontrado y que cumple los criterios de búsqueda. La condición de búsqueda se indica en la variable lpFileName que contiene la ruta de búsqueda del archivo y un posible nombre del archivo. Esta función es adecuada, ya que podemos especificar la búsqueda por máscara, por ejemplo, encontrar un archivo por máscara "C:\folder\*.txt". La función devolverá el primer archivo encontrado en la carpeta "C:\folder" y que tenga la extensión txt.

El resultado devuelto de la función es del tipo 'int' en MQL4. Para pasar el parámetro de entrada, podemos usar el tipo 'string'. Ahora tenemos que saber qué pasar a esta función como segundo parámetro y cómo procesar dicho parámetro más tarde. La función será importada aproximadamente así:

#import "kernel32.dll"
int  FindFirstFileA(string path, .some second parameter);
#import

Aquí podemos ver la ya conocida biblioteca de kernel32.dll. Sin embargo, el nombre de la función se especifica como FindFirstFileA(), no como FindFirstFile(). Esto proviene del hecho de que muchas funciones en esta biblioteca tienen dos versiones: para trabajar con strings en Unicode, la letra 'W' FindFirstFileW) se añade al nombre, mientras que para trabajar con ANSI se añade la letra 'A' (FindFirstFileA).

Ahora tenemos que saber el segundo parámetro de la función descrito como:

lpFindFileData [out] - Un puntero a la estructura WIN32_FIND_DATA que recibe información sobre un archivo o directorio encontrado.

Esto significa que es el puntero a una cierta estructura de WIN32_FIND_DATA. En este caso, la estructura es una cierta área en la RAM del PC. El puntero a esta área (dirección) se pasa a la función. Podemos asignar memoria en MQL4 usando una matriz de datos. El puntero se especifica con el carácter '&'. Solo tenemos que saber el tamaño de la memoria necesaria en bytes a pasar al puntero. A continuación se indica la descripción de la estructura.

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, 

En MQL4, no hay tipos DWORD, TCHAR o FILETIME. Se sabe que DWORD ocupa 4 bytes, como int en MQL4, TCHAR tiene una representación interna de un byte. Para calcular el tamaño total de la estructura WIN32_FIND_DATA en bytes, solo tenemos que saber qué es FILETIME.

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

FILETIME contiene dos DWORD, lo que significa que DWORD ocupa 8 bytes. Vamos a tabular todo esto:

Tipo Tamaño en bytes
DWORD 4
TCHAR 1
FILETIME 8

Ahora podemos calcular el tamaño de la estructura WIN32_FIND_DATA y visualizar qué y dónde puede encontrarse.

Tipo
Tamaño en bytes Nota:
dwFileAttributes 4Atributos de archivo
ftCreationTime 8Tiempo de creación del archivo/carpeta
ftLastAccessTime 8Momento del último acceso
ftLastWriteTime 8Momento de la última escritura
nFileSizeHigh 4 Tamaño máximo en bytes
nFileSizeLow 4 Tamaño mínimo en bytes
dwReserved0 4 No se suele definir y no se usa
dwReserved1 4 Se reserva para el futuro
cFileName[MAX_PATH] 260 (MAX_PATH = 260)
Nombre de archivo
cAlternateFileName[14] 14Nombre alternativo en el formato 8.3

Por tanto, el tamaño total de la estructura es: 4 + 8 + 8 + 8 + 4 + 4 + 4 +4 + 260 +14 = 318 bytes.


Como puede ver en la figura anterior, el nombre del archivo comienza en el byte 45 y los 44 bytes anteriores contienen información complementaria. Es necesario pasar a la función FindFirstFile() alguna estructura en MQL4 dimensionada a 318 bytes, como segundo parámetro. Sería más conveniente usar una matriz del tipo 'int', que tendría un tamaño no inferior al requerido. Dividimos 318 por 4 (ya que la representación interna del tipo 'int' es de 4 bytes), obtenemos 79.5 y lo redondeamos al entero mayor más cercano y vemos que necesitamos una matriz de 80 elementos.

La importación de la función será ahora la siguiente:

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

Aquí estamos usando la versión de la función con la letra 'A' al final del nombre, FindFirstFileA(), para el código ANSI. La matriz 'respuesta' se pasa por un enlace y sirve para ser llenada con la estructura de WIN32_FIND_DATA. Una llamada de ejemplo:

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

Funciones FindNextFileA() y FindClose()

La función FindNextFileA() recibe como primer parámetro el 'handle' del archivo previamente obtenido por la función FindFirstFileA() o por otra llamada anterior a la función FindNextFileA(). El segundo parámetro es lo mismo. La función FindClose() solo cierra la búsqueda. Por esta razón, toda la aparición de los datos de importación de las funciones será la siguiente:

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

Ahora tenemos que aprender a extraer el nombre de un archivo escrito en la matriz 'answer[]'.

Obtención del nombre del archivo

El nombre del archivo está incluido en la matriz, comenzando desde el byte 45 hasta el 304. El tipo 'int' contiene 4 bytes, por lo que cada elemento de la matriz contiene 4 caracteres, si asumimos que la matriz se rellena con caracteres. Por tanto, para referirnos al primer carácter en el nombre del archivo debemos saltar 44/4=11 elementos de la matriz 'answer[]'. El nombre del archivo dentro de la cadena de 65 (260/4=65) elementos de la matriz, comenzando por 'answer[11]' (el indexado comienza en cero) y finalizando con 'answer[76]'.

De este modo, podemos obtener de la matriz 'answer[]' el nombre del archivo en bloques de 4 caracteres en cada uno. El número 'int' representa una secuencia de 32 bits que, a su vez, representa 4 bloques de 8 bits cada uno.

El byte más reciente está a la derecha y el más antiguo a la izquierda. Los bits se numeran en orden ascendente, es decir, los bits desde el 1 hasta el 8 son los más recientes. Podemos extraer los bytes necesarios usando operaciones a nivel de bit. Para obtener el valor del byte más reciente, debemos rellenar todos los bits con los valores cero, desde el 9 hasta el 32. Esto se realiza usando el operador lógico AND.

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

Aquí 0x000000FF es el entero de 32 bits que tiene valores de todos los lugares, comenzando por el 9, mientras que los lugares desde el 1 hasta el 8 son iguales a uno. De este modo, el número b obtenido contendrá solo un byte (el más reciente) del número a. Convertiremos el byte obtenido (el código del carácter) en un string de un carácter usando la función CharToStr().

De acuerdo, hemos obtenido el primer carácter. ¿Cómo debemos obtener el siguiente? Muy fácil: Hacemos un cambio de 8 bits a nivel de bits a la derecha y el segundo bit sustituye al más reciente. Luego aplicamos la operación ya conocida del operador lógico AND.

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

Como podrá adivinar, el tercer byte se obtendrá cambiando a 16 bits, mientras que el más antiguo se obtendrá cambiando a 24 bits. De este modo, podemos extraer 4 caracteres de un elemento de la matriz del tipo 'int'. A continuación se muestra cómo se obtienen los primeros 4 caracteres en el nombre de archivo a partir de la matriz '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);

Vamos a crear una función que devuelve un string de texto desde la matriz pasada llamada 'buffer'.

//+------------------------------------------------------------------+
//|  read text from 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);
   }

De este modo se resuelve el problema de obtener el nombre del archivo a partir de la estructura.

Obtención de la lista de todos los asesores expertos con sus códigos fuente

Un simple script muestra las características de las funciones anteriores:

//+------------------------------------------------------------------+
//|                                                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);
  }
  
//+------------------------------------------------------------------+
//|  read text from 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);
   }  
//+------------------------------------------------------------------+

Aquí, después de las llamadas a las funciones FindFirstFileA() y FindNextFileA(), se cambia la matriz de win32_DATA (estructura WIN32_FIND_DATA) a un estado "vacío", es decir, todos los elementos de la matriz se rellenan con ceros:

   ArrayInitialize(win32_DATA,0);

Si no hacemos esto puede ocurrir que las "capturas" del nombre de archivo de la llamada anterior entren en el proceso y obtengamos algún galimatías.


Ejemplo de realización: Creación de copias de seguridad del código fuente

Un ejemplo simple que muestra las características prácticas de lo anterior es la creación de copias de seguridad de códigos fuente en un directorio especial. Para ello, debemos combinar las funciones consideradas en este artículo con las del artículo Operaciones con archivos mediante WinAPI. Obtendremos un script simple backup.mq4 que se muestra a continuación. Puede ver el código completo en el archivo adjunto. Aquí es solo la función start() la que usa todas las funciones descritas en ambos artículos:

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
   string expert[1000]; // must be enough 
   string EAname="";    // EA name
   int EAcounter = 0;   // EAs counter   
   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); // now it will surely be enough
      }
 
   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);
  }
//+------------------------------------------------------------------+


Conclusión

Se ha mostrado cómo realizar operaciones con un grupo de archivos del mismo tipo. Puede leer y procesar archivos usando las funciones anteriores y realizando su propia lógica. Puede hacer copias de seguridad puntuales usando un asesor experto o sincronizar dos conjuntos de archivos en distintas, importar las cotizaciones de algunas bases de datos y demás. Se recomienda controlar el uso de las DLL y no permitir conectar con bibliotecas de archivos de terceros con la extensión ex4. Debe asegurarse de que el programa que ejecuta no dañará los datos de su PC.