[C++] 321 BBS閲覧アプリの製作 その7 スレッドタイトルの取得 libxml++は使えず

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

libxmlのC++ラッパーであるlibxml++を使おうとしたものの、上手くいきませんでした。

アクセスしようとしているglibmmconfig.hは最新版glibmm 2.68にはありません。2.4にはあるとのことだったのでダウンロードして存在を確認しました。ただしこれは2014年リリースのものです。

本家のlibxmlで事足りているため、libxml++に対する需要はあまり高くないのでしょうか。開発自体は今も続いていますが、深追いはやめておきます。

#include <iostream>
#include <string>
#include <vector>
#include <curl/curl.h>
#include <libxml++/libxml++.h>
#include <iostream>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* buffer)
{
    size_t totalSize = size * nmemb;
    buffer->append((char*)contents, totalSize);
    return totalSize;
}

int main()
{
    // URLからHTMLファイルを取り込む
    std::string url = "HTMLのURL";
    std::string htmlBuffer;
    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &htmlBuffer);
        CURLcode res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: Failed to download HTML" << std::endl;
            return 1;
        }
    } else {
        std::cerr << "Error: Failed to initialize CURL" << std::endl;
        return 1;
    }

    try {
        // HTMLをXMLとして解析
        xmlpp::DomParser parser;
        parser.parse_memory(htmlBuffer);

        // ルート要素の取得
        xmlpp::Document* document = parser.get_document();
        xmlpp::Element* rootElement = document->get_root_node();

        // XPathの評価
        std::string xpathExpression = "/root/small/a";
        xmlpp::NodeSet result = rootElement->find(xpathExpression);

        // 結果の表示
        for (auto node : result) {
            if (xmlpp::Element* element = dynamic_cast<xmlpp::Element*>(node)) {
                std::cout << "Element: " << element->get_name() << std::endl;
                std::cout << "Value: " << element->get_child_text()->get_content() << std::endl;
            }
        }
    }
    catch (const std::exception& ex) {
        std::cerr << "Exception caught: " << ex.what() << std::endl;
        return 1;
    }

    return 0;
}
/opt/homebrew/Cellar/glibmm/2.76.0/include/glibmm-2.68/glibmm/ustring.h:20:10: fatal error: 'glibmmconfig.h' file not found
#include <glibmmconfig.h>
         ^~~~~~~~~~~~~~~~
1 error generated.

[C++] 320 BBS閲覧アプリの製作 その6 スレッドタイトルの取得 libxmlでXPathを使ってパース

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

libxmlライブラリでXPathを使ってスクレイピングしました。

前の方法ではタグ内の文字列比較で絞り込んでいくためコードの階層が深くなって可読性が低かったのですが、そのような問題が解消されてとてもシンプルになりました。

これぞスクレイピングといった内容になり満足です。今後はC++でも自在に情報収集できそうです。

8年前(2015年)のStack Overflowの記事が大変参考になりました。

#include <iostream>
#include <string>
#include <vector>
#include <curl/curl.h>
#include <libxml/HTMLparser.h>
#include <libxml/xpath.h>

xmlNodeSetPtr executeXpath(xmlDocPtr &doc, xmlChar *xpath_expr) {
    xmlXPathContextPtr xpath_context;
    xmlXPathObjectPtr  xpath_obj;

    xpath_context = xmlXPathNewContext(doc);
    if (xpath_context == NULL) {
        cerr << "Error: unable to create new XPath context" << endl;
        xmlFreeDoc(doc);
        return NULL;
    }
    xmlNodePtr node = xmlDocGetRootElement(doc);

    /* Evaluate xpath expression */
    xpath_obj = xmlXPathEvalExpression(xpath_expr, xpath_context);
    if (xmlXPathNodeSetIsEmpty(xpath_obj->nodesetval)) {
        cerr << "Error: unable to evaluate xpath expression" << endl;
        xmlXPathFreeContext(xpath_context);
        xmlFreeDoc(doc);
        return NULL;
    }

    /* Print results */
    return xpath_obj->nodesetval;
}

std::string convertToString(const xmlChar* xmlString) {
    if (xmlString == nullptr) {
        return "";
    }
    return std::string(reinterpret_cast<const char*>(xmlString));
}

// コールバック関数
size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* buffer) {
    size_t totalSize = size * nmemb;
    buffer->append((char*)contents, totalSize);
    return totalSize;
}

int main() {
    std::vector<std::pair<std::string, std::string>> idTitlePairs;

    // URLからHTMLファイルを取り込む
    std::string url = "HTMLファイルのURL";
    std::string htmlBuffer;
    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &htmlBuffer);
        CURLcode res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: Failed to download HTML" << std::endl;
            return 1;
        }
    } else {
        std::cerr << "Error: Failed to initialize CURL" << std::endl;
        return 1;
    }

    // htmlBufferの確認
    // cout << "htmlBuffer: \n" << htmlBuffer << endl;

    xmlDoc* doc = htmlReadMemory(htmlBuffer.c_str(), htmlBuffer.size(), nullptr, nullptr, HTML_PARSE_RECOVER);
    if (doc) {
        // XPath:/small/a, HTML:<small><a> を抽出
        xmlNodeSetPtr tradObj = executeXpath(doc, (xmlChar *)"//*[name()='small']/*[name()='a']");

        if (tradObj) {
            for (int i = 0; i < tradObj->nodeNr; i++) {
                xmlNodePtr node = tradObj->nodeTab[i];
                xmlChar* id0 = xmlGetProp(node, (xmlChar*)"href");
                xmlChar* title0 = xmlNodeGetContent(node);

                string id = convertToString(id0);
                id.erase(id.size() - 4);
                string title = convertToString(title0);

                idTitlePairs.push_back(std::make_pair(id, title));
                
                xmlFree(id0);
                xmlFree(title0);
            }
        }
        xmlFreeDoc(doc);
    }


    for (const auto& pair : idTitlePairs) {
        std::cout << "id: " << pair.first << std::endl;
        std::cout << "title: " << pair.second << std::endl;
    }

    return 0;
}

参考記事