【题外话】

这是2010年参加比赛时候做的研究,当时为了实现对Word、Excel、PowerPoint文件文字内容的抽取研究了很久,由于Java有POI库,可以轻松的抽取各种Office文档,而.NET虽然有移植的NPOI,但是只实现了最核心的Excel文件的读写,所以之后查了很多资料才实现了Word和PowerPoint文件文字的抽取。之后忙于各种事情一直没时间整理,后来虽然想写成文章但由于时间太久也记不清很多细节,现在重新查找资料并整理如下,希望对大家有用。

【系列索引】 

  1. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(一)
    获取Office二进制文档的DocumentSummaryInformation以及SummaryInformation
  2. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(二)
    获取Word二进制文档(.doc)的文字内容(包括正文、页眉、页脚、批注等等)
  3. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(三)
    详细介绍Office二进制文档中的存储结构,以及获取PowerPoint二进制文档(.ppt)的文字内容
  4. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(完)
    介绍Office Open XML文档(.docx、.pptx)如何进行解析以及解析Office文件常见开源类库

【文章索引】

  1. .NET下读取Office文件的方式
  2. Windows复合二进制文件及其Header
  3. 我们从Directory开始
  4. DocumentSummaryInformation和SummaryInformation
  5. 相关链接

【一、.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之后可以分为不同的Sector,Sector的种类有FAT、Mini-FAT(属于Mini-Sector)、Directory、DIF、Stroage等五种。为了方便称呼,我们规定每个Sector都有一个SectorID,Header后的Sector为第一个Sector,其SectorID为0。

我们先来说Header,一个Header的部分截图及包含的信息如下,比较重要的用粗体表示。

  1. Header的前8字节Byte[],也就是整个文件的前8字节,都是固定的0xD0 0xCF 0x11 0xE0 0xA1 0xB1 0x1A 0xE1,如果不是则说明不是复合文件。
  2. 从008H到017H的16字节,是Class Id,不过很多文件都置的0。
  3. 从018H到019H的2字节UInt16,是文件格式的次要版本。
  4. 从01AH到01BH的2字节UInt16,是文件格式的主要版本。
  5. 从01CH到01DH的2字节UInt16,是固定为0xFE 0xFF,表示文档使用的是Little Endian(低位在前,高位在后)。
  6. 从01EH到01FH的2字节UInt16,是Sector大小的幂,默认为9(0x09 0x00),即每个Sector为512字节。
  7. 从020H到021H的2字节UInt16,是Mini-Sector大小的幂,默认为6(0x06 0x00),即每个Mini-Sector为64字节。
  8. 从022H到023H的2字节UInt16,是预留的,必须置0。
  9. 从024H到027H的4字节UInt32,是预留的,必须置0。
  10. 从028H到02BH的4字节UInt32,是预留的,必须置0。
  11. 从02CH到02FH的4字节UInt32,是FAT的数量。
  12. 从030H到033H的4字节UInt32,是Directory开始的SectorID。
  13. 从034H到037H的4字节UInt32,是用于事务的,必须置0。
  14. 从038H到03BH的4字节UInt32,是最小串(Stream)的最大大小,默认为4096(0x00 0x10 0x00 0x10)。
  15. 从03CH到03FH的4字节UInt32,是MiniFAT表开始的SectorID
  16. 从040H到043H的4字节UInt32,是MiniFAT表的数量。
  17. 从044H到047H的4字节UInt32,是DIFAT开始的SectorID
  18. 从048H到04BH的4字节UInt32,是DIFAT的数量。
  19. 从04CH到1FFH的436字节UInt32[],是前109块FAT表的SectorID。

那么我们可以写如下的代码将Header中重要的内容解析出来。

View Code

 1 #region 字段
 2 private FileStream m_stream;
 3 private BinaryReader m_reader;
 4 private Int64 m_length;
 5 private DirectoryEntry m_dirRootEntry;
 6
 7 #region 头部信息
 8 private UInt32 m_sectorSize;//Sector大小
 9 private UInt32 m_miniSectorSize;//Mini-Sector大小
