
目次
サンプル・プログラム
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 | テキスト正規化クラス(ヘッダ) |
sour/makefile | GUI版ビルド |
sour/makefile_cmd | CUI版ビルド |
使用ライブラリ
また、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の導入
簡単に振り返っておくと、入力されたテキストを日本語の品詞に分解する形態素解析エンジンと呼ばれるプログラムである。
なぜテキストの正規化に形態素解析エンジンが必要かというと、たとえば「千と千尋の神隠しという映画を見た」というテキストをそのまま正規化してしまうと、「千」を数字と解釈し、「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 メソッドを使って半角か可能な文字を全て半角にする。その後、正規化オプションの値に応じて全角化、漢数字化を行ってゆく。
解説:テキスト正規化クラス
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: #define OPTION_CONTROL_DEL 'c' //制御文字を削除する
0047:
0048: //正規化オプションの初期値
0049: #define OPTION_INIT "anYSHF"
解説:MeCabの呼び出し
0119: private:
0120: 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" などを指定する。
解説:半角数字を漢数字に変換
0221: /**
0222: * 半角数字を漢数字に変換する
0223: * @param wstring instr 半角数字
0224: * 小数、負数に対応;指数表記には未対応
0225: * カンマは削除
0226: * @return wstring 漢数字
0227: */
0228: wstring pahooNormalizeText::num2kanji(wstring instr) {
0229: static wchar_t kantbl1[] =
0230: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
0231: L'8', L'9', L'.', L'-' };
0232: static wchar_t kantbl2[] =
0233: { 0x0000, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, //一〜九
0234: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; //.−
0235: static wchar_t kantbl3[] = { 0x0000, 0x5341, 0x767E, 0x5343 }; //十百千
0236: static wchar_t kantbl4[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; //万億兆京
0237:
0238: wstring outstr = L"";;
0239: wstring ws2;
0240: wchar_t wch1, wch2;
0241: int m = (int)instr.length() / 4;
0242: //一、万、億、兆‥‥の繰り返し
0243: for (int i = 0; i <= m; i++) {
0244: ws2 = L"";
0245: //一、十、百、千の繰り返し
0246: for (int j = 0; j < 4; j++) {
0247: int pos = instr.length() - i * 4 - j - 1;
0248: if (pos >= 0) {
0249: wchar_t* wch = (wchar_t*)instr.substr(pos, 1).c_str();
0250: if (*wch == L',') continue; //カンマは無視
0251: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
0252: //漢数字 or 半角数字のまま
0253: wch1 = 0x0000;
0254: if (*wch == kantbl1[k]) {
0255: wch1 = kantbl2[k];
0256: break;
0257: }
0258: }
0259: wch2 = 0x0000;;
0260: if ((j >= 0) && (j <= 3)) {
0261: wch2 = kantbl3[j];
0262: }
0263:
0264: //冒頭が「一」の場合の処理
0265: if (wch1 != 0x0000) {
0266: if ((wch1 == 0x4E00) && (wch2 != 0x0000)) {
0267: ws2 = (wstring){wch2} + ws2;
0268: } else if (wch2 != 0x0000) {
0269: ws2 = (wstring){wch1} + (wstring){wch2} + ws2;
0270: } else {
0271: ws2 = (wstring){wch1} + ws2;
0272: }
0273: }
0274: }
0275: }
0276: if (ws2 != L"") {
0277: if (kantbl4[i] == 0x0000) {
0278: outstr = ws2 + outstr;
0279: } else {
0280: outstr = ws2 + (wstring){kantbl4[i]} + outstr;
0281: }
0282: }
0283: }
0284:
0285: return outstr;
0286: }
右から左へ向かって逆順に処理し、4桁ごとに、万、億、兆、京の単位を付けてゆく。各々の4桁の中で、十、百、千を付ける。冒頭が「一」の場合は「一百」にならないよう配慮する。
解説:解説:半角数字を漢数字(単純)に変換
0351: /**
0352: * 半角数字を漢数字に変換する(単純変換)
0353: * @param wstring wsour 半角数字を含む文字列
0354: * @return wstring 漢数字
0355: */
0356: wstring pahooNormalizeText::num2kanSimple(wstring wsour) {
0357: static wchar_t kantbl1[] =
0358: { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
0359: L'8', L'9', L'.', L'-' };
0360: static wchar_t kantbl2[] =
0361: { 0x3007, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D, //一〜九
0362: 0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D }; //.−
0363:
0364: //左から1文字ずつ処理
0365: wstring wdest = L"";
0366: wstring wstr;
0367: wchar_t* wch;
0368: bool flag;
0369: for (int pos = 0; pos < (int)wsour.length(); pos++) {
0370: flag = FALSE;
0371: wstr = wsour.substr(pos, 1);
0372: wch = (wchar_t*)wstr.c_str();
0373: for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
0374: if (*wch == kantbl1[k]) {
0375: wdest += (wstring){kantbl2[k]};
0376: flag = TRUE;
0377: break;
0378: }
0379: }
0380: if (! flag) {
0381: wdest += *wch;
0382: }
0383: }
0384: return wdest;
0385: }
左から右へ向かって、算用数字にマッチする文字があれば漢数字に置換する。
解説:半角数字を位取り記法に変換
0288: /**
0289: * 半角数字を位取り記法に変換する
0290: * @param wstring instr 半角数字(小数,負数,指数表記は未対応)
0291: * @return wstring 位取り記法
0292: */
0293: wstring pahooNormalizeText::num2scale(wstring instr) {
0294: static wchar_t kantbl[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC }; //万億兆京
0295: //余計な'0'を除く
0296: wregex re(_SW("0+([1-9]+)|0+([1-9]+)"));
0297:
0298: //桁あふれ
0299: if (instr.length() > 20) {
0300: return instr;
0301: }
0302:
0303: //右から1文字ずつ処理
0304: wstring outstr = L"";
0305: wstring ws = L"";
0306: int i = 0;
0307: bool flag = FALSE;
0308: for (int pos = instr.length() - 1; pos >= 0; pos--) {
0309: if (flag) {
0310: outstr = (wstring){kantbl[(int)(i / 4)]} + outstr;
0311: flag = FALSE;
0312: }
0313: wstring ss = instr.substr(pos, 1);
0314: ws = ss + ws;
0315: i++;
0316: if (i % 4 == 0) {
0317: if ((ws == L"0000") || (ws == _SW("0000"))) {
0318: outstr = (wstring){kantbl[(int)(i / 4)]};
0319: } else {
0320: outstr = regex_replace(ws, re, L"$1") + outstr;
0321: flag = TRUE;
0322: }
0323: ws = L"";
0324: }
0325: }
0326: outstr = ws + outstr;
0327:
0328: return outstr;
0329: }
解説:漢数字を半角数字に変換
0407: /**
0408: * 漢数字を半角数字に変換する
0409: * @param wstring kanji 漢数字
0410: * @param int mode 出力書式/1=3桁カンマ区切り,2=漢字混じり, それ以外=ベタ打ち
0411: * @return wstring 半角数字
0412: */
0413: wstring pahooNormalizeText::kan2num(wstring kanji, int mode) {
0414: wstring dest = L"";
0415: wsmatch mt1;
0416:
0417: //全角=半角対応
0418: const wstring kan_num1 = _SW("0〇○1一壱2二弐3三参4四5五6六7七8八9九");
0419: const wstring kan_num2 = _SW("000111222333445566778899");
0420:
0421: //位取り
0422: const wstring kan_deci_sub = _SW("十\百千");
0423: const wstring kan_deci = _SW("万億兆京");
0424:
0425: //半角数字が混在していたら何もしない
0426: wregex re1(_SW("[0-9]+"));
0427: if (regex_search(kanji, mt1, re1)) {
0428: return kanji;
0429: }
0430:
0431: //右側から解釈していく
0432: size_t ll = kanji.length();
0433: wstring a = L"";
0434: long long int deci = 1;
0435: long long int deci_sub = 1;
0436: long long int m = 0;
0437: long long int n = 0;
0438:
0439: for (int pos = ll - 1; pos >= 0; pos--) {
0440: wstring c = kanji.substr(pos, 1);
0441: size_t ps1 = kan_num1.find(c);
0442: size_t ps2 = kan_deci_sub.find(c);
0443: size_t ps3 = kan_deci.find(c);
0444: if (ps1 != wstring::npos) {
0445: a = kan_num2.substr(ps1, 1) + a;
0446: } else if (ps2 != wstring::npos) {
0447: if (a != L"") {
0448: m = m + stol(a) * deci_sub;
0449: } else if (deci_sub != 1) {
0450: m = m + deci_sub;
0451: }
0452: a = L"";
0453: deci_sub = pow(10, ps2 + 1);
0454: } else if (ps3 != wstring::npos) {
0455: if (a != L"") {
0456: m = m + stol(a) * deci_sub;
0457: } else if (deci_sub != 1) {
0458: m = m + deci_sub;
0459: }
0460: n = m * (long long int)deci + n;
0461: m = 0;
0462: a = L"";
0463: deci_sub = 1;
0464: deci = (long long int)pow(10000, ps3 + 1);
0465: }
0466: }
0467:
0468: wstring ss = L"";
0469: wregex re2(_SW("^(0+)"));
0470: if (regex_search(a, mt1, re2)) {
0471: ss = mt1[1].str();
0472: }
0473: if (a != L"") {
0474: m = m + stol(a) * deci_sub;
0475: } else if (deci_sub != 1) {
0476: m = m + deci_sub;
0477: }
0478: n = m * deci + n;
0479:
0480: return to_wstring(n);
0481: }
解説:日本語テキストを半角に統一
0520: /**
0521: * 日本語テキストを半角に統一
0522: * @param wstring sour 変換元テキスト
0523: * @param bool trim 行頭・行末の空白を除くかどうか
0524: * @return string 変換後テキスト
0525: */
0526: wstring pahooNormalizeText::toHankaku(wstring wsour, bool trim) {
0527: regex sep{"\\t|,"};
0528: wsmatch mt1, mt2, mt3;
0529: //数字パターン
0530: wregex pat_kannum(_SW("^[^数]*[01234567890123456789○〇一二三四五六七八九十\百千万億兆京]+$"));
0531: //MeCabの品詞パターン
0532: wregex re1(_SW("[数幾]"));
0533: //月の漢数字
0534: wregex re2(_SW("([一二三四五六七八九十\]+)(月)"));
0535: //名詞接続の場合はそのまま
0536: wregex re3(_SW("名詞接続"));
0537: //行頭空白パターン
0538: wregex re4(_SW("^[ \\t\\n\\r]+"));
0539: //行末空白パターン
0540: wregex re5(_SW("[ \\t\\n\\r]+$"));
0541: //無変換パターン・その1
0542: wregex re71(_SW("副詞可.+"));
0543: wregex re72(_SW("接尾|格助詞"));
0544: //無変換パターン・その2
0545: wregex re8(_SW("^[千万億兆]+$"));
0546: //無変換パターン・その3
0547: wregex re9(_SW("一生"));
0548:
0549: //カンマ置換
0550: wregex re6(_SW(","));
0551: wsour = regex_replace(wsour, re6, _SW(","));
0552:
0553: //形態素に分解
0554: string input = _WS(wsour);
0555: const char *words = tagger->parse(input.c_str());
0556:
0557: //変換処理
0558: bool flag = FALSE;
0559: bool adverb = FALSE;
0560: wstring dest = L"";
0561: wstring numstr = L"";
0562: wstring surface, pos;
0563:
0564: //1行ずつ読み込む
0565: string ss0;
0566: stringstream ss;
0567: ss << words;
0568: while(ss && getline(ss, ss0)) {
0569: int cnt = 0;
0570: for (std::cregex_token_iterator end,
0571: ite{ss0.c_str(), ss0.c_str() + strlen(ss0.c_str()), sep, -1};
0572: ite != end; ++ite) {
0573: if (cnt == 0) surface = _SW((*ite).str().c_str());
0574: else if (cnt == 2) pos = _SW((*ite).str().c_str());
0575: cnt++;
0576: }
0577: // cout << _WS(surface) << " : " << _WS(pos) << endl;
0578:
0579: //最後
0580: if (surface == _SW("EOS")) {
0581: break;
0582: //月の処理
0583: } else if (regex_search(surface, mt1, re2)) {
0584: dest += this->kan2num(mt1[1].str(), 2) + mt1[2].str();
0585: //無変換
0586: } else if (regex_search(pos, mt1, re71) || regex_search(surface, mt1, re9)) {
0587: dest += surface;
0588: } else if (flag == FALSE) {
0589: //副詞可能な数字
0590: if (regex_search(pos, mt2, re71)) {
0591: numstr = surface;
0592: flag = TRUE;
0593: adverb = TRUE;
0594: //漢数字の1文字目
0595: } else if (regex_search(surface, mt1, pat_kannum) && regex_search(pos, mt2, re1)) {
0596: numstr = surface;
0597: flag = TRUE;
0598: adverb = FALSE;
0599: //数字ではない
0600: } else {
0601: dest += surface;
0602: }
0603: } else {
0604: //無変換(副詞可能+接尾)
0605: if (adverb && regex_search(pos, mt2, re72)) {
0606: dest += (numstr + surface);
0607: numstr = L"";
0608: flag = FALSE;
0609: adverb = FALSE;
0610: //漢数字の2文字目以降
0611: } else if (regex_search(surface, mt1, pat_kannum)) {
0612: numstr += surface;
0613: flag = TRUE;
0614: //数字以外
0615: } else {
0616: //名詞接続の場合はそのまま
0617: if (regex_search(pos, mt2, re3)) {
0618: dest += (numstr + surface);
0619: //無変換パターン
0620: } else if (regex_search(numstr, mt1, re8)) {
0621: dest += (numstr + surface);
0622: //副詞可能の場合
0623: } else if (adverb) {
0624: dest += (this->kanword2num(numstr) + surface);
0625: //ここまでの漢数字を半角数字に
0626: } else {
0627: dest += (this->kan2num(numstr, 2) + surface);
0628: }
0629: numstr = L"";
0630: flag = FALSE;
0631: adverb = FALSE;
0632: }
0633: }
0634: }
0635:
0636: //末尾処理
0637: if (flag == TRUE) {
0638: if (adverb == TRUE) {
0639: dest += numstr;
0640: } else {
0641: dest += this->kan2num(numstr, 2);
0642: }
0643: }
0644:
0645: //行頭・行末空白処理
0646: if (trim) {
0647: wstring wss = regex_replace(dest, re4, L"");
0648: wss = regex_replace(wss, re5, L"");
0649: dest = wss + L"\\n";
0650: }
0651:
0652: return wconvString(dest, LCMAP_HALFWIDTH);
0653: }
解説:日本語テキストを全角に変換
0679: /**
0680: * 半角→全角変換に変換
0681: * @param wstring sour 変換元テキスト
0682: * @param bool (*func) 該当文字判定関数
0683: * @return wstring 変換後テキスト
0684: */
0685: wstring pahooNormalizeText::han2zen(const wstring wsour, bool (*func)(wchar_t wch)) {
0686: wstring wss = L"";
0687: wstring wdest = L"";
0688: bool flag = FALSE;
0689:
0690: //先頭から1文字ずつ
0691: for (size_t i = 0; i < wsour.length(); i++) {
0692: wchar_t* wch = (wchar_t*)wsour.substr(i, 1).c_str();
0693: //該当文字ならwss0へ
0694: if (func(*wch)) {
0695: wss += (wstring){*wch};
0696: flag = TRUE;
0697: //半角→全角変換
0698: } else if (flag) {
0699: wdest += wconvString(wss, LCMAP_FULLWIDTH);
0700: wdest += wsour.substr(i, 1);
0701: flag = FALSE;
0702: wss = L"";
0703: } else {
0704: wdest += wsour.substr(i, 1);
0705: }
0706: }
0707: //最後の1文字が該当文字
0708: if (flag) {
0709: wdest += wconvString(wss, LCMAP_FULLWIDTH);
0710: }
0711:
0712: return wdest;
0713: }
解説:日本語テキストを正規化する
0715: /**
0716: * 日本語テキストを正規化する
0717: * @param wstring sour 漢数字混じりテキスト
0718: * @param char* option 変換オプション
0719: * @param bool trim 行頭・行末の空白を除くかどうか
0720: * @return wstring 変換後テキスト
0721: */
0722: wstring pahooNormalizeText::normalizeText(wstring wsour, const char* option, bool trim) {
0723: wstring wdest = wsour;
0724:
0725: //フロントエンド置換
0726: wdest = this->frontend_replace(wdest);
0727:
0728: //制御文字を削除する
0729: if (strchr(option, OPTION_CONTROL_DEL) != NULL) {
0730: wregex re2(_SW("([\\t\\r\\r]+)"));
0731: wdest = regex_replace(wdest, re2, L"");
0732: }
0733:
0734: //いったん半角に
0735: wdest = this->toHankaku(wdest, trim);
0736:
0737: //全角文字と隣り合う空白文字を除く
0738: wregex re1(_SW("[ \\t]+([^!-~].)"));
0739: wdest = regex_replace(wdest, re1, L"$1");
0740:
0741: //英字:半角→全角
0742: if (strchr(option, OPTION_ALP_ZEN) != NULL) {
0743: wdest = this->han2zen(wdest, _alphabet);
0744: }
0745: //数字:位取り記法
0746: if (strchr(option, OPTION_NUM_SCALE) != NULL) {
0747: wdest = this->bignum2scale(wdest);
0748: }
0749: //数字:半角→全角
0750: if (strchr(option, OPTION_NUM_ZEN) != NULL) {
0751: wdest = this->han2zen(wdest, _decimal);
0752: //数字:半角→漢数字
0753: } else if (strchr(option, OPTION_NUM_KAN) != NULL) {
0754: wdest = this->num2kan(wdest);
0755: //数字:半角→漢数字(単純)
0756: } else if (strchr(option, OPTION_NUM_KAN2) != NULL) {
0757: wdest = this->num2kanSimple(wdest);
0758: }
0759: //記号:半角→全角
0760: if (strchr(option, OPTION_YAK_ZEN) != NULL) {
0761: wdest = this->han2zen(wdest, _yakumono);
0762: }
0763: //カタカナ:半角→全角
0764: if (strchr(option, OPTION_KANA_ZEN) != NULL) {
0765: wdest = this->han2zen(wdest, _katakana);
0766: }
0767: //小数点の全角を半角に変換
0768: wdest = this->hanfloat(wdest);
0769:
0770: //半角文字と隣り合う全角空白は半角空白へ
0771: wregex re2(_SW("[ ]+([!-~].)"));
0772: wdest = regex_replace(wdest, re2, L" $1");
0773:
0774: return wdest;
0775: }
解説:定数など
0038: // 定数など ==================================================================
0039: #define MAKER "pahoo.org" //作成者
0040: #define APPNAME "normalizetextwin" //アプリケーション名
0041: #define APPNAMEJP "テキストの正規化" //アプリケーション名(日本語)
0042: #define APPVERSION "1.8" //バージョン
0043: #define APPYEAR "2020-2022" //作成年
0044: #define REFERENCE "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-18-01.shtm" //参考サイト
0045:
0046: //ヘルプ・ファイル
0047: #define HELPFILE ".\\etc\\help.chm"
0048:
0049: //デフォルト保存ファイル名
0050: #define SAVEFILE "normalizetextwin.txt"
0051:
0052: //オプション保存ファイル名:変更不可
0053: #define OPTIONFILE "option.txt"
0054:
0055: //エラー・メッセージ格納用:変更不可
0056: string ErrorMessage;
0057:
0058: //現在のインターフェイス
0059: HINSTANCE hInst;
0060:
0061: //アプリケーション・ウィンドウ
0062: HWND hParent;
0063:
0064: //アプリケーション・ウィンドウ位置
0065: unsigned hParent_X, hParent_Y;
0066:
0067: //pahooNormalizeTextオブジェクト
0068: pahooNormalizeText* pNT;
0069:
0070: //char*バッファサイズ
0071: #define SIZE_BUFF 5120
0072:
0073: //変換元テキスト(初期値)
0074: #define DEF_SOUR "『千と千尋の神隠し』(せんとちひろのかみかくし)は、スタジオジブリ制作の長編アニメーション映画。監督は宮崎駿。二〇〇一年七月二十\日に日本公開。興行収入は三百億円を超え、日本歴代興行収入第一位を達成した。英語のタイトルは『Spirited Away』。\n千尋という名の十\歳の少女が、引っ越し先へ向かう途中に立ち入ったトンネルから、神々の世界へ迷い込んでしまう物語。"
とくに注意記載が無い限り、定数は自由に変更できる。
解説:正規化オプション取得・設定
0483: /**
0484: * 正規化オプションを設定する
0485: * @param HWND hDlg ウィンドウ・ハンドラ
0486: * @param string opt 正規化オプション
0487: * @return なし
0488: */
0489: void setOption(HWND hDlg, string opt) {
0490: for (int i = 0; i < (int)opt.length(); i++) {
0491: char* ch = (char*)opt.substr(i, 1).c_str();
0492: switch (*ch) {
0493: case OPTION_ALP_HAN:
0494: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
0495: BM_SETCHECK, BST_CHECKED, 0);
0496: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
0497: BM_SETCHECK, BST_UNCHECKED, 0);
0498: break;
0499: case OPTION_ALP_ZEN:
0500: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN),
0501: BM_SETCHECK, BST_UNCHECKED, 0);
0502: SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN),
0503: BM_SETCHECK, BST_CHECKED, 0);
0504: break;
0505: case OPTION_NUM_HAN:
0506: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0507: BM_SETCHECK, BST_CHECKED, 0);
0508: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0509: BM_SETCHECK, BST_UNCHECKED, 0);
0510: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0511: BM_SETCHECK, BST_UNCHECKED, 0);
0512: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0513: BM_SETCHECK, BST_UNCHECKED, 0);
0514: break;
0515: case OPTION_NUM_ZEN:
0516: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0517: BM_SETCHECK, BST_UNCHECKED, 0);
0518: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0519: BM_SETCHECK, BST_CHECKED, 0);
0520: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0521: BM_SETCHECK, BST_UNCHECKED, 0);
0522: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0523: BM_SETCHECK, BST_UNCHECKED, 0);
0524: break;
0525: case OPTION_NUM_KAN:
0526: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0527: BM_SETCHECK, BST_UNCHECKED, 0);
0528: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0529: BM_SETCHECK, BST_UNCHECKED, 0);
0530: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0531: BM_SETCHECK, BST_CHECKED, 0);
0532: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0533: BM_SETCHECK, BST_UNCHECKED, 0);
0534: break;
0535: case OPTION_NUM_KAN2:
0536: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN),
0537: BM_SETCHECK, BST_UNCHECKED, 0);
0538: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN),
0539: BM_SETCHECK, BST_UNCHECKED, 0);
0540: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN),
0541: BM_SETCHECK, BST_UNCHECKED, 0);
0542: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2),
0543: BM_SETCHECK, BST_CHECKED, 0);
0544: break;
0545: case OPTION_NUM_SCALE:
0546: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
0547: BM_SETCHECK, BST_CHECKED, 0);
0548: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
0549: BM_SETCHECK, BST_UNCHECKED, 0);
0550: break;
0551: case OPTION_NUM_NOSCALE:
0552: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE),
0553: BM_SETCHECK, BST_UNCHECKED, 0);
0554: SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE),
0555: BM_SETCHECK, BST_CHECKED, 0);
0556: break;
0557: case OPTION_YAK_HAN:
0558: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
0559: BM_SETCHECK, BST_CHECKED, 0);
0560: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
0561: BM_SETCHECK, BST_UNCHECKED, 0);
0562: break;
0563: case OPTION_YAK_ZEN:
0564: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN),
0565: BM_SETCHECK, BST_UNCHECKED, 0);
0566: SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN),
0567: BM_SETCHECK, BST_CHECKED, 0);
0568: break;
0569: case OPTION_KANA_HAN:
0570: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
0571: BM_SETCHECK, BST_CHECKED, 0);
0572: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
0573: BM_SETCHECK, BST_UNCHECKED, 0);
0574: break;
0575: case OPTION_KANA_ZEN:
0576: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN),
0577: BM_SETCHECK, BST_UNCHECKED, 0);
0578: SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN),
0579: BM_SETCHECK, BST_CHECKED, 0);
0580: break;
0581: case OPTION_SPEC_HAN:
0582: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
0583: BM_SETCHECK, BST_CHECKED, 0);
0584: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
0585: BM_SETCHECK, BST_UNCHECKED, 0);
0586: break;
0587: case OPTION_SPEC_ZEN:
0588: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN),
0589: BM_SETCHECK, BST_UNCHECKED, 0);
0590: SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN),
0591: BM_SETCHECK, BST_CHECKED, 0);
0592: break;
0593: case OPTION_CONTROL_DEL:
0594: SendMessage(GetDlgItem(hDlg, IDC_CHECK_CONTROL_DEL),
0595: BM_SETCHECK, BST_CHECKED, 0);
0596: break;
0597: default:
0598: break;
0599: }
0600: }
0601: }
0603: /**
0604: * 正規化オプションを取得する
0605: * @param HWND hDlg ウィンドウ・ハンドラ
0606: * @return string 正規化オプション
0607: */
0608: string getOption(HWND hDlg) {
0609: string opt = "";
0610: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_HAN), BM_GETCHECK, 0, 0)) {
0611: opt += OPTION_ALP_HAN;
0612: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ALP_ZEN), BM_GETCHECK, 0, 0)) {
0613: opt += OPTION_ALP_ZEN;
0614: }
0615: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_HAN), BM_GETCHECK, 0, 0)) {
0616: opt += OPTION_NUM_HAN;
0617: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_ZEN), BM_GETCHECK, 0, 0)) {
0618: opt += OPTION_NUM_ZEN;
0619: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN), BM_GETCHECK, 0, 0)) {
0620: opt += OPTION_NUM_KAN;
0621: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_KAN2), BM_GETCHECK, 0, 0)) {
0622: opt += OPTION_NUM_KAN2;
0623: }
0624: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_SCALE), BM_GETCHECK, 0, 0)) {
0625: opt += OPTION_NUM_SCALE;
0626: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_NUM_NOSCALE), BM_GETCHECK, 0, 0)) {
0627: opt += OPTION_NUM_NOSCALE;
0628: }
0629: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_HAN), BM_GETCHECK, 0, 0)) {
0630: opt += OPTION_YAK_HAN;
0631: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_YAK_ZEN), BM_GETCHECK, 0, 0)) {
0632: opt += OPTION_YAK_ZEN;
0633: }
0634: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_HAN), BM_GETCHECK, 0, 0)) {
0635: opt += OPTION_KANA_HAN;
0636: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_KANA_ZEN), BM_GETCHECK, 0, 0)) {
0637: opt += OPTION_KANA_ZEN;
0638: }
0639: if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_HAN), BM_GETCHECK, 0, 0)) {
0640: opt += OPTION_SPEC_HAN;
0641: } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SPEC_ZEN), BM_GETCHECK, 0, 0)) {
0642: opt += OPTION_SPEC_ZEN;
0643: }
0644: if (SendMessage(GetDlgItem(hDlg, IDC_CHECK_CONTROL_DEL), BM_GETCHECK, 0, 0)) {
0645: opt += OPTION_CONTROL_DEL;
0646: }
0647:
0648: return opt;
0649: }
解説:正規化オプション読込・保存
0317: /**
0318: * AppDataのパスを取得
0319: * @param char* appname アプリケーション名
0320: * @return string パス
0321: */
0322: string getMyPath(const char* appname) {
0323: static TCHAR myPath[MAX_PATH] = "";
0324:
0325: if (strlen(myPath) == 0) {
0326: if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
0327: TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
0328: if (ptmp != NULL) {
0329: ptmp = _tcsinc(ptmp);
0330: *ptmp = _T('\0');
0331: }
0332: strcat(myPath, _T("Roaming"));
0333: CreateDirectory((LPCTSTR)myPath, NULL);
0334: strcat(myPath, _T("\\pahoo.org"));
0335: CreateDirectory((LPCTSTR)myPath, NULL);
0336: strcat(myPath, _T("\\"));
0337: strcat(myPath, _T(appname));
0338: CreateDirectory((LPCTSTR)myPath, NULL);
0339: strcat(myPath, _T("\\"));
0340: } else {
0341: }
0342: }
0343: return (string)myPath;
0344: }
0346: /**
0347: * オプションの読み込み
0348: * @param なし
0349: * @return string オプション
0350: */
0351: string loadOption(void) {
0352: hParent_X = 0;
0353: hParent_Y = 0;
0354: string option = OPTION_INIT;
0355: ptree pt;
0356:
0357: //XMLファイル読み込み
0358: try {
0359: xml_parser::read_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt);
0360:
0361: //XML解釈
0362: try {
0363: //形式チェック
0364: if (optional<string>str = pt.get_optional<string>("parameter")) {
0365: } else {
0366: return OPTION_INIT;
0367: }
0368: //パラメータ読み込み
0369: for (auto it : pt.get_child("parameter")) {
0370: string type= it.second.get_optional<string>("<xmlattr>.type").value();
0371: if (type == "option") {
0372: option =(string)it.second.data();
0373: } else if (type == "wx") {
0374: hParent_X = (unsigned)stoi(it.second.data());
0375: } else if (type == "wy") {
0376: hParent_Y = (unsigned)stoi(it.second.data());
0377: }
0378: }
0379: //解釈失敗したら初期値設定
0380: } catch (xml_parser_error& e) {
0381: hParent_X = 0;
0382: hParent_Y = 0;
0383: return OPTION_INIT;
0384: }
0385: //読み込み失敗したら初期値設定
0386: } catch (xml_parser_error& e) {
0387: hParent_X = 0;
0388: hParent_Y = 0;
0389: return OPTION_INIT;
0390: }
0391:
0392: //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
0393: HWND hDesktop = GetDesktopWindow();
0394: WINDOWINFO windowInfo;
0395: windowInfo.cbSize = sizeof(WINDOWINFO);
0396: GetWindowInfo(hDesktop, &windowInfo);
0397: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
0398: hParent_X = 0;
0399: }
0400: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
0401: hParent_Y = 0;
0402: }
0403:
0404: return option;
0405: }
0407: /**
0408: * オプションの保存
0409: * @param string option オプション
0410: * @return なし
0411: */
0412: void saveOption(string option) {
0413: #ifndef CMDAPP
0414: //アプリケーション・ウィンドウの位置取得
0415: WINDOWINFO windowInfo;
0416: windowInfo.cbSize = sizeof(WINDOWINFO);
0417: GetWindowInfo(hParent, &windowInfo);
0418: hParent_X = (unsigned)windowInfo.rcWindow.left;
0419: hParent_Y = (unsigned)windowInfo.rcWindow.top;
0420: if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
0421: hParent_X = 0;
0422: }
0423: if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
0424: hParent_Y = 0;
0425: }
0426: #endif
0427:
0428: //XMLファイルへ書き込む
0429: ptree pt;
0430: ptree& child1 = pt.add("parameter.param", option);
0431: child1.add("<xmlattr>.type", "option");
0432: ptree& child2 = pt.add("parameter.param", (string)to_string(hParent_X));
0433: child2.add("<xmlattr>.type", "wx");
0434: ptree& child3 = pt.add("parameter.param", (string)to_string(hParent_Y));
0435: child3.add("<xmlattr>.type", "wy");
0436:
0437: // cout << "option=" << option << endl;
0438:
0439: const int indent = 4;
0440: write_xml(getMyPath(APPNAME) + APPNAME + ".xml", pt, std::locale(),
0441: xml_writer_make_settings<std::string>(' ', indent));
0442: }
保存場所は、getMyPath 関数によって、ユーザーのAppDataの下にアプリケーションフォルダを作って、XMLファイルとして保存する。
この処理は、GUI/CUI共通だ。
解説:フロントエンド置換
0186: /**
0187: * フロントエンド置換:入力直後に文字列置換を行う
0188: * @param string wsour 入力テキスト
0189: * @return string 変換後文字列
0190: */
0191: wstring pahooNormalizeText::frontend_replace(wstring wsour) {
0192: wstring tbl[2];
0193:
0194: //文字列置換定義ファイルを読み込む
0195: ifstream ifs(FRONT_REPLACE_CSV);
0196: //文字列置換定義ファイルが無い
0197: if (! ifs) {
0198: return wsour;
0199: }
0200:
0201: //1行ずつ変換
0202: string ss;
0203: string delim = "\t";
0204: list<string> list_string;
0205: wstring wstr = L"";
0206: while (getline(ifs, ss)) {
0207: int n = 0;
0208: boost::split(list_string, ss, boost::is_any_of(delim));
0209: BOOST_FOREACH(string s, list_string) {
0210: tbl[n] = _SW(s);
0211: n++;
0212: if (n > 2) break;
0213: }
0214: wregex re(tbl[0]);
0215: wsour = regex_replace(wsour, re, tbl[1]);
0216: }
0217:
0218: return wsour;
0219: }
OCRソフト等を利用して取得したテキストについて、特定のパターンが正しく読み取れない場合がある。そこで、誤認識パターンを正しいテキストに置換するのがフロントエンド置換機能である。
GUI版、CUI版ともに、すべての正規化処理の前に、定数 FRONT_REPLACE_CSV に定義された置換定義ファイルに従って、文字列の置換を行う。
FRONT_REPLACE_CSV には複数のパターンを置換することが可能で、このCSV形式ファイルにはシフトJISで次のように記述する。
置換前文字列1 (TAB) 置換後文字列1Excelなどを使って、タブ区切りでCSV形式ファイルに保存するといいだろう。
置換前文字列2 (TAB) 置換後文字列2
‥‥
何も記述しない(空ファイル)と、フロントエンド置換機能は働かない。
CUI用メインプログラム
0829: /**
0830: * CUI用メインプログラム
0831: * @param int argc
0832: * @paramm char* argv[]
0833: * @return int リターンコード
0834: */
0835: int main(int argc, char* argv[]) {
0836: wstring wsour = L"";
0837: wstring wdest = L"";
0838: string sour = "";
0839:
0840: //正規化オプションの初期値
0841: string lopt = OPTION_INIT;
0842:
0843: //pahooNormalizeTextオブジェクト
0844: pNT = new pahooNormalizeText();
0845:
0846: //コマンドライン・オプションの定義
0847: options_description options("コマンドライン・オプション");
0848: options.add_options()
0849: ("option,o", value<std::string>()->default_value(lopt), "正規化オプション")
0850: ("sour,s", value<std::string>(), "テキストファイルから入力")
0851: ("clip,c", "クリップボードから入力")
0852: ("dest,d", value<std::string>(), "テキストファイルへ出力")
0853: ("paste,p", "クリップボードへ出力")
0854: ("help,h", "ヘルプ")
0855: ("version,v", "バージョン情報")
0856: ;
0857: //コマンドライン・オプションの取得
0858: variables_map vm;
0859: try {
0860: store(parse_command_line(argc, argv, options), vm);
0861: } catch(const boost::program_options::error_with_option_name& e) {
0862: ErrorMessage = e.what();
0863: cerr << ErrorMessage << endl;
0864: return 1;
0865: }
0866: notify(vm);
0867:
0868: //正規化オプション
0869: auto opt = vm["option"].as<string>();
0870:
0871: //ヘルプ情報
0872: if (vm.count("help")) {
0873: wdest = _SW(Help);
0874: //バージョン情報
0875: } else if (vm.count("version")) {
0876: wdest = _SW(Version);
0877:
0878: //正規化実行
0879: } else {
0880: //入力ファイル
0881: wsour = _SW(DEF_SOUR);
0882: if (vm.count("sour")) {
0883: auto infile = vm["sour"].as<string>();
0884: ifstream ifs(infile.c_str());
0885: if (ifs.fail()) {
0886: ErrorMessage = infile + " が見つかりません";
0887: cerr << ErrorMessage << endl;
0888: return 1;
0889: }
0890: string ss;
0891: while (getline(ifs, ss)) {
0892: sour += ss + "\r";
0893: }
0894: wsour = _SW(sour);
0895: //クリップボードから
0896: } else if (vm.count("clip")) {
0897: char *ss = getClipboardData();
0898: sour = ss;
0899: wsour = _SW(sour);
0900: //標準入力から
0901: } else {
0902: cin >> sour;
0903: if (sour.length() > 0) {
0904: wsour = _SW(sour);
0905: }
0906: }
0907: wdest = pNT->normalizeText(wsour, opt.c_str(), FALSE);
0908: }
0909:
0910: //出力ファイル
0911: if (vm.count("dest")) {
0912: //改行コード置換
0913: wregex re(_SW("\r"));
0914: wdest = regex_replace(wdest, re, _SW("\n"));
0915: auto outfile = vm["dest"].as<string>();
0916: ofstream ofs(outfile.c_str());
0917: ofs << _WS(wdest);
0918: if (ofs.bad()) {
0919: ErrorMessage = outfile + " への書き込みに失敗しました";
0920: cerr << ErrorMessage << endl;
0921: return 1;
0922: }
0923: ofs.close();
0924: //クリップボードへ
0925: } else if (vm.count("paste")) {
0926: //改行コード置換
0927: wregex re(_SW("\r"));
0928: wdest = regex_replace(wdest, re, _SW("\n"));
0929: setClipboardData(_WS(wdest));
0930: //標準出力へ
0931: } else {
0932: //改行コード置換
0933: wregex re(_SW("\r"));
0934: wdest = regex_replace(wdest, re, _SW("\n"));
0935: cout << _WS(wdest);
0936: }
0937:
0938: //オブジェクト解放
0939: delete pNT;
0940:
0941: return 0;
0942: }
GUI版の WinMain 関数はマルチスレッド対応で、すべてのスレッドが終わる前にコマンドラインに戻ってきてしまう。また、標準入出力のパイプ処理への対応も面倒であるため、CUI版は実行ファイルを分けることにした。

コマンドラインオプションの解釈は、Boost C++ライブラリ の options を利用した。
参考サイト
- PHPで日本語テキストを正規化:ぱふぅ家のホームページ
- PHPで日本語テキストを正規化(Windowsアプリ版):ぱふぅ家のホームページ
- MeCab
- PHPで MeCabのユーザー辞書を作成する:ぱふぅ家のホームページ
- C++ でテキスト中の和暦・西暦年号を統一する:ぱふぅ家のホームページ
- WiX によるWindowsインストーラー作成:ぱふぅ家のホームページ
- C++ 開発環境の準備:ぱふぅ家のホームページ
また、ソースを共通化してコマンドライン版アプリケーションを作り、バッチや他のプログラムから流し込んだテキストを正規化することができるようにする。
「PHPで日本語テキストを正規化(Windowsアプリ版)」で作ったPHPプログラムをC++に移植したものである。
(2022年7月18日)フロントエンド置換機能を追加,saveOption() 修正
(2022年4月29日)制御文字削除機能追加.CUI版に --clip, --paste オプション追加,MeCabユーザー辞書更新,不具合修正
(2022年2月19日)不具合修正
(2022年1月3日)「一律」「何千万」など文字化け対策を品詞を用いて実現.