6. 書式指定・ファイル入出力・文字列処理

ここではこれまではオマジナイとして使ってきた write(*,*)read(*,*) の意味を理解し,ファイル入出力や,出力時の書式指定の仕方,さらには文字列処理の方法について学ぼう.

参考

6.1. 書式指定

実は write(*,*) の2つめの引数は出力される値の書式を表す 編集記述子 を指定するためのもので, * はデフォルトの書式を意味している(したがって一般にコンパイラによって出力の書式は異なる).ここに決められた形式で書式を指定することで桁を揃えたりして,綺麗な出力を得ることが出来る.例えば

 4  character(128) :: fmt
 5  character(128) :: text
 6  integer :: year = 2014
 7
 8  real(8) :: x, y, z
 9
10  ! 6桁で出力
11  write(*, '(i6)') year
12
13  ! 編集記述子を文字型変数として渡す
14  fmt = '(i6)'
15  write(*, fmt) year

のように * の代わりの編集記述子(ここでは '(i6)'fmt)を指定することになる.11行目のように直接指定してもよいし,14-15行目のように文字型の変数を用いてもどちらでも構わない.

さて,問題は書式の指定の仕方(上の例では i6)についてである.書式指定の詳細については適宜他の文献 [1] を参照してもらうとして,ここでは以下の表に挙げる典型的な例を理解しておけば良い.

編集記述子の使い方の例

データ型

記述子

例の説明

整数型

I

i6

6桁で出力する.

i8.6

8桁で出力する. ただし6桁に満たない部分は0で埋められる.

実数

F

f12.5

12桁で出力,うち5桁が小数点以下.

E

e20.7

科学的表記 [2] の20桁で出力,うち7桁が小数点以下.

文字型

A

a

与えられた文字型の字数に対応する桁数を確保して出力.

a30

30桁で右寄せの出力.

空白

X

5X

指定された数だけ空白を出力.この場合は5桁.

改行

/

/

改行される.

上の表は編集記述子の使い方の典型的な例を示している.基本的には 編集記述子表示桁数 の組み合わせの形で表すことになっている.以下にいくつか注意点を挙げておこう:

  • 数値データは指定された書式で出力が出来ない場合の出力は 全て "*" が出力されてしまう ので気をつけよう.実数データの出力では符号,小数点,指数部の桁数を考慮しなくてはならないので,E 型記述子では 全桁数 \(\geq\) 小数部の桁数 + 6 となっている必要がある.F 型編集記述子の場合はデータ(整数部の桁数)に依存するが少なくとも 全桁数 \(\geq\) 小数部の桁数 + 2 が必要である.これは以下のような例によって確認することが出来る.

1write(*,'(f5.3)') 3.1415      ! これはOK
2write(*,'(f5.3)') 3.1415 * 10 ! これはダメ
3write(*,'(e9.3)') 3.1415      ! これはOK
4write(*,'(e5.3)') 3.1415      ! これはダメ
  • 文字型データでは,桁数が渡された文字数よりも少ない場合は文字型の先頭から桁数分だけが出力される.

  • 複数の編集記述子をカンマ , で区切って並べることが出来る.この際に,編集記述子ではない通常の文字列も同様にカンマで区切って並べることが出来る.ただし指定子の文字列が ' (シングルクオート)で区切られている場合には,その中の文字列は " (ダブルクオート)で囲まなければならない.使い方は以下の通りである.

43  ! 複数の記述子を並べる
44  write(*, '(e10.3, e10.3, e10.3)') x, y, z
45
46  ! 文字列および改行を使う
47  write(*, '("x = ", e10.3, /, "y = ", e10.3, /, "z = ", e10.3)') x, y, z
  • 同じ書式指定子を複数回繰り返す場合には書式指定子の前に繰り返す回数を指定する省略記法が使える.例えば '(a10, 3i5)''(a10, i5, i5, i5)' と等しい.

また書式指定は write(*, fmt='(a)') のように行うこともできる [3].他には改行を抑制するために advance='no' のような指定も使うことがしばしばあるので覚えておいて損はない.Fortranでは通常 write 文によって最後に自動的に改行が挿入されてしまうが,

49  ! 出力後に改行しない
50  write(*, fmt='(a)', advance='no') 'Input some text : '

