本站曾发布文章:《Fortran 二进制文件读写》,介绍了以直接读取法进行二进制文件读写的方法。阅读本文前,如有必要,您可先阅读此文,以便了解二进制文件的基本概念。
直接读取(Access = 'Direct' )时,需指定记录长度(RecL)。
而不少二进制文件,其结构不规则,间或有 2 字节、4 字节 或 8 字节、甚至其他长度的数据,例如 SEGY 记录、Surfer 的 grid 网格化文件,此时使用直接读取会显得非常笨拙,常常需要多次 Open 同一个文件,使用不同的 RecL,以便读写特定的数据。
网络上曾有不少文章,介绍可以自由读写任意字节或位置的 fortran 代码,但通常使用扩展的语法实现。或者通过调用操作系统的 API 实现,不规范、只适合一部分编译器。
二. 流文件的基本用法
流文件(access="stream")是 Fortran2003 新增的一种读写方式,目前主流的尚在更新的编译器均支持。(包括但不限于 IVF、GFortran、PGI、NAG、Lahey、Ftn95、Absoft ,不含 CVF 和 PowerStation )
它是一种读写方式,而不是文件本身的格式。它并不把二进制文件视为一个一个的“记录”,而是视为一个整体。因此,无需指定记录长度(RecL)。
文件打开后,操作位置就在文件的第一个字节上,每次读取多少字节,就自动向后移动多少字节。下一次读写紧接着此处。
同时,流文件也提供任意位置(字节数),以及查询当前位置的方法。
1. 打开流文件
一个典型的流文件读写二进制,可使用以下语句:
Open( 12 , File = "fcode_test.bin" , access="stream" , form = "unformatted" )
access 指定打开方式为流文件,form 指定文件为无格式文件(二进制文件)
2.读写
读写时也无需指定记录(rec),典型的读取语句:
Read( 12 ) 变量1 , 变量2 , 变量3...... 变量 N
此时,在当前操作位置读写。读写后,操作位置自动向后移动 M 个字节(M等于所有读写的变量占有的字节数)
注意:无格式读写,不能指定格式,也不能写 *
如果想在其他位置读写,可使用这样的语句:
Read( 12 , pos = i ) 变量1 , 变量2 , 变量3...... 变量 N
此时,在第 i 个字节处开始读取。读写后,操作位置相对 i 向后移动 M 个字节(M 同上)
3.查询当前操作位置
经过若干 read 语句或 write 语句后,可能就不知道当前读取位置在哪儿了。此时,可通过 Inquire 查询:
Inquire( 12 , Pos = i )
以上语句执行后,i 的值会变为 12 号文件当前的操作位置(字节数)
4.设置当前操作位置
我们经常要跳过若干字节,或回退若干字节进行读写。这可以通过查询操作位置后,加减一定数量,再设置为当前操作位置。例如:
Inquire( 12 , Pos = i )
Read( 12 , Pos= i + 32 , iostat = iErr )
用 Read 语句来设置当前位置为 i + 32(即向后跳过 32 字节),而 Read 后面没有任何变量。
iostat = iErr 可以防止超过文件大小而出错。
5.关闭流文件
使用 Close(12) 关闭,与常规文件没有区别。
可以看出,流文件方式读写,比直接读写更简单,更容易理解。
三. 读取 Surfer 网格化 grd 文件的范例
Surfer 是 Goldensoft 公司出品的一款绘图软件,以等值线绘制为特长,广泛应用在地质、水文等领域。其 grd 文件为(二维)网格化文件。
这种文件有两个版本。6.0 的单精度版本、7.0以上的双精度版本。在这里,我们读取 6.0 的单精度版本,因为它比 7.0 更不规则,更能体现流文件的强大与方便。
以下是它的格式说明
可以看出,里面有 4 字节的 char(标识),2 字节的 short(网格大小),8 字节的 double(网格边界:XYZ的最小值最大值),以及 4 字节的 float(网格矩阵)
如果使用直接读写,需要分别考虑读写不同长度数据的记录长度 RecL,多次打开文件。而使用流文件方式,就非常的方便了。
Program www_fcode_cn Use, Intrinsic :: ISO_C_BINDING !// 使用内部模块,保持与 C 语言变量类型一致 Implicit None character(len=4) :: flag !// 4字节的标识,必须为 “DSBB” Integer(kind=C_SHORT) :: m , n !// 网格大小 Real(Kind=C_DOUBLE) :: rXMin , rXMax !// X Y Z 的边界 Real(Kind=C_DOUBLE) :: rYMin , rYMax Real(Kind=C_DOUBLE) :: rZMin , rZMax Real , allocatable :: d(:,:) !// 网格矩阵 integer :: i Open( 12 , File = "fcode_test.grd" , access="stream" , form = "unformatted" ) Read( 12 ) flag Write( * , * ) 'flag=' , flag Read( 12 ) m , n Write( * , * ) 'M/N=' , m , n allocate( d( m , n ) ) Read( 12 ) rXMin , rXMax , rYMin , rYMax , rZMin , rZMax write( * , * ) 'X_Min=' , rXMin , 'X_Max=' , rXMax write( * , * ) 'Y_Min=' , rYMin , 'Y_Max=' , rYMax write( * , * ) 'Z_Min=' , rZMin , 'Z_Max=' , rZMax Inquire( 12 , Pos = i ) !// 查询当前位置 write( * , '(a,1x,i4,1x,a)' ) '头部信息一共',i-1,'字节' !//当前操作位置 i 字节 Read( 12 ) d(:,:) Do i = 1 , n write( * , '(999f6.3)' ) d(:,i) End Do Close( 12 ) End Program www_fcode_cn
以上代码有注释,相信很容易理解。您可以点这里下载示范数据:focde_test.grd
如果您对派生类型(type)有一定了解,您还可以更方便的一次读取整个头部信息。
Program www_fcode_cn Use, Intrinsic :: ISO_C_BINDING !// 使用内部模块,保持与 C 语言变量类型一致 Implicit None Type , Bind(C) :: GRAD_HEAD character(len=4) :: flag !// 4字节的标识,必须为 “DSBB” Integer(kind=C_SHORT) :: m , n !// 网格大小 Real(Kind=C_DOUBLE) :: rXMin , rXMax !// X Y Z 的边界 Real(Kind=C_DOUBLE) :: rYMin , rYMax Real(Kind=C_DOUBLE) :: rZMin , rZMax End Type GRAD_HEAD Type( GRAD_HEAD ) :: Head Real , allocatable :: d(:,:) !// 网格矩阵 integer :: i Open( 12 , File = "fcode_test.grd" , access="stream" , form = "unformatted" ) Read( 12 ) HEAD allocate( d( HEAD%m , HEAD%n ) ) Write( * , * ) 'flag=' , HEAD%flag Write( * , * ) 'M/N=' , HEAD%m , HEAD%n write( * , * ) 'X_Min=' , HEAD%rXMin , 'X_Max=' , HEAD%rXMax write( * , * ) 'Y_Min=' , HEAD%rYMin , 'Y_Max=' , HEAD%rYMax write( * , * ) 'Z_Min=' , HEAD%rZMin , 'Z_Max=' , HEAD%rZMax Inquire( 12 , Pos = i ) !// 查询当前位置 write( * , '(a,1x,i4,1x,a)' ) '头部信息一共',i-1,'字节' !//当前操作位置 i 字节 Read( 12 ) d(:,:) Do i = 1 , HEAD%n write( * , '(*(f6.3))' ) d(:,i) End Do Close( 12 ) End Program www_fcode_cn输出结果应为: