C++ でテキストの正規化

(1/1)
>C++でテキストの正規化
日本語テキストに混在する全角・半角文字を統一したり、漢数字を算用数字に変換したり、その逆の変換を一気に行うことができる正規化アプリケーションを作る。正規化したテキストは、クリップボードにコピーしたり、テキストファイルに保存することができる。
また、ソースを共通化してコマンドライン版アプリケーションを作り、バッチや他のプログラムから流し込んだテキストを正規化することができるようにする。
PHPで日本語テキストを正規化」で作ったPHPプログラムをC++に移植したものである。

(2025年8月30日)表記ゆれ統一機能を追加
(2025年8月2日)使用ライブラリ更新,辞書更新
(2025年3月29日)使用ライブラリ更新,辞書更新
(2024年12月15日)使用ライブラリ更新,辞書更新

目次

サンプル・プログラム

圧縮ファイルの内容
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/makefileGUI版ビルド
sour/makefile_cmdCUI版ビルド
normalizetextwin.cpp 更新履歴
バージョン 更新日 内容
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ユーザー辞書更新,使用ライブラリ更新
pahooNormalizeText.cpp 更新履歴
バージョン 更新日 内容
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億が混在する場合にも対応

使用ライブラリ

漢数字と固有名詞(例:『千と千尋の神隠し』)を識別するために、形態素解析エンジン「MeCab (めかぶ) 」を利用する。導入方法は後述する。
また、CUI版でコマンダイン・オプションを操作する場合などに、オープンソースのライブラリ Boost C++ライブラリが必要になる。Boost C++ライブラリの導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

リソースの準備

Eclipse を起動し、新規プロジェクト normalizetextwin を用意する。
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」の導入

本プログラムはオープンソースの日本語形態素解析システム「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 を使い iconvMeCab ライブラリをインストールできる。
64ビット版:
pacman -S mingw-w64-x86_64-libiconv
pacman -S mingw-w64-x86_64-mecab


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

プログラムの流れ

>C++でテキストの正規化
プログラムの流れは上図の通りである。
変換ボタンがクリックされたら、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"

正規化オプションは、英文字1文字で識別するようにしている。これを同時に複数指定することができる。ただし、aとAのように排他的であるオプションは、同時に指定しても意味をなさない。

解説: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");

MeCab はクラス化されており、createTagger メソッドで呼び出す。引数は、MeCab.exe のオプションとほぼ同じである。
ここで、前述の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 = 0i <mi++) {
 258:         ws2 = L"";
 259:         // 一、十、百、千の繰り返し
 260:         for (int j = 0j < 4j++) {
 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 = 0k < (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: }

半角数字を漢数字に変換するメソッドは num2kanji である。
右から左へ向かって逆順に処理し、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 = 0pos < (int)wsour.length(); pos++) {
 384:         flag = FALSE;
 385:         wstr = wsour.substr(pos, 1);
 386:         wch  = (wchar_t*)wstr.c_str();
 387:         for (int k = 0k < (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: }

半角数字を漢数字(単純)に変換するメソッドは num2kanSimple である。2020年(令和2年)を「二〇二〇年」のように変換する。
左から右へ向かって、算用数字にマッチする文字があれば漢数字に置換する。

解説:半角数字を位取り記法に変換

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() - 1pos >0pos--) {
 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: }

「三百億」を算用数字に変換すると「30000000000」となってしまい、視認性が悪くなる。そこで、算用数字に変換した場合でも「300億」になるよう、半角数字を位取り記法に変換するメソッド num2scale を用意した。
前述の num2kanji と同じように右から左へ向かって逆順に処理するが、変換するのは万、億、兆、京の単位だけである。

解説:漢数字を半角数字に変換

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 - 1pos >0pos--) {
 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: }

漢数字を算用数字に変換するメソッド kan2num である。単位としては「京」まで対応している。
前述の num2kanji と同じように右から左へ向かって逆順に処理するが、long long型の整数として扱い、最後にテキストに変換する。

解説:日本語テキストを半角に統一

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: }

正規化処理の前段では、すべてのテキストを半角に変換する。そのためのメソッドが toHankaku である。
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: }

たとえば「幽遊白書」という単語を、正式な綴りの「幽☆遊☆白書」に変換するのが、ユーザー定義メソッド variable2standard である。このような変換を表記ゆれの統一と呼ぶ

表記ゆれ統一辞書ファイルは "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 = 0i < 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: }

前述の toHankaku で半角になったテキストから、正規化オプションによって該当する文字だけ全角に変換するメソッドが han2zen である。
文字種変換は、Win32APIを呼び出す wconvString を利用している。

解説:日本語テキストを正規化する

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: }

これまで紹介してきたメソッドを利用し、正規化オプションにもとづいて日本語テキストを正規化するメソッドが normalizeText である。

オプション 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千尋という名の十\歳の少女が、引っ越し先へ向かう途中に立ち入った隧道から、神々の世界へ迷い込んでしまう物語。"

GUIおよびCUI版のプログラムは、"normalizetextwin.cpp" に記述しており、マクロ定数 CMDAPP が定義されている場合にはCUI版が、そうでない場合にはGUI版がビルドできるようにしてある。
とくに注意記載が無い限り、定数は自由に変更できる。

解説:正規化オプション取得・設定

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 = 0i < (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: }

GUI版は、正規化オプションをラジオボタンに設定したり取り出すために、関数 setOptiongetOption の2種類の関数を用意した。

解説:正規化オプション読込・保存

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 typeit.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: }

プログラム起動時に loadOption 関数によって、正規化オプションを読み込む。また、終了時に saveOption 関数によって、正規化オプションを保存する。変数 option の値をテキスト・ファイルとして読み書きしている。
保存場所は、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: }

バージョン1.8でフロントエンド置換機能を追加した、
OCRソフト等を利用して取得したテキストについて、特定のパターンが正しく読み取れない場合がある。そこで、誤認識パターンを正しいテキストに置換するのがフロントエンド置換機能である。
GUI版、CUI版ともに、すべての正規化処理の前に、定数 FRONT_REPLACE_CSV に定義された置換定義ファイルに従って、文字列の置換を行う。
FRONT_REPLACE_CSV には複数のパターンを置換することが可能で、このCSV形式ファイルにはシフトJISで次のように記述する。
置換前文字列1 (TAB) 置換後文字列1
置換前文字列2 (TAB) 置換後文字列2
‥‥
Excelなどを使って、タブ区切りでCSV形式ファイルに保存するといいだろう。
何も記述しない(空ファイル)と、フロントエンド置換機能は働かない。

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: }

CUI版は、昔ながらの main 関数ではじまる。
GUI版の WinMain 関数はマルチスレッド対応で、すべてのスレッドが終わる前にコマンドラインに戻ってきてしまう。また、標準入出力のパイプ処理への対応も面倒であるため、CUI版は実行ファイルを分けることにした。

コマンドラインオプションの解釈は、Boost C++ライブラリoptions を利用した。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header