[C++] 355 ChatGPTアプリの製作 その40 GPT-4 Turbo with visionへの対応

[Mac M2 Pro 12CPU, MacOS Ventura 13.6, clang 15.0.0]

自製ChatGPTアプリをGPT-4 Turbo with visionに対応させました。

画像のURLを送ると内容を読み取り、プロンプトに対応します。

漢字、ひらがな、カタカナなど非ラテン文字は読めません。これらが画像に含まれているとレスポンスしなくなるので、モザイクを掛けるなり前処理が必要です。

プログラミングでUIについて質問する際に画像を使うとやりやすいです。特に見てほしいところを色線で囲むなど、工夫を入れるとより深い分析をしてくれます。ここまでできるとプログラミングスクールにとどまらず教育業界全体が相当な危機でしょう。

GPT-4までは心強いパートナーという感じでしたが、with visionになって凄まじい眼力を身に付け、いささか脅威を覚えるようになりました。

今のAIがどのような状況になっているのか、もっと周知しないとヤバい気がします。

[C++] 349 FLTK : ChatGPTアプリの製作 その36 jsonファイルをフォルダに振り分ける

[Mac M2 Pro 12CPU, MacOS Ventura 13.5, clang 14.0.3]

自製ChatGPTアプリに関する記事は3月以来5ヶ月ぶりです。記事にはしていませんが、これまで何度も更新を重ねグレードアップしています。

ChatGPTアプリでのやりとりはjsonファイルとして保存しています。大分ファイルがたまってきたため、年月フォルダ(今月の場合は”2308″)を自動作成して振り分けるMovJSボタンを実装しました。

ChatGPTにコードを書いてもらいました。一応コードはチェックして問題なさそうなのでビルド&実行したところ一発で成功しました。

実装完了までものの10分です。AIアシストでプログラミングすると生産性爆上がりです。

#include <iostream>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

void movJSONCB(Fl_Widget*, void*){
    fs::path srcDir = "/ChatGPT";

    // srcDirにあるjsonファイルのpathをリスト化する
    std::vector<fs::path> jsonFiles;
    for (const auto& entry : fs::directory_iterator(srcDir)) {
        if (entry.path().extension() == ".json") {
            jsonFiles.push_back(entry.path());
        }
    }
    
    // ファイル名の先頭4文字のフォルダを作成する
    for (const auto& jsonFile : jsonFiles) {
        std::string folderName = jsonFile.filename().stem().string().substr(0, 4);
        fs::path folderPath = srcDir / folderName;
        
        // フォルダがすでに存在する場合は上書き作成しない
        if (!fs::exists(folderPath)) {
            fs::create_directory(folderPath);
        }
        
        // ファイルをそれぞれのフォルダに振り分ける
        fs::path destinationPath = folderPath / jsonFile.filename();
        fs::rename(jsonFile, destinationPath);
    }
}

[AI] GPT-4 APIの利用資格 2023/08

先月7月からOpenAIへ1ドル以上の支払実績のあるユーザーにGPT-4 API利用資格が付与されるようになりました。

低課金ユーザーの私は7月0.52ドル、8月1.5ドルの支払いでようやく資格を得ることができましたが、今のところGPT-4を使える状態になっていません。

モデルの利用可否はOpenAIサイトのPlaygroundで確認できます。

Bingで使った感触ではGPT-4は能書きが多いだけでコーディングのアシストには余り向いていない印象ですが、早く突っ込んだ検証をしたいです。

自製アプリはGPT-4に対応済み

23/08/09追記
23時の時点でGPT-4 APIを使えるようになっていました。これから検証に取り掛かります。

GPT-4 APIが使用可能になった

[C++] 342 BBS閲覧アプリの製作 その28 スレッドタイトル検索機能実装

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

これは簡単に実装できました。15分位でしょうか。

これで基本的な機能は一通り備わったように思います。ボードによってはまれにスレッドタイトル読込で落ちてしまったりしますが、ぼちぼち直していきます。

ソースファイルが13個になり分かりにくくなってきたので、複数フォルダへの振り分けを検討しています。

高度な検索にかけたり、過去ログも含めて管理するのでしたらMySQLなどデータベースの出番もあるのでしょうが、今の用途であれば都度2次元ベクター化で事足りますね。

#include "cppstd.h" // 自製ライブラリ
#include "MyTable.h"

extern vector<tuple<string, string, string, string>> numTitlePostnumID;
extern Fl_Input* searchInput;
extern MyTable* table;