10 private UInt32 m_fatCount;//FAT数量
11 private UInt32 m_dirStartSectorID;//Directory开始的SectorID
12 private UInt32 m_miniFatStartSectorID;//Mini-FAT开始的SectorID
13 private UInt32 m_miniFatCount;//Mini-FAT数量
14 private UInt32 m_difStartSectorID;//DIF开始的SectorID
15 private UInt32 m_difCount;//DIF数量
16 #endregion
17 #endregion
18
19 #region 读取头部信息
20 private void ReadHeader()
21 {
22     if (this.m_reader == null)
23     {
24         return;
25     }
26
27     //先判断是否是Office文件格式
28     Byte[] sig = (this.m_length > 512 ? this.m_reader.ReadBytes(8) : null);
29     if (sig == null ||
30         sig[0] != 0xD0 || sig[1] != 0xCF || sig[2] != 0x11 || sig[3] != 0xE0 ||
31         sig[4] != 0xA1 || sig[5] != 0xB1 || sig[6] != 0x1A || sig[7] != 0xE1)
32     {
33         throw new Exception("该文件不是Office文件!");
34     }
35
36     //读取头部信息
37     this.m_stream.Seek(22, SeekOrigin.Current);
38     this.m_sectorSize = (UInt32)Math.Pow(2, this.m_reader.ReadUInt16());
39     this.m_miniSectorSize = (UInt32)Math.Pow(2, this.m_reader.ReadUInt16());
40
41     this.m_stream.Seek(10, SeekOrigin.Current);
42     this.m_fatCount = this.m_reader.ReadUInt32();
43     this.m_dirStartSectorID = this.m_reader.ReadUInt32();
44
45     this.m_stream.Seek(8, SeekOrigin.Current);
46     this.m_miniFatStartSectorID = this.m_reader.ReadUInt32();
47     this.m_miniFatCount = this.m_reader.ReadUInt32();
48     this.m_difStartSectorID = this.m_reader.ReadUInt32();
49     this.m_difCount = this.m_reader.ReadUInt32();
50 }
51 #endregion

说个比较有意思的,.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字节,其主要结构如下:

  1. 从000H到040H的64字节,是存储DirectoryEntry名称的,并且是以Unicode存储的,即每个字符占2个字节,其实可以看做是UInt16。
  2. 从041H到042H的2字节UInt16,是DirectoryEntry名称的长度(包括最后的“\0”)。
  3. 从042H到042H的1字节Byte,是DirectoryEntry的类型。(主要的有:1为目录,2为节点,5为根节点)
  4. 从044H到047H的4字节UInt32,是该DirectoryEntry左兄弟的EntryID(第一个DirectoryEntry的EntryID为0,下同)。
  5. 从048H到04BH的4字节UInt32,是该DirectoryEntry右兄弟的EntryID。
  6. 从04CH到04FH的4字节UInt32,是该DirectoryEntry一个孩子的EntryID。
  7. 从074H到077H的4字节UInt32,是该DirectoryEntry开始的SectorID。
  8. 从078H到07BH的4字节UInt32,是该DirectoryEntry存储的所有字节长度。

显然,Directory其实是一个树形的结构,我们只要从第一个Entry(Root Entry)开始递归搜索就可以了。

为了方便开发,我们创建一个DirectoryEntry的类

