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

<以下略>

[C++,Python] 316 BBS閲覧アプリの製作 その2 スレッドタイトルの取得 Python編

[M1 Mac, MacOS Ventura 13.3.1, Python 3.10.4]

スレッドタイトルとスレッドIDを取得するPythonスクリプトを書きました。

次はこのスクリプトをC++へ変換したいです。難しくなるようであればモジュール化します。

機能はPythonで下書きしてC++へ変換あるいはモジュール化、GUIはFLTK(C++)でコーディングしていきます。

import requests
import re
from bs4 import BeautifulSoup

# URLからHTMLファイルを取り込む
url = 'スレッドタイトル表示URL(HTMLファイル)'
response = requests.get(url)
response.encoding = response.apparent_encoding
html = response.text

# BeautifulSoupを使用してHTMLを解析する
soup = BeautifulSoup(html, 'html.parser')

# <small id="trad">タグで囲まれた部分の内容を取得する
trad_tags = soup.find_all('small', id='trad')

thread_list = []
for trad_tag in trad_tags:
    content = trad_tag.get_text()

    # <a>タグのhref属性の値を正規表現で取得する
    pattern = r'<a\s+href=[\'"]([^\'"]+)[\'"]'
    href = re.findall(pattern, str(trad_tag))

    # contentをaタグごとに分割してリストにする
    content_list = re.split(r'<a\s+href=[\'"][^\'"]+[\'"]', str(trad_tag))
    
    print(f"hrefの要素数: {len(href)}")
    print(f"content_listの要素数: {len(content_list)}")

    # 辞書型データのリストを作成する
    for i in range(len(href)):
        thread_list.append({'href': (href[i])[:-4], 'content': (content_list[i+1].strip().replace("</a>","").replace("</small>",""))[1:]})

# リストの内容を出力する
for thread in thread_list:
    print(thread['href'])
    print(thread['content'])

[C++,Python] 315 BBS閲覧アプリの製作 その1 DATファイルの保存

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

とあるBBSのDATファイルへのアクセスが可能になったようなので、早速遊んでみることにしました。

とりあえずDATファイルをダウンロードしてみます。PythonスクリプトをChatGPTに変換してもらったコードがそのまま使えました。

DATファイルの文字コードがシフトJISですから、Macの場合はUTF-8に変換する必要がありますね。

#include <iostream>
#include <fstream>
#include <curl/curl.h>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
    std::ofstream* file = static_cast<std::ofstream*>(userp);
    file->write(static_cast<char*>(contents), size * nmemb);
    return size * nmemb;
}

int main() {
    std::string url = "DATファイルのurl";
    std::string filename = "保存先DATファイルのパス";

    CURL* curl = curl_easy_init();
    if (curl) {
        std::ofstream file(filename, std::ios::binary);
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: " << curl_easy_strerror(res) << std::endl;
        }
        curl_easy_cleanup(curl);
    } else {
        std::cerr << "Failed to initialize curl" << std::endl;
    }

    return 0;
}

[Swift] 40 ChatGPTアプリ製作 その4 Apple Watchで使用

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

Apple WatchでもChatGPTを使えるようにしました。一問一答形式です。

ただしiCloudにデータを上手く保存できず、それぞれのデバイスのローカルに保存されます。

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2224): <NSCloudKitMirroringDelegate: 0x600003d780e0> - Attempting recovery from error: <CKError 0x600000a14630: 
"Partial Failure" (2/1011); "Failed to modify some record zones"; 
"Server Rejected Request"

[Swift] 39 Apple WatchのComplication改良 複数のComplication作成

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

自製カレンダーアプリのComplicationを純正アプリと同様にラベル有りとラベル無しの2種類作成しました。

TargetとしてWidget Extensionを増やすだけなので特に問題なくできますが、作成時に”Include Configuration Intent”にチェックを入れないようにします。チェックを入れるとそのままではコマンドの重複が発生しビルドエラーになります。

Complication選択画面
@main
struct DateToolComplicationLabel: Widget {
    let kind: String = "DateToolComplicationLabel"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            DateToolComplicationLabelEntryView(entry: entry)
        }
        .configurationDisplayName("Date ラベル付き")
        .description("")
        .supportedFamilies([.accessoryCircular,.accessoryCorner,.accessoryRectangular,.accessoryInline])
     }

}

[Swift] 38 メモアプリ製作 その11 Apple Watchで新規メモ作成

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

メモアプリのwatchOS版では既存のメモを編集できるだけでした。さらに新規メモを作成できるようにしました。TextFieldなので1行の文字列として入力することになります。

後でiPhoneなどで改行を入れ整形する必要がありますが、とりあえずメモしたい時に便利です。

ToolbarItemのplacementを.automaticにして正常に配置できました。本当はプラスボタンをbottomに置きたいのですが、watchOS 9では不可のようです。watchOS 10で新機能として追加されるという情報を目にしました。本当だとしたらありがたいです。

これで大体完成と言ったところでしょうか。

struct ContentView: View {
    @Environment(\.managedObjectContext)var viewContext

