Парсинг HTML с помощью curl

23 августа 2019, 09:30
Andrei Novichkov
0
1 269

Введение

Еще одна вполне возможная задача, возникающая в том случае, когда какие то данные со страницы сайта нельзя получить обычными запросами. Что делать в таком случае? Конечно, первое, что приходит в голову, это поискать источник данных, к которому можно обратиться с GET или POST запросами. Но такого источника может и не найтись. Например, если речь идет о работе какого-то уникального индикатора, о каких то обновляемых редких статистических данных.

Как и в других таких случаях, вполне может возникнуть такой вопрос: "А зачем это нужно?" Можно прямо из MQL скрипта принять страницу сайта и прочитать в заранее известном месте заранее известное количество символов. А затем работать с получившейся строкой. Да, так можно сделать. Но такой подход намертво привяжет код MQL скрипта к HTML коду страницы. А если этот код хотя бы минимально изменится? Вот в таких случаях и возникает потребность в парсере, который предоставляет возможность работы c HTML-документом, как с деревом (к этому мы еще вернемся в соответствующем разделе). И его можно реализовать на MQL, но правильно ли это будет, как скажется на производительности, удобно ли будет сопровождать такой код? Поэтому задача парсинга выносится в отдельную библиотеку. Сразу стоит сказать, что и парсер не решает всех проблем. Он свою часть работы сделает, но что, если дизайн сайта изменится настолько радикально, что станут другими имена классов, атрибуты? В этом случае придется оперативно менять объект поиска, а возможно и не один объект. Поэтому одна из задач, которая будет перед нами стоять, это создание нужного кода максимально быстро и с наименьшими усилиями. И желательно из уже готовых частей. В таком случае разработчик сможет легче сопровождать код и оперативно вносить в него изменения в случае возникновения описанной ситуации.

Мы подберем какой-либо сайт с не слишком объемными страницами и попытаемся получить с него интересную информацию. Какую именно, это не так уж и важно, но все-таки постараемся, чтобы наши усилия были не совсем бесполезны. Разумеется, эта информация должна быть доступна MQL скриптам в терминале. Программный код будет оформлен в виде стандартной DLL библиотеки.

В настоящей статье задача будет решаться без асинхронных вызовов и учета многопоточности.

Имеющиеся возможные решения

Решению вопроса о получению данных из сети, с последующей их обработкой, разработчики интересовались всегда, вероятно, будут интересоваться и в будущем. В частности, на данном сайте можно найти несколько статей с интересными и разнообразными подходами к теме:

