C++ で国立国会図書館検索

(1/1)
C++で国立国会図書館検索
PHPで国立国会図書館を検索」で作った検索プログラムをC++に移植する。検索結果を画面上で並べ替えたり、CSVファイルに保存することができる。

(2024年11月16日)使用ライブラリ更新
(2024年7月27日)使用ライブラリ更新
(2024年3月9日)API 1.1版に対応,使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
searchndl.msiインストーラ
bin/searchndl.exe実行プログラム本体
bin/libcurl-x64.dll 実行時に必要になるDLL
bin/etc/help.chmヘルプ・ファイル
sour/searchndl.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル
sour/application.icoアプリケーション・アイコン
sour/makefileビルド
searchndl.cpp 更新履歴
バージョン 更新日 内容
1.3.2 2024/11/16 使用ライブラリ更新
1.3.1 2024/07/27 使用ライブラリ更新
1.3.0 2024/03/09 API 1.1版に対応,使用ライブラリ更新
1.2.4 2023/10/14 使用ライブラリ更新
1.2.3 2023/06/10 使用ライブラリ更新,ISBN検索できない不具合修正

国立国会図書館サーチ

国立国会図書館が用意するWebAPI「国立国会図書館サーチ(OpenSearch)」を利用する。API仕様は「WebAPI:国立国会図書館サーチ(OpenSearch)」をご覧いただきたい。

使用ライブラリ

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

リソースの準備

Eclipse を起動し、新規プロジェクト searchndl を用意する。
ResEdit を起動し、resource.rc を用意する。
Eclipse に戻り、ソース・プログラム "searchndl.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" を利用してほしい。

解説:ヘッダファイル等

searchndl.cpp

  11: // 初期化処理 ======================================================
  12: #include <iostream>
  13: #include <stdio.h>
  14: #include <stdlib.h>
  15: #include <tchar.h>
  16: #include <time.h>
  17: #include <sstream>
  18: #include <regex>
  19: #include <string>
  20: #include <winsock2.h>
  21: #include <windows.h>
  22: #include <shlobj.h>
  23: #include <commctrl.h>
  24: #include <richedit.h>
  25: #include <curl/curl.h>
  26: #include <boost/property_tree/xml_parser.hpp>
  27: #include <boost/format.hpp>
  28: #include "resource.h"
  29: 
  30: using namespace std;
  31: using namespace boost;
  32: using namespace boost::property_tree;
  33: 
  34: #define MAKER       "pahoo.org"             //作成者
  35: #define APPNAME     "gsearchndl"            //アプリケーション名
  36: #define APPNAMEJP   "国立国会図書館検索"    //アプリケーション名(日本語)
  37: #define APPVERSION  "1.3.2"                 //バージョン
  38: #define APPYEAR     "2020-2024"             //作成年
  39: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-09-01.shtm"   // 参考サイト
  40: #define REFERENCE_NDL   "https://iss.ndl.go.jp/information/api/"    //国立国会図書館APIについて
  41: 
  42: //国立国会図書館API
  43: #define URL_NDLAPI  "https://ndlsearch.ndl.go.jp/api/opensearch?";
  44: 
  45: //char*バッファサイズ
  46: #define SIZE_BUFF       5120
  47: 
  48: //現在のインターフェイス
  49: static HINSTANCE hInst;
  50: 
  51: //アプリケーション・ウィンドウ
  52: HWND hParent;
  53: 
  54: //アプリケーション・ウィンドウ位置
  55: unsigned hParent_X, hParent_Y;
  56: 
  57: //エラー・メッセージ格納用
  58: string ErrorMessage;
  59: 
  60: //検索キー格納用
  61: string Query;
  62: 
  63: //ヘルプ・ファイル
  64: #define HELPFILE    ".\\etc\\help.chm"
  65: 
  66: //デフォルト保存ファイル名
  67: #define SAVEFILE    "searchndl.csv"
  68: 
  69: //UserAgent
  70: string UserAgent;

cURLOpenSSLiconvBoost C++ライブラリから "format.hpp" を利用する関係で、includeするヘッダ・ファイルが多くなっている。

その他の定数は、自由に変更できる。

解説:データ構造