View Code

  1 public enum DirectoryEntryType : byte
  2 {
  3     Invalid = 0,
  4     Storage = 1,
  5     Stream = 2,
  6     LockBytes = 3,
  7     Property = 4,
  8     Root = 5
  9 }
 10
 11 public class DirectoryEntry
 12 {
 13     #region 字段
 14     private UInt32 m_entryID;
 15     private String m_entryName;
 16     private DirectoryEntryType m_entryType;
 17     private UInt32 m_sectorID;
 18     private UInt32 m_length;
 19
 20     private DirectoryEntry m_parent;
 21     private List<DirectoryEntry> m_children;
 22     #endregion
 23
 24     #region 属性
 25     /// <summary>
 26     /// 获取DirectoryEntry的EntryID
 27     /// </summary>
 28     public UInt32 EntryID
 29     {
 30         get { return this.m_entryID; }
 31     }
 32
 33     /// <summary>
 34     /// 获取DirectoryEntry名称
 35     /// </summary>
 36     public String EntryName
 37     {
 38         get { return this.m_entryName; }
 39     }
 40
 41     /// <summary>
 42     /// 获取DirectoryEntry类型
 43     /// </summary>
 44     public DirectoryEntryType EntryType
 45     {
 46         get { return this.m_entryType; }
 47     }
 48
 49     /// <summary>
 50     /// 获取DirectoryEntry的SectorID
 51     /// </summary>
 52     public UInt32 SectorID
 53     {
 54         get { return this.m_sectorID; }
 55     }
 56
 57     /// <summary>
 58     /// 获取DirectoryEntry的内容大小
 59     /// </summary>
 60     public UInt32 Length
 61     {
 62         get { return this.m_length; }
 63     }
 64
 65     /// <summary>
 66     /// 获取DirectoryEntry的父节点
 67     /// </summary>
 68     public DirectoryEntry Parent
 69     {
 70         get { return this.m_parent; }
 71     }
 72
 73     /// <summary>
 74     /// 获取DirectoryEntry的子节点
 75     /// </summary>
 76     public List<DirectoryEntry> Children
 77     {
 78         get { return this.m_children; }
 79     }
 80     #endregion
 81
 82     #region 构造函数
 83     /// <summary>
 84     /// 初始化新的DirectoryEntry
 85     /// </summary>
 86     /// <param name="parent">父节点</param>
 87     /// <param name="entryID">DirectoryEntryID</param>
 88     /// <param name="entryName">DirectoryEntry名称</param>
 89     /// <param name="entryType">DirectoryEntry类型</param>
 90     /// <param name="sectorID">SectorID</param>
 91     /// <param name="length">内容大小</param>
 92     public DirectoryEntry(DirectoryEntry parent, UInt32 entryID, String entryName, DirectoryEntryType entryType, UInt32 sectorID, UInt32 length)
 93     {
 94         this.m_entryID = entryID;
 95         this.m_entryName = entryName;
 96         this.m_entryType = entryType;
 97         this.m_sectorID = sectorID;
 98         this.m_length = length;
 99         this.m_parent = parent;
100
101         if (entryType == DirectoryEntryType.Root || entryType == DirectoryEntryType.Storage)
102         {
103             this.m_children = new List<DirectoryEntry>();
104         }
105     }
106     #endregion
107
108     #region 方法
109     public void AddChild(DirectoryEntry entry)
110     {
111         if (this.m_children == null)
112         {
113             this.m_children = new List<DirectoryEntry>();
114         }
115
116         this.m_children.Add(entry);
117     }
118
119     public DirectoryEntry GetChild(String entryName)
120     {
121         for (Int32 i = 0; i < this.m_children.Count; i++)
122         {
123             if (String.Equals(this.m_children[i].EntryName, entryName))
124             {
125                 return this.m_children[i];
126             }
127         }
128
129         return null;
130     }
131     #endregion
132 }

然后我们递归搜索就可以了

