前回は, テキストファイルおよびバイナリファイルから データを 1 byte ずつ(1文字ずつ)入出力できるようになった. 今回は,テキストファイルに限定し,データを1行ずつ入出力しよう.
さらに,1行の文字列から単語や数値を取り出したり, 単語や数値を組み合わせて文字列を作る方法についても勉強しよう.
テキストファイルを取り扱う場合, 複数の単語から成る1行分の文字列を処理対象とすることがよくある.
行の入力には,次の関数を利用する:
#include <stdio.h> char *fgets(char buf[], int n, FILE *fp)
ここで,buf[ ] は 1 行の文字列を記録するためのバッファ, n はバッファのサイズ(記録可能な最大の文字数)である. 戻り値は,通常,文字列バッファ buf へのポインタである. ただし,データを読み尽くした場合には,NULL を返す.
次のコードを読めば,動作を理解できるだろう:
/* fgets( ) のクローン */ char *fgets(char buf[], int n, FILE *fp) { int c; int i = 0; for (i = 0; i < n-1; i++) { // n-1 文字まで入力 if ((c = fgetc(fp)) == EOF) { if (i == 0) return (NULL); // 0 文字目で EOF ならば入力失敗 break; // 1 文字目以降ならば入力終了 } buf[i] = (char)c; i++; // 入力された文字をバッファへ格納 if (c == '\n') break; // 改行ならば入力終了 } buf[i] = '\0'; // 終端記号を追加 return (buf); // 行の先頭アドレスを返す }
なお,fgets( ) で取得したバッファ文字列には, 行末の改行 '\n' も記録される. さらに,バッファの末尾には,通常の文字列と同様に, 終端記号 '\0' が追加される. したがって,実際にファイルから入力される文字の個数は, バッファの要素数n 以内ではなく, n - 1 以内である.
この関数の利用例として, テキストファイルの行数を数えるプログラムを List 1 に示す.
#include <stdio.h> #include <stdlib.h> #define BUFSIZE 256 int lc(FILE *fp) { char buf[BUFSIZE]; int n = 0; while (fgets(buf, BUFSIZE, fp) != NULL) { // 一行ずつ入力 n++; // 行数をカウント } return (n); } int main(int argc, char *argv[]) { FILE *fp; int n; if (argc == 1) { fp = stdin; } else { if ((fp = fopen(argv[1], "r")) == NULL) return (EXIT_FAILURE); } n = lc(fp); printf("%d\n", n); fclose(fp); return (EXIT_SUCCESS); }
なお,fgets( ) とは逆に, 文字列をファイルへ出力する関数もある:
#include <stdio.h> int fputs(char *s, FILE *fp)
しかし,この関数を無理に使う必要はない. fprintf(fp, s) でも代用できるので. (fprintf(fp, "%s", s) だと少々冗長ですね.)
ちなみに, fgets( ) を fscanf(..., "%s", ... ) で代用することはできない. なぜなら,fscanf( ) では,空白が文字列の区切りとみなされるので, 空白を含むような文字列では, 一部分だけ(最初の一区切りまで)しか入力されないことになる.
参考:fgets() と scanf() の動作の違いを確かめるためのプログラム のソースコードと実行結果を比較せよ.
関数 fgets( ) で入力された文字列は,通常, 複数のトークン(単語や数字列など)と 区切り文字(空白やカンマなど)から構成されている. 行から各トークンを取り出すには, 関数 strtok( ) を利用する:
#include <string.h> char *strtok(行文字列, 区切り文字列)
なお,この関数は,標準ライブラリ関数としては異端の存在であり, 正しく利用するには十分な注意が必要である. 教科書 p.314 も参考にしよう. 複数のトークンを取り出すには, strtok( ) の呼び出しを繰り返すことになるが, 1回目と2回目以降とで引数を変更する必要がある:
char line[] = "Hello, World."; // 後で strtok() で書き換えるので文字配列に.文字列定数だとダメ char *word; // strtok() 1回目 word = strtok(line, " ,.\t\n"); // word = "Hello";(line[] の最初の単語)と同じこと
// 2回目
word = strtok(NULL, " ,.\t\n"); // word = "World";(line[] の2番目の単語)
// 最後
word = strtok(NULL, " ,.\t\n"); // word = NULL;
printf("%s", line); // "Hello, World." ではなく "Hello" だけを表示
List 2 に,strtok( ) の利用例として, 再び wc のクローンを示す.
#include <stdio.h> #include <stdlib.h> #include <string.h> #define BUFLEN 256 int wc(FILE *fp) { char buf[BUFLEN]; char *word; char *p; int n = 0; while (fgets(buf, BUFLEN, fp) != NULL) { p = buf; while (1) { word = strtok(p, " \t\n"); // 最初は strtok(buf, ...) if (word == NULL) break; n++; p = NULL; // 2回目以降 strtok(NULL, ....) } } return (n); } int main(int argc, char *argv[]) { FILE *fp; int n; if (argc == 1) { // コマンドライン引数無しの場合,標準入力 fp = stdin; } else if (argc == 2) { // 有りの場合,ファイル入力 if ((fp = fopen(argv[1], "r")) == NULL) return (EXIT_FAILURE); } else return (EXIT_FAILURE); n = wc(fp); printf("%d\n", n); fclose(fp); return (EXIT_SUCCESS); }
2回目以降の呼び出しで, 入力文字列の何文字目までが処理済みであるのかを strtok( ) に教えなくてもよいのか?と思った人は, グローバル変数や静的変数のことを思い出そう.
strtok( ) は,その関数の内部で, ローカルな静的変数を利用している. (これが strtok() が異端である所以.) そのため,複数の文字列に対して交互に strtok( ) を使ったりすると, おかしなことになってしまう. (なぜだかわかるかな?)
なお,複数の文字列に対して同時並行的に分割処理については, C言語の標準機能ではないが,strtok_r() を使えばできる.
なお,行文字列の形式(単語の個数やデータ型)が事前にわかっている場合には, strtok( ) よりも sscanf( ) が便利である:
#include <stdio.h> sscanf(文字列, 書式, ポインタ1, ポインタ2, ...);
これも fscanf( ) の仲間であるが, ファイルからではなく,文字列からデータを読み取るものである.
複数の単語や数字を組み合わせて文字列を作るには, sprintf( ) を使う:
#include <stdio.h> sprintf(文字配列, 書式, データ1, データ2, ...);
これも fprintf( ) の仲間であるが, ファイルではなく,文字配列へデータを書き込むものである.
利用例を List 3 に示す. これは,バックアップファイル(ファイルのコピー)を作成するプログラムであり, 元のファイル名に拡張子 .bak を追加して, バックアップのファイル名としている. データのコピーには, 前回の cp() 関数(fgetc版)を流用している.
#include <stdio.h> #include <stdlib.h> void cp(FILE *fin, FILE *fout) { ... } int main(int argc, char *argv[]) { FILE *fin, *fout; char file[256]; if (argc != 2) goto ERR_ARG; if ((fin = fopen(argv[1], "r")) == NULL) goto ERR_FIN; sprintf(file, "%s.bak", argv[1]); if ((fout = fopen(file, "w")) == NULL) goto ERR_FOUT; cp(fin, fout); fclose(fin); fclose(fout); return (EXIT_SUCCESS); ERR_FOUT : fclose(fin); ERR_FIN : ERR_ARG : return (EXIT_FAILURE); }
実行例:
$ ls
backup backup.c
$ ./backup backup.c
$ ls
backup backup.c backup.c.bak
$ less backup.c.bak
... # backup.c と同じ内容のハズ
Tips: 以前にも紹介したように, sprintf() で書式文字列を生成すると, 見栄えの良い出力結果を得られるようになる.
例: sprintf(fmt, "%%d\n", w); printf(fmt, data); これは,書式文字列 fmt を生成し, 数値 data を桁数 w で表示している.
このテクニックはいろいろ応用できるので, 上級者を目指すなら是非,理解しておこう.
次の2題のうち,どちらか一方または両方に取り組もう.
実行例:
$ cat table.txt 氏名 国語 数学 英語 秋元才加 65 70 70 板野友美 60 75 70 ... $ ./p-cut 0 table.txt 氏名 秋元才加 板野友美 ... $ ./p-cut 2 table.txt 数学 70 75 ... $ ./p-cut 10 table.txt # 簡単のため(たとえば入力に空行があっても OK とするため), # 存在しない列を指定してもエラーとはしないでよい. $ ./p-cut 使い方:p-cut 列番号 ファイル名
実行例:
$ wc -l p-split.c # 元のファイルの行数を調べている. 67 $ ./p-split 10 p-split.c # 10 行ずつに分割している. $ ls p-split p-split.c # 元のファイルの他に... p-split.c.000 # 分割ファイルができた! p-split.c.001 p-split.c.002 : : p-split.c.006 $ wc -l psplit.c.* # 分割ファイルの行数を調べている. 10 p-split.c.000 # 10 行ずつになっている! 10 p-split.c.001 10 p-split.c.002 : : 7 p-split.c.006 $ less p-split.c.002 # 内容も確認してみる(11行目から20行目のハズ) $ cat p-split.c.* > copy.c # 分割ファイルを連結している. $ diff -s p-split.c copy.c # ファイルの違いを調べている. Files p-split.c and copy.c are identical. # 元通りになった!
ヒント:分割ファイルの名前に番号を付けるには, sprintf(..., "%s.%03d", ...) などとすればよい. 復元の際の連結順序を維持するために, 番号の桁数を揃える必要がある. そのため,変換指定子 "%03d"の「0」が非常に重要. もし,桁数を揃えないと,たとえば,1,2,3,...,100 の順序が, 1,10,100,11,12,...,2,20,21,...,99 とかに乱れてしまうだろう.
なお,オリジナルの cut および split に興味のある人は, オンラインマニュアル等を参照しよう.
$ man cut $ man split