[C++] 326 BBS閲覧アプリの製作 その12 レスのGUI表示 Fl_Text_Display

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

レスを右下に表示できるようになりました。

シフトJISのdatファイルとして一旦保存してしまうと、UTF-8への変換が自分のスキルではかなり難しいので、文字列の状態で変換してからファイルにしました。macOSは日本語を扱う上で不利になることが多いです。

これで基本機能の実装は完了です。だらだら作って4日半でした。GUIは丸一日掛かりました。

レス表示欄は生データそのままなのでかなり見づらいです。テキスト整形は今後の課題とします。

extern vector<tuple<string, string, string, string>> numTitlePostnumID;
extern string id, num, title, postnum, boardURL;
extern vector<tuple<string, string, string>> urlNameCat;
extern Fl_Text_Display* contentDisplay;

void button_cb(Fl_Widget *w, void*) {
    string selectID;
    const char* label = w->label();

    cout << "label: " << label << endl;

    for (const auto& tuple : numTitlePostnumID) {
        if (std::get<1>(tuple) == label) {
            selectID = std::get<3>(tuple);
        }
    }

    string url = boardURL + "dat/" + selectID + ".dat";
    std::string datBuffer;

    string filename = "/Users/[ユーザ名]/BBS_Browser/" + selectID + ".dat";

    cout << "url: " << url << endl;
    cout << "filename: " << filename << endl;

    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, &datBuffer);
        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: " << curl_easy_strerror(res) << std::endl;
            return;
        }

        curl_easy_cleanup(curl);

    } else {
        std::cerr << "Failed to initialize curl" << std::endl;

        return;
    }

    // 文字コードをCP932からUTF-8へ変換
    datBuffer = ConvertShiftJISToUTF8(datBuffer);

    std::ofstream file(filename, std::ios::binary);
    file << datBuffer;
    file.close();

    // ファイル内容表示
    std::ifstream ifile(filename);
    if (!ifile.is_open()) {
        cout << "ファイルopen失敗" << endl;

        return;
    }

    Fl_Text_Buffer* buffer = new Fl_Text_Buffer();
    contentDisplay->buffer(buffer);

    string line;
    while (std::getline(ifile, line)) {
        buffer->append(line.c_str());
        buffer->append("\n");
    }

    // 現在の行数を取得
    int lineCount = contentDisplay -> buffer() -> count_lines(0, contentDisplay -> buffer() -> length());
    // 最終行が見えるようにスクロールを設定
    contentDisplay -> scroll(lineCount, 0);

    ifile.close();

    return;
}

[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++] 324 BBS閲覧アプリの製作 その10 スレッドタイトルのGUI表示 Fl_Table 仮表示

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

Fl_Tableを継承したWidgetTableクラスを使い、スレッドタイトル一覧表示の土台を作成しました。

クラスの作成は久しぶりだったので、少々手こずりました。

FLTKについてはChatGPTは余りあてにならず、ソースコードにあるサンプルを参考にしています。

ようやくゴールが見えてきた感じです。

#include "cppstd.h" // 自製ライブラリ
#include <FLstd.h>
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Tree.H>
#include "getBoard.h"
#include "WidgetTable.h"

int main(int argc, char *argv[]) {
  Fl_Double_Window *win = new Fl_Double_Window(1400, 1020, "BBS Browser");

  win->begin();
  {
    // Create the tree
    Fl_Tree* tree = new Fl_Tree(10, 45, 230, win->h()-45-10);
    tree->showroot(0);

    vector<tuple<string, string, string>> idTitleCat = getBoard();
    vector<string> categories;

    for (const auto& element : idTitleCat) {
        string path = get<2>(element) + "/" + get<1>(element);
        cout << "path: " << path << endl;

        string category = get<2>(element);
    
        if (find(categories.begin(), categories.end(), category) == categories.end()) {
            tree->add(category.c_str())->close();
            categories.push_back(category);
        }

        tree->add(path.c_str());
    }

    WidgetTable* table = new WidgetTable(240, 45, 1400-230-10, 350, "");
    table->SetSize(5, 5);

  }
  win->end();

  win->resizable(win);
  win->show(argc, argv);
  return(Fl::run());
}