searchndl.cpp

  72: //書籍情報格納用クラス
  73: #define SIZE_BOOKS      500     //格納上限
  74: class _Books {
  75: public:
  76:     bool flag = false;
  77:     char title[SIZE_BUFF + 1] = {};         //書籍名
  78:     char author[SIZE_BUFF + 1] = {};        //作者
  79:     char publisher[SIZE_BUFF + 1] = {};     //出版社
  80:     char pubdate[SIZE_BUFF + 1] = {};       //出版日
  81:     char link[SIZE_BUFF + 1] = {};          //リンクURL
  82:     char isbn[SIZE_BUFF + 1] = {};          //ISBN番号
  83:     char ndc9[SIZE_BUFF + 1] = {};          //NDC番号
  84: };
  85: unique_ptr<_Books> Books[SIZE_BOOKS] = {};
  86: 
  87: //ソート用構造体
  88: struct _stBooks {
  89:     char *title;        //書籍名
  90:     char *author;       //作者
  91:     char *publisher;    //出版社
  92:     char *pubdate;      //出版日
  93:     char *isbn;         //ISBN番号
  94:     char *ndc9;         //NDC番号
  95:     char *link;         //リンクURL
  96: };
  97: vector<_stBooks> Vbooks;

WebAPIの応答(RSSファイル)をPHPの連想配列のようにして使うために、「C++でGoogleニュース検索」で作ったデータ構造を流用する。

解説:国立国会図書館サーチAPIのURLを取得する

searchndl.cpp

 611: /**
 612:  * 国立国会図書館サーチAPI のURLを取得する
 613:  * @param   char* query     タイトル(SJIS;部分一致)
 614:  *                         またはISBN(10桁または13桁;完全一致または前方一致)
 615:  *                         【省略不可】
 616:  * @param   char* creater   作成者(SJIS;部分一致)
 617:  * @param   char* url       URLを格納
 618:  * @param   size_t sz       urlの最大長
 619:  * @return  なし
 620: */
 621: void getURL_searchNDL(char *query, char *creater, char *url, size_t sz) {
 622:     const string ndl = URL_NDLAPI;
 623:     static char buff1[SIZE_BUFF + 1], buff2[SIZE_BUFF + 1];
 624: 
 625:     strncpy(buff1, sjis_utf8((string)query).c_str(), SIZE_BUFF);
 626: 
 627:     CURL *curl = curl_easy_init();
 628:     strncpy(buff2, curl_easy_escape(curl, (const char *)buff1, strlen(buff1)), SIZE_BUFF);
 629: 
 630:     //ISBNパターン
 631:     regex re("^[0-9]+$");
 632:     smatch mt;
 633:     string ss;
 634:     ss = (string)buff2;
 635:     if (regex_match(ss, mt, re)) {
 636:         ss = ndl + "isbn=" + (string)buff2;
 637:     //書名
 638:     } else {
 639:         ss = ndl + "title=" + (string)buff2;
 640:     }
 641:     strncpy(url, ss.c_str(), SIZE_BUFF);
 642: 
 643:     curl_easy_cleanup(curl);
 644: }

国立国会図書館サーチAPIのURLを生成する関数が getURL_searchNDL である。
前述のユーザー関数 sjis_utf8 を使って検索キーワードをUTF-8に変換した後、cURLライブラリにある curl_easy_escape 関数を使ってURLエスケープする。なお、cURLライブラリを使い始めるには curl_easy_init 関数を、終了するには curl_easy_cleanup 関数を呼び出す必要がある。

解説:検索結果を配列に格納する

