利用 curl 解析 HTML

Andrei Novichkov | 13 十一月, 2019

概述

我们讨论这样一种情况,即当使用常规请求无法从某个网站获得数据时。 在这种情况下能够做什么呢? 第一个可能的想法是寻找可以使用 GET 或 POST 请求进行访问的资源。 有时,这样的资源根本不存在。 例如,这可能涉及某些独有指标的操作,或访问一些罕有更新的统计信息。

也许有人会问:“这有什么意义?”,一个简单的解决方案是直接从 MQL 脚本访问网站页面,并在已知页面位置读取已知数量的持仓信息。 然后,可以进一步处理接收到的字符串。 这是可能的方法之一。 但是在这种情况下,MQL 脚本代会与特定页面的 HTML 代码紧密绑定。 若是 HTML 代码变化了怎么办? 这就是为什么我们需要一个解析器来,可将 HTML 文档整理为类似树形的操作(详细信息将在单独的章节中进行讨论)。 如果我们以 MQL 实现解析器,那么就性能而言是否更佳便捷、高效? 这样的代码能够维护得当吗? 这就是为什么解析功能应放在单独的函数库中来实现的原因。 不过,解析器无法解决所有问题。 它会执行所期望的功能。 但如果网站设计发生重大变化,并换用了其他类名和属性该怎么办? 在这种情况下,我们将需要修改搜索对象或事件的多个对象。 所以,我们的目标之一是尽快以最小的工作量创建必要的代码。 如果我们有现成的部件可用则更好。 在上述状况下,这可令开发人员轻松维护代码,并快速对其进行编辑。

我们将选择一个页面不太大的网站,并尝试从该网站获取感兴趣的数据。 在这种情况下,数据的类型并不重要,尽管如此,我们还是来尝试创建一个有用的工具。 当然,该数据必须可用终端的 MQL 脚本进行处理。 程序代码将创建为标准 DLL。

在本文中,我们将实现的工具不支持异步调用和多线程。

现有解决方案

对于开发人员而言,从互联网上获取数据并进行处理的可能性一直很具有吸引力。 mql5.com 网站上有几篇文章,阐述了一些有趣且多样化的方法:

我强烈建议您阅读这些文章。

定义任务

我们将针对以下网站进行实验:https://www.mataf.net/en/forex/tools/volatility。 顾名思义,该站点提供有关货币对波动率的数据。 波动率以三种不同单位显示:点数,美元和百分比。 该网站页面不太庞大,因此可以有效地接收并解析,从中获得所需的数值。 对源文本的初步研究表明,我们必须从单独的表格单元中获取所存储的数值。 因此,我们将主要任务分为两个子任务:

  1. 获取和存储页面。
  2. 解析获取的页面,接收文档结构,并依据此结构搜索所需的信息。 数据处理并传递给客户端。

我们从实现第一部分开始。 我们需要将获取的页面另存为文件吗? 在实际工作的版本中,很明显,不需要保存页面。 我们需要一个可调整的缓存,该缓存将每隔一定时间进行更新。 在特殊情况下,该可禁用缓存。 例如,如果一款 MQL 指标在每次即时报价时将查询发送到源页面,则该网站很可能会将该指标封禁。 如果数据请求脚本针对多个货币对运行,则封禁会更迅速地降临。 在任何情况下,正确设计的工具都不会过于频繁地发送请求。 代之,它只发送一次请求,将结果保存到文件,稍后再从文件中请求数据。 直至缓存有效性到期后,会发新请求更新文件。 这可避免过于频繁的发请求。

在我们的情况下,我们不会创建缓存。 只是发送若干个练手请求,我们不会影响该站点的运行。 相反,我们可以专注于更重要的方面。 有关保存文件到磁盘会提供更深入的注释,但在这种情况下,数据将保存在内存之中,并被传递到第二个程序模块,即解析器。 只要适用,我们都会使用简化的代码,对于初学者这样可以容易理解,且能够揭示主体思路的实质。

获取第三方网站的 HTML 页面

如前所述,思路之一是利用现有的即用型控件,和现成的函数库。 然而,我们仍然需要确保整个系统的可靠性和安全性。 控件的选择将根据其信誉。 为了获得所需的页面,我们将使用众所周知的开源项目 curl

该项目可接收和发送几乎任意来源的文件:http,https,ftp 服务器和许多其他形式。 它支持服务器上进行认证所需的登录名和密码设置,以及重定向和超时处理。 该项目提供了全面的文档,详述了该项目的所有功能。 甚或,这是一个跨平台的开源项目,从而成为一个绝对优势。 还有另一个项目可以实现相同的功能。 它就是 “wget” 项目。 在这种情况下,出于以下两个原因依然选择使用 curl:

  • curl 可以接收和发送文件,而 wget 仅接收文件。
  • wget 仅可用于 wget.exe 控制台应用程序。