Все эти статью настоятельно рекомендуются к прочтению.

    Постановка задач

    Для эксперимента предлагается следующий сайт: 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, разработчик вполне может принять решение об использовании curl и в дальнейшем, где её универсальность может пригодиться.

    Наличие просто утилиты wget.exe и отсутствие каких либо библиотек типа wget.dll, wget.lib для наших целей имеет решающий характер по очевидным причинам:

    • Для того чтобы задействовать wget из dll библиотеки, присоединенной к MetaTrader, придется создавать целый процесс, что долго и в данном случае неоправданно.
    • Данные, полученные в результате работы wget, можно будет передать только в виде файла, а мы этого решили не делать, как писали выше.

    Всех этих недостатков лишен curl. Главное, что он, кроме существования в виде консольной программы curl.exe, имеет и библиотеки — libcurl-x64.dll и libcurl-x64.lib. А это позволяет включить curl в свою программу без создания дополнительного процесса, что, в свою очередь, дает возможность  не оформлять результаты работы curl в виде файла, а работать с буфером в памяти. Curl так же доступен в виде исходных кодов, но создание из него необходимых библиотек может оказаться достаточно непростым делом. Поэтому в прилагаемый архив уже включены уже созданные  библиотеки, зависимости и все включаемые файлы, нужные в работе.

    Создаем библиотеку

    Открываем Visual Studio (я использовал Visual Studio 2017) и создаем проект обычной dll, как уже не раз объяснялось. Назовем проект GetAndParse и в последствии получим библиотеку с тем же именем. Создадим в папке проекта папку "lib" и папку "include". Эти две папки нужны для подключения сторонних библиотек. В папку lib мы копируем libcurl-x64.lib, а в папке include создадим  папку "curl". В эту папку копируем все включаемые файлы. Теперь откроем пункт меню  "Project -> GetAndParse Properties". В левой части диалогового окна раскрываем свиток "C/C++" и выбираем  "General". В правой части выбираем "Additional Include Directories", нажимаем на галочку вниз, выбираем "Edit". В новом диалоговом окне нажимаем на крайнюю левую кнопку в верхнем ряду "New Line". Это действие добавляет строку в список ниже, которую можно редактировать. Нажимая на кнопку справа выбираем папку "include", которую только что создали и  жмем "OK".

    Раскрываем свиток "Linker", выбираем General, а справа пункт "Additional Library Directories". Повторяя уже описанные действия добавляем созданную папку "lib".

    В этом же свитке выбираем строку "input", а справа "Additional Dependencies". Добавляем в верхнее окно название "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); //редиректы
    #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, на его "handle", которое используется в последующих вызовах. Выполняются настройки будущего соединения, которых может быть очень много. В данном случае мы определяем адрес соединения, следует ли проверять сертификаты, указываем буфер, куда будут записываться ошибки, определяем длину таймаутов, "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 корректно выделяет "PATH" из uri, а затем проверяется, не заканчивается ли "PATH" символом '/'. Если заканчивается, то имя файла должно быть "index.html". Если же нет, то "PATH" разделяется на отдельные элементы, а имя файла должно быть последним в списке таких элементов.

        В нашем проекте реализованы отдельные вышеописанные методы, но  в целом, задача по сохранению данных в виде файла не решалась, как не актуальная в данном случае.

        Помимо описанных, в классе CCurlExec предусмотрены два элементарных метода для получения доступа к буферу памяти, куда были сохранены полученные из сети данные. Данные могут быть предоставлены в виде

        std::vector<char>
        , либо в виде
        std::string

        что зависит от нашего дальнейшего выбора парсера html. Остальные методы и свойства класса CCurlExec интереса не представляют и рассматриваться здесь не будут.

        В заключении этой главы скажем еще несколько слов. Библиотека curl безопасна по отношению к потокам. В данном случае она используется синхронно, для чего применяются методы типа curl_easy_init. Все функции curl, в названия которых входит "easy" применяются только синхронно. Для асинхронного применения библиотеки служат функции, в названия которых входит слово "multi", например асинхронный аналог синхронной функции curl_easy_init будет curl_multi_init. Работа с асинхронными функциями в curl особой сложностью не отличается, но должна сопровождаться значительным по объему вызывающим кодом. Поэтому асинхронную работу мы рассматривать в настоящий момент не будем, но вполне возможно вернемся к этому в будущем.

        Класс для парсинга HTML

        Для выполнения этой задачи мы тоже постараемся подобрать готовый компонент. Таковых довольно много, разного качества и степени готовности. При выборе компонента для работы мы станем руководствоваться теми же критериями, что и в предыдущей главе. В этом случае предпочтительным вариантом для работы оказывается проект Gumbo от Google. Он представлен на github-е, ссылка будет вложена в архив с проектом. Желающие могут собрать проект самостоятельно, это проще, чем собирать curl, но в проект, находящийся в архиве уже вложены все необходимые файлы:

        • gumbo.lib в папку проекта lib
        • gumbo.dll в папки debug и release

        Еще раз откроем пункт меню  "Project -> GetAndParse Properties". Раскрываем свиток "Linker", выбираем строку "input", а справа "Additional Dependencies". Добавляем в верхнее окно название "gumbo.lib".

        Кроме того, в созданной ранее папке include создадим папку gumbo и скопируем в неё все подключаемые файлы. Сделаем необходимую запись в файле stdafx.h:

        #include <gumbo\gumbo.h>
        

        Два слова про gumbo. Это парсер html5 кода на Си99. Достоинства:

        • Полное соответствие со спецификацией HTML5.
        • Устойчивость к неправильным входным данным.
        • Простые API, которые могут быть вызваны из других языков.
        • Проходит все тесты html5lib-0.95 tests.
        • Проверен на более чем двух с половиной миллиардов страниц из индекса гугла.

        Недостатки:

        • Не самая высокая производительность

        Кроме того, возможно, недостатком стоит назвать то, что парсер строит дерево страницы и больше не делает ничего. Он предоставляет это дерево разработчику и уже тот может работать с ним так, как считает нужным. Можно обнаружить источники, предоставляющие обертки данного парсера, но мы использовать их не станем. Мы в данной статье не будем стремиться к "усовершенствованиям" парсера, а используем его, как есть. Он нам посторит дерево, мы в этом дереве выполним поиск нужных нам данных. Работа с компонентом проста:

                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"... По этим данным можно легко отыскать нужную строку. Каждая строка состоит из пяти столбцов. Первые два нам не интересны, а вот в последних трех содержатся нужные сведения. Выберем какой либо столбец, получим текстовые данные, преобразуем их double и вернем клиенту. Для простоты восприятия разложим задачу поиска в дереве на три этапа:

        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_ИмяПары" найдена, поиск заканчивается вызовом метода, считывающего значение ячейки таблицы определенного столбца найденной строки:
        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 и значение волатильности в случае успеха.

        Сразу стоит отметить недостаток такого подхода. Код все равно получается сильно привязанным к данным, повлиять на которые у разработчика не получится, хотя парсер и существенно ослабляет эту зависимость. В итоге, разработчику придется смbриться с тем, что в том случае, если на сайте произойдет действительно существенное изменение дизайна, придется переделывать весь поиск в дереве. Но это немного компенсируется простотой этого поиска, тем, что переделать описанные несколько функций труда не составляет.

        Прочие члены класса 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 страницы в максимально упрощенной форме. Библиотека была собрана из готовых компонентов, код предельно облегчен для понимания начинающих, или не достаточно хорошо знакомых с C++. Нужно заметить, что основным недостатком принятого подхода является его синхронность. Скрипт не получит управление, прежде, чем библиотека не получит HTML страницу и не обработает её. Это занимает время, что неприемлемо для индикаторов и советников. Там нужен другой подход, что мы и попробуем сделать в других статьях.


        Программы, используемые в статье:

         # Имя
        Тип
         Описание
        1 GetVolat.mq5
        Скрипт
        Скрипт, получающий данные по волатильности.
        2
        GetAndParse.zip Архив
        Исходный код библиотеки и тестового консольного приложения


        Прикрепленные файлы |
        GetVolat.mq5 (1.45 KB)
        GetAndParse.zip (4605.35 KB)
        Библиотека для простого и быстрого создания программ для MetaTrader (Часть XVIII): Интерактивность объекта-аккаунт и любых других объектов библиотеки Библиотека для простого и быстрого создания программ для MetaTrader (Часть XVIII): Интерактивность объекта-аккаунт и любых других объектов библиотеки

        В статье организована работа объекта-аккаунт на новом базовом объекте всех объектов библиотеки, доработан базовый объект CBaseObj и протестирована установка отслеживаемых параметров, а также получение событий для любых объектов библиотеки.

        Новый подход к интерпретации классической и обратной дивергенции. Часть 2 Новый подход к интерпретации классической и обратной дивергенции. Часть 2

        В этой статье мы критическом ключе рассмотрим классическую дивергенцию и проанализируем эффективность различных индикаторов. А также предложим варианты фильтрации для повышения точности анализа и продолжим рассматривать нестандартные решения. Как результат - создадим нетипичный инструмент для решения поставленной задачи.

        Библиотека для простого и быстрого создания программ для MetaTrader (Часть XIX): Класс сообщений библиотеки Библиотека для простого и быстрого создания программ для MetaTrader (Часть XIX): Класс сообщений библиотеки

        В статье рассмотрим класс вывода текстовых сообщений. Сейчас у нас имеется достаточное количество различных текстовых сообщений, и уже стоит подумать о реорганизации способа их хранения, вывода и удобства правки русских сообщений на иной язык, а так же об удобном способе добавления новых языков в библиотеку и быстром переключении между ними.

        Библиотека для простого и быстрого создания программ для MetaTrader (Часть XX): Создание и хранение ресурсов программы Библиотека для простого и быстрого создания программ для MetaTrader (Часть XX): Создание и хранение ресурсов программы

        В статье рассмотрим способ хранения данных в исходниках программы и создание из них звуковых и графических файлов. Часто при создании программы, нам требуется использовать звуки и изображения. В языке MQL есть несколько возможностей использования таких данных.