searchndl.cpp

 652: /**
 653:  * 検索結果を配列に格納する
 654:  * @param   array $item 配列
 655:  * @param   int   $i    カウンタ
 656:  * @return  int 検索結果の件数
 657: */
 658: int searchBooks(char *title) {
 659:     //初期化
 660:     for (int i = 0i < SIZE_BOOKSi++) {
 661:         Books[i].reset();
 662:         Books[i] = NULL;
 663:     }
 664: 
 665:     //cURLによる結果取得
 666:     CURL *curl;
 667:     CURLcode res = (CURLcode)0;
 668:     curl = curl_easy_init();
 669:     string chunk;
 670:     char url[SIZE_BUFF];
 671:     getURL_searchNDL(title, (char *)"", (char *)url, SIZE_BUFF);
 672: 
 673:     //cURLによる国立国会図書館サーチAPI
 674:     if (curl) {
 675:         curl_easy_setopt(curl, CURLOPT_URL, url);
 676:         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
 677:         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callBackFunk);
 678:         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (string*)&chunk);
 679:         res = curl_easy_perform(curl);
 680:         curl_easy_cleanup(curl);
 681:     }
 682:     if (res !CURLE_OK) {
 683:         ErrorMessage = "国立国会図書館サーチAPIのエラー";
 684:         return (-1);
 685:     }
 686: 
 687:     //XML読み込み
 688:     std::stringstream ss;
 689:     ss << chunk;
 690:     ptree pt;
 691:     xml_parser::read_xml(ss, pt);
 692: 
 693:     //XML解釈
 694:     ptree tree;
 695:     struct tm dt;
 696:     char buff[SIZE_BUFF + 1];
 697:     int cnt = -1;
 698:     for (auto it : pt.get_child("rss.channel")) {
 699:         //item以外は読み飛ばし
 700:         if (it.first !"item")     continue;
 701:         cnt++;
 702:         Books[cnt] = make_unique<_Books>();
 703: 
 704:         //書籍名
 705:         if (optional<string>title = it.second.get_optional<string>("title")) {
 706:             strncpy(Books[cnt]->title, utf8_sjis((string)(title->c_str())).c_str(), SIZE_BUFF);
 707:         }
 708:         //巻数
 709:         if (optional<string>volume = it.second.get_optional<string>("dcndl:volume")) {
 710:             strncpy(buff, utf8_sjis((string)(volume->c_str())).c_str(), SIZE_BUFF);
 711:             strcat(Books[cnt]->title, " ");
 712:             strcat(Books[cnt]->title, buff);
 713:             strncat(Books[cnt]->title, (char *)"巻", SIZE_BUFF);
 714:         }
 715: 
 716:         //作者
 717:         if (optional<string>creator = it.second.get_optional<string>("dc:creator")) {
 718:             strncpy(Books[cnt]->author, utf8_sjis((string)(creator->c_str())).c_str(), SIZE_BUFF);
 719:         }
 720:         if (optional<string>author = it.second.get_optional<string>("author")) {
 721:             if (*Books[cnt]->author == 0) {
 722:                 strncpy(Books[cnt]->author, utf8_sjis((string)(author->c_str())).c_str(), SIZE_BUFF);
 723:             }
 724:         }
 725: 
 726:         //出版社
 727:         if (optional<string>publisher = it.second.get_optional<string>("dc:publisher")) {
 728:             strncpy(Books[cnt]->publisher, utf8_sjis((string)(publisher->c_str())).c_str(), SIZE_BUFF);
 729:         }
 730: 
 731:         //出版日
 732:         if (optional<string>pubdate = it.second.get_optional<string>("pubDate")) {
 733:             {
 734:             istringstream ss(pubdate->c_str());
 735:             ss >> get_time(&dt, "%a, %d %b %Y");
 736:             strftime(buff, SIZE_BUFF, "%Y-%m-%d", &dt);
 737:             }
 738:             strcpy(Books[cnt]->pubdate, buff);
 739:         }
 740:         if (optional<string>dcterms = it.second.get_optional<string>("dcterms:issued")) {
 741:             if (*Books[cnt]->pubdate == 0) {
 742:                 if (it.second.get_optional<string>("dcterms:issued.<xmlattr>.xsi:type") == (string)"dcterms:W3CDTF") {
 743:                     strncpy(Books[cnt]->pubdate, dcterms->c_str(), SIZE_BUFF);
 744:                 }
 745:             }
 746:         }
 747: 
 748:         //リンクURL
 749:         if (optional<string>link = it.second.get_optional<string>("link")) {
 750:             strncpy(Books[cnt]->link, link->c_str(), SIZE_BUFF);
 751:         }
 752: 
 753:         //ISBN
 754:         if (optional<string>isbn = it.second.get_optional<string>("dc:identifier")) {
 755:             if (it.second.get_optional<string>("dc:identifier.<xmlattr>.xsi:type") == (string)"dcndl:ISBN") {
 756:                 strncpy(Books[cnt]->isbn, isbn->c_str(), SIZE_BUFF);
 757:             }
 758:         }
 759: 
 760:         //NDC9
 761:         if (optional<string>ndc9 = it.second.get_optional<string>("dc:subject")) {
 762:             if (it.second.get_optional<string>("dc:subject.<xmlattr>.xsi:type") == (string)"dcndl:NDC9") {
 763:                 strncpy(Books[cnt]->ndc9, ndc9->c_str(), SIZE_BUFF);
 764:             }
 765:         }
 766:     }
 767:     chunk.clear();
 768: 
 769:     return cnt;
 770: }

検索結果を _Books 配列へ格納するユーザー関数が searchBooks である。
まず、前述のユーザー関数 getURL_searchNDL とcURLライブラリを使い、RSSファイルをstring変数 chunk に読み込む。

次に、RSSはXMLファイルの一種であるから、「C++でCPU情報を取得」で紹介した方法を使ってXML解釈を進める。

