プログラム言語で日本語を多めに(FLEX日本語活用編)

FLEX日本語活用編

(Ubuntu 20.04LTS版)

Windows版のFLEX日本語活用編に引き続き、Debian Linux系OSであるUbuntu( ぅぉー ぶーん つぅーと発音するようです。Ubuntuの意味は、南アフリカの倫理的な概念で、「他者への思いやり」だそうです)上で、Windowsと同じようにFLEXを用いて日本語文書処理をやってみたいと思います。 (FLEXについての概説はWindows版の方に記載しています)FLEXはもともとUNIXというOS上で作られたLEXというツールの後継にあたりますので、UNIXの流れをくむUbuntuの方がより洗練されたFLEXの使い方ができるのではないでしょうか。 また、Ubuntuは優秀なOSであるだけではなく、誰でもフリーで入手することがてきます。ここではUbuntu20.04LTS(本家公式版)というOSをインストールして使っています。

今回はUbuntu18.04に引き続き、このUbuntu20.04LTS版にFLEX-OLD(version 2.5.4)をインストールしましたが、FLEX(version 2.6.4)でもこのサイト内のプログラムが正常に動作することを確認しています。 このFLEXプログラムを端末(terminal)というCUIを利用して動かしてみます。FLEXを動かすにはC言語の処理系が必要となりますので、build-essentialパッケージをインスールしてC言語処理系(gcc)が使えるようにしておきます。 FLEX-OLD(version 2.5.4)については、端末から「sudo apt-get install flex-old」コマンド、FLEX(version 2.6.4)については、端末から「sudo apt-get install flex」コマンドでインストールできました。また、このUbuntu20.04LTSは64ビットバージョンです。

FLEX(version 2.6.4)の場合

FLEX-OLD(version 2.5.4)の場合

Ubuntu-OSもバージョンによって正式なサポート期間が決まっていて、現在Ubuntu 20.04LTS版では2025年の4月までは確実にサポートされます。20.04LTS版でも以前のバージョンと同様に各練習問題の動作確認を行っています。

Ubuntu 18.04LTS版はこちら

Ubuntu 14.04LTS版(サポート期間終了)

Ubuntu 6.10版(サポート期間終了)

FLEXのプログラムは一度C言語に変換されます。それをC言語処理系によって翻訳することにより、プログラムを実行することができます。従ってFLEXのプログラムを動かすにはC言語の処理系が必要となります。

FLEXの起動確認
例題FU─1 入力ファイルをそのまま出力する
例題FU─2 文字列を変換する(その一)
例題FU─3 文字列を変換する(その二)
例題FU─4 簡単なC言語風日本語プログラムの実行
例題FU─5 FLEX規則を利用してC言語を日本語で記述する
例題FU─6 文字クラスを使った日本語文字処理の基本練習
例題FU─7 FLEXを利用した送り仮名のチェック
例題FU─8 FLEXを利用した簡単な同音異義語のチェック
例題FU─9 FLEXでスタート状態を使った練習
例題FU─10 FLEXを利用した簡単な表記チェッカー
例題FU-11 FLEXでCGIを動かす(その一)
例題FU-12 FLEXでCGIを動かす(その二)
例題FU-13 自動番号の付いた規則をつくる
例題FU-14 FLEX規則を利用してC言語を日本語で記述する(続編)
例題FU-15 FLEXを利用して自動販売機のシミュレーションを行なう
日本語カスタマイザーについて

FLEXの起動確認

FLEXのインストールが終わったら、次にFLEXプログラムの作業用フォルダ/ディレクトリに移動します。ここではカレントフォルダー~/flexという場所を使います。本来はユーザー名やホスト名の後にフォルダー名が表示されるのですが、ここではユーザー名やホスト名は非表示にして、Ubuntu OSのバージョンを表示しています。

FLEXのインストールを行った後、次のようなコマンド入力をします。(-V または --version) うまくインストールできている場合には、バージョン番号が出力されます。

FLEX(version 2.6.4)の場合

FLEX-OLD(version 2.5.4)の場合

例題FU─1 入力ファイルをそのまま出力する

ここで最初に簡単なFLEXのプログラムを作ってみたいと思います。

GEDITなどで、次のようにプログラムを入力します。


/* FLEXテストプログラム1 */
%{
	#define YY_SKIP_YYWRAP
	int yywrap( void )	{ return(1); }
%}

%%
%%

int main(void)
{
	yylex();
	return(0);
}

このファイルをtest1.lexという名前で作業用フォルダ/ディレクトリ内に保存します。Windows版のときにはmainの関数をvoid型で定義していますが、Ubuntuの場合にはmainの関数をint型で定義して、戻り値をreturn(0)で返すようにしてください。

コマンドプロンプトから次のように入力するか、簡単なシェルスクリプト(バッチファイル)によって自動的にtest1を生成できるようにします。

コマンドプロンプトから入力する場合


$flex test1.lex
$gcc lex.yy.c 
$cp a.out test1

簡単なシェルスクリプトlccの内容


#!/bin/bash
flex $1.lex
gcc lex.yy.c
cp a.out $1
exit 0

シェルスクリプトから実行する場合

$./lcc test1

ここではシェルスクリプトで実行させてみました。シェルスクリプトは、必ずプログラムとして実行できるアクセス権を与えておかないといけません。このやり方は、ファイル・ブラウザでlccを右クリックしてプロパティを選択します。プロパティ画面の「アクセス権」タブを選択し、「実行権」の項目にある「プログラムとして実行することを許す」にチェックをするだけです。

次にGEDITなどで、次のようなテキストを入力してex1.txtという名前で保存します。


あいうえお
かきくけこ
さしすせそ
たちつてと
なにぬねの
はひふへほ
まみむめも
やゆよ
らりるれろ
わをん

コマンドプロンプトから次のように入力します。

$./test1 < ex1.txt

ex1.txtの内容がそのまま次のように表示されることを確認します。

Windows版の日本語文書処理編をまだご覧になっていない方にとっては、これが一体何をするプログラムかということは、まだよく分からないかもしれませんが、これで一応FLEXプログラム環境で、うまく動作するかどうかの確認ができたと思います。

FLEXプログラムでは、大まかに次のような構造になっているようです。


定義や初期Cコード
%%
規則部分
%%
その他Cコード

プログラム中に直接C言語のコードを含める場合には、そのコードを%{と%}の間に含めます。


