CURLを使用した HTML の解析

Andrei Novichkov | 22 1月, 2020

イントロダクション

通常のリクエストではウェブサイトからのデータが得られない場合について議論しましょう。 この場合、何ができるでしょうか。 最初に考えられる考え方は、GET リクエストまたは POST リクエストを使用してアクセスできるリソースを見つけることです。 ただし、このようなリソースが存在しない場合があります。 たとえば、ユニークなインジケータの動作や、めったに更新されない統計へのアクセスに関係する場合があります。

"要点は何でしょうか。" 簡単な解決策は、MQLスクリプトから直接サイトページにアクセスし、既知のページポジションにある既知の位置を読み取ることです。 その後、受信した文字列をさらに処理できます。 可能性のある方法の 1 つです。 しかし、この場合、MQL スクリプトコードはページのHTMLコードに密接にバインドされます。 HTML コードが変更された場合はどうなるでしょうか。 そのため、HTML ドキュメントを使用してツリーのような操作を可能にするパーサーが必要です (詳細については別のセクションで説明します)。 MQLでパーサーを実装する場合、パフォーマンスの面で便利で効率的でしょうか? このようなコードを適切に保守できるでしょうか。 そのため、解析関数は別のライブラリで実装します。 ただし、パーサーはすべての問題を解決するわけではありません。 必要な機能を実行します。 しかし、サイトのデザインが根本的に変更され、他のクラス名と属性を使用する場合はどうでしょうか。 この場合、検索オブジェクトまたはイベント複数のオブジェクトを変更する必要があります。 したがって、目標の1つは、できるだけ早く、最小限の労力で必要なコードを作成することです。 既製のパーツを使えば良くなります。 これより、デベロッパはコードを保持し、上記の状況に応じてすばやく編集できます。

ページが大きすぎないウェブサイトを選定し、該当サイトから興味深いデータを取得しましょう。 この場合、データの種類は重要ではありませんが、便利なツールを作成してみましょう。 もちろん、このデータはターミナルの MQL スクリプトで使用できる必要があります。 プログラム コードは標準DLLとして作成されます。

この記事では、非同期呼び出しとマルチスレッドを使用せずにツールを実装します。

既存のソリューション

インターネットからデータを取得し、処理する可能性は、デベロッパにとって常に興味深いものでした。 mql5.comのウェブサイトには、興味深く多様なアプローチを説明する記事があります。

これらの記事を読んでおくことをお勧めします。

タスクの定義

まず次のサイトを試します。https://www.mataf.net/en/forex/tools/volatility. 名前から明らかなように、サイトは通貨ペアのボラティリティに関するデータを備えています。 このボラティリティは、pips、米ドル、パーセントの3つの異なる単位で表示されます。 Web サイトページはあまりにもかさばらないので、効率的に受け入れることができ、必要な値を取得できます。 ソーステキストの予備的な研究は、別々のテーブルセルに格納された値を取得する必要があることを示します。 したがって、メイン タスクを 2 つのサブタスクに分割してみましょう。

  1. ページの取得と保存。
  2. 取得したページを解析してドキュメント構造を受け取り、この構造体で必要な情報を詳しく見ることができます。 データ処理とクライアントへの受け渡し。

最初の部分の実装から始めましょう。 取得したページをファイルとして保存する必要があるでしょうか。 実際に動作しているバージョンでは、ページを保存する必要がないことは明らかです。 一定の間隔で更新される調整可能なキャッシュが必要です。 特殊な場合には、キャッシュの使用を無効にする可能性があります。 たとえば、MQLインジケータがティックのたびにソースページにクエリを送信する場合、このインジケータはこのサイトからバンされる可能性があります。 スクリプトをリクエストするデータが複数の通貨ペアで実行されている場合、アクセス禁止はさらに迅速に行われます。 いずれにせよ、正しく設計されたツールはリクエストをあまり頻繁に送信しません。 代わりに、リクエストが 1 回送信され、結果がファイルに保存され、後でファイルからデータがリクエストされます。 キャッシュの有効性が満了すると、新しいリクエストを使用してファイルが更新されます。 これにより、リクエストの頻度が低くなります。

この例では、このキャッシュは作成しません。 複数のトレーニングリクエストを送信することで、サイト運営に影響を与えなくなります。 代わりに、より重要なポイントに焦点を当てることができます。 ディスクへのファイルの保存に関する追加のコメントが提供されますが、この場合、データはメモリに保存され、2 番目のプログラム ブロック、すなわちパーサーに渡されます。 初心者にも理解できるように、まだ主なアイデアの本質を反映して、適用可能な場合はいつでも簡略化されたコードを使用します。

サードパーティサイトの HTML ページを取得する

既に説明したように、このアイデアの 1 つは、既存のすぐに使用できるコンポーネントと既製のライブラリを利用することです。 ただし、システム全体の信頼性とセキュリティを確保する必要があります。 コンポーネントは、その評判に基づいて選択します。 目的のページを取得するには、よく知られているプロジェクト curl を使用します。

