Análisis sintáctico HTML con ayuda de curl

23 octubre 2019, 15:17
Andrei Novichkov
0
507

Introducción

Otra tarea totalmente plausible, que surge cuando no es posible obtener algunos datos por parte del sitio web con solicitudes normales. ¿Qué hacer en tal caso? Está claro que lo primero que nos viene a la mente es buscar una fuente a la que podamos recurrir con solicitudes GET o POST. Pero es posible que no podamos encontrar tal fuente. Por ejemplo, si hablamos del funcionamiento de algún indicador único, sobre algún tipo de datos estadísticos que se actualizan con poca frecuencia.

Como sucede en otros casos, puede perfectemente surgir la pregunta: "¿Para qué necesitamos esto?" Podemos tomar de un script MQL la página de un sitio web y leer en un lugar conocido de antemano un número de símbolos conocido de antemano. Y, a continuación, trabajar con la línea obtenida. Sí, podemos hacerlo de esa forma. Pero este enfoque vinculará inalterablemente el código MQL del script al código HTML de la página. ¿Y si este código cambia, aunque sea solo un poco? Precisamente en estos casos necesitaremos un parser (analizador sintáctico), que nos dará la posibilidad de trabajar con un documento HTML como si se tratara de un árbol (volveremos a este punto en el apartado correspondiente). Es posible implementarlo en MQL, pero, ¿será esto correcto? ¿Cómo se reflejará en el rendimiento? ¿Resultará cómodo corregir y mejorar un código así? Por eso, la tarea del análisis sintáctico se mostrará en una biblioteca aparte. Debemos destacar de inmediato que el parser no resolverá todos los problemas. Hará su parte del trabajo, pero, ¿qué ocurrirá si el diseño del sitio web cambia de una forma tan radical que los nombres de las clases y atributos sean otros totalmente distintos? En este caso, deberemos cambiar el objeto de la búsqueda de forma operativa, y es posible que no solo un objeto. Por eso, una de las tareas a las que nos enfrentaremos será la creación del código necesario, lo más rápido posible y con el mínimo de esfuerzo. Y, preferiblemente, a partir de componentes ya preparados. En este caso, si se da la situación descrita, el desarrollador podrá corregir y mejorar el código, así como introducir los cambios de forma operativa.

Vamos a elegir algún sitio web con páginas no demasiado voluminosas, e intentaremos obtener del mismo información interesante. La elección en sí no es algo tan importante, pero de todas formas, intentaremos que nuestros esfuerzos no sean en vano. COmo es lógico, esta información deberá estar disponible para los scripts MQL en el terminal. El código de programación se creará en forma de biblioteca DLL estándar.

En el presente artículo, la tarea se resolverá sin llamadas asincrónicas y sin tener en cuenta el flujo múltiple.

Posibles soluciones disponibles

Los desarrolladores siempre se han interesado por la cuestión de la obtención de los datos de la red, con su posterior procesamiento, y es muy probable que lo sigan haciendo en el futuro. En concreto, en este sitio web podemos encontrar varios artículos con interesantes y diversos enfoques sobre el tema:

Recomendamos encarecidamente la lectura de todos estos artículos.

    Estableciendo las tareas

    Para realizar el experimento, proponemos el siguiente sitio web: https://www.mataf.net/en/forex/tools/volatility. Como podemos entender fácilmente por su nombre, en el sitio web están disponibles los datos sobre la volatilidad de las parejas de divisas. El valor de la volatilidad se ofrece en tres unidades de medición diferentes: en pips, en dólares estadounidenses y en tanto por ciento. La página del sitio web no es demasiado "pesada", podremos manejarla y parsearla sin problemas para obtener los valores necesarios. El estudio preliminar del texto fuente de la página nos muestra que debemos conseguir acceso a los valores guardados en celdas aparte del recuadro. Esto no debería suponer una complicación, pero resulta obvio que tenemos que dividir la tarea en dos subtareas:

    1. Obtener y guardar la página.
    2. Parsear la página obtenida, para obtener así la estructura del documento y buscar posteriormente la información necesaria en dicha estructura. Procesar la página y transmitirla al cliente.

    Vamos a concretar de inmediato el método de ejecución de la primera tarea. ¿Merece la pena guardar la página obtenida en forma de archivo? En una variante que funcione de verdad, resulta obvio que debemos hacerlo. Debemos ajustar la caché, que se actualizará con cierta periodicidad. También deberemos prever la posibilidad de renunciar a trabajar con esta caché en ciertos casos. Para qué necesitamos esto: si un cierto indicador MQL empieza a solicitar en cada tick la página de un sitio web de terceros, el uauario, probablemente, será baneado de este sitio. Y esto sucederá más rápido si el script que solicita los datos está instalado en varias parejas de divisas. En cualquier caso, un instrumento proyectado correctamente no atormentará a un sitio web de terceros con solicitudes constantes, sino que enviará la solicitud una sola vez, guardará el resultado en un archivo, y luego recurrirá a este archivo para conocer los datos. Al finalizar el plazo de validez/veracidad de la caché, el archivo se actualizará con una nueva solicitud, lo cual no sobrecargará los servidores de terceros.

    En nuestro caso, no vamos a proyectar una caché semejante. El envío de varias solicitudes duplicadas a un sitio web no podrá influir en su funcionamiento, mientras que nosotros nos centraremos en puntos más importantes. Mientras se realiza el proyecto, se darán ciertos comentarios con respecto al guardado de archivos en el disco, pero, en este caso, los datos se ubicarán en la memoria y se transmitirán al segundo bloque del programa, es decir, al parser propiamente dicho. En todo lo demás, se dará preferencia al código simplificado, que resulta comprensible para los principiantes y refleja la esencia de la cuestión analizada.

    Obteniendo la página HTML de un sitio web de terceros

    Como ya hemos dicho antes, una de las tareas principales es el uso de los componentes disponibles y adecuados, así como las bibliotecas preparadas. Esto no significa que para resolverla tengamos que sacrificar la fiabilidad o la seguridad de todo el sistema. Por eso, la selección de todos los componentes se basará en la reputación conocida del proyecto. Para obtener la página necesaria, usaremos el famoso proyecto abierto curl.

    Este proyecto es conocido por su utilidad a la hora de recibir y enviar archivos a prácticamente cualquier fuente: http, https, servidores ftp y muchas otras. Podemos establecer el nombre de usuario y la contraseña para entrar en el servidor, procesar las redirecciones y los timeouts. Dispone de una documentación bastante buena, en la que se describen las demás posibilidades del proyecto, que no son pocas. Entre otros aspectos positivos del proyecto, podemos nombrar su carácter multiplataforma y su código abierto. Debemos mencionar que existe otro proyecto más con el mismo cometido y la misma escala. Se llama wget. Sin embargo, aquí vamos a utilizar precisamente curl. Lo haremos por dos motivos:

    • curl puede recibir y enviar archivos, wget solo puede recibirlos.
    • wget solo existe en forma de programa de consola wget.exe.

    El hecho de que wget no pueda transmitir archivos no es importante para la tarea actual, ya que tenemos que obtener una página HTML. No obstante, tras familiarizarse con curl un poco más a fondo, el desarrollador quizá tome la decisión de usar curl en lo sucesivo, ya que su universalidad le podrá resultar útil.

    La existencia de la utilidad wget.exe y la ausencia de bibliotecas del tipo wget.dll, wget.lib tiene para nuestros objetivos un carácter decisivo, por motivos obvios:

    • Para incluir wget de una biblioteca dll conectada a MetaTrader, deberemos crear un proceso completo, lo cual conllevará mucho tiempo, y en este caso no está justificado.
    • Los datos obtenidos como el resultado de wget se podrán transmitir solo como archivo, pero hemos decidido no hacerlo, como hemos escrito más arriba.

    curl carece de estos defectos. Los más importante es que él, aparte del programa curl.exe en forma de consola, también tiene las bibliotecas libcurl-x64.dll y libcurl-x64.lib. Y esto permite incluir curl en nuestro programa sin crear un proceso adicional, lo cual, a su vez, nos da la posibilidad de no dar a los resultados del funcionamietno de curl la forma de archivo, sino trabajar con un búfer en la memoria. Asimismo, Curl también está disponible en forma de códigos fuente, pero crear a partir de ellos las bibliotecas necesarias puede resultar una tarea muy complicada. Por eso, en el directorio propuesto se incluyen las bibliotecas ya creadas, las dependencias y todos los archivos de inclusión necesarios para el trabajo.

    Creando la biblioteca

    Abrimos Visual Studio (hemos usado Visual Studio 2017) y creamos el proyecto de una dll normal, como se ha explicado más de una vez. Llamaremos al proyecto GetAndParse, y como consecuencia, obtendremos una biblioteca con el mismo nombre. Vamos a crear en la carpeta del proyecto la carpeta "lib" y la carpeta "include". Necesitaremos estas dos carpetas para conectarnos a las bibliotecas de terceros. En la biblioteca lib, copiamos libcurl-x64.lib, mientras que en la biblioteca include, creamos la carpeta "curl". Luego copiamos en esta carpeta todos los archivos de inclusión. Ahora, abrimos el punto del menú "Project -> GetAndParse Properties". En la parte izquierda de la ventana de diálogo, desplegamos el submenú "C/C++" y elegimos "General". Seleccionamos en la parte derecha "Additional Include Directories", pulsamos sobre el signo hacia abajo y elegimos "Edit". En la nueva ventana de diálogo, pulsamos el botón a la izquierda del todo, en la línea superior de "New Line". Esta acción añadirá una línea a la lista de abajo, que se podrá editar. Pulsando el botón de la derecha, seleccionamos la carpeta "include" que acabamos de crear y pulsamos "OK".

    Desplegamos el submenú "Linker", seleccionamos General, y luego el punto de la derecha "Additional Library Directories". Repitiendo las acciones descritas, añadimos la carpeta "lib" creada.

    En este mismo submenú, seleccionamos la línea "input", y a la derecha "Additional Dependencies". Añadimos en la ventana superior el nombre "libcurl-x64.lib".

    Tampoco podremos arreglárnoslas sin libcurl-x64.dll. Copiamos este archivo junto con la biblioteca de soporte de cifrado en las carpetas debug y release.

    En el directorio propuesto, los archivos necesarios se encuentran en su sitio, y también se han ejecutado todos los cambios necesarios en las propiedades del proyecto, por eso, no tenemos que realizar ninguna acción adicional.

    Clase para obtener la página HTML

    Creamos en el proyecto la clase CCurlExec, que precisamente se ocupará de la tarea establecida. Tendrá que interactuar con curl, por eso tenemos que realizar la inclusión necesaria:

    #include <curl\curl.h>

    Podemos hacerlo en el archivo CCurlExec.cpp, pero preferimos incluirla en stdafx.h

    Definimos el pseudónimo del tipo para la función de llamada inversa, que es necesaria al guardar los datos obtenidos:

    typedef size_t (*callback)(void*, size_t, size_t, void*);

    Proyectamos las estructuras sencillas para guardar en la memoria los datos obtenidos:

    typedef struct MemoryStruct {
            vector<char> membuff;
            size_t size = 0;
    } MSTRUCT, *PMSTRUCT;
    

    ... y en el archivo:

    typedef struct FileStruct {
            std::string CalcName() {
                    char cd[MAX_PATH];
                    char fl[MAX_PATH];
                    srand(unsigned(std::time(0)));
                    ::GetCurrentDirectoryA(MAX_PATH, cd);
                    ::GetTempFileNameA(cd, "_cUrl", std::rand(), fl);
                    return std::string(fl);
            }
            std::string filename;
            FILE* stream = nullptr;
    } FSTRUCT, *PFSTRUCT;
    

    Seguramente, no será necesario realizar ninguna aclaración para estas estructuras. En primer lugar, nos interesa el guardado de información en la memoria, para ello, existe en la estructura MSTRUCT un búfer y su tamaño.

    Para guardar la información como archivo (vamos a preparar un proyecto para este método, pero en realidad solo trabajaremos con el guardado en la memoria), añadimos en la estructura FSTRUCT la función de obtención del nombre del archivo. Para ello, se usa Windows API para trabajar con archivos temporales.

    Ahora, vamos a crear un par de funciones de llamada inversa para rellenar la estructuras descritas. Método para rellenar una estructura del tipo MSTRUCT:

    size_t CCurlExec::WriteMemoryCallback(void * contents, size_t size, size_t nmemb, void * userp)
    {
            size_t realsize = size * nmemb;
            PMSTRUCT mem = (PMSTRUCT)userp;
            vector<char>tmp;
            char* data = (char*)contents;
            tmp.insert(tmp.end(), data, data + realsize);
            if (tmp.size() <= 0) return 0;
            mem->membuff.insert(mem->membuff.end(), tmp.begin(), tmp.end() );
            mem->size += realsize;
            return realsize;
    }
    

    No vamos a mostrar el segundo método para guardar los datos en un archivo, pues es muy semejante. Solo destacaremos que las signaturas de estas funciones han sido tomadas de la documentación disponible en el sitio web del proyecto curl.

    Ambos métodos son necesarios como "funciones por defecto". Se llamarán en el caso de que el desarrollador no escriba sus métodos para este fin. 

    La esencia de ambos métodos es muy sencilla. En los parámetros de los métodos se transmiten los datos sobre el tamaño de los datos obtenidos, el puntero de la fuente del búfer interno de curl y la estructura receptora de MSTRUCT. En el método se realizan algunas transformaciones preliminares, después de lo cual se rellenan los campos de la estructura receptora.

    Y, finalmente, el método que realiza las acciones básicas: obtener la página HTML y rellenar con los datos obtenidos la estructura del tipo MSTRUCT:

    bool CCurlExec::GetFiletoMem(const char* pUri)
    {
            CURL *curl;
            CURLcode res;
            res  = curl_global_init(CURL_GLOBAL_ALL);
            if (res == CURLE_OK) {
                    curl = curl_easy_init();
                    if (curl) {
                            curl_easy_setopt(curl, CURLOPT_URL, pUri);
                            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
                            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
                            curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, m_errbuf);
                            curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L);
                            curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 60L);
                            curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0");
                            curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); //redirecciones
    #ifdef __DEBUG__ 
                            curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    #endif
                            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
                            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &m_mchunk);
                            res = curl_easy_perform(curl);
                            if (res != CURLE_OK) PrintCurlErr(m_errbuf, res);
                            curl_easy_cleanup(curl);
                    }// if (curl)
                    curl_global_cleanup();
            } else PrintCurlErr(m_errbuf, res);
            return (res == CURLE_OK)? true: false;
    }
    

        Vamos a destacar los momentos importantes relacionados con el funcionamiento de curl. En primer lugar, se dan dos inicializaciones, como resultado de las cuales, el desarrollador recibe el puntero al "núcleo" de curl, a su "handle" utilizado en las siguientes llamadas. Se realizan los ajustes de la futura conexión, que pueden ser muchos. En este caso, determinamos la dirección de la conexión, si debemos comprobar los certificados, indicamos el búfer donde se van a registrar los errores, determinamos la longitud de los timeouts, el "user-agent" de los encabezados, si hay que procesar las redirecciones, indicamos la función se llamará para procesar los datos obtenidos (en este caso, se trata del método por defecto mencionado más arriba) y el objeto para guardar estos datos. Si ajustamos la opción CURLOPT_VERBOSE  activaremos la muestra de información detallada sobre las operaciones realizadas, lo cual resulta muy útil durante la depuración. Después de establecer todas las opciones, se llama la función curl curl_easy_perform, que precisamente realiza el trabajo y la posterior limpieza.

        Vamos a añadir un método más, de carácter más general:

        bool CCurlExec::GetFile(const char * pUri, callback pFunc, void * pTarget)
        {
                CURL *curl;
                CURLcode res;
                res = curl_global_init(CURL_GLOBAL_ALL);
                if (res == CURLE_OK) {
                        curl = curl_easy_init();
                        if (curl) {
                                curl_easy_setopt(curl, CURLOPT_URL, pUri);
                                curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
                                curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
                                curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, m_errbuf);
                                curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L);
                                curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 60L);
                                curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0");
                                curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 
        #ifdef __DEBUG__ 
                                curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
        #endif
                                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, pFunc);
                                curl_easy_setopt(curl, CURLOPT_WRITEDATA, pTarget);
                                res = curl_easy_perform(curl);
                                if (res != CURLE_OK) PrintCurlErr(m_errbuf, res);
                                curl_easy_cleanup(curl);
                        }// if (curl)
                        curl_global_cleanup();
                }       else PrintCurlErr(m_errbuf, res);
        
                return (res == CURLE_OK) ? true : false;
        }
        

        Este método permite al desarrollador usar su propia función de llamada inversa para procesar los datos obtenidos (parámetro pFunc) y su propio objeto de guardado (parámetro pTarget). De esta forma, será posible guardar fácilmente la página HTML obtenida en forma de archivo csv, por ejemplo.

        Vamos a decir unas palabras sobre el guardado de información en forma de archivo, sin detenernos en esta tarea con detalle. Ya hemos mencionado la función corresondiente de llamada inversa y el objeto auxiliar FSTRUCT con un cierto código para seleccionar el nombre del archivo. Sin embargo, en la mayoría de los casos, el trabajo no finaliza con ello. Para obtener el nombre del archivo, podemos o bien establecerlo de antemano (y comprobar antes del registro si ya existe un archivo con ese nombre), o bien permitir a la biblioteca obtener un nombre de archivo legible y meditado. Conviene obtener ese nombre a partir de la dirección real según la cual se realizado la lectura después de procesar las redirecciones. Obteniendo la dirección real mostrada en el método 

        bool CCurlExec::GetFiletoFile(const char * pUri)

        que no se muestra por completo aquí, pero que existe en el directorio. Esta dirección debe ser parseada con las fuerzas del propio curl, que posee para ello todos los recursos necesarios:

        std::string CCurlExec::ParseUri(const char* pUri) {
        #if !CURL_AT_LEAST_VERSION(7, 62, 0)
        #error "this example requires curl 7.62.0 or later"
                return  std::string();
        #endif
                CURLU *h  = curl_url();
                if (!h) {
                        cerr << "curl_url(): out of memory" << endl;
                        return std::string();
                }
                std::string szres{};
                if (pUri == nullptr) return  szres;
                char* path;
                CURLUcode res;
                res = curl_url_set(h, CURLUPART_URL, pUri, 0);
                if ( res == CURLE_OK) {
                        res = curl_url_get(h, CURLUPART_PATH, &path, 0);
                        if ( res == CURLE_OK) {
                                std::vector <string> elem;
                                std::string pth = path;
                                if (pth[pth.length() - 1] == '/') {
                                        szres = "index.html";
                                }
                                else {
                                        Split(pth, elem);
                                        cout << elem[elem.size() - 1] << endl;
                                        szres =  elem[elem.size() - 1];
                                }
                                curl_free(path);
                        }// if (curl_url_get(h, CURLUPART_PATH, &path, 0) == CURLE_OK)
                }// if (curl_url_set(h, CURLUPART_URL, pUri, 0) == CURLE_OK)
                return szres;
        }
        

        Preste atención: curl separa correctamente "PATH" de uri, y después se comprueba si "PATH" finaliza con el símbolo '/'. Si finaliza, el nombre deberá ser "index.html". Si no, "PATH" se dividirá en elementos aparte, mientras que el nombre del archivo deberá ser el último en la lista de tales elementos.

        En nuestro proyecto, se implementan los métodos descritos anteriormente, pero, en general, la tarea de conservar los datos en forma de archivo no ha sido resuelta, por actual que sea en este caso.

        Aparte de los descritos, en la clase CCurlExec se han previsto dos métodos elementales para obtener acceso al búfer de memoria en el que se han guardado los datos obtenidos de la red. Los datos pueden ofrecerse en forma de

        std::vector<char>
        , o bien en forma de
        std::string

        lo cual depende de nuestra posterior selección del parser html. Los demás métodos y propiedades de la clase CCurlExec no tienen interés alguno, y no vamos a analizarlos aquí.

        Al final del presente capítulo, diremos unas palabras más. La biblioteca curl es segura con respecto a los flujos. En este caso, esta se usa de forma sincrónica, aplicando para ello los métodos de tipo curl_easy_init. Todas las funciones curl en cuyo nombre se encuentre "easy", se usarán solo de forma sincrónica. En el uso asincrónico de la bibiloteca, ayudarán las funciones en cuyo nombre se encuentre la palabra "multi", por ejemplo, el análogo asincrónico de la función sincrónica curl_easy_init será curl_multi_init. El trabajo con las funciones asincrónicas curl no supone especial dificultad, pero deberá acompañarse con una cantidad de código considerable en cuanto a su volumen. Por eso, no vamos a analizar el trabajo asincrónico en estos momentos, aunque es totalmente plausible que volvamos a ello más tarde.

        Clase para el parseo HTML

        Para realizar esta tarea, también intentaremos seleccionar un componente preparado. Hay bastantes así, de diferente calidad y nivel de preparación. Al seleccionar un componente para trabajar, nos guiaremos por los mismos criterios que en el capítulo anterior. En este caso, la variable preferible para trabajar será Gumbo, de Google. Esta se ofrece en github, incluiremos un enlace a la misma en el directorio con el proyecto. Quien así lo desee, podrá recopilar el proyecto de forma independiente, eso resulta más sencillo que recopilar curl, pero en el proyecto que se encuentra en el directorio ya se ha incluido todos los archivos necesarios:

        • gumbo.lib en la carpeta del proyecto lib
        • gumbo.dll en las carpetas debug y release

        Abrimos de nuevo el punto del menú  "Project -> GetAndParse Properties". Abrimos el submenú "Linker", elegimos la línea "input", y a la derecha "Additional Dependencies". Añadimos a la ventana superior el nombre "gumbo.lib".

        Además, en la carpeta include creada anteriormente, creamos la carpeta gumbo y copiamos en ella todos los archivos de inclusión. Registramos la entrada necesaria en el archivo stdafx.h:

        #include <gumbo\gumbo.h>
        
        

        Un par de palabras sobre gumbo. Se trata de un parser de código html5 con Si99. Ventajas:

        • Correspondencia total con las especificaciones de HTML5.
        • Resistencia a los datos de entrada erróneos.
        • API sencillas, que pueden ser llamadas desde otros lenguajes.
        • Supera todos las pruebas html5lib-0.95 tests.
        • Comprobado en más de dos mil quinientos millones de páginas del índice de Google.

        Desventajas:

        • Rendimiento no demasiado alto

        Además, podemos mencionar entre las posibles desventajas el hecho de que el parser construya el árbol de páginas y no haga nada más. Ofrece el árbol al desarrollador y ya este puede trabajar con él como considere necesario. Podemos detectar las fuentes que proporcionan los envoltorios de este parser, pero no vamos a utilizarlas. En este artículo, no vamos a tratar de "perfeccionar" el parser, sino de usarlo como está. Este nos ayudará a construir el árbol, donde realizaremos la búsqueda de los datos que necesitamos. El trabajo con el componente es sencillo:

                GumboOutput* output = gumbo_parse(input); 
        //      ... do something with output ...
                gumbo_destroy_output(&options, output);
        

        Llamamos la función, transmitiendo a la misma el puntero al búfer con los datos HTML fuente. La función construye el árbol con el que trabaja el desarrollador. El desarrollador llama la función y libera recursos.

        Vamos a proceder a ejecutar esta tarea. Comenzaremos estudiando el código html de la página que necesitamos. El objetivo es bastante obvio: debemos comprender qué tenemos que buscar y dónde se encuentran los datos necesarios. Entramos en el enlace _https://www.mataf.net/en/forex/tools/volatility y miramos el código fuente de la página. Los datos sobre la volatilidad se observan en el recuadro <table id="volTable" ... Estos datos son más que suficientes para encontrar dicho recuadro en el árbol. Claro está que deberemos obtener la volatilidad para una pareja de divisas concreta. Estudiamos el recuadro y observamos que sus líneas continen en sus atributos el nombre de la pareja de divisas: <tr id="row_AUDCHF" class="data_volat" name="AUDCHF"... En función de estos datos, podemos buscar fácilmente la línea necesaria. Cada línea consta de cinco columnas. Las dos primeras no nos interesan, pero las tres últimas sí que contienen información necesaria. Elegimos alguna columna, obtenemos los datos de texto, los convertimos en double y los retornamos al cliente. Para que todo sea más sencillo de entender, distribuiremos la tarea de búsqueda en el árbol en tres etapas:

        1. Buscamos el recuadro según su identificador ("volTable" ).
        2. Buscamos la línea usando su identificador ("row_NombredelaPareja de Divisas").
        3. Buscamos el valor de la volatilidad en una de las tres últimas columnas y retornamos el valor encontrado.
        Vamos a proceder a escribir el código. Creamos en el proyecto la clase CVolatility. Ya hemos incluido la biblioteca del parser, por eso, no necesitamos preocuparnos sobre ello. Como recordamos, la volatilidad se representaba en el recuadro necesario en tres columnas, de tres formas diferentes. Por eso, vamos a crear la enumeración correspondiente para realizar la selección:
        typedef enum {
                byPips = 2,
                byCurr = 3,
                byPerc = 4
        } VOLTYPE;
        

        Imaginamos que la enumeración no requiere de ningún comentario adicional. Se trata simplemente de elegir el número de columna.

        A continuación, crearemos un método que nos devolverá el valor de la volatilidad:

        double CVolatility::FindData(const std::string& szHtml, const std::string& pair, VOLTYPE vtype)
        {
                if (pair.empty()) return -1;
                m_pair = pair;
                TOUPPER(m_pair);
                m_column = vtype;
                GumboOutput* output = gumbo_parse(szHtml.c_str() );
                double res = FindTable(output->root);
                const GumboOptions mGumboDefaultOptions = { &malloc_wrapper, &free_wrapper, NULL, 8, false, -1, GUMBO_TAG_LAST, GUMBO_NAMESPACE_HTML };
                gumbo_destroy_output(&mGumboDefaultOptions, output);
                return res;
        }// void CVolatility::FindData(char * pHtml)
        
        

        Llamamos el método con los siguientes argumentos:

        • szHtml — enlace al búfer con los datos obtenidos en el formato html.
        • pair — denominación de la pareja de divisas para la que buscamos la volatilidad
        • vtype — "tipo" de volatilidad, número de columna en el recuadro buscado

        Retorna al método el valor de la volatilidad, en caso de error -1.

        Como podemos ver por el código, el trabajo comienza por la búsqueda del recuadro desde el inicio del árbol. Esta búsqueda es ejecutada por el siguiente método cerrado:

        double CVolatility::FindTable(GumboNode * node) {
                double res = -1;
                if (node->type != GUMBO_NODE_ELEMENT) {
                        return res; 
                }
                GumboAttribute* ptable;
                if ( (node->v.element.tag == GUMBO_TAG_TABLE)                          && \
                        (ptable = gumbo_get_attribute(&node->v.element.attributes, "id") ) && \
                        (m_idtable.compare(ptable->value) == 0) )                          {
                        GumboVector* children = &node->v.element.children;
                        GumboNode*   pchild = nullptr;
                        for (unsigned i = 0; i < children->length; ++i) {
                                pchild = static_cast<GumboNode*>(children->data[i]);
                                if (pchild && pchild->v.element.tag == GUMBO_TAG_TBODY) {
                                        return FindTableRow(pchild);
                                }
                        }//for (int i = 0; i < children->length; ++i)
                }
                else {
                        for (unsigned int i = 0; i < node->v.element.children.length; ++i) {
                                res = FindTable(static_cast<GumboNode*>(node->v.element.children.data[i]));
                                if ( res != -1) return res;
                        }// for (unsigned int i = 0; i < node->v.element.children.length; ++i) 
                }
                return res;
        }//void CVolatility::FindData(GumboNode * node, const std::string & szHtml)
        

        El método se llama de forma recursiva hasta que no se encuentre un elemento que cumpla dos requisitos:

        1. Deberá ser un recuadro.
        2. Su "id" deberá ser "volTable".
        Si no se localiza un elemento así, el método retornará -1. En caso contrario, el método retornará el valor que devolverá el método análogo que realiza la búsqueda de la línea en el recuadro:
        double CVolatility::FindTableRow(GumboNode* node) {
                std::string szRow = "row_" + m_pair;
                GumboAttribute* prow       = nullptr;
                GumboNode*      child_node = nullptr;
                GumboVector* children = &node->v.element.children;
                for (unsigned int i = 0; i < children->length; ++i) {
                        child_node = static_cast<GumboNode*>(node->v.element.children.data[i]);
                        if ( (child_node->v.element.tag == GUMBO_TAG_TR) && \
                                 (prow = gumbo_get_attribute(&child_node->v.element.attributes, "id")) && \
                                (szRow.compare(prow->value) == 0)) {
                                return GetVolatility(child_node);
                        }
                }// for (unsigned int i = 0; i < node->v.element.children.length; ++i)
                return -1;
        }// double CVolatility::FindVolatility(GumboNode * node)
        
        
        Después de que hayamos localizado la línea con una "id" igual a "row_Nombredelapareja" en el recuadro, la búsqueda finalizará con la llamada del método que lee el valor de la celda del recuadro de la columna determinada de la línea encontrada:
        double CVolatility::GetVolatility(GumboNode* node)
        {
                double res = -1;
                GumboNode*      child_node = nullptr;
                GumboVector* children = &node->v.element.children;
                int j = 0;
                for (unsigned int i = 0; i < children->length; ++i) {
                        child_node = static_cast<GumboNode*>(node->v.element.children.data[i]);
                        if (child_node->v.element.tag == GUMBO_TAG_TD && j++ == (int)m_column) {
                                GumboNode* ch = static_cast<GumboNode*>(child_node->v.element.children.data[0]);
                                std::string t{ ch->v.text.text };
                                std::replace(t.begin(), t.end(), ',', '.');
                                res = std::stod(t);
                                break;
                        }// if (child_node->v.element.tag == GUMBO_TAG_TD && j++ == (int)m_column)
                }// for (unsigned int i = 0; i < children->length; ++i) {
                return res;
        }
        
        

        Debemos decir que en el recuadro se usa como separador una coma, y no un punto. Por eso, disponemos de varias líneas qué eliminarán este problema. Al igual que en los anteriores métodos análogos, se retornará -1 en caso de error, o el valor de la volatilidad en caso de éxito.

        No obstante, hay que mencionar de inmediato un defecto de este enfoque. En cualquier caso, el código estará fuertemente vinculado a datos sobre los que el desarrollador no podrá influir en forma alguna, aunque el parser debilita considerablemente esta dependencia. Como resultado, el desarrollador tendrá que aceptar que deberá rehacer toda la búsqueda en el árbol, en el caso de que suceda un cambio verdaderamente sustancial en el diseño del sitio web. Pero esto se verá un poco compensado por la sencillez de la búsqueda, y por el hecho de que rehacer las pocas funciones descritas no requerirá de mucho esfuerzo.

        Los demás miembros de la clase CVolatility no suponen ningún interés, se encuentran en el directorio adjunto y no serán analizados aquí.

        Compilamos todo junto

        El código principal ya ha sido descrito. Ahora debemos recopilarlo todo y proyectar la función que creará los objetos y ejecutará las llamadas en la secuencia necesaria. Pegaremos este código en el archivo GetAndParse.h:

        #ifdef GETANDPARSE_EXPORTS
        #define GETANDPARSE_API extern "C" __declspec(dllexport)
        #else
        #define GETANDPARSE_API __declspec(dllimport)
        #endif
        
        GETANDPARSE_API double GetVolatility(const wchar_t* wszPair, UINT vtype);
        

        La definición de la macro ya se encuentra ahí, la hemos redactado un poco para que el código mql pueda llamar esta función. Cómo y para qué se hace esto, se describe aquí.

        Escribimos el código para esta función en el archivo GetAndParse.cpp:

        const static char vol_url[] = "https://www.mataf.net/ru/forex/tools/volatility";
        
        GETANDPARSE_API double GetVolatility(const wchar_t*  wszPair, UINT vtype) {
                if (!wszPair) return -1;
                if (vtype < 2 || vtype > 4) return -1;
        
                std::wstring w{ wszPair };
                std::string s(w.begin(), w.end());
        
                CCurlExec cc;
                cc.GetFiletoMem(vol_url);
                CVolatility cv;
                return cv.FindData(cc.GetBufferAsString(), s, (VOLTYPE)vtype);
        }
        
        

        ¿Convendría establecer en el código la dirección de la página? ¿Por qué no hacer esta dirección el argumento de la llamada de la función GetVolatility? No tiene sentido, dado que el algoritmo de búsqueda de información en el árbol que retorna el parser está forzosamente vinculado a los elementos de la página HTML. Por eso, lo lógico es vincular también la biblioteca completa a una dirección concreta. Este no es el método que nos conviene usar, pero aquí tendremos que seguir precisamente este.

        Montando e instalando la biblioteca

        Compilamos la biblioteca de la forma habitual. Recopilamos de la carpeta Release todas las dll que haya allí: GETANDPARSE.dll, gumbo.dll, libcrypto-1_1-x64.dll, libcurl-x64.dll, libssl-1_1-x64.dll y las copiamos en la carpeta Libraries del terminal. La bibloteca ya está instalada.

        Script tutorial de uso de la biblioteca

        El script es muy simple, así que lo mostraremos entero:

        #property copyright "Copyright 2019, MetaQuotes Software Corp."
        #property link      "https://www.mql5.com"
        #property version   "1.00"
        #property script_show_inputs
        
        #import "GETANDPARSE.dll"
        double GetVolatility(string wszPair,uint vtype);
        #import
        //+------------------------------------------------------------------+
        //|                                                                  |
        //+------------------------------------------------------------------+
        enum ReqType 
          {
           byPips    = 2, //Volatility by Pips
           byCurr    = 3, //Volatility by Currency
           byPercent = 4  //Volatility by Percent
          };
        
        input string  PairName="EURUSD";
        input ReqType tpe=byPips; 
        //+------------------------------------------------------------------+
        //| Script program start function                                    |
        //+------------------------------------------------------------------+
        
        void OnStart()
          {
           double res=GetVolatility(PairName,tpe);
           PrintFormat("Volatility for %s is %.3f",PairName,res);
          }
        

        Creemos que el script no necesita ningún comentario, el lector podrá encontrarlo en los archivos adjuntos

        Conclusión

        Hemos analizado un método de parseo HTML de una página web sencillo hasta su máxima expresión. La biblioteca está compuesta de elementos ya preparados, y el código se ha simplificado al máximo para los principiantes en C++, o aquellos que no lo conozcan lo suficiente. Debemos decir que la principal desventaja del enfoque aplicado es su carácter sincrónico. El script no tomará el control hasta que la biblioteca no obtenga y procese la página HTML. Esto requiere tiempo, lo que resulta inaceptable para indicadores y asesores. Necesitamos otro enfoque, que trataremos de encontrar en próximos artículos.


        Programas usados en el artículo:

         # Nombre
        Tipo
         Descripción
        1 GetVolat.mq5
        Script
        Script que obtiene los datos sobre la volatilidad.
        2
        GetAndParse.zip Directorio
        Código fuente de la biblioteca y aplicación de consola de prueba


        Traducción del ruso hecha por MetaQuotes Software Corp.
        Artículo original: https://www.mql5.com/ru/articles/7144

        Archivos adjuntos |
        GetVolat.mq5 (1.45 KB)
        GetAndParse.zip (4605.35 KB)
        Creando un EA gradador multiplataforma (Parte III): cuadrícula de correcciones con martingale Creando un EA gradador multiplataforma (Parte III): cuadrícula de correcciones con martingale

        En este artículo, intentaremos crear el mejor asesor posible de aquellos que funcionan según el principio del gradador. Como siempre, se tratará de un asesor multiplataforma, capaz de funcionar tanto en MetaTrader 4, como en MetaTrader 5. El primer asesor era bueno en todo, excepto en que no podía traer beneficios en un periodo de tiempo prolongado. El segundo asesor podía funcionar en intervalos superiores a varios años. Pero era incapaz de lograr más de un 50% de beneficio anual con una reducción máxima inferior al 50%.

        Nuevo enfoque a la interpretación de la divergencia clásica e inversa. Parte 2 Nuevo enfoque a la interpretación de la divergencia clásica e inversa. Parte 2

        En este artículo vamos a analizar en clave crítica la divergencia clásica y estudiar la efectividad de diferentes indicadores. Asimismo, ofreceremos distintas variantes de filtrado para aumentar la precisión de análisis y continuaremos analizando soluciones no estándar. Como resultado, crearemos una herramienta atípica para resolver la tarea marcada.

        Recetas MQL5 – Prueba de estrés de una estrategia comercial con ayuda de símbolos personalizados Recetas MQL5 – Prueba de estrés de una estrategia comercial con ayuda de símbolos personalizados

        En el artículo se analiza un enfoque sobre la prueba de estrés de estrategias comerciales con ayuda de símbolos personalizados Para este objetivo se crea una clase de símbolo de usuario. Con su ayuda, se obtendrán los datos de ticks desde fuentes de terceros y se cambiarán las propiedades del símbolo. Según los resultados del trabajo realizado, se ofrecerán variantes de cambio de las condiciones comerciales con respecto a las cuales se simula la estrategia comercial.

        Creando un EA gradador multiplataforma (última parte): la diversificación como método para aumentar la rentabilidad Creando un EA gradador multiplataforma (última parte): la diversificación como método para aumentar la rentabilidad

        En los anteriores artículos de la serie, hemos intentado crear de formas distintas un asesor gradador más o menos rentable. En esta ocasión, vamos a tratar de aumentar la rentabilidad del asesor comercial con la ayuda de la diversificación. Nuestro objetivo es el 100% de beneficio anual anhelado por todos, con un 20% de reducción máxima del balance.