のようにすれば 'Input some text : ' が表示された後に改行されない.

なお read(*,*) についても同様の指定が可能であるが,それほど必要な場面はないかもしれない.一つ考えられるのはスペースを含んだ文字列(例えばファイルの1行分)を一つの文字型変数に読み込みたいときであろう.このときには

52  ! 改行まで文字を読み込む
53  read(*, '(a)') text

のように '(a)' を指定しないとスペースまでの文字列が text に取り込まれてしまう.これはFortranがデフォルトでスペースや改行などを変数の区切りとして読み込んでしまうからである.

6.2. ファイル入出力

6.2.1. openclose

これまではファイルから何らかのデータを読み込んだり,ファイルに出力する時にはシェルのリダイレクト機能を用いてきた.しかし,これだと例えば複数のファイルから別々に違うデータを読み込んだり,複数ファイルへの出力などの柔軟は処理はできない.このような場合にはソースコードの中で明示的に入出力に用いるファイルを指定し,そのファイルに対する入出力処理を行うように指定しなければならない.Fortranプログラムからファイルを開くには open 文を用いることになる.例えば以下の例は予め存在しないファイル filename.dat を新規作成して開く.(この例では,ファイルが既に存在している時にはエラーとなる.)

 4  integer :: ios
 5
 6  !
 7  ! ファイルを開く
 8  !
 9  ! unit     : 装置番号(ファイルを識別するための数値)
10  ! iostat   : 正常時は0
11  ! file     : ファイル名
12  ! action   : 操作
13  ! form     : formatted(アスキー形式) or unformatted(バイナリ形式)
14  ! status   : new, old, replace
15  ! position : rewind, append
16  !
17
18  ! * ファイルの新規作成(既に存在する場合はエラー)
19  open(unit=10, iostat=ios, file='ascii.dat', action='write', &
20       & form='formatted', status='new')

open には多くの引数を指定することになる.(あまりに多いので通常は optional属性とキーワード引数 † で必要なものだけ渡す.) それぞれの意味は以下の様なものである.

unit

装置番号を指定する.装置番号とはファイルを特徴付ける一意な整数である.なお標準入力は5,標準出力は6,標準エラー出力は0,と予め決められているのでこれら以外の値を指定すること.1つのプログラム中で同時に複数のファイルを開く際には違う値を指定しなければならない.自分でプログラムを書く際には10以上の重複しない整数にしておくのが無難である.

iostat

ここに指定した整数型の変数に open 文の終了ステータスが格納される.ファイルが正常に開けた場合には 0,そうでない場合には 0 以外の値が返される.この変数をチェックすることでエラーチェックをするのが定石である.

file

ファイル名を指定する.

action

ファイルに対する操作を指定する.読み取り専用なら read,書き込み専用なら write,読み書きどちらもしたい場合には readwrite を指定する.デフォルトでは(可能ならば) readwrite となる.

form

アスキー形式(書式付き)なら formatted,バイナリ形式(書式なし)なら unformatted を指定する.デフォルトでは formatted となる.

status

ファイルの状態を指定する.new はファイルの新規作成を意味し,そのファイルが既に存在する時にはエラーが発生する.replace も新規作成であるが,そのファイルが既に存在する場合は中身を破棄して空のファイルとして開く.old は既に存在するファイルを開く.このときそのファイルが存在していない場合はエラーが発生する.他にも処理系依存の unknown があり,これがデフォルトである.

ファイルが正常に開けたかどうかは以下のようにチェックすることが出来る.

30  ! ファイルが正常に開けたかどうかをチェックする
31  if(ios /= 0) then
32    write(*, *) 'Failed to open file for output'
33    stop
34  endif

いちいちチェックするのは面倒なようだが,プログラミングにおいてこのようなエラーチェックは非常に大切なのでいつもチェックする癖をつけよう.プログラム作成の際にその半分がエラーチェックになるというのもよくある話である [4]

以下にいくつかよく使う例を挙げておこう.(念の為に言っておくと open に渡す引数の詳細を覚えておく必要は無い.大まかな使い方さえ知っておけば,後はgoogleで検索する方が早い.)

  • ファイルを新規作成する.ただしファイルが既に存在する場合にはその中身を破棄する(上書き).

