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

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

(2020 年 11 月 15 日)数字変換に漢字(単純)を追加,オプションの読込・保存機能追加,変換テキスト長を約 5 千文字に増強,バグ修正

目次

サンプル・プログラム

圧縮ファイルの内容
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テキスト正規化クラス(ヘッダ)

使用ライブラリ

漢数字と固有名詞(例:『千と千尋の神隠し』)を識別するために、形態素解析エンジン「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"" に設定する。

MeCabの導入

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

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

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

プログラムの流れ

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

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

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

MeCab はクラス化されており、createTagger メソッドで呼び出す。引数は、MeCab.exe のオプションとほぼ同じである。
ここで、前述の 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:         { 0x00000x4E000x4E8C0x4E090x56DB0x4E940x516D,    //一〜九
0190:             0x4E030x516B0x4E5D0xFF0E0xFF0D };             //.−
0191:     static wchar_t kantbl3[] = { 0x00000x53410x767E0x5343 };   //十百千
0192:     static wchar_t kantbl4[] = { 0x00000x4E070x51040x51460x4EAC };   //万億兆京
0193: 
0194:     wstring outstr = L"";;
0195:     wstring ws2;
0196:     wchar_t wch1wch2;
0197:     int m = (int)instr.length() / 4;
0198:     //一、万、億、兆‥‥の繰り返し
0199:     for (int i = 0; i <= mi++) {
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_twch  = (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: }

半角数字を漢数字に変換するメソッドは num2kanji である。
右から左へ向かって逆順に処理し、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:         { 0x30070x4E000x4E8C0x4E090x56DB0x4E940x516D,    //一〜九
0318:             0x4E030x516B0x4E5D0xFF0E0xFF0D };             //.−
0319: 
0320:     //左から1文字ずつ処理
0321:     wstring wdest = L"";
0322:     wstring wstr;
0323:     wchar_twch;
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: }

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

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

0244: /**
0245:  * 半角数字を位取り記法に変換する
0246:  * @param wstring instr 半角数字(小数,負数,指数表記は未対応)
0247:  * @return wstring 位取り記法
0248: */
0249: wstring pahooNormalizeText::num2scale(wstring instr) {
0250:     static wchar_t kantbl[] = { 0x00000x4E070x51040x51460x4EAC };    //万億兆京
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(wsreL"$1") + outstr;
0277:                 flag = TRUE;
0278:             }
0279:             ws = L"";
0280:         }
0281:     }
0282:     outstr = ws + outstr;
0283: 
0284:     return outstr;
0285: }

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

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

0363: /**
0364:  * 漢数字を半角数字に変換する
0365:  * @param wstring kanji 漢数字
0366:  * @param int mode 出力書式/1=3桁カンマ区切り,2=漢字混じり, それ以外=ベタ打ち
0367:  * @return wstring 半角数字
0368: */
0369: wstring pahooNormalizeText::kan2num(wstring kanjiint 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(kanjimt1re1)) {
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(amt1re2)) {
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: }

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

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

