Parsen von HTML mit curl

31 Oktober 2019, 09:05
Andrei Novichkov
0
188

Einführung

Lassen Sie uns den Fall diskutieren, wie Daten von einer Website mit normalen Anfragen nicht abgerufen werden können. Was kann man in diesem Fall tun? Die allererste mögliche Idee ist es, eine Ressource zu finden, auf die über GET- oder POST-Anfragen zugegriffen werden kann. Manchmal gibt es solche Ressourcen nicht. Dies kann beispielsweise die Funktion eines eindeutigen Indikators oder den Zugriff auf selten aktualisierte Statistiken betreffen.

Man kann sich fragen: "Wozu das Ganze?" Eine einfache Lösung besteht darin, direkt aus einem MQL-Skript auf die Seite der Website zuzugreifen und die bereits bekannte Anzahl von Positionen an der bekannten Seitenposition zu lesen. Anschließend kann die empfangene Zeichenkette weiterverarbeitet werden. Dies ist eine der möglichen Methoden. Aber in diesem Fall wird der MQL-Skriptcode eng an den HTML-Code der Seite gebunden. Was passiert, wenn sich der HTML-Code ändert? Deshalb benötigen wir einen Parser, der eine baumartige Operation mit einem HTML-Dokument ermöglicht (die Details werden in einem separaten Abschnitt erläutert). Wenn wir den Parser in MQL implementieren, wird dies bequem und effizient in Bezug auf die Leistung sein? Kann ein solcher Code ordnungsgemäß gepflegt werden? Aus diesem Grund werden die Parsing-Funktionen in einer separaten Bibliothek implementiert. Der Parser wird jedoch nicht alle Probleme lösen. Er erfüllt die gewünschte Funktionen. Aber was ist, wenn sich das Design der Website radikal ändert und andere Klassennamen und Attribute verwendet werden? In diesem Fall müssen wir das Suchobjekt ändern oder mehrere Objekte bearbeiten. Daher ist es eines unserer Ziele, den notwendigen Code so schnell wie möglich und mit geringstem Aufwand zu erstellen. Es wird besser sein, wenn wir fertige Module verwenden. Dies ermöglicht es dem Entwickler, den Code einfach zu pflegen und im Falle der oben genannten Situation schnell zu bearbeiten.

Wir werden eine nicht zu umfangreiche Webseite auswählen und versuchen, interessante Daten von dieser Webseite zu laden. Die Art der Daten ist in diesem Fall nicht wesentlich, aber lassen Sie uns versuchen, ein nützliches Werkzeug zu schaffen. Natürlich müssen diese Daten den MQL-Skripten im Terminal zur Verfügung stehen. Der Programmcode wird als Standard-DLL erstellt.

In diesem Artikel werden wir das Tool ohne asynchrone Aufrufe und Multithreading implementieren.

Bestehende Lösungen

Die Möglichkeit, Daten aus dem Internet zu beziehen und zu verarbeiten, war schon immer für Entwickler interessant. Die Website mql5.com enthält mehrere Artikel, die interessante und vielfältige Ansätze beschreiben:

