C++ で今日は何の日か調べる

(1/1)
C++で今日は何の日か調べる
Wikipedia にアクセスし、カレンダーで指摘した日付で起きた出来事を表示したり、ファイルに保存するプログラムを作る。「PHP で今日は何の日か調べる(Windows アプリ版)」で作った PHP プログラムを C++に移植したものである。画面上で並べ替えることもできる。

目次

サンプル・プログラム

圧縮ファイルの内容
daytodaywin.msiインストーラ
bin/daytodaywin.exe実行プログラム本体
bin/libcrypto-1_1-x64.dll
bin/libcurl-x64.dll
bin/libssl-1_1-x64.dll
実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/daytodaywin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン

使用ライブラリ

Wikipediaにアクセスするために、オープンソースのライブラリ Boost C++ライブラリcURL (カール)  および OpenSSL が必要になる。導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

リソースの準備

Eclipse を起動し、新規プロジェクト daytodaywin を用意する。
ResEdit を起動し、resource.rc を用意する。

Eclipse に戻り、ソース・プログラム "daytodaywin.cpp" を追加する。
リンカー・フラグを -mwindows -static -lstdc++ -lgcc -lwinpthread -lcurl -lssl -llzma -lz -lws2_32 "C:\pleiades\eclipse\mingw\x86_64-w64-mingw32\bin\libcurl-x64.dll" に設定する。

解説:ヘッダファイル等

0011: // 初期化処理 ======================================================
0012: #include <iostream>
0013: #include <fstream>
0014: #include <stdio.h>
0015: #include <stdlib.h>
0016: #include <tchar.h>
0017: #include <time.h>
0018: #include <sstream>
0019: #include <string>
0020: #include <iterator>
0021: #include <list>
0022: #include <regex>
0023: #include <locale>
0024: #include <winsock2.h>
0025: #include <windows.h>
0026: #include <commctrl.h>
0027: #include <richedit.h>
0028: #include <curl/curl.h>
0029: #include <boost/format.hpp>
0030: #include "resource.h"
0031: 
0032: using namespace std;
0033: using namespace boost;
0034: 
0035: #define APPNAME     "今日は何の日"    //アプリケーション名
0036: #define APPVERSION "1.0"                    //バージョン
0037: #define APPYEAR     "2020"                    //作成年
0038: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-10-01.shtm"  // 参考サイト
0039: 
0040: //char*バッファサイズ
0041: #define SIZE_BUFF     512
0042: 
0043: //ListViewItemの最大文字長:変更不可
0044: #define MAX_LISTVIEWITEM 258
0045: 
0046: //現在のインターフェイス
0047: static HINSTANCE hInst;
0048: 
0049: //親ウィンドウ
0050: static HWND hParent;
0051: 
0052: //エラー・メッセージ格納用
0053: string ErrorMessage;
0054: 
0055: //ヘルプ・ファイル
0056: #define HELPFILE ".\\etc\\help.chm"
0057: 
0058: //デフォルト保存ファイル名
0059: #define SAVEFILE "daytodaywin.csv"

Windows の ListView クラスは、1 つのカラムに格納できるデータ長制限がある。これを定数 MAX_LISTVIEWITEM で定義しておき、ListView に代入するときに substr メソッドを使ってこの範囲に収まるようにする。

その他、とくに注意記載が無い限り、定数は自由に変更できる。

解説:データ構造

0061: //出来事格納用クラス
0062: #define SIZE_EVENTS     500        //格納上限
0063: class _Events {
0064: public:
0065:     wstring category = L"";     //区分
0066:     int year = 0;                //西暦年
0067:     wstring era = L"";          //元号
0068:     wstring event = L"";        //出来事
0069:     wstring plus = L"";         //追加情報
0070: };
0071: unique_ptr<_EventsEvents[SIZE_EVENTS] = {};
0072: 
0073: //ソート用構造体
0074: struct _stEvents {
0075:     wchar_tcategory;           //区分
0076:     int *year;                   //西暦年
0077:     wchar_tevent;              //出来事
0078:     size_t id;                   //Eventsの添字
0079: };
0080: vector<_stEventsVevents;

取得した HTML コンテンツを PHP の連想配列のようにして使うために、C++スマートポインタ unique_ptr を導入する。
コンテンツの必要な部分をクラス _Events に収め、このクラスの実体であるオブジェクトへのポインタに unique_p 配列 tr を利用する。出来事の数が未知なので、オブジェクトは必要に応じて動的に確保する(つまり、配列の要素数は不定)。