void threadSearch(Fl_Widget *w, void*){
    vector<tuple<string, string, string, string>> searchResult;
    const char* word = searchInput -> value();

    for (const auto& tuple : numTitlePostnumID) {
        string title = std::get<1>(tuple);
        if (title.find(word) != string::npos) {
            searchResult.push_back(make_tuple(std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple), std::get<3>(tuple)));
        }
    }

    int hitNum = searchResult.size();
    numTitlePostnumID = searchResult;

    table->SetSize(hitNum, 5);
}

[C++] 341 BBS閲覧アプリの製作 その27 リンク画像をポップアップ表示 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

画像のURLをマウスオーバーするとぼかし画像がポップアップ表示され、クリックすると加工無しの画像がポップアップ表示されるようにしました。

ここが山場になるかと構えていましたが、思っていたよりもあっさりでした。それでもChatGPTを駆使しながら4時間程度掛かっています。

最も手こずったのはクリックして指定サイズでポップアップさせるところでしょうか。どうしても普通にリンクを踏む形になって大きいサイズでの埋め込み表示になってしまいました。結局、event.preventDefault()でデフォルトのリンク処理をスキップして解決しました。

たまにポップアップの後に影が残ったりしますが、後日対応することにします。

マウスオーバーでぼかし画像表示
クリックで元の画像表示

<html lang="ja">
<HEAD>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="styles.css" />
<script type="text/javascript" src="function.js"></script>
<STYLE TYPE="text/css">
    .popup-image {
        position: absolute;
        top: 0;
        left: 300px;
        width: 300px;
        height: auto;
        display: none;
        filter: blur(10px); // ぼかし加工
      }
    .popup-image2 {
        position: absolute;
        top: 0;
        left: 300px;
        width: 500px;
        height: auto;
        display: none;
    }
</STYLE>
</HEAD>
<BODY bgcolor="#ffffff">

<p id ='1'><font size="2" color="#008080">1 : テスト<br></font>
<font size="2" color="#000080">2023/07/22(土) 10:23:46.76 ID:xxx<br></font>
<font size="2" color="#000B00"> テスト <br> <a href="画像のURL" 
rel="noopener noreferrer" onmouseover="showImage(this,event);" onmouseout="hideImage();" 
onclick="showImage2(this,event);">画像のURL</a>
</font></p>

<script>
    // ぼかしあり画像表示
    function showImage(element,event) {
        var imageSrc = element.getAttribute("href");
        var imageElement = document.createElement("img");
        imageElement.src = imageSrc;
        imageElement.classList.add("popup-image");
        document.body.appendChild(imageElement);
        imageElement.style.display = "block";

        var rect = imageElement.getBoundingClientRect();
        var y1 = rect.top;
        console.log("Popup y1 = " + y1);

        var y2 = event.clientY;
        console.log("Popup y2 = " + y2);

        var y = -y1 + y2 -100;
        if (y < -y1){
            y = -y1;
        }
        console.log("Popup y = " + y);

        imageElement.style.top = y;
    }

    // ぼかしなし画像表示
    function showImage2(element,event) {
        event.preventDefault();

        var imageSrc = element.getAttribute("href");
        var imageElement = document.createElement("img");
        imageElement.src = imageSrc;
        imageElement.classList.add("popup-image2");
        document.body.appendChild(imageElement);
        imageElement.style.display = "block";

        var rect = imageElement.getBoundingClientRect();
        var y1 = rect.top;
        console.log("Popup y1 = " + y1);

        var y2 = event.clientY;
        console.log("Popup y2 = " + y2);

        var y = -y1 + y2 -100;
        if (y < -y1){
            y = -y1;
        }
        console.log("Popup y = " + y);

        imageElement.style.top = y;
    }
    
    // 画像消去
    function hideImage() {
        var popupImages = document.getElementsByClassName("popup-image");
        while (popupImages.length > 0) {
            var image = popupImages[0];
            image.parentNode.removeChild(image);
        }

        var popupImages2 = document.getElementsByClassName("popup-image2");
        while (popupImages2.length > 0) {
            var image2 = popupImages2[0];
            image2.parentNode.removeChild(image2);
        }
    }
</script>
  
</BODY></html>

[C++] 333 BBS閲覧アプリの製作 その19 レスアンカー先をポップアップ表示 Y座標の取得 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

前回の設定ではポップアップのTOP位置が絶対座標のゼロだったため、スクロールすると見えなくなってしまいます。

ホバーした時のFl_WebView上端Y座標を基準にTOP設定することで常にGUI上にポップアップが出現するようにしました。

Google Chromeの検証画面でY座標を出力させながら動作確認しました。生成するHTMLはサイズが倍になりましたが、表示速度は体感では変わらずでした。

今回はJavaScript初学者にはキツい内容でした。4時間ほど掛かったでしょうか。ChatGPTはまあまあ貢献してくれました。