解説:ニュース記事のソート

_Books 配列をソートする方法は、「C++でGoogleニュース検索」で作ったソート方法を流用する。

解説:検索結果をCSVファイルに保存

_Books 配列をCSVファイルに保存する方法は、「C++でGoogleニュース検索」で作った保存方法を流用する。

解説:ニュース一覧作成

_Books 配列の内容は ListView クラスを使って一覧表示する。「C++でGoogleニュース検索」で作った一覧表示方法を流用する。

解説:イベントハンドラ:メインウィンドウ

searchndl.cpp

1064: /**
1065:  * イベントハンドラ:メインウィンドウ
1066:  * @param   HWND hDlg           親ウィンドウ・ハンドラ
1067:  * @paramm  UINT uMsg           メッセージ識別子
1068:  * @param   WPARAM wParam       メッセージの最初のパラメータ
1069:  * @paramL  PARAM lParam        メッセージの2番目のパラメータ
1070:  * @return  INT_PTR CALLBACK    TRUE:メッセージ処理完了/FALSE:未完了
1071: */
1072: INT_PTR CALLBACK processMain(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
1073:     HICON hIcon;
1074:     char *str;
1075:     LVITEM item;
1076:     LV_HITTESTINFO lvinfo;
1077:     NM_LISTVIEW *pNMLV;
1078:     TCHAR buff[SIZE_BUFF + 1];
1079:     static int subsort[100];
1080: 
1081:     switch(uMsg) {
1082:     //ダイアログ初期化
1083:     case WM_INITDIALOG:
1084:         hParent = hDlg;
1085:         hIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(IDI_ICON), IMAGE_ICON, 16, 16, 0);
1086:         SendMessage(hParent, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
1087:         ErrorMessage = "";
1088:         //オプション読み込み
1089:         loadParameter();
1090:         setStrEditBox(hDlg, IDC_EDIT_QUERY, Query);
1091:         //アプリケーション・ウィンドウ移動
1092:         SetWindowPos(hParent, NULL, hParent_X, hParent_Y, 0, 0, (SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER));
1093:         makeListViewFrame(GetDlgItem(hDlg, IDC_LISTVIEW_BOOKS));
1094:         break;
1095: 
1096:     //ボタン押下
1097:     case WM_COMMAND:
1098:          switch (LOWORD(wParam)) {
1099:         //実行
1100:         case IDM_EXEC:
1101:         case IDC_BUTTON_SEARCH:
1102:             ErrorMessage = "";
1103:             //カーソルを砂時計に
1104:             SetCursor(LoadCursor(NULL, IDC_WAIT));
1105:             Query = getStrEditBox(hDlg, IDC_EDIT_QUERY);
1106:             //書籍検索
1107:             searchBooks((char *)Query.c_str());
1108:             sortBooks(SORT_NONE);
1109:             //一覧表示
1110:             makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_BOOKS));
1111:             setStrEditBox(hDlg, IDC_TEXT_ERROR, ErrorMessage);
1112:             break;
1113:         //保存
1114:         case IDC_BUTTON_SAVE:
1115:         case IDM_SAVE:
1116:             saveCSV();
1117:             break;
1118:         //設定クリア+アプリ終了
1119:         case IDM_CLEAR_PARAMETER:
1120:             delParameter();
1121:             EndDialog(hParent, 0);
1122:             return 0;
1123:             break;
1124:         //ヘルプ
1125:         case IDM_HELP:
1126:             ShellExecute(hParent, _T("open"), _T(HELPFILE), NULL, NULL, SW_RESTORE);
1127:             break;
1128:         //バージョン表示
1129:         case IDM_VERSION:
1130:             createHelp(hParent, processHelp);
1131:             break;
1132:         //技術情報
1133:         case IDM_PAHOO:
1134:             ShellExecute(NULL, _T("open"), _T(REFERENCE), NULL, NULL, SW_RESTORE);
1135:             break;
1136:         //国立国会図書館APIについて
1137:         case IDM_NDL:
1138:             ShellExecute(NULL, _T("open"), _T(REFERENCE_NDL), NULL, NULL, SW_RESTORE);
1139:             break;
1140:         //コピー
1141:         case IDM_COPY:
1142:             setClipboardData(getStrEditBox(hParent, IDC_EDIT_QUERY));
1143:             break;
1144:         //貼り付け
1145:         case IDM_PASTE:
1146:             str = getClipboardData();
1147:             if (str !NULL) {
1148:                 setStrEditBox(hParent, IDC_EDIT_QUERY, str);
1149:             }
1150:             break;
1151:         //切り取り
1152:         case IDM_DELETE:
1153:             setStrEditBox(hParent, IDC_EDIT_QUERY, "");
1154:             break;
1155:         //プログラム終了
1156:         case IDM_QUIT:
1157:             //オプション保存
1158:             saveParameter();
1159:             EndDialog(hParent, 0);
1160:             return FALSE;
1161:         default:
1162:             return 1;
1163:         }
1164:         break;
1165: 
1166:     //通知
1167:     case WM_NOTIFY:
1168:         switch(((LPNMHDR)lParam)->idFrom) {
1169:         case IDC_LISTVIEW_BOOKS:
1170:             switch (((LPNMLISTVIEW)lParam)->hdr.code) {
1171:             //一覧のラベルがクリック
1172:             case LVN_COLUMNCLICK:
1173:                 pNMLV = (NM_LISTVIEW *)lParam;
1174:                 if (subsort[pNMLV->iSubItem] == SORT_ASC) {
1175:                     subsort[pNMLV->iSubItem] = SORT_DEC;
1176:                 } else {
1177:                     subsort[pNMLV->iSubItem] = SORT_ASC;
1178:                 }
1179:                 //書籍一覧の並べ替え
1180:                 sortBooks(pNMLV->iSubItem * 2 + subsort[pNMLV->iSubItem]);
1181:                 //一覧表示
1182:                 makeListView(GetDlgItem(hDlg, IDC_LISTVIEW_BOOKS));
1183:                 break;
1184:             //書籍一覧の1行を選択
1185:             case LVN_ITEMCHANGED:
1186:             //書籍一覧の1行をダブルクリック
1187:             //case NM_DBLCLK:
1188:                 GetCursorPos((LPPOINT)&lvinfo.pt);
1189:                 ScreenToClient(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &lvinfo.pt);
1190:                 ListView_HitTest(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &lvinfo);
1191:                 if ((lvinfo.flags & LVHT_ONITEM!0) {
1192:                     item.mask = TVIF_HANDLE | TVIF_TEXT;
1193:                     item.iItem = lvinfo.iItem;
1194:                     item.iSubItem = COL_LINK;
1195:                     item.pszText = buff;
1196:                     item.cchTextMax = SIZE_BUFF;
1197:                     ListView_GetItem(((LPNMLISTVIEW)lParam)->hdr.hwndFrom, &item);
1198:                     //ブラウザ起動
1199:                     ShellExecute(hParent, _T("open"), _T(buff), NULL, NULL, SW_RESTORE);
1200:                 }
1201:                 break;
1202:             default:
1203:                 break;
1204:             }
1205:             break;
1206:         default:
1207:             break;
1208:         }
1209:         break;
1210: 
1211:     //プログラム終了
1212:     case WM_CLOSE:
1213:         //オプション保存
1214:         saveParameter();
1215:         EndDialog(hParent, 0);
1216:         return FALSE;
1217:     }
1218:     return FALSE;
1219: }

