C言語の研究・演習編5
第5章 ファイル処理
いよいよC言語の研究・演習編の最後を飾る章の到来となった。
ここではデータベースの基礎となるファイル処理についてしっかり研究しよう。
1 ファイルの順次処理
ファイルに書きこまれている1つ1つのデータをレコードと呼ぶ。レコードについては構造体のところで説明したが、ここではファイルに書きこまれているレコードを順に取り出して処理する方法、すなわち順次処理について見て行こう。
(1) レコードのソート
ファイルに書きこまれているレコードをある項目をキーとしてソートしてみよう。
最初に、名前と年齢をスペースで区切った次のようなデータを用意しよう。
Pell 21
Akira 20
Bruce 21
Dragon 19
Sae 18
Jacky 25
Hideo 30
Ayuko 14
Zoro 28
Pana 16
Londo 59
Ben 26
Tree 36
Kura 89
Voice 12
Godzilla 10
Maruko 8
Hero 21
Super 45
Void 14
Xero 11
Year 16
Kreo 12
Rich 26
Polo 58
Kuku 81
Garo 22
Earth 1234
データ数はいくつあるか知らないが、このデータを「sort.txt」と名づけて適当なディレクトリに保存しておこう。私の場合は、C:\Cというディレクトリに保存した。
a. 少量データの場合のソート
今回は少量データを対象として、データを最初に全部メモリに読み込んでソートさせる方法を採用しよう。
今のデータファイル、sort.txtを使うこととして、今回は頭から8個のデータだけに絞ってソートさせてみよう。
このファイルをfscanf関数で読んで、ソート処理を行った後、同一ディレクトリに「sort_kekka.txt」なるファイルを自動的に作らせてこれに書きこんでやろう。
それでは見てみよう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ARRAY_SIZE 8
typedef struct in_data{
char name[10];
int age;
}DATA;
DATA *s_sort(DATA *, int);
void swap(DATA *, DATA *);
void main()
{
FILE *fpi;
FILE *fpo;
DATA buff[ARRAY_SIZE];
DATA *pb;
int i;
pb=buff;
fpi = fopen("C:\\c\\sort.txt","r");
for(i = 0; i < ARRAY_SIZE ; i++){
fscanf(fpi, "%s %d" ,(pb+i)->name
,&(pb+i)->age);
printf("%s %-3d\n"
,(pb+i)->name , (pb+i)->age);
}
fclose(fpi);
s_sort(pb, ARRAY_SIZE);
printf("\n");
fpo = fopen("c:\\c\\sort_kekka.txt","w");
for(i=0; i<ARRAY_SIZE; i++){
fprintf(fpo, "%s
%d\n",(pb+i)->name ,(pb+i)->age);
printf("%s
%-3d\n",(pb+i)->name, (pb+i)->age);
}
fclose(fpo);
}
DATA *s_sort(DATA *p, int n )
{
int i, a ;
for(i = 0; i < n-1; i++){
for(a = i+1; a < n; a++)
if( strcmp(
(p+i)->name , (p+a)->name ) >0) swap((p+i), (p+a));
}
return p;
}
void swap(DATA *pa, DATA *pb)
{
DATA temp;
temp = *pa;
*pa = *pb;
*pb = temp;
}
いかがだろう、久々のファイルオープンとクローズを思い出していただいただろうか。
このようにファイル処理中は何をしているか見えないので、途中でprintf関数を入れてやる方法は前にも説明したとおりだ。
どうだろうか、ちゃんと8つのソートされたデータのファイルが「sort_kekka.txt」として作成されただろうか。
b. 大容量のデータのソート
大容量のデータのソートはディスクに作業領域を作って、全部ここでソートを行わせる方法もある。
しかし、メモリーの速さはディスクの1000倍であることから、ここではファイルに書き込まれているデータをいくつかに分割し、ソートとマージを組み合わせる方法をとってみよう。
そのやり方は以下のとおりである。
・ファイルに書き込まれているレコードのうち、可能な分だけメモリーに読み込む。
・読み込んだレコードを先ほどの方法でソートする。
・結果を一時ファイルに書き込む。
レコードがあるだけ、上記の処理を繰り返す。すると、繰り返した分の一時ファイルが作成される。
・作成された一時ファイルをマージして昇順にソートし、出力ファイルに書き込む。
ここで、作成されたn個の一時ファイルをマージする方法を説明しよう。
・n個の一時ファイルの全てに対し、処理が終了したかどうかを示すスイッチを用意し、全てOFFにしておく。
・n個の一時ファィルの全てと出力ファイルをオープンする。
・n個の一時ファイルから最初のレコードを1つずつ読み込む。
・n個のレコードからキーが最も小さいレコードを探し、それを出力ファイルに書き込む。この一時ファイルは次のレコードを読み込む。ファイルが終了(EOF)の場合は、その一時ファイルの処理が終了したことを明確にするため、スイッチをONにする。
・全ての一時ファィルがONになるまで赤字の部分を繰り返す。
・n個の一時ファイルと出力ファイルをクローズする。
長くなったが、以上のような方法で、一旦ファイルを分割して一時ファイルに分け、あとでマージするだけのことである。
それでは早速見てみよう。
今回も C:\Cにあるsort.txt、つまり先に使ったデータを同一ディレクトリのsort_kekka.txtに書き込むこととした。なお、一時ファイルは「TEMP」の後に0から順番に番号をつけた拡張子なしの名前にした。
#include <stdio.h>
#include <string.h>
#define ARRAY_SIZE 10 /*一時ファイルの最大データ数を10個とする*/
#define OFF 1
#define ON 0
typedef struct in_data{
char name[10];
int age;
}DATA;
char f_mei[20];
void merge(int, char *);
int sw_check(int *, int);
int get_min(int *, int, DATA *);
DATA *s_sort(DATA *, int);
void swap(DATA *, DATA *);
void main()
{
FILE *fpi;
FILE *fpo;
DATA buff[ARRAY_SIZE];
DATA *pb;
int i;
int sw = OFF;
int n;
char *temp_name = "TEMP";
int temp_n = 0;
pb=buff;
fpi = fopen("c:\\c\\sort.txt","r");
while(sw == OFF){
for(i = 0; i < ARRAY_SIZE ; i++){ /*10個またはEOFまでfscanf*/
if(fscanf(fpi, "%s
%d",(pb+i)->name, &(pb+i)->age) == EOF){
sw = ON;
fclose(fpi);
break;
}
}
s_sort(pb, i); /*先ほどと同様のソート*/
n = i;
*(temp_name + 4) = '0' + temp_n; /*temp_nameの5文字目temp_n*/
strcpy(f_mei ,"c:\\c\\"); /*フルパス*/
strcat(f_mei, temp_name); /*フルパスと一時ファイル名*/
temp_n++;
fpo = fopen(f_mei,"w");
for(i = 0; i < n; i++){
fprintf(fpo, "%s
%d\n",(pb+i)->name ,(pb+i)->age); /*データ書き込み*/
}
printf("一時ファイル%sはデータ%d個\n",f_mei
,i);
fclose(fpo);
}
merge(temp_n ,"c:\\c\\sort_kekka.txt");
}
DATA *s_sort(DATA *p, int n )
{
int i, a ;
for(i = 0; i < n-1; i++){
for(a = i+1; a < n; a++)
if( strcmp(
(p+i)->name , (p+a)->name ) >0) swap((p+i), (p+a));
}
return p;
}
void swap(DATA *pa, DATA *pb)
{
DATA temp;
temp = *pa;
*pa = *pb;
*pb = temp;
}
void merge(int temp_n, char *out_name)
{
FILE *fpi[10];
FILE *fpo;
DATA buff[10];
DATA *pb;
int i;
int sw[10];
int min_i;
char *temp_name = "TEMP";
pb =buff;
for(i = 0; i < temp_n; i++){ /*一時ファイルを全部open*/
strcpy(f_mei,"c:\\c\\");
*(temp_name+4) = '0' + i;
strcat(f_mei,temp_name);
fpi[i] = fopen(f_mei,"r");
}
fpo= fopen(out_name,"w"); /*出力ファイルをオープン*/
for(i = 0; i < temp_n; i++){ /*全一時ファイルの最初のレコードを読み込む*/
fscanf(fpi[i], "%s
%d",(pb+i)->name ,&(pb+i)->age);
sw[i] = OFF;
}
while(sw_check(sw, temp_n) == OFF){ /*全部の一時ファイルがEOFなら終了*/
min_i = get_min(sw, temp_n, pb); /*最小のデータを求める*/
fprintf(fpo,"%s
%d\n",(pb+min_i)->name, (pb+min_i)->age);
if(fscanf(fpi[min_i], "%s
%d",(pb+min_i)->name,&(pb+min_i)->age) != EOF)
; /*何もしないの意味*/
else
sw[min_i] = ON;
}
for(i = 0; i < temp_n; i++) fclose(fpi[i]); /*全一時ファイルのクローズ*/
fclose(fpo);
}
int sw_check(int *sw, int n)
{
int i;
int ret_code = ON;
for(i = 0; i < n; i++){
if(*(sw+i) == OFF){
ret_code = OFF;
break;
}
}
return ret_code;
}
int get_min(int *sw, int n, DATA *pb)
{
int i;
int min_i;
for (min_i = 0; min_i < n; min_i++){
if(*(sw+min_i) == OFF) break;
}
for(i=min_i; i < n; i++){
if(*(sw+i) == OFF){
if(strcmp((pb+min_i)->name,(pb+i)->name) >0)
min_i = i;
}
}
return min_i;
}
どうだろうか、長いプログラムで頭が痛いぜと言われるかも知れない。
しかし、今回増えたのは、merge、get_min、sw_checkの各関数だけで、あとは一時ファィルを途中でフルパス名を加えて書き込み、そして読み込みしているくらいのことだ。よく見てみると、どうってことのないプログラムであることがお分かりになることだろう。
(2) レコードの分類
プログラマやシステム設計にたずさわったことのある人は、プログラムやシステムを作成するよりも、データ作成の方がはるかに大変であることを知っていると思う。プログラムなんかは一度完成すればメンテナンスなどは別にして、一時は解放される。しかし、データ作成はその業務が終了するか業務が変わるかしない限り、半永久的に続く作業だからである。
この気持ちで、今回も次のデータを「rei.txt」と命名して、C:\Cなんかに置いておくこととしよう。
Pell 11
Akira 10
Bruce 11
Dragon 19
Sae 18
Jacky 15
Hideo 10
Ayuko 14
Zoro 10
Pana 16
Londo 19
Ben 15
Tree 16
Kura 19
Voice 14
Godzilla 15
Maruko 18
Hero 11
Super 15
Void 14
Xero 19
Year 16
Kreo 18
Rich 12
Polo 15
Kuku 18
Garo 12
Earth 13
今回はこれらのデータを年齢別に分類するプログラムである。
方法としては、データを順番に読んで、何種類かの出力ファイルに同一年齢の者のデータを書き込んでいく訳であるが、どのファイルに書き込んだらいいかを示すint型配列を準備し、今回は10種類のファイルを用意するとして、配列も10個用意し、これを全部-1で初期化しておき、順番に読み出すデータの年齢が配列の内容になければ、その年齢を配列の0番要素に書き込み、ファィル名もREI0.txtのように、配列の要素番号を赤字の部分に付けてやればいい。
次のデータを読んで、前の年齢と同一であれば、配列を探すと同じ年齢の要素が見つかるので、その配列の要素番号を示すファイル、この場合はREI0.txtに書き込んでやればいい。
もし、次に読んだデータの年齢が配列になければ、要素が-1の配列要素として書き込み、新規ファイル名、この場合は、REI1.txtとして同じ処理の繰り返しさ。
さあ、見てみよう。
#include <stdio.h>
#include <string.h>
typedef struct in_data{
char name[21];
int age;
}DATA;
void main()
{
FILE *fpi;
FILE *fpo[10];
int key_age[10];
int *pk;
DATA buff;
DATA *pb;
int i;
char *file_name = "REIa.txt";
char f_name[20];
pk = key_age;
pb = &buff;
for(i = 0 ; i < 10 ; i++) *(pk+i)=-1;
fpi = fopen("c:\\c\\rei.txt", "r");
while (fscanf(fpi, "%s %d", pb->name, &pb->age) !=
EOF){
for(i = 0; *(pk+i) != -1 && *(pk+i) !=
pb->age; i++); /*-1でない同年齢の配列要素を探す*/
if(*(pk+i) == -1){ /*配列にない場合は新規処理*/
*(pk+i) = pb->age;
*(file_name + 3) = '0'
+ i;
strcpy(f_name ,
"C:\\C\\");
strcat(f_name,
file_name);
fpo[i] =
fopen(f_name,"w");
printf("%d歳なし、新規にREI%d.txtを作成\n",pb->age,
i);
}
else
printf("%d歳あり、REI%d.txtに追加\n",pb->age,
i);
fprintf(fpo[i], "%s
%d\n",pb->name, pb->age); /*既にある場合はファイルも既にopenしている*/
}
fclose(fpi);
for (i = 0; *(pk+i) <0; i++) fclose(fpo[i]);
}
どうだろうか、この場合いくつかの年齢毎に分類したファイルが作成されるが、REI0.txtから年齢の順番で分類されているわけではない。
これを年齢順に分類したければ、あらかじめソートしておけばよいだろう。
2 ファイルのランダム処理
ファイルに書きこまれているデータ、つまりレコードの中から必要なレコードを探す場合、順次アクセスしていたら時間がかかってしかたがない。
これを避けるために、索引ファイルを準備しておこう。
索引ファイルの内容は、各レコードのキーおよび位置である。
(1) 索引ファイルの作成
いま、C:\Cのディレクトリに、「data.txt」というファイルがあり、先頭行がレコードの件数が書き込まれ、続いて次の行から学生番号と名前と年齢をスペースで区切った下のデータがあるとする。
10
0001 Akira 21
0002 Ayuko 22
0003 Ben 23
0004 Bruce 24
0005 Dragon 25
0006 Earth 26
0007 Garo 27
0008 Godzilla 28
0009 Hero 29
0010 Hideo 30
このデータを元にして、学生番号とレコードの位置をスペースで区切った索引ファイル、「idx.txt」を同一ディレクトリに作成してみよう。
レコード位置を求める標準ライブラリのftell関数を思い出して、さっそく見てみよう。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct list{
char no[5];
char name[21];
int age;
}DATA;
typedef struct index_list{
char no[5];
long int pos;
}IND;
void main()
{
FILE *fpi;
FILE *fpo;
DATA buff;
DATA *pb;
IND i_buff;
IND *pi;
int n, i;
pb = &buff;
pi = &i_buff;
fpi=fopen("c:\\c\\data.txt", "r");
fpo=fopen("c:\\c\\idx.txt", "w");
fscanf(fpi,"%d",&n);
for(i=0; i < n; i++){
pi->pos = ftell(fpi);
fscanf(fpi,"%s %s %d",pb->no,
pb->name, &pb->age);
fprintf(fpo,"%s %ld\n", pb->no
,pi->pos);
}
printf("データ件数は%d件です\n",n);
printf("索引ファイル c:\\c\\idx.txtを作成しました\n");
fclose(fpi);
fclose(fpo);
}
どうだろうか、同一ディレクトリに下のような内容の索引ファイル・idx.txtが作成されただろうか。
<プログラムによって作成された索引ファイル・idx.txtの内容〜参考>
0001 2
0002 17
0003 32
0004 45
0005 60
0006 76
0007 91
0008 105
0009 123
0010 137
(2) 索引ファイルによるデータ検索
こんどは、先ほど(1)で作成した索引ファイル「idx.txt」を用いて指定された学生番号を持つレコードを読みとって表示させてみよう。
方法はいたって簡単である。最初にメモリ上に索引ファイルの内容を全て読み取る。これは例のmalloc関数を使用して索引レコードの構造体へのポインタの大きさの領域をレコード分実行時に確保しておこう。
後は、このメモリ上の索引を探して、データファイルの位置を知った上で、標準ライブラリのfseek関数で、ファイルの位置を設定して表示するだけである。
最初にレコード数を表示して、scanf関数により探したい学生番号をキーボードから入力してもらうようにした。
データファイルについても、(1)で使ったデータ「data.txt」をそのまま利用する。
それでは見てみよう。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct list{
char no[5];
char name[10];
int age;
}DATA;
typedef struct index_list{
char no[5];
long int pos;
}IND;
char bango[5];
void main()
{
FILE *fpi;
FILE *fpd;
DATA buff;
DATA *pb;
IND *i_buff, *p;
int n, i;
pb = &buff;
fpd = fopen("c:\\c\\data.txt","r");
fpi = fopen("c:\\c\\idx.txt","r");
fscanf(fpd,"%d",&n);
printf("レコード件数は%d件です\n\n",n);
i_buff = (IND *)malloc(n*sizeof(IND)); /*メモリ領域の確保*/
p = i_buff;
for(i = 0; i < n; i++){ /*メモリへの索引内容のコピー*/
fscanf(fpi,"%s %ld", p->no
,&p->pos);
p++;
}
printf("検索する学生番号を入力してください(終了はCTRL+Z)
: ");
while(gets(bango) != NULL){
printf("-----Result-----\n");
p = i_buff;
for(i = 0; i < n; i++){ /*メモリの学生番号との対比*/
if(strcmp(bango,
p->no) == 0) break;
p++;
}
if(i != n){
fseek(fpd, p->pos,
SEEK_SET);
fscanf(fpd,"%s %s
%d",pb->no, pb->name, &pb->age);
printf("番号は
%s\n",pb->no);
printf("名前は%s\n",pb->name);
printf("年は%d歳です\n\n",pb->age);
}
else
printf("%s番は該当なし\n\n",bango);
printf("検索する学生番号を入力してください(終了はCTRL+Z)
: ");
}
free(i_buff); /*メモリ領域の解放*/
fclose(fpi);
fclose(fpd);
}
いかがだろうか。コンパクトながら、なかなか楽しいプログラムだね。
さて例によって、最後の章の終わりも情報処理技術者試験を味わってみよう。次のような問題だ。
電話による企業等団体向けの情報提供サービスの利用状況が、情報利用ログファイルに格納されている。これを読んで、顧客コード別に利用回数、利用時間を集計し、利用料金を計算して出力するプログラムである。
(1) 情報利用ログファィル(is_log.txt)は次のテキストファイルであり、その各行は1回の情報提供サービスの利用データレコードである。利用データコードには、利用者コード、利用日、利用時間(秒数)の順でデータ項目が並んでいる。
000005#1234 980215 489
000002#1234 980101 87
000004#1234 980505 123
000004#1234 980807 456
000002#1234 980215 89
000005#1234 980101 235
000002#1234 981231 451
000001#1234 980105 140
000001#1234 980215 280
000003#1234 980505 235
000003#1234 980904 563
000004#1234 980101 586
000005#1234 980203 100
000003#1234 980101 123
000005#1234 980904 123
000001#1234 980412 890
@各データ項目は1つ以上の連続する空白文字で区切られている。
A利用者コードの形式は、 顧客コード#個人コードとし、この例では個人コードは全部同じにしておいた。
B顧客コードは長さ6文字の数字列、個人コードは長さ6文字以下の数字列とする。
C利用日は長さ6文字の数字列である。
D1回の利用時間は1時間以内である。
E顧客コードの総利用時間については分単位で出力し、端数は切り上げる。
F出力項目は、顧客コード、利用回数、利用時間(分)、利用料金であり、顧客コードの昇順に出力する。
この条件で顧客のために利用情報を出力するプログラムだ。
さあ見てみよう。
例によって私の場合、あいも変わらず情報利用ログファィル(is_log.txt)はC:\Cというディレクトリに置いた。
#include<stdio.h>
#include<string.h>
void main()
{
typedef struct table{
char ccode[7];
int count;
long gtime;
} DATA;
DATA tbl[10];
DATA *pt;
FILE *fp;
char ucode[20];
char date[10];
int utime, ntbl, cmp, k, m;
long min, charge;
pt = tbl;
fp = fopen("C:\\C\\is_log.txt", "r");
ntbl = 0;
while(fscanf(fp, "%s %s %d", ucode, date, &utime) != EOF){
cmp = 1;
for(k = 0 ; k < ntbl ; k++)
if((cmp =
strncmp((pt+k)->ccode , ucode ,6)) >= 0)
break;
if(cmp == 0){
(pt+k)->count++;
(pt+k)->gtime += utime;
}
else{
for(m = ntbl ; m > k ; m--) tbl[m] = tbl[m-1];
strncpy((pt+k)->ccode, ucode, 6);
(pt+k)->ccode[6] = NULL;
(pt+k)->count = 1;
(pt+k)->gtime = utime;
ntbl++;
}
}
fclose(fp);
printf(" ---Information Service Account
Report---\n");
printf(" Client
Access Time
Charge\n");
printf(" Code
Count (min)
(yen)\n");
printf("
---------------------------------------\n");
for(k = 0 ; k < ntbl ; k++){
min = ( (pt+k)->gtime +
59)/60;
charge = min * 50;
printf( "
%-9s %5d %5ld
%5ld\n"
,(pt+k)->ccode
,(pt+k)->count ,min ,charge);
}
}
どうだろうか。これまた例によって赤字部分が穴埋め式だ。
面白いのは、顧客コードの順番に並んでいないログファイルから、顧客コードの昇順に出力するため、読んだレコードの顧客コードより上のレコードを全部後にずらす処理をしているところかな。
結構、現実的な事務処理的プログラムだったね。