繁簡切換您正在訪問的是FX168財經網,本網站所提供的內容及信息均遵守中華人民共和國香港特別行政區當地法律法規。

FX168財經網>人物頻道>帖子

利用 curl 解析 HTML

作者/我是一個土豆 2019-11-13 17:00 0 來源: FX168財經網人物頻道

概述

我們討論這樣一種情況,即當使用常規請求無法從某個網站獲得數據時。 在這種情況下能夠做什麼呢? 第一個可能的想法是尋找可以使用 GET 或 POST 請求進行訪問的資源。 有時,這樣的資源根本不存在。 例如,這可能涉及某些獨有指標的操作,或訪問一些罕有更新的統計信息。

也許有人會問:“這有什麼意義?”,一個簡單的解決方案是直接從 MQL 腳本訪問網站頁面,並在已知頁面位置讀取已知數量的持倉信息。 然後,可以進一步處理接收到的字符串。 這是可能的方法之一。 但是在這種情況下,MQL 腳本代會與特定頁面的 HTML 代碼緊密綁定。 若是 HTML 代碼變化了怎麼辦? 這就是為什麼我們需要一個解析器來,可將 HTML 文檔整理為類似樹形的操作(詳細信息將在單獨的章節中進行討論)。 如果我們以 MQL 實現解析器,那麼就性能而言是否更佳便捷、高效? 這樣的代碼能夠維護得當嗎? 這就是為什麼解析功能應放在單獨的函數庫中來實現的原因。 不過,解析器無法解決所有問題。 它會執行所期望的功能。 但如果網站設計發生重大變化,並換用了其他類名和屬性該怎麼辦? 在這種情況下,我們將需要修改搜索對象或事件的多個對象。 所以,我們的目標之一是盡快以最小的工作量創建必要的代碼。 如果我們有現成的部件可用則更好。 在上述狀況下,這可令開發人員輕松維護代碼,並快速對其進行編輯。

我們將選擇一個頁面不太大的網站,並嘗試從該網站獲取感興趣的數據。 在這種情況下,數據的類型並不重要,盡管如此,我們還是來嘗試創建一個有用的工具。 當然,該數據必須可用終端的 MQL 腳本進行處理。 程序代碼將創建為標準 DLL。

在本文中,我們將實現的工具不支持異步調用和多線程。

現有解決方案

對於開發人員而言,從互聯網上獲取數據並進行處理的可能性一直很具有吸引力。 mql5.com 網站上有幾篇文章,闡述了一些有趣且多樣化的方法:

  • MQL5-RPC, 從 MQL5 遠程過程調用。 這篇文章提供了有關如何運用 XML-RPC 技術訪問資源,並從中接收數據的詳細分析。 該請求直接從 MQL 腳本發送。
  • 使用 CSS 選擇器從 HTML 頁面提取結構化數據。 該示例展示如何利用 MQL 獲取數據,並創建 HTML 代碼解析器。
  • 自己動手開發多線程異步 MQL5 WebRequest。 這篇文章提供了創建異步請求的有趣版本,且無需運用 MQL 手段進一步分析。
我強烈建議您閱讀這些文章。

    定義任務

    我們將針對以下網站進行實驗: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 更容易。 無論如何,隨附的項目包含所有必需的文件:

        • gumbo.lib 在 lib 項目文件夾中可用
        • gumbo.dll 包含在 debug 和 release 文件夾中

        再次打開菜單“項目 -> 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 存檔
        函數庫和控制台應用程序測試的源代碼


        分享到:
        舉報財經168客戶端下載

        全部回複

        0/140

        投稿 您想發表你的觀點和看法?

        更多人氣分析師

        • 張亦巧

          人氣2208文章4145粉絲45

          暫無個人簡介信息

        • 張迎妤

          人氣1904文章3305粉絲34

          個人專注於行情技術分析,消息面解讀剖析,給予您第一時間方向...

        • 指導老師

          人氣1864文章4423粉絲52

          暫無個人簡介信息

        • 李冉晴

          人氣2320文章3821粉絲34

          李冉晴,專業現貸實盤分析師。

        • 梁孟梵

          人氣2184文章3177粉絲39

          qq:2294906466 了解群指導添加微信mfmacd

        • 王啟蒙現貨黃金

          人氣312文章3409粉絲8

          本人做分析師以來,並專注於貴金屬投資市場,尤其是在現貨黃金...

        • 金泰鉻J

          人氣2328文章3925粉絲51

          投資問答解咨詢金泰鉻V/信tgtg67即可獲取每日的實時資訊、行情...

        • 金算盤

          人氣2696文章7761粉絲125

          高級分析師,混過名校,廝殺於股市和期貨、證券市場多年,專注...

        • 金帝財神

          人氣4760文章8329粉絲119

          本文由資深分析師金帝財神微信:934295330,指導黃金,白銀,...

        FX168財經

        FX168財經學院

        FX168財經

        FX168北美