テキストファイルを最終行から読み込みたいが…
C言語でテキストファイルを読み込む際に、先頭行からではなく最終行から逆順に一行ずつ読み込みたかったのですが、標準の機能ではそういったことはできませんでした。(方法が見つかりませんでした。)
何とか最終行から読み込む方法は無いかと調査した結果、効率がよくなかったり、ファイルの行数が膨大な場合は対応できなかったりはするのですが、一つ方法があったので記録しておきます。
【方法】一度全体を読み込み各ポジションを保存しておく
現在のファイルポインタのポジション(次の読み込み開始位置)を取得できる ftell()
という関数があります。また、指定したポジションにファイルポインタを移動する fseek()
という関数があります。この 2 つの関数を組み合わせて使用します。
処理の流れは以下の通りです。
処理の流れ
- 対象ファイルを開く
fgets()
関数で先頭行から最終行まで一度読み込むと同時に、一行読み込むごとにftell()
関数でファイルポインタのポジションを取得してそれを保存する- 保存しておいたファイルポインタのポジション情報と
fseek()
関数を使用し、最終行から先頭行の順にそのポインタポジションに移動してからfgets()
関数で対象行の内容を読み込み、各行に対する処理を行う - 対象ファイルを閉じる
① 対象ファイルを開く
FILE *fh_input ;
fh_input = fopen(FILENAME, "r") ;
② ftell() 関数でファイルポインタのポジションを保存する
ftell() 関数の使い方
ftell( <ファイルポインタ> )
- 戻り値
- ファイルポインタのポジション(long int 型の値)
- 失敗時は -1L
- 戻り値
char readline[N] ;
long int fpos[M] ;
int linenum ;
linenum = 0 ;
do {
linenum++ ;
fpos[linenum] = ftell(fh_input) ;
} while (fgets(readline, N, fh_input) != NULL) ;
- ファイルポインタのポジションを保存する
long int
型の配列を使用します- 要素数は読み込むテキストの行数より大きくしておく必要があります
- テキストの行数を保存する
int
型の変数を使用します fgets()
関数で先頭から最後まで一行ずつ読み込み、一行読み込むごとにftell()
関数でファイルポインタのポジションを保存します
- 上記のコード例では結果的に
fpos[linenum]
にlinenum
行目の先頭位置のポジションが保存されます- 配列
fpos
の 0 番目の要素は使用しない方針です
- 配列
③ fseek() 関数でポジション移動しながらテキストを読み込む
fseek() 関数の使い方
fseek(<ファイルポインタ>, <ポジション>, <基準位置>)
- ポジション
- 基準位置からの移動バイト数
- 基準位置
- SEEK_SET : ファイルの先頭
- SEEK_CUR : 現在の位置
- SEEK_END : ファイルの終端
- ポジション
for (i = linenum - 1 ; i > 0 ; i--){
fseek(fh_input, fpos[i], SEEK_SET) ;
fgets(readline, N, fh_input) ;
//ここに各行に対する処理を記述
}
- 上記コード例では
fpos[linenum]
にlinenum
行目の先頭位置のポジションが保存されていますが、最終行から順に読み込みたいためfor
文でi = linenum - 1
からスタートし、デクリメントしていきます(linenum 番目はファイルの終端のため無視します)
ftell()
関数で取得したポジションはファイルの先頭を基準にしているため、fseek()
関数を使用時の基準位置はSEEK_SET
にします
④ 対象ファイルを閉じる
fclose(fh_input) ;
サンプルプログラム
テキストファイルの内容を最終行から先頭行に向かって順に出力するプログラムです。
#include <stdio.h>
#include <string.h>
#define N 1000 //一行の文字数上限
#define M 1000 //行数の上限
#define FILENAME "sample.txt" //読み込むファイル名
int main(int argc, char *argv[]){
char readline[N] ;
char *p ;
long int fpos[M] ;
int linenum ;
int i ;
FILE *fh_input ;
fh_input = fopen(FILENAME, "r") ;
//ポジションの取得と保存
linenum = 0 ;
do {
linenum++ ;
fpos[linenum] = ftell(fh_input) ;
} while (fgets(readline, N, fh_input) != NULL) ;
//linnum-1 番目のポジションが最終行の頭に該当するため i を linenum-1 から開始
for (i = linenum - 1 ; i > 0 ; i--){
fseek(fh_input, fpos[i], SEEK_SET) ;
fgets(readline, N, fh_input) ;
//改行を削除する処理(必要に応じて記述)
p = strchr(readline, '\n') ;
if(p != NULL) {
*p = '\0' ;
}
//ここに各行に対する処理を記述
printf("%s\n", readline) ;
}
fclose(fh_input) ;
return 0 ;
}
■sample.txt
line 1
line 2
line 3
line 4
line 5
■実行結果出力
line 5
line 4
line 3
line 2
line 1
「指定行の読み込み」も可能
上記のサンプルプログラムではテキストファイルの最終行から逆順に一行ずつ読み込みをしていますが、すべての行についてのファイルポインタのポジションを保存していることから、「指定した行を読み込む」ということも可能です。
■指定した行を読み込むコード
fseek(fh_input, fpos[i], SEEK_SET) ;
fgets(readline, N, fh_input) ;
※一行目の fseek()
内の fpos[i]
の「i
」を任意の数値(行番号)に変えれば指定行を読み込むことができます
【その他の方法】行順序が逆のテキストを用意する
上に記載した、ファイルポインタのポジションを保存する方法ではテキスト行数分の要素を持つ配列を使用する必要があるため、行数が数百万行単位のテキストを読み込む際には使用できません。
最終手段として、行順序が逆のテキストをあらかじめ作成しておき、それをプログラムで読み込むという方法があります。
行順序が逆のテキストの作成方法については以下記事に記載しています。