1open(unit=10, iostat=ios, file='ascii.dat', action='write', &
2     & form='formatted', status='replace')
  • 既に存在するファイルを開き,位置をファイル終端に指定する( position='append' ).既存のファイルの終端にデータを追加する場合に用いる.

1open(unit=10, iostat=ios, file='ascii.dat', action='write', &
2     & form='formatted', status='old', position='append')
  • 既に存在するファイルを読み込み専用で開き,位置をファイル先頭に指定する( position='rewind').既存のファイルの先頭からデータを読み込む場合に用いる.ただし position=rewind は必ずしも指定しなくても良い.

1open(unit=10, iostat=ios, file='ascii.dat', action='read', &
2     & form='formatted', status='old', position='rewind')

なお,open で開いたファイルは close で閉じるのが作法である.ファイルを閉じないままプログラムが異常終了してしまうと,せっかくの結果が正しく出力されないこともあり得るので注意して欲しい.close には open で開いた時に用いた装置番号を指定する.例えば装置番号が10であれば

1close(10)

とすれば良い.

6.2.2. アスキー(書式付き)入出力

アスキー形式とかテキスト形式と呼ばれるファイルはテキストエディタや cat などのコマンドで人間が理解できる形で表示できるファイルである.アスキー形式でファイルを開くには openform='formatted' と指定するのは既に説明した通りである.

開いたファイルへの入出力をするには,writeread に装置番号を指定しなければならない.実はこれまで使ってきた write(*,*)read(*,*) の1番目の引数は装置番号を意味するものである.ここでも"* "はデフォルトの装置番号を意味し,通常は write であれば標準出力の6,read であれば標準入力の5を指定したことと同じ意味となる.ここに open 文で指定した装置番号を代わりに指定することで,write の出力先,read の入力先ファイルが指定出来る.

なお装置番号が何であっても2番目の引数には同じように編集記述子を指定すれば良い.例えばファイル(装置番号=10)にデータを出力するには

20  ! ファイル(装置番号=10)にデータを書き出し
21  do i = 1, 64
22    x = real(i, 8) / real(n - 1, 8)
23    y = cos(2 * 3.1415_8 * x)
24    write(10, '(e20.8, e20.8)') x, y
25  enddo

のようにすればよい.もちろん編集記述子をデフォルトの * にすることも可能である.

6.2.3. バイナリ(書式なし)入出力

バイナリ形式とは一般にテキスト形式以外の,人間がそのままでは解釈できない形式のファイルの総称である.アスキー形式での入出力では計算機の内部表現(メモリ上のビット列)を人間に解釈できる形式に逐一変換して入出力を行っている.Fortranでは openform='unformatted' を指定して開いたファイルに対してはこのような変換が行われず,メモリ上のビット列が(ほぼ)そのまま出力されることになる.アスキー形式への変換が行われないため,編集記述子は指定することは出来ず,write(10)read(20) のような形で装置番号のみを指定して入出力を行う.

バイナリ入出力を行うメリットとして,まず高速であることが挙げられる.これはテキスト形式への変換を行わないことに加えて,テキスト形式での入出力に比べてデータ量を少なく抑えることが出来るためである.また内部的に2進数で表されている実数は10進数では正確に表現出来ないことから,実数型のデータはアスキー形式への変換に伴い情報が失われてしまうが,バイナリ形式での入出力ではこの問題がない.

デメリットは人間の目ではデータの中身が判別出来ないことである.従って,バイナリで出力されたデータを読み込むにはどのような形式で出力されたのかを予め正確に知っていなければならない.例えば,

1write(10) x
2write(10) y

と出力されたデータを読み込むには

1read(10) x
2read(10) y

としなければならないし,出力が

1write(10) x, y

であれば入力は

1read(10) x, y

とする必要がある.すなわち write で指定した変数並びと全く同じ変数並びで read しなければ正しく読み込みが出来ない.(大変困ったものであるが,ストリーム入出力 † を用いれば少なくともこの問題は生じない .)また,当然であるが,xy配列の入出力 の時と同様に配列でも良い.

以下では,unformatted形式でファイルにデータを出力しそれを読み込む例 sample4.f90 を見ながら具体的に考えてみよう.

29  ! 配列サイズの出力
30  write(10) n
31
32  ! 配列の出力
33  write(10) x, y