wget 能否发送文件与我们的任务无关,因为我们只需要接收一个 HTML 页面。 不过,如果我们掌握了 curl,稍后我们能够将其用于其他任务,而那时可能需要发送数据。

一个更严重的缺点与以下事实有关,即它只能作为 wget.exe 实用程序调用,从而没有提供任何诸如 wget.dll,wget.lib 这样的函数库。

  • 在这种情况下,为了从 MetaTrader 关联的 dll 中使用 wget,我们需要创建一个单独的进程,这样做很费时、费力。
  • 通过 wget 获得的数据只能作为文件传递,这对我们来说很麻烦,原因在于我们决定将来用缓存替代。

出于这些条款,curl 更方便。 除了控制台应用程序 curl.exe 之外,它还提供了函数库:libcurl-x64.dll 和 libcurl-x64.lib。 因此,我们只要将 curl 包含到程序当中,且无需任何其他开发过程,即可使用内存缓冲区,更不用为 curl 的操作结果单独创建文件。 Curl 也可以作为源代码嵌入,但是基于源代码创建函数库可能很耗时。 所以,随附的存档文件包含已创建好的函数库,所有依赖资源,以及包含所有操作所需的文件。

创建一个函数库

打开 Visual Studio(我的版本是 Visual Studio 2017)并创建一个简单的 dll。 我们将项目称为 GetAndParse — 结果函数库将具有相同的名称。 在项目文件夹中创建两个文件夹:“ lib” 和 “include”。 这两个文件夹将用于连接第三方函数库。 将 libcurl-x64.lib 复制到 “lib” 文件夹,并在 “include” 文件夹下创建 “curl” 文件夹。 将所有包含文件复制到 “curl”。 打开菜单:“项目 -> GetAndParse 属性”。 在对话框的左侧,打开 “C/C++”,然后选择“常规”。 在右侧,选择“其他包含目录”,单击向下箭头,然后选择“编辑”。 在新对话框中,打开上一行“新建行”中最左边的按钮。 此命令在下面的列表中添加可编辑的行。 单击右侧的按钮,选择新创建的 “include” 文件夹,然后单击“确定”。

展开“链接器”,选择“常规”,然后单击右侧的“其他函数库目录”。 按照相同的操作重复,添加创建的 “lib” 文件夹。

从同一列表中,选择“输入”行,然后单击右侧的“其他依赖项”。 在上方的框中添加 “libcurl-x64.lib” 名称。

我们还需要添加 libcurl-x64.dll。 将此文件以及加密支持函数库复制到 “debug” 和 “release” 文件夹中。

随附的存档文件包含所要的文件,这些文件位于相应的文件夹中。 随附的项目属性也已修改,因此您无需执行任何其他操作。

获取 HTML 页面的类

在项目中,创建 CCurlExec 类,该类将实现主要任务。 它将与 curl 交互,所以按以下方式进行连接:

#include <curl\curl.h>

可以在 CCurlExec.cpp 文件中完成此操作,但我更喜欢将其包含在 stdafx.h 之中

为回调函数定义类型别名,该别名用于保存接收到的数据:

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

创建简单的结构以便将接收到的数据保存在内存中:

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

... 以及文件中:

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;

我认为这些结构无需解释。 该工具应该能够在内存中存储信息。 为此目的,我们在 MSTRUCT 结构中提供了一个缓冲区。

若要将信息存储为文件(我们将在项目中实现这种可能性,尽管目前情况下,我们仅使用存储在内存中),则将文件名获取函数添加到 FSTRUCT 结构中。 为此目的,请使用 Windows API 处理临时文件。

现在创建几个回调函数来填充所描述的结构。 填充 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;
}

在此,我们不会提供第二种将数据保存到文件中的方法,因其方法类似于第一种方法。 函数签名取自 curl 项目网站上的文档。

这两种方法将用作“默认函数”。 如果开发人员未提供自己的方法,则会使用它们。 

这些方法的思路非常简单。 在方法参数中传递以下内容:有关接收到的数据大小的信息,指向源(即内部 curl 缓冲区)的指针,以及接收者(MSTRUCT 结构)。初步进行一些转换后,填充到收件人结构字段。