Chrome検証画面で動作確認
<html lang="ja">
    <HEAD>
    <meta charset="utf-8">
    <STYLE TYPE="text/css">
    A:link {    color: #0000ff;
                text-decoration: none; }
    A:visited { color: #008080;
                text-decoration: none; }
    A:hover {   color: #ff0000;
                text-decoration: underline; }
    body {
        font-family:"Helvetica","ヒラギノ角ゴ";
        margin-left: 20px;
    }

    .tips {
        position: absolute;
        top: 0px;
        left: 100px;
        visibility:hidden;
        background-color: #E0FFFF;
        margin-left:2%;
        padding:1em;
    }

    </STYLE>
    <script>
        function showPopup(id) {
            if(document.getElementById){
                var element = document.getElementById(id);
                element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";

                var rect = element.getBoundingClientRect(); // ホバー時のFl_WebViewを取得する
                var y = rect.top; // 上端のY座標を取得する
                console.log("Popup y = " + y); // Y座標を出力

                element.style.top = -y + 20; // Y座標に-1を掛け20px下へずらした位置をTOPとする          
            }
        }
    </script>
<以下略>
// GUI表示用
<p><font size="2" color="#008080">5 : 投稿者名 <br>
</font><font size="2" color="#000080">2023/07/22(土) 18:48:05.94 ID:xxx<br>
</font><font size="2" color="#000B00"> <span onmouseover = "showPopup(2);" onmouseout = "showPopup(2);">
<a href="../test/read.cgi/xxx/2" rel="noopener noreferrer" target="_blank">>>2</a>
</span> <br> 本文 </font></p>

// ポップアップ用
<p id="5" class="tips"><font size="2" color="#008080">5 : 投稿者名 <br>
</font><font size="2" color="#000080">2023/07/22(土) 18:48:05.94 ID:xxx<br>
</font><font size="2" color="#000B00"> <span onmouseover = "showPopup(2);" onmouseout = "showPopup(2);">
<a href="../test/read.cgi/xxx/2" rel="noopener noreferrer" target="_blank">>>2</a>
</span> <br> 本文 </font></p>

[C++] 325 BBS閲覧アプリの製作 その11 スレッドタイトルのGUI表示 Fl_Table 本表示

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

スレッドタイトル一覧を表示させました。

自分なりに色々手を入れてC++らしくしたかったのですが上手くいかないので、C言語風味を残す形にしました。

今回のアプリ製作では最大の山場かもしれません。かなり手こずりました。

あとはスレッドタイトルをクリックして、右下にスレッド内容を表示させるだけです。

#include "cppstd.h" // 自製ライブラリ
#include "FLstd.h" // 自製ライブラリ
#include <WidgetTable.h>

extern vector<tuple<string, string, string, string>> numTitlePostnumID;
extern string id, num, title, postnum;

WidgetTable::WidgetTable(int x, int y, int w, int h, const char *l) : Fl_Table(x,y,w,h,l){
    col_header(1);
    col_resize(1);
    col_header_height(25);
    // row_header(1);
    row_resize(1);
    row_header_width(80);
    end();
}

WidgetTable::~WidgetTable(){}

void WidgetTable::SetSize(int newrows, int newcols) {
    clear();		// clear any previous widgets, if any
    rows(newrows);
    cols(newcols);

    col_width(0, 50);
    col_width(1, 1160-50-50-100);
    col_width(2, 50);
    col_width(3, 100);

    begin();		// start adding widgets to group
    {
      for ( int r = 0; r<newrows; r++ ) {
        for ( int c = 0; c<newcols; c++ ) {
            int X,Y,W,H;
            find_cell(CONTEXT_TABLE, r, c, X, Y, W, H);

            // switch文でも可
            if (c == 0) {
                char s[20];
                num = get<0>(numTitlePostnumID[r]);
                sprintf(s, "%s", num.c_str());
                Fl_Input *in = new Fl_Input(X,Y,W,H);
                in->value(s);
            } else if(c == 1) {
                char s[200];
                title = get<1>(numTitlePostnumID[r]);
                sprintf(s, "%s", title.c_str());
                Fl_Input *in = new Fl_Input(X,Y,W,H);
                in->value(s);
            } else if(c == 2){
                char s[20];
                postnum = get<2>(numTitlePostnumID[r]);
                sprintf(s, "%s", postnum.c_str());
                Fl_Input *in = new Fl_Input(X,Y,W,H);
                in->value(s);
            } else if (c == 3){
                char s[20];
                id = get<3>(numTitlePostnumID[r]);
                sprintf(s, "%s", id.c_str());
                Fl_Input *in = new Fl_Input(X,Y,W,H);
                in->value(s);
            }
        }
      }
    }
    end();
  }

