Analisador Sintático HTML com o curl

Andrei Novichkov | 4 janeiro, 2020

Introdução

Vamos discutir o caso, quando os dados de um site não podem ser obtidos usando os métodos de requisição normais. O que pode ser feito neste caso? A primeira ideia possível é encontrar um recurso que possa ser acessado usando os métodos HTTP GET ou POST. Às vezes, esses recursos não existem. Por exemplo, isso pode dizer respeito à operação de algum indicador exclusivo ou ao acesso a dados estatísticos que são raramente atualizados.

Podemos questionar: "Qual é o objetivo?" Uma solução simples é acessar a página do site diretamente de um script em MQL e ler um número já conhecido de posições de uma página já vista. Assim, a porção de código recebida pode ser processada posteriormente. Este é um dos métodos possíveis. Mas, neste caso, o código do script em MQL será fortemente vinculado ao código HTML da página. E se o código HTML mudar? É por isso que nós precisamos de um analisador sintático (parser) que permita uma operação semelhante a uma árvore com um documento HTML (os detalhes serão discutidos em uma seção separada). Se nós implementarmos o analisador em MQL, isso será conveniente e eficiente em termos de desempenho? Esse código pode ser mantido de forma adequada? É por isso que a tarefa de um analisador será implementado em uma biblioteca separada. No entanto, o analisador não resolverá todos os problemas. Ele executará a funcionalidade desejada. Mas e se o leiaute do site mudar radicalmente e usar outros nomes e atributos de classe? Nesse caso, nós precisaremos alterar o objeto de busca ou vários objetos deste evento. Portanto, um dos nossos objetivos é criar o código da maneira mais rápida possível e com o mínimo de esforço. Será melhor se nós usarmos ferramentas que já estão prontas. Isso permitirá que o desenvolvedor mantenha o código com facilidade e edite-o rapidamente em caso de acontecer a situação descrita acima.

Nós selecionaremos um site com páginas não muito extensas e tentaremos obter alguns dados interessantes dele. O tipo de dados não é importante neste caso, no entanto, vamos tentar criar uma ferramenta útil. Obviamente, esses dados devem estar disponíveis nos scripts em MQL da plataforma. O código do programa será criado como uma DLL padrão.

Neste artigo, nós implementaremos a ferramenta sem chamadas assíncronas e multithread.

Soluções existentes

A possibilidade de obter os dados da Internet e processá-los sempre foi interessante para os desenvolvedores. O site mql5.com apresenta vários artigos, que descrevem abordagens interessantes e diversas:

Eu recomendo que você leia esses artigos.

Definindo as Tarefas

Nós vamos experimentar o seguinte site: https://www.mataf.net/en/forex/tools/volatility. Como fica claro no nome, o site apresenta dados sobre a volatilidade dos pares de moedas. A volatilidade é mostrada em três unidades diferentes: pips, dólar americano e percentual. A página do site não é muito extensa; portanto, pode ser aceita e analisada com eficiência para obter os valores necessários. O estudo preliminar do código fonte mostra que nós teremos que obter os valores armazenados em células de tabela separadas. Assim, vamos dividir a tarefa principal em duas subtarefas:

  1. Obtenção e armazenamento da página.
  2. Análise da página obtida para receber a estrutura do documento e busca das informações necessárias nessa estrutura. Processamento de dados e envio para o cliente.

Vamos começar com a implementação da primeira parte. Nós precisamos salvar a página obtida como um arquivo? Em uma versão realmente funcional, é óbvio que não há necessidade de salvar a página. Nós precisamos de uma cache ajustável, que será atualizada em determinados intervalos. Deve-se haver a possibilidade de desativar o uso dela em casos especiais. Por exemplo, se um indicador em MQL enviar consultas para a página de origem a cada tick, é provável que esse indicador seja banido deste site. O banimento ocorrerá de forma mais rápida se o script de requisição de dados estiver sendo executado em vários pares de moedas. De qualquer forma, a ferramenta projetada corretamente não enviará requisições com muita frequência. Em vez disso, ela enviará uma solicitação uma vez, salvará o resultado em um arquivo e posteriormente solicitará os dados deste arquivo. Após a expiração da validade da cache, o arquivo será atualizado usando uma nova solicitação. Isso evita o envio de solicitações muito frequentes.