[C++] 323 BBS閲覧アプリの製作 その9 ボードタイトルのGUI表示 Fl_Tree

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

いよいよGUIの作成に取りかかります。

FLTKのFl_Treeを使ってボードタイトルをカテゴリーごとにツリー表示できるようにしました。

今回はChatGPTをほとんど使わず、FLTKのtestコードを参考にしました。

なお、自製ライブラリcppstd.hでusing std::stringなどを設定しているのでstd::は省略されています。そうしないと可読性が著しく低くなります。

C++版がある程度形になったらSwiftへの移植も考えています。

#include "cppstd.h" // 自製ライブラリ
#include <stdio.h>
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Tree.H>
#include "getBoard.h"

int main(int argc, char *argv[]) {
  Fl_Double_Window *win = new Fl_Double_Window(1400, 1050, "BBS Browser");
  win->begin();
  {
    // Create the tree
    Fl_Tree *tree = new Fl_Tree(10, 10, 230, win->h()-20);
    tree->showroot(0);

    vector<tuple<string, string, string>> idTitleCat = getBoard();
    vector<string> categories;

    for (const auto& element : idTitleCat) {
        string path = get<2>(element) + "/" + get<1>(element);
        cout << "path: " << path << endl;

        string category = get<2>(element);
    
        if (find(categories.begin(), categories.end(), category) == categories.end()) {
            tree->add(category.c_str())->close();
            categories.push_back(category);
        }

        tree->add(path.c_str());

    }

  }
  win->end();
  win->resizable(win);
  win->show(argc, argv);
  return(Fl::run());
}

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

参考記事

[C++] 319 BBS閲覧アプリの製作 その5 スレッドタイトルの取得 libxmlでパース

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

libxmlを使ったスクレイピングに成功しました。

ただしPythonのBeautifulSoup & パーサー(html.parserなど)によるものとは違い、文字列比較で絞り込んでいく原始的な手法です。

今回の方法ではUTF-8への変換は不要でした。

IDやXPathを指定してパースできるのか、今後調査します。

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

std::vector<std::pair<std::string, std::string>> idTitlePairs;

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

void parseAnchorTags(xmlNode* node) {
    for (xmlNode* cur = node; cur; cur = cur->next) {
        if (cur->type == XML_ELEMENT_NODE && xmlStrcmp(cur->name, (const xmlChar*)"small") == 0) {
            xmlChar* id = xmlGetProp(cur, (const xmlChar*)"id");
            if (xmlStrcmp(id, (const xmlChar*)"trad") == 0) {
                for (xmlNode* child = cur->children; child; child = child->next) {
                    if (child->type == XML_ELEMENT_NODE && xmlStrcmp(child->name, (const xmlChar*)"a") == 0) {
                        xmlChar* id0 = xmlGetProp(child, (const xmlChar*)"href");
                        xmlChar* title0 = xmlNodeListGetString(child->doc, child->children, 1);
                        // std::cout << "id: " << id0 << std::endl;
                        // std::cout << "title: " << title0 << std::endl;

                        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);
                    }
                }
            }
            xmlFree(id);
        }
        parseAnchorTags(cur->children);
    }
}

// コールバック関数
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;
    }

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

    xmlDoc* doc = htmlReadMemory(htmlBuffer.c_str(), htmlBuffer.size(), nullptr, nullptr, HTML_PARSE_RECOVER);
    if (doc) {
        xmlNode* root = xmlDocGetRootElement(doc);
        parseAnchorTags(root);
        xmlFreeDoc(doc);
    }

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

    return 0;
}

[C++] 318 BBS閲覧アプリの製作 その4 スレッドタイトルの取得 HTMLパーサーを使わず文字列処理

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

tidy-html5やlibxml2といった有名どころのHTMLパーサーを使おうとしたものの、上手く使いこなせません。

