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

(1/1)
C++で今日は何の日か調べる
Wikipedia および 一般社団法人 日本記念日協会にアクセスし、カレンダーで指摘した日付で起きた出来事や記念日を表示したり、ファイルに保存するプログラムを作る。「PHPで今日は何の日か調べる」「PHPで記念日を表示する」で作ったPHPプログラムを合体し、C++に移植したものである。画面上で並べ替えることもできる。

(2024年3月16日)使用ライブラリ更新
(2023年11月4日)注釈番号を除去,使用ライブラリ更新.
(2023年6月18日)使用ライブラリを更新.

目次

サンプル・プログラム

圧縮ファイルの内容
daytodaywin.msiインストーラ
bin/daytodaywin.exe実行プログラム本体
bin/libcurl-x64.dll 実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/daytodaywin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/mystrings.cpp汎用文字列処理関数など(ソース)
sour/mystrings.h汎用文字列処理関数など(ヘッダ)
sour/makefileビルド
daytodaywin.cpp 更新履歴
バージョン 更新日 内容
1.4.1 2024/03/16 使用ライブラリ更新
1.4.0 2023/11/04 注釈番号を除去,使用ライブラリ更新
1.3.4 2023/06/18 使用ライブラリを更新
1.3.3 2023/03/11 使用ライブラリを更新
1.3.2 2023/01/08 getDayToday() -- の取得パターンを変更した

使用ライブラリ

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" に設定する。

MSYS2 コマンドラインからビルドするのであれば、"makefile" を利用してほしい。

解説:ヘッダファイル等

  11: // 初期化処理 ======================================================
  12: #include <iostream>
  13: #include <fstream>
  14: #include <stdio.h>
  15: #include <stdlib.h>
  16: #include <tchar.h>
  17: #include <time.h>
  18: #include <sstream>
  19: #include <string>
  20: #include <iterator>
  21: #include <list>
  22: #include <regex>
  23: #include <locale>
  24: #include <winsock2.h>
  25: #include <windows.h>
  26: #include <commctrl.h>
  27: #include <richedit.h>
  28: #include <curl/curl.h>
  29: #include <boost/format.hpp>
  30: #include <boost/property_tree/xml_parser.hpp>
  31: #include "mystrings.h"
  32: #include "resource.h"
  33: 
  34: using namespace std;
  35: using namespace boost;
  36: using namespace boost::property_tree;
  37: 
  38: #define MAKER       "pahoo.org"             //作成者
  39: #define APPNAME     "daytodaywin"           //アプリケーション名
  40: #define APPNAMEJP   "今日は何の日"          //アプリケーション名(日本語)
  41: #define APPVERSION  "1.4.1"                 //バージョン
  42: #define APPYEAR     "2020-24"               //作成年・更新日
  43: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-11-01.shtm"   // 参考サイト
  44: 
  45: //char*バッファサイズ
  46: #define SIZE_BUFF       5120
  47: 
  48: //ListViewItemの最大文字長:変更不可
  49: #define MAX_LISTVIEWITEM    258
  50: 
  51: //リクエストURL(日本記念日協会)(※変更不可)
  52: #define KINENBI_URL     "https://www.kinenbi.gr.jp/"
  53: 
  54: //記念日識別値(※変更不可)
  55: #define KINENBI_YEAR    (-99999)
  56: 
  57: //現在のインターフェイス
  58: static HINSTANCE hInst;
  59: 
  60: //アプリケーション・ウィンドウ
  61: HWND hParent;
  62: 
  63: //アプリケーション・ウィンドウ位置
  64: unsigned hParent_X, hParent_Y;
  65: 
  66: //エラー・メッセージ格納用
  67: string ErrorMessage;
  68: 
  69: //ヘルプ・ファイル
  70: #define HELPFILE    ".\\etc\\help.chm"
  71: 
  72: //デフォルト保存ファイル名
  73: #define SAVEFILE    "daytodaywin.csv"
  74: 
  75: //UserAgent
  76: string UserAgent;

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

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

解説:データ構造

  78: //出来事格納用クラス
  79: #define SIZE_EVENTS     500     //格納上限
  80: class _Events {
  81: public:
  82:     wstring category = L"";     //区分
  83:     int year = 0;               //西暦年
  84:     wstring era = L"";          //元号
  85:     wstring event = L"";        //出来事
  86:     wstring plus = L"";         //追加情報
  87: };
  88: unique_ptr<_Events> Events[SIZE_EVENTS] = {};
  89: 
  90: //ソート用構造体
  91: struct _stEvents {
  92:     wchar_t* category;          //区分
  93:     int *year;                  //西暦年
  94:     wchar_t* event;             //出来事
  95:     size_t id;                  //Eventsの添字
  96: };
  97: vector<_stEvents> Vevents;

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

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

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