Ich empfehle sehr, diese Artikel zu lesen.

    Definition der Aufgabe

    Wir werden mit der folgenden Seite experimentieren: https://www.mataf.net/en/forex/tools/volatility. Wie der Name schon sagt, findet man dort Daten über die Volatilität von Währungspaaren. Die Volatilität wird in drei verschiedenen Einheiten dargestellt: Pips, US-Dollar und Prozent. Die Webseite ist nicht zu umfangreich, so dass sie effizient akzeptiert und analysiert werden kann, um die erforderlichen Werte zu erhalten. Die Vorstudie des Quelltextes zeigt, dass wir Werte erhalten müssen, die in separaten Tabellenzellen gespeichert sind. Lassen Sie uns also die Hauptaufgabe in zwei Teilaufgaben unterteilen:

    1. Abrufen und Speichern der Seite.
    2. Parsen der erhaltenen Seite, um die Dokumentenstruktur zu erhalten und nach den gewünschten Informationen in dieser Struktur zu suchen. Datenverarbeitung und Weitergabe an den Kunden.

    Beginnen wir mit der Umsetzung des ersten Teils. Müssen wir die erhaltene Seite als Datei speichern? In einer wirklich funktionierenden Version ist es offensichtlich, dass es nicht notwendig ist, die Seite zu speichern. Wir benötigen einen anpassbaren Cache, der in bestimmten Abständen aktualisiert wird. Es sollte die Möglichkeit bestehen, die Verwendung des Caches in Sonderfällen zu deaktivieren. Wenn beispielsweise ein MQL-Indikator bei jedem Tick Anfragen an die Quellseite sendet, ist es wahrscheinlich, dass dieser Indikator von dieser Website verbannt wird. Dies geschieht noch schneller, wenn das anfragende Skript auf den Charts mehrerer Währungspaare installiert ist. Auf jeden Fall wird das richtig gestaltete Werkzeug nicht zu häufig Anfragen senden. Stattdessen sendet es einmalig eine Anfrage, speichert das Ergebnis in einer Datei und fordert später Daten aus der Datei an. Nach Ablauf der Cache-Gültigkeit wird die Datei mit einer neuen Anforderung aktualisiert. Dadurch werden zu häufige Anfragen vermieden.

    In unserem Fall werden wir diesen Cache nicht erstellen. Durch das Senden mehrerer Trainingsanfragen haben wir keinen Einfluss auf das Vorgehen der Site. Stattdessen konzentrieren wir uns auf wichtigere Punkte. Weitere Kommentare zum Speichern von Dateien auf einer Festplatte werden gegeben, aber in diesem Fall werden die Daten im Speicher gespeichert und an den zweiten Programmblock, d.h. an den Parser, übergeben. Wir werden, wann immer möglich, vereinfachten Code verwenden, der für Anfänger verständlich ist und dennoch das Wesen der Grundidee widerspiegelt.

    Abrufen der HTML-Seite einer Website eines Drittanbieters

    Wie bereits erwähnt, ist eine der Ideen, bestehende gebrauchsfertige Komponenten und vorgefertigte Bibliotheken zu nutzen. Allerdings müssen wir auch die Zuverlässigkeit und Sicherheit des gesamten Systems gewährleisten. Die Komponenten werden nach ihrem Ruf ausgewählt. Um die gewünschte Seite zu erhalten, verwenden wir das bekannte offene Projekt curl.

    Dieses Projekt ermöglicht das Abrufen und Senden von Dateien an fast alle Quellen: http, https, ftp-Server und viele andere. Es unterstützt die Einstellung von Login und Passwort für die Autorisierung auf dem Server, die Verarbeitung von Redirects und Timeouts. Das Projekt wird mit einer umfassenden Dokumentation geliefert, die alle Funktionen des Projekts beschreibt. Darüber hinaus ist es ein plattformübergreifendes Open-Source-Projekt, was definitiv ein Vorteil ist. Es gibt ein weiteres Projekt, das die gleiche Funktionalität implementieren kann. Es ist das Projekt "wget". In diesem Fall wird jedoch aus den folgenden zwei Gründen curl verwendet:

    • curl kann Dateien empfangen und senden, während wget nur Dateien abruft.
    • wget ist nur als Konsolenanwendung wget.exe verfügbar.

    Die Unmöglichkeit, Dateien per wget zu versenden, ist für unsere Aufgabe nicht relevant, da wir nur eine HTML-Seite herunterladen müssen. Wenn wir uns jedoch mit curl vertraut machen, können wir es später für andere Aufgaben nutzen, die das Senden von Daten erfordern können.

    Ein größerer Nachteil besteht darin, dass es nur als wget.exe Utility ohne Bibliotheken wie wget.dll, wget.lib verfügbar ist.

    • In diesem Fall müssen wir, um wget von einer mit MetaTrader verbundenen DLL aus zu verwenden, einen separaten Prozess erstellen, der Zeit und Mühe kostet.
    • Daten, die über wget erhalten werden, können nur als Datei übergeben werden, was für uns unangenehm ist, da wir uns stattdessen für den Cache entschieden haben.

    Unter diesen Bedingungen ist das curl bequemer. Zusätzlich zur Konsolenanwendung curl.exe gibt es die Bibliotheken: libcurl-x64.dll und libcurl-x64.lib. So können wir ohne zusätzlichen Entwicklungsprozess curl in unser Programm einbinden und mit dem Speicherpuffer arbeiten, anstatt eine separate Datei mit den Ergebnissen der Operation von curl zu erstellen. curl ist auch als Quellcode verfügbar, aber die Erstellung von Bibliotheken, die auf dem Quellcode basieren, kCurlann zeitaufwendig sein. Daher enthält das angehängte Archiv die erstellten Bibliotheken, Abhängigkeiten und alle für den Betrieb erforderlichen Include-Dateien.

    Erstellen einer Bibliothek

    Öffnen Sie Visual Studio (ich habe Visual Studio 2017 verwendet) und erstellen Sie eine einfache DLL. Lassen Sie uns das Projekt GetAndParse nennen — die resultierende Bibliothek wird den gleichen Namen haben. Erstellen Sie zwei Ordner im Projektordner: "lib" und "include". Diese beiden Ordner werden für die Verbindung von Bibliotheken von Drittanbietern verwendet. Kopieren Sie libcurl-x64.lib in den Ordner 'lib' und erstellen Sie den Ordner 'curl' im Ordner 'include'. Kopieren Sie alle Include-Dateien nach 'curl'. Öffnen Sie das Menü: "Project -> GetAndParse Properties". Öffnen Sie im linken Teil des Dialogfensters "C/C++" und wählen Sie "General". Wählen Sie im rechten Teil "Additional Include Directories", klicken Sie auf den Pfeil nach unten und wählen Sie "Edit". Öffnen Sie im neuen Dialogfeld die linke Schaltfläche ganz oben in der oberen Zeile "New Line". Mit diesem Befehl wird eine bearbeitbare Zeile in der folgenden Liste hinzugefügt. Durch Anklicken der Schaltfläche rechts wählen Sie den neu erstellten "include"-Ordner aus und klicken auf "OK".

    Öffnen Sie "Linker", wählen Sie General und klicken Sie dann rechts auf "Additional Library Directories". Fügen Sie den erstellten Ordner "lib" hinzu, indem Sie die gleichen Aktionen wiederholen.

    Wählen Sie aus der gleichen Liste die Zeile "input" und klicken Sie rechts auf "Additional Dependencies". Fügen Sie den Namen "libcurl-x64.lib" in das obere Feld ein.

    Wir müssen auch libcurl-x64.dll hinzufügen. Kopieren Sie diese Datei zusammen mit den Bibliotheken zur Unterstützung der Verschlüsselung in die Ordner "debug" und "release".

    Das angehängte Archiv enthält die erforderlichen Dateien, die sich in entsprechenden Ordnern befinden. Die angehängten Projekteigenschaften wurden ebenfalls geändert, so dass Sie keine zusätzlichen Aktionen durchführen müssen.

    Klasse für den Abruf von HTML-Seiten

    Erstellen Sie im Projekt die Klasse CCurlExec, die die Hauptaufgabe implementiert. Sie interagiert mit curl, deshalb binden Sie es wie folgt ein:

    #include <curl\curl.h>

    Dies kann in der Datei CCurlExec.cpp geschehen, aber ich habe es vorgezogen, es in stdafx.h aufzunehmen.

    Definieren Sie einen Alias-Typ für die Callback-Funktion, der zum Speichern der empfangenen Daten verwendet wird:

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

    Erstellen Sie einfache Strukturen, um die empfangenen Daten in den Speicher zu schreiben:

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

    ... und in die Datei:

    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;

    Ich denke, diese Strukturen brauchen keine Erklärung. Das Tool sollte in der Lage sein, Informationen in den Speicher zu schreiben. Zu diesem Zweck stellen wir einen Puffer in der MSTRUCT-Struktur und deren Größe zur Verfügung.

    Um Informationen als Datei zu speichern (wir werden diese Möglichkeit im Projekt implementieren, obwohl wir in unserem Fall nur die Speicherung im Speicher verwenden werden), fügen Sie der FSTRUCT-Struktur die Funktion zum Erhalten des Dateinamens hinzu. Verwenden Sie zu diesem Zweck die Windows-API, um mit temporären Dateien zu arbeiten.

    Erstellen Sie nun eine Reihe von Callback-Funktionen, um die beschriebenen Strukturen zu füllen. Methode zum Füllen der Struktur vom Typ 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;
    }

    Wir werden hier nicht die zweite Methode zum Speichern von Daten in einer Datei anbieten, die der ersten ähnlich ist. Die Funktionssignaturen stammen aus der Dokumentation auf der Website des curl-Projekts.

    Diese beiden Methoden werden als "Standardfunktionen" verwendet. Sie werden verwendet, wenn der Entwickler keine eigenen Methoden für diese Zwecke zur Verfügung stellt. 

    Die Idee dieser Methoden ist sehr einfach. In den Methodenparametern wird folgendes übergeben: Informationen über die empfangene Datengröße, ein Zeiger auf die Quelle, d.h. interner curl-Puffer, und der Empfänger, die MSTRUCT-Struktur. Nach einigen Vorkonvertierungen werden die Felder der Empfängerstruktur gefüllt.

    Und schließlich die Methode, die die Hauptaktionen durchführt: Sie empfängt eine HTML-Seite und füllt eine Struktur vom Typ MSTRUCT mit den empfangenen Daten aus:

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

        Achten Sie auf wichtige Aspekte der Arbeit von curl. Zunächst werden zwei Initialisierungen durchgeführt, wodurch der Nutzer einen Zeiger auf den "Kern" von curl und auf dessen "Handle" erhält, die bei weiteren Aufrufen verwendet wird. Die weitere Verbindung ist konfiguriert, was viele Einstellungen erfordern kann. In diesem Fall bestimmen wir die Verbindungsadresse, die Notwendigkeit, die Zertifikate zu überprüfen, den Puffer, in den Fehler geschrieben werden, die Timeout-Dauer, den Kopf des "User-Agents", die Notwendigkeit der Behandlung von Redirects, die Funktion, die zur Verarbeitung empfangener Daten aufgerufen wird (die oben beschriebene Standardmethode) und das Objekt zum Speichern der Daten. Die Einstellung der Option CURLOPT_VERBOSE ermöglicht die Anzeige von Detailinformationen über durchgeführte Operationen, was für Debuggingzwecke nützlich sein kann. Sind alle Optionen angegeben, wird die curl-Funktion curl_easy_perform aufgerufen. Sie leistet die Hauptarbeit. Danach werden die Daten gelöscht.

        Lassen Sie uns eine weitere noch allgemeinere Methode hinzufügen:

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

        Diese Methode ermöglicht es dem Entwickler, die empfangenen Daten (den pFunc-Parameter) mit einer benutzerdefinierten Callback-Funktion zu verarbeiten und ein benutzerdefiniertes Objekt zum Speichern dieser Daten (den pTarget-Parameter). So kann eine HTML-Seite einfach gespeichert werden, z.B. als CSV-Datei.

        Lassen Sie uns sehen, wie Informationen in einer Datei gespeichert werden, ohne ins Detail zu gehen. Die entsprechende Callback-Funktion wurde bereits erwähnt, ebenso wie das Hilfsobjekt FSTRUCT mit einem Code zur Auswahl des Dateinamens. In den meisten Fällen endet die Arbeit jedoch nicht damit. Um den Dateinamen zu erhalten, kann er entweder im Voraus festgelegt werden (in diesem Fall sollten Sie vor dem Schreiben prüfen, ob die Datei mit diesem Namen existiert), oder die Bibliothek kann einen lesbaren und "sprechenden" Dateinamen erhalten. Dieser Name sollte die tatsächliche Adresse beinhalten, an der die Daten nach der Verarbeitung von Umleitungen gelesen wurden. Das folgende Verfahren zeigt, wie eine tatsächliche Adresse erhalten wird. 

        bool CCurlExec::GetFiletoFile(const char * pUri)

        Der vollständige Code der Methode ist im Archiv verfügbar. Die in 'curl' zur Verfügung gestellten Tools werden für das Parsen von Adressen verwendet:

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

        Man sieht, dass curl "PATH" aus der URL korrekt extrahiert und überprüft, ob der PATH mit dem Zeichen "/" endet. In diesem Fall sollte der Dateiname "index.html" lauten. Wenn nicht, wird "PATH" in separate Elemente aufgeteilt, während der Dateiname das letzte Element in der Ergebnisliste ist.

        Beide der oben genannten Methoden sind im Projekt implementiert, obwohl die Aufgabe der Datenspeicherung in einer Datei im Allgemeinen den Rahmen dieses Artikels sprengt.

        Zusätzlich zu den beschriebenen Methoden stellt die Klasse CCurlExec zwei elementare Methoden zum Empfangen von Zugriff auf den Speicherpuffer zur Verfügung, in dem die vom Netzwerk empfangenen Daten gespeichert wurden. Die Daten können wie folgt dargestellt werden

        std::vector<char>
        oder in folgender Form:
        std::string

        abhängig von der weiteren HTML-Parserauswahl. Es ist nicht notwendig, andere Methoden und Eigenschaften der Klasse CCurlExec zu untersuchen, da sie in unserem Projekt nicht anwendbar sind.

        Zum Abschluss dieses Kapitels möchte ich einige Bemerkungen hinzufügen. Die curl-Bibliothek ist threadsicher. In diesem Fall wird es synchron verwendet, für welche Methoden des Typs curl_easy_init Methoden verwendet werden. Alle curl-Funktionen mit "easy" im Namen werden nur synchron verwendet. Die asynchrone Nutzung der Bibliothek erfolgt über Funktionen, die im Namen "multi" enthalten. Beispielsweise hat curl_easy_init eine analoge asynchrone Funktion curl_multi_init. Die Arbeit mit asynchronen Funktionen in curl ist nicht sehr kompliziert, aber es beinhaltet langwierigen Aufrufcode. Daher werden wir jetzt keinen asynchronen Betrieb in Betracht ziehen, aber wir können später darauf zurückkommen.

        Die Klasse für das Parsen von HTML

        Lassen Sie uns versuchen, eine fertige Komponente für diese Aufgabe zu finden. Es stehen viele verschiedene Komponenten zur Verfügung. Verwenden Sie bei der Auswahl einer Komponente die gleichen Kriterien wie im vorherigen Kapitel. In diesem Fall ist die bevorzugte Option das Gumbo-Projekt von Google. Es ist auf github verfügbar. Der entsprechende Link ist im beigefügten Projektarchiv verfügbar. Sie können das Projekt selbst kompilieren, was einfacher sein kann als die Verwendung von curl. Jedenfalls enthält das angehängte Projekt alle notwendigen Dateien:

        • gumbo.lib ist im Projektordner lib verfügbar
        • gumbo.dll ist im Debug und Release enthalten

        Öffnen Sie erneut das Menü "Project -> GetAndParse Properties". Öffnen Sie "Linker", wählen Sie "input" und wählen Sie dann rechts "Additional Dependencies". Füge den Namen "gumbo.lib" in das obere Feld ein.

        Darüber hinaus erstellen Sie im zuvor erstellten "include"-Ordner den Ordner "gumbo" und kopieren Sie alle Include-Dateien in diesen. Machen Sie einen Eintrag in der Datei stdafx.h:

        #include <gumbo\gumbo.h>

        Zwei Worte über Gumbo. Dies ist der html5-Codeparser in C++. Vorteile:

        • Volle Übereinstimmung mit der HTML5-Spezifikation.
        • Robustheit bei falschen Eingangsdaten.
        • Einfache API, die aus anderen Sprachen aufgerufen werden kann.
        • Besteht alle html5lib-0.95 Tests.
        • Getestet auf mehr als zweieinhalb Milliarden Seiten aus dem Google-Index.

        Nachteile

        • Die Durchführung ist nicht besonders schnell

        Der Parser erstellt nur den Baum der Seiten, nichts anderes. Dies kann auch als Nachteil sein. Der Entwickler kann dann mit dem Baum mit bevorzugten Methoden arbeiten. Es gibt Ressourcen, die Wrapper für diesen Parser bereitstellen, aber wir werden sie nicht verwenden. Unser Ziel ist es nicht, den Parser zu "verbessern", also werden wir ihn so verwenden, wie er ist. Es wird ein Baum aufgebaut, in dem wir nach den gewünschten Daten suchen. Die Arbeit mit einer Komponente ist einfach:

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

        Wir rufen eine Funktion auf und übergeben ihr einen Zeiger auf einen Puffer mit den Quelldaten der HTML-Seite. Die Funktion erstellt einen Parser, mit dem der Entwickler arbeitet. Der Entwickler ruft die Funktion auf und gibt Ressourcen frei.

        Lassen Sie uns mit dieser Aufgabe fortfahren und mit der Untersuchung des HTML-Codes der gewünschten Seite beginnen. Der Zweck ist klar - wir müssen verstehen, wonach wir suchen und wo sich die notwendigen Daten befinden. Öffnen Sie den Link https://www.mataf.net/en/forex/tools/volatility und schauen Sie sich den Quellcode der Seite an. Volatilitätsdaten sind in der Tabelle <table id="volTable"...> enthalten Diese Daten reichen aus, um die Tabelle im Baum zu finden. Natürlich müssen wir die Volatilität für ein bestimmtes Währungspaar erhalten. Die Attribute von Tabellenzeilen enthalten Namen von Währungssymbolen: <tr id="row_AUDCHF" class="data_volat" name="AUDCHF"...> Anhand dieser Daten kann die gewünschte Zeile leicht gefunden werden. Jede Zeile besteht aus fünf Spalten. Wir brauchen die ersten beiden Spalten nicht, während die drei anderen die erforderlichen Daten enthalten. Lassen Sie uns eine Spalte auswählen, Textdaten empfangen, in double konvertieren und zum Kunden zurückkehren. Um den Prozess klarer zu machen, lassen Sie uns die Datensuche in drei Phasen unterteilen:

        1. Finden Sie die Tabelle anhand ihres Bezeichners ("volTable").
        2. Suchen Sie die Zeile anhand ihres Bezeichners ("row_Currency Pair Name").
        3. Finden Sie den Volatilitätswert in einer der letzten drei Spalten und geben Sie den gefundenen Wert zurück.
        Beginnen wir, den Code zu schreiben. Erstellen Sie die Klasse CVolatility im Projekt. Die Parser-Bibliothek ist bereits verbunden, so dass hier keine zusätzlichen Aktionen erforderlich sind. Wie Sie sich erinnern, wurde die Volatilität in der gewünschten Tabelle in drei Spalten und auf drei verschiedene Arten dargestellt. Lassen Sie uns daher die entsprechende Aufzählung für die Auswahl einer davon erstellen:
        typedef enum {
                byPips = 2,
                byCurr = 3,
                byPerc = 4
        } VOLTYPE;

        Ich denke, dieser Teil ist klar und bedarf keiner zusätzlichen Erklärung. Sie ist als Auswahl der Spaltennummer implementiert.

        Als Nächstes erstellen Sie eine Methode, die den Volatilitätswert zurückgibt:

        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)

        Rufen Sie die Methode mit den folgenden Argumenten auf:

        • szHtml — Die Referenz auf einen Puffer mit empfangenen Daten im html-Format.
        • pair — Der Name des Währungspaares, nach dem die Volatilität gesucht wird.
        • vtype — Die Volatilitätsart, die Tabellenspaltennummer.

        Die Methode gibt den Volatilitätswert zurück oder -1 im Fehlerfall.

        Somit beginnt die Operation mit der Tabellensuche, vom Anfang des Baumes an. Diese Suche wird mit der folgenden geschlossenen Methode durchgeführt:

        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)

        Die Methode wird rekursiv aufgerufen, bis ein Element gefunden wird, das folgende beide Anforderungen erfüllt:

        1. Es muss eine Tabelle sein.
        2. Die "id" muss "volTable" sein.
        Wenn ein solches Element nicht gefunden wird, gibt die Methode -1 zurück. Andernfalls gibt die Methode den Wert zurück, der eine ähnliche Methode zurückgibt, die nach einer Tabellenzeile sucht:
        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)
        Wird eine Tabellenzeile mit der ID = "row_PairName" gefunden, wird die Suche durch den Aufruf der Methode abgeschlossen, die den Zellwert in einer bestimmten Spalte der gefundenen Zeile liest:
        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;
        }

        Bitte beachten Sie, dass für das Trennzeichen der Daten in der Tabelle anstelle des Punktes ein Komma verwendet wird. Daher hat der Code ein paar Zeilen, um dieses Problem zu lösen. Wie in früheren Fällen gibt die Methode -1 im Fehlerfall oder den Volatilitätswert im Erfolgsfall zurück.

        Dieser Ansatz hat jedoch einen Nachteil. Der Code ist immer noch stark an Daten gebunden, die der Benutzer nicht beeinflussen kann, obwohl der Parser diese Abhängigkeit teilweise aufhebt. Wenn sich das Website-Design also erheblich ändert, muss der Entwickler den gesamten Teil der Suche im Baum neu schreiben. Jedenfalls ist das Suchverfahren einfach, und die damit verbundenen verschiedenen Funktionen können leicht bearbeitet werden.

        Weitere Mitglieder der Klasse CVolatility sind im beigefügten Archiv verfügbar. Wir werden sie in diesem Artikel nicht berücksichtigen.

        Alles zusammenfügen

        Der Hauptcode ist fertig. Jetzt müssen wir alles zusammenfügen und eine Funktion entwerfen, die Objekte erzeugt und die Aufrufe in der richtigen Reihenfolge durchführt. Der folgende Code wird in die Datei GetAndParse.h eingefügt:

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

        Es enthält bereits eine Makrodefinition, die wir leicht modifiziert haben, um diesen Funktionsaufruf durch mql zu ermöglichen. Siehe die Erklärung, warum dies geschieht, hier.

        Der Funktionscode wird in die Datei GetAndParse.cpp geschrieben:

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

        Ist es eine gute Idee, die Seitenadresse fest zu kodieren? Warum kann es nicht als Argument des GetVolatility-Funktionsaufrufs implementiert werden? Es macht keinen Sinn, denn der vom Parser zurückgegebene Informationssuchalgorithmus ist zwangsweise an die HTML-Seitenelemente gebunden. Es handelt sich also um eine adressenspezifische Bibliothek. Diese Methode sollte nicht immer angewendet werden, ist aber in unserem Fall eine geeignete Lösung.

        Zusammenstellung und Installation der Bibliothek

        Erstellen Sie die Bibliothek wie gewohnt. Nehmen Sie alle Dlls aus dem Ordner Release, einschließlich: GETANDPARSE.dll, gumbo.dll, libcrypto-1_1-x64.dll, libcurl-x64.dll und libssl-1_1-x64.dll, und kopieren Sie sie in den Ordner 'Libraries' des Terminals. Damit ist die Bibliothek installiert.

        Ein als Skript Anleitung zur Bibliotheksnutzung

        Dies ist ein einfaches Skript:

        #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 Programm Start Funktion                                   |
        //+------------------------------------------------------------------+
        
        void OnStart()
          {
           double res=GetVolatility(PairName,tpe);
           PrintFormat("Volatility for %s is %.3f",PairName,res);
          }

        Das Skript bedarf keiner weiteren Erklärung. Der Skriptcode ist unten angehängt.

        Schlussfolgerung

        Wir haben eine Methode zum Parsen der Seite HTML in der am meisten vereinfachten Form diskutiert. Die Bibliothek besteht aus fertigen Komponenten. Der Code wurde stark vereinfacht, um Anfängern das Verständnis der Idee zu erleichtern. Der Hauptnachteil dieser Lösung ist die synchrone Ausführung. Das Skript übernimmt die Kontrolle erst, wenn die Bibliothek die HTML-Seite erhält und verarbeitet. Dies kann einen Zeitraum in Anspruch nehmen, der für Indikatoren und Expert Advisors inakzeptabel ist. Für den Einsatz in solchen Anwendungen ist ein anderer Ansatz erforderlich. In weiteren Artikeln werden wir versuchen, bessere Lösungen zu finden.


        Programme, die im diesem Artikel verwendet werden

         # Name
        Typ
         Beschreibung
        1 GetVolat.mq5
        Skript
        Das Skript, das die Volatilitätsdaten erhält.
        2
        GetAndParse.zip Archive
        Der Quellcode der Bibliothek und der Anwendung für den Test auf der Konsole


        Übersetzt aus dem Russischen von MetaQuotes Software Corp.
        Originalartikel: https://www.mql5.com/ru/articles/7144

        Beigefügte Dateien |
        GetVolat.mq5 (1.45 KB)
        GetAndParse.zip (4605.35 KB)
        Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XV): Kollektion von Symbolobjekten Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XV): Kollektion von Symbolobjekten

        In diesem Artikel werden wir die Erstellung einer Symbolsammlung auf der Grundlage des im vorherigen Artikel entwickelten abstrakten Symbolobjekts in Betracht ziehen. Die abstrakten, abgeleiteten Symbole sollen Symboldaten verdeutlichen und die Verfügbarkeit der grundlegenden Eigenschaften der Symbolobjekte in einem Programm definieren. Solche Symbolobjekte sind durch ihre Zugehörigkeit zu Gruppen zu unterscheiden.

        Ein neuer Ansatz der Interpretation der klassischen und der versteckten Divergenz Teil II Ein neuer Ansatz der Interpretation der klassischen und der versteckten Divergenz Teil II

        Der Artikel bietet eine kritische Untersuchung der normalen Divergenz und Effizienz verschiedener Indikatoren. Darüber hinaus enthält er Filtermöglichkeiten für eine erhöhte Analysegenauigkeit und Funktionsbeschreibungen von Nicht-Standard-Lösungen. Als Ergebnis werden wir ein neues Werkzeug zur Lösung der technischen Herausforderungen schaffen.

        Das MQL5-Kochbuch: Stresstests von Handelsstrategien unter Verwendung nutzerdefinierter Symbole Das MQL5-Kochbuch: Stresstests von Handelsstrategien unter Verwendung nutzerdefinierter Symbole

        Der Artikel betrachtet einen Ansatz für einen Stresstest einer Handelsstrategie unter Verwendung nutzerdefinierter Symbole. Zu diesem Zweck wird eine eigene Symbolklasse angelegt. Diese Klasse wird verwendet, um Tickdaten von Drittanbietern zu empfangen und die Symboleigenschaften zu ändern. Basierend auf den Ergebnissen der Arbeit werden wir mehrere Optionen für sich ändernde Handelsbedingungen in Betracht ziehen, unter denen eine Handelsstrategie getestet wird.

        Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XVI): Ereignisse der Kollektionssymbole Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XVI): Ereignisse der Kollektionssymbole

        In diesem Artikel werden wir eine neue Basisklasse aller Bibliotheksobjekte erstellen, die die Ereignisfunktionen allen ihren Nachkommen hinzufügt, und die Klasse zur Verfolgung von Ereignissen der Kollektionssymbole auf der Grundlage der neuen Basisklasse entwickeln. Wir werden auch die Konto- und Ereignisklassen für die Entwicklung der neuen Basisobjektfunktionalität ändern.