void WidgetTable::draw_cell(TableContext context, 
			  int R, int C, int X, int Y, int W, int H) {
  switch ( context ) {
    case CONTEXT_STARTPAGE:
      fl_font(FL_HELVETICA, 12);		// font used by all headers
      break;

    case CONTEXT_RC_RESIZE: {
      int X, Y, W, H;
      int index = 0;

      for ( int r = 0; r<rows(); r++ ) {
        for ( int c = 0; c<cols(); c++ ) {
            if ( index >= children() ) break;
            find_cell(CONTEXT_TABLE, r, c, X, Y, W, H);
            child(index++)->resize(X,Y,W,H);
        }
      }
      init_sizes();			// tell group children resized
      return;
    }

    case CONTEXT_ROW_HEADER:
      fl_push_clip(X, Y, W, H);
      {
        static char s[40];
        sprintf(s, "Row %d", R);
        fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, row_header_color());
        fl_color(FL_BLACK);
        fl_draw(s, X, Y, W, H, FL_ALIGN_CENTER);
      }
      fl_pop_clip();
      return;

    case CONTEXT_COL_HEADER:
      fl_push_clip(X, Y, W, H);
      {
        if (C == 0){
            static char s[40];
            sprintf(s, "番号");
            fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, col_header_color());
            fl_color(FL_BLACK);
            fl_draw(s, X, Y, W, H, FL_ALIGN_CENTER);
        } else if (C == 1){
            static char s[40];
            sprintf(s, "タイトル");
            fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, col_header_color());
            fl_color(FL_BLACK);
            fl_draw(s, X, Y, W, H, FL_ALIGN_CENTER);
        } else if (C == 2){
            static char s[40];
            sprintf(s, "レス数");
            fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, col_header_color());
            fl_color(FL_BLACK);
            fl_draw(s, X, Y, W, H, FL_ALIGN_CENTER);
        } else if (C == 3){
            static char s[40];
            sprintf(s, "ID");
            fl_draw_box(FL_THIN_UP_BOX, X, Y, W, H, col_header_color());
            fl_color(FL_BLACK);
            fl_draw(s, X, Y, W, H, FL_ALIGN_CENTER);
        }
        
      }
      fl_pop_clip();
      return;

    case CONTEXT_CELL:
      return;		// fltk handles drawing the widgets

    default:
      return;
  }
}

[C++] 322 BBS閲覧アプリの製作 その8 ボードタイトルの取得 libxml, XPath

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

BBSのボードタイトルをカテゴリーと共に2次元vectorとして取得しました。

全てのaタグをnodeのリストとして取得し、aタグのhref、aタグの内容、すぐ上にあるbタグの内容(カテゴリー)の3要素をvectorにプッシュバックしています。bタグがない場合は”XXX”をカテゴリーとします。

2要素の場合はmake_pair関数かmake_tuple関数、3要素以上の場合はmake_tuple関数を使います。make_pairでは変数名.first、変数名.secondでそれぞれ呼び出すことができます。make_tupleの要素はstd::get<番号>(変数名)で取得します。

aタグから遡ってbタグを探す、などややこしめの課題を与えるとChatGPTからまともな答えがほぼ返ってこないのですが、今回は一発正解でした。

int main() {
    std::vector<std::tuple<std::string, std::string, std::string>> idTitleCat;
    std::string cat;

<中略>
    xmlDoc* doc = htmlReadMemory(htmlBuffer.c_str(), htmlBuffer.size(), nullptr, nullptr, HTML_PARSE_RECOVER);
    if (doc) {
        xmlNodeSetPtr boardObj = executeXpath(doc, (xmlChar *)"//*[name()='a']");

        if (boardObj) {
            for (int i = 0; i < boardObj->nodeNr; i++) {
                xmlNodePtr node = boardObj->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);

                xmlNodePtr catNode = node->prev;
                while (catNode != nullptr && xmlStrcmp(catNode->name, (const xmlChar*)"b") != 0) {
                    catNode = catNode->prev;
                }

                if (catNode != nullptr) {
                    xmlChar* cat0 = xmlNodeGetContent(catNode);
                    cat = convertToString(cat0);
                    xmlFree(cat0);
                } else {
                    cat = "XXX";
                }

                idTitleCat.push_back(std::make_tuple(id, title, cat));
                
                xmlFree(id0);
                xmlFree(title0);
            }
        }
        xmlFreeDoc(doc);
    }


    for (const auto& element : idTitleCat) {
        std::cout << "id: " << std::get<0>(element) << std::endl;
        std::cout << "title: " << std::get<1>(element) << std::endl;
        std::cout << "cat: " << std::get<2>(element) << std::endl;
    }

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

参考記事