データ要素としては、日本語テキストを格納する要素にはワイド文字列型(wstring 型)を割り当てた。西暦年は、intg 型で格納しておく。

クラス _Events のメンバのうち、ソート可能な項目(文字列)へのポインタを vector クラス へコピーする背景については、「C++ で Google ニュース検索」で述べたとおりである。

解説:Wikipedia URL取得

0422: /**
0423:  * Wikipedia URL取得
0424:  * @param int month, day 月,日
0425:  * @param char* url      URLを格納
0426:  * @param size_t sz       urlの最大長
0427:  * @return なし
0428: */
0429: void getURL_Wikipedia(int monthint daychar *urlsize_t sz) {
0430:     const string wiki = "https://ja.wikipedia.org/wiki/";
0431:     static char buff1[SIZE_BUFF + 1], buff2[SIZE_BUFF + 1];
0432: 
0433:     snprintf(buff1SIZE_BUFF, "%d月%d日", monthday);
0434:     string s1 = sjis_utf8(buff1);
0435: 
0436:     CURL *curl = curl_easy_init();
0437:     strncpy(buff2curl_easy_escape(curl, (const char *)s1.c_str(), strlen(s1.c_str())), SIZE_BUFF);
0438: 
0439: 
0440:     string ss = wiki + (string)buff2;
0441:     strncpy(urlss.c_str(), SIZE_BUFF);
0442: 
0443:     curl_easy_cleanup(curl);
0444: }

Wikipedia には「×月×日」という見出しがあり、ここから「今日は何の日」を取得する。
見出し URL を作成するユーザー関数が getURL_Wikipedia である。

解説:今日は何の日 取得