このプロジェクトでは、http、https、fTPサーバーなど、ほぼすべてのソースに対してファイルの送受信を行えます。 サーバー上の承認のログインとパスワードの設定、リダイレクトとタイムアウトの処理をサポートします。 このプロジェクトには、プロジェクトのすべての関数を説明する包括的なドキュメントが用意されています。 さらに、オープンソースのクロスプラットフォームプロジェクトであり、間違いなく有利です。 同じ関数を実装できる別のプロジェクトがあります。 'wget'プロジェクトです。 ただし、この場合、CURLを次の 2 つの理由で使用します。

  • curl はファイルを送受信できますが、wget は受信したファイルのみを受信できます。
  • wget は wget.exe コンソール アプリケーションとしてのみ使用できます。

Wget でファイルを送信できないことは、HTML ページを受信するだけで済むため、タスクには関係ありません。 しかし、CURLの場合は、後で他のタスクに流用することができ、データ送信が必要になる場合があります。

より深刻な欠点は、wget.dll、wget.libのようなライブラリなしでwget.exeユーティリティとしてのみ利用可能であるという事実です。

  • この場合、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" の 2 つのフォルダを作成します。 2 つのフォルダは、サードパーティのライブラリの接続に使用します。 libcurl-x64.lib を 'lib' フォルダにコピーし、'include' フォルダの下に 'curl' フォルダを作成します。 すべてのインクルード ファイルを 'curl' にコピーします。 メニューを開きます: "Project -> GetAndParse プロパティ"。 ダイアログ ボックスの左側で "C/C++" を開き、"General" を選択します。 右側の部分で"追加インクルードディレクトリ"を選択し、下矢印をクリックして"Edit"を選択します。 新しいダイアログ ボックスで、上の行 "New Line" の左端のボタンを開きます。 このコマンドは、次の一覧に編集可能な行を追加します。 右側のボタンをクリックして、新しく作成した "include" フォルダを選択し、"OK" をクリックします。

"リンカー" のラップを解除し、[全般] を選択して、右側の "追加のライブラリ ディレクトリ" をクリックします。 同じ操作を繰り返すことで、作成した "lib" フォルダを追加します。

同じ一覧から "input" 行を選択し、右側の "追加の依存関係" をクリックします。 上のボックスに "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;

... and in a file:

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

ここでは、最初のメソッドと同様に、ファイルにデータを保存する 2 番目のメソッドは提供しません。 関数シグネチャは、curl プロジェクト Web サイトのドキュメントから取得されます。

2 つのメソッドは "デフォルトの関数" として使用します。 デベロッパが独自のメソッドを提供しない場合に使用します。 

