Office文件格式

储备知识 专栏收录该内容
3 篇文章 0 订阅

1. Ole物件文件

Office档案或是EmbededObject,这些档案都是透过IStorage界面来储存的,一般称为OLE对象文件(也称为Laola檔)。什么是IStorage界面呢?它是Windows所提供的一个OLE界面,主要是提供给OLE对象做为储存数据之用。IStorage之所以好用,主要是它提供类似一个目录/子目录/档案的阶层式组织,统包在一个档案里,如此其它对象便可以在同一个档案里,以目录阶层的方式,储存多种不同的数据。因此要解Office档,首先必须要弄清IStorage所储存的OLE对象文件格式。

为了快速存取类似目录档案的结构,IStorage模仿了类似实际的目录档案结构。它将档案中每512 byte视为一个单位,称为大区块数据(BBD,Big BlockData)。不过说实在的,这些名称真的很容易令人混淆不清(看过MS的文件就会知道,因为还有很多定义的用字都很接近)。因此这边我不沿用MS的名称定义,大家把它想成是一个扇区(sector)就对了,反正IStorage就是在模仿磁盘目录结构,直接使用磁盘的用词反而容易懂。而为了管理这些扇区,当然就要有FAT(档案扇区配置表,MS称它为大区块库,真难懂)。不过档案一词在这边反而会混淆,因此储存在IStorage的“档案”,我便沿用MS的名称,称为资料串(stream)。以下便开始从档头说起:

档头当然就是在档案的开头处,刚好是一个扇区(512 byte)。由于这个档头是固定有的,不能被使用,因此实际的扇区位置,必须从512 byte开始计算起。也就是说Sec#0的位置在512,Sec#1的位置在1024, 以此类推。以下便是文件头的重要数据:

00h (8 byte):文件头标记,一开始的前8个byte固定为D0 CF 11 E0 A1 B1 1A E1,否则便不是OLE对象文件
2Ch (long):FAT使用的扇区数
30h (long):档案目录结构属性开始的扇区
3Ch (long):小数据储存区FAT开始的扇区
44h:额外记录FAT使用扇区的开始扇区
48h:额外记录FAT使用扇区的扇区数
4Ch开始:FAT使用的扇区(long),数量由2Ch中的扇区数决定

这边注意到有一个小数据储存区。由于每个扇区都是512 byte,拿来放小数据的话,会非常浪费空间,因此IStorage便将较小的数据,统一另行储存。各位可以将小资料储存区想成是另一个档案,这个档案又是仿真目录档案结构,只是每个扇区缩小到64 byte而已。这个部份我待会再谈,先将基本512 byte扇区的仿真方式弄懂,小数据储存区的格式便更容易懂了。

由于在IStorage中,所有数据在扇区的储存次序,都和FAT有关,因此必须先弄清FAT的配置方式。从档头中,我们可以知道FAT使用了那些扇区,将这些扇区的数据组合在一起,便是真正的FAT数据(扇区可能跳来跳去的)。而在FAT数据里,其实便是记录着每个扇区的下一个扇区是什么(每笔数据均为long)。 这边的扇区值可能为:

0xfffffffd (-3):特殊区块(FAT使用的扇区便是)
0xfffffffe (-2):结束标记(表示已无下个数据扇区)
0xffffffff (-1):尚未被使用的扇区
其它:下个数据所在的扇区

因此如果知道一个数据串从那个扇区开始,便可以直接参照FAT,看看数据所在的下个扇区是在那里。例如Sec#10,便查看FAT中第10个(由0编起)的long值扇区编号,便是下一个。如此一路查下去,便可以得到整个数据串所使用的扇区数和次序。应该很简单吧?

这里有一个情况必须特别处理的,那就是超大型的OLE檔。由于FAT表使用的扇区是放在Ole文件头4Ch开始的地方,但因为档头只有512 byte而已,因此只能记录109个扇区(约为7MB左右)。如果Ole文件更大,使得FAT使用的扇区表记录空间不够使用时,便必须读取44h所指的扇区,视为下一个记录FAT使用扇区的扇区。只是如果还是不够储存时,再下一个扇区在那里呢?其实它是记录在该扇区内容的最后一个值,以串行形式组成(-1表示结束)。因此在读取整个FAT表时,必须考虑到此一情形。

接下来我们来看看档案目录结构属性的数据(起始扇区记录在文件头)。这个部份也是一个数据串,因此算出来的位置,都是相对于数据串,你必须换算成是第几个扇区,然后再从FAT里得到实际所在的扇区。例如所要的数据是在第540 byte处,那么由数据串开头算起是Sec#1,如果在FAT表里查到这个数据串的扇区编号为{9,13,22,41},实际所在的扇区便是Sec#13(offset也要重算)。不懂的话请再想想弄清楚。

档案目录结构属性的数据,每个占128 byte,并以指标加以串连。以下便是每个结构属性的值(第一个结构属性就是根目录):

00h:共64 byte,记录数据串名称(unicode),根目录一律为"Root Entry"(把它想成是文件名或目录名就对了)
40h (short):数据串名称的byte长度,0表这个属性没有用到(deleted)
42h (char):本结构属性的形态,1=目录,2=档案,5=根目录
44h (long):上一个结构属性指针,-1表没有
48h (long):下一个结构属性指针,-1表没有
4Ch (long):若本结构属性的形态为目录或根目录,则指向本目录里各子目录/档案的第一个结构属性(指针)
74h (long):数据所在的起始扇区
78h (long):资料的byte数

大家可以看到,这个结构其实和磁盘的档案目录结构没什么两样,同样也是树状阶层式的。其中结构属性指针指的是第几个结构属性值(由0编起),相对于数据串起点的位置,便是指针值*128。

有一点必须特别注意的是,上一个/下一个结构属性指针并非双向链接,而是随意键结,例如:

attr#3:上一个是#2,下一个是#4
attr#2:上一个是#5,没有下一个
attr#5:没有上一个,没有下一个
attr#4:没有上一个,没有下一个

因此共计有2,3,4,5等4个结构属性。也就是说,你必须将所有键结的结构属性都展开到,才能得知整层的档案目录数据。

另外,数据所在的起始扇区,可能指向标准的512 byte扇区,也可能指向小数据储存区里的64 byte扇区,其分别在于数据的byte数。若byte数>=4096,便是储存在512 byte扇区,否则便是储存在64 byte扇区。 唯一例外的是根目录,一律储存在512 byte扇区中。

至于数据串的内容,除了根目录RootEntry外,其它都是使用者自己订的。因此每个数据串里的数据如何安排,表示什么意思,都必须另行处理,这点无关IStorage的事。IStorage只是尽责地,将使用者要储存的数据,依照上面的格式储存下来而已。

接下来开始说明小数据储存区的部份。在文件头3Ch的地方记录了小数据储存区FAT开始的扇区。小数据储存区FAT也是一个数据串,必须将整个数据串读入后才能处理。这个小数据储存区FAT里的数据格式,和之前提到FAT数据格式都是完全一模一样,以串行的方式来记录各数据串使用的扇区。但小数据扇区里的数据实际上在那里呢?其实就是根目录里的数据串。这也就是为什么根目录的数据,一律都是放在标准512 byte扇区里。而在根目录数据串里,便是以64 byte为一单位,切割成小扇区,供小数据存放使用。于是,要取得一个资料串的过程,便成为:

(1) 在档案目录结构里,找出该数据串相同名称的属性。属性里的数据扇区指针和大小,便是数据所在的位置。
(2) 如果数据串的数据,是在标准512byte扇区中,便到FAT表里找出该数据串使用的扇区,一个一个依次加载。注意OLE档可能不是刚好512 byte,如果是最后一个sector,必须只读取档案长度剩余的部份。
(3) 如果数据串的数据,是在小数据储存区中,便必须加载根目录的整个数据串,然后到小数据储存区FAT表里,找出该数据串使用的小数据扇区,再一个一个从根目录数据串的小扇区里加载。

其实整个OLE对象文件的结构应该算是很简单。各位有空可以去看一下类似的文件(http://user.cs.tu-berlin.de/~schwartz/pmh/guide.html),即使你都已经弄懂了OLE对象文件,还是可能看不懂这些文件。

2. Office档的摘要内容

Office档的摘要主要有两个部份,分别放在"\005DocumentSummaryInformation"和"\005SummaryInformation"这两个资料串中。以下便分别加以说明(请配合MS Word的摘要设定操作来看比较容易懂):

(1) "\005DocumentSummaryInformation"资料串

18h (long):GUID数目
1Ch开始每20 byte:依次存放{GUID+属性组数据位置},前者为16 byte,后者为long

在这个数据串里,可以记录两种属性组,一个是DocumentSummaryInformation,一个是UserDefinedProperties。以下便是这两个属性组的GUID:

DocumentSummaryInformation:
0x02,0xD5,0xCD,0xD5,0x9C,0x2E,0x1B,0x10,0x93,0x97,0x08,0x00,0x2B,0x2C,0xF9,0xAE
UserDefinedProperties:
0x05,0xD5,0xCD,0xD5,0x9C,0x2E,0x1B,0x10,0x93,0x97,0x08,0x00,0x2B,0x2C,0xF9,0xAE

使用者自订的摘要部份待会再说,底下便先针对标准的摘要部份,也就是DocumentSummaryInformation。从上述的GUID比对到后,后面便是指向这个属性组(Property Set)的数据区块。属性组数据区块的格式如下(这边的位置都是相对于数据区块,而非整个数据串):

00h (long):资料区块大小
04h (long):属性数目
08h开始每8 byte:属性编号(long)+属性数据位置

标准的摘要属性编号(PropertyID)是固定的,以下便是各编号的意义(大部份配合MS Word摘要设定就能懂了):

01: CodePage, long - 属性组文字数据使用的编码方式(固定的ProperSet ID),这个部份我最后再来说明
02: Category, 字符串 - 类别
03: PresentationTarget, 字符串 - 展示方式(打印机/屏幕),PowerPoint在用的
04: Bytes, long - 文件byte数
05: Lines, long - 文件行数
06: Paragraphs, long - 文件段落数
07: Slides, long - 文件Slides数,PowerPoint在用的
08: Notes, long - 有注记的页数,PowerPoint在用的
09: HiddenSlides, long - 隐藏的Slides数,PowerPoint在用的
0A: MMClips, long - 声音/影片数,PowerPoint在用的
0B: ScaleCrop, bool - 是否需要缩放Thumbnail,FindFile在用的
0C: HeadingPairs,variant/vector - Office内部在用的,不管它
0D: TitlesofParts, 字符串/vector - 所有的文件名称(如Excel的Sheet名称,PowerPoint的Slide标题等等)
0E: Manager, 字符串 - 主管
0F: Company, 字符串 - 公司
10: LinksUpToDate, bool - Office内部在用的,不管它

至于属性的数据,其格式为:

属性数据形态(long)+实际属性数据

由于我们的目的是要建索引,因此只需取出文字的属性数据即可。文字属性的数据形态为0x1E,实际的属性数据则为:

字符串数据byte数+字符串数据

如果要解使用者自订的属性组,只需找出所有的属性数据,只要是文字属性的,都加以读出即可。

(2) "\005SummaryInformation"资料串

其格式和"\005DocumentSummaryInformation"完全一模一样,差别只有属性编号的意义。以下我只挑出重要的列一下:

GUID:0xE0,0x85,0x9F,0xF2,0xF9,0x4F,0x68,0x10,0xAB,0x91,0x08,0x00,0x2B,0x27,0xB3,0xD9

02: Title, 字符串 - 标题
03: Subject, 字符串 - 主旨
04: Author, 字符串 - 作者
05: Keyword, 字符串 - 关键词
06: Commenct, 字符串 - 批注

其中若标题没有在本数据串时,应该到前述DocumentSummaryInformation信息里的TitlesofParts中取得。

关于编码方式(CodePage)的重要值域:

932: 日文
936: 简体中文
949: 韩文
950: 繁体中文
1200: Unicode
1252: 英文

如果取得的属性值料是字符串的话,便必须依照指定的编码方式进行转码的动作。

3. Word 97的格式

Word档案的数据,主要是记录在"WordDocument"数据串与"0Table"/"1Table"数据串中,由于文字数据主要是记录在"WordDocument"数据串中,因此我们先从此一部份着手。至于Embeded Object是另行记录的, 这我们最后再来说明。

"WordDocument"数据串一开头的地方,称为FIB(FileInformation Block),里面记录了各种重要的信息与指针。因此要解出文字数据,首先必须弄懂FIB。以下便列出FIB比较重要的部份:

0002h (short):版本
000Ah (short):状态旗标(以bit0为最低位)
bit 2 (mask=0x0004):是否为复杂格式
bit 8 (mask=0x0100):档案是否加密
bit 9 (mask=x00200):0表使用"0Table"数据串, 1表"1Table"资料串
bit 14 (mask=0x4000):是否为远东版
000Eh (short):加密键值
0018h (long):文字数据起始位置(非复杂格式时)
001Ch (long):文字数据结束位置+1(非复杂格式时)
0020h (short):后面的短整数参数数目
0022h开始:短整数参数,比较重要的是第13个(由0编起,003Ch),若为远东版,则这个参数记录了语系ID(后述)
????h (short):后面的长整数参数数目
????h开始:长整数参数
#0:数据串长度
#1:建立日期
#2:修改日期
#3:文件(document)文字长度
#4:脚注(footnote)文字长度
#5:页眉(header)文字长度
#6:宏(macro)文字长度
#7:annotation文字长度
#8:endnote文字长度
#9:文字块文字长度
#10:页眉文字块文字长度
????h (short):FC/LCB数目
????h (long/long):FC/LCB资料
#33:piece table位置/大小

其中处理起来比较麻烦的是复杂格式,这是当使用者使用快速存盘(Quick Save)时,才会形成的格式,这部份后面再说明。至于加密的文件,目前不予以处理(要自行对Word档解密,会花太多时间,同时加密文件无法建索引,应该是很正常的)。

关于语系ID的值域,bit 0-9 = 主语系,bit 10-15 = 次语系,相关资料可以参考MSDN,以下是一些辨识方法:

语系ID = 0x0404表繁体中文,0x0804表简体中文
主语系 = 0x09表英文,0x11表日文,0x12表韩文

这些语系资料可以提供,当Word文件里面记录的不是Unicode时,应该如何转码。这样即使外界设错内码格式,我们仍能正确转成Unicode处理。以下便开始说明非复杂非加密格式的Word文件如何抽出文字数据。

Word的文字数据,分成document,footnote,header...等好几个部份(参见FIB里的长整数参数),这些文字数据都是相连接在一起储存的。储存的起终位置便记录在FIB的18h,1Ch里。然而我们却无法直接到里面取出文字数据,因为这些文字数据是以512byte为一单位放在一起,而且语系并不一定相同(可能是ASCII,也可能是Unicode,这样做的目的当然是要档案小一些)。因此我们必须藉助piecetable的信息来取得真正的文字数据。至于如何取得,待会再来说明。因为较早期的word档并没有piecetable,这种情况下表示文字数据是以同一种语系储存的,如此取得文字数据的方式就很简单,只需到文字数据起始位置开始,依照FIB长整数参数里记录的各部份文字长度,一个一个加以读出处理便可以了。不过在处理前必须先辨别储存的文字是ASCII形式,还是Unicode形式。方法就是:

文字byte数=文字数据结束位置-文字数据开始位置
文字总字数=文件文字长度+脚注文本长度+页眉文字长度+....

如果文字总字数*2=文字byte数(unicode每个字是2 byte),便是unicode形式。不过由于Word有时在文字数据最后面会多加一个段落标记(总字数少了),因此判断时要以"文字总字数*2<=文字byte数"为准。另外,取出的Word文字数据里也有一些控制字符必须特别加以处理,这些字符包括(以ASCII字码10进位列出):

07:cell mark
09:tab
11:break line
12:pagebreak/section mark
13:paragraph end
14:clumn break
19:field begin
20:fieldseperator
21:field end
30:non-breakinghyphen
31:non-requiredhyphen
160:non-breakingspace

如果字符串是ASCII形式,则还会有以下的一些特殊字符:

85h:...
92h:'
93h:"
96h:--
97h:---

其中比较需要注意的是fieldstart(19)/field seperator(20)/field end(21)等三个字符。这些字符是用来夹住word的一些特殊标记文字,例如hyperlink的相关信息,以及"目录"等由word自行做出的结构,其中field start到field seperator之间的字符串是控制用的(不显示),field seperator到field end之间的字符串则是显示用的,故前半部的资料应泸除,后半部的数据要取出。如果没有特别处理的话,便会出现一堆如"HYPERLINK \l "TOC1899651""等无意义字符串。

如果有piece table时,文字数据便不能像前面所说的,直接判断并加以读取,必须经由piecetable的信息加以判断。piecetable信息是记录在Table数据串,至于是使用"0Table"数据串,还是"1Table"数据串,可由FIB里的信息得知。取得Table数据串后,piece table的信息为:

(byte) 1
(short) grpprl大小
grpprl
(byte) 1
(short) grpprl大小
grpprl
...
(byte) 2
(long) plcfpcd大小
plcfpcd

我们要的是plcfpcd的信息,因此必须将grpprl全部略过。plcfpcd主要由PLC和PCD两个数组所组成,因此必须先算出元素数目:(plcfpcd大小-4)/12。其中PLC元素数目要再加一。PLC数组的主要目的,是用来记录累积的文字数,因此第i个文字piece的文字字数,便是PLC[i+1]-PLC[i],这也就是为何PLC元素要多一个的原因。PCD数组,主要用来取得文字piece的位置,其元素格式如下:

(short) 状态值
(long) 文字piece的位置,最高的第二个位(0x40000000)若为1,表示文字是ASCII,否则为Unicode
(short) 记录PRM或grpprl的索引值(这部份无关重要,可以不管)

因此要解出所有文字信息,只须依次取得各文字piece的位置/字数,并决定为何种语系,再加以读出处理即可。不过要注意的是,文字piece的位置,当形式为ASCII时,其位置会x2,因此换算成实际位置时,必须再除以2。

至于复杂格式是什么呢?其实就是文字数据并没有集中放在一起,而是随着编辑过程而分散在各处。要取得这些文字数据,其实只须依照piece table里面的信息来取便可以了。

4. Word 95的格式

Word 95的档案格式,基本上和Word97差不多,然而由于该时期的版本并未支持Unicode,因此档案中文字的编码并非Unicode,而是以一种很怪、类似于Unicode的方式储存。也就是说,中/英文都是2 byte,但中文记录的不是Unicode,而是它的两个ASCII字码。例如"一"的BIG5码是A440h,它便将A440h视为一个2 byte字码储存起来,因此先存40h,再存A4h。于是文字数据读取以后,还是必须进行转码的动作,才能得到实际的Unicode。

5. Word更早期版本的格式

Word更早期版本的档案格式,本身并非OLE对象文件。事实上,该档案的内容便是OLE里的WordDocument数据串。也就是说,当IStorage界面制订出来以后,word便将整个档案视为数据串,存到OLE对象文件里。因此要解这种早期版本,只需直接将它视为WordDocument取出的数据串,然后一样到18h的地方读取文字位置和长度,即可解出文字数据。不过这种早期版本的word档,还没有Unicode的观念,因此存的全部都是ASCII码。

6. RTF檔格式

.doc的档案,不只是Word档格式而已,还可能是RTF文件或是纯文字文件,因此在处理前必须先行判断。以下便针对RTF档的格式进行说明。在说明RTF的格式之前,我们先看一下RTF的一个简单范例:

{\rtf1\ansi\ansicpg950 {\fonttbl ...} ...}

RTF档的内容,主要由三个部份所组成,一是命令,也就是以\开头的字;一是群组,也就是{}括起来的部份;最后一个当然就是数据。RTF命令的格式如下:

\<keyword><number><delimitor>

keyword必须都是英文字母(RTF文件是大小写有关的),或是单一特殊字符。number可有可无,当有的时候,便做为命令的参数。这个数字可能是负的(以‘-’做开头)‘而且RTF里的数字一律为2 byte的短整数。delimitor可以是空白,或者任何一个非英文字母的字符,若是空白便必须将它吃掉,不视为数据处理。

RTF的命令,主要可分为下列三种:

(1) 数据属性定义命令:用来定义数据的相关属性,例如语系、字型、版面等等
(2) 数据意义命令:用来说明数据实际的意义,例如内文、脚注、页眉、字型表等等
(3) 特殊字符命令:用来输入一些特殊字符

除了上述三种命令之外,还有一个特别的命令\*,这个命令是表示如果后面紧接着的命令不懂的话,可以将其后的数据全数略过。这个命令主要是提供给应用程序,以便植入一些自己定义的命令与数据。因此遇到\*命令时,必须再读取下个命令,才能决定是要处理,还是要全数略过舍弃。如果命令是在一个群组之中(即在{}之内),则该命令只作用在该群组里其后的数据(包括下层群组),当离开群组后,数据的属性必须回复到外层群组的属性。以下便开始说明这三种RTF的命令(只列出与取文字有关的):

(1) 数据属性定义命令

\rtf:RTF的文件头标记,后面的数字为RTF的版本(目前都是1)
\ansi:使用ANSI字集
\mac:使用AppleMacintosh字集(目前不支持,视为错误)
\pc:使用IBM PC codepage 437字集(目前不支持,视为错误)
\pca:使用IBM PC codepage 850字集(目前不支持,视为错误)
\ansicpg:使用的语系(实际语系需视字型语系而定),后面的数字可为:
932 日文
936 简体中文
949 韩文
950 繁体中文
\langfenp:使用的字型语系,1028= 繁体中文,2052 = 简体中文
\ud:数据采用Unicode编码(\u命令)
\upr:后面接两个群组,第一个群组是ANSI编码,第二个群组是Unicode编码,可任挑一个做为数据处理(这是为了与无法处理Unicode的RTF Reader兼容所设,若能处理Unicode,应以Unicode为准)

(2) 数据意义命令

\info:摘要信息
\title:标题
\author:作者
\subject:主旨
\manager:主管
\company:公司
\doccomm:文件批注
\comment:批注(无作用,可省略的批注)
\category:类别
\keywords:关键词
\userprops:使用者自订属性(Word自己的定义)
\propname:使用者自订属性的属性名称(Word自己的定义)
\staticval:使用者自订属性的属性值(Word自己的定义)
\header:页眉
\footer:页脚
\footnote:脚注
\fonttbl:字型表
\colortbl:颜色表
\stylesheet:样式表
\pict:图片
\shptxt:方块文字数据(Word自己的定义)
\shpinst:方块文字开始(Word自己的定义)

未遇到上述命令前的数据,一律为内文。
(3) 特殊字符命令

\{:'{'字符
\}:'}'字符
\-:'-'字符
\_:'-'字符
\~:空格符
\':16进位字符,后面接两个16进位数字,例如\'A4表示A4h字符
\u:Unicode字符,后面的数字即为Unicode字码,如果命令结束后紧接着一个'?',要将之吃掉
\emspace:空格符
\enspace:空格符
\qmspace:空格符
\emdash:'-'字符
\endash:'-'字符
\lquote:单引号
\rquote:单引号
\ldblquote:双引号
\rdblquote:双引号
\tab:TAB字符
\trowd:表格开始
\row:表格行结束
\nestrow:表格行结束
\cell:表格栏结束
\nestcell:表格栏结束
\column:column break
\page:page break
\line:line break
\par:段落结束
\sect:段落/区段结束

因此要抽取RTF文字数据,只需一层一层群组地剖析下去,当遇到数据时,看看数据是什么意义,是什么样属性,即可抽取出来。但注意在处理数据时,若是遇到换行字符(ASCII 13,ASCII 10),必须将之略去不处理。

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值