文字列加工関数や正規表現を使ってスレッドIDとスレッドタイトルを2次元vectorにまとめました。

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <curl/curl.h>
#include <iconv.h>

// コールバック関数
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;
}

std::string ConvertShiftJISToUTF8(const std::string& input) {
    std::string output;

    iconv_t cd = iconv_open("UTF-8", "CP932");
    if (cd == (iconv_t)-1) {
        std::cerr << "Error: Failed to open iconv" << std::endl;
        return output;
    }

    size_t inBytes = input.size();
    size_t outBytes = inBytes * 4; // 変換後の最大バイト数を予測して確保する

    char* inBuf = const_cast<char*>(input.c_str());
    char* outBuf = new char[outBytes];
    char* outPtr = outBuf;

    if (iconv(cd, &inBuf, &inBytes, &outPtr, &outBytes) == (size_t)-1) {
        std::cerr << "Error: Failed to convert encoding: " << strerror(errno) << std::endl;
        delete[] outBuf;
        iconv_close(cd);
        return output;
    }

    output.assign(outBuf, outPtr - outBuf);

    delete[] outBuf;
    iconv_close(cd);

    return output;
}

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

    // 文字コードをCP932(Microsoftの拡張Shift-JIS)からUTF-8へ変換
    htmlBuffer = ConvertShiftJISToUTF8(htmlBuffer);

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

    // smallタグ部分を抽出
    std::string delimiter = "<small id=\"trad\">";
    htmlBuffer = htmlBuffer.substr(htmlBuffer.find(delimiter) + delimiter.length());
    delimiter = "</small>";
    htmlBuffer = htmlBuffer.substr(0, htmlBuffer.find(delimiter));

    // std::cout << "抽出内容:\n" << htmlBuffer << std::endl;

    // 正規表現を使ってaタグ内のhrefとaタグの内容を抽出 (.*?)部分
    std::regex pattern("<a.*?href=\"(.*?)/l50\".*?>(.*?)</a>");
    std::smatch matches;

    std::string::const_iterator searchStart(htmlBuffer.cbegin());
    std::vector<std::pair<std::string, std::string>> idTitlePairs;
    while (std::regex_search(searchStart, htmlBuffer.cend(), matches, pattern)) {
        std::string id = matches[1];
        std::string title = matches[2];
        idTitlePairs.push_back(std::make_pair(id, title));
        searchStart = matches.suffix().first;
    }

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

    return 0;
}

[C++] 317 BBS閲覧アプリの製作 その3 スレッドタイトルの取得 curl

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

まずcurlライブラリを使ってHTMLファイルを取り込みます。

文字コードがシフトJISであってもMicrosoftの拡張シフトJISの場合はCP932を使わないと文字化けを起こします。

#include <curl/curl.h>
#include <iconv.h>

std::string ConvertShiftJISToUTF8(const std::string& input) {
    std::string output;

    iconv_t cd = iconv_open("UTF-8", "CP932"); // SHIFT-JISではエラーになった
    if (cd == (iconv_t)-1) {
        std::cerr << "Error: Failed to open iconv" << std::endl;
        return output;
    }

    size_t inBytes = input.size();
    size_t outBytes = inBytes * 4; // 変換後の最大バイト数を予測して確保する

    char* inBuf = const_cast<char*>(input.c_str());
    char* outBuf = new char[outBytes];
    char* outPtr = outBuf;

    if (iconv(cd, &inBuf, &inBytes, &outPtr, &outBytes) == (size_t)-1) {
        std::cerr << "Error: Failed to convert encoding: " << strerror(errno) << std::endl;
        delete[] outBuf;
        iconv_close(cd);
        return output;
    }

    output.assign(outBuf, outPtr - outBuf);

    delete[] outBuf;
    iconv_close(cd);

    return output;
}

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

    // 文字コードをCP932(Microsoftの拡張Shift-JIS)からUTF-8へ変換
    htmlBuffer = ConvertShiftJISToUTF8(htmlBuffer);

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

<以下略>