View Code

 1 #region 常量
 2 private const UInt32 HeaderSize = 0x200;//512字节
 3 private const UInt32 DirectoryEntrySize = 0x80;//128字节
 4 #endregion
 5
 6 #region 读取目录信息
 7 private void ReadDirectory()
 8 {
 9     if (this.m_reader == null)
10     {
11         return;
12     }
13
14     UInt32 leftSiblingEntryID, rightSiblingEntryID, childEntryID;
15     this.m_dirRootEntry = GetDirectoryEntry(0, null, out leftSiblingEntryID, out rightSiblingEntryID, out childEntryID);
16     this.ReadDirectoryEntry(this.m_dirRootEntry, childEntryID);
17 }
18
19 private void ReadDirectoryEntry(DirectoryEntry rootEntry, UInt32 entryID)
20 {
21     UInt32 leftSiblingEntryID, rightSiblingEntryID, childEntryID;
22     DirectoryEntry entry = GetDirectoryEntry(entryID, rootEntry, out leftSiblingEntryID, out rightSiblingEntryID, out childEntryID);
23
24     if (entry == null || entry.EntryType == DirectoryEntryType.Invalid)
25     {
26         return;
27     }
28
29     rootEntry.AddChild(entry);
30
31     if (leftSiblingEntryID < UInt32.MaxValue)//有左兄弟节点
32     {
33         this.ReadDirectoryEntry(rootEntry, leftSiblingEntryID);
34     }
35
36     if (rightSiblingEntryID < UInt32.MaxValue)//有右兄弟节点
37     {
38         this.ReadDirectoryEntry(rootEntry, rightSiblingEntryID);
39     }
40
41     if (childEntryID < UInt32.MaxValue)//有孩子节点
42     {
43         this.ReadDirectoryEntry(entry, childEntryID);
44     }
45 }
46
47 private DirectoryEntry GetDirectoryEntry(UInt32 entryID, DirectoryEntry parentEntry, out UInt32 leftSiblingEntryID, out UInt32 rightSiblingEntryID, out UInt32 childEntryID)
48 {
49     leftSiblingEntryID = UInt16.MaxValue;
50     rightSiblingEntryID = UInt16.MaxValue;
51     childEntryID = UInt16.MaxValue;
52
53     this.m_stream.Seek(GetDirectoryEntryOffset(entryID), SeekOrigin.Begin);
54
55     if (this.m_stream.Position >= this.m_length)
56     {
57         return null;
58     }
59
60     StringBuilder temp = new StringBuilder();
61     for (Int32 i = 0; i < 32; i++)
62     {
63         temp.Append((Char)this.m_reader.ReadUInt16());
64     }
65
66     UInt16 nameLen = this.m_reader.ReadUInt16();
67     String name = (temp.ToString(0, (temp.Length < (nameLen / 2 - 1) ? temp.Length : nameLen / 2 - 1)));
68     Byte type = this.m_reader.ReadByte();
69
70     if (type > 5)
71     {
72         return null;
73     }
74
75     this.m_stream.Seek(1, SeekOrigin.Current);
76     leftSiblingEntryID = this.m_reader.ReadUInt32();
77     rightSiblingEntryID = this.m_reader.ReadUInt32();
78     childEntryID = this.m_reader.ReadUInt32();
79
80     this.m_stream.Seek(36, SeekOrigin.Current);
81     UInt32 sectorID = this.m_reader.ReadUInt32();
82     UInt32 length = this.m_reader.ReadUInt32();
83
84     return new DirectoryEntry(parentEntry, entryID, name, (DirectoryEntryType)type, sectorID, length);
85 }
86 #endregion
87
88 #region 辅助方法
89 private Int64 GetSectorOffset(UInt32 sectorID)
90 {
91     return HeaderSize + this.m_sectorSize * sectorID;
92 }
93
94 private Int64 GetDirectoryEntryOffset(UInt32 sectorID)
95 {
96     return HeaderSize + this.m_sectorSize * this.m_dirStartSectorID + DirectoryEntrySize * sectorID;
97 }
98 #endregion

【四、DocumentSummaryInformation和SummaryInformation】

Office文档包含很多摘要信息,比如标题、作者、编辑时间等等,如下图。

摘要信息又分为两类,一类是DocumentSummaryInformation,另一类是SummaryInformation,分别包含不同种类的摘要信息。通过上述的代码应该能获取到Root Entry下有一个叫“\005DocumentSummaryInformation”的Entry和一个叫“\005SummaryInformation”的Entry。

对于DocumentSummaryInformation,其结构如下

  1. 从018H到01BH的4字节UInt32,是存储属性组的个数。
  2. 从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的偏移。

对于每个属性组,其结构如下:

  1. 从000H到003H的4字节UInt32,是属性组大小。
  2. 从004H到007H的4字节UInt32,是属性组中属性的个数。 从008H开始的每8字节,是属性的信息:
  • 对于前4字节UInt32,是属性编号,表示属性的种类。
  • 对于后4字节UInt32,是属性内容相对于属性组的偏移。

常见的属性编号有以下这些:

View Code

 1 public enum DocumentSummaryInformationType : uint
 2 {
 3     Unknown                 = 0x00,
 4     CodePage                = 0x01,
 5     Category                = 0x02,
 6     PresentationTarget      = 0x03,
 7     Bytes                   = 0x04,
 8     LineCount               = 0x05,
 9     ParagraphCount          = 0x06,
10     Slides                  = 0x07,
11     Notes                   = 0x08,
12     HiddenSlides            = 0x09,
13     MMClips                 = 0x0A,
14     Scale                   = 0x0B,
15     HeadingPairs            = 0x0C,
16     DocumentParts           = 0x0D,
17     Manager                 = 0x0E,
18     Company                 = 0x0F,
19     LinksDirty              = 0x10,
20     CountCharsWithSpaces    = 0x11,
21     SharedDoc               = 0x13,
22     HyperLinksChanged       = 0x16,
23     Version                 = 0x17,
24     ContentStatus           = 0x1B
25 }

