第34回 プログラミングについて 『テキストファイルの読み込み』

『テキストファイルの読み込み』
以前テキストファイルの形式について触れたことがありましたので、今回はテキスト
ファイルの読み込みについて考えてみましょう。
テキストファイルを読み込むときには、通常は文字単位または行単位のどちらかで行ないます。特殊なときにはテキストファイルとは考えずに任意のバイト数で読み込むこともありますが、ここでは特殊な場合は考えないことにします。
かなり特殊な形式のものですが次のテキストファイルを見て下さい。
data1.txt :
(
(Title "Text sample")
(SubTitle "Sample format 1")
(Data
(Name "Data1")
(Size
(X 100.0)
(Y 200.0)
(Z 300.0)
)
)
)
ちょっとLISP風の形式です。このテキストファイルの形式にはカラム数、文字列以外のスペースや文字ケース、および改行は意味を持たないとすると、次のようになっていても同じ意味になります。
data2.txt :
((tItLE "Text sample")(s ub tiTLE"Sample format 1" )
(dATa(Name"Data1")(Si
ze(X10 0.0)(Y 20
0.0)(z300.0)) ))
かなり意地悪にしましたが、この data2.txt を data1.txt のようにきれいに書き直すことを考えてみましょう。
このようなファイルを読み込むときにまず考えなければならないことは、1文字づつ読み込むか、1行づつ読み込むかを決めることです。ここでは1行づつ読み込むことにしましょう。
メイン関数を次のようにします。簡単にするために入出力のファイル名を固定し、エラーの処理は行ないません。またテキストの文字列にはダブルクォーテーションと2バイト文字は含まれないものとします。
#include <stdio.h>
main()
{
FILE * in_fp;
FILE *out_fp;
in_fp=fopen("data2.txt","r");
out_fp=fopen("data3.txt","w");
convert_form(in_fp,out_fp);
fclose( in_fp);
fclose(out_fp);
}
次に convert_form 関数を作成してみましょう。どこからどこまでをこの関数が管理するかはプログラムの目的や好みにもよりますが、ここではこの関数は読み込んだ文字がスペースまたはタブのときには文字列以外のときには無視し、無視できないときには出力することまでを管理することとします。
convert_form(in_fp,out_fp)
FILE *in_fp;
FILE *out_fp;
{
char c;
int is_string;
is_string=0; -(1)
while(get_next_char(in_fp,&c)){ -(2)
if( c=='"' )is_string^=1; -(3)
else if((c==' ' || c=='\t') && !is_string)continue; -(4)
output(out_fp,is_string,c);
}
}
ここで、
get_next_char は読み込んだ次の文字を教えてくれる関数
output は出力を実際に行なってくれる関数
とします。
変数 is_string 現在の文字が文字列内のものか否かを保持するもので、行(1)で0に
し、文字列ではないとしておきます。
(2)の get_next_char 関数は次の文字があるときにはその文字を変数 c に代入して教
えてくれ、戻り値は真を返し、ないときには偽を返すようにしループを形成します。
(3)では文字がダブルクォーテーションのときには is_string の値を切り替えます。
1でXORをとると0が1に1が0になることはXORの話をしたときに説明しまし
た。
(4)では文字が文字列中でないときにスペースかタブであれば無視します。
次に関数 get_next_char は、
get_next_char(in_fp,c)
FILE *in_fp;
char *c;
{
static char text[MAX_TEXT];
static char *t=NULL;
while(1){
if(t==NULL){
fgets(text,MAX_TEXT,in_fp);
if(feof(in_fp))return 0;
t=text;
}
if(*t=='\n'){
t=NULL;
}
else{
*c= *t++;
return 1;
}
}
}
この関数は保持している文字列がそれ以上ないときにはファイルから行単位で読み込み、改行文字以外のすべての文字を順番に呼び出し側に教えます。この関数が改行文字についての処理を行ないますので、呼び出し側からすればファイルから読み込んだテキストには改行がないものとして処理して構わないことになります。
次に出力を行なう関数 output は次のようになります。
output(out_fp,is_string,c)
FILE *out_fp;
int is_string;
char c;
{
#define INDENT 1
static char *keyword[]={
"(Title","(SubTitle","(Data","(Name","(Size","(X","(Y","(Z",NULL};
static char text[MAX_TEXT];
static char *t=NULL;
static int depth=0;
int i;
if(c=='(' && !is_string){
if(t){
for(i=0;i<depth-INDENT;i++)fprintf(out_fp," ");
fprintf(out_fp,"%s\n",text);
t=NULL;
}
depth+=INDENT;
}
if(!t)t=text;
*t++ =c;
*t ='\0';
if(!is_string){
if(c==')'){
depth-=INDENT;
for(i=0;i<depth;i++)fprintf(out_fp," ");
fprintf(out_fp,"%s\n",text);
t=NULL;
}
else{
char **k;
k=keyword;
while(*k){
if(stricmp(text,*k)==0){
strcpy(text,*k);
*t++ =' ' ;
*t ='\0';
break;
}
k++;
}
}
}
#undef INDENT
}
ちょっと長くなってしまいましたが、この関数では出力しなければならない文字を受け取った後、インデント(段下げ)と改行を行い出力します。出力するタイミングは、右カッコを受け取ったときにはそれまで保持していた文字列のみを出力し、左カッコを受け取ったときには受け取った文字も含めてすべて出力します。それ以外のときには文字を保持します。
また (Title などのキーになる文字列をチェックしているのは、ソースデータとなるテキストが文字列以外のときには空白文字は無視しても構わないことになっていますので、 (Title"Text sample") や (X100.0) などとキーになる文字と、文字列や数値との間に1つの空白を入れることと、キーの文字列の大文字、小文字を見やすくするのが目的です。
上の例では清書するのが目的でしたが、このファイルを解釈してデータを取得するとなれば、プログラムの書き方を少し変えなければなりません。また項目の順番が任意であり、必要のないものは省略しても構わないとなるとちょっとしたテクニックが必要になります。プロいグラムの全文を紹介すると長くなってしまいますので、大切な部分のみをお話します。
このテキストファイルからデータを取得するには、例文ではキーになる文字列が違ったキーの中にはありませんが、次のようなときには各キーによってプログラムもそれに従って入れ子になるようにした方が作りやすくなります(プログラムは長くなってしまいますが)。
(
(Title "Text sample")
(SubTitle "Sample format 1")
(Data
(Name "Data1")
(X 100.0)
(Z 300.0)
(Size
(Name "Data2")
(X 100.0)
(Y 200.0)
(Z 300.0)
)
)
)
また、不要な項目は記述しなくてもいいときには、左カッコで閉じられた後、次のカッコが右カッコか左カッコなのかを調べなくてはなりません。次のカッコが右カッコのときには自分自身でその内容を処理し、左カッコのときにはその左カッコは呼び出し側に処理させるようにする仕組みを作らなければなりません。
そこでプログラムを簡単にするために、関数 get_next_char が文字列中の文字か否かを管理し、更に呼び出し側が読み込んだ文字をこの関数に戻せるようにします。
get_next_char(in_fp,is_get,c)
FILE *in_fp;
int is_get;
char *c;
{
static char pend[MAX_TEXT];
static char *p=NULL;
static char text[MAX_TEXT];
static char *t=NULL;
static int is_string=0;
if(is_get){
if(p){
*c = *(--p);
if(p==pend)p=NULL;
return 1;
}
while(1){
if(t==NULL){
fgets(text,MAX_TEXT,in_fp);
if(feof(in_fp))return 0;
t=text;
}
if(*t=='\n'){
t=NULL;
}
else{
if(*t=='"')is_string^=1;
if((*t==' ' || *t=='\t') && !is_string){
t++;
}
else{
*c= *t++;
return 1;
}
}
}
}
else{
if(p==NULL)p=pend;
*p++ = c;
}
}
実際にデータを取得する関数は最初の例の関数 convert_form の代わりに、get_dataとすると、
get_data(in_fp)
FILE *in_fp;
{
char c;
while(get_next_char(in_fp,1,&c)){
if(c=='(')get_data_depth1(in_fp);
else if(c==')')break;
}
}
とし、一番外側のカッコのみを処理させます。次に関数 get_data_depth1 は1つ内側のカッコを処理させます。
get_data_depth1(in_fp)
FILE *in_fp;
{
char text[MAX_TEXT];
char *t;
char c;
t=text;
while(get_next_char(in_fp,1,&c)){
if(c==')'){
get_text_char(in_fp,0,&c);
break;
}
*t++ =c;
*t ='\0';
if(stricmp(text,"(title")==0){
:
:
}
else if(stricmp(text,"(subtitle")==0){
:
:
}
else if(stricmp(text,"(data")==0){
get_data_depth2(in_fp);
}
}
}
このようにテキストファイルの内容に応じて素直にプログラムも入れ子にしていきます。
今回は少しへそ曲がりなテキストを対象にしてファイルの読み込みについて考えてみました。通常はこのような形式のテキストを処理することはあまりありませんが、へそ曲がりなテキストほど読み込むときには関数の役割をハッキリと分担させないとプログラムが難しくなってしまいます。
要するに、ファイルから読み込み、次の文字を教える関数はそれだけに専念し、出力する関数は出力だけに専念できるようにしておくことです。こうすることでプログラムを作成するときも改修するときも全体のことを気にしなくてもよく、完成までの時間が短縮されるはずです。
それでは次回。