このメソッドの考え方は簡単です。 メソッド パラメータには、受信データ サイズに関する情報、ソースへのポインタ、内部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操作の重要な側面に注意を払いましょう。 まず、2 つの初期化が実行され、その結果、ユーザーはCURL "core" とその "handle" へのポインタを受け取り、以降の呼び出しで使用します。 さらに接続が構成され、多くの設定が必要になる場合があります。 この場合、接続アドレスの決定、証明書の確認、エラーの書き込み先のバッファの指定、タイムアウト期間の決定、"user-agent" ヘッダ、リダイレクトの処理の必要性、呼び出される関数の指定を使用して、受信したデータ (上記のデフォルトの方法) とデータを格納するオブジェクトを処理します。 CURLOPT_VERBOSE オプション を設定すると、実行されている操作に関する詳細情報を表示できます。 すべてのオプションを指定すると、curl_easy_performの curl 関数が呼び出されます。 主な操作を行う。 その後、データがクリアされます。

      もう 1 つの一般的なメソッドを追加してみましょう。

      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 クラスには、説明されているメソッドに加えて、ネットワークから受信したデータが保存されたメモリ バッファへのアクセスを受信するための 2 つの基本メソッドが用意されています。 データは次のように表示できます。

      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 を使用するよりも簡単な場合があります。 とにかく、添付されたプロジェクトには、すべての必要なファイルがあります。

      もう一度メニュー "Project -> GetAndParse プロパティ" を開きます。 "Linker(リンカー)" のラップを解除し、"インプット" を選択し、右側の "Additional Dependencies(追加の依存関係)" を選択します。 上部のボックスに "gumbo.lib" という名前を追加します。

      さらに、以前に作成した "include" フォルダに "gumbo" フォルダを作成し、すべてのインクルード ファイルをコピーします。 stdafx.h ファイルにインプットを作成します。

      #include <gumbo\gumbo.h>
      
      
      

      ガンボについて2つの注意。 これはC++ の html5 コード パーサーです。 Pros:

      • HTML5仕様に完全に対応。
      • 誤ったインプットデータに対するレジスタンス。
      • 他の言語から呼び出すことができるシンプルな API。
      • すべての html5lib-0.95 テストに合格します。
      • Google インデックスから 2 50 億 ページ以上でテスト済み。

      Cons:

      • このパフォーマンスはほど高くないです。

      パーサーはページ ツリーのみを構築し、それ以外は何も実行しません。 これは欠点として扱うこともできます。 デベロッパは、優先メソッドを使用してツリーを操作できます。 このパーサーのラッパを提供するリソースがありますが、ここでは使用しません。 今回の目的は、パーサーの"改善"ではないので、この方法で使用します。 ツリーを構築し、目的のデータを詳しく見ることができます。 コンポーネントの操作は簡単です。

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

      関数を呼び出し、ソース HTML データを持つバッファへのポインタを渡します。 この関数は、デベロッパが機能するパーサーを作成します。 このデベロッパーは関数を呼び出し、リソースをインプットします。

      このタスクに進み、目的のページの html コードの調べから始めましょう。 今回の目的は明らかです - 何を探し、必要なデータがどこにあるかを理解する必要があります。 _https://www.mataf.net/ja/forex/tools/volatilityリンクを開き、ページのソースコードを見てください。 ボラティリティ データはテーブル <id="volTable" に含まれています。.. このデータは、ツリー内のテーブルを見つけるのに十分です。 明らかに、特定の通貨ペアのボラティリティを受け取る必要があります。 テーブル行の属性には通貨シンボル名が含まれています: <a0> </a0> 通貨シンボル名: "tr id="row_AUDCHF" クラス ="data_volat" 名前="AUDCHF"... このデータを使用すると、目的の行を見つけることができます。 各行は 5 つの列で構成されます。 最初の 2 つの列は必要ありませんが、他の 3 列には必要なデータがあります。 列を選択し、テキストデータを受け取り、浮動小数点数に変換してクライアントに戻りましょう。 プロセスを明確にするために、データ検索を 3 つの段階に分割します。

      1. 識別子 ("volTable") でテーブルを詳しく見ることができます。
      2. 識別子 ("row_Currency ペア名") を使用して行を詳しく見ることができます。
      3. 直近の 3 つの列のいずれかでボラティリティ値を見つけ、見つかった値を返します。
      コードの記述を開始してみましょう。 プロジェクトに CVolatility クラスを作成します。 パーサー ライブラリは既に接続されているので、ここで追加の操作は必要ありません。 ご存知のように、目的のテーブルのボラティリティは3つの異なる方法で3つの列に示されました。 したがって、いつの選択に適した列挙体を作成してみましょう。
      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 — ボラティリティタイプ、テーブル列番号

      このメソッドは、ボラティリティ値を返します。

      したがって、操作は、ツリーの最初からテーブル検索から開始されます。 この検索は、次のクローズ メソッドによって実装されます。

      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)
      
      

      このメソッドは、次の 2 つの要件を満たす要素が見つかるまで再帰的に呼び出されます。

      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 を返し、成功した場合はボラティリティ値を返します。

      ただし、この方法には欠点があります。 パーサーは、この依存性をある程度インプットしますが、コードは依然としてユーザーが影響を与えることができないデータと強く結び付けられています。 したがって、Web サイトのデザインが大幅に変更された場合、デベロッパはツリー検索に関連する部分全体を書き換える必要があります。 とにかく検索ステップは簡単で、関連する複数の関数を編集できます。

      その他の 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 によるこの関数呼び出しを有効にするために少し変更しました。 この link で行われる理由の説明を参照してください。

      この関数コードは、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 ページ要素に強制的にバインドされるため、意味がありません。 したがって、アドレス固有のライブラリです。 このメソッドは常に使用するべきではありませんが、今回の場合は適切なアプリケーションです。

      ライブラリのコンパイルとインストール

      通常の方法でライブラリをビルドします。 GETANDPARSE.dll、gumbo.dll、libcrypto-1_1-x64.dll、libcurl-x64.dll、libssl-1_1-x64.dll を含むすべての.dll をリリース フォルダから取得し、ターミナルの 'Library' フォルダにコピーします。 したがって、ライブラリがインストールされています。

      ライブラリ使用チュートリアル スクリプト

      簡単なスクリプトです。

      #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 ReqTypeTPe=byPips; 
      //+------------------------------------------------------------------+
      //| Script program start function                                    |
      //+------------------------------------------------------------------+
      
      void OnStart()
        {
         double res=GetVolatility(PairName,tpe);
         PrintFormat("Volatility for %s is %.3f",PairName,res);
        }
      
      

      このスクリプトはこれ以上の説明を必要としないでしょう。 スクリプト コードは次のとおりです。

      結論

      ページ HTML を最もシンプルな形式で解析する方法について説明しました。 ライブラリは準備完了コンポーネントで構成されています。 このコードは、初心者がアイデアを理解するのに役立つ大幅に簡略化されています。 このソリューションの主な欠点は、同期実行です。 ライブラリが HTML ページを受け取って処理するまで、スクリプトは制御を取得しません。 時間がかかる場合があり、インジケータやEAには受け入れられない場合があります。 このようなアプリケーションで使用するには、別のアプローチが必要です。 さらなる記事でより良い解決策を見つけることを試みます。


      この記事で使用されているプログラム

       # 名前
      タイプ
       詳細
      1 GetVolat.mq5
      スクリプト
      ボラティリティ データを受け取るスクリプト。
      2
      GetAndParse.zip アーカイブ
      ライブラリとテスト コンソール アプリケーションのソース コード