では配列 x および y の長さである整数 n と,配列そのものをファイルに出力している.ここで作成したファイルを一度閉じた上で,

49  read(20) n
50
51  allocate(xx(n))
52  allocate(yy(n))
53
54  ! 配列を読み込む
55  read(20) xx, yy

によってデータを読み込む.ここで, write の形式と read の形式が全く同じになっていることが分かるだろう.

なお,同じ形式で入出力をしたとしても一般には異なる環境で作成したバイナリファイルには互換性が無い.ただし,この問題については多くの場合に対応が可能である(次節参照).

6.2.4. バイトオーダー

マルチバイトのデータを計算機のメモリ上に配置する方法のことをバイトオーダーとかエンディアンなどと呼ぶ.分かりやすい(?)例としてIPアドレスを考えよう.IPアドレスは(IPv4では)4バイトで表され,192.168.1.0のように1バイトごとに"."で区切って記述するのが一般的である.これをメモリに格納する際に192,168,1,0の順に格納する方法をビッグエンディアン,逆に0,1,168,192の順に格納する方法をリトルエンディアンと呼んでいる.このバイトオーダーはCPU依存である.Intel系のCPUの場合はリトルエンディアンが採用されているので,普通のPCを扱っている限りはバイトオーダーを気にする必要は無い.一方でスーパーコンピューターなどではビッグエンディアンが採用されているCPUも比較的多く,そのような計算機で出力したデータを手元のPCで読み込む際にはバイトオーダーの変換が必要になってくる.

この変換は手動で行うことも出来るが,コンパイラのオプションで自動的に変換を行うように指定することも可能である.例えばgfortranの場合はコンパイルオプションに -fconvert=big-endian を用いると,入出力がビッグエンディアンのバイトオーダーで行われる.他にも環境変数 F_UFMTENDIAN を指定することでもバイトオーダーを指定することが出来るようである.

6.2.5. ストリーム入出力

Fortranの unformatted のバイナリデータは一般にはC言語の freadfwrite による入出力と互換性が無い.これはFortranは各 write 文で出力されるデータの前後に余計なデータ(ヘッダーおよびフッター)を付加するためである [5].新しいFortran 2003規格ではストリーム入出力というC言語の freadfwrite と同じ(余計なデータを付加しない)読み書きが出来るようになっている.これには以下のように access='stream' を指定してファイルを開く.

1open(unit=10, file='binary.dat', access='stream', form='unformatted')

このように開かれたファイルに対しては

1write(10) x, y, z

1write(10) x
2write(10) y
3write(10) z

では全く同じデータが出力される.このように生成されたデータファイルはC言語の fread を用いて正常に読み込みが出来るし,また fwrite で出力したデータもFortranの read 文で読み込むことが可能である.

6.3. 文字列処理

6.3.1. 文字型変数の宣言

文字型変数の宣言は

1character(len=長さ) :: 変数名 = 初期化文字列

のように行えば良いことは既に学んだ.ここで指定した文字数に足りない部分はスペースで埋められる.従って,例えば長さ10の文字型変数に 'ABCDE' を代入すると,その後ろに空白文字が5文字代入される.定数の場合は文字型の初期化時には長さを陽に指定しなくても

4  character(len=*), parameter :: text = 'initialization by this string'

のように len=* を指定して初期化すると,自動的に初期化文字列の長さを持った文字型変数となる [6]

6.3.2. 結合と代入

文字型変数は // 演算子を用いて結合することや,結合した文字列を他の文字型変数に代入することが出来る.ただしこの際には空白の存在に注意しなければならない.例えば

5  character(len=16) :: a, b, c, d

のように宣言して

 9  a = 'This'
10  b = 'is'
11  c = 'a'
12  d = 'pen'
13
14  ! 何も考えずに出力
15  write(*, *) a, b, c, d
16
17  ! 文字列を結合して出力
18  write(*, *) a//b//c//d

のようにした場合には,15行目と18行目の出力はいずれも

1This            is              a               pen

のようになるであろう.これは abcd のどれも16文字の文字列であるので,16文字に満たない部分は全て空白で埋められているためである.したがって,そのまま出力しても(15行目), // で結合しても(18行目),空白は取り除かれずに残ることになってしまう.

文字列の前後の空白文字を除去するには trim という組み込み関数を用いれば良い.空白を除去して各文字列間に1文字分ずつ空白を入れて出力するには