メインウィンドウに関わるイベント・ハンドラが processMain である。いくつかのボタンとメニューは共通の機能を担っている。
「C++でGoogleニュース検索」で作ったイベントハンドラ:メインウィンドウを流用している。

解説:Windowsメインプログラム

searchndl.cpp

1222: /**
1223:  * Windowsメインプログラム
1224:  * @param   HINSTANCE hInstance         インスタンスハンドル
1225:  * @paramm  HINSTANCE hPrevInstance     未使用(常にNULL):Win16時代の名残
1226:  * @param   LPSTR lpCmdLine             コマンドライン引数
1227:  * @paramL  int nShowCmd                ウィンドウの表示方法
1228:  * @return  int リターンコード
1229: */
1230: int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
1231:     LoadLibrary("RICHED20.DLL");
1232: 
1233:     //UserAgent生成
1234:     static OSVERSIONINFOEX os;
1235:     GetVersion2(&os);
1236:     UserAgent = (string)"Mozilla/5.0 (" + APPNAME + "/"
1237:         + APPVERSION + "/" + MAKER
1238:         + ", Windows NT " + to_string(os.dwMajorVersion+ "."
1239:         + to_string(os.dwMinorVersion+ ")";
1240: 
1241:     hInst = hInstance;
1242:     DialogBox(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, (DLGPROC)processMain);
1243:     return 0;
1244: }

検索キーワードを入力するエディットボックスは、コントロールキーが使えるように RichEdit にしてある。プログラムの冒頭で "RICHED20.DLL" を読み込んでやる必要がある。

参考サイト

(この項おわり)
header