最后,执行主要动作的方法:它接收 HTML 页面,并利用接收到的数据填充 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); //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;
}

      注意 curl 操作的重要方面。 首先,执行两个初始化,结果就是用户收到指向 curl “核心”以及指向“句柄”的指针,这些指针会在稍后的调用中用到。 配置进一步的连接,这可能涉及很多设定。 在这种情况下,我们判断连接地址,需要检查证书,指定出错信息的写入缓冲区,判断超时区间,“user-agent” 标头,需要处理重定向,指定处理接收数据所要调用的函数(上述默认方法),和存储数据的对象。 设置 CURLOPT_VERBOSE 选项启用显示有关正在执行的操作的详细信息,这对于调试很有用。 指定所有选项后,将调用 curl 函数 curl_easy_perform。 它执行主要操作。 之后,数据将被清除。

      我们来添加另一种通用方法:

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

      此方法令开发人员可以使用自定义回调函数来处理接收到的数据(pFunc 参数),并使用自定义对象来存储此数据(pTarget 参数)。 因此,HTML 页面可以轻松保存为任何格式,如 csv 文件。

      我们看看如何无需深入详节即可将信息保存到文件中。 前面已经提到了相应的回调函数,以及可用于选择文件名的辅助对象 FSTRUCT。 然而,在大多数情况下,操作到此还没有结束。 若要获取文件名,可以预先设置文件名(在这种情况下,应预先检查该名称的文件是否存在),或者可以允许函数库获取可读且有意义的文件名。 该名称应从实际地址获得,即在处理重定向后读取数据的地方。 以下方法展示了如何获取实际地址 

      bool CCurlExec::GetFiletoFile(const char * pUri)

      完整的方法代码已在存档中提供。 “ curl” 中提供的解析地址的工具:

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

      如您所见,curl 正确地从 uri 中提取到 “PATH”,并检查 PATH 是否以 '/' 字符结尾。 如果是这样,文件名应为 “index.html”。 否则,“PATH” 将被拆分为单独的元素,而文件名将是结果列表中的最后一个元素。

      以上两种方法都是在项目中实现的,尽管通常将数据保存到文件的任务不在本文的讨论范围之内。

      除了所描述的方法之外,CCurlExec 类还提供了两种基本方法,用来访问内存缓冲区,从网络接收的数据会保存到该缓冲区当中。 数据可以表示为

      std::vector<char>
      或以下形式:
      std::string

      这要取决于 html 解析器的进一步选择。 在此无需研究 CCurlExec 类的其他方法和属性,因为在我们的项目里不会用到它们。

      在结束本章之前,我想再补充一点。 curl 函数库是线程安全的。 在本示例中它以同步使用,为此使用 curl_easy_init 类型的方法。 名称中带有 “easy” 的所有 curl 函数只能用于同步。 而函数库里提供的异步调用方法,其名称里包含 “multi”。 例如,curl_easy_init 具有异步模拟函数 curl_multi_init。 在 curl 中调用异步函数并不是很复杂,但是它涉及到冗长的调用代码。 所以,我们现在不会考虑异步操作,但稍后我们可能会再次讨论。

      HTML 解析类

      我们尝试寻找现成的控件来执行此任务。 有许多不同的控件可用。 选择控件时,请使用与上一章相同的准则。 在本例中,首选项是 Google 的 Gumbo 项目。 可以在 github 上找到它。 随附的项目存档中提供了相应的链接。 您可以自行编译项目,这可能比使用 curl 更容易。 无论如何,随附的项目包含所有必需的文件:

      再次打开菜单“项目 -> GetAndParse 属性”。 展开“链接器”,选择“输入”,然后在右侧选择“其他依赖项”。 在上方的框中添加 “gumbo.lib” 名称。

      此外,在之前创建的 “include” 文件夹中,创建 “gumbo” 文件夹,并将所有包含文件复制到该文件夹。 在 stdafx.h 文件中输入以下内容:

      #include <gumbo\gumbo.h>
      
      
      

      有关 gumbo 的两个关键词。 这是 C++ 中的 html5 代码解析器。 优点:

      • 完全符合 HTML5 规范。
      • 阻拦错误的输入数据。
      • 简单的 API,可以从其他语言中调用。
      • 已通过所有 html5lib-0.95 测试。
      • 测试基于来自 Google 检索到的超过二十五亿个页面。

      缺点:

      • 性能不高。

      解析器仅构建页面树,此外不执行其他任何操作。 这也可以视为一个缺点。 然后,开发人员可以使用首选方法处理该树。 有一些资源可以为该解析器提供包装器,但我们不会使用它们。 我们的目标不是“改进”解析器,因此我们将按原样使用它。 它将构建一棵树,在其中搜索所需的数据。 使用控件很简单:

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

      我们调用一个函数,将指向源 HTML 数据的缓冲区的指针传递给该函数。 该函数创建一个符合开发人员操作的解析器。 开发人员调用该函数并释放资源。

      我们继续执行此任务,并开始检验所需页面的 html 代码。 目的很明显 - 我们需要了解所要查找的内容,以及所需数据的位置。 打开链接 _https://www.mataf.net/en/forex/tools/volatility,然后查看页面的源代码。 波动率数据包含在表格标签 <table id="volTable" ... 中 此数据足以在树中找到表格。 显然,我们需要接收特定货币对的波动率。 表格行的属性包含货币符号名称:<tr id="row_AUDCHF" class="data_volat" name="AUDCHF"... 使用此数据,可以轻松找到所需的行。 每行由五列组成。 我们不需要前两列,而其他三列则包含我们所需的数据。 我们选择一列,接收文本数据,将其转换为双精度值,并返回给客户端。 为了令过程更清晰,我们将数据搜索分为三个阶段:

      1. 按其标识符(“ volTable”)搜索表格。
      2. 按其标识符(“ row_货币对名称”)搜索行。
      3. 在最后三列之一中搜索波动率数值,然后返回找到的数值。
      我们开始编写代码。 在项目中创建 CVolatility 类。 已连接解析器的函数库,所以此处无需执行其他操作。 如您所记,波动率以三种不同的形式显示在所需表格的三列中。 因此,为了选择其中之一,我们要创建相应的枚举:
      typedef enum {
              byPips = 2,
              byCurr = 3,
              byPerc = 4
      } VOLTYPE;
      
      

      我认为这部分很清楚,无需任何多余解释。 它实现了按照列号进行选择。

      接下来,创建一个返回波动率值的方法:

      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)
      
      
      

      按照以下参数调用该方法:

      • szHtml — 接收 html 格式数据的缓冲区的引用。
      • pair — 搜索波动率的货币对名称
      • vtype — 波动率类型,表格列号

      该方法返回波动率数值,如果有错误,则返回 -1。

      因此,该操作从树的最开始搜索表格。 搜索是按照以下封闭方法实现的:

      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)
      
      

      递归调用该方法,直到找到满足以下两个要求的元素:

      1. 这必须是一个表格。
      2. 其 “id” 必须为 “volTable”。
      如果未找到这样的元素,则该方法将返回 -1。 否则,该方法将返回数值,这与返回搜索表格行的方法类似:
      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)
      
      
      
      一旦找到 id = "row_PairName" 的表格行,就可以调用该方法来完成搜索,该方法从所发现行的特定列中的单元格里读取数值:
      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;
      }
      
      
      

      请注意,逗号在表格里是分隔符,代替句点。 因此,有几行代码可以解决此问题。 与以前的情况类似,如果发生错误,该方法将返回 -1;如果成功,则该方法将返回波动率值。

      然而,这种方法有一个缺点。 代码仍然与数据紧密绑定,而用户对此无法产生影响,尽管解析器在某种程度上释放了这种依赖性。 因此,如果网站设计发生重大变化,则开发人员将不得不重写与树形搜索相关的整个部分。 无论如何,搜索过程很简单,并且相关的几个函数也能轻松地进行编辑。

      其他 CVolatility 类成员在随附的存档中提供。 我们不会在本文中考虑它们。

      合并组合

      主体代码已准备就绪。 现在,我们需要将所有内容统合,并设计一个函数,该函数将按相应的顺序创建对象,并执行调用。 将以下代码插入到 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);
      
      

      它已经包含了宏定义,我们对其进行了一些修改,以便启用由 mql 调用此函数。 为何执行此操作的解释请参阅链接

      函数代码在 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);
      }
      
      
      

      页面地址写为硬编码是一个好主意吗? 为什么不能将其作为 GetVolatility 函数调用的参数实现? 这没有任何意义,因为解析器返回的信息搜索算法与 HTML 页面元素强制绑定。 因此,它是一个特定地址的函数库。 不应始终使用此方法,但我们的例子里,则是恰当的用法。

      函数库的编译和安装

      按往常方式构建函数库。 从终端的 Release 文件夹中获取所有 dll,包括:GETANDPARSE.dll,gumbo.dll,libcrypto-1_1-x64.dll,libcurl-x64.dll 和 libssl-1_1-x64.dll,并将它们复制到 “Libraries” 文件夹中。 如此,函数库即已安装完毕。

      函数库用法教程脚本

      这是一个简单的脚本:

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

      该脚本似乎不需要进一步说明。 脚本代码已附带于后。

      结束语

      我们讨论了一种以最简化的形式解析页面 HTML 的方法。 该函数库由现成的控件构成。 该代码已大大简化,以便帮助初学者理解该思路。 该解决方案的主要缺点是同步执行。 直至函数库接收到 HTML 页面,并对其进行处理,否则脚本无法受控。 这也许要花费一些时间,而这种情况对于指标和智能交易系统是无法接受的。 在此类应用程序中需要运用另一种方式。 我们将在后续文章中尝试找到更好的解决方案。


      本文中用到的程序

       # 名称
      类型
       说明
      1 GetVolat.mq5
      脚本
      接收波动率数据的脚本。
      2
      GetAndParse.zip 存档
      函数库和控制台应用程序测试的源代码