1write(*, '(a,x,a,x,a,x,a)') trim(a), trim(b), trim(c), trim(d)

などとすれば良い.ここで書式記述子の x が空白を表す.これによって以下の様な出力が得られる.

1This is a pen

6.3.3. 部分文字列

配列の場合には a(1:10) のように部分配列を用いることが出来た.これと同じことが文字型変数に対しても出来る.例えば

6  character(len=64) :: str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

のように宣言された str に対して,たとえば str(1:1) であれば Astr(6:10) であれば FGHIJ のように文字列の一部(部分文字列)を取り出すことができる.以下は特定の文字列の検索を行い,該当部分だけを取り出すような処理の例である.

29  ! XYZと一致する部分文字列を検索し, 開始位置のインデックスを返す
30  i1 = index(str, 'XYZ')
31  i2 = i1 + len('XYZ')
32  write(*, *) str(i1:i2)  ! XYZを出力

ここで組込み関数 index(s1, s2) は文字列 s1 の中から s2 に一致する部分文字列を検索し,開始位置のインデックスを返す関数である.文字列操作に関するその他の組み込み関数については富田・齋藤(2011,7章)などを参照のこと.

6.3.4. 内部ファイル

プログラム中で必要な文字列のフォーマットを揃えるのには 内部ファイル と呼ばれる機能を知っていると何かと便利である.これまでに write の一番目の引数には装置番号を指定することを学んだが,以下の例では装置番号の代わりに文字型変数 str が指定されている.

34  ! 内部ファイルを用いて連番のファイル名を作成
35  do n = 1, 8
36    write(str, '("data",i3.3,".dat")') n
37    write(*, '(a)') str
38  enddo

これによりフォーマットされた文字列が str に代入される.この例では data001.datdata002.dat , ... のように連番のファイル名を作成して出力する.

6.4. ファイル終端までの逐次処理

文字列処理ではファイルの中身を一行ずつ読み込んで何らかの処理を行うことが多い.以下のサンプルコードは標準入力から1行ずつ順に読み込み処理をする例である.ファイルが終端に達したかどうかを調べるには read の終了ステータス iostat を取得すれば良い.正常に読み込みが実行された時には iostat が0となり,これが負の場合にはファイルの終端,正の場合には何らかのエラーが発生したことを意味する.なお,実際にはこのような処理はシェルスクリプトやPerl,Python,Rubyなどを使って実装する方が圧倒的に簡単である.(何でもかんでもFortranでやろうとするのは無駄が多い.)

 4  integer, parameter :: max_line = 256
 5  integer :: ios, nline
 6  character(max_line) :: line
 7
 8  nline = 0
 9
10  !
11  ! fmt='(a)'  : 行末までを読み込むのに必要(途中のスペースで途切れないように)
12  ! iostat=ios : ステータスがiosに代入される
13  !
14  read(*, fmt='(a)', iostat=ios) line
15
16  !
17  ! ios >  0 : 何らかのエラー
18  ! ios == 0 : 正常に読み込まれた
19  ! ios <  0 : ファイルの終端に達した
20  !
21  do while(ios == 0)
22    ! 空白行以外をカウント(line == '' なら空白行である)
23    if(line /= '') then
24      nline = nline + 1
25    endif
26    ! 次の行を読み込む
27    read(*, fmt='(a)', iostat=ios) line
28  enddo

ここでは標準入力からリダイレクトによって

1$ ./a.out < somefile

のように与えられたファイルのうち空白行以外の行数を数える処理となっている.

6.5. 第6章 演習課題

6.5.1. 課題1

サンプルプログラムをコンパイル・実行して動作を確認せよ.さらに,適宜修正してその実行結果を確認せよ.

6.5.2. 課題2

以下のように掛け算九九の表を標準出力にキレイに表示するプログラムを作成せよ.

 1$ ./a.out
 2   1   2   3   4   5   6   7   8   9
 3   2   4   6   8  10  12  14  16  18
 4   3   6   9  12  15  18  21  24  27
 5   4   8  12  16  20  24  28  32  36
 6   5  10  15  20  25  30  35  40  45
 7   6  12  18  24  30  36  42  48  54
 8   7  14  21  28  35  42  49  56  63
 9   8  16  24  32  40  48  56  64  72
