用 Python 读写 Fortran 格式化的数据

Fortran 至今仍然是学术界流行的编程语言。得益于其在科学计算方面的出色表现(以及某些历史传承),众多的科研软件都基于 Fortran 编写。同时,很多文件格式也以 Fortran 的格式化输出风格定义。大地测量学领域众多的数据格式都使用 Fortran 格式定义,比如 RINEX、IONEX、SINEX、SP3 等。

Fortran 对输入输出的格式化要求严谨,使用 Python 自己的字符串格式化语法得到的文本很难符合其规范(或者代码可读性很差)。本文介绍在 Python 中使用 Fortran 格式化风格的包:fortranformat,可以使用它对代码进行一些优化。

安装

fortranformat 支持 Python 2 和 Python 3,其代码目前以 MIT 许可证托管于 Bitbucket。你可以直接使用 pip 命令来安装 fortranformat:

1
$ pip install fortranformat

使用

fortranformat 使用起来非常简单,它只定义了两个接口:FortranRecordReaderFortranRecordWriter,分别用于数据的输入和输出。使用特定的 Fortran 格式化语法实例化接口后,就可以进行数据的读写操作。

以 Fortran 格式输入

以 RINEX 2.11 为例,其首行的格式定义为 (F9.2, 11X, 2(A1, 19X), A20),你可以使用 fortranformat 以如下方法读取数据:

1
2
3
4
5
6
>>> import fortranformat as ff
>>> header = (' 2.11 OBSERVATION DATA M (MIXED)'
... ' RINEX VERSION / TYPE') # Raw string in RINEX
>>> hreader = ff.FortranRecordReader('(F9.2, 11X, 2(A1, 19X), A20)')
>>> hreader.read(header)
[2.11, 'O', 'M', 'RINEX VERSION / TYPE']

如代码所示,FortranRecordReader 对象有一个 read 方法,它从你输入的字符串中读取并解析数据,最终返回一个列表。

以 Fortran 格式输出

相比读取 Fortran 格式的数据,以 Fortran 约定的格式化风格输出其实才是难题。但 fortranformat 能够很好的处理这些问题。比如输出类似 -.6789 或 .1234567D+02 这样看上去有些 “奇怪” 数字:

1
2
3
4
5
6
7
8
9
>>> import fortranformat as ff
>>> number_writer = ff.FortranRecordWriter('(F6.4, 1X, F6.4)')
>>> number_writer.write([1.2345, -0.6789])
'1.2345 -.6789'
>>> number_writer = ff.FortranRecordWriter('(D12.7, 1X, F6.4)')
>>> number_writer.write([12.34567, 89])
'.1234567D+02 ******'
>>> number_writer.write([12.34567])
'.1234567D+02'

如代码所示,FortranRecordWriter 对象有一个 write 方法,可以很方便得将数据列表转换为你期望(或 Fortran 期望)的文本。甚至连异常情况下的表现也是相同的。

配置行为

各个 Fortran 编译器的行为可能略有不同,并且,为了表现得更 Pythonic,fortranformat 未完全模仿 Fortran 的行为。但你可以通过进行设置来达到你想要的效果。

RET_UNWRITTEN_VARS_NONE

Fortran 从字符串解析数据时,但数据为空时通常会使用对应数据类型的默认值填充。fortranformat 的默认行为不同,它将空值设置为 None。你可以配置 RET_UNWRITTEN_VARS_NONE 来禁用这一行为。示例:

1
2
3
4
5
6
7
>>> import fortranformat as ff
>>> reader = ff.FortranRecordReader('(I1, I2, F3.1)')
>>> reader.read('1')
[1, None, None]
>>> ff.config.RET_UNWRITTEN_VARS_NONE = False
>>> reader.read('1')
[1, 0, 0.0]

RET_WRITTEN_VARS_ONLY

上文的变量配置的是数据在原始文本中不存在时如何填充的行为。RET_WRITTEN_VARS_ONLY 也跟这种情况有关。它设置的是:仅返回存在的变量的值。示例如下:

1
2
3
4
5
6
7
>>> import fortranformat as ff
>>> reader = ff.FortranRecordReader('(I1, I2, F3.1)')
>>> reader.read('1')
[1, None, None]
>>> ff.config.RET_WRITTEN_VARS_ONLY = True
>>> reader.read('1')
[1]

演示

以读取一段 RINEX 格式的广播星历文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>> import fortranformat as ff
>>> raw_brdc = [
... ' 1 19 4 4 0 0 0.0-0.194287858903D-03-0.795807864051D-11'
... ' 0.000000000000D+00',
... ' 0.780000000000D+02 0.500937500000D+02 0.466305127844D-08'
... ' 0.137803614030D+01',
... ' 0.273436307907D-05 0.858972908463D-02 0.430457293987D-05'
... ' 0.515365416336D+04',
... ' 0.345600000000D+06-0.912696123123D-07-0.197833589171D+01'
... ' 0.201165676117D-06',
... ' 0.974669925177D+00 0.305250000000D+03 0.686056322280D+00'
... '-0.838499225608D-08',
... ' 0.186436338590D-09 0.100000000000D+01 0.204700000000D+04'
... ' 0.000000000000D+00',
... ' 0.200000000000D+01 0.000000000000D+00 0.558793544769D-08'
... ' 0.780000000000D+02',
... ' 0.345600000000D+06 0.000000000000D+00 0.000000000000D+00'
... ' 0.000000000000D+00'] # A block of data in RINEX
>>> epo_fmt = '(I2, 1X, I2.2, 4(1X, I2), F5.1, 3D19.12)' # The 1st line's format
>>> orb_fmt = '(3X, 4D19.12)' # Format of the left lines
>>> epo_reader = ff.FortranRecordReader(epo_fmt)
>>> orb_reader = ff.FortranRecordReader(orb_fmt)
>>> brdc_orb = [tuple(epo_reader.read(raw_brdc[0]))]
>>> for line in raw_brdc[1:]:
... brdc_orb.append(tuple(orb_reader.read(line)))
...
>>> brdc_orb
[(1, 19, 4, 4, 0, 0, 0.0, -0.000194287858903, -7.95807864051e-12, 0.0),
(78.0, 50.09375, 4.66305127844e-09, 1.3780361403),
(2.73436307907e-06, 0.00858972908463, 4.30457293987e-06, 5153.65416336),
(345600.0, -9.12696123123e-08, -1.97833589171, 2.01165676117e-07),
(0.974669925177, 305.25, 0.68605632228, -8.38499225608e-09),
(1.8643633859e-10, 1.0, 2047.0, 0.0),
(2.0, 0.0, 5.58793544769e-09, 78.0),
(345600.0, 0.0, 0.0, 0.0)]

将上面读取的数据 brdc_orb 仍以 RINEX 格式输出:

1
2
3
4
5
6
7
8
>>> epo_writer = ff.FortranRecordWriter(epo_fmt)
>>> orb_writer = ff.FortranRecordWriter(orb_fmt)
>>> output = [epo_writer.write(brdc_orb[0])]
>>> for record in brdc_orb[1:]:
... output.append(orb_writer.write(record))
...
>>> output == raw_brdc
True

可以看出:经 fortranformat 格式化的字符串与原 RINEX 文本数据完全一致。