解説:Wikipedia URL取得

 320: /**
 321:  * Wikipedia URL取得
 322:  * @param   int month, day  月,日
 323:  * @param   char* url       URLを格納
 324:  * @param   size_t sz       urlの最大長
 325:  * @return  なし
 326: */
 327: void getURL_Wikipedia(int month, int day, char *url, size_t sz) {
 328:     const string wiki = "https://ja.wikipedia.org/wiki/";
 329:     static char buff1[SIZE_BUFF + 1], buff2[SIZE_BUFF + 1];
 330: 
 331:     snprintf(buff1, SIZE_BUFF, "%d月%d日", month, day);
 332:     string s1 = sjis_utf8(buff1);
 333: 
 334:     CURL *curl = curl_easy_init();
 335:     strncpy(buff2, curl_easy_escape(curl, (const char *)s1.c_str(), strlen(s1.c_str())), SIZE_BUFF);
 336: 
 337: 
 338:     string ss = wiki + (string)buff2;
 339:     strncpy(url, ss.c_str(), SIZE_BUFF);
 340: 
 341:     curl_easy_cleanup(curl);
 342: }

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

解説:今日は何の日 取得

 354: /**
 355:  * 今日は何の日 取得
 356:  * @param   int  month, day 月,日
 357:  * @param   bool lfn        脚注を残す(省略時=FALSE)
 358:  * @return  int 情報件数
 359: */
 360: int getDayToday(int month, int day, bool lfn=FALSE) {
 361:     //初期化
 362:     for (int i = 0i < SIZE_EVENTSi++) {
 363:         Events[i].reset();
 364:         Events[i] = NULL;
 365:     }
 366: 
 367:     //Wikipediaコンテンツ取得
 368:     char url[SIZE_BUFF];
 369:     string contents = "";
 370:     getURL_Wikipedia(month, day, (char *)url, SIZE_BUFF);
 371:     bool res = readWebContents(url, UserAgent, &contents);
 372:     if (res == FALSE) {
 373:         ErrorMessage = "Wikipediaにアクセスできません.";
 374:         return (-1);
 375:     }
 376: 
 377:     //コンテンツの解釈
 378:     setlocale(LC_ALL, "Japanese");
 379:     int cnt = -1;
 380:     stringstream ss;
 381:     string ss0;
 382:     wstring mode = L"";
 383:     wstring ws, ws2;
 384:     wstringstream wss;
 385:     static wsmatch mt1, mt2, mt3;
 386:     static smatch mt4;
 387:     wregex re1(_SW("<span[\\s\\S]+id=\"できごと\""));
 388:     wregex re2(_SW("<span[\\s\\S]+id=\"誕生日\""));
 389:     wregex re3(_SW("<span[\\s\\S]+id=\"忌日\""));
 390:     wregex re5(_SW("<li>[^\\>]+([0-9]+(年|世紀).+)<\\/li>"));
 391:     wregex re6(_SW("([^0-9]*)([0-9]+)年(\\s*[\((].[^\\))]+[)\\)].)?[ \\-]+(.+)"));   //年 - 出来事
 392:     wregex re7(_SW("([^0-9]*)([0-9]+)年(\\s*[\\((].[^\\))]+[)\\)].)?[ \\-]+([^、]+)、?([^(\\(]+)*([^0-9]*)([0-9]*)"));      //年 - 名前、職業(生没年)
 393:     wregex re8(_SW("<span[\\s\\S]+id=\"フィクションのできごと\">"));
 394:     wregex re9(_SW("^\\([\\*\\+]."));          //年 - 名前、職業(生没年)
 395:     wregex re11(_SW("紀元前"));
 396:     wregex re99(_SW("<span[\\s\\S]+class=\"vector-menu-heading-label\">他言語版"));     //1行読み込み打ち切り
 397: 
 398:     ss << contents;
 399:     while(ss && getline(ss, ss0)) {
 400:         //1行をwstring変換
 401:         ws = _UW(ss0);
 402:         //4000文字以上なら処理しない
 403:         if (ws.length() > 4000) {
 404:             continue;
 405:         }
 406:         //マッチング処理
 407:         if (regex_search(ws, mt1, re99)) {
 408:             break;
 409:         } else if (regex_search(ws, mt1, re1)) {
 410:             mode = _SW("出来事");
 411:         } else if (regex_search(ws, mt1, re8)) {
 412:             mode = _SW("架空");
 413:         } else if (regex_search(ws, mt1, re2)) {
 414:             mode = _SW("誕生");
 415:         } else if (regex_search(ws, mt1, re3)) {
 416:             mode = _SW("死亡");
 417:         } else if ((mode.length() !0&& (regex_search(ws, mt1, re5) == TRUE)) {
 418:             //HTMLタグ除去
 419:             ws = wstrip_tags(ws);
 420:             //注釈番号除去
 421:             ws = wstripAnnotations(ws);
 422:             //出来事
 423:             if ((mode == _SW("出来事")) && (regex_search(ws, mt2, re6) == TRUE)) {
 424:                 cnt++;
 425:                 Events[cnt] = make_unique<_Events>();
 426:                 Events[cnt]->category = mode;
 427:                 if (mt2[1].str().length() == 0) {
 428:                     Events[cnt]->year = atoi(_WS(mt2[2].str()).c_str());
 429:                 } else {
 430:                     Events[cnt]->year = 0 - atoi(_WS(mt2[2].str()).c_str());
 431:                 }
 432:                 Events[cnt]->era   = mt2[3].str();
 433:                 Events[cnt]->event = mt2[4].str();
 434: 
 435:             //誕生・死亡
 436:             } else if (regex_search(ws, mt2, re7)) {
 437:                 cnt++;
 438:                 Events[cnt] = make_unique<_Events>();
 439:                 Events[cnt]->category = mode;
 440:                 if (mt2[1].str().length() == 0) {
 441:                     Events[cnt]->year = atoi(_WS(mt2[2].str()).c_str());
 442:                 } else {
 443:                     Events[cnt]->year = 0 - atoi(_WS(mt2[2].str()).c_str());
 444:                 }
 445:                 Events[cnt]->era = mt2[3].str();
 446:                 ws2 = mt2[6].str();
 447:                 if (regex_search(ws2, mt3, re11) == TRUE) {
 448:                     Events[cnt]->plus = L"-" + mt2[7].str();
 449:                 } else if (regex_search(ws2, mt3, re9) == TRUE) {
 450:                     Events[cnt]->plus = mt2[7].str();
 451:                 } else {
 452:                     Events[cnt]->plus = L"";
 453:                 }
 454:                 wss << mt2[4].str() << _SW("("<< mt2[5].str();
 455:                 if (mode == _SW("誕生")) {
 456:                     wss << _SW(")誕生");
 457:                     if (Events[cnt]->plus.length() !0) {
 458:                         wss << _SW("(〜"<< Events[cnt]->plus << _SW("年)");
 459:                     }
 460:                 } else {
 461:                     wss << _SW(")死去");
 462:                     if (Events[cnt]->plus.length() !0) {
 463:                         wss << _SW("("<< Events[cnt]->plus << _SW("年〜")<< Events[cnt]->year << _SW("年)");
 464:                     }
 465:                 }
 466:                 Events[cnt]->event = wss.str();
 467:                 wss.str(L"");
 468:             }
 469:         }
 470:     }
 471:     return cnt;
 472: }

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

解説:記念日を取得

 474: /**
 475:  * 指定月日の記念日を取得する
 476:  * @param   int month, day 月日
 477:  * @param   int cnt 情報を追加する配列の最初の番号
 478:  * @return  int 情報の総数
 479: */
 480: int getAnniversary(int month, int day, int cnt=0) {
 481:     //コンテンツ読み込み
 482:     static char post[SIZE_BUFF + 1];
 483:     snprintf(post, SIZE_BUFF, "MD=%d&M=%d&D=%d", month, month, day);
 484:     string contents = "";
 485:     bool res = readWebContents(KINENBI_URL, UserAgent, &contents, post);
 486: 
 487:     if (res == FALSE) {
 488:         ErrorMessage = "日本記念日協会のサイトにアクセスできません.";
 489:         return (-1);
 490:     }
 491: 
 492:     //スクレイピング
 493:     wregex re1(_SW("<a\\s+class=\"winDetail\"\\s+href=\"([^\"]+)\"><font[^>]+>([^<]+)</font>"), wregex::icase);
 494:     static wsmatch mt1;
 495: 
 496:     stringstream ss;
 497:     wstring mode = _SW("記念日");
 498:     string ss0;
 499:     wstring ws;
 500:     ss << contents;
 501:     while(ss && getline(ss, ss0)) {
 502:         //1行をwstring変換
 503:         ws = _UW(ss0);
 504:         if (regex_search(ws, mt1, re1)) {
 505:             Events[cnt] = make_unique<_Events>();
 506:             Events[cnt]->category = mode;
 507:             Events[cnt]->event = mt1[2].str();
 508:             Events[cnt]->year  = KINENBI_YEAR;
 509:             cnt++;
 510:         }
 511:     }
 512: 
 513:     return cnt;
 514: }

日本記念日協会にアクセスし、指定した月日の記念日を取り出す。
PHPで記念日を表示する」で紹介した関数 scrapingAnniversarygetAnniversary を合体させた。
前述の getDayToday で取り込んだ情報配列 Events に記念日を追加していく。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header