10   9  18  27  36  45  54  63  72  81

6.5.3. 課題3

helix1.dat から実数データ \(x_i, y_i, z_i (i=1, \ldots, 32)\) を読み込み,全く同じフォーマットで標準出力に表示するプログラムを作成せよ.以下のようにリダイレクトでファイルに出力し, diff コマンドによってフォーマットが同じかどうかを確かめよ.

1$ ./a.out > test.dat
2$ diff helix1.dat test.dat

上記の diff コマンドを実行して,出力が何も無ければファイルが同一であることを意味する.

データファイルには \(i\) 行目に \(x_i\), \(y_i\), \(z_i\) の3つの実数データが記述されている. open を用いて読み込むこと.

6.5.4. 課題4

課題3と同様に読み込んだデータ \(x_i, y_i, z_i\) をそれぞれ別のファイル(例えば x.dat, y.dat, z.dat)にアスキー形式で出力せよ.各データの書式は helix1.dat のものと同一とする.

このとき以下のコマンドによって結果を確認できる.

1$ paste -d" " x.dat y.dat z.dat > test.dat
2$ diff helix1.dat test.dat

helix1.dattest.dat が同一ファイルになっていれば( diff コマンドが何も出力しなければ)良い.

6.5.5. 課題5

バイナリファイル helix2.datopen を用いて開き,実数データ \(x_i, y_i, z_i (i=1, \ldots, 32)\) を読み込み,先ほどの helix1.dat と全く同じフォーマットで標準出力に表示するプログラムを作成せよ.

作成したプログラムを

1$ ./a.out > test.dat
2$ diff helix1.dat test.dat

のように実行し,結果が helix1.dat と同じになることを diff コマンドによって確かめよ.

なお,このバイナリファイルは form='unformatted'open したファイルに(その装置番号を10として)

1write(10) x
2write(10) y
3write(10) z

のように出力したものである. ただしここで, x, y, z はそれぞれ長さ32の倍精度の実数配列である.すなわち

1real(8) :: x(32), y(32), z(32)

のように宣言されたものであると考えれば良い.

6.5.6. 課題6

Fortranのソースコードから,何らかのFortranの命令文を含む行数(コメントのみの行および空白行を除いた行数)を数えるプログラムを作成せよ.

入力はリダイレクトによって

1$ ./a.out < chap06/sample5.f90
2 Number of lines with valid fortran statement :           24

のようにすれば良い. (チェックが出来ればファイル名は何でもよい.)

なお,コメントのみの行は最初の空白以外の文字が " ! "である行,空白行は空白のみで表される行であるとして判定すれば良い.組込み関数 adjustl を用いると良い.

6.5.7. 課題7

Fortranの通常の unformatted バイナリファイルは一般には他の言語と互換性が無いが, ストリーム入出力 を使うことで他の言語と同様にバイナリファイルを扱うことが出来る.ここではC言語で

 1// 配列サイズ
 2const int N = 10;
 3
 4// 倍精度実数の配列
 5double x[N];
 6
 7// xには1.0から5.5まで0.5刻みでデータを格納
 8
 9// xに格納された倍精度実数をN個分ファイルにバイナリで出力
10fwrite(x, sizeof(double), N, fp);

のように生成した cbinary.dat をFortranから読み込むプログラムを作成せよ. 実行結果は例えば以下のようになる.

 1$ ./a.out
 2data read from binary.dat in stream access
 31.00
 41.50
 52.00
 62.50
 73.00
 83.50
 94.00
104.50
115.00
125.50

なおこのデータを作るのに用いたC言語のコードは mkbin.c である.

6.5.8. 課題8

Fortranの unformatted バイナリファイル helix2.dat をストリーム入出力を用いて読み込み,課題5と同様に出力するプログラムを作成せよ.ここで多くのコンパイラが unformatted の場合には実際のデータの前後に4バイトずつヘッダーとフッター(データのバイト数を表す整数)を付与するので,これらを読み飛ばす必要があることに注意せよ.

これを理解しておけば多言語からもデータの読み書きが可能である.例えば,C言語では helix.c , Pythonでは helix.py が同じ動作をするプログラムになっている.(Pythonの場合は scipy がインストールされていれば scipy.io.FortranFile を使って簡単に読み込むことが出来る.)