
目次
サンプル・プログラム
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 bin/etc/vardic.csv | 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 | テキスト正規化クラス(ヘッダ) |
sour/makefile | GUI版ビルド |
sour/makefile_cmd | CUI版ビルド |
バージョン | 更新日 | 内容 |
---|---|---|
2.0.0 | 2025/08/30 | 表記ゆれ統一機能を追加 |
1.9.5 | 2025/08/02 | MeCabユーザー辞書更新,使用ライブラリ更新 |
1.9.4 | 2025/03/29 | MeCabユーザー辞書更新,使用ライブラリ更新 |
1.9.3 | 2024/12/15 | MeCabユーザー辞書更新,使用ライブラリ更新 |
1.9.2 | 2024/08/24 | MeCabユーザー辞書更新,使用ライブラリ更新 |
バージョン | 更新日 | 内容 |
---|---|---|
2.0.0 | 2025/08/30 | 表記ゆれ統一機能を追加 |
1.9.1 | 2023/12/17 | pahooNormalizeText() - MECAB非使用時の対策 |
1.9.0 | 2023/10/25 | normalizeText() - 二重引用符の開閉処理 |
1.8.0 | 2023/10/25 | pahooNormalizeText() - MeCab動作チェック追加 |
1.7.2 | 2023/07/30 | bignum2scale() - wsourの中に400万と4億が混在する場合にも対応 |
使用ライブラリ
また、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"" に設定する。

MSYS2 コマンドラインからビルドするのであれば、"makefile" と "makefile_cmd" を利用してほしい。
日本語形態素解析システム「MeCab」の導入

公式ダウンロートサイトから Binary package for MS-Windows をダウンロードする。2023年(令和5年)10月現在、"mecab-0.996.exe" がダウンロードできる。ダウンロードした実行プログラムを実行すると、インストールがはじまる。
インストール先は任意。辞書ファイルのエンコードは Shift-JIS を指定すること。

MeCab については「PHPで MeCabのユーザー辞書を作成する」で紹介したが、簡単に振り返っておくと、入力されたテキストを日本語の品詞に分解する形態素解析エンジンと呼ばれるプログラムである。
なぜテキストの正規化に形態素解析エンジンが必要かというと、たとえば「千と千尋の神隠しという映画を見た」というテキストをそのまま正規化してしまうと、「千」を数字と解釈し、「1000と1000尋の神隠しという映画を見た」という変換結果になってしまうためである。
テキスト中にある固有名詞は数字変換の対象としないように除外するため、形態素解析エンジンを利用する。形態素解析エンジンであれば MeCab でなくても構わないのだが、C++から利用可能なDLLファイルが利用できることから、ここでは MeCab を採用する。

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

MSYS2 開発環境では、次のように pacman を使い iconv と MeCab ライブラリをインストールできる。
64ビット版:
pacman -S mingw-w64-x86_64-libiconv
pacman -S mingw-w64-x86_64-mecab

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