No nosso caso, nós não criaremos essa cache. Ao enviar várias solicitações de teste, nós não afetaremos a operação do site. Em vez disso, nós podemos nos concentrar em pontos mais importantes. Comentários adicionais serão fornecidos sobre o armazenamento de arquivos em disco, mas, neste caso, os dados serão salvos na memória e passados para o segundo bloco do programa, ou seja, para o analisador. Nós usaremos o código simplificado sempre que aplicável, tornando-o compreensível para os iniciantes e refletindo ainda a essência da ideia principal.

Obtenção da página HTML de um site de terceiros

Como mencionado anteriormente, uma das ideias é utilizar componentes prontos para uso existentes e bibliotecas prontas. No entanto, nós ainda precisamos garantir a confiabilidade e a segurança de todo o sistema. Os componentes serão selecionados com base em sua reputação. Para obtermos a página desejada, nós usaremos o famoso projeto de código aberto chamado curl.

Este projeto permite o recebimento e envio de arquivos para quase todas as fontes: http, https, servidores ftp e muitas outras. Ele suporta a definição de login e senha para autenticação em servidores, processamento de redirecionamentos e timeouts. O projeto é fornecido com uma documentação abrangente que descreve todos os recursos do projeto. Além disso, ele é um projeto multiplataforma de código aberto, que é definitivamente uma vantagem. Há outro projeto que pode implementar a mesma funcionalidade. O projeto é chamado de 'wget'. No entanto, nesse caso, o curl é usada pelos dois motivos a seguir:

  • o curl pode receber e enviar arquivos, enquanto o wget apenas recebe arquivos.
  • O wget está disponível apenas como o aplicativo de console wget.exe.

A incapacidade de enviar arquivos pelo wget não é relevante para a nossa tarefa, porque nós precisamos receber apenas uma página em HTML. No entanto, se nos familiarizarmos com o curl, nós poderemos usá-lo posteriormente para outras tarefas, que podem exigir o envio de dados.

Uma desvantagem mais séria está relacionada ao fato de que ele só está disponível como utilitário wget.exe sem bibliotecas como wget.dll, wget.lib.

  • Nesse caso, para usar o wget de uma dll conectada ao MetaTrader, nós precisaremos criar um processo separado, que consome tempo e esforço.
  • Os dados obtidos via wget podem ser passados apenas como um arquivo, o que é inconveniente para nós, pois decidimos usar a cache.

Nesses termos, o curl é mais conveniente. Além do aplicativo de console curl.exe, ele fornece as bibliotecas: libcurl-x64.dll e libcurl-x64.lib. Portanto, nós podemos incluir o curl em nosso programa sem nenhum processo de desenvolvimento adicional e trabalhar com o buffer de memória em vez da criação de um arquivo separado com os resultados da operação do curl. O curl também está disponível em código-fonte, mas a criação de bibliotecas com base no código-fonte pode levar muito tempo. Portanto, o arquivo anexado inclui as bibliotecas criadas, dependências e todos os arquivos de inclusão necessários para a operação.

Criação da Biblioteca