    @FetchRequest(
    entity: Note.entity(),
    sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
    private var contents: FetchedResults<Note>

    var body: some View {
        NavigationView{
            List{
                ForEach(contents){content in
                    NavigationLink{
                        if((content.content?.isEmpty) == false){
                            DraftAppleWatch(text:content.content!, note: content)
                        }
                    }label:{
                        if((content.content?.isEmpty) == false){
                            Text(content.content!)
                        }
                    }
                }
                .onDelete(perform:deleteContent)
            }
            .navigationTitle("メモリスト")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                ToolbarItem(placement:.automatic){
                    NavigationLink{
                        DraftAppleWatch()
                    }label:{
                        Text("+")
                        .font(.system(size: 24))
                    }
                }
            }
        }
        .accentColor(.blue)
    }
<以下略>

[Swift] 37 Apple WatchのComplication改良 しばらく経つと消える 原因判明

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

自製カレンダーアプリのComplicationがしばらく経ってアプリごと消えてしまう原因が判明しました。

どうやらXcodeのバグのようです。

iOSのWatchアプリを開いて”APPLE WATCH上にインストール済み”が表示された瞬間にそこからカレンダーアプリが消えていくのを目撃しました。再現性も確認しています。

この画面になった瞬間にアプリが消えていった

アプリそのものに関する設定に問題があるのではと考え、調べていくとBundle Identiferが不完全な表記になっているのが分かりました。これはwatchOSアプリのプロジェクト作成時に正しく設定しても反映されていないことを意味します。

最初にiOSアプリのプロジェクトを作成し、watchOSアプリ、Widget Extensionと順にターゲットを増やしていくとこのような問題は起こりません。メモアプリはこの手順だったためトラブルにはなりませんでした。

さすがにこれは酷すぎるのでAppleにバグ報告します。

正直こんなことで振り回されたくない。Flutterでこの種の不毛な作業をせずに済むのであれば本気で移行したいです。

アプリのカテゴリーは異なりますが、VSCodeでこのような不具合はあり得ないです。Xcodeへの信頼を著しく損なう事になり残念至極。

プロジェクト作成時はBundle Identiferに特に問題はない
後でBundle Identiferを確認すると.watchkitappに勝手に変わっている
これでは固有IDとして機能しない

[Swift] 36 Apple WatchのComplication改良 しばらく経つと消える

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

カレンダーアプリのComplicationがしばらく経つとアプリごと消えてしまいます。同様にComplication設定しているメモアプリは無事です。

“インフォグラフ”文字盤の中央上部は純正カレンダー以外のアプリを受け付けないのかもしれません。

中央上部は純正カレンダー、右下は自製カレンダーに設定して様子を見ます。

これで消えたら以下の対策を順次試してみます。
1.プロジェクトから作り直し
2.CloudKitを導入しDateなどを適当に保存
3.文字盤を作成
4.正式にアプリ登録する(非公開)

中央上部と右下に設定
しばらくすると消える
(経過時間は不定)
この設定で様子を見る
struct ComplicationCircular : View {
    @Environment(\.showsWidgetLabel) var showsWidgetLabel
    var entry: Provider.Entry
    
    var body: some View {
        VStack (spacing: -6){
            if showsWidgetLabel {
                Text(getWeekday(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.yellow)
                .widgetLabel {
                    Text(getFormattedDate() + getFormattedWeekday() + getFormattedYear())
                    .foregroundColor(.blue) // 中央上部は色設定不可
                }
                
                Text(getMonth(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.green)
                
                Text(getDay(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.white)
                
            } else {
                Text(getWeekday(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.yellow)
                
                Text(getMonth(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.green)
                
                Text(getDay(entry.date))
                .font(.system(size: 18))
                .foregroundColor(.white)
            }
        }
    }
<以下略>

[Swift] 35 Apple WatchのComplication改良 accessoryCorner

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

ComplicationのaccessoryCornerにも日付を表示できるようにしました。

ただし現時点では文字盤に沿って円弧状に表示できるのはラベル[23/07/08(土)]だけで本体[令5]は水平のままです。

詳しくは書けませんが、次期watchOSで何らかの進化があるようです。

中央上部と右下に表示
struct ComplicationCorner : View {
    var entry: Provider.Entry
    
    var body: some View {
        Text(getFormattedYear())
        .font(.system(size: 22))
            .foregroundColor(.green)
            .widgetLabel {
                Text(getFormattedDate() + getFormattedWeekday())
                .foregroundColor(.yellow)
            }
     }
    
    func getFormattedDate() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yy/MM/dd"
        return dateFormatter.string(from: entry.date)
    }
    
    func getFormattedWeekday() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return "(" + dateFormatter.string(from: entry.date) + ")"
    }
    
    func getFormattedYear() -> String {
        let calendar = Calendar(identifier: .japanese)
        let year = calendar.component(.year, from: entry.date)
        return "令" + String(year)
    }
    
}

struct DateToolComplicationEntryView : View {
    @Environment(\.widgetFamily) var widgetFamily
    var entry: Provider.Entry
    
    var body: some View {
        switch widgetFamily {
            case .accessoryCorner:
                ComplicationCorner(entry: entry)
            case .accessoryCircular:
                ComplicationCircular(entry: entry)
            case .accessoryInline:
                ComplicationInline()
            case .accessoryRectangular:
                ComplicationRectangular()
            @unknown default:
                Text("Not an implemented widget yet")
        }
    }
}