0452: /**
0453:  * 今日は何の日 取得
0454:  * @param int  month, day    月,日
0455:  * @param bool lfn       脚注残す(省略時=FALSE)
0456:  * @return int 情報件数
0457: */
0458: int getDayToday(int monthint daybool lfn=FALSE) {
0459:     //初期化
0460:     for (int i = 0; i < SIZE_EVENTSi++) {
0461:         Events[i].reset();
0462:         Events[i] = NULL;
0463:     }
0464: 
0465:     //cURLによる結果取得
0466:     CURL *curl;
0467:     CURLcode res = (CURLcode)0;
0468:     curl = curl_easy_init();
0469:     string chunk;
0470:     char url[SIZE_BUFF];
0471:     getURL_Wikipedia(monthday, (char *)urlSIZE_BUFF);
0472: 
0473:     //cURLによるWikipediaコンテンツ取得
0474:     if (curl) {
0475:         curl_easy_setopt(curlCURLOPT_URLurl);
0476:         curl_easy_setopt(curlCURLOPT_SSL_VERIFYPEER, 0);
0477:         curl_easy_setopt(curlCURLOPT_WRITEFUNCTIONcallBackFunk);
0478:         curl_easy_setopt(curlCURLOPT_WRITEDATA, (string*)&chunk);
0479:         res = curl_easy_perform(curl);
0480:         curl_easy_cleanup(curl);
0481:     }
0482:     if (res != CURLE_OK) {
0483:         ErrorMessage = "Wikipediaのアクセスエラー";
0484:         return (-1);
0485:     }
0486: 
0487:     //コンテンツの解釈
0488:     setlocale(LC_ALL, "Japanese");
0489:     int cnt = -1;
0490:     stringstream ss;
0491:     string ss0;
0492:     wstring mode = L"";
0493:     wstring wsws2;
0494:     wstringstream wss;
0495:     static wsmatch mt1mt2mt3;
0496:     wregex re1(_SW("<span[\\s\\S]+id=\"できごと\">"));
0497:     wregex re2(_SW("<span[\\s\\S]+id=\"誕生日\">"));
0498:     wregex re3(_SW("<span[\\s\\S]+id=\"忌日\">"));
0499:     wregex re5(_SW("<li>[^\\>]+([0-9]+年.+)<\\/li>"));
0500:     wregex re6(_SW("([^0-9]*)([0-9]+)年(\\s*[\((].[^\\))]+[)\\)].)?[ \\-]+(.+)"));    //年 - 出来事
0501:     wregex re7(_SW("([^0-9]*)([0-9]+)年(\\s*[\\((].[^\\))]+[)\\)].)?[ \\-]+([^、]+)、?([^(\\(]+)*([^0-9]*)([0-9]*)"));      //年 - 名前、職業(生没年)
0502:     wregex re8(_SW("<span[\\s\\S]+id=\"フィクションのできごと\">"));
0503:     wregex re9(_SW("^\\([\\*\\+]."));         //年 - 名前、職業(生没年)
0504: 
0505:     ss << chunk;
0506:     while(ss && getline(ssss0)) {
0507:         //1行をwstring変換
0508:         ws = _UW(ss0);
0509:         if (regex_search(wsmt1re1)) {
0510:             mode = _SW("出来事");
0511:         } else if (regex_search(wsmt1re8)) {
0512:             mode = _SW("架空");
0513:         } else if (regex_search(wsmt1re2)) {
0514:             mode = _SW("誕生");
0515:         } else if (regex_search(wsmt1re3)) {
0516:             mode = _SW("死亡");
0517:         } else if ((mode.length() != 0) && (regex_search(wsmt1re5) == TRUE)) {
0518:             ws = wstrip_tags(ws);
0519:             //出来事
0520:             if ((mode == _SW("出来事")) && (regex_search(wsmt2re6) == TRUE)) {
0521:                 cnt++;
0522:                 Events[cnt] = make_unique<_Events>();
0523:                 Events[cnt]->category = mode;
0524:                 if (mt2[1].str().length() == 0) {
0525:                     Events[cnt]->year = atoi(_WS(mt2[2].str()).c_str());
0526:                 } else {
0527:                     Events[cnt]->year = 0 - atoi(_WS(mt2[2].str()).c_str());
0528:                 }
0529:                 Events[cnt]->era   = mt2[3].str();
0530:                 Events[cnt]->event = mt2[4].str();
0531: 
0532:             //誕生・死亡
0533:             } else if (regex_search(wsmt2re7)) {
0534:                 cnt++;
0535:                 Events[cnt] = make_unique<_Events>();
0536:                 Events[cnt]->category = mode;
0537:                 if (mt2[1].str().length() == 0) {
0538:                     Events[cnt]->year = atoi(_WS(mt2[2].str()).c_str());
0539:                 } else {
0540:                     Events[cnt]->year = 0 - atoi(_WS(mt2[2].str()).c_str());
0541:                 }
0542:                 Events[cnt]->era = mt2[3].str();
0543:                 ws2 = mt2[6].str();
0544:                 if (regex_search(ws2mt3re9) == TRUE) {
0545:                     Events[cnt]->plus = mt2[7].str();
0546:                 } else {
0547:                     Events[cnt]->plus = L"";
0548:                 }
0549:                 wss << mt2[4].str() << _SW("") << mt2[5].str();
0550:                 if (mode == _SW("誕生")) {
0551:                     wss << _SW(")誕生");
0552:                     if (Events[cnt]->plus.length() != 0) {
0553:                         wss << _SW("(〜") << Events[cnt]->plus << _SW("年)");
0554:                     }
0555:                 } else {
0556:                     wss << _SW(")死去");
0557:                     if (Events[cnt]->plus.length() != 0) {
0558:                         wss << _SW("") << Events[cnt]->plus << _SW("年〜")<< Events[cnt]->year << _SW("年)");
0559:                     }
0560:                 }
0561:                 Events[cnt]->event = wss.str();
0562:                 wss.str(L"");
0563:             }
0564:         }
0565:     }
0566:     return cnt;
0567: }

取得する URL が決まったら、cURL 関数群を使って全コンテンツを変数 chunk に代入する。
次に、この内容をスクレイピングしていくのだが、今回は、ワイド文字列に対する正規表現を使うことにした。ソースは SJIS で書いているので、ユーザーマクロ関数 _SW を使ってワイド文字列に変換し、これを使って正規表現によるパターンマッチングを行う。ちなみに、C++に正規表現が正式導入されたのは C++11 からで、Boost C++ライブラリをベースにしている。
スクレイピング用パターン
パターン名内容
re1できごと
re2誕生日
re3忌日
re5西暦年
re6西暦年(出来事)
re7年 - 名前、職業(生没年)
re8フィクションのできごと
re9年 - 名前、職業(生没年)
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header