プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第115回 プログラミングについて『テキストファイルを読み込む その3』
コラム
2025/07/31

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

アポロレポート

           プログラミングについて 第115回目

           『テキストファイルを読み込む その3』

 今回はコンマで区切られたフォーマットのファイルを扱うことを考えてみましょう。
コンマで区切られたフォーマットのファイルといえば、エクセルのCSVファイルなどが良くみかけるファイルです。ここでは次のような規則のファイルとします。

(1)1つのレコードは1行に記述される。

(2)各項目はコンマで区切られる。

(3)各項目の空白文字は意味を持たない。ただし文字列内のすべての文字は意味を持つ。

(4)文字列はダブルクォーテーションで囲む。文字列内のダブルクォーテーションはバックスラッシュでエスケープする。バックスラッシュもバックスラッシュでエスケープする。

(5)1レコードの項目数の制限はない。

(6)1項目のバイト数に制限はない。

(7)ファイルの終端(EOF)がデータの終端とする。

なんだか面倒くさい定義ですが、要するに次のようなものです。

abc, d e f , " d e f " , "\"\\\"" ,,

最初の項目 abc は問題ありませんね。2番目の d e f は空白が意味を持たないのでdef を意味します。3番目の " d e f " は文字列ですので空白は無視できません。
4番目の "\"\\\"" は "\" という文字を意味します。5番目は何も値のない項目です。

1つのレコードは1行に記述されるとしていますので改行コードまでが1つのレコードになりますが、1つの項目のバイト数に制限がないとしていますので行単位ではなく文字単位でファイルから読み込むことにします。実際に文字を読み込む部分は関数として、1つの項目の文字列へのポインタと、まだ項目が続くのか、その項目で1つのレコードが終わりなのか、ファイルの終端(EOF)に到達してそれ以上読み込めなかったのかの状態を返すようにします(関数名は next_item とします)。

まずはメインの関数です。

 #include <stdio.h>

 #define NEXT_ITEM_STS_EOF  1
 #define NEXT_ITEM_STS_EOL  2
 #define NEXT_ITEM_STS_CONT 3

 main()
 {
  FILE *fp;
  char *item;
  int sts;

  fp=fopen("comma.dat","r");

  while(1){
   sts=next_item(fp,&item);
   if(sts==NEXT_ITEM_STS_EOF)break; /* EOFに達したら終わり */

   printf("%d |%s|\n",sts,item);   /* 状態値と項目の値を表示 */
  }

  fclose(fp);
 }

