C言語講座 第5回

ファイルからの読み込み(written by Taku Nakahara)

皆さんがこれから卒業研究で書くであろうプログラムは

[何らかの入力ファイル] -> 【プログラム】 -> [何らかの出力]

という格好になると思います。たとえば GenBank 形式のファイルを読み込んでfasta形式に変換して書き出すとか、そういう感じです。

今回は、このようにプログラムにファイルを読ませて、何か処理をするプログラムの書き方を勉強します。

まず、Pythonの場合、以下のようになります。

(neco.py)

#!/usr/bin/env python3.4
import sys
for line in open(sys.argv[1]):
    print(line.rstrip('\n'))

これは、単にコマンド引数で指定したファイルを読み込んで表示するだけのプログラムです。Linuxコマンドで言えばcatと同じ働きをします。実質2行で書けてます。すばらしい。

これをCで書いてみますと、

(neco.c)

#include <stdio.h>
int main(int argc, char *argv[])
{
    FILE *fp; /* a file pointer. */
    char  line[100]; /* a string of an input file. */
    fp = fopen(argv[1], "r"); /* setting the file pointer. */
    while (fgets(line, 100, fp) != NULL) { /* fgets! */
        printf("%s", line); /* showing the line. */
    }
    fclose(fp); /* closing the file pointer. */
    return 0;
}

となります。main()関数だけで11行ですからPythonの5倍以上です。めんどくさいですね。

このプログラムを解説すると、1行目はおまじないです。これがないとファイルを読み込んだり結果を書き出したりできないんです。

2行目は前回やったコマンドラインからの引数です。各自復習してください。

4行目では「ファイルポインター」と言うものを定義しています。とりあえず、ファイルの実体を入れるための箱だと理解しておいてください。(ほんとはウソなんですが、いまはそれでいいです)

5行目では、読み込んだファイルの1行1行の文字列を入れておくための箱を用意しています。この変数定義だと100文字までしか入りませんので、1行に100文字以上含むような場合には使えません。ちなみにPDB fileは1行80文字と決まっているので、100文字分用意しておけば十分です。

6行目では、さっき用意した「ファイルを入れておくための箱」にファイルを入れています。ファイルオープン関数であるfopen()は、ファイル名とファイルを開く方式を与えると、その指定されたファイル(ポインター)を返してくれる関数です。

fopen(“ファイル名”, “ファイルを開く方式”) -> 指定されたファイルポインター

ファイルを開く方式とは、読み取り専用(“r”)、書き込み(“w”)、追加書き込み(“a”)等です。

7行目は説明が大変です。まず、while (×××) { …. } という構文は ×××が真のとき { …. } を繰り返し行うという意味です。Pythonでも同じなので分かると期待します。

×××にあたる(fgets(line, 100, fp) != NULL)ですが、fgets()というのはファイルを一行一行読み込む関数です。

fgets(“文字列を入れるための変数”, “文字列の長さ”, “ファイルポインター”)

という構文です。ファイルから一行読み込んで「文字列を入れるための変数」に入れます。1回目のfgetsでは一行目をよみこみ、2回目は2行目、3回目は3行目、、、という風に動作します。while ()の中で動いているので繰り返しfgetsが呼び出される訳です。そしてついにファイルの中身を全部読み込んでしまって読むものがなくなるとfgets()関数は「NULL」を返します。NULLというのは「何もない」という意味です。

fgets()がNULLを返すと、(fgets() == NULL)ということになります(当然)。そうすると、さきほどの while (×××) の中身である(fgets() != NULL)というのが偽になりますよね。そうすると while () はおしまいです。

while () { …. } の { …. } の部分(7行目)では、読み込んだ文字列(変数lineに入っている)を書き出しています。これで一行ずつ読み込んだファイルの中身を書き出している訳です。書き出すものがなくなると while (×××) の ×××が偽になって終了するという訳です。

9行目のfclose(fp);というのは、さっき開いたファイルポインターを閉じなさいよという命令になります。実はこのプログラムにおいてはこの動作は必要ないんですが、もっと複雑なプログラムを書くようになったときに、ファイルを開きっぱなしにしておくと不具合がおこる場合があるんです。今のうちから開いたファイルは必ず閉じるようにしておいた方が無難でしょう。まあエチケットみたいなもんです。

以上で、ファイルを開いて、読み込んで、閉じるという一連の基本動作を学んだ訳です。ただ、これだけではつまらないので、少し使えるプログラムを書いてみましょう。今度は、任意のPDBファイルを開いて、その中に何個原子が入っているのか数えるプログラムに改造してみましょう。

と、その前にPDB fileのフォーマットを復習しておきましょう。PDB fileにはそのタンパク質の由来であるとか発表された論文がどこにあるかと言った情報と、全原子の座標情報が含まれています。原子の座標情報は行頭がATOMから始まる行とHETATMから始まる行に書かれています。PDB file formatの詳細はここに書いてあります。今から書こうとしているプログラムは、PDB fileを読み込んでATOM行とHETATM行の数を数えるものです。

(countATOM.c)

#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
    FILE *fp; /* a file pointer. */
    char  line[100]; /* a string of an input file. */
    int   N = 0; /* the number of atoms. */
    fp = fopen(argv[1], "r"); /* setting the file pointer. */
    while (fgets(line, 100, fp) != NULL) { /* fgets! */
        if (strncmp("ATOM", line, 4) == 0 || strncmp("HETATM", line, 6) == 0) {
            printf("%s", line); /* showing the line. */
            N++; /* increment. */
        }
    }
    fclose(fp); /* closing the file pointer. */
    printf("The total number of atoms in the given PDB file is %d.\n", N);
}

neco.cと比較してcountATOM.cに追加されたところは7行目、10行目、12行目、16行目です。このプログラムのもっとも重要な部分は8行目です。

if (×××) { …. } というのは ×××が真のとき { …. } を行うという意味です。Pythonと同じですね。 ×××の中身にはstrncmp()という関数が使われています。これは

strncmp(“文字列1”, “文字列2”, “比較する文字数”)

という構文で“文字列1”と“文字列2”が等しいか判断して、同じなら0を返し、ちがう場合には0以外を返します。

つまり8行目のあたりは「もし、行の頭が“ATOM”か“HETATM”だったら、{ …. } をしなさいね」という意味になります。

{ …. } の中で個数を数えるint型の変数Nがインクリメントされて(1が足されて)、15行目で結果が表示されています。

練習問題

今度はコマンド引数に「ファイル名」と「探したい文字列」を指定できるようにneco.cを改造してください。これはLinuxコマンドgrepのしょぼい版です。(shobogrep.c)