変換ボタンがクリックされたら、toHankaku メソッドを使って半角か可能な文字を全て半角にする。その後、正規化オプションの値に応じて全角化、漢数字化を行ってゆく。
解説:テキスト正規化クラス
pahooNormalizeText.hpp
32: // 正規化オプション
33: #define OPTION_SPC_TRIM1 't' // 行頭・行末の空白文字を除く
34: #define OPTION_SPC_TRIM2 'T' // 全角文字と隣り合う空白文字を除く
35: #define OPTION_NUM_HAN 'n' // 数字を半角に統一
36: #define OPTION_NUM_ZEN 'N' // 数字を全角に統一
37: #define OPTION_NUM_KAN 'K' // 数字を漢字に統一
38: #define OPTION_NUM_KAN2 'k' // 数字を漢字(単純)に統一
39: #define OPTION_ALP_HAN 'a' // 英字を半角に統一
40: #define OPTION_ALP_ZEN 'A' // 英字を全角に統一
41: #define OPTION_YAK_HAN 'y' // 記号を半角に統一
42: #define OPTION_YAK_ZEN 'Y' // 記号を全角に統一
43: #define OPTION_KANA_HAN 'h' // カタカナを半角に統一
44: #define OPTION_KANA_ZEN 'H' // カタカナを全角に統一
45: #define OPTION_SPEC_HAN 's' // 特殊文字を半角に統一
46: #define OPTION_SPEC_ZEN 'S' // 特殊文字を全角に統一
47: #define OPTION_NUM_SCALE 'F' // 数字を位取り記法にする
48: #define OPTION_NUM_NOSCALE 'f' // 数字を位取り記法にしない
49: #define OPTION_CONTROL_DEL 'c' // 制御文字を削除する
50: #define OPTION_DBL_QUOTE 'D' // 二重引用符の開閉置換
51: #define OPTION_VARIABLE 'V' // 表記ゆれを統一する
52:
53: // 正規化オプションの初期値
54: #define OPTION_INIT "anYSHF"
解説:MeCabの呼び出し
pahooNormalizeText.hpp
133: private:
134: #ifdef MECAB
135: 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" などを指定する。
解説:半角数字を漢数字に変換
pahooNormalizeText.cpp
235: /**
236: * 半角数字を漢数字に変換する
237: * @param wstring instr 半角数字
238: * 小数、負数に対応;指数表記には未対応
239: * カンマは削除
240: * @return wstring 漢数字
241: */
242: wstring pahooNormalizeText::num2kanji(wstring instr) {
243: static wchar_t kantbl1[] =
244: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
245: L'8', L'9', L'.', L'-' };
246: static wchar_t kantbl2[] =
247: { 0x0000, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, // 一〜九
248: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; // .−
249: static wchar_t kantbl3[] = { 0x0000, 0x5341, 0x767E, 0x5343 }; // 十百千
250: static wchar_t kantbl4[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; // 万億兆京
251:
252: wstring outstr = L"";;
253: wstring ws2;
254: wchar_t wch1, wch2;
255: int m = (int)instr.length() / 4;
256: // 一、万、億、兆‥‥の繰り返し
257: for (int i = 0; i <= m; i++) {
258: ws2 = L"";
259: // 一、十、百、千の繰り返し
260: for (int j = 0; j < 4; j++) {
261: int pos = instr.length() - i * 4 - j - 1;
262: if (pos >= 0) {
263: wchar_t* wch = (wchar_t*)instr.substr(pos, 1).c_str();
264: if (*wch == L',') continue; // カンマは無視
265: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
266: // 漢数字 or 半角数字のまま
267: wch1 = 0x0000;
268: if (*wch == kantbl1[k]) {
269: wch1 = kantbl2[k];
270: break;
271: }
272: }
273: wch2 = 0x0000;;
274: if ((j >= 0) && (j <= 3)) {
275: wch2 = kantbl3[j];
276: }
277:
278: // 冒頭が「一」の場合の処理
279: if (wch1 != 0x0000) {
280: if ((wch1 == 0x4E00) && (wch2 != 0x0000)) {
281: ws2 = (wstring){wch2} + ws2;
282: } else if (wch2 != 0x0000) {
283: ws2 = (wstring){wch1} + (wstring){wch2} + ws2;
284: } else {
285: ws2 = (wstring){wch1} + ws2;
286: }
287: }
288: }
289: }
290: if (ws2 != L"") {
291: if (kantbl4[i] == 0x0000) {
292: outstr = ws2 + outstr;
293: } else {
294: outstr = ws2 + (wstring){kantbl4[i]} + outstr;
295: }
296: }
297: }
298:
299: return outstr;
300: }
右から左へ向かって逆順に処理し、4桁ごとに、万、億、兆、京の単位を付けてゆく。各々の4桁の中で、十、百、千を付ける。冒頭が「一」の場合は「一百」にならないよう配慮する。
解説:解説:半角数字を漢数字(単純)に変換
pahooNormalizeText.cpp
365: /**
366: * 半角数字を漢数字に変換する(単純変換)
367: * @param wstring wsour 半角数字を含む文字列
368: * @return wstring 漢数字
369: */
370: wstring pahooNormalizeText::num2kanSimple(wstring wsour) {
371: static wchar_t kantbl1[] =
372: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
373: L'8', L'9', L'.', L'-' };
374: static wchar_t kantbl2[] =
375: { 0x3007, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, // 一〜九
376: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; // .−
377:
378: // 左から1文字ずつ処理
379: wstring wdest = L"";
380: wstring wstr;
381: wchar_t* wch;
382: bool flag;
383: for (int pos = 0; pos < (int)wsour.length(); pos++) {
384: flag = FALSE;
385: wstr = wsour.substr(pos, 1);
386: wch = (wchar_t*)wstr.c_str();
387: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
388: if (*wch == kantbl1[k]) {
389: wdest += (wstring){kantbl2[k]};
390: flag = TRUE;
391: break;
392: }
393: }
394: if (! flag) {
395: wdest += *wch;
396: }
397: }
398: return wdest;
399: }
左から右へ向かって、算用数字にマッチする文字があれば漢数字に置換する。
解説:半角数字を位取り記法に変換
pahooNormalizeText.cpp
302: /**
303: * 半角数字を位取り記法に変換する
304: * @param wstring instr 半角数字(小数,負数,指数表記は未対応)
305: * @return wstring 位取り記法
306: */
307: wstring pahooNormalizeText::num2scale(wstring instr) {
308: static wchar_t kantbl[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; // 万億兆京
309: // 余計な'0'を除く
310: wregex re(_SW("0+([1-9]+)|0+([1-9]+)"));
311:
312: // 桁あふれ
313: if (instr.length() > 20) {
314: return instr;
315: }
316:
317: // 右から1文字ずつ処理
318: wstring outstr = L"";
319: wstring ws = L"";
320: int i = 0;
321: bool flag = FALSE;
322: for (int pos = instr.length() - 1; pos >= 0; pos--) {
323: if (flag) {
324: outstr = (wstring){kantbl[(int)(i / 4)]} + outstr;
325: flag = FALSE;
326: }
327: wstring ss = instr.substr(pos, 1);
328: ws = ss + ws;
329: i++;
330: if (i % 4 == 0) {
331: if ((ws == L"0000") || (ws == _SW("0000"))) {
332: outstr = (wstring){kantbl[(int)(i / 4)]};
333: } else {
334: outstr = regex_replace(ws, re, L"$1") + outstr;
335: flag = TRUE;
336: }
337: ws = L"";
338: }
339: }
340: outstr = ws + outstr;
341:
342: return outstr;
343: }
解説:漢数字を半角数字に変換
pahooNormalizeText.cpp
421: /**
422: * 漢数字を半角数字に変換する
423: * @param wstring kanji 漢数字
424: * @param int mode 出力書式/1=3桁カンマ区切り,2=漢字混じり, それ以外=ベタ打ち
425: * @return wstring 半角数字
426: */
427: wstring pahooNormalizeText::kan2num(wstring kanji, int mode) {
428: wstring dest = L"";
429: wsmatch mt1;
430:
431: // 全角=半角対応
432: const wstring kan_num1 = _SW("0〇○1一壱2二弐3三参4四5五6六7七8八9九");
433: const wstring kan_num2 = _SW("000111222333445566778899");
434:
435: // 位取り
436: const wstring kan_deci_sub = _SW("十\百千");
437: const wstring kan_deci = _SW("万億兆京");
438:
439: // 半角数字が混在していたら何もしない
440: wregex re1(_SW("[0-9]+"));
441: if (regex_search(kanji, mt1, re1)) {
442: return kanji;
443: }
444:
445: // 右側から解釈していく
446: size_t ll = kanji.length();
447: wstring a = L"";
448: long long int deci = 1;
449: long long int deci_sub = 1;
450: long long int m = 0;
451: long long int n = 0;
452:
453: for (int pos = ll - 1; pos >= 0; pos--) {
454: wstring c = kanji.substr(pos, 1);
455: size_t ps1 = kan_num1.find(c);
456: size_t ps2 = kan_deci_sub.find(c);
457: size_t ps3 = kan_deci.find(c);
458: if (ps1 != wstring::npos) {
459: a = kan_num2.substr(ps1, 1) + a;
460: } else if (ps2 != wstring::npos) {
461: if (a != L"") {
462: m = m + stol(a) * deci_sub;
463: } else if (deci_sub != 1) {
464: m = m + deci_sub;
465: }
466: a = L"";
467: deci_sub = pow(10, ps2 + 1);
468: } else if (ps3 != wstring::npos) {
469: if (a != L"") {
470: m = m + stol(a) * deci_sub;
471: } else if (deci_sub != 1) {
472: m = m + deci_sub;
473: }
474: n = m * (long long int)deci + n;
475: m = 0;
476: a = L"";
477: deci_sub = 1;
478: deci = (long long int)pow(10000, ps3 + 1);
479: }
480: }
481:
482: wstring ss = L"";
483: wregex re2(_SW("^(0+)"));
484: if (regex_search(a, mt1, re2)) {
485: ss = mt1[1].str();
486: }
487: if (a != L"") {
488: m = m + stol(a) * deci_sub;
489: } else if (deci_sub != 1) {
490: m = m + deci_sub;
491: }
492: n = m * deci + n;
493:
494: return to_wstring(n);
495: }
解説:日本語テキストを半角に統一
pahooNormalizeText.cpp
535: /**
536: * 日本語テキストを半角に統一
537: * @param wstring sour 変換元テキスト
538: * @param bool trim 行頭・行末の空白を除くかどうか
539: * @param bool variable 表記ゆれ統一するかどうか
540: * @return string 変換後テキスト
541: */
542: wstring pahooNormalizeText::toHankaku(wstring wsour, bool trim, bool variable) {
543: regex sep{"\\t|,"};
544: wsmatch mt1, mt2, mt3;
545: // 数字パターン
546: wregex pat_kannum(_SW("^[^数]*[01234567890123456789○〇一二三四五六七八九十\百千万億兆京]+$"));
547: // MeCabの品詞パターン
548: wregex re1(_SW("[数幾]"));
549: // 「一九」対応
550: wregex re19(_SW("一九"));
551: // 月の漢数字
552: wregex re2(_SW("([一二三四五六七八九十\]+)(月)"));
553: // 名詞接続の場合はそのまま
554: wregex re3(_SW("名詞接続"));
555: // 行頭空白パターン
556: wregex re4(_SW("^[ \\t\\n\\r]+"));
557: // 行末空白パターン
558: wregex re5(_SW("[ \\t\\n\\r]+$"));
559: // 無変換パターン・その1
560: wregex re71(_SW("副詞可.+"));
561: wregex re72(_SW("接尾|格助詞"));
562: // 無変換パターン・その2
563: wregex re8(_SW("^[千万億兆]+$"));
564: // 無変換パターン・その3
565: wregex re9(_SW("一生"));
566:
567: // カンマ置換
568: wregex re6(_SW(","));
569: wsour = regex_replace(wsour, re6, _SW(","));
570:
571: // 形態素に分解
572: string input = _WS(wsour);
573: const char *words = tagger->parse(input.c_str());
574:
575: // 変換処理
576: bool flag = FALSE;
577: bool adverb = FALSE;
578: wstring dest = L"";
579: wstring numstr = L"";
580: wstring surface, pos;
581:
582: // 1行ずつ読み込む
583: string ss0;
584: stringstream ss;
585: ss << words;
586: while(ss && getline(ss, ss0)) {
587: int cnt = 0;
588: for (std::cregex_token_iterator end,
589: ite{ss0.c_str(), ss0.c_str() + strlen(ss0.c_str()), sep, -1};
590: ite != end; ++ite) {
591: if (cnt == 0) surface = _SW((*ite).str().c_str());
592: else if (cnt == 2) pos = _SW((*ite).str().c_str());
593: cnt++;
594: }
595: // clog << _WS(surface) << " : " << _WS(pos) << endl;
596:
597: // 表記ゆれを統一する品詞パターン
598: if (variable) {
599: wregex reNoun(_SW("名詞"));
600: if (regex_search(pos, mt1, reNoun)) {
601: // 初回の辞書読み込み
602: if (VariableDict.empty()) {
603: VariableDict = this->loadVariableDictionary(VARIABILITY_REPLACE_CSV);
604: }
605: surface = this->variable2standard(surface);
606: }
607: }
608:
609: // 最後
610: if (surface == _SW("EOS")) {
611: break;
612: // 月の処理
613: } else if (regex_search(surface, mt1, re2)) {
614: dest += this->kan2num(mt1[1].str(), 2) + mt1[2].str();
615: // 無変換
616: } else if (regex_search(pos, mt1, re71) || regex_search(surface, mt1, re9)) {
617: // clog << _WS(surface) << endl;
618: dest += surface;
619: } else if (flag == FALSE) {
620: // 副詞可能な数字
621: if (regex_search(pos, mt2, re71)) {
622: numstr = surface;
623: flag = TRUE;
624: adverb = TRUE;
625: // 漢数字の1文字目
626: } else if (regex_search(surface, mt1, pat_kannum) && regex_search(pos, mt2, re1)) {
627: numstr = surface;
628: flag = TRUE;
629: adverb = FALSE;
630: // 「一九」対応
631: } else if (regex_search(surface, mt2, re19)) {
632: numstr = surface;
633: flag = TRUE;
634: adverb = FALSE;
635: // 数字ではない
636: } else {
637: dest += surface;
638: }
639: } else {
640: // 無変換(副詞可能+接尾)
641: if (adverb && regex_search(pos, mt2, re72)) {
642: dest += (numstr + surface);
643: numstr = L"";
644: flag = FALSE;
645: adverb = FALSE;
646: // 漢数字の2文字目以降
647: } else if (regex_search(surface, mt1, pat_kannum)) {
648: numstr += surface;
649: flag = TRUE;
650: // 数字以外
651: } else {
652: // 名詞接続の場合はそのまま
653: if (regex_search(pos, mt2, re3)) {
654: dest += (numstr + surface);
655: // 無変換パターン
656: } else if (regex_search(numstr, mt1, re8)) {
657: dest += (numstr + surface);
658: // 副詞可能の場合
659: } else if (adverb) {
660: dest += (this->kanword2num(numstr) + surface);
661: // ここまでの漢数字を半角数字に
662: } else {
663: dest += (this->kan2num(numstr, 2) + surface);
664: }
665: numstr = L"";
666: flag = FALSE;
667: adverb = FALSE;
668: }
669: }
670: }
671:
672: // 末尾処理
673: if (flag == TRUE) {
674: if (adverb == TRUE) {
675: dest += numstr;
676: } else {
677: dest += this->kan2num(numstr, 2);
678: }
679: }
680:
681: // 行頭・行末空白処理
682: if (trim) {
683: wstring wss = regex_replace(dest, re4, L"");
684: wss = regex_replace(wss, re5, L"");
685: dest = wss + L"\\n";
686: }
687:
688: // 「十八番」対応
689: wregex re11(_SW("一八([^番]+)"));
690: dest = regex_replace(dest, re11, L"18$1");
691: // 「・」対応
692: wregex re12(_SW("([0-9]+)・([0-9]+)"));
693: dest = regex_replace(dest, re12, L"$1.$2");
694:
695: return wconvString(dest, LCMAP_HALFWIDTH);
696: }
MeCab による形態素解析を実行するメソッド parse によってテキストを形態素の分解、個々の形態素に対して、前述のフロー図に示した変換処理を順次実行していく。

まず最初に、表記ゆれを統一するフラグが立っていたら、メソッド variable2standard を実行する。このメソッドを初めて読み出すタイミングで、表記ゆれ統一辞書を読み込む。これは、表記ゆれを統一の行わない場合、起動時に辞書を読み込むと余計な時間がかかるので、このタイミングで読み込むようにした。

最後に、Win32APIを呼び出す wconvString を使って、変換可能な全ての文字を半角にする。
解説:単語の表記ゆれを統一する
pahooNormalizeText.cpp
875: /**
876: * 単語の表記ゆれを統一する
877: * @param wstring 単語
878: * @return wstring 統一表記の単語
879: */
880: wstring pahooNormalizeText::variable2standard(wstring word) {
881: auto it = std::find_if(VariableDict.begin(), VariableDict.end(),
882: [&word](const auto& p){ return p.first == word; });
883:
884: // 一致したら value に置換
885: if (it != VariableDict.end()) {
886: return it->second;
887: }
888: // 一致しなければそのまま
889: return word;
890: }

表記ゆれ統一辞書ファイルは "vardic.csv" として用意した。同梱の辞書ファイルは、Wikipediaのリダイレクト辞書とページ情報から取りだした。
形式は下記の通りの簡単なCSVファイルである。自由に追加、変更、削除できる。
(単語1),(正式表記1)
(単語2),(正式表記2)
...
解説:日本語テキストを全角に変換
pahooNormalizeText.cpp
723: /**
724: * 半角→全角変換に変換
725: * @param wstring sour 変換元テキスト
726: * @param bool (*func) 該当文字判定関数
727: * @return wstring 変換後テキスト
728: */
729: wstring pahooNormalizeText::han2zen(wstring wsour, bool (*func)(wchar_t wch)) {
730: wstring wss = L"";
731: wstring wdest = L"";
732: bool flag = FALSE;
733: // clog << _WS(wsour) << endl;
734:
735: // 先頭から1文字ずつ
736: for (size_t i = 0; i < wsour.length(); i++) {
737: wchar_t* wch = (wchar_t*)wsour.substr(i, 1).c_str();
738: // 該当文字ならwss0へ
739: if (func(*wch)) {
740: wss += (wstring){*wch};
741: flag = TRUE;
742: // 半角→全角変換
743: } else if (flag) {
744: wdest += wconvString(wss, LCMAP_FULLWIDTH);
745: wdest += wsour.substr(i, 1);
746: flag = FALSE;
747: wss = L"";
748: } else {
749: wdest += wsour.substr(i, 1);
750: }
751: }
752: // 最後の1文字が該当文字
753: if (flag) {
754: wdest += wconvString(wss, LCMAP_FULLWIDTH);
755: }
756:
757: return wdest;
758: }
解説:日本語テキストを正規化する
pahooNormalizeText.cpp
760: /**
761: * 日本語テキストを正規化する
762: * @param wstring sour 漢数字混じりテキスト
763: * @param char* option 変換オプション
764: * @param bool trim 行頭・行末の空白を除くかどうか
765: * @return wstring 変換後テキスト
766: */
767: wstring pahooNormalizeText::normalizeText(const wstring wsour, const char* option, bool trim) {
768: wstring wdest = wsour;
769:
770: // フロントエンド置換
771: wdest = this->frontend_replace(wdest);
772:
773: // 制御文字を削除する
774: if (strchr(option, OPTION_CONTROL_DEL) != NULL) {
775: wregex re2(_SW("([\\t\\r\\r]+)"));
776: wdest = regex_replace(wdest, re2, L"");
777: }
778: // 表記ゆれを統一する
779: bool variable = FALSE;
780: if (strchr(option, OPTION_VARIABLE) != NULL) {
781: variable = TRUE;
782: }
783:
784: // いったん半角に
785: #ifdef MECAB
786: wdest = this->toHankaku(wdest, trim, variable);
787: #endif
788: // clog << _WS(wdest) << endl;
789:
790: // 全角文字と隣り合う空白文字を除く
791: wregex re11(_SW("[ \\t]+([^!-~].)"));
792: wdest = regex_replace(wdest, re11, L"$1");
793: wregex re12(_SW("([^!-~].)[ \\t]+"));
794: wdest = regex_replace(wdest, re12, L"$1");
795:
796: // 英字:半角→全角
797: if (strchr(option, OPTION_ALP_ZEN) != NULL) {
798: wdest = this->han2zen(wdest, _alphabet);
799: }
800: // 数字:位取り記法
801: if (strchr(option, OPTION_NUM_SCALE) != NULL) {
802: wdest = this->bignum2scale(wdest);
803: }
804: // 数字:半角→全角
805: if (strchr(option, OPTION_NUM_ZEN) != NULL) {
806: wdest = this->han2zen(wdest, _decimal);
807: // 数字:半角→漢数字
808: } else if (strchr(option, OPTION_NUM_KAN) != NULL) {
809: wdest = this->num2kan(wdest);
810: // 数字:半角→漢数字(単純)
811: } else if (strchr(option, OPTION_NUM_KAN2) != NULL) {
812: wdest = this->num2kanSimple(wdest);
813: }
814: // 記号:半角→全角
815: if (strchr(option, OPTION_YAK_ZEN) != NULL) {
816: wdest = this->han2zen(wdest, _yakumono);
817: }
818: // カタカナ:半角→全角
819: if (strchr(option, OPTION_KANA_ZEN) != NULL) {
820: wdest = this->han2zen(wdest, _katakana);
821: }
822: // 小数点の全角を半角に変換
823: wdest = this->hanfloat(wdest);
824:
825: // 半角文字と隣り合う全角空白は半角空白へ
826: wregex re2(_SW("[ ]+([!-~].)"));
827: wdest = regex_replace(wdest, re2, L" $1");
828:
829: // 全角二重引用符の開閉
830: if (strchr(option, OPTION_DBL_QUOTE) != NULL) {
831: wregex re3(_SW("?W([^?W]*)?W"));
832: wdest = regex_replace(wdest, re3, _SW("“") + L"$1" + _SW("”"));
833: }
834:
835: return wdest;
836: }

オプション OPTION_DBL_QUOTE が指定されているとき、全角二重引用符の開閉置換を行う。
日本語テキストを半角に統一するとき、二重引用符系の記号は全て半角二重引用符 " (U+0022) に変換するのだが、記号の全角変換 OPTION_YAK_ZEN を指定しているときには、"..." を全角二重引用符の開閉“...”(U+201C…U+201D) に置換する。
解説:定数など
normalizetextwin.cpp
44: // 定数など ==================================================================
45: #define MAKER "pahoo.org" // 作成者
46: #define APPNAME "normalizetextwin" // アプリケーション名
47: #define APPNAMEJP "テキストの正規化" // アプリケーション名(日本語)
48: #define APPVERSION "2.0.0" // バージョン
49: #define APPYEAR "2020-25" // 作成年
50: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-18-01.shtm" // 参考サイト
51:
52: // ヘルプ・ファイル
53: #define HELPFILE ".\\etc\\help.chm"
54:
55: // デフォルト保存ファイル名
56: #define SAVEFILE "normalizetextwin.txt"
57:
58: // オプション保存ファイル名:変更不可
59: #define OPTIONFILE "option.txt"
60:
61: // エラー・メッセージ格納用:変更不可
62: string ErrorMessage;
63:
64: // 現在のインターフェイス
65: HINSTANCE hInst;
66:
67: // アプリケーション・ウィンドウ
68: HWND hParent;
69:
70: // アプリケーション・ウィンドウ位置
71: unsigned hParent_X, hParent_Y;
72:
73: // pahooNormalizeTextオブジェクト
74: pahooNormalizeText* pNT;
75:
76: // char*バッファサイズ
77: #define SIZE_BUFF 5120
78:
79: // 変換元テキスト(初期値)
80: #define DEF_SOUR "『千と千尋』は、ジブリ制作の長編アニメーション映画。監督は宮崎駿。二〇〇一年七月二十\日に日本公開。興行収入は三百億円を超え、日本歴代興行収入第一位を達成した。英語のタイトルは『Spirited Away』。\n千尋という名の十\歳の少女が、引っ越し先へ向かう途中に立ち入った隧道から、神々の世界へ迷い込んでしまう物語。"
とくに注意記載が無い限り、定数は自由に変更できる。
解説:正規化オプション取得・設定
normalizetextwin.cpp
512: /**
513: * 正規化オプションを設定する
514: * @param HWND hDlg ウィンドウ・ハンドラ
515: * @param string opt 正規化オプション
516: * @return なし
517: */
518: void setOption(HWND hDlg, string opt) {
519: for (int i = 0; i < (int)opt.length(); i++) {
520: char* ch = (char*)opt.substr(i, 1).c_str();
521: switch (*ch) {
522: case OPTION_ALP_HAN:
523: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
524: BM_SETCHECK, BST_CHECKED, 0);
525: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
526: BM_SETCHECK, BST_UNCHECKED, 0);
527: break;
528: case OPTION_ALP_ZEN:
529: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
530: BM_SETCHECK, BST_UNCHECKED, 0);
531: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
532: BM_SETCHECK, BST_CHECKED, 0);
533: break;
534: case OPTION_NUM_HAN:
535: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
536: BM_SETCHECK, BST_CHECKED, 0);
537: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
538: BM_SETCHECK, BST_UNCHECKED, 0);
539: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
540: BM_SETCHECK, BST_UNCHECKED, 0);
541: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
542: BM_SETCHECK, BST_UNCHECKED, 0);
543: break;
544: case OPTION_NUM_ZEN:
545: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
546: BM_SETCHECK, BST_UNCHECKED, 0);
547: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
548: BM_SETCHECK, BST_CHECKED, 0);
549: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
550: BM_SETCHECK, BST_UNCHECKED, 0);
551: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
552: BM_SETCHECK, BST_UNCHECKED, 0);
553: break;
554: case OPTION_NUM_KAN:
555: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
556: BM_SETCHECK, BST_UNCHECKED, 0);
557: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
558: BM_SETCHECK, BST_UNCHECKED, 0);
559: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
560: BM_SETCHECK, BST_CHECKED, 0);
561: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
562: BM_SETCHECK, BST_UNCHECKED, 0);
563: break;
564: case OPTION_NUM_KAN2:
565: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
566: BM_SETCHECK, BST_UNCHECKED, 0);
567: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
568: BM_SETCHECK, BST_UNCHECKED, 0);
569: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
570: BM_SETCHECK, BST_UNCHECKED, 0);
571: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
572: BM_SETCHECK, BST_CHECKED, 0);
573: break;
574: case OPTION_NUM_SCALE:
575: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
576: BM_SETCHECK, BST_CHECKED, 0);
577: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
578: BM_SETCHECK, BST_UNCHECKED, 0);
579: break;
580: case OPTION_NUM_NOSCALE:
581: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
582: BM_SETCHECK, BST_UNCHECKED, 0);
583: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
584: BM_SETCHECK, BST_CHECKED, 0);
585: break;
586: case OPTION_YAK_HAN:
587: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
588: BM_SETCHECK, BST_CHECKED, 0);
589: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
590: BM_SETCHECK, BST_UNCHECKED, 0);
591: break;
592: case OPTION_YAK_ZEN:
593: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
594: BM_SETCHECK, BST_UNCHECKED, 0);
595: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
596: BM_SETCHECK, BST_CHECKED, 0);
597: break;
598: case OPTION_KANA_HAN:
599: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
600: BM_SETCHECK, BST_CHECKED, 0);
601: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
602: BM_SETCHECK, BST_UNCHECKED, 0);
603: break;
604: case OPTION_KANA_ZEN:
605: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
606: BM_SETCHECK, BST_UNCHECKED, 0);
607: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
608: BM_SETCHECK, BST_CHECKED, 0);
609: break;
610: case OPTION_SPEC_HAN:
611: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
612: BM_SETCHECK, BST_CHECKED, 0);
613: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
614: BM_SETCHECK, BST_UNCHECKED, 0);
615: break;
616: case OPTION_SPEC_ZEN:
617: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
618: BM_SETCHECK, BST_UNCHECKED, 0);
619: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
620: BM_SETCHECK, BST_CHECKED, 0);
621: break;
622: case OPTION_CONTROL_DEL:
623: SendMessage(GetDlgItem(hDlg, IDC_CHECK_CONTROL_DEL),
624: BM_SETCHECK, BST_CHECKED, 0);
625: break;
626: case OPTION_DBL_QUOTE:
627: SendMessage(GetDlgItem(hDlg, IDC_CHECK_DBL_QUOTE),
628: BM_SETCHECK, BST_CHECKED, 0);
629: break;
630: case OPTION_VARIABLE:
631: SendMessage(GetDlgItem(hDlg, IDC_CHECK_VARIABLE),
632: BM_SETCHECK, BST_CHECKED, 0);
633: break;
634: default:
635: break;
636: }
637: }
638: }
normalizetextwin.cpp
640: /**
641: * 正規化オプションを取得する
642: * @param HWND hDlg ウィンドウ・ハンドラ
643: * @return string 正規化オプション
644: */
645: string getOption(HWND hDlg) {
646: string opt = "";
647: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN), BM_GETCHECK, 0, 0)) {
648: opt += OPTION_ALP_HAN;
649: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN), BM_GETCHECK, 0, 0)) {
650: opt += OPTION_ALP_ZEN;
651: }
652: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN), BM_GETCHECK, 0, 0)) {
653: opt += OPTION_NUM_HAN;
654: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN), BM_GETCHECK, 0, 0)) {
655: opt += OPTION_NUM_ZEN;
656: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN), BM_GETCHECK, 0, 0)) {
657: opt += OPTION_NUM_KAN;
658: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2), BM_GETCHECK, 0, 0)) {
659: opt += OPTION_NUM_KAN2;
660: }
661: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE), BM_GETCHECK, 0, 0)) {
662: opt += OPTION_NUM_SCALE;
663: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE), BM_GETCHECK, 0, 0)) {
664: opt += OPTION_NUM_NOSCALE;
665: }
666: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN), BM_GETCHECK, 0, 0)) {
667: opt += OPTION_YAK_HAN;
668: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN), BM_GETCHECK, 0, 0)) {
669: opt += OPTION_YAK_ZEN;
670: }
671: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN), BM_GETCHECK, 0, 0)) {
672: opt += OPTION_KANA_HAN;
673: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN), BM_GETCHECK, 0, 0)) {
674: opt += OPTION_KANA_ZEN;
675: }
676: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN), BM_GETCHECK, 0, 0)) {
677: opt += OPTION_SPEC_HAN;
678: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN), BM_GETCHECK, 0, 0)) {
679: opt += OPTION_SPEC_ZEN;
680: }
681: if (SendMessage(GetDlgItem(hDlg, IDC_CHECK_CONTROL_DEL), BM_GETCHECK, 0, 0)) {
682: opt += OPTION_CONTROL_DEL;
683: }
684: if (SendMessage(GetDlgItem(hDlg, IDC_CHECK_DBL_QUOTE), BM_GETCHECK, 0, 0)) {
685: opt += OPTION_DBL_QUOTE;
686: }
687: if (SendMessage(GetDlgItem(hDlg, IDC_CHECK_VARIABLE), BM_GETCHECK, 0, 0)) {
688: opt += OPTION_VARIABLE;
689: }
690:
691: return opt;
692: }
解説:正規化オプション読込・保存
normalizetextwin.cpp
326: /**
327: * AppDataのパスを取得
328: * @param char* appname アプリケーション名
329: * @return string パス
330: */
331: string getMyPath(const char* appname) {
332: static TCHAR myPath[MAX_PATH] = "";
333:
334: if (strlen(myPath) == 0) {
335: if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
336: TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
337: if (ptmp != NULL) {
338: ptmp = _tcsinc(ptmp);
339: *ptmp = _T('\0');
340: }
341: strcat(myPath, _T("Roaming"));
342: CreateDirectory((LPCTSTR)myPath, NULL);
343: strcat(myPath, _T("\\pahoo.org"));
344: CreateDirectory((LPCTSTR)myPath, NULL);
345: strcat(myPath, _T("\\"));
346: strcat(myPath, _T(appname));
347: CreateDirectory((LPCTSTR)myPath, NULL);
348: strcat(myPath, _T("\\"));
349: } else {
350: }
351: }
352: return (string)myPath;
353: }
normalizetextwin.cpp
367: /**
368: * オプションの読み込み
369: * @param なし
370: * @return string オプション
371: */
372: string loadOption(void) {
373: ptree pt;
374:
375: // 初期値設定
376: string option = initOption();
377:
378: // XMLファイル読み込み
379: try {
380: xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
381:
382: // XML解釈
383: try {
384: // 形式チェック
385: if (optional<string>str = pt.get_optional<string>("parameter")) {
386: } else {
387: return OPTION_INIT;
388: }
389: // パラメータ読み込み
390: for (auto it : pt.get_child("parameter")) {
391: string type= it.second.get_optional<string>("<xmlattr>.type").value();
392: if (type == "option") {
393: option =(string)it.second.data();
394: } else if (type == "wx") {
395: hParent_X = (unsigned)stoi(it.second.data());
396: } else if (type == "wy") {
397: hParent_Y = (unsigned)stoi(it.second.data());
398: }
399: }
400: // 解釈失敗したら初期値設定
401: } catch (xml_parser_error& e) {
402: return initOption();
403: }
404: // 読み込み失敗したら初期値設定
405: } catch (xml_parser_error& e) {
406: return initOption();
407: }
408:
409: // アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
410: HWND hDesktop = GetDesktopWindow();
411: WINDOWINFO windowInfo;
412: windowInfo.cbSize = sizeof(WINDOWINFO);
413: GetWindowInfo(hDesktop, &windowInfo);
414: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
415: hParent_X = 0;
416: }
417: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
418: hParent_Y = 0;
419: }
420:
421: return option;
422: }
normalizetextwin.cpp
424: /**
425: * オプションの保存
426: * @param string option オプション
427: * @return なし
428: */
429: void saveOption(string option) {
430: #ifndef CMDAPP
431: // アプリケーション・ウィンドウの位置取得
432: WINDOWINFO windowInfo;
433: windowInfo.cbSize = sizeof(WINDOWINFO);
434: GetWindowInfo(hParent, &windowInfo);
435: hParent_X = (unsigned)windowInfo.rcWindow.left;
436: hParent_Y = (unsigned)windowInfo.rcWindow.top;
437: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
438: hParent_X = 0;
439: }
440: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
441: hParent_Y = 0;
442: }
443: #endif
444:
445: // XMLファイルへ書き込む
446: ptree pt;
447: ptree& child1 = pt.add("parameter.param", option);
448: child1.add("<xmlattr>.type", "option");
449: ptree& child2 = pt.add("parameter.param", (string)to_string(hParent_X));
450: child2.add("<xmlattr>.type", "wx");
451: ptree& child3 = pt.add("parameter.param", (string)to_string(hParent_Y));
452: child3.add("<xmlattr>.type", "wy");
453:
454: // clog << "option=" << option << endl;
455:
456: const int indent = 4;
457: write_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt, std::locale(),
458: xml_writer_make_settings<std::string>(' ', indent));
459: }
保存場所は、getMyPath 関数によって、ユーザーのAppDataの下にアプリケーションフォルダを作って、XMLファイルとして保存する。
この処理は、GUI/CUI共通だ。
解説:フロントエンド置換
pahooNormalizeText.cpp
200: /**
201: * フロントエンド置換:入力直後に文字列置換を行う
202: * @param string wsour 入力テキスト
203: * @return string 変換後文字列
204: */
205: wstring pahooNormalizeText::frontend_replace(wstring wsour) {
206: wstring tbl[2];
207:
208: // 文字列置換定義ファイルを読み込む
209: ifstream ifs(FRONT_REPLACE_CSV);
210: // 文字列置換定義ファイルが無い
211: if (! ifs) {
212: return wsour;
213: }
214:
215: // 1行ずつ変換
216: string ss;
217: string delim = "\t";
218: list<string> list_string;
219: wstring wstr = L"";
220: while (getline(ifs, ss)) {
221: int n = 0;
222: boost::split(list_string, ss, boost::is_any_of(delim));
223: BOOST_FOREACH(string s, list_string) {
224: tbl[n] = _SW(s);
225: n++;
226: if (n > 2) break;
227: }
228: wregex re(tbl[0]);
229: wsour = regex_replace(wsour, re, tbl[1]);
230: }
231:
232: return wsour;
233: }
OCRソフト等を利用して取得したテキストについて、特定のパターンが正しく読み取れない場合がある。そこで、誤認識パターンを正しいテキストに置換するのがフロントエンド置換機能である。
GUI版、CUI版ともに、すべての正規化処理の前に、定数 FRONT_REPLACE_CSV に定義された置換定義ファイルに従って、文字列の置換を行う。
FRONT_REPLACE_CSV には複数のパターンを置換することが可能で、このCSV形式ファイルにはシフトJISで次のように記述する。
置換前文字列1 (TAB) 置換後文字列1Excelなどを使って、タブ区切りでCSV形式ファイルに保存するといいだろう。
置換前文字列2 (TAB) 置換後文字列2
‥‥
何も記述しない(空ファイル)と、フロントエンド置換機能は働かない。
CUI用メインプログラム
normalizetextwin.cpp
886: /**
887: * CUI用メインプログラム
888: * @param int argc
889: * @paramm char* argv[]
890: * @return int リターンコード
891: */
892: int main(int argc, char* argv[]) {
893: wstring wsour = L"";
894: wstring wdest = L"";
895: string sour = "";
896:
897: // 正規化オプションの初期値
898: string lopt = OPTION_INIT;
899:
900: // pahooNormalizeTextオブジェクト
901: pNT = new pahooNormalizeText();
902:
903: // MeCab動作チェック
904: #ifdef MECAB
905: if (pNT->isError()) {
906: ErrorMessage = "エラー:" + _WS(pNT->getError());
907: cerr << ErrorMessage << endl;
908: return 1;
909: }
910: #endif
911:
912: // コマンドライン・オプションの定義
913: options_description options("コマンドライン・オプション");
914: options.add_options()
915: ("option,o", value<std::string>()->default_value(lopt), "正規化オプション")
916: ("sour,s", value<std::string>(), "テキストファイルから入力")
917: ("clip,c", "クリップボードから入力")
918: ("dest,d", value<std::string>(), "テキストファイルへ出力")
919: ("paste,p", "クリップボードへ出力")
920: ("help,h", "ヘルプ")
921: ("version,v", "バージョン情報")
922: ;
923: // コマンドライン・オプションの取得
924: variables_map vm;
925: try {
926: store(parse_command_line(argc, argv, options), vm);
927: } catch(const boost::program_options::error_with_option_name& e) {
928: ErrorMessage = e.what();
929: cerr << ErrorMessage << endl;
930: return 1;
931: }
932: notify(vm);
933:
934: // 正規化オプション
935: auto opt = vm["option"].as<string>();
936:
937: // ヘルプ情報
938: if (vm.count("help")) {
939: wdest = _SW(Help);
940: // バージョン情報
941: } else if (vm.count("version")) {
942: wdest = _SW(Version);
943:
944: // 正規化実行
945: } else {
946: // 入力ファイル
947: wsour = _SW(DEF_SOUR);
948: if (vm.count("sour")) {
949: auto infile = vm["sour"].as<string>();
950: ifstream ifs(infile.c_str());
951: if (ifs.fail()) {
952: ErrorMessage = infile + " が見つかりません";
953: cerr << ErrorMessage << endl;
954: return 1;
955: }
956: string ss;
957: while (getline(ifs, ss)) {
958: sour += ss + "\r";
959: }
960: wsour = _SW(sour);
961: // クリップボードから
962: } else if (vm.count("clip")) {
963: char *ss = getClipboardData();
964: sour = ss;
965: wsour = _SW(sour);
966: // 標準入力から
967: } else {
968: cin >> sour;
969: if (sour.length() > 0) {
970: wsour = _SW(sour);
971: }
972: }
973: wdest = pNT->normalizeText(wsour, opt.c_str(), FALSE);
974: }
975:
976: // 出力ファイル
977: if (vm.count("dest")) {
978: // 改行コード置換
979: wregex re(_SW("\r"));
980: wdest = regex_replace(wdest, re, _SW("\n"));
981: auto outfile = vm["dest"].as<string>();
982: ofstream ofs(outfile.c_str());
983: ofs << _WS(wdest);
984: if (ofs.bad()) {
985: ErrorMessage = outfile + " への書き込みに失敗しました";
986: cerr << ErrorMessage << endl;
987: return 1;
988: }
989: ofs.close();
990: // クリップボードへ
991: } else if (vm.count("paste")) {
992: // 改行コード置換
993: wregex re(_SW("\r"));
994: wdest = regex_replace(wdest, re, _SW("\n"));
995: setClipboardData(_WS(wdest));
996: // 標準出力へ
997: } else {
998: // 改行コード置換
999: wregex re(_SW("\r"));
1000: wdest = regex_replace(wdest, re, _SW("\n"));
1001: clog << _WS(wdest);
1002: }
1003:
1004: // オブジェクト解放
1005: delete pNT;
1006:
1007: return 0;
1008: }
GUI版の WinMain 関数はマルチスレッド対応で、すべてのスレッドが終わる前にコマンドラインに戻ってきてしまう。また、標準入出力のパイプ処理への対応も面倒であるため、CUI版は実行ファイルを分けることにした。

コマンドラインオプションの解釈は、Boost C++ライブラリ の options を利用した。
参考サイト
- PHPで日本語テキストを正規化:ぱふぅ家のホームページ
- MeCab
- PHPで MeCabのユーザー辞書を作成する:ぱふぅ家のホームページ
- C++ でテキスト中の和暦・西暦年号を統一する:ぱふぅ家のホームページ
- WiX によるWindowsインストーラー作成:ぱふぅ家のホームページ
- C++ 開発環境の準備:ぱふぅ家のホームページ
また、ソースを共通化してコマンドライン版アプリケーションを作り、バッチや他のプログラムから流し込んだテキストを正規化することができるようにする。
「PHPで日本語テキストを正規化」で作ったPHPプログラムをC++に移植したものである。
(2025年8月30日)表記ゆれ統一機能を追加
(2025年8月2日)使用ライブラリ更新,辞書更新
(2025年3月29日)使用ライブラリ更新,辞書更新
(2024年12月15日)使用ライブラリ更新,辞書更新