ここまでは難しくはないと思います。次が問題の next_item です。ちょっと長くなってしまいましたが頑張って理解して下さい。

 next_item(fp,item)
 FILE *fp;
 char **item;
 {
                 /* 読み込んでいる項目を単語とし、変数名は
                   word にしてあります */
  static char *word  =NULL;  /* 読み込んでいる項目用のバッファ */
  static int max_word=0;    /* word が確保しているメモリサイズ */

  int lword,sts,c,is_string;

  sts=NEXT_ITEM_STS_EOF;    /* 戻り値。
                   最初はEOFに到達したと設定しておく */

  lword=0;           /* 現在読み込んでいる単語の長さ。
                   最初は 0 にしておく /

  is_string=0;         /* 文字列(ダブルクォーテーションで囲まれたも
                   の)か否かを示す変数。最初は 0(文字列では
                   ない)にしておく */

  while((c=fgetc(fp))!=EOF){
   if(is_string){       /* is_string が 0 以外のときには文字列で、
                   エスケープ文字 \ のときには次の文字を
                   無条件で値として使用するため is_string
                   の値を 2 にして再び読み込みます。
                   is_string が 2 のときには無条件で採用する
                   ので is_string を 1 に戻します。
                   それ以外のとき読み込んだ文字が " のときに
                   は文字列を終了し再び読み込みます。 */

    if(is_string==2){ is_string=1;      } /* 2 のときには 1 にする */
    else if(c=='\\'){ is_string=2; continue; } /* \ のときには 2 にする */
    else if(c=='"' ){ is_string=0; continue; } /* " のときには 0 にする */
   }
   else{            /* 文字列でないとき */

                 /* スペースとタブを無視します。
    if(c==' ' || c=='\t')continue;

                 /* 読み込んだ文字が " のときには文字列の開始
                   になりますので、is_string を 1 にして再び
                   読み込みます。
                   コンマのときには1つの項目の終了ですので、
                   sts にまだ項目が続くという値を設定してルー
                   プを終了します。
                   改行('\n')のときには1つの項目の終了で
                   あり、レコードの終わりでもあるので、
                   sts に1つのレコードの終了の値を設定して
                   ループを終了します。*/

    if(c=='"' ){ is_string=1         ; continue; }
    if(c==',' ){ sts   =NEXT_ITEM_STS_CONT; break  ; }
    if(c=='\n'){ sts   =NEXT_ITEM_STS_EOL ; break  ; }
   }
                  /* word に確保してあるメモリーサイズより
                   項目の値の長さが長いときにはそれまでより
                   1バイト大きいサイズのメモリを確保し、読
                   み込んだ文字を追加し、そうでないときには
                   そのまま追加します。*/

   if(lword>=max_word)word=(char *)realloc(word,++max_word);
   word[lword++]=c;
  }
                  /* 項目の文字列の最後にヌルを追加します。
                   このとき word に確保してあるメモリサイズ
                   が足りないときには1バイト多く確保します
                   。*/
  if(lword>=max_word)word=(char *)realloc(word,++max_word);
  word[lword]='\0';
                  /* 引数 item の示している場所に word への
                   ポインタを代入し、sts を戻り値として呼び
                   出し側に戻ります。*/
  *item=word;

  return sts;
 }

理解できたでしょうか。前回の方法では1文字ごとにメモリを再確保していたのですが今回のものはそれまで確保していたメモリサイズでは足りないときにのみ再確保するようにしました。この方法の場合、最大サイズの項目+1の回数しかメモリを再確保しませんので、読み込むデータが多い場合でもメモリ確保に消費される時間が大幅に低減されます。

ここまではなんとか理解できたでしょうか。

次に、テキストファイルを読み込むという本題からは若干外れますが、ここまでは標準出力に出力するだけでしたが、このデータをメモリー内に保持することを考えてみましょう。数種類考えられますが、構造体を使ってリストを生成するのが最も効率がいいと思います。

 struct list_data_strct{
  char *item;
  struct list_data_strct *next;
 };

 struct list_head_strct{
  struct list_data_strct *list_data;
  struct list_head_strct *next;
 };

このような構造体を使ってみましょう。この構造体の場合のデータの格納のイメージは次のようになります。

1行目 list_head→list_data→next→next ...
     ↓
2行目 list_head→list_data→next→next ...
     ↓
3行目 list_head→list_data→next→next ...
     ↓
4行目 list_head→list_data→next→next ...

list_head_strct 構造体の先頭(1行目)が次の list_head_strct 構造体を指し示し、
これが順に最後の行まで同様に続きます。またそれぞれの list_head_strct 構造体は
list_data_strct 構造体の先頭(各行の最初の項目)を指し示します。list_data_strct
構造体は次の list_head_strct 構造体(次の項目)を指し示し1行の最終の項目までを
指し示します。これをプログラムにしてみるとメイン関数は、

 #include <stdio.h>
 #include <malloc.h>

 #define NEXT_ITEM_STS_EOF  1
 #define NEXT_ITEM_STS_EOL  2
 #define NEXT_ITEM_STS_CONT 3

 struct list_data_strct{
  char *item;
  struct list_data_strct *next;
 };

 struct list_head_strct{
  struct list_data_strct *list_data;
  struct list_head_strct *next;
 };
 struct list_head_strct *list_head_top=NULL;
 struct list_head_strct *list_head_end=NULL;
 struct list_data_strct *list_data_end=NULL;

 main()
 {
  FILE *fp;
  char *item;
  int sts;

  fp=fopen("comma.dat","r");

  while(1){
   sts=next_item(fp,&item);
   if(sts==NEXT_ITEM_STS_EOF)break;

      if(sts==NEXT_ITEM_STS_CONT)add_item(item,0);
   else if(sts==NEXT_ITEM_STS_EOL )add_item(item,1);
  }

  fclose(fp);
 }