对于每个属性,其结构如下:

  1. 从000H到003H的4字节UInt32,是属性内容的类型。

    • 类型为0x02时为UInt16。
    • 类型为0x03时为UInt32。
    • 类型为0x0B时为Boolean。
    • 类型为0x1E时为String。
  2. 剩余的字节为属性的内容。
    1. 除了类型是String时为不定长,其余三种均为4位字节(多余字节置0)。
    2. 类型是String时前4字节是字符串的长度(包括“\0”),所以没法使用BinaryReader的ReadString读取。之后长度为字符串内容,字符串是使用单字节编码进行存储的,可以使用Encoding中的GetString获取字符串内容。

为了方便开发,我们创建一个DocumentSummary的类。比较有意思的是,不论DocumentSummaryInformation还是SummaryInformation,第一个属性都是记录该组内容的代码页编码,可以通过Encoding.GetEncoding()获取对应的编码然后用GetString把对应的字符串解析出来:

View Code

 1 public class DocumentSummaryInformation
 2 {
 3     #region 字段
 4     private DocumentSummaryInformationType m_propertyID;
 5     private Object m_data;
 6     #endregion
 7
 8     #region 属性
 9     /// <summary>
10     /// 获取属性类型
11     /// </summary>
12     public DocumentSummaryInformationType Type
13     {
14         get { return this.m_propertyID; }
15     }
16
17     /// <summary>
18     /// 获取属性数据
19     /// </summary>
20     public Object Data
21     {
22         get { return this.m_data; }
23     }
24     #endregion
25
26     #region 构造函数
27     /// <summary>
28     /// 初始化新的非字符串型DocumentSummaryInformation
29     /// </summary>
30     /// <param name="propertyID">属性ID</param>
31     /// <param name="propertyType">属性数据类型</param>
32     /// <param name="data">属性数据</param>
33     public DocumentSummaryInformation(UInt32 propertyID, UInt32 propertyType, Byte[] data)
34     {
35         this.m_propertyID = (DocumentSummaryInformationType)propertyID;
36         if (propertyType == 0x02) this.m_data = BitConverter.ToUInt16(data, 0);
37         else if (propertyType == 0x03) this.m_data = BitConverter.ToUInt32(data, 0);
38         else if (propertyType == 0x0B) this.m_data = BitConverter.ToBoolean(data, 0);
39     }
40
41     /// <summary>
42     /// 初始化新的字符串型DocumentSummaryInformation
43     /// </summary>
44     /// <param name="propertyID">属性ID</param>
45     /// <param name="propertyType">属性数据类型</param>
46     /// <param name="codePage">代码页标识符</param>
47     /// <param name="data">属性数据</param>
48     public DocumentSummaryInformation(UInt32 propertyID, UInt32 propertyType, Int32 codePage, Byte[] data)
49     {
50         this.m_propertyID = (DocumentSummaryInformationType)propertyID;
51         if (propertyType == 0x1E) this.m_data = Encoding.GetEncoding(codePage).GetString(data).Replace("\0", "");
52     }
53     #endregion
54 }

然后我们进行读取就可以了:

View Code

 1 private List<DocumentSummaryInformation> m_documentSummaryInformation;
 2
 3 #region 读取DocumentSummaryInformation
 4 private void ReadDocumentSummaryInformation()
 5 {
 6     DirectoryEntry entry = this.m_dirRootEntry.GetChild('\x05' + "DocumentSummaryInformation");
 7
 8     if (entry == null)
 9     {
10         return;
11     }
12
13     Int64 entryStart = this.GetSectorOffset(entry.SectorID);
14
15     this.m_stream.Seek(entryStart + 24, SeekOrigin.Begin);
16     UInt32 propertysCount = this.m_reader.ReadUInt32();
17     UInt32 docSumamryStart = 0;
18
19     for (Int32 i = 0; i < propertysCount; i++)
20     {
21         Byte[] clsid = this.m_reader.ReadBytes(16);
22         if (clsid[0] == 0x02 && clsid[1] == 0xD5 && clsid[2] == 0xCD && clsid[3] == 0xD5 &&
23             clsid[4] == 0x9C && clsid[5] == 0x2E && clsid[6] == 0x1B && clsid[7] == 0x10 &&
24             clsid[8] == 0x93 && clsid[9] == 0x97 && clsid[10] == 0x08 && clsid[11] == 0x00 &&
25             clsid[12] == 0x2B && clsid[13] == 0x2C && clsid[14] == 0xF9 && clsid[15] == 0xAE)//如果是DocumentSummaryInformation
26         {
27             docSumamryStart = this.m_reader.ReadUInt32();
28             break;
29         }
30         else
31         {
32             this.m_stream.Seek(4, SeekOrigin.Current);
33         }
34     }
35
36     if (docSumamryStart == 0)
37     {
38         return;
39     }
40
41     this.m_stream.Seek(entryStart + docSumamryStart, SeekOrigin.Begin);
42     this.m_documentSummaryInformation = new List<DocumentSummaryInformation>();
43     UInt32 docSummarySize = this.m_reader.ReadUInt32();
44     UInt32 docSummaryCount = this.m_reader.ReadUInt32();
45     Int64 offsetMark = this.m_stream.Position;
46     Int32 codePage = Encoding.Default.CodePage;
47
48     for (Int32 i = 0; i < docSummaryCount; i++)
49     {
50         this.m_stream.Seek(offsetMark, SeekOrigin.Begin);
51         UInt32 propertyID = this.m_reader.ReadUInt32();
52         UInt32 properyOffset = this.m_reader.ReadUInt32();
53
54         offsetMark = this.m_stream.Position;
55
56         this.m_stream.Seek(entryStart + docSumamryStart + properyOffset, SeekOrigin.Begin);
57         UInt32 propertyType = this.m_reader.ReadUInt32();
58         DocumentSummaryInformation info = null;
59         Byte[] data = null;
60
61         if (propertyType == 0x1E)
62         {
63             UInt32 strLen = this.m_reader.ReadUInt32();
64             data = this.m_reader.ReadBytes((Int32)strLen);
65             info = new DocumentSummaryInformation(propertyID, propertyType, codePage, data);
66         }
67         else
68         {
69             data = this.m_reader.ReadBytes(4);
70             info = new DocumentSummaryInformation(propertyID, propertyType, data);
71
72             if (info.Type == DocumentSummaryInformationType.CodePage)//如果找到CodePage的属性
73             {
74                 codePage = (Int32)(UInt16)info.Data;
75             }
76         }
77
78         this.m_documentSummaryInformation.Add(info);
79     }
80 }
81 #endregion

而SummaryInformation与DocumentSummaryInformation相比读取方式是一样的,只不过属性组的16位标识为0xE0 0x85 0x9F 0xF2 0xF9 0x4F 0x68 0x10 0xAB 0x91 0x08 0x00 0x2B 0x27 0xB3 0xD9。

常见的SummaryInformation属性的属性编号如下:

View Code

 1 public enum SummaryInformationType : uint
 2 {
 3     Unknown = 0x00,
 4     CodePage = 0x01,
 5     Title = 0x02,
 6     Subject = 0x03,
 7     Author = 0x04,
 8     Keyword = 0x05,
 9     Commenct = 0x06,
10     Template = 0x07,
11     LastAuthor = 0x08,
12     Reversion = 0x09,
13     EditTime = 0x0A,
14     CreateDateTime = 0x0C,
15     LastSaveDateTime = 0x0D,
16     PageCount = 0x0E,
17     WordCount = 0x0F,
18     CharCount = 0x10,
19     ApplicationName = 0x12,
20     Security = 0x13
21 }

其他代码由于与DocumentSummaryInformation相近就不再单独给出了。

附,本文所有代码下载:https://github.com/mayswind/SimpleOfficeReader

【五、相关链接】

1、Microsoft Open Specifications:http://www.microsoft.com/openspecifications/en/us/programs/osp/default.aspx
2、用PHP读取MS Word(.doc)中的文字:https://imethan.com/post-2009-10-06-17-59.html
3、Office檔案格式:http://www.programmer-club.com.tw/ShowSameTitleN/general/2681.html
4、LAOLA file system:http://stuff.mit.edu/afs/athena/astaff/project/mimeutils/share/laola/guide.html

 

【后记】

花了好几天的时间才写完读取DocumentSummaryInformation和SummaryInformation,果然自己写程序用和写成文章区别太大了,前者差不多就行,后者还得仔细查阅资料。如果您觉得好就点下推荐呗。