Abrimos o Visual Studio (eu usei o Visual Studio 2017) e criamos uma dll simples. Vamos chamar o projeto de GetAndParse — a biblioteca resultante terá o mesmo nome. Criamos duas pastas no diretório do projeto: "lib" e "include". Essas duas pastas serão usadas para conectar as bibliotecas de terceiros. Copiamos a libcurl-x64.lib para a pasta 'lib' e criamos a pasta 'curl' dentro da pasta 'include'. Copiamos todos os arquivos de inclusão para a pasta 'curl'. Abrimos o menu: "Project -> GetAndParse Properties". Na parte esquerda da caixa de diálogo, abrimos "C/C++" e selecionamos "General". Na parte direita, selecionamos "Additional Include Directories", clicamos na seta para baixo e selecionamos "Edit". Na nova caixa de diálogo, nós clicamos no botão mais à esquerda na linha superior "New Line". Este comando adiciona uma linha editável na lista abaixo. Ao clicar no botão à direita, selecionamos a pasta recém-criada "include" e clicamos em "OK".

Expandimos o "Linker", selecionamos General e clicamos em "Additional Library Directories" à direita. Repetindo as mesmas ações, adicionamos a pasta "lib" que foi criada anteriormente.

Na mesma lista, nós selecionamos a linha "input" e clicamos em "Additional Dependencies" à direita. Adicionamos o nome "libcurl-x64.lib" na caixa superior.

Nós também precisamos adicionar a libcurl-x64.dll. Copiamos esse arquivo junto com as bibliotecas de suporte à criptografia para as pastas "debug" e "release".

O arquivo em anexo inclui todos os arquivos necessários, localizados em suas pastas correspondentes. As propriedades do projeto anexado também foram modificadas, portanto, você não precisará executar nenhuma ação adicional.

Classe para obter as páginas HTML

No projeto, foi criado a classe CCurlExec, que implementará a tarefa principal. Ele irá interagir com o curl, portanto, devemos incluí-la da seguinte maneira:

#include <curl\curl.h>

Isso pode ser feito no arquivo CCurlExec.cpp, mas eu preferi incluí-lo no stdafx.h

Definimos um "sinônimo" para o tipo da função de retorno de chamada, que é usada para salvar os dados recebidos:

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

Criamos as estruturas simples para salvar os dados recebidos na memória:

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

... e no arquivo:

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;

Eu acho que essas estruturas não precisam de explicação. A ferramenta deve ser capaz de armazenar as informações na memória. Para esse fim, nós fornecemos um buffer na estrutura MSTRUCT e seu tamanho.

Para armazenar as informações como um arquivo (nós implementaremos essa possibilidade no projeto, embora, no nosso caso, nós usaremos apenas o armazenamento em memória), nós adicionamos a função para obter o nome do arquivo à estrutura FSTRUCT. Para esse fim, nós usamos a API do Windows para trabalhar com os arquivos temporários.

Agora, nós criamos algumas funções de retorno de chamada para preencher as estruturas descritas. Método para preencher a estrutura do 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;
}

Nós não forneceremos aqui o segundo método para salvar os dados em um arquivo, que é semelhante ao primeiro. As assinaturas de função são obtidas da documentação no site do projeto curl.

Esses dois métodos serão usados como "funções padrão". Eles serão usados caso o desenvolvedor não forneça os seus próprios métodos para esses fins. 

A ideia desses métodos é muito simples. Para os parâmetros do método são passados o seguinte: informações sobre o tamanho dos dados recebidos, um ponteiro para a origem do buffer interno do curl e a estrutura MSTRUCT para o destinatário. Após algumas conversões preliminares, os campos da estrutura do destinatário são preenchidos.