ここで、

 struct list_head_strct *list_head_top=NULL;
 struct list_head_strct *list_head_end=NULL;
 struct list_data_strct *list_data_end=NULL;

の部分は、

 struct list_head_strct *list_head_top=NULL;

が最初の行を指し示すのに使用します。

 struct list_head_strct *list_head_end=NULL;

は読み込み中の最後の行を指し示すのに使用します。

 struct list_data_strct *list_data_end=NULL;

は読み込み中の最後の項目を指し示します。

関数 next_item は同じですので省略します。

関数 add_item は、最初の引数は項目の文字列へのポインタ、第2引数は最後の項目のときには 1 を、そうでないときには 0 を指定します。

関数 add_item は次のようになります。

 add_item(item,is_end)
 char *item;
 int is_end;
 {
  struct list_data_strct *ld;

  ld=(struct list_data_strct *)malloc(sizeof(struct list_data_strct));
  ld->item=(char *)malloc(strlen(item)+1);
  strcpy(ld->item,item);
  ld->next=NULL;

  if(list_data_end==NULL){
   struct list_head_strct *lh;

   lh=(struct list_head_strct *)malloc(sizeof(struct list_head_strct));

   if(list_head_end==NULL)list_head_top   =lh;
   else          list_head_end->next=lh;

   list_head_end=lh;

   list_head_end->list_data=ld;
  }
  else{
   list_data_end->next=ld;
  }

  if(is_end)list_data_end=NULL;
  else   list_data_end=ld;
 }

ちょっと難しいのですが頑張って理解して下さい。

さて、上の構造体を使ったデータ表現では、1つの行の項目数が多く、プログラムの処理中に頻繁に各項目をアクセスしようとしたときには、項目の検索に時間がかかってしまいます。というのも、

 list_head→list_data→next→next ...

という形式で各項目を保持していますので、10番目の項目のときには、

 list_head→list_data→next→next ...
      1     2   3   ... 10

というように10回ポインタを辿っていかなければなりません。この無駄を省くには、

 struct list_head_strct{
  char **item;
  int  litem;
  struct list_head_strct *next;
 };
 struct list_head_strct *list_head_top=NULL;
 struct list_head_strct *list_head_end=NULL;
 struct list_head_strct *list_head_cur=NULL;

というような構造体にします(list_data_strct は使いません)。そしてメンバーのitem は配列にしてしまうのです。そうすると関数 add_item は、

 add_item(item,is_end)
 char *item;
 int is_end;
 {
  if(list_head_cur==NULL){
   list_head_cur=(struct list_head_strct *)malloc(sizeof(struct list_head_strct));

   if(list_head_end==NULL)list_head_top  =list_head_cur;
   else         list_head_end->next=list_head_cur;

   list_head_end=list_head_cur;

   list_head_cur-> item=NULL;
   list_head_cur->litem=0;
   list_head_cur->next =NULL;
  }

  list_head_cur->item=(char **)realloc(
  list_head_cur->item,
  sizeof(char *)*(list_head_cur->litem+1));

  list_head_cur->item[list_head_cur->litem]=(char *)malloc(strlen(item)+1);

  strcpy(list_head_cur->item[(list_head_cur->litem)++],item);

  if(is_end)list_head_cur=NULL;
 }

こうすると、10番目の要素は

 list_head→item[9]

というように無駄なくアクセスできるようになります。どのようなデータ構造を採用するかは、扱うデータの内容とプログラムが処理しなければならない内容で変えなければなりません。

それではまた次回。

そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?