0450: /**
0451:  * 日本語テキストを半角に統一
0452:  * @param wstring sour 変換元テキスト
0453:  * @param bool    trim 行頭・行末の空白を除くかどうか
0454:  * @return string  変換後テキスト
0455: */
0456: wstring pahooNormalizeText::toHankaku(wstring wsourbool trim) {
0457:     regex sep{"\\t|,"};
0458:     wsmatch mt1mt2mt3;
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(wsourre6_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 surfacepos;
0485: 
0486:     //1行ずつ読み込む
0487:     string ss0;
0488:     stringstream ss;
0489:     ss << words;
0490:     while(ss && getline(ssss0)) {
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(surfacemt1re2)) {
0504:             dest += this->kan2num(mt1[1].str(), 2) + mt1[2].str();
0505:         } else if (flag == FALSE) {
0506:             //漢数字の1文字目
0507:             if (regex_search(surfacemt1pat_kannum) && regex_search(posmt2re1)) {
0508:                 numstr = surface;
0509:                 flag = TRUE;
0510:             //数字ではない
0511:             } else {
0512:                 dest += surface;
0513:             }
0514:         } else {
0515:             //漢数字の2文字目以降
0516:             if (regex_search(surfacemt1pat_kannum)) {
0517:                 numstr += surface;
0518:                 flag = TRUE;
0519:             //数字以外
0520:             } else {
0521:                 //"漢数字+大"の場合はそのまま
0522:                 if (regex_search(surfacemt1re3)) {
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(destre4L"");
0541:         wss = regex_replace(wssre5L"");
0542:         dest = wss + L"\\n";
0543:     }
0544: 
0545:     return wconvString(destLCMAP_HALFWIDTH);
0546: }

正規化処理の前段では、すべてのテキストを半角に変換する。そのためのメソッドが toHankaku である。
MeCab による形態素解析を実行するメソッド parse によってテキストを形態素の分解、個々の形態素に対して、kan2num を実行するかどうかを判断する。
最後に、Win32API を呼び出す wconvString を使って、変換可能な全ての文字を半角にする。

解説:日本語テキストを全角に変換

0570: /**
0571:  * 半角→全角変換に変換
0572:  * @param wstring sour 変換元テキスト
0573:  * @param bool (*func) 該当文字判定関数
0574:  * @return wstring  変換後テキスト
0575: */
0576: wstring pahooNormalizeText::han2zen(const wstring wsourbool (*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_twch = (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(wssLCMAP_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(wssLCMAP_FULLWIDTH);
0601:     }
0602: 
0603:     return wdest;
0604: }

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

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

0606: /**
0607:  * 日本語テキストを正規化する
0608:  * @param wstring sour   漢数字混じりテキスト
0609:  * @param char*   option 変換オプション
0610:  * @param bool    trim   行頭・行末の空白を除くかどうか
0611:  * @return wstring 変換後テキスト
0612: */
0613: wstring pahooNormalizeText::normalizeText(wstring wsourconst charoptionbool trim) {
0614:     wstring wdest = wsour;
0615: 
0616:     //いったん半角に
0617:     wdest = this->toHankaku(wdesttrim);
0618: 
0619:     //全角文字と隣り合う空白文字を除く
0620:     wregex re1(_SW("[  \\t]+([^!-~].)"));
0621:     wdest = regex_replace(wdestre1L"$1");
0622: 
0623:     //英字:半角→全角
0624:     if (strchr(optionOPTION_ALP_ZEN) != NULL) {
0625:         wdest = this->han2zen(wdest_alphabet);
0626:     }
0627:     //数字:位取り記法
0628:     if (strchr(optionOPTION_NUM_SCALE) != NULL) {
0629:         wdest = this->bignum2scale(wdest);
0630:     }
0631:     //数字:半角→全角
0632:     if (strchr(optionOPTION_NUM_ZEN) != NULL) {
0633:         wdest = this->han2zen(wdest_decimal);
0634:     //数字:半角→漢数字
0635:     } else if (strchr(optionOPTION_NUM_KAN) != NULL) {
0636:         wdest = this->num2kan(wdest);
0637:     //数字:半角→漢数字(単純)
0638:     } else if (strchr(optionOPTION_NUM_KAN2) != NULL) {
0639:         wdest = this->num2kanSimple(wdest);
0640:     }
0641:     //記号:半角→全角
0642:     if (strchr(optionOPTION_YAK_ZEN) != NULL) {
0643:         wdest = this->han2zen(wdest_yakumono);
0644:     }
0645:     //カタカナ:半角→全角
0646:     if (strchr(optionOPTION_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(wdestre2L" $1");
0655: 
0656:     return wdest;
0657: }

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

解説:定数など

0036: // 定数など ==================================================================
0037: #define MAKER     "pahoo.org"                //作成者
0038: #define APPNAME     "normalizetextwin"        //アプリケーション名
0039: #define APPNAMEJP "テキストの正規化"        //アプリケーション名(日本語)
0040: #define APPVERSION "1.1"                    //バージョン
0041: #define APPYEAR     "2020"                    //作成年
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: pahooNormalizeTextpNT;
0064: 
0065: //char*バッファサイズ
0066: #define SIZE_BUFF 5120
0067: 
0068: //変換元テキスト(初期値)
0069: #define DEF_SOUR "『千と千尋の神隠し』(せんとちひろのかみかくし)は、スタジオジブリ制作の長編アニメーション映画。監督は宮崎駿。二〇〇一年七月二十\日に日本公開。興行収入は三百億円を超え、日本歴代興行収入第一位を達成した。英語のタイトルは『Spirited Away』。\n千尋という名の十\歳の少女が、引っ越し先へ向かう途中に立ち入ったトンネルから、神々の世界へ迷い込んでしまう物語。"

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

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

0377: /**
0378:  * 正規化オプションを設定する
0379:  * @param HWND hDlg   ウィンドウ・ハンドラ
0380:  * @param string opt   正規化オプション
0381:  * @return なし
0382:  */
0383: void setOption(HWND hDlgstring opt) {
0384:     for (int i = 0; i < (int)opt.length(); i++) {
0385:         charch = (char*)opt.substr(i, 1).c_str();
0386:         switch (*ch) {
0387:         case OPTION_ALP_HAN:
0388:             SendMessage(GetDlgItem(hDlgIDC_RADIO_ALP_HAN),
0389:                             BM_SETCHECKBST_CHECKED,   0);
0390:             SendMessage(GetDlgItem(hDlgIDC_RADIO_ALP_ZEN),
0391:                             BM_SETCHECKBST_UNCHECKED, 0);
0392:             break;
0393:         case OPTION_ALP_ZEN:
0394:             SendMessage(GetDlgItem(hDlgIDC_RADIO_ALP_HAN),
0395:                             BM_SETCHECKBST_UNCHECKED, 0);
0396:             SendMessage(GetDlgItem(hDlgIDC_RADIO_ALP_ZEN),
0397:                             BM_SETCHECKBST_CHECKED,   0);
0398:             break;
0399:         case OPTION_NUM_HAN:
0400:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_HAN),
0401:                             BM_SETCHECKBST_CHECKED,   0);
0402:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_ZEN),
0403:                             BM_SETCHECKBST_UNCHECKED, 0);
0404:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN),
0405:                             BM_SETCHECKBST_UNCHECKED, 0);
0406:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN2),
0407:                             BM_SETCHECKBST_UNCHECKED, 0);
0408:             break;
0409:         case OPTION_NUM_ZEN:
0410:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_HAN),
0411:                             BM_SETCHECKBST_UNCHECKED, 0);
0412:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_ZEN),
0413:                             BM_SETCHECKBST_CHECKED,   0);
0414:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN),
0415:                             BM_SETCHECKBST_UNCHECKED, 0);
0416:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN2),
0417:                             BM_SETCHECKBST_UNCHECKED, 0);
0418:             break;
0419:         case OPTION_NUM_KAN:
0420:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_HAN),
0421:                             BM_SETCHECKBST_UNCHECKED, 0);
0422:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_ZEN),
0423:                             BM_SETCHECKBST_UNCHECKED, 0);
0424:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN),
0425:                             BM_SETCHECKBST_CHECKED,   0);
0426:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN2),
0427:                             BM_SETCHECKBST_UNCHECKED, 0);
0428:             break;
0429:         case OPTION_NUM_KAN2:
0430:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_HAN),
0431:                             BM_SETCHECKBST_UNCHECKED, 0);
0432:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_ZEN),
0433:                             BM_SETCHECKBST_UNCHECKED, 0);
0434:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN),
0435:                             BM_SETCHECKBST_UNCHECKED, 0);
0436:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN2),
0437:                             BM_SETCHECKBST_CHECKED,   0);
0438:             break;
0439:         case OPTION_NUM_SCALE:
0440:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_SCALE),
0441:                             BM_SETCHECKBST_CHECKED,   0);
0442:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_NOSCALE),
0443:                             BM_SETCHECKBST_UNCHECKED, 0);
0444:             break;
0445:         case OPTION_NUM_NOSCALE:
0446:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_SCALE),
0447:                             BM_SETCHECKBST_UNCHECKED, 0);
0448:             SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_NOSCALE),
0449:                             BM_SETCHECKBST_CHECKED,   0);
0450:             break;
0451:         case OPTION_YAK_HAN:
0452:             SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_HAN),
0453:                             BM_SETCHECKBST_CHECKED,   0);
0454:             SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_ZEN),
0455:                             BM_SETCHECKBST_UNCHECKED, 0);
0456:             break;
0457:         case OPTION_YAK_ZEN:
0458:             SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_HAN),
0459:                             BM_SETCHECKBST_UNCHECKED, 0);
0460:             SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_ZEN),
0461:                             BM_SETCHECKBST_CHECKED,   0);
0462:             break;
0463:         case OPTION_KANA_HAN:
0464:             SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_HAN),
0465:                             BM_SETCHECKBST_CHECKED,   0);
0466:             SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_ZEN),
0467:                             BM_SETCHECKBST_UNCHECKED, 0);
0468:             break;
0469:         case OPTION_KANA_ZEN:
0470:             SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_HAN),
0471:                             BM_SETCHECKBST_UNCHECKED, 0);
0472:             SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_ZEN),
0473:                             BM_SETCHECKBST_CHECKED,   0);
0474:             break;
0475:         case OPTION_SPEC_HAN:
0476:             SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_HAN),
0477:                             BM_SETCHECKBST_CHECKED,   0);
0478:             SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_ZEN),
0479:                             BM_SETCHECKBST_UNCHECKED, 0);
0480:             break;
0481:         case OPTION_SPEC_ZEN:
0482:             SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_HAN),
0483:                             BM_SETCHECKBST_UNCHECKED, 0);
0484:             SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_ZEN),
0485:                             BM_SETCHECKBST_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(hDlgIDC_RADIO_ALP_HAN), BM_GETCHECK, 0, 0)) {
0501:         opt += OPTION_ALP_HAN;
0502:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_ALP_ZEN), BM_GETCHECK, 0, 0)) {
0503:         opt += OPTION_ALP_ZEN;
0504:     }
0505:     if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_HAN), BM_GETCHECK, 0, 0)) {
0506:         opt += OPTION_NUM_HAN;
0507:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_ZEN), BM_GETCHECK, 0, 0)) {
0508:         opt += OPTION_NUM_ZEN;
0509:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN), BM_GETCHECK, 0, 0)) {
0510:         opt += OPTION_NUM_KAN;
0511:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_KAN2), BM_GETCHECK, 0, 0)) {
0512:         opt += OPTION_NUM_KAN2;
0513:     }
0514:     if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_SCALE), BM_GETCHECK, 0, 0)) {
0515:         opt += OPTION_NUM_SCALE;
0516:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_NUM_NOSCALE), BM_GETCHECK, 0, 0)) {
0517:         opt += OPTION_NUM_NOSCALE;
0518:     }
0519:     if (SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_HAN), BM_GETCHECK, 0, 0)) {
0520:         opt += OPTION_YAK_HAN;
0521:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_YAK_ZEN), BM_GETCHECK, 0, 0)) {
0522:         opt += OPTION_YAK_ZEN;
0523:     }
0524:     if (SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_HAN), BM_GETCHECK, 0, 0)) {
0525:         opt += OPTION_KANA_HAN;
0526:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_KANA_ZEN), BM_GETCHECK, 0, 0)) {
0527:         opt += OPTION_KANA_ZEN;
0528:     }
0529:     if (SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_HAN), BM_GETCHECK, 0, 0)) {
0530:         opt += OPTION_SPEC_HAN;
0531:     } else if (SendMessage(GetDlgItem(hDlgIDC_RADIO_SPEC_ZEN), BM_GETCHECK, 0, 0)) {
0532:         opt += OPTION_SPEC_ZEN;
0533:     }
0534: 
0535:     return opt;
0536: }

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

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