E, finalmente, o método que executa as principais ações: ele recebe uma página HTML e preenche uma estrutura do tipo MSTRUCT usando os dados recebidos:

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); //redirects
#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 prestar atenção aos aspectos importantes do funcionamento de curl. Primeiramente, são executadas duas inicializações, como resultado o usuário recebe um ponteiro para o "núcleo" de curl e para o seu "identificador", que é usado para as chamadas seguintes. Uma conexão adicional está configurada, o que pode envolver muitas configurações. Nesse caso, nós determinamos o endereço de conexão, devemos verificar os certificados, especificamos o buffer no qual os erros serão gravados, determinamos a duração do tempo limite, o cabeçalho "user-agent", a necessidade de lidar com redirecionamentos, a função que será chamada para processar os dados recebidos (o método padrão descrito acima) e o objeto para armazenar os dados. Configurando a opção CURLOPT_VERBOSE, nos permite exibir informações detalhadas sobre as operações que estão sendo executadas, o que pode ser útil para fins de depuração. Depois que todas as opções são especificadas, a função curl curl_easy_perform é chamada. Ela executa a operação principal. Depois que os dados são limpos.

      Vamos adicionar mais um método geral:

      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;
      }
      
      

      Esse método permite que o desenvolvedor use uma função de retorno de chamada personalizada para processar os dados recebidos (o parâmetro pFunc) e um objeto personalizado para armazenar esses dados (o parâmetro pTarget). Assim, uma página HTML pode ser facilmente salva, por exemplo, como um arquivo csv.

      Vamos ver como as informações são salvas em um arquivo sem entrar em detalhes. A função de retorno de chamada apropriada foi mencionada anteriormente, juntamente com o objeto auxiliar FSTRUCT com um código para selecionar o nome do arquivo. No entanto, na maioria dos casos, o trabalho não termina aí. Para obter o nome do arquivo, ele pode ser definido com antecedência (nesse caso, você deve verificar se o arquivo com esse nome existe antes de escrever) ou se pode permitir que a biblioteca obtenha um nome de arquivo legível e significativo. Esse nome deve ser obtido do endereço real, no qual os dados foram lidos após o processamento dos redirecionamentos. O método a seguir mostra como um endereço real é obtido 

      bool CCurlExec::GetFiletoFile(const char * pUri)

      O código completo do método está disponível no arquivo em anexo. As ferramentas fornecidas no curl são usadas para realizar a análise sintática do endereço:

      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;
      }
      
      

      Você vê que o curl extrai corretamente o "PATH" do uri e verifica se o PATH termina com o caractere '/'. Se isso acontecer, o nome do arquivo deve ser "index.html". Caso contrário, o "PATH" é dividido em elementos separados, enquanto o nome do arquivo será o último elemento da lista resultante.

      Ambos os métodos acima são implementados no projeto, embora geralmente a tarefa de salvar os dados em um arquivo esteja além do escopo deste artigo.

      Além dos métodos descritos, a classe CCurlExec fornece dois métodos elementares para receber acesso ao buffer de memória, no qual os dados recebidos da rede foram salvos. Os dados podem ser apresentados como

      std::vector<char>
      ou da seguinte forma:
      std::string

      dependendo da seleção do parser html. Não há necessidade de estudar outros métodos e propriedades da classe CCurlExec, pois eles não se aplicam em nosso projeto.

      Para concluir este capítulo, eu gostaria de acrescentar algumas observações. A biblioteca curl é thread-safe. Nesse caso, ela é usada de forma síncrona, para a qual os métodos do tipo curl_easy_init são utilizados. Todas as funções curl contendo "easy" em seu nome são usadas apenas de forma síncrona. O uso assíncrono da biblioteca é fornecido através das funções que incluem "multi" em seus nomes. Por exemplo, curl_easy_init possui uma função análoga assíncrona curl_multi_init. O trabalho com funções assíncronas em curl não é muito complicado, mas envolve um longo código de chamada. Portanto, nós não consideraremos a operação assíncrona agora, mas nós podemos voltar a ela mais tarde.

      Classe para o Analisador Sintático HTML

      Vamos tentar encontrar um componente pronto para executar esta tarefa. Existem muitos componentes diferentes disponíveis. Ao selecionar um componente, nós usamos os mesmos critérios do capítulo anterior. Nesse caso, a opção preferida é o projeto Gumbo do Google. Ele está disponível no github. O link apropriado está disponível no arquivo do projeto anexado. Você pode compilar o projeto de forma independente, podendo ser mais fácil do que usar o curl. De qualquer forma, o projeto anexado inclui todos os arquivos necessários:

      Mais uma vez, abrimos o menu "Project -> GetAndParse Properties". Expandimos o "Linker", selecionamos "input" e, em seguida, selecionamos "Additional Dependencies" à direita. Adicionamos o nome "gumbo.lib" na caixa superior.

      Além disso, na pasta "include" criada anteriormente, nós criamos a pasta "gumbo" e copiamos todos os arquivos de inclusão para ela. Adicionamos sua entrada no arquivo de cabeçalho stdafx.h:

      #include <gumbo\gumbo.h>
      
      
      

      Duas palavras sobre o gumbo. Este é um analisador sintático html5 em C++. Vantagens:

      • Correspondência completa para a especificação HTML5.
      • Resistência à dados de entrada incorretos.
      • API simples, que pode ser chamada de outros linguagens.
      • Passa em todos os testes html5lib-0.95.
      • Testado em mais de dois bilhões e meio de páginas do índice do Google.

      Desvantagens:

      • O desempenho não é muito alto.

      O analisador apenas cria a árvore da página e não faz mais nada. Isso também pode ser tratado como uma desvantagem. O desenvolvedor pode trabalhar com a árvore usando os métodos preferidos. Existem recursos que fornecem wrappers para esse analisador, mas nós não os usaremos. Nosso objetivo não é "melhorar" o analisador, portanto, nós usaremos ele da maneira que ele está. Ele construirá uma árvore, na qual nós procuraremos os dados desejados. O trabalho com um componente é simples:

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

      Nós chamamos uma função, passando para ela um ponteiro para o buffer com os dados HTML de origem. A função cria um analisador com o qual o desenvolvedor trabalha. O desenvolvedor chama a função e libera os recursos.

      Vamos prosseguir com esta tarefa e começar examinando o código html da página desejada. O objetivo é óbvio - nós precisamos entender o que procurar e onde os dados necessários estão localizados. Abra o link _https://www.mataf.net/en/forex/tools/volatility e veja o código-fonte da página. Os dados de volatilidade estão contidos na tabela <table id="volTable" ... Esses dados são suficientes para encontrar a tabela na árvore. Obviamente, nós precisamos receber a volatilidade para um par de moedas específico. Os atributos das linhas da tabela contêm os nomes dos símbolos do par de moeda: <tr id="row_AUDCHF" class="data_volat" name="AUDCHF"... Usando esses dados, a linha desejada pode ser facilmente encontrada. Cada linha consiste em cinco colunas. Nós não precisamos das duas primeiras colunas, já que as outras três contêm os dados necessários. Vamos escolher uma coluna, receber os dados de texto, convertê-los no tipo double e retornar ao cliente. Para tornar o processo mais claro, vamos dividir a pesquisa de dados em três estágios:

      1. Encontrar a tabela pelo seu identificador ("volTable").
      2. Encontrar a linha usando o seu identificador ("row_Currency Pair Name").
      3. Encontrar o valor da volatilidade em uma das três últimas colunas e retornar o valor encontrado.
      Vamos começar a escrever o código. Criamos a classe CVolatility no projeto. A biblioteca do analisador já foi conectada, portanto, nenhuma ação adicional é necessária aqui. Como você se lembra, a volatilidade na tabela desejada foi mostrada em três colunas, de três maneiras diferentes. Portanto, vamos criar a enumeração apropriada para selecionar uma delas:
      typedef enum {
              byPips = 2,
              byCurr = 3,
              byPerc = 4
      } VOLTYPE;
      
      

      Penso que esta parte está clara e não precisa de nenhuma explicação adicional. Ele é implementado como a seleção do número da coluna.

      Em seguida, criamos um método que retorna o valor da volatilidade:

      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)
      
      
      

      Chamamos o método com os seguintes argumentos:

      • szHtml — referência a um buffer com os dados recebidos no formato html.
      • pair — nome do par de moedas na qual se busca pela volatilidade
      • vtype — tipo da volatilidade, o número da coluna da tabela

      O método retorna o valor da volatilidade ou -1 em caso de erro.

      Assim, a operação começa com a busca da tabela, desde o início da árvore. Esta busca é implementada pelo seguinte método fechado:

      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)
      
      

      O método é chamado recursivamente até que um elemento que atenda aos dois requisitos a seguir seja encontrado:

      1. Isso deve ser uma tabela.
      2. Seu "id" deve ser "volTable".
      Se esse elemento não for encontrado, o método retornará -1. Caso contrário, o método retornará o valor, que retornará um método semelhante que busca por uma linha da tabela:
      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)
      
      
      
      Após encontrar uma linha da tabela com o id = "row_PairName", a busca é concluída chamando-se o método, que lê o valor da célula em uma determinada coluna da linha 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;
      }
      
      
      

      Observe que a vírgula é usada como um separador de dados na tabela, em vez do ponto. Portanto, o código possui algumas linhas para resolver esse problema. Semelhante aos casos anteriores, o método retorna -1 em caso de erro ou o valor da volatilidade em caso de sucesso.

      No entanto, essa abordagem tem uma desvantagem. O código ainda está fortemente vinculado aos dados, que o usuário não pode afetar, embora o analisador libere essa dependência até certo ponto. Portanto, se o design do site mudar significativamente, o desenvolvedor precisará reescrever toda a parte relacionada à busca em árvore. De qualquer forma, o procedimento de busca é simples e as diversas funções relacionadas podem ser facilmente editadas.

      Outros membros da classe CVolatility estão disponíveis no arquivo em anexo. Nós não consideraremos neste artigo.

      Juntando tudo

      O código principal está pronto. Agora, nós precisamos juntar tudo e projetar uma função, que criará objetos e executará chamadas na sequência adequada. O seguinte código é inserido no arquivo de cabeçalho 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);
      
      

      Ele já contém uma definição de macro, que nós modificamos levemente para ativar essa chamada de função pelo mql. Veja a explicação do por que isso é feito aqui.

      O código da função está escrito no arquivo 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);
      }
      
      
      

      É uma boa ideia colocar o endereço da página no código? Por que ele não pode ser implementado como argumento da chamada de função GetVolatility? Não faz sentido, porque o algoritmo de busca de informações retornado pelo analisador é forçado a vincular aos elementos da página HTML. Portanto, ela é uma biblioteca específica para determinado endereço. Este método nem sempre deve ser usado, é uma aplicação apropriada no nosso caso.

      Compilação e Instalação da Biblioteca

      Criamos a biblioteca de maneira usual. Obtemos todas as DLLs da pasta Release, incluindo: GETANDPARSE.dll, gumbo.dll, libcrypto-1_1-x64.dll, libcurl-x64.dll e libssl-1_1-x64.dll e copiamos ela na pasta 'Libraries' do terminal. Assim, a biblioteca foi instalada.

      Script Tutorial de Uso da Biblioteca

      Este é um script simples:

      #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);
        }
      
      

      O script parece não precisar de mais explicações. O código do script se encontra em anexo abaixo.

      Conclusão

      Nós discutimos um método para analisar a página HTML de forma simplificada. A biblioteca é feita de componentes prontos. O código foi bastante simplificado para ajudar os iniciantes a entender a ideia. A principal desvantagem desta solução é a execução síncrona. O script não assumirá o controle até que a biblioteca receba a página HTML e a processe. Isso pode levar tempo, o que é inaceitável para indicadores e Expert Advisors. Outra abordagem é necessária para uso em tais aplicações. Nós vamos tentar encontrar melhores soluções em outros artigos.


      Programas utilizados no artigo

       # Nome
      Tipo
       Descrição
      1 GetVolat.mq5
      Script
      Script que recebe os dados de volatilidade.
      2
      GetAndParse.zip Arquivo
      Código fonte da biblioteca e do aplicativo de console de teste