从事
SAS
临床试验程序员工作,常常会根据
SAP (
统计分析计划
)
产生大量的
TFL (
表格、图形和列表
)
文件,而它们都是
SAS
输出的
RTF
格式的文档,里面存储着
SAS
生成的统计分析报表。一般来说,一个项目可能会有
100~300
份
RTF
结果文档,显然,逐个打开、浏览、修订它们是费时费力的,通常的做法是将多份
RTF
结果文档合并成一份,然后直接使用合并后的文档。那么,要如何将多份
RTF
文档合并成一份呢?
直接的思路是使用
Microsoft Word
软件的「插入文件」功能,它将允许使用者在光标处一次性插入多份文档,然后将当前文档另存为新的文档即可。
图1 Word 中的插入文件功能
该功能无需任何辅助工具,操作简便,速度较快,但也存在一些缺点:不好定制插入文件的顺序,不好在相邻文件之间插入换行、分页、分节等特殊操作。这些缺点可以通过编写
VBA
程序克服,而笔者给出的做法,是通过
SAS
程序来完成
RTF
文档的合并,并通过特定的步骤、宏变量来增强合并功能。
使用
SAS
程序合并
RTF
文档,共有五个步骤:
获取
RTF
文件列表;
按照文件编号对列表进行排序;
按列表逐个读取
RTF
文件,并将它们的内容输出到数据集;
对内容数据集进行处理;
将内容数据集输出为单份
RTF
文件;
在执行这些业务之前,首先定义三个宏变量以供使用:
1.
获取
RTF
文件列表
要将指定路径下的文件列表写入数据集,可以借助带管道操作符的终端命令语句
: X filename pipe statement
,或者使用文件
I/O
类函数。考虑到一些
SAS
客户端禁用了终端命令,笔者使用的是后者:
注意,若没有文件的读取权限,则后续的程序将运行失败,因此笔者加入了
fopen()
与
fclose()
操作,以确保文件可被读取。
2.
对文件列表排序
接下来,需要对这份文件列表进行排序,以保证后续输出内容时,按照预想的顺序执行。排序是基于文件名本身的,文件格式后缀不包括在内。
在命名一个多层级图表文档时,通常的做法是使用下划线、横杠或点号连接不同层级中的序号,例如存储表
4-2
的文档命名为
"Tab4_2.rtf"
,存储图
3.1.4
的文档命名为
"Fig3.1.4.rtf"
。有时,图表制作人员希望图
2-1
紧随在表
2-3
的后面,还会将存储图形的文档命名为
"Tab2_3_Fig2_1.rtf"
。
因此,对文件名排序首先要将名称中的各层级的编号提取出来,然后再按照层级依次升序排序。
上述程序使用了一个长度为
10
的字符数组,最多可容纳
10
个层级的文件名称,对于日常使用已经足够。而在使用
proc sort
进行排序时,将选项
sortseq=
指定为
linguistic(numeric_collation=on)
将允许对存储在字符型变量中的数值进行排序,若不使用这种做法,
proc sort
完全按照字符的编码顺序对字符型变量进行排序,会得到
"Tab11"
排在
"Tab2"
前面这种不期望的结果。
3.
按列表将
RTF
文件读入数据集
RTF
文件本质上是一种文本文件,由无格式文本、控制字、控制符和群组构成,可以使用文本查看器看到其中的内容。而使用专业的文字处理软件打开——例如
Microsoft Word
——看到的则是排版整齐、布局漂亮的图文,这些格式控制,便是由
RTF
中的控制字和控制符定义的。
图2 示例 RTF 文档内容
图3 示例 RTF 文档在 MS WORD 中的显示内容
在拥有文本文件名称列表的情况下,要将其内容读入 SAS,可以继续使用文件I/O类函数,也可以使用带 filevar= 选项的 infile 语句
¹
。
令
filevar=
选项指向存储文件全路径名称的数据集变量,再配合
input
语句,可依次将文件列表中全部文件的内容依次读入并存储到
SAS
数据集的变量
content
中。这里用到了一个复合循环,通常的复合循环有两个终止条件,但此处只有一个
: "eof
为真
"
,这让
contnum
成为了一个记录文件内容行数的自增变量。即
contnum
从
1
开始自增,每循环一次就增加
1
,直到读取到当前文件的最后一行内容时,自增结束,读取下个文件时
contnum
又从
1
开始自增,这等价于:
4.
处理
RTF
内容
按余阳
²
等人的研究,RTF 可分为文件头和文档区,信息群组<info>是文档区的开头,它存储了文档标题、作者、创建时间等信息。
按Zhiping Yan
³
等人的研究,合并多份 RTF 文件时,应仅保留第一个文件的文件头,最后一个文件的文档区群组闭合右括号,以及所有文件的文档区其它内容。合并规则的示意图如下:
图4 RTF 合并规则示意图
因此,只需要找到每个
RTF
文档内容中的
<info>
群组,以标记文件头和文档区,再按照合并规则删去部分内容就可以了。
上述代码中,有一条
if 0 then set …
语句,乍看上去有些奇怪,条件为假,后面的命令便不会执行,但数据步的运行包含「编译」和「运行」两个部分,当数据步包含
set
语句时,会在「编译」阶段获取
nobs=
、
end=
等数据集选项的值,并存入
PDV
中,而进入「运行」阶段后,
if
语句才开始执行,此时尽管条件为假,
nobs=
选项的值却早就被获取到了。
另外,在 RTF 中要表示换行、分页、分节,应当使用控制字
⁴
: \line、\page、\sect
,这就是程序中trim(content)||"\&split."的含义,即在变量 content 的尾部连接控制字。而当控制字写成 \none 时,这不是合法的 RTF 控制字,RTF 阅读器会自动将其忽略。
5.
输出
RTF
内容
要将数据集中的
RTF
内容重新输出到
RTF
文档,只需要在数据步中使用
file
语句指定输出文件,配合
put
语句就可以将指定变量内容进行输出。
一个值得注意的地方是对输入数据集使用的
keep=
选项,由于工作中生成的
RTF
文件数量很多,到当前步骤时,内容数据集的观测数往往高达数十万条,此时限制读入
PDV
的变量数就能够避免不必要的
I/O
操作,从而加快运行速度。
参考文献
[1]
巫银良
. SAS
技术内幕
[M]. 1.
清华大学出版社
, 2018 :163-164.
[2]
余阳
. RTF
格式文件分析及在多媒体中的应用
[J].
计算机工程
, 1999, 25(4).
[3]
Zhiping Yan. A Fully Automated Approach to Concatenate RTF outputs and Create TOC[EB/OL]. 2015[2023-12-26]. https://lexjansen.com/pharmasug-cn/2015/DV/PharmaSUG-China-2015-DV28.pdf.
[4]Biblioscape 9. Rich Text Format (RTF) Version 1.5 Specification[EB/OL]. [2023-12-26]. https://www.biblioscape.com/rtf15_spec.htm.