转载于:https://www.cnblogs.com/mayswind/archive/2013/03/17/2962205.html

Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(一)...相关推荐

  1. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(完)...

    原文 http://www.cnblogs.com/mayswind/archive/2013/04/01/2991271.html [题外话] 这是这个系列的最后一篇文章了,为了不让自己觉得少点什么 ...

  2. Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析

    转载http://www.cnblogs.com/mayswind/archive/2013/03/17/2962205.html [题外话] 这是2010年参加比赛时候做的研究,当时为了实现对Wor ...

  3. Linux 平台下如何使用GCC得到各种格式的文件正文(office文件,PDF,邮件,html,zip等)

    在文件数据挖掘处理技术中,如何从文本数据中抽取有价值的信息和知识是一个重要的数据挖据分支,是机器学习.自然语言处理.数理统计的基础技术之一,是信息检索,机器学习,AI智能等高端技术的的底层技术支持之一 ...

  4. linux文件恢复dbf,linux平台下数据文件被误删后,如何及时得知并进行恢复-dbf文件怎么打开...

    我们知道在windows平台下,一旦文件在程序中打开,则不能被删除,所以不存在误删数据文件的情况,如下图所示. 但是在LINUX操作系统中,被进程打开的文件仍可以被删除,因此存在DM7数据文件可能被误 ...

  5. linux查找时间文件,Linux基础教程 linux下使用find命令根据系统时间查找文件用法(示例代码)...

    LinuxFind 兄弟连Linux培训总结这些时间戳包括 复制代码代码如下: mtime 文件内容上次修改时间 atime 文件被读取或访问的时间 ctime 文件状态变化时间 mtime 和 at ...

  6. linux 文件 图标 覆盖,在Deepin下处理Microsoft Edge替换图标及替换文件

    Microsoft推出的Edge for Linux版本可正常使用在Deepin系统下,本文介绍其替换图标及替换文件的方法.如果要安装,可去下载,并在终端中执行sudo dpkg -i  micros ...

  7. linux删除中文名文件,linux运维 - linux下 怎样删除文件名中包含特殊字符的文件...

    目录中无意间出现了 -- 这个文件 [root@dev tmp]# ls -- 00 01 02 03 04 05 06 07 08 09 [root@dev tmp]# ll total 0 -rw ...

  8. STM32平台下官方DMP库6.12超详细移植教程

    前记 Motion Driver官方库:Motion_Driver_6.12 STM32工程源码:STM32F103C8-软件MPU6050(DMP) MPU6050软件I2C驱动,带OLED显示,移 ...

  9. linux下扩展名为sh的安装程序文件如何运行?

    linux下扩展名为sh的安装程序文件如何运行? 扩展名为sh到文件是什么东东? 如何运行扩展名为sh的安装程序? 扩展名为sh到文件是什么东东?   是linux系统下的脚本文件,很多linux下的 ...

最新文章

  1. 干货|浅谈强化学习的方法及学习路线
  2. 企业价值观念形成的四个阶段
  3. linux下eaccelerator,memcache,memcached安装
  4. 源码分析-HashSet、LinkedHashSet
  5. struts 2中为什么抽象包不能包含action?
  6. 代码练习中的bug及修改方法
  7. python与正则表达式(part4)--正则表达式分组
  8. 图灵奖大佬 Lecun 发表对比学习新作,比 SimCLR 更好用!
  9. PyQt5笔记(06) -- 菜单
  10. erl_0015 《硝烟中的erlang》 读书笔记002 “为过载做计划”
  11. 这份中台与数据报表的干货我写了10小时,真不想告诉你
  12. thinkphp5 接收layui上传的文件
  13. DLLPasswordFilterImplant:DLL密码过滤器
  14. java倍数增长计算公式,增长倍数计算公式是什么
  15. Word公式居中,公式编号右对齐
  16. 倒是应该看看教科书了
  17. Spring Cloud Alibaba——Nacos服务配置中心
  18. 模糊查询忽略大小写解决方案
  19. spyder的安装配置及无法使用第三方包的问题
  20. 强化学习 qlearning解决tsp问题

热门文章

  1. 初中数学知识遗忘记录(持续更新中)
  2. flink sql实现interval join的图解
  3. Python中的Mixin详解
  4. xfce4自己使用的主题设置备份以及xfce4默认的壁纸路径
  5. colab长时间处于正在连接
  6. Clion:undefined reference to `cv::noArray()解决方案
  7. 用scikit-learn进行LDA降维(转载+注释)
  8. sublime,gedit,vim和mousepad等都出现fribidi_get_par_embedding_levels_ex
  9. url 转换中文_数字快速转换成中文大写,我有妙招
  10. 两个sql交集_如何使用性能分析工具定位SQL执行慢的原因?