0123: /**
0124:  * AppDataのパスを取得
0125:  * @param char* appname アプリケーション名
0126:  * @return TCHAR* パス
0127:  */
0128: TCHARgetMyPath(const charappname) {
0129:     static TCHAR myPath[MAX_PATH] = "";
0130: 
0131:     if (strlen(myPath) == 0) {
0132:         if (SHGetSpecialFolderPath(NULLmyPathCSIDL_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)myPathNULL);
0140:             strcat(myPath_T("\\pahoo.org"));
0141:             CreateDirectory((LPCTSTR)myPathNULL);
0142:             strcat(myPath_T("\\"));
0143:             strcat(myPath_T(appname));
0144:             CreateDirectory((LPCTSTR)myPathNULL);
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(ifsoption);
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: }

プログラム起動時に loadOption 関数によって、正規化オプションを読み込む。また、終了時に saveOption 関数によって、正規化オプションを保存する。変数 option の値をテキスト・ファイルとして読み書きしている。
保存場所は、getMyPath 関数によって、ユーザーの AppData の下にアプリケーションフォルダを作って保存する。
この処理は、GUI/CUI 共通だ。

CUI用メインプログラム

0712: /**
0713:  * CUI用メインプログラム
0714:  * @param int argc
0715:  * @paramm char* argv[]
0716:  * @return int リターンコード
0717:  */
0718: int main(int argccharargv[]) {
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(argcargvoptions), vm);
0738:     } catch(const boost::program_options::error_with_option_namee) {
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(ifsss)) {
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(wsouropt.c_str(), FALSE);
0784:     }
0785: 
0786:     //出力ファイル
0787:     if (vm.count("dest")) {
0788:         //改行コード置換
0789:         wregex re(_SW("\r"));
0790:         wdest = regex_replace(wdestre_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(wdestre_SW("\n"));
0805:         cout << _WS(wdest);
0806:     }
0807: 
0808:     //オブジェクト解放
0809:     delete pNT;
0810: 
0811:     //オプション保存
0812:     saveOption(lopt);
0813: 
0814:     return 0;
0815: }

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

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

参考サイト

(この項おわり)
header