)
【一、.NET下读取Office文件的方式】10年的时候参加比赛要做一个文件检索的系统要包含Word、PowerPoint等文件格式的全文检索。由于之前用过.NET并且考虑到这些是微软的格式可能使用.NET读取会更容易些但没想到.NET这边查到的资料只有Interop的方式读取Office文件。后来接触了Java的POI发现.NET也有移植的NPOI但是只移植了核心的Excel读写并没有Word、PowerPoint等文件的读写所以最后没有办法只能硬着头皮自己去做Word和PowerPoint文件的解析。那么Interop是什么Interop的全称是“Interoperability”即微软希望托管的.NET能与非托管的COM进行互相调用的一种方式。通过Interop读写Office即调用安装在计算机上的Office软件来实现Office的读写其优点显而易见文件还是由Office生成或读取的所以与自己打开Office是没有任何区别的但缺点也非常明显即运行程序的计算机上必须安装有对应版本的Office软件同时操作Office文件时实际上是打开了对应的Office组件所以运行效率低、耗内存大并且还可能产生内存泄露的问题。关于Interop方式读写Office文件的例子网上有很多有兴趣的可以自行查阅这里就不再多讲了。那么有没有方式不借助Office软件实现Office文件的读写呢答案肯定是肯定的就像Java中的POI及.NET中的NPOI实现的那样即通过程序自己读写文件来实现Office文件的读写。不过由于Office文件结构非常复杂这里只提供文件摘要信息和文件文本内容的解析。不过即使如此对于全文检索什么的还是足够的。【二、Windows复合二进制文件以及Header】前几年微软开放了一些私有格式的规范使得所有人都可以对其文件进行解析而不需要支付任何费用这也使得我们编写解析文件的程序成为可能相关链接在文章最后可以找到。对于一个Microsoft Office文件其实质是一个Windows复合二进制文件Windows Compound Binary File文件的头Header是固定的512字节Header记录文件最重要的参数。Header之后可以分为不同的SectorSector的种类有FAT、Mini-FAT属于Mini-Sector、Directory、DIF、Stroage等五种。为了方便称呼我们规定每个Sector都有一个SectorIDHeader后的Sector为第一个Sector其SectorID为0。我们先来说Header一个Header的部分截图及包含的信息如下比较重要的用粗体表示。Header的前8字节Byte[]也就是整个文件的前8字节都是固定的0xD0 0xCF 0x11 0xE0 0xA1 0xB1 0x1A 0xE1如果不是则说明不是复合文件。从008H到017H的16字节是Class Id不过很多文件都置的0。从018H到019H的2字节UInt16是文件格式的次要版本。从01AH到01BH的2字节UInt16是文件格式的主要版本。从01CH到01DH的2字节UInt16是固定为0xFE 0xFF表示文档使用的是Little Endian低位在前高位在后。从01EH到01FH的2字节UInt16是Sector大小的幂默认为90x09 0x00即每个Sector为512字节。从020H到021H的2字节UInt16是Mini-Sector大小的幂默认为60x06 0x00即每个Mini-Sector为64字节。从022H到023H的2字节UInt16是预留的必须置0。从024H到027H的4字节UInt32是预留的必须置0。从028H到02BH的4字节UInt32是预留的必须置0。从02CH到02FH的4字节UInt32是FAT的数量。从030H到033H的4字节UInt32是Directory开始的SectorID。从034H到037H的4字节UInt32是用于事务的必须置0。从038H到03BH的4字节UInt32是最小串Stream的最大大小默认为40960x00 0x10 0x00 0x10。从03CH到03FH的4字节UInt32是MiniFAT表开始的SectorID。从040H到043H的4字节UInt32是MiniFAT表的数量。从044H到047H的4字节UInt32是DIFAT开始的SectorID。从048H到04BH的4字节UInt32是DIFAT的数量。从04CH到1FFH的436字节UInt32[]是前109块FAT表的SectorID。那么我们可以写如下的代码将Header中重要的内容解析出来。View Code说个比较有意思的.NET中的BinaryReader有很多读取的方法比如ReadUInt16、ReadInt32之类的只有ReadUInt16的Summary写着“使用 Little-Endian 编码...”见下图其实不仅仅是ReadUInt16所有ReadIntX、ReadUIntX、ReadSingle、ReadDouble都是使用Little-Endian编码方式从流中读的大家可以放心使用而不需要一个字节一个字节的读再反转数组我在10年的时候就走过弯路。解释在MSDN各个方法中的备注里http://msdn.microsoft.com/zh-cn/library/vstudio/system.io.binaryreader_methods.aspx【三、我们从Directory开始】复合文档中其实存放着很多内容这么多内容需要有个目录那么Directory就是这个目录。从Header中我们可以读取出Directory开始的SectorID我们可以Seek到这个位置0x200 sectorSize * dirStartSectorID。Directory中每个DirectoryEntry固定为128字节其主要结构如下从000H到040H的64字节是存储DirectoryEntry名称的并且是以Unicode存储的即每个字符占2个字节其实可以看做是UInt16。从041H到042H的2字节UInt16是DirectoryEntry名称的长度包括最后的“\0”。从042H到042H的1字节Byte是DirectoryEntry的类型。主要的有1为目录2为节点5为根节点从044H到047H的4字节UInt32是该DirectoryEntry左兄弟的EntryID第一个DirectoryEntry的EntryID为0下同。从048H到04BH的4字节UInt32是该DirectoryEntry右兄弟的EntryID。从04CH到04FH的4字节UInt32是该DirectoryEntry一个孩子的EntryID。从074H到077H的4字节UInt32是该DirectoryEntry开始的SectorID。从078H到07BH的4字节UInt32是该DirectoryEntry存储的所有字节长度。显然Directory其实是一个树形的结构我们只要从第一个EntryRoot Entry开始递归搜索就可以了。为了方便开发我们创建一个DirectoryEntry的类View Code然后我们递归搜索就可以了View Code【四、DocumentSummaryInformation和SummaryInformation】Office文档包含很多摘要信息比如标题、作者、编辑时间等等如下图。摘要信息又分为两类一类是DocumentSummaryInformation另一类是SummaryInformation分别包含不同种类的摘要信息。通过上述的代码应该能获取到Root Entry下有一个叫“\005DocumentSummaryInformation”的Entry和一个叫“\005SummaryInformation”的Entry。对于DocumentSummaryInformation其结构如下从018H到01BH的4字节UInt32是存储属性组的个数。从01CH开始的每20字节是属性组的信息对于前16字节Byte[]如果是0x02 0xD5 0xCD 0xD5 0x9C 0x2E 0x1B 0x10 0x93 0x97 0x08 0x00 0x2B 0x2C 0xF9 0xAE则表示是DocumentSummaryInformation如果是0x05 0xD5 0xCD 0xD5 0x9C 0x2E 0x1B 0x10 0x93 0x97 0x08 0x00 0x2B 0x2C 0xF9 0xAE则表示是UserDefinedProperties。对于后4字节UInt32则是该属性组相对于Entry的偏移。对于每个属性组其结构如下从000H到003H的4字节UInt32是属性组大小。从004H到007H的4字节UInt32是属性组中属性的个数。从008H开始的每8字节是属性的信息对于前4字节UInt32是属性编号表示属性的种类。对于后4字节UInt32是属性内容相对于属性组的偏移。常见的属性编号有以下这些View Code对于每个属性其结构如下从000H到003H的4字节UInt32是属性内容的类型。类型为0x02时为UInt16。类型为0x03时为UInt32。类型为0x0B时为Boolean。类型为0x1E时为String。剩余的字节为属性的内容。除了类型是String时为不定长其余三种均为4位字节多余字节置0。类型是String时前4字节是字符串的长度包括“\0”所以没法使用BinaryReader的ReadString读取。之后长度为字符串内容字符串是使用单字节编码进行存储的可以使用Encoding中的GetString获取字符串内容。为了方便开发我们创建一个DocumentSummary的类。比较有意思的是不论DocumentSummaryInformation还是SummaryInformation第一个属性都是记录该组内容的代码页编码