%{
	#define YY_SKIP_YYWRAP
	int yywrap( void )	{ return(1) ; 
%}

上記%{と%}の間に含まれる二行については、ラッパー関数といわれる定義の部分ですが、ここでは深入りせずに、このように書くということで進めたいと思います。 (詳しくは、yywrapの説明を参照

また、さまざまな規則は、%%と%%に挟まれた行間に記述し、その他のCコードは二番目の%%の次の行以後に書きます。

int main(void)
{
	yylex();
	return(0);
}

上記のCコードについては、その他のCコードにあたる部分ですが、yylex()が実際に字句解析を行うための関数呼び出しです。これはint main( ){ }という関数内に記述しますが、これについてもあまり深入りせずに、Ubuntu版ではこのように書くということで先へ進めたいと思います。

例題FU─2 文字列を変換する(その一)

次のプログラムはtest1.lexをベースに規則部分の記述の方法について練習します。test1.lexの規則部分に次のような一行を加え、test2.lexという名前で保存します。/p>


/* FLEXテストプログラム2 */
%{
#define YY_SKIP_YYWRAP
int	yywrap( void )	{ return(1); }
%}

%%
"あいうえお"	{ printf("かきくけこ"); }
%%

int main(void)
{
	yylex();
	return(0);
}

このtest2.lexを先ほどのtest1.lexと同様に、シェルスクリプトを利用してコンパイルし、次のようにex1.txtを読み込むように実行します。

この結果から、規則部分に"あいうえお"     {printf("かきくけこ");}と記述すると、ex1.txtファイル中の「あいうえお」という文字列がそっくり「かきくけこ」という文字列に置き換えられて表示されました。つまり、"あいうえお"の後に区切り文字(空白、タブ)をはさんで、{printf("かきくけこ");}と書くと、"あいうえお"という文字列を見つけるたびに、"かきくけこ"と出力せよという規則になります。{ }内にはC言語のプログラムをそのまま書くことができます。printf("かきくけこ");とは文字列「かきくけこ」を出力させるC言語のコードです。

ここで規則部分は必ず行の先頭から記述してください。行頭に空白やタブが入っているとFLEXからエラーメッセージが出ます。

表示された内容をそのまま別のファイルとして保存する場合には、次のようにコマンドプロンプトのリダイレクション機能を利用します。ここでは保存するファイル名をexout.txtとしました。

exout.txtの内容をメモ帳で表示させてみます。

また、このFLEXによる規則は入力ファイルの1箇所だけではなく、すべての箇所について適用されます。

例題FU─3 文字列を変換する(その二)

次に複数の規則を記述してみます。次のようなFLEXプログラムを書いて、これをtest3.lexとして保存します。


/* FLEX テストプログラム3 */
%{
	#define YY_SKIP_YYWRAP
	int	yywrap( void )	{ return(1); }
%}

%%
"らりるれろ"	{printf("あいうえお");}
"やゆよ"	{printf("わをん");}
"わをん"	{printf("やゆよ");}
%%

int main(void)
{
	yylex();
	return(0);
}

次にGEDITなどで、次のようなテキストを入力してex3.txtという名前で保存します。


らりるれろ
わをん
なにぬねの
らりるれろ
やゆよ
がぎぐげご
たちつてと
ぱぴぷぺぽ
やゆよ

このtest3.lexをコンパイルし、次のようにex3.txtを読み込むように実行します。

規則部分で指定したように、入力ファイルの内容が変換されたことが分かります。このように複数の規則でも、%%と%%に挟まれた行間に並べて記述することができます。ここではFLEX文法の詳細については立ち入りませんが、より知識を深めたい方は、ダウンロードしたFLEXプログラムに付随するマニュアル/ドキュメント等を参照してください。

例題FU─4 簡単なC言語風日本語プログラムの実行

次に簡単なC言語風の日本語プログラムをC言語に変換するためのFLEXプログラムを作ってみます。ex4.txtファイルを作成して、以下のように入力、保存しておきます。

ここで、前回までのFLEXプログラムでは、初期Cコードとしてラッパー関数の定義をそのまま記述しておりましたが、これを別のインクルードファイルflex_test.incに記述します。

このようにするとFLEXプログラムは、よりすっきり見えるのではないかと思います。これに変換規則を適用させたものをtest4.lexとします。


/* FLEX テストプログラム4 */
%{
#include "flex_test.inc"
%}

%%
"型なし"	{printf("void");}
"整数型"	{printf("int");}
"はじまり"	{printf("main");}
"印字"		{printf("printf");}
%%

int main(void)
{
	yylex();
	return(0);
}

このtest4.lexをコンパイルし、次のようにex4.txtを読み込むように実行します。

また、この結果をexout.cというファイルにして、C言語の処理系に通せば、立派なプログラムとして動作することも分かります。(C言語処理系のgccでは、オプションを何も指定しなければ、デフォルトでa.outという実行可能ファイルを出力します)

例題FU─5 FLEX規則を利用してC言語を日本語で記述する

flexやgccにはいくつかのコマンドラインオプション機能があります。-oオプションを指定することにより、出力ファイル名の指定もできますので、今までのシェルスクリプトを以下のように変更します。これでよりすっきりとコンパイル、実行ができるようになると思います。

シェルスクリプトの内容(lcc)


#!/bin/bash
flex -o$1 $1.lex
gcc -o$1 $1.c
exit 0

次に日本語カスタマイザーC言語編からの例題ですが、構造体を使ったC言語風の日本語プログラムをC言語に変換するFLEXプログラムを作ってみます。ex5.txtファイルを作成して、以下のように入力、保存しておきます。


/* 個人情報を印字する */
#include <stdio.h>

構造体 個人情報型 {
       文字型 名前[30];
       整数型 年齢;
       文字型 性別[4];
       整数型 生まれた年;
       整数型 生まれた月;
       整数型 生まれた日;
};

整数型 はじまり(型なし)
{

       構造体 個人情報型 鈴木さんの記録 = {"鈴木太郎",46,"男",1963,2,14};
       構造体 個人情報型 山田さんの記録 = {"山田花子",35,"男",1974,8,23};

       印字("名前=%s\n",鈴木さんの記録.名前);
       印字("年齢=%d歳\n",鈴木さんの記録.年齢);
       印字("性別=%s\n",鈴木さんの記録.性別);
       印字("生年月日=%d年",鈴木さんの記録.生まれた年);
       印字("%d月",鈴木さんの記録.生まれた月);
       印字("%d日生まれ\n\n",鈴木さんの記録.生まれた日);

       印字("名前=%s\n",山田さんの記録.名前);
       印字("年齢=%d歳\n",山田さんの記録.年齢);
       印字("性別=%s\n",山田さんの記録.性別);
       印字("生年月日=%d年",山田さんの記録.生まれた年);
       印字("%d月",山田さんの記録.生まれた月);
       印字("%d日生まれ\n",山田さんの記録.生まれた日);

	通常終了;

}

Windows版のFLEXでは、シフトJISコードの2バイト目が16進数で「5C」のコード(文字ではバックスラッシュ、¥マーク)は、文字としてうまく認識させるときには注意が必要でした。 しかし、Ubuntu版では文字コード体系がUTF-8というUNICODEの文字体系が採用されていますので、特に気にしなくても規則のパターンマッチ部分は、問題なく日本語で書くことができるようです。 ちなみに、Ubuntu20.04のテキストエディタでは、¥マークではなく、バックスラッシュとして表示されます。


/* FLEX テストプログラム5 */
%{
#include "flex_test.inc"
%}
%%
"型なし"        { printf("void"); }
"整数型"	{ printf("int"); }
"文字型"	{ printf("char"); }
"はじまり"	{ printf("main"); }
"通常終了"	{ printf("return(0)"); }
"印字"		{ printf("printf"); }
"構造体"	{ printf("struct"); }
"個人情報型"	{ printf("person"); }
"名前"		{ printf("jp_str_1"); }
"年齢"		{ printf("jp_str_2"); }
"性別"		{ printf("jp_str_3"); }
"生まれた年"	{ printf("jp_str_4"); }
"生まれた月"	{ printf("jp_str_5"); }
"生まれた日"	{ printf("jp_str_6"); }
"鈴木さんの記録"	{ printf("jp_str_7"); }
"山田さんの記録"	{ printf("jp_str_8"); }
%%
int main(void)
{
    yylex();
    return(0);
}

このtest5.lexをコンパイルし、ex5.txtを読み込むと次のような結果が表示されます。


/* 個人情報をprintfする */
#include 

struct person {
       char jp_str_1[30];
       int jp_str_2;
       char jp_str_3[4];
       int jp_str_4;
       int jp_str_5;
       int jp_str_6;
};

int main(void)
{

       struct person jp_str_7 = {"鈴木太郎",46,"男",1963,2,14};
       struct person jp_str_8 = {"山田花子",35,"男",1974,8,23};

       printf("jp_str_1=%s\n",jp_str_7.jp_str_1);
       printf("jp_str_2=%d歳\n",jp_str_7.jp_str_2);
       printf("jp_str_3=%s\n",jp_str_7.jp_str_3);
       printf("生年月日=%d年",jp_str_7.jp_str_4);
       printf("%d月",jp_str_7.jp_str_5);
       printf("%d日生まれ\n\n",jp_str_7.jp_str_6);

       printf("jp_str_1=%s\n",jp_str_8.jp_str_1);
       printf("jp_str_2=%d歳\n",jp_str_8.jp_str_2);
       printf("jp_str_3=%s\n",jp_str_8.jp_str_3);
       printf("生年月日=%d年",jp_str_8.jp_str_4);
       printf("%d月",jp_str_8.jp_str_5);
       printf("%d日生まれ\n",jp_str_8.jp_str_6);

	return(0);

}

この結果をexout.cというファイルに保存し、コンパイルして実行結果を確認します。

実行結果からも分かるように、実際の(C言語用)日本語カスタマイザーでは、二重引用符(ダブルクォーテーション)で囲まれた文字列は変換しないようにしていますが、今回のテストプログラムでは、すべての文字列について変換規則が適用されています。

例題FU─6 文字クラスを使った日本語文字処理の基本練習

ここで、少し文字コードについて触れておきたいと思います。UbuntuのエディターGEDITを使用してファイルを保存する場合をここでは想定します。このとき文字コードは通常UTF-8として保存されます。これはUNICODEと呼ばれるコード体系の中でも、日本語文字を3~4バイトで表現することができます。`UTF-8は多国語をエンコードする方法の一つで、UNIXのシステムプログラマーのKen Thompson氏が策定に貢献したことでも有名です。

また、英語圏で通常使用されるアスキー(ascii)コードは、1バイトコードであり、16進数で00~7fですが、UTF-8でもアスキーコードと同じ半角文字は1バイト目に同じコードが割り当てられています。

さらに、日本独自の規格である1バイトカタカナ(半角カタカナ)も、UTF-8ではコードが割り当てられています。

このあたりの様子を理解するためにも、次のようなtest6.lexというFLEXプログラムを書いてみます。ただし、文字クラスと呼ばれる定義部については、一行があまりに長くなり過ぎたため少し見づらいとは思いますが、GEDITの右端折り返しで表示しています。


/* FLEX テストプログラム6 */
%{
#include "flex_test.inc"
#define		pr_text		printf("%s",yytext)
%}


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

int main(void)
{
	yylex();
	return(0);
}

文字クラスの詳細な文法については、ここでは触れませんが、文字クラスのラベルとそれに対する文字集合を定義できます。test6.lexにおいて、それぞれの意味は次のようなものです。

katakana_1 半角カタカナ文字の定義 UTF-8では3バイト EF BD B0からの割り当て
katakana_2 全角カタカナ文字の定義 UTF-8では3バイト E3 82 A0からの割り当て
hiragana ひらがな文字の定義 UTF-8では3バイト EF BD B0からの割り当て
utf8_code 日本語文字を含む文字コードを数バイト単位で定義したコード 基本的なCJK統合、拡張、互換漢字(ただしハングル部分を除く)やひらがな、カタカナ、ラテン文字、ギリシャ文字など
[[:alpha:]] 1バイトのアルファベット(大文字と小文字)の文字クラス式
[[:digit:]] 1バイトの数字の文字クラス式
. 文字クラスではないが、上記と改行以外の1バイト文字
\n 文字クラスではないが、改行文字

UTF-8コードについては、漢字や記号コードのすべてを列挙すると、それだけで膨大な量になりますので、簡易的な定義をしています。また、このようにすることで、よく利用する漢字や記号のすべてについてスキャンすることができます。 もともと日本語カスタマイザー向けに考えた文字定義ですので、漢字や記号が多く使えるようにという配慮をしています。ハングル文字部分は入っていませんが、必要に応じて追加して使ってもよいと思います。ubuntuのC言語編では日本語、中国語、韓国語 共通のCJK文字コード部分を使っています。

Windowsなどから、UTF-8コード以外で作られたテキストファイルをGEDITで読み込む場合には、注意が必要です。表面には現れませんが、FLEXで重要な働きをするバックスラッシュ(¥マーク)コードが内部で化けるような現象も確認しています。 また、改行コードもWindowsからのファイルでは、GEDITで読み込んだときに「\r\n」となりますので、これを「\n」で置き換えることも必要のようです。簡単な例題などは、文字コードをUTF-8に設定しておいて、文字はなるべくタイピングで入力することをお勧めします。

ここで、以下のようなテキストを作り、ex6.txtというファイルに保存します。

最初に定義したtest6.lexをコンパイル、実行してex6.txtを読み込ませます。そうすると改行だけが表示されます。

2番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ pr_text; }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行して、ex6.txtを読み込ませます。すると1バイトカナカナ文字だけが表示されます。ここでpr_textというのは、パターンマッチした文字を表示させるためのマクロ名です。マクロの定義は#defineで定義してあります。

3番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ pr_text; }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。すると1バイトのアルファベットだけが表示されます。

4番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ pr_text; }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。すると1バイトの数字だけが表示されます。このとき規則部のパターンマッチ条件を記述して、それに対するアクション部分に{ }という空文を定義していたところが、今回はアクション部には何も記述していません。しかし、アクション部に何も書かないことと、{ }という空文を書くこととは結果的に同じであるといえます。

5番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ pr_text; }
{hiragana}	{ }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。する全角のカタカナ文字だけが表示されます。

6番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ pr_text; }
{utf8_code}	{ }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。するとひらがな文字だけが表示されます。

7番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ pr_text; }
.		{ }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。すると漢字や句読点だけが表示されます。

8番目にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ }
[[:alpha:]]	{ }
[[:digit:]]	{ }
{katakana_2}	{ }
{hiragana}	{ }
{utf8_code}	{ }
.		{ pr_text; }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。するとこれまでに表示されていない1バイトの文字が表示されます。

最後にFLEXプログラムtest6.lexの規則部分を以下のように変更します。


%%
{katakana_1}	{ pr_text; }
[[:alpha:]]	{ pr_text; }
[[:digit:]]	{ pr_text; }
{katakana_2}	{ pr_text; }
{hiragana}	{ pr_text; }
{utf8_code}	{ pr_text; }
.		{ pr_text; }
\n		{ pr_text; }
%%

これをコンパイル、実行してex6.txtを読み込ませます。するとすべての文字が表示されます。

実は今回定義した「hiragana」、「katakana1」、「katakana2」の文字セット(ひらがな、全角カタカナ)は、「utf8_code」の文字セットに含まれます。FLEXは文字定義の競合を厳密にチェックしますが、キャラクター型の文字セットと16進バイトコードに続く任意バイトデータが混在している場合には競合チェックを見逃してくれるようです。 これを利用してひらがな、全角カタカナ、漢字を区別しています。厳密にこれらを区別して定義するというやり方もありますが、定義が複雑になるので今回は簡単にしています。

例題FU─7 FLEXを利用した送り仮名のチェック

近年、日本語文書処理の中心はなんといっても日本語ワードプロセッサー(略してワープロ)ではないでしょうか。最近のワープロは専用の処理言語を搭載したものが珍しくないという状況になっているようです。

ワープロに標準で装備されていない機能でも、付属の処理言語を使えば機能を増やすことができるようになっているようです。しかし、それは一般の人々には非常に難解な部分でもあります。このときFLEXで日本語処理を行えば、かなり楽にできることもたくさんあるのではないでしょうか。

その一例として、送り仮名のチェックがあります。最近はパソコンに付属の標準日本語入力システムのみならず、さまざまな日本語入力システムが使用されています。そのため、かな漢字変換機能を利用すれば、送り仮名についてあまり意識せずに使うことができるようになっています。しかし、中には日本語入力システムでも、あいまいさを残した語句もあります。次のような文章を入力して、ex7.txtとします。

この文章では「押さえる」を「押える」という送り仮名を使っています。また、「捕らえる」を「捕える」という送り仮名を使っていますが、送り仮名の付け方として、誤読、難読がないようにという通則に従ってチェックをするプログラムをtest7.lexとします。

文字クラス定義部分については、test6.lexと同じ内容です。また、規則部分についても一行が長くなり過ぎたため、少し見づらいとは思いますが、メモ帳の表示画面を右端で折り返すモードで表示しています。さらにpr_cmntは文字列を表示させるマクロ定義です。


/* FLEX テストプログラム7 */
%{
#include "flex_test.inc"
#define		pr_cmnt(str)		printf("%s\n",str)
%}

この文字クラス定義部はtest6.lexと同じ内容です。


%%
"押え"			{ pr_cmnt("送り仮名に注意「押える」は「押さえる」とする。"); }
("捕わ")|("捕え")	{ pr_cmnt("送り仮名に注意「捕われる」は「捕らわれる」、「捕える」は「捕らえる」とする。"); }
{utf8_code}
.
\n
%%

int main(void)
{
	yylex();
	return(0);
}

これをコンパイル、実行してex7.txtを読み込ませます。すると「押える」、「捕われる」についての注意事項が表示されます。このとき「捕われる」「捕える」を同じメッセージとして検出するために、パターンマッチ部の「捕わ」と「捕え」を「|」(バーティカル・バー)で結び、「または」という意味を持たせています。

例題FU─8 FLEXを利用した簡単な同音異義語のチェック

次もFLEXプログラムで、文書チェッカーの機能を練習してみます。

日本語は同音異義語が大変多く存在する言語ではないでしょうか。この同音異義語は、かな漢字変換時には変換候補として表示されることもありますが、最終的には人間が選択し、決定をしなければなりません。うっかりすると用法をよく確かめずに、同音異義語の使い方を誤って使用してしまう場合もあるかもしれません。このようなことを防ぐためにも、文書を作成した後でチェッカーを利用して全体を校正することが考えられます。

<いま、次のような同音異義語の使い方が適切でない文書を作成し、これをex8.txtとして保存します。

この文章では、「遍在」(どこにでも広くあるという意)を「偏在」(かたよっているという意)という言葉を使っています。また、「機運」(機会、時機がめぐるという意)を「気運」(歴史的背景の中での時流の意)という言葉を使っています。さらに「不断」(たゆまないという意)を「普段」(日常、平生の意)という言葉を使っています。これらをチェックをするFLEXプログラムをtest8.lexとします。

/* FLEX テストプログラム8 */
%{
#include	"flex_test.inc"
#define		pr_cmnt(str)		printf("%s\n",str)
%}
この文字クラス定義部はtest6.lexと同じ内容です。
%%
("遍在")|("偏在")	{ pr_cmnt("同音異義語の使い分けに注意「遍在/偏在」"); }
("機運")|("気運")	{ pr_cmnt("同音異義語の使い分けに注意「機運/気運」"); }
("不断")|("普段")	{ pr_cmnt("同音異義語の使い分けに注意「不断/普段」"); }
{utf8_code}
.
\n
%%

int main(void)
{
	yylex();
	return(0);
}

これをコンパイル、実行してex8.txtを読み込ませます。すると同音異義語についての注意事項が表示されますので、文書作成者がこれに気付いて、あらためて注意箇所を校正することができます。

例えば、日本語ワープロで1ページ(40文字、36行)あたり1,440文字とすると、10ページで軽く1万文字を超えてしまいます。つまり一つの文書で数万文字を扱うというようなことは、日常的によくあることだと考えられます。この数万文字について、送り仮名や同音異義語をはじめ、表記についてもさまざまな規則や要求があると考えれらます。このような日本語文書を取り巻く状況で、大量の文書を素早く必要に応じてチェックをするために、FLEXで日本語文書処理を行うことは大変便利であると感じています。

例題FU─9 FLEXでスタート状態を使った練習

Windows版(Shift-JISコード)では2バイト文字のうち、2バイト目が(16進数で)「5C」の文字には注意をする必要がありましたが、UbuntuでUTF-8コードを使う上ではそういった制約はありません。 この例題はWindwos版で2バイト目が「5C」の文字である「暴」を含む文書を作成しパターンマッチをするという確認を行いましたが、Ubuntu版では、特に漢字文字コードについて気にする必要はないのですが、Windwos版と同じようにやってみます。 このテキストをex9.txtとして保存します。

この文書において、「無暴」を「無謀」、「無謀」を「無暴」と自動的に変換するFLEXプログラムをtest9.lexとします。 ただし、二重引用符(ダブルクォーテーション)で囲まれた「無謀」という文字列については、変換しないというテクニックも使ってみます。


/* FLEX テストプログラム9 */
%{
#include	"flex_test.inc"
#define		pr_str		printf("%s",yytext)
%}

%x string

%%
\"			{ pr_str; BEGIN(string); }
<string>\"		{ pr_str; BEGIN(INITIAL); }
<string>.		{ pr_str; }
"無暴"			{ printf("無謀"); }
"無謀"			{ printf("無暴"); }
%%

int main(void)
{
	yylex();
	return(0);
}

これをコンパイル、実行してex9.txtを読み込ませます。 すると「無暴」を「無謀」と変換してくれますが、二重引用符(ダブルクォーテーション)で囲まれた「無謀」という文字列はそのまま残っていることが分かります。

ここで「%x string」という記述部分は、スタート状態というFLEXの機能を使っています。FLEXの文法について詳しいことは触れませんが、スタート状態によってある規則が活性化する条件をFLEXに通知するための論理値のようなものとされています。これによって二重引用符が認識されると<string>で始まる規則部分が適用されます。また、二度目の二重引用符が認識されると通常の規則処理に戻ります。

二重引用符の処理テクニックは日本語カスタマイザーでは、コメントの処理にも用いられています。ただし、C言語などは二重引用符の中でもさらに「\"」などが使われたりしますので、実際の日本語カスタマイザーではこれらのことにも対処できるようにしています。

例題FU─10 FLEXを利用した簡単な表記チェッカー

文章を書くとき注意する事柄の一つに、表記の問題があると思います。日本語の中では、ある漢字複合語について別の読み方と区別するために、表記を分けて記述することがあります。 例えば名詞、形容動詞の「じょうず」を漢字で「上手」と書く場合がありますが、形容詞として「うまい」を「上手い」と同じ漢字を用いて表記する場合などです。これに対して、この二つの読み方を明確に分けるために、「上手い」という読み方に対しては、すべて「うまい」という平仮名で表記するという規則が求められる場合もあるかもしれません。 また、自分流の文書作成スタイルでは、特にこの二つを分けて書く習慣がない方もいらっしゃると思います。このようなときに、文書をすべて作成した後、一括して表記をチェックするという例についてFLEXプログラムで書いてみます。 次のような「上手」(じょうず)と「上手い」(うまい)を含む文章を作成し、ex10.txtとして保存します。

この文書において、「上手い」を「うまい」、「上手く」を「うまく」という表記上の注意を文章内に埋め込むFLEXプログラムをtest10.lexとします。ただし、「上手」(じょうず)という文字列については、何もしないというようにします。


/* FLEX テストプログラム10 */
%{
#include	"flex_test.inc"
#define		pr_str		printf("%s",yytext)
%}

KEIYOSHI_1		"い"|"か"|"く"|"け"

%%
"上手"/{KEIYOSHI_1}		{ printf("[注意]上手 → うま"); }
%%

int main(void)
{
	yylex();
	return(0);
}

「上手い」は「い」だけでなく、「上手かろう」「上手く」「上手ければ」などの活用が考えられますので、「上手」の後に続く数種類の平仮名を認識できることが必要となります。ここでは、文字クラスの中で数種類の平仮名を一括して定義しています。パターンマッチ部で用いる"/"(スラッシュ記号)は、先読み演算子として機能します。従って、「上手」に続く「い」「か」「く」「け」などがある限りパターンマッチしますが、そのときの「い」「か」「く」「け」は、まだマッチの対象とはなりません。 これをコンパイル、実行してex10.txtを読み込ませます。すると「上手い」や「上手く」について表記の注意はそこに記入されますが、「上手」(じょうず)の場合は何もしないことが分かります。

例題FU-11 FLEXでCGIを動かす(その一)

近年インターネット利用者数は、国内だけでも一億人を超えたといわれていますが、ネットサーフィンをするときには、気が付かないうちにCGI(コモン・ゲート・インターフェース)と呼ばれるプログラムが動作していることがあります。これは、ホームページなどを閲覧するブラウザーと呼ばれるソフトウェアが、インターネットサーバー上の情報を閲覧するだけではなく、ユーザーの方からサーバー側に情報を発信するときなどに、サーバー側の処理機能としてよく利用されています。

UbuntuというLinux/Unix系OS上では、apache2という定番のWebサーバーソフトが用意されています。これも システム管理者であれば、インターネットから簡単にダウンロードすることができます。(apache2のバージョンはUbuntu 20.04LTSではV2.4.41です) CGIはこのサーバーソフトと連動させるものです。最近は、Perlというスクリプト言語がCGIとして有名ですが、今回はこのCGIにFLEXを利用してみます。もともとC言語はCGIとして利用されることがありましたので、FLEXでもCGIは記述できるはずです。

apache2のV2.4.29ではデフォルトでホームページは/var/www/html/、CGIは/usr/lib/以下がlocalhostのフォルダーになってるので、そのままこれを利用することにします。

それではまず、テスト用のホームページtest11.htmをHTML言語で以下のように記述します。また、test11.htmは/var/www/html/フォルダー下に システム管理者権限でコピーしておきます。ここでは、formとinput要素を使って、氏名、郵便番号、住所が入力できるようにしました。送信ボタンをクリックすると、test11.cgiというプログラムが動作するようにします。


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>CGIのテスト</title>
</head>
<body>
<form METHOD="POST" ACTION="/cgi-bin/test11.cgi">
名 前<input TYPE="TEXT" NAME="NAME" size="18" ><br><br>
郵便番号<input TYPE="TEXT" NAME="ZIP" size="10"><br><br>
住 所<input TYPE="TEXT" NAME="ADDRESS" size="40"><br><br>
<input TYPE="submit" VALUE="送信">
</body>
</html>

test11.cgiを作るために、FLEXでtest11.lexというプログラムを以下のように記述しておきます。ここでは、inputで指定したNAMEに対応して、文字列を表示するという簡単なものです。また、「&」文字を受け取ると、ブラウザー上で改行するようにしています。


/* FLEX テストプログラム11 */

%{
#include "flex_test1.inc"
%}

%%
NAME=		{ pr_text; }
ZIP=		{ pr_text; }
ADDRESS=	{ pr_text; }
&		{ printf("<br>"); }
%%

int main(void)
{
	pr_web_out1;
	yylex();
	pr_web_out2;
	return(0);
}

また、flex_test1.incの内容は以下のようになります。POSTメソッドの場合には、最初に"CONTENT_LENGTH"という環境変数によって、文字列全体の長さをチェックすることが義務付けられていますので、その部分の最大長さを10,000バイトとしています。


#define YY_SKIP_YYWRAP
int	yywrap( void )	{ return(1); }

#define pr_text		printf("%s",yytext)

#define MAX_DATA_SIZE	10000

#define check_size	if(atoi(getenv("CONTENT_LENGTH")) > MAX_DATA_SIZE ) exit(1)

#define	pr_web_out1	check_size;\
			printf("content-type: text/html\n");\
			printf("\n");\
			printf("<!DOCTYPE HTML PUBLIC \"-//W3C/DTD HTML 4.01 Transitional//EN\">\n");\
			printf("<html lang=\"ja\">\n");\
			printf("<head>\n");\
			printf("<meta http-equiv=\"Content-Language\" content=\"ja\">\n");\
			printf("<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n");\
			printf("<body>\n")

#define	pr_web_out2	printf("</body></html>\n")

test11.lexをflexとgccでコンパイルした後、実行可能ファイル名をtest11.cgiに変更し、Ubuntuの場合には、/usr/lib/cgi-bin/というフォルダーの下にシステム管理者権限でコピーしておきます。

ここで、apacheサーバーが動いているときには、http://localhost/test11.htmをブラウザーで表示させると以下のようになりますので、実際に名前、郵便番号、住所の項目に入力して、送信ボタンをクリックします。

すると、以下のようにNAME(名前)、ZIP(郵便番号)、ADRESS(住所)に対応した文字列を受け取って表示します。また、HTML言語のform要素において、POSTメソッドを指定した場合には、標準入力で文字列が受け渡されます。ただし、日本語の文字列は%に続く数字によって表されています。

このようにapacheサーバー上では、FLEXプログラムがCGIとして動作することが分かりました。CGIが動作しない場合にはCGIモジュールが有効になっていないことが考えられます。有効にするには端末から「sudo a2enmod cgid」を実行したあとで、apacheを再起動すればよいです。

例題FU-12 FLEXでCGIを動かす(その二)

次に、apacheサーバーが動作しているUbuntuマシンに、Wi-Fi経由でAndroid端末からアクセスしてCGI動作を確認してみます。また、今回はCGIに渡された文字列を正しい日本語に変換するということもやってみたいと思います。

まず、テスト用のホームページtest12.htmをHTML言語で以下のように記述し、test12.htmとして システム管理者権限で/var/www/html/フォルダー下にコピーしておきます。送信ボタンをクリックすると、test12.cgiというプログラムが動作するようにします。


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>CGIのテスト</title>
</head>
<body>
<form METHOD="POST" ACTION="/cgi-bin/test12.cgi">
名 前<input TYPE="TEXT" NAME="NAME" size="18" ><br><br>
郵便番号<input TYPE="TEXT" NAME="ZIP" size="10"><br><br>
住 所<input TYPE="TEXT" NAME="ADDRESS" size="40"><br><br>
<input TYPE="submit" VALUE="送信">
</body>
</html>

test12.cgiは、FLEXでtest12.lexというプログラムにより記述します。ここでは、前回のtest11.lexに加えて文字列を変換する機能を付けています。一応UTF-8で 連続した3バイト分のコードパターンだけを文字に変換することはやっていますが、あくまでも簡単なテスト的なものです。また、郵便番号は赤文字で表示させるようにしてみます。(flex_test1.incは前回と同じものです)


/* FLEX テストプログラム12 */

%{
#include	<string.h>
#include "flex_test1.inc"
unsigned char ch_code( unsigned char, unsigned char);
#define ch_code(ch1,ch2)	( ( ch1 & 0x40 ? ch1 - 0x37 : ch1 - 0x30) << 4 ) +\
				  ( ch2 & 0x40 ? ch2 - 0x37 : ch2 - 0x30)
%}

other_char	.
zip_type	[0-9]+-[0-9]+
utf8_3bt_top	E([3-9]|F)

%%
NAME=		{ pr_text; }
ZIP=		{ pr_text; }
ADDRESS=	{ pr_text; }
&		{ printf("
"); } {zip_type} { printf("%s",yytext); } (%{utf8_3bt_top})(%..)(%..) { printf("%c%c%c", ch_code(yytext[1],yytext[2]), ch_code(yytext[4],yytext[5]), ch_code(yytext[7],yytext[8])); } {other_char} %% int main(void) { pr_web_out1; yylex(); pr_web_out2; return(0); }

test12.lexをflexとgccでコンパイルした後、実行可能ファイル名をtest12.cgiに変更しシステム管理者権限で/usr/lib/cgi-bin/フォルダーの下にコピーしておきます。

ここで、Wi-Fi接続されているAndroid端末からは、Ubuntuのローカルアドレス(プライベートIPアドレス)が192.168.3.Xとして見えることを確認して、http://192.168.3.X/test12.htmをブラウザーで表示させます。(Xは接続しているルーターによって決められている範囲内の数字です)

実際に名前、郵便番号、住所の項目に入力して、送信ボタンをクリックします。

前回と違い、今度は日本語に変換されて表示されました。

これまではHTML言語のform要素で、POSTメソッドを使用して文字列のやり取りを行いましたが、form要素にはGETメソッドという方法により、文字列のやり取りもできるようになっています。POSTメソッドの場合には、標準入力が文字列のやり取りに使われているため、FLEX側では特に何も気にせずに字句解析できましたが、GETメソッドを使用する場合にはFLEXプログラム側もひと工夫する必要があるようです。GETメソッドでは環境変数の「QUERY_STRING」というものが使用されます。

GETメソッドテスト用のホームページtest12-G.htmをHTML言語で以下のように記述し、test12-G.htmとして/var/www/html/フォルダー下にコピーしておきます。送信ボタンをクリックすると、test12-G.cgiというプログラムが動作するようにします。


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>CGIのテスト</title>
</head>
<body>
<form METHOD="GET" ACTION="/cgi-bin/test12-G.cgi">
名 前<input TYPE="TEXT" NAME="NAME" size="18" ><br><br>
郵便番号<input TYPE="TEXT" NAME="ZIP" size="10"><br><br>
住 所<input TYPE="TEXT" NAME="ADDRESS" size="40"><br><br>
<input TYPE="submit" VALUE="送信">
</body>
</html>

test12-G.cgiは、test12-G.lexというプログラムにより記述します。ここでは、環境変数の「QUERY_STRING」をシェルコマンドの「printenv」で表示させる機能がありますが、これをC言語のpopenというパイプ機能を使ってFLEXのyyinで読めるようにしています。また、「YY_NEW_FILE」により、yyinが新しいファイルを指すようにしています。


/* FLEX テストプログラム12ーG */

%{
#include	<string.h>
#include	<stdlib.h>
#include "flex_test2.inc"
#define ch_code(ch1,ch2)	( ( ch1 & 0x40 ? ch1 - 0x37 : ch1 - 0x30) << 4 ) +\
				  ( ch2 & 0x40 ? ch2 - 0x37 : ch2 - 0x30)
%}

other_char	.
zip_type	[0-9]+-[0-9]+

%%
NAME=		{ pr_text; }
ZIP=		{ pr_text; }
ADDRESS=	{ pr_text; }
&		{ printf("
"); } {zip_type} { printf("%s",yytext); } (%..)(%..)(%..) { printf("%c%c%c", ch_code(yytext[1],yytext[2]), ch_code(yytext[4],yytext[5]), ch_code(yytext[7],yytext[8])); } {other_char} %% int main(void) { pr_web_out1; yyin = popen( "printenv QUERY_STRING","r" ); YY_NEW_FILE; yylex(); pclose(yyin); pr_web_out2; return(0); }

また、flex_test2.incの内容は以下のようになります。


#define YY_SKIP_YYWRAP
int	yywrap( void )	{ return(1); }

#define pr_text		printf("%s",yytext)

#define	pr_web_out1	printf("content-type: text/html\n");\
			printf("\n");\
			printf("<!DOCTYPE HTML PUBLIC \"-//W3C/DTD HTML 4.01 Transitional//EN\">\n");\
			printf("<html lang=\"ja\">\n");\
			printf("<head>\n");\
			printf("<meta http-equiv=\"Content-Language\" content=\"ja\">\n");\
			printf("<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n");\
			printf("<body>\n")

#define	pr_web_out2	printf("</body></html>\n")

ここで、http://192.168.3.X/test12-G.htmをWi-Fi接続しているAndroid端末で表示させます。(Xは接続しているルーターによって決められている範囲内の数字です)

実際に名前、郵便番号、住所の項目に入力して、送信ボタンをクリックします。

このようにGETメソッドでも、FLEXプログラムのCGIは、うまく動作することを確認しました。今回は簡単のためにローカルアドレスをそのまま使用しましたが、Ubuntuでは/etc/hostsをはじめ、他の設定ファイルを編集することにより、ドメインネームが使用できます。また、Windwosでも同様にhostsファイルを編集することで、ローカルアドレスをドメインネームとして使うことができます。(説明が長くなりますので、ここでは割愛させていただきます)

さらに、apacheサーバーを外部に公開し、グローバルIPアドレス(WAN側)から参照するときには、ルーターのDHCP設定や静的IPマスカレード設定などを適切に行えば、グローバルIPアドレスでホームページ参照や、CGIの実行ができるようになります。

例題FU-13 自動番号の付いた規則をつくる

これまで種々のFLEXを利用した文字処理の練習問題を提示してきましたが、これらの基本的なテクニックを使用して、日本語カスタマイザーのヒントともいうべき練習をやってみます。

次のようなテキストファイルex13.txtを準備します。このテキストには、各行の先頭に「#日本語定義」という文字列があります。その後に区切り文字(空白、タブ)をはさんで日本語の文字列が入力されています。

このテキストファイルをFLEXプログラムから読み込むと、FLEXの規則部分を出力するようにします。


/* FLEX テストプログラム13 */
%{
#include "flex_test.inc"
int auto_num = 0;

#define		pr_text		printf("%s",yytext)
#define		pr_dq_text	printf("\"%s\"",yytext)
#define		pr_autonum	printf(" { printf(\"jp_str_%-d\"); }\n",auto_num);++auto_num
#define		pr_rule		printf("%%%%\n")
%}

この文字クラス定義部はtest6.lexと同じ内容です。


%x JP_DEF
%x JP_STRING

%%
^"#日本語定義"		{ BEGIN(JP_DEF); }
<JP_DEF>[ \t]+		{ BEGIN(JP_STRING); }
<JP_STRING>{utf8_code}	{ pr_dq_text; }
<JP_STRING>\n		{ pr_autonum; BEGIN(INITIAL); }
.
\n
%%

int main(void)
{
	pr_rule;
	yylex();
	pr_rule;
	return(0);
}

このFLEXプログラムでは、あらかじめ「JP_DEF」状態と「JP_STRING」状態の二つの状態を持っています。まず、テキスト各行の先頭で「#日本語定義」という文字列を見つけると、「JP_DEF状態」に遷移します。

そして、区切り文字(空白、タブ)が一つ以上あれば、さらに「JP_STRING状態」に遷移します。そして、日本語文字列を認識するたびにダブルクォートで囲んで出力するという処理を行っています。さらに、改行を認識すると初期状態に戻ります。Ubuntu6.10の処理系では、区切り文字を文字クラスで定義していましたが、Ubuntu18.04/20.04の処理系ではうまく動いてくれないようです。そこで、直接規則の部分に区切り文字の定義をしています。 こういうところが、やはりプログラミングは実際にコードを書いて動かしてみないと分からないものだとつくづく思いました。

このプログラムを実行した結果を以下に示します。

このプログラムは、規則の部分を非常にシンプルに書いていますが、実際の日本語カスタマイザーは、もう少し複雑な処理を行っていて、日本語文字列の間に半角英数記号が入っていたときや、コメント処理などのさまざまな文字列の場合に、柔軟な対応ができるようにしてあります。

この例題プログラムでは、日本語文字一文字をそれぞれダブルクォートで囲んでいますが、このような書き方をしても、FLEX処理系は問題なく処理してくれるようです。また、jp_strという文字列に続く自動番号は、マクロ定義によってシンプルに実現しています。さらに、FLEXの規則部分に使用される「%%」記号は、main(void)において、yylex()による字句解析の前と後でそれぞれ出力しています。

また、この例題だけを考えると、何も状態を二つも持つ必要はありませんが、あとで機能を拡張することを考えると、二つある方が良いのではないでしょうか。簡単な例題ではありますが、日本語カスタマイザー作成の十分なヒントになると考えています。

例題FU-14 FLEX規則を利用してC言語を日本語で記述する(続編)

今回は、例題FU-5の続編として、もう少し日本語で書いたC言語風プログラムを処理するための練習をやってみたいと思います。このFLEXの記述は、日本語カスタマイザーの2パス目の処理を行うためのヒントになると考えています。

まず、次のようなテキストファイルex14.txtを準備します。ex14.txtファイルを作成して、以下のように入力、保存しておきます。このテキストは、例題FU-5で使用したテキストの先頭部分に、日本語定義の宣言が付加されたものです。


/* 個人情報を印字する */
#include <stdio.h>

#日本語定義	型なし		"void"
#日本語定義	整数型		"int"
#日本語定義	文字型		"char"
#日本語定義	はじまり	"main"
#日本語定義	通常終了	"return(0)"
#日本語定義	印字		"printf"
#日本語定義	構造体		"struct"
#日本語定義	個人情報型	"person"

#日本語定義	名前
#日本語定義	年齢
#日本語定義	性別
#日本語定義	生まれた年
#日本語定義	生まれた月
#日本語定義	生まれた日
#日本語定義	鈴木さんの記録
#日本語定義	山田さんの記録

構造体 個人情報型 {
       文字型 名前[30];
       整数型 年齢;
       文字型 性別[4];
       整数型 生まれた年;
       整数型 生まれた月;
       整数型 生まれた日;
};

整数型 はじまり(型なし)
{

       構造体 個人情報型 鈴木さんの記録 = {"鈴木太郎",46,"男",1963,2,14};
       構造体 個人情報型 山田さんの記録 = {"山田花子",35,"男",1974,8,23};

       印字("名前=%s\n",鈴木さんの記録.名前);
       印字("年齢=%d歳\n",鈴木さんの記録.年齢);
       印字("性別=%s\n",鈴木さんの記録.性別);
       印字("生年月日=%d年",鈴木さんの記録.生まれた年);
       印字("%d月",鈴木さんの記録.生まれた月);
       印字("%d日生まれ\n\n",鈴木さんの記録.生まれた日);

       印字("名前=%s\n",山田さんの記録.名前);
       印字("年齢=%d歳\n",山田さんの記録.年齢);
       印字("性別=%s\n",山田さんの記録.性別);
       印字("生年月日=%d年",山田さんの記録.生まれた年);
       印字("%d月",山田さんの記録.生まれた月);
       印字("%d日生まれ\n",山田さんの記録.生まれた日);

       通常終了;

}

例題FU-5と違うところは、「#日本語定義」という文字列から始まる一行文は、そのまま何も処理しないで、スキップさせています。これは状態遷移を利用しています。さらに、例題FU-5でも少し触れましたが、C言語の文字列中は、FLEXの置き替え規則を適用しないようにしています。これも状態遷移を利用しています。このFLEXプログラムを以下に示します。


/* FLEX テストプログラム14 */
%{
#include "flex_test.inc"
#define		pr_text		printf("%s",yytext)
%}

%x	SKIP
%x	STRING

%%
^"#日本語定義"  { BEGIN(SKIP);}
<SKIP>\n        { BEGIN(INITIAL);}
<SKIP>. 	{ }

\" 		{ pr_text; BEGIN(STRING);}
<STRING>\" 	{ pr_text; BEGIN(INITIAL);}
<STRING>. 	{ pr_text; }

"型なし"        { printf("void"); }
"整数型"	{ printf("int"); }
"文字型"	{ printf("char"); }
"はじまり"	{ printf("main"); }
"通常終了"	{ printf("return(0)"); }
"印字"	        { printf("printf"); }
"構造体"         { printf("struct"); }
"個人情報型"     { printf("person"); }

"名前"	{ printf("jp_str_1"); }
"年齢"	{ printf("jp_str_2"); }
"性別"	{ printf("jp_str_3"); }
"生まれた年"	{ printf("jp_str_4"); }
"生まれた月"	{ printf("jp_str_5"); }
"生まれた日"	{ printf("jp_str_6"); }
"鈴木さんの記録"	{ printf("jp_str_7"); }
"山田さんの記録"	{ printf("jp_str_8"); }
%%

int main(void)
{
    yylex();
    return(0);
} 

このtest14.lexをコンパイルし、ex14.txtを読み込むと次のような結果が出力されます。


/* 個人情報をprintfする */
#include <stdio.h>



struct person {
       char jp_str_1[30];
       int jp_str_2;
       char jp_str_3[4];
       int jp_str_4;
       int jp_str_5;
       int jp_str_6;
};

int main(void)
{

       struct person jp_str_7 = {"鈴木太郎",46,"男",1963,2,14};
       struct person jp_str_8 = {"山田花子",35,"男",1974,8,23};

       printf("名前=%s\n",jp_str_7.jp_str_1);
       printf("年齢=%d歳\n",jp_str_7.jp_str_2);
       printf("性別=%s\n",jp_str_7.jp_str_3);
       printf("生年月日=%d年",jp_str_7.jp_str_4);
       printf("%d月",jp_str_7.jp_str_5);
       printf("%d日生まれ\n\n",jp_str_7.jp_str_6);

       printf("名前=%s\n",jp_str_8.jp_str_1);
       printf("年齢=%d歳\n",jp_str_8.jp_str_2);
       printf("性別=%s\n",jp_str_8.jp_str_3);
       printf("生年月日=%d年",jp_str_8.jp_str_4);
       printf("%d月",jp_str_8.jp_str_5);
       printf("%d日生まれ\n",jp_str_8.jp_str_6);

       return(0);

}

この結果をexout.cというファイルに保存し、コンパイルして実行結果を確認します。

例題FU-5の結果とは違い、二重引用符(ダブルクォーテーション)で囲まれた文字列は変換されないことが分かります。この例はシンプルな文字列に対応して処理していますが、実際のC言語では、文字列中の中で、さらに二重引用符が使われることもあります。一方、C言語用の日本語カスタマイザーでは、このことにも対応しています。今回は、「/*」「*/」のコメント処理について触れてはいませんが、大体この二重引用符と同じような規則の書き方になります。

また、「#日本語定義」という文字列から始まる一行文は、完全に読み飛ばし(無視)されていることも分かります。この点について、C言語用の日本語カスタマイザーでは、この行の最後にコメントが入っている場合であっても、コメント部分だけは出力するようにしてあります。以上のことから、日本語カスタマイザーの2パス目では、変換規則と共に、これらの処理ができるようにしておくことが必要となります。

例題FU-15 FLEXを利用して自動販売機のシミュレーションを行なう

今回は、日本語処理というよりも、FLEXを使って自動販売機のシミュレーションをやってみたいと思います。最近、自動販売機はおサイフケータイでも使えるようになっているみたいですが、全国的には、まだまだお札やコインで商品を購入することが一般的なようです。

普通、自動販売機の動作を考えるときには、まず状態遷移図で設計することが多いと思いますが、自分で書いてみると、かなり複雑な図になってしまいます。そこで、FLEXで直接その動作を記述できないかと考え、やってみたところ意外とすっきり書けることが分かりました。

今回の自動販売機としては、商品は「商品A」、「商品B」、「商品C」の3種類だけで、価格はそれぞれ100円、120円、150円とします。この自動販売内に投入できるお金は、1000円札、500円コイン、100円コイン、50円コイン、10円コインだけとします。1円コイン、5円コインなどは受け付けられずに、自動的にコイン返却口に戻るものとします。 また、2000円札、5000円札、10000円札も同様に紙幣判別機では受け付けられずに、入れても自動的に返却されるものとします。

さらに、1000円札、500円コイン、100円コイン、50円コイン、10円コインについては、それぞれ許容上限が決まっていて、1000円札は1枚、500円コインは3枚、100円コインは4枚、50円コインは18枚、10円コインは40枚までしか受け付けないこととし、上限を超えた紙幣やコインは自動的に返却されるものとします。この制限も今回のプログラムでチェックするようにしています。

こられのシミュレーションを行なうFLEXプログラムをtest15.lexとして、以下に示します。


/* FLEX テストプログラム15 */
%{
#include "flex_test.inc"
void ad_money( int ), out_shouhin( int );
int	yen_10, yen_50, yen_100, yen_500, yen_1000, goukei=0;

#define pr_money		printf("%sが投入されました。",yytext)
#define pr_goukei		printf(" 合計は%d円\n",goukei)
#define henkyaku		printf("お金は返却されました\n")
#define reset			yen_10, yen_50, yen_100, yen_500, yen_1000, goukei=0

#define LIMIT_10		40
#define LIMIT_50		18
#define LIMIT_100		4
#define LIMIT_500		3
#define LIMIT_1000		1
%}

%%
10円	{ ad_money(10);  }
50円	{ ad_money(50);  }
100円	{ ad_money(100);  }	
500円	{ ad_money(500);  }
1000円	{ ad_money(1000);  }

商品A	{ out_shouhin(100); pr_goukei; if(goukei==0) reset; }
商品B	{ out_shouhin(120); pr_goukei; if(goukei==0) reset; }
商品C	{ out_shouhin(150); pr_goukei; if(goukei==0) reset; }

返却	{ printf("おつりを%d円返却しました。\n\n",goukei); reset; }

\n	{}
.	{}
%%

void ad_money( int yen) {
	
	switch (yen) {
	   case 10: if (yen_10 < LIMIT_10) 
		{ ++yen_10; goukei+=yen; pr_money; pr_goukei;} 
		else henkyaku; break;
	   case 50: if (yen_50 < LIMIT_50) 
		{ ++yen_50; goukei+=yen; pr_money; pr_goukei; } 
		else henkyaku; break;
	   case 100: if (yen_100 < LIMIT_100) 
		{ ++yen_100; goukei+=yen; pr_money; pr_goukei; } 
		else henkyaku; break;
	   case 500: if (yen_500 < LIMIT_500) 
		{ ++yen_500; goukei+=yen; pr_money; pr_goukei; } 
		else henkyaku; break;
	   case 1000: if (yen_1000 < LIMIT_1000) 
		{ ++yen_1000; goukei+=yen; pr_money; pr_goukei; } 
		else henkyaku; break;
	}
}

void out_shouhin( int yen) {

	if ( goukei - yen  >=0 ) {
		goukei-=yen;
		printf("%d円の商品を出しました。",yen);
	} else
		printf("残高が足りません。");
}

int main(void)
{
	yylex();
	return(0);
}

次に、入力用のファイルex15.txtを用意します。このとき、入れた紙幣やコインは、「1000円」、「500円」、「100円」、「50円」、「10円」として入力します。また、商品選択ボタンが押された場合として、それぞれ「商品A」、「商品B」、「商品C」を入力します。最後に、返却レバーが回されることに対応して、「返却」を入力します。

FLEXプログラムtest15.lexをコンパイルし、このex15.txtを読み込むと次のような結果が出力されます。

いろいろと入力を変えてみて、動作を観察するのも面白いと思います。

日本語カスタマイザーについて

このサイトでは日本語カスタマイザーを利用して、プログラム言語の中で何とかして日本語を記述できるようにしようという試行の記録を掲載してきました。

試作当初は学会発表のレジュメで記述した日本語カスタマイザーの内容から、さまざまな試行実験を行うちにFLEXプログラムも少しずつ変わってきました。これからも変わっていくことは予想されますが、ここで簡単に「#日本語定義」部分について解説してみたいと思います。

プログラム言語の中で記述する日本語定義部分は、必ず行の先頭で「#日本語定義」から始まる文字列によって指定します。その後に1文字以上の区切り文字に続き、UTF-8において考えられる日本語の全角文字集合から始まる文字列とします。このとき、二文字目以降には英数文字でも構いませんが、最初の一文字は必ず日本語文字でなければならないこととします。この部分までだけならばこの日本語文字列(英数文字混在も可)は、自動的に変数が割り当てられます。

次に日本語文字列から、さらに区切り文字に続いて「"」(半角ダブルクォーテーションマーク)で囲った英数記号文字列を記述すると、日本語文字列をその英数記号文字列で置き換えることとします。この半角英数記号文字とは、半角英数文字に加えて、各種記号も認識できるようにしますが、これはプログラム言語で使われる予約語やコメント記号などとの兼ね合いによって、決められるものと考えています。以下にこの部分の状態遷移図を示します。

ここで、区切り文字というのは空白やタブのことです。これはC言語用の状態遷移図で、C言語のコメントは/*と*/で囲まれた部分ですので、コメント前部は/*を意味します。

一方、Java言語のコメントは/*から*/までと、//以降改行までの二種類がありますが、このJava言語用日本語FLEXプログラムを以下に示してみます。


/* 日本語カスタマイザー java */

§定義や前処理部のはじまり

#include	"jjava_rule.inc"

§定義や前処理部の終わり

この文字クラス定義部の全角UTF8文字集合はtest6.lexと同じ内容です。


英数記号文字集合	[[:alpha:][:digit:]_{}()., ~!#@$%^&+*-=:;/|<>']|"["|"]"|(\\)|(\\\x22)

区切り文字集合	[ \t]
二重引用符	(\x22)

§排他的開始 注釈*
§排他的開始 注釈//

§排他的開始 日本語定義
§排他的開始 日本語文字列
§排他的開始 英数記号文字列

§規則のはじまり

"/*"				{ 状態遷移→(注釈*); }
<注釈*>"*/"			{ 状態遷移→(初期状態); }
<注釈*>ほかの文字

"//"				{ 状態遷移→(注釈//); }
<注釈//>\n			{ 状態遷移→(初期状態); }
<注釈//>ほかの文字

行頭|"#日本語定義"		{ 状態遷移→(日本語定義); }

<日本語定義>{区切り文字集合}+			{ 状態遷移→(日本語文字列); }
<日本語定義>{区切り文字集合}+{英数記号文字集合}	{ エラー処理; }
<日本語定義>ほかの文字				{ エラー処理; }

<日本語文字列>{全角UTF8文字集合}			{ 文字列の出力; }
<日本語文字列>[[数字集合][英文字集合]]+		{ 文字列の出力; }
<日本語文字列>{区切り文字集合}*改行		{ 番号の付いた識別子を生成; 状態遷移→(初期状態); }
<日本語文字列>{区切り文字集合}*"//"		{ 番号の付いた識別子を生成; 状態遷移→(注釈//); }
<日本語文字列>{区切り文字集合}*"/*"		{ 番号の付いた識別子を生成; 状態遷移→(注釈*); }
<日本語文字列>{区切り文字集合}*{二重引用符}	{ 文字出力の定義前部; 状態遷移→(英数記号文字列); }
<日本語文字列>ほかの文字			{ エラー処理; }

<英数記号文字列>{英数記号文字集合}+{二重引用符}	{ 文字出力の文字部; 文字出力の定義後部; 状態遷移→(初期状態);}
<<英数記号文字列>ほかの文字				{ エラー処理; }

ほかの文字	{ 出力停止; }
改行		{ 出力停止; }
ファイルの終わり	{ 解析を強制終了; }

§規則の終わり

整数型 はじまり(型なし)
{

	ファイル2への出力前半部;

	字句解析部;

	ファイル2への出力後半部;

	解析終了;

}

Javaの日本語カスタマイザープログラムは、これ自身も日本語で書いています。日本語定義部分と「jjava_rule.inc」は掲載していませんが、FLEXプログラムは意外とすっきり書けるのではないかという気がします。 言うまでもありませんが、これが決定版のJava用日本語カスタマイザーというわけてはなく、このサイトの演習ぐらいはできるという程度ですので、改良のため変更することも度々あると思いますが、 これからプログラム言語の日本語化をやってみたいという方々の一助となれば幸いです。

yywrap関数について

yywrap関数の返す値は、構文解析器であるbisonと字句解析器であるflexとのインターフェースとして使われているようです。両者のインターフェースでは、トークンと呼ばれる数値により、やり取りが行なわれています。このサイトでは、主にflexを単体で使用することがほとんどですからyywrap関数の値を1(通常値)にしておけば、特にこの関数について気にする必要はないと考えています。

ここで、もし日本語カスタマイザーの前半処理部分をflexではなく、bisonで書き直せばどうなるのでしょうか。確かに「#日本語定義」部分だけはflexで書くよりもすっきりと記述できそうです。しかし、bisonで書こうとすると、「#日本語定義」部分以外をどうするかが問題となります。もし、各プログラム言語で規定されているすべての構文規則について記述するとなると、エラーの出ないように記述しないと、パーサーエラーメッセージが続出してしまいます。これではまず実現がとても難しいと思います。また、「#日本語定義」部以外はflexの方で処理しないようにするという方法も考えられますが、flexとbison間の文字列をやり取りするインターフェースを考える必要があります。やはり日本語カスタマイザーについては、flexの字句解析を2回行う方が、より簡単に実現できるのではないでしょうか。

 

参考図書
オートマトン・言語理論 本多波雄著 コロナ社 1972
UNIXプログラミング環境 Brian W.Kernighan Rob Pike 石田晴久監訳 アスキー出版局 1985
プログラミング言語C B.W. カーニハン D.M. リッチー 石田晴久訳 共立出版社 1981
UNIX 石田晴久著 共立出版社 1983
情報通信プロトコル 秋丸春夫 奥山徹共著 オーム社 2001
yacc/lex 五川女健治著 啓学出版社 1992
flex, version2.5 A Fast Scannar Generator ドキュメント University of California. Vern Paxsonほか多数のAuthor編著
Cプリプロセッサー・パワー 林晴比古著 日本ソフトバンク出版社 1988
HTMLハンドブック 渡辺竜生著 ソフトバンクパブリッシング 1999
Ubuntu6.10 システム utf-8 manual page
RFC3875 The Common Gateway Interface CGI Version 1.1