
目次
サンプル・プログラム
normalizetextwin.msi | インストーラ |
bin/normalizetextwin.exe | 実行プログラム本体(GUI版) |
bin/nmtxt.exe | 実行プログラム本体(CUI版) |
bin/option.txt | 正規化オプションを保存するファイル |
bin/libgcc_s_seh-1.dll bin/libiconv-2.dll bin/libmecab-2.dll bin/libstdc++-6.dll bin/libwinpthread-1.dll | 実行時に必要になるDLL |
bin/etc/help.chm | ヘルプ・ファイル |
bin/etc/char.bin bin/etc/dicrc bin/etc/matrix.bin bin/etc/sys.dic bin/etc/unk.dic bin/etc/user_wiki.dic | MeCabが参照する辞書ファイル等 |
sour/normalizetextwin.cpp | ソース・プログラム |
sour/resource.h | リソース・ヘッダ |
sour/resource.rc | リソース・ファイル(GUI版) |
sour/resource2.rc | リソース・ファイル(CUI版) |
sour/application.ico | アプリケーション・アイコン(GUI版) |
sour/application2.ico | アプリケーション・アイコン(CUI版) |
sour/pahooNormalizeText.cpp | テキスト正規化クラス(ソース) |
sour/pahooNormalizeText.hpp | テキスト正規化クラス(ヘッダ) |
使用ライブラリ
また、CUI版でコマンダイン・オプションを操作する場合などに、オープンソースのライブラリ Boost C++ライブラリが必要になる。Boost C++ライブラリの導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。
リソースの準備
ResEdit を起動し、resource.rc を用意する。

Eclipse に戻り、ソース・プログラム "normalizetextwin.cpp" を追加する。
リンカー・フラグを -mwindows -static-libstdc++ -static-libgcc -lpthread -lwinpthread -static "(任意のパス)\libboost_program_options-mt.dll" -static "(任意のパス)\libmecab-2.dll" に設定する。
また、CUI版をビルドするために、構成 CMD を追加し、リンカー・フラグを -static-libstdc++ -static-libgcc -lpthread -static "(任意のパス)\libboost_program_options-mt.dll" -static "(任意のパス)\libmecab-2.dll"" に設定する。
MeCabの導入
簡単に振り返っておくと、入力されたテキストを日本語の品詞に分解する形態素解析エンジンと呼ばれるプログラムである。
なぜテキストの正規化に形態素解析エンジンが必要かというと、たとえば「千と千尋の神隠しという映画を見た」というテキストをそのまま正規化してしまうと、「千」を数字と解釈し、「1000 と 1000 尋の神隠しという映画を見た」という変換結果になってしまうためである。
テキスト中にある固有名詞は数字変換の対象としないように除外するため、形態素解析エンジンを利用する。形態素解析エンジンであれば MeCab でなくても構わないのだが、C++から利用可能な DLL ファイルが利用できることから、ここでは MeCab を採用する。

MeCab を 64 ビット環境でビルドすると、"libmecab-2.dll" が得られる。これを適当なディレクトリに配置し、リンクするようにする。

MeCab のユーザー辞書であるが、前述の通り、固有名詞を識別するのが目的であるので、Wikipedia の見出し語をユーザー辞書として用意することにした。Wikipedia ユーザー辞書の作るプログラムは、前述の「PHP で MeCab のユーザー辞書を作成する」で紹介している。
プログラムの流れ

変換ボタンがクリックされたら、toHankaku メソッドを使って半角か可能な文字を全て半角にする。その後、正規化オプションの値に応じて全角化、漢数字か化を行ってゆく。
解説:テキスト正規化クラス
0029: //正規化オプション
0030: #define OPTION_SPC_TRIM1 't' //行頭・行末の空白文字を除く
0031: #define OPTION_SPC_TRIM2 'T' //全角文字と隣り合う空白文字を除く
0032: #define OPTION_NUM_HAN 'n' //数字を半角に統一
0033: #define OPTION_NUM_ZEN 'N' //数字を全角に統一
0034: #define OPTION_NUM_KAN 'K' //数字を漢字に統一
0035: #define OPTION_NUM_KAN2 'k' //数字を漢字(単純)に統一
0036: #define OPTION_ALP_HAN 'a' //英字を半角に統一
0037: #define OPTION_ALP_ZEN 'A' //英字を全角に統一
0038: #define OPTION_YAK_HAN 'y' //記号を半角に統一
0039: #define OPTION_YAK_ZEN 'Y' //記号を全角に統一
0040: #define OPTION_KANA_HAN 'h' //カタカナを半角に統一
0041: #define OPTION_KANA_ZEN 'H' //カタカナを全角に統一
0042: #define OPTION_SPEC_HAN 's' //特殊文字を半角に統一
0043: #define OPTION_SPEC_ZEN 'S' //特殊文字を全角に統一
0044: #define OPTION_NUM_SCALE 'F' //数字を位取り記法にする
0045: #define OPTION_NUM_NOSCALE 'f' //数字を位取り記法にしない
0046:
0047: //正規化オプションの初期値
0048: #define OPTION_INIT "anYSHF"
解説:MeCabの呼び出し
0114: private:
0115: MeCab::Tagger *tagger = MeCab::createTagger("--dicdir=etc --userdic=etc/user_wiki.dic --node-format=%M\\t%f[0],%f[1],%f[2],%f[3],%f[4],%f[5],%f[6],%f[7],%f[8]\\n --unk-format=%M\\t%f[0],%f[1],%f[2],%f[3],%f[4],%f[5]\\n");
ここで、前述の Wikipedia ユーザー辞書 "user_wiki.dic" などを指定する。
解説:半角数字を漢数字に変換
0177: /**
0178: * 半角数字を漢数字に変換する
0179: * @param wstring instr 半角数字
0180: * 小数、負数に対応;指数表記には未対応
0181: * カンマは削除
0182: * @return wstring 漢数字
0183: */
0184: wstring pahooNormalizeText::num2kanji(wstring instr) {
0185: static wchar_t kantbl1[] =
0186: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
0187: L'8', L'9', L'.', L'-' };
0188: static wchar_t kantbl2[] =
0189: { 0x0000, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, //一〜九
0190: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; //.−
0191: static wchar_t kantbl3[] = { 0x0000, 0x5341, 0x767E, 0x5343 }; //十百千
0192: static wchar_t kantbl4[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; //万億兆京
0193:
0194: wstring outstr = L"";;
0195: wstring ws2;
0196: wchar_t wch1, wch2;
0197: int m = (int)instr.length() / 4;
0198: //一、万、億、兆‥‥の繰り返し
0199: for (int i = 0; i <= m; i++) {
0200: ws2 = L"";
0201: //一、十、百、千の繰り返し
0202: for (int j = 0; j < 4; j++) {
0203: int pos = instr.length() - i * 4 - j - 1;
0204: if (pos >= 0) {
0205: wchar_t* wch = (wchar_t*)instr.substr(pos, 1).c_str();
0206: if (*wch == L',') continue; //カンマは無視
0207: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
0208: //漢数字 or 半角数字のまま
0209: wch1 = 0x0000;
0210: if (*wch == kantbl1[k]) {
0211: wch1 = kantbl2[k];
0212: break;
0213: }
0214: }
0215: wch2 = 0x0000;;
0216: if ((j >= 0) && (j <= 3)) {
0217: wch2 = kantbl3[j];
0218: }
0219:
0220: //冒頭が「一」の場合の処理
0221: if (wch1 != 0x0000) {
0222: if ((wch1 == 0x4E00) && (wch2 != 0x0000)) {
0223: ws2 = (wstring){wch2} + ws2;
0224: } else if (wch2 != 0x0000) {
0225: ws2 = (wstring){wch1} + (wstring){wch2} + ws2;
0226: } else {
0227: ws2 = (wstring){wch1} + ws2;
0228: }
0229: }
0230: }
0231: }
0232: if (ws2 != L"") {
0233: if (kantbl4[i] == 0x0000) {
0234: outstr = ws2 + outstr;
0235: } else {
0236: outstr = ws2 + (wstring){kantbl4[i]} + outstr;
0237: }
0238: }
0239: }
0240:
0241: return outstr;
0242: }
右から左へ向かって逆順に処理し、4 桁ごとに、万、億、兆、京の単位を付けてゆく。各々の 4 桁の中で、十、百、千を付ける。冒頭が「一」の場合は「一百」にならないよう配慮する。
解説:解説:半角数字を漢数字(単純)に変換
0307: /**
0308: * 半角数字を漢数字に変換する(単純変換)
0309: * @param wstring wsour 半角数字を含む文字列
0310: * @return wstring 漢数字
0311: */
0312: wstring pahooNormalizeText::num2kanSimple(wstring wsour) {
0313: static wchar_t kantbl1[] =
0314: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
0315: L'8', L'9', L'.', L'-' };
0316: static wchar_t kantbl2[] =
0317: { 0x3007, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, //一〜九
0318: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; //.−
0319:
0320: //左から1文字ずつ処理
0321: wstring wdest = L"";
0322: wstring wstr;
0323: wchar_t* wch;
0324: bool flag;
0325: for (int pos = 0; pos < (int)wsour.length(); pos++) {
0326: flag = FALSE;
0327: wstr = wsour.substr(pos, 1);
0328: wch = (wchar_t*)wstr.c_str();
0329: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
0330: if (*wch == kantbl1[k]) {
0331: wdest += (wstring){kantbl2[k]};
0332: flag = TRUE;
0333: break;
0334: }
0335: }
0336: if (! flag) {
0337: wdest += *wch;
0338: }
0339: }
0340: return wdest;
0341: }
左から右へ向かって、算用数字にマッチする文字があれば漢数字に置換する。
解説:半角数字を位取り記法に変換
0244: /**
0245: * 半角数字を位取り記法に変換する
0246: * @param wstring instr 半角数字(小数,負数,指数表記は未対応)
0247: * @return wstring 位取り記法
0248: */
0249: wstring pahooNormalizeText::num2scale(wstring instr) {
0250: static wchar_t kantbl[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; //万億兆京
0251: //余計な'0'を除く
0252: wregex re(_SW("0+([1-9]+)|0+([1-9]+)"));
0253:
0254: //桁あふれ
0255: if (instr.length() > 20) {
0256: return instr;
0257: }
0258:
0259: //右から1文字ずつ処理
0260: wstring outstr = L"";
0261: wstring ws = L"";
0262: int i = 0;
0263: bool flag = FALSE;
0264: for (int pos = instr.length() - 1; pos >= 0; pos--) {
0265: if (flag) {
0266: outstr = (wstring){kantbl[(int)(i / 4)]} + outstr;
0267: flag = FALSE;
0268: }
0269: wstring ss = instr.substr(pos, 1);
0270: ws = ss + ws;
0271: i++;
0272: if (i % 4 == 0) {
0273: if ((ws == L"0000") || (ws == _SW("0000"))) {
0274: outstr = (wstring){kantbl[(int)(i / 4)]};
0275: } else {
0276: outstr = regex_replace(ws, re, L"$1") + outstr;
0277: flag = TRUE;
0278: }
0279: ws = L"";
0280: }
0281: }
0282: outstr = ws + outstr;
0283:
0284: return outstr;
0285: }
解説:漢数字を半角数字に変換
0363: /**
0364: * 漢数字を半角数字に変換する
0365: * @param wstring kanji 漢数字
0366: * @param int mode 出力書式/1=3桁カンマ区切り,2=漢字混じり, それ以外=ベタ打ち
0367: * @return wstring 半角数字
0368: */
0369: wstring pahooNormalizeText::kan2num(wstring kanji, int mode) {
0370: wstring dest = L"";
0371: wsmatch mt1;
0372:
0373: //全角=半角対応
0374: const wstring kan_num1 = _SW("0〇○1一壱2二弐3三参4四5五6六7七8八9九");
0375: const wstring kan_num2 = _SW("000111222333445566778899");
0376:
0377: //位取り
0378: const wstring kan_deci_sub = _SW("十\百千");
0379: const wstring kan_deci = _SW("万億兆京");
0380:
0381: //半角数字が混在していたら何もしない
0382: wregex re1(_SW("[0-9]+"));
0383: if (regex_search(kanji, mt1, re1)) {
0384: return kanji;
0385: }
0386:
0387: //右側から解釈していく
0388: size_t ll = kanji.length();
0389: wstring a = L"";
0390: long long int deci = 1;
0391: long long int deci_sub = 1;
0392: long long int m = 0;
0393: long long int n = 0;
0394:
0395: for (int pos = ll - 1; pos >= 0; pos--) {
0396: wstring c = kanji.substr(pos, 1);
0397: size_t ps1 = kan_num1.find(c);
0398: size_t ps2 = kan_deci_sub.find(c);
0399: size_t ps3 = kan_deci.find(c);
0400: if (ps1 != wstring::npos) {
0401: a = kan_num2.substr(ps1, 1) + a;
0402: } else if (ps2 != wstring::npos) {
0403: if (a != L"") {
0404: m = m + stol(a) * deci_sub;
0405: } else if (deci_sub != 1) {
0406: m = m + deci_sub;
0407: }
0408: a = L"";
0409: deci_sub = pow(10, ps2 + 1);
0410: } else if (ps3 != wstring::npos) {
0411: if (a != L"") {
0412: m = m + stol(a) * deci_sub;
0413: } else if (deci_sub != 1) {
0414: m = m + deci_sub;
0415: }
0416: n = m * (long long int)deci + n;
0417: m = 0;
0418: a = L"";
0419: deci_sub = 1;
0420: deci = (long long int)pow(10000, ps3 + 1);
0421: }
0422: }
0423:
0424: wstring ss = L"";
0425: wregex re2(_SW("^(0+)"));
0426: if (regex_search(a, mt1, re2)) {
0427: ss = mt1[1].str();
0428: }
0429: if (a != L"") {
0430: m = m + stol(a) * deci_sub;
0431: } else if (deci_sub != 1) {
0432: m = m + deci_sub;
0433: }
0434: n = m * deci + n;
0435:
0436: return to_wstring(n);
0437: }
解説:日本語テキストを半角に統一
0450: /**
0451: * 日本語テキストを半角に統一
0452: * @param wstring sour 変換元テキスト
0453: * @param bool trim 行頭・行末の空白を除くかどうか
0454: * @return string 変換後テキスト
0455: */
0456: wstring pahooNormalizeText::toHankaku(wstring wsour, bool trim) {
0457: regex sep{"\\t|,"};
0458: wsmatch mt1, mt2, mt3;
0459: //数字パターン
0460: wregex pat_kannum(_SW("^[^数]*[01234567890123456789○〇一二三四五六七八九十\百千万億兆京]+|一+$"));
0461: //MeCabの品詞パターン
0462: wregex re1(_SW("[数幾]|副詞*"));
0463: //月の漢数字
0464: wregex re2(_SW("([一二三四五六七八九十\]+)(月)"));
0465: //漢数字の後にこの文字が付く場合はそのまま
0466: wregex re3(_SW("大"));
0467: //行頭空白パターン
0468: wregex re4(_SW("^[ \\t\\n\\r]+"));
0469: //行末空白パターン
0470: wregex re5(_SW("[ \\t\\n\\r]+$"));
0471:
0472: //カンマ置換
0473: wregex re6(_SW(","));
0474: wsour = regex_replace(wsour, re6, _SW(","));
0475:
0476: //形態素に分解
0477: string input = _WS(wsour);
0478: const char *words = tagger->parse(input.c_str());
0479:
0480: //変換処理
0481: bool flag = FALSE;
0482: wstring dest = L"";
0483: wstring numstr = L"";
0484: wstring surface, pos;
0485:
0486: //1行ずつ読み込む
0487: string ss0;
0488: stringstream ss;
0489: ss << words;
0490: while(ss && getline(ss, ss0)) {
0491: int cnt = 0;
0492: for (std::cregex_token_iterator end,
0493: ite{ss0.c_str(), ss0.c_str() + strlen(ss0.c_str()), sep, -1};
0494: ite != end; ++ite) {
0495: if (cnt == 0) surface = _SW((*ite).str().c_str());
0496: else if (cnt == 2) pos = _SW((*ite).str().c_str());
0497: cnt++;
0498: }
0499: //最後
0500: if (surface == _SW("EOS")) {
0501: break;
0502: //月の処理
0503: } else if (regex_search(surface, mt1, re2)) {
0504: dest += this->kan2num(mt1[1].str(), 2) + mt1[2].str();
0505: } else if (flag == FALSE) {
0506: //漢数字の1文字目
0507: if (regex_search(surface, mt1, pat_kannum) && regex_search(pos, mt2, re1)) {
0508: numstr = surface;
0509: flag = TRUE;
0510: //数字ではない
0511: } else {
0512: dest += surface;
0513: }
0514: } else {
0515: //漢数字の2文字目以降
0516: if (regex_search(surface, mt1, pat_kannum)) {
0517: numstr += surface;
0518: flag = TRUE;
0519: //数字以外
0520: } else {
0521: //"漢数字+大"の場合はそのまま
0522: if (regex_search(surface, mt1, re3)) {
0523: dest += (numstr + surface);
0524: //ここまでの漢数字を半角数字に
0525: } else {
0526: dest += this->kan2num(numstr, 2) + surface;
0527: }
0528: numstr = L"";
0529: flag = FALSE;
0530: }
0531: }
0532: }
0533:
0534: if (flag == TRUE) {
0535: dest += this->kan2num(numstr, 2);
0536: }
0537:
0538: //行頭・行末空白処理
0539: if (trim) {
0540: wstring wss = regex_replace(dest, re4, L"");
0541: wss = regex_replace(wss, re5, L"");
0542: dest = wss + L"\\n";
0543: }
0544:
0545: return wconvString(dest, LCMAP_HALFWIDTH);
0546: }
解説:日本語テキストを全角に変換
0570: /**
0571: * 半角→全角変換に変換
0572: * @param wstring sour 変換元テキスト
0573: * @param bool (*func) 該当文字判定関数
0574: * @return wstring 変換後テキスト
0575: */
0576: wstring pahooNormalizeText::han2zen(const wstring wsour, bool (*func)(wchar_t wch)) {
0577: wstring wss = L"";
0578: wstring wdest = L"";
0579: bool flag = FALSE;
0580:
0581: //先頭から1文字ずつ
0582: for (size_t i = 0; i < wsour.length(); i++) {
0583: wchar_t* wch = (wchar_t*)wsour.substr(i, 1).c_str();
0584: //該当文字ならwss0へ
0585: if (func(*wch)) {
0586: wss += (wstring){*wch};
0587: flag = TRUE;
0588: //半角→全角変換
0589: } else if (flag) {
0590: wdest += wconvString(wss, LCMAP_FULLWIDTH);
0591: wdest += wsour.substr(i, 1);
0592: flag = FALSE;
0593: wss = L"";
0594: } else {
0595: wdest += wsour.substr(i, 1);
0596: }
0597: }
0598: //最後の1文字が該当文字
0599: if (flag) {
0600: wdest += wconvString(wss, LCMAP_FULLWIDTH);
0601: }
0602:
0603: return wdest;
0604: }
解説:日本語テキストを正規化する
0606: /**
0607: * 日本語テキストを正規化する
0608: * @param wstring sour 漢数字混じりテキスト
0609: * @param char* option 変換オプション
0610: * @param bool trim 行頭・行末の空白を除くかどうか
0611: * @return wstring 変換後テキスト
0612: */
0613: wstring pahooNormalizeText::normalizeText(wstring wsour, const char* option, bool trim) {
0614: wstring wdest = wsour;
0615:
0616: //いったん半角に
0617: wdest = this->toHankaku(wdest, trim);
0618:
0619: //全角文字と隣り合う空白文字を除く
0620: wregex re1(_SW("[ \\t]+([^!-~].)"));
0621: wdest = regex_replace(wdest, re1, L"$1");
0622:
0623: //英字:半角→全角
0624: if (strchr(option, OPTION_ALP_ZEN) != NULL) {
0625: wdest = this->han2zen(wdest, _alphabet);
0626: }
0627: //数字:位取り記法
0628: if (strchr(option, OPTION_NUM_SCALE) != NULL) {
0629: wdest = this->bignum2scale(wdest);
0630: }
0631: //数字:半角→全角
0632: if (strchr(option, OPTION_NUM_ZEN) != NULL) {
0633: wdest = this->han2zen(wdest, _decimal);
0634: //数字:半角→漢数字
0635: } else if (strchr(option, OPTION_NUM_KAN) != NULL) {
0636: wdest = this->num2kan(wdest);
0637: //数字:半角→漢数字(単純)
0638: } else if (strchr(option, OPTION_NUM_KAN2) != NULL) {
0639: wdest = this->num2kanSimple(wdest);
0640: }
0641: //記号:半角→全角
0642: if (strchr(option, OPTION_YAK_ZEN) != NULL) {
0643: wdest = this->han2zen(wdest, _yakumono);
0644: }
0645: //カタカナ:半角→全角
0646: if (strchr(option, OPTION_KANA_ZEN) != NULL) {
0647: wdest = this->han2zen(wdest, _katakana);
0648: }
0649: //小数点の全角を半角に変換
0650: wdest = this->hanfloat(wdest);
0651:
0652: //半角文字と隣り合う全角空白は半角空白へ
0653: wregex re2(_SW("[ ]+([!-~].)"));
0654: wdest = regex_replace(wdest, re2, L" $1");
0655:
0656: return wdest;
0657: }
解説:定数など
0036: // 定数など ==================================================================
0037: #define MAKER "pahoo.org" //作成者
0038: #define APPNAME "normalizetextwin" //アプリケーション名
0039: #define APPNAMEJP "テキストの正規化" //アプリケーション名(日本語)
0040: #define APPVERSION "1.2" //バージョン
0041: #define APPYEAR "2020-2021" //作成年
0042: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-18-01.shtm" //参考サイト
0043:
0044: //ヘルプ・ファイル
0045: #define HELPFILE ".\\etc\\help.chm"
0046:
0047: //デフォルト保存ファイル名
0048: #define SAVEFILE "normalizetextwin.txt"
0049:
0050: //オプション保存ファイル名:変更不可
0051: #define OPTIONFILE "option.txt"
0052:
0053: //エラー・メッセージ格納用:変更不可
0054: string ErrorMessage;
0055:
0056: //現在のインターフェイス
0057: HINSTANCE hInst;
0058:
0059: //親ウィンドウ
0060: HWND hParent;
0061:
0062: //pahooNormalizeTextオブジェクト
0063: pahooNormalizeText* pNT;
0064:
0065: //char*バッファサイズ
0066: #define SIZE_BUFF 5120
0067:
0068: //変換元テキスト(初期値)
0069: #define DEF_SOUR "『千と千尋の神隠し』(せんとちひろのかみかくし)は、スタジオジブリ制作の長編アニメーション映画。監督は宮崎駿。二〇〇一年七月二十\日に日本公開。興行収入は三百億円を超え、日本歴代興行収入第一位を達成した。英語のタイトルは『Spirited Away』。\n千尋という名の十\歳の少女が、引っ越し先へ向かう途中に立ち入ったトンネルから、神々の世界へ迷い込んでしまう物語。"
とくに注意記載が無い限り、定数は自由に変更できる。
解説:正規化オプション取得・設定
0377: /**
0378: * 正規化オプションを設定する
0379: * @param HWND hDlg ウィンドウ・ハンドラ
0380: * @param string opt 正規化オプション
0381: * @return なし
0382: */
0383: void setOption(HWND hDlg, string opt) {
0384: for (int i = 0; i < (int)opt.length(); i++) {
0385: char* ch = (char*)opt.substr(i, 1).c_str();
0386: switch (*ch) {
0387: case OPTION_ALP_HAN:
0388: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
0389: BM_SETCHECK, BST_CHECKED, 0);
0390: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
0391: BM_SETCHECK, BST_UNCHECKED, 0);
0392: break;
0393: case OPTION_ALP_ZEN:
0394: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
0395: BM_SETCHECK, BST_UNCHECKED, 0);
0396: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
0397: BM_SETCHECK, BST_CHECKED, 0);
0398: break;
0399: case OPTION_NUM_HAN:
0400: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0401: BM_SETCHECK, BST_CHECKED, 0);
0402: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0403: BM_SETCHECK, BST_UNCHECKED, 0);
0404: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0405: BM_SETCHECK, BST_UNCHECKED, 0);
0406: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0407: BM_SETCHECK, BST_UNCHECKED, 0);
0408: break;
0409: case OPTION_NUM_ZEN:
0410: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0411: BM_SETCHECK, BST_UNCHECKED, 0);
0412: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0413: BM_SETCHECK, BST_CHECKED, 0);
0414: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0415: BM_SETCHECK, BST_UNCHECKED, 0);
0416: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0417: BM_SETCHECK, BST_UNCHECKED, 0);
0418: break;
0419: case OPTION_NUM_KAN:
0420: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0421: BM_SETCHECK, BST_UNCHECKED, 0);
0422: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0423: BM_SETCHECK, BST_UNCHECKED, 0);
0424: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0425: BM_SETCHECK, BST_CHECKED, 0);
0426: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0427: BM_SETCHECK, BST_UNCHECKED, 0);
0428: break;
0429: case OPTION_NUM_KAN2:
0430: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0431: BM_SETCHECK, BST_UNCHECKED, 0);
0432: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0433: BM_SETCHECK, BST_UNCHECKED, 0);
0434: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0435: BM_SETCHECK, BST_UNCHECKED, 0);
0436: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0437: BM_SETCHECK, BST_CHECKED, 0);
0438: break;
0439: case OPTION_NUM_SCALE:
0440: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
0441: BM_SETCHECK, BST_CHECKED, 0);
0442: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
0443: BM_SETCHECK, BST_UNCHECKED, 0);
0444: break;
0445: case OPTION_NUM_NOSCALE:
0446: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
0447: BM_SETCHECK, BST_UNCHECKED, 0);
0448: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
0449: BM_SETCHECK, BST_CHECKED, 0);
0450: break;
0451: case OPTION_YAK_HAN:
0452: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
0453: BM_SETCHECK, BST_CHECKED, 0);
0454: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
0455: BM_SETCHECK, BST_UNCHECKED, 0);
0456: break;
0457: case OPTION_YAK_ZEN:
0458: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
0459: BM_SETCHECK, BST_UNCHECKED, 0);
0460: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
0461: BM_SETCHECK, BST_CHECKED, 0);
0462: break;
0463: case OPTION_KANA_HAN:
0464: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
0465: BM_SETCHECK, BST_CHECKED, 0);
0466: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
0467: BM_SETCHECK, BST_UNCHECKED, 0);
0468: break;
0469: case OPTION_KANA_ZEN:
0470: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
0471: BM_SETCHECK, BST_UNCHECKED, 0);
0472: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
0473: BM_SETCHECK, BST_CHECKED, 0);
0474: break;
0475: case OPTION_SPEC_HAN:
0476: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
0477: BM_SETCHECK, BST_CHECKED, 0);
0478: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
0479: BM_SETCHECK, BST_UNCHECKED, 0);
0480: break;
0481: case OPTION_SPEC_ZEN:
0482: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
0483: BM_SETCHECK, BST_UNCHECKED, 0);
0484: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
0485: BM_SETCHECK, BST_CHECKED, 0);
0486: break;
0487: default:
0488: break;
0489: }
0490: }
0491: }
0493: /**
0494: *正規化オプションを取得する
0495: * @param HWND hDlg ウィンドウ・ハンドラ
0496: * @return string 正規化オプション
0497: */
0498: string getOption(HWND hDlg) {
0499: string opt = "";
0500: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN), BM_GETCHECK, 0, 0)) {
0501: opt += OPTION_ALP_HAN;
0502: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN), BM_GETCHECK, 0, 0)) {
0503: opt += OPTION_ALP_ZEN;
0504: }
0505: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN), BM_GETCHECK, 0, 0)) {
0506: opt += OPTION_NUM_HAN;
0507: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN), BM_GETCHECK, 0, 0)) {
0508: opt += OPTION_NUM_ZEN;
0509: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN), BM_GETCHECK, 0, 0)) {
0510: opt += OPTION_NUM_KAN;
0511: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2), BM_GETCHECK, 0, 0)) {
0512: opt += OPTION_NUM_KAN2;
0513: }
0514: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE), BM_GETCHECK, 0, 0)) {
0515: opt += OPTION_NUM_SCALE;
0516: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE), BM_GETCHECK, 0, 0)) {
0517: opt += OPTION_NUM_NOSCALE;
0518: }
0519: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN), BM_GETCHECK, 0, 0)) {
0520: opt += OPTION_YAK_HAN;
0521: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN), BM_GETCHECK, 0, 0)) {
0522: opt += OPTION_YAK_ZEN;
0523: }
0524: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN), BM_GETCHECK, 0, 0)) {
0525: opt += OPTION_KANA_HAN;
0526: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN), BM_GETCHECK, 0, 0)) {
0527: opt += OPTION_KANA_ZEN;
0528: }
0529: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN), BM_GETCHECK, 0, 0)) {
0530: opt += OPTION_SPEC_HAN;
0531: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN), BM_GETCHECK, 0, 0)) {
0532: opt += OPTION_SPEC_ZEN;
0533: }
0534:
0535: return opt;
0536: }
解説:正規化オプション読込・保存
0123: /**
0124: * AppDataのパスを取得
0125: * @param char* appname アプリケーション名
0126: * @return TCHAR* パス
0127: */
0128: TCHAR* getMyPath(const char* appname) {
0129: static TCHAR myPath[MAX_PATH] = "";
0130:
0131: if (strlen(myPath) == 0) {
0132: if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
0133: TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
0134: if (ptmp != NULL) {
0135: ptmp = _tcsinc(ptmp);
0136: *ptmp = _T('\0');
0137: }
0138: strcat(myPath, _T("Roaming"));
0139: CreateDirectory((LPCTSTR)myPath, NULL);
0140: strcat(myPath, _T("\\pahoo.org"));
0141: CreateDirectory((LPCTSTR)myPath, NULL);
0142: strcat(myPath, _T("\\"));
0143: strcat(myPath, _T(appname));
0144: CreateDirectory((LPCTSTR)myPath, NULL);
0145: strcat(myPath, _T("\\"));
0146: } else {
0147: }
0148: }
0149: return myPath;
0150: }
0152: /**
0153: * オプションの読み込み
0154: * @param なし
0155: * @return string オプション
0156: */
0157: string loadOption(void) {
0158: string option = OPTION_INIT;
0159:
0160: string fname = (string)getMyPath(APPNAME) + OPTIONFILE;
0161: ifstream ifs(fname);
0162: if (! ifs) {
0163: ErrorMessage = (string)fname + " の読み込みに失敗しました";
0164: return option;
0165: }
0166: getline(ifs, option);
0167: ifs.close();
0168:
0169: return option;
0170:
0171: }
0173: /**
0174: * オプションの保存
0175: * @param string option オプション
0176: * @return なし
0177: */
0178: void saveOption(string option) {
0179: string fname = (string)getMyPath(APPNAME) + OPTIONFILE;
0180: ofstream ofs(fname);
0181: ofs << option;
0182:
0183: if (ofs.bad()) {
0184: ErrorMessage = (string)fname + " の書き込みに失敗しました";
0185: cout << ErrorMessage << endl;
0186: }
0187: ofs.close();
0188: }
保存場所は、getMyPath 関数によって、ユーザーの AppData の下にアプリケーションフォルダを作って保存する。
この処理は、GUI/CUI 共通だ。
CUI用メインプログラム
0712: /**
0713: * CUI用メインプログラム
0714: * @param int argc
0715: * @paramm char* argv[]
0716: * @return int リターンコード
0717: */
0718: int main(int argc, char* argv[]) {
0719: //オプション読み込み
0720: string lopt = loadOption();
0721:
0722: //pahooNormalizeTextオブジェクト
0723: pNT = new pahooNormalizeText();
0724:
0725: //コマンドライン・オプションの定義
0726: options_description options("コマンドライン・オプション");
0727: options.add_options()
0728: ("option,o", value<std::string>()->default_value(lopt), "正規化オプション")
0729: ("sour,s", value<std::string>(), "入力テキスト")
0730: ("dest,d", value<std::string>(), "出力テキスト")
0731: ("help,h", "ヘルプ")
0732: ("version,v", "バージョン情報")
0733: ;
0734: //コマンドライン・オプションの取得
0735: variables_map vm;
0736: try {
0737: store(parse_command_line(argc, argv, options), vm);
0738: } catch(const boost::program_options::error_with_option_name& e) {
0739: ErrorMessage = e.what();
0740: cerr << ErrorMessage << endl;
0741: return 1;
0742: }
0743: notify(vm);
0744:
0745: //正規化オプション
0746: auto opt = vm["option"].as<string>();
0747:
0748: wstring wsour = L"";
0749: wstring wdest = L"";
0750: //ヘルプ情報
0751: if (vm.count("help")) {
0752: wdest = _SW(Help);
0753: //バージョン情報
0754: } else if (vm.count("version")) {
0755: wdest = _SW(Version);
0756:
0757: //正規化実行
0758: } else {
0759: //入力ファイル
0760: wsour = _SW(DEF_SOUR);
0761: if (vm.count("sour")) {
0762: auto infile = vm["sour"].as<string>();
0763: ifstream ifs(infile.c_str());
0764: if (ifs.fail()) {
0765: ErrorMessage = infile + " が見つかりません";
0766: cerr << ErrorMessage << endl;
0767: return 1;
0768: }
0769: string sour = "";
0770: string ss;
0771: while (getline(ifs, ss)) {
0772: sour += ss + "\r";
0773: }
0774: wsour = _SW(sour);
0775: //標準入力から
0776: } else {
0777: string sour;
0778: cin >> sour;
0779: if (sour.length() > 0) {
0780: wsour = _SW(sour);
0781: }
0782: }
0783: wdest = pNT->normalizeText(wsour, opt.c_str(), FALSE);
0784: }
0785:
0786: //出力ファイル
0787: if (vm.count("dest")) {
0788: //改行コード置換
0789: wregex re(_SW("\r"));
0790: wdest = regex_replace(wdest, re, _SW("\n"));
0791: auto outfile = vm["dest"].as<string>();
0792: ofstream ofs(outfile.c_str());
0793: ofs << _WS(wdest);
0794: if (ofs.bad()) {
0795: ErrorMessage = outfile + " への書き込みに失敗しました";
0796: cerr << ErrorMessage << endl;
0797: return 1;
0798: }
0799: ofs.close();
0800: //標準出力へ
0801: } else {
0802: //改行コード置換
0803: wregex re(_SW("\r"));
0804: wdest = regex_replace(wdest, re, _SW("\n"));
0805: cout << _WS(wdest);
0806: }
0807:
0808: //オブジェクト解放
0809: delete pNT;
0810:
0811: //オプション保存
0812: saveOption(lopt);
0813:
0814: return 0;
0815: }
GUI版の WinMain 関数はマルチスレッド対応で、すべてのスレッドが終わる前にコマンドラインに戻ってきてしまう。また、標準入出力のパイプ処理への対応も面倒であるため、CUI版は実行ファイルを分けることにした。

コマンドラインオプションの解釈は、Boost C++ライブラリ の options を利用した。
参考サイト
- PHP で日本語テキストを正規化(Windows アプリ版):ぱふぅ家のホームページ
- PHP で日本語テキストを正規化:ぱふぅ家のホームページ
- MeCab
- PHP で MeCab のユーザー辞書を作成する:ぱふぅ家のホームページ
- C++ でテキスト中の和暦・西暦年号を統一する:ぱふぅ家のホームページ
- WiX による Windows インストーラー作成:ぱふぅ家のホームページ
- C++ 開発環境の準備:ぱふぅ家のホームページ
また、ソースを共通化してコマンドライン版アプリケーションを作り、バッチや他のプログラムから流し込んだテキストを正規化することができるようにする。
「PHP で日本語テキストを正規化(Windows アプリ版)」で作った PHP プログラムを C++に移植したものである。
(2021 年 2 月 3 日)不足していた記号類を追加。
(2020 年 11 月 15 日)数字変換に漢字(単純)を追加,オプションの読込・保存機能追加,変換テキスト長を約 5 千文字に増強,バグ修正