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

【一、初见Office Open XML(OOXML)】

先来看一段微软官方对Office Open XML的说明(详细见http://office.microsoft.com/zh-cn/support/HA010205815.aspx?CTT=3):

可以看到,与Windows 复合文档不同的是,OOXML生来就是开放的,而且由于基于zip+xml的格式,使得读取变得更容易,如果仅是为了抽取文字,我们甚至不需要读取文档的任何参数!

如果您之前不了解OOXML的话,我们可以把手头docx、pptx以及xlsx文件的扩展名改为zip,然后用压缩软件打开看看。

打开的这三个文件分别是docx、pptx和xlsx,我们可以看到,目录结构清晰可见,所以我们只需要使用读取zip的类库读取zip文件,然后再解析xml文件即可。对于使用.NET Framework 3.0及以上的,可以直接使用.NET自带的Package类(System.IO.Packaging,在WindowsBase.dll中)进行解压,个人感觉如果只是读取zip流中的文件流或内容,WindowsBase中的Package还是很好用的。如果用于.NET CF或者2.0甚至以下的CLR可以使用SharpZipLib(支持CLR 1.1、2.0、4.0,官方网站http://www.icsharpcode.net/),也可以使用DotNetZip(支持CLR 2.0,官方网站http://dotnetzip.codeplex.com/),个人感觉后者的License更友好些。

比如我们使用自带的Package打开OOXML文件:

View Code

1 #region 字段

2 protected FileStream m_stream;

3 protected Package m_package;

4 #endregion

5

6 #region 构造函数

7 /// <summary>

8 /// 初始化OfficeOpenXMLFile

9 /// </summary>

10 /// <param name="filePath">文件路径</param>

11 public OfficeOpenXMLFile(String filePath)

12 {

13 try

14 {

15 this.m_stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

16 this.m_package = Package.Open(this.m_stream);

17

18 this.ReadProperties();

19 this.ReadCoreProperties();

20 this.ReadContent();

21 }

22 finally

23 {

24 if (this.m_package != null)

25 {

26 this.m_package.Close();

27 }

28

29 if (this.m_stream != null)

30 {

31 this.m_stream.Close();

32 }

33 }

34 }

35 #endregion

【二、OOXML文档属性的解析】

OOXML文件的文档属性其实存在于docProps目录下,比较重要的有三个文件

app.xml:记录文档的属性,内容类似之前的DocumentSummaryInformation。

core.xml:记录文档核心的属性,比如创建时间、最后修改时间等等,内容类似之前的SummaryInformation。

thumbnail.*:文档的缩略图,不同文件存储的是不同的格式,比如Word为emf,Excel为wmf,PowerPoint为jpeg。

我们只需要遍历XML文件中所有的子节点就可以读出所有的属性,为了好看,这里还用的Windows复合文件中的名称:

View Code

1 #region 常量

2 private const String PropertiesNameSpace = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";

3 private const String CorePropertiesNameSpace = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";

4 #endregion

5

6 #region 字段

7 protected Dictionary<String, String> m_properties;

8 protected Dictionary<String, String> m_coreProperties;

9 #endregion

10

11 #region 属性

12 /// <summary>

13 /// 获取DocumentSummaryInformation

14 /// </summary>

15 public override Dictionary<String, String> DocumentSummaryInformation

16 {

17 get

18 {

19 return this.m_properties;

20 }

21 }

22

23 /// <summary>

24 /// 获取SummaryInformation

25 /// </summary>

26 public override Dictionary<String, String> SummaryInformation

27 {

28 get

29 {

30 return this.m_coreProperties;

31 }

32 }

33 #endregion

34

35 #region 读取Properties

36 private void ReadProperties()

37 {

38 if (this.m_package == null)

39 {

40 return;

41 }

42

43 PackagePart part = this.m_package.GetPart(new Uri("/docProps/app.xml", UriKind.Relative));

44 if (part == null)

45 {

46 return;

47 }

48

49 XmlDocument doc = new XmlDocument();

50 doc.Load(part.GetStream());

51

52 XmlNodeList nodes = doc.GetElementsByTagName("Properties", PropertiesNameSpace);

53 if (nodes.Count < 1)

54 {

55 return;

56 }

57

58 this.m_properties = new Dictionary<String, String>();

59 foreach (XmlElement element in nodes0)

60 {

61 this.m_properties.Add(element.LocalName, element.InnerText);

62 }

63 }

64 #endregion

65

66 #region 读取CoreProperties

67 private void ReadCoreProperties()

68 {

69 if (this.m_package == null)

70 {

71 return;

72 }

73

74 PackagePart part = this.m_package.GetPart(new Uri("/docProps/core.xml", UriKind.Relative));

75 if (part == null)

76 {

77 return;

78 }

79

80 XmlDocument doc = new XmlDocument();

81 doc.Load(part.GetStream());

82

83 XmlNodeList nodes = doc.GetElementsByTagName("coreProperties", CorePropertiesNameSpace);

84 if (nodes.Count < 1)

85 {

86 return;

87 }

88

89 this.m_coreProperties = new Dictionary<String, String>();

90 foreach (XmlElement element in nodes0)

91 {

92 this.m_coreProperties.Add(element.LocalName, element.InnerText);

93 }

94 }

95 #endregion

【三、Word 2007文件的解析】

Word文件(.docx)主要的内容基本都存在于word目录下,比较重要的有以下的内容

document.xml:记录Word文档的正文内容

footer*.xml:记录Word文档的页脚

header*.xml:记录Word文档的页眉

comments.xml:记录Word文档的批注

footnotes.xml:记录Word文档的脚注

endnotes.xml:记录Word文档的尾注

这里我们只读取Word文档的正文内容,由于OOXML文档在存储文字时也是嵌套结构存储的,比如对于Word而言,<w:p></w:p>之间存储的是段落,段落中会嵌套着<w:t></w:t>,而这个存储的是文字。除此之外<w:tab/>是Tab符号,<w:br w:type="page"/>是分页符等等,所以我们需要写一个方法递归处理这些标签:

View Code

1 /// <summary>

2 /// 抽取Node中的文字

3 /// </summary>

4 /// <param name="node">XmlNode</param>

5 /// <returns>Node中的文字</returns>

6 public static String ReadNode(XmlNode node)

7 {

8 if ((node == null) || (node.NodeType != XmlNodeType.Element))//如果node为空

9 {

10 return String.Empty;

11 }

12

13 StringBuilder nodeContent = new StringBuilder();

14

15 foreach (XmlNode child in node.ChildNodes)

16 {

17 if (child.NodeType != XmlNodeType.Element)

18 {

19 continue;

20 }

21

22 switch (child.LocalName)

23 {

24 case "t"://正文

25 nodeContent.Append(child.InnerText.TrimEnd());

26

27 String space = ((XmlElement)child).GetAttribute("xml:space");

28 if ((!String.IsNullOrEmpty(space)) && (space == "preserve")) nodeContent.Append(' ');

29 break;

30 case "cr"://换行符

31 case "br"://换页符

32 nodeContent.Append(Environment.NewLine);

33 break;

34 case "tab"://Tab

35 nodeContent.Append("\t");

36 break;

37 case "p"://段落

38 nodeContent.Append(ReadNode(child));

39 nodeContent.Append(Environment.NewLine);

40 break;

41 default://其他情况

42 nodeContent.Append(ReadNode(child));

43 break;

44 }

45 }

46

47 return nodeContent.ToString();

48 }

然后我们从根标签开始读取就可以了

View Code

1 #region 常量

2 private const String WordNameSpace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

3 #endregion

4

5 #region 字段

6 private String m_paragraphText;

7 #endregion

8

9 #region 属性

10 /// <summary>

11 /// 获取文档正文内容

12 /// </summary>

13 public String ParagraphText

14 {

15 get { return this.m_paragraphText; }

16 }

17 #endregion

18

19 #region 读取内容

20 protected override void ReadContent()

21 {

22 if (this.m_package == null)

23 {

24 return;

25 }

26

27 PackagePart part = this.m_package.GetPart(new Uri("/word/document.xml", UriKind.Relative));

28 if (part == null)

29 {

30 return;

31 }

32

33 StringBuilder content = new StringBuilder();

34 XmlDocument doc = new XmlDocument();

35 doc.Load(part.GetStream());

36

37 XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);

38 nsManager.AddNamespace("w", WordNameSpace);

39

40 XmlNode node = doc.SelectSingleNode("/w:document/w:body", nsManager);

41

42 if (node == null)

43 {

44 return;

45 }

46

47 content.Append(NodeHelper.ReadNode(node));

48

49 this.m_paragraphText = content.ToString();

50 }

51 #endregion

【四、PowerPoint 2007文件的解析】

PowerPoint文件(.pptx)主要的内容都存在于ppt目录下,而幻灯片的信息则又在slides子目录下,这里边幻灯片按照slide + 页序号 +.xml的名称进行存储,我们挨个顺序读取就可以。不过需要注意的是,由于字符串比较的问题,如"slide10.xml"<"slide2.xml",所以如果你按顺序读取的话可能会出现页码错乱的情况,所以我们可以先进行排序然后再挨个页面从根标签读取就可以了。

View Code

1 #region 常量

2 private const String PowerPointNameSpace = "http://schemas.openxmlformats.org/presentationml/2006/main";

3 #endregion

4

5 #region 字段

6 private StringBuilder m_allText;

7 #endregion

8

9 #region 属性

10 /// <summary>

11 /// 获取PowerPoint幻灯片中所有文本

12 /// </summary>

13 public String AllText

14 {

15 get { return this.m_allText.ToString(); }

16 }

17 #endregion

18

19 #region 构造函数

20 /// <summary>

21 /// 初始化PptxFile

22 /// </summary>

23 /// <param name="filePath">文件路径</param>

24 public PptxFile(String filePath) :

25 base(filePath) { }

26 #endregion

27

28 #region 读取内容

29 protected override void ReadContent()

30 {

31 if (this.m_package == null)

32 {

33 return;

34 }

35

36 this.m_allText = new StringBuilder();

37

38 XmlDocument doc = null;

39 PackagePartCollection col = this.m_package.GetParts();

40 SortedList<Int32, XmlDocument> list = new SortedList<Int32, XmlDocument>();

41

42 foreach (PackagePart part in col)

43 {

44 if (part.Uri.ToString().IndexOf("ppt/slides/slide", StringComparison.OrdinalIgnoreCase) > -1)

45 {

46 doc = new XmlDocument();

47 doc.Load(part.GetStream());

48

49 String pageName = part.Uri.ToString().Replace("/ppt/slides/slide", "").Replace(".xml", "");

50 Int32 index = 0;

51 Int32.TryParse(pageName, out index);

52

53 list.Add(index, doc);

54 }

55 }

56

57 foreach (KeyValuePair<Int32, XmlDocument> pair in list)

58 {

59 XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);

60 nsManager.AddNamespace("p", PowerPointNameSpace);

61

62 XmlNode node = pair.Value.SelectSingleNode("/p:sld", nsManager);

63

64 if (node == null)

65 {

66 continue;

67 }

68

69 this.m_allText.Append(NodeHelper.ReadNode(node));

70 }

71 }

72 #endregion

附,本系列全部代码下载:https://github.com/mayswind/SimpleOfficeReader

【五、常见Office文档(Word、PowerPoint、Excel)文件的开源类库】

1、NPOI:http://npoi.codeplex.com

这个没的说,.NET上最好的,没有之一,Office文档类库,提供完整的Excel读取与编辑操作,目前支持二进制(.xls)文件和OOXML(.xlsx)两种格式。如果用过Apache的Java类库POI的话,NPOI提供几乎一样的类库。实际上,对于ASP.NET,需要编辑的Office文档大多都是Excel文件,或者也可以使用Excel文件代替,所以使用NPOI几乎已经能满足所有需要。目前已经支持docx文件,而doc的支持则在NPOI.ScratchPad中,大家可以去Source Code中下载自己编译。如果不需要OOXML的话,类库仅有1.5MB,并且支持.NET CLR 2.0和4.0。

2、Open XML SDK 2.0 for Microsoft Office:http://msdn.microsoft.com/en-us/library/bb448854(office.14).aspx

微软提供的Open XML SDK,支持读写任意OOXML文档,其同时提供了一个工具,可以打开Office文档然后直接生成使用该类库生成该文档的程序代码。只不过类库确实大了些,有5MB之多,并且需要.NET Framework 3.5的支持。

3、Office Binary Translator to Open XML:http://b2xtranslator.sourceforge.net/

这是我最近才知道的一个类库,其实很早很早以前就有了,其可以将Windows复合文档(.doc、.ppt、.xls)转换为对应的OOXML格式(.docx、.pptx、.xlsx),当然你也可以获取文件中存储的内容。不知道为什么,这个网站被墙了。如果你想研究Windows复合文档的话,我比较推荐这个类库,因为NPOI实在是太完美的一个类库,要想走一遍文件读取的流程实在是太复杂,但是如果用这个类库单步的话还是很容易懂的。这个类库将每种文件的支持(以及支持的模块等)都拆分到了不同的项目中,支持每种文件仅需要几百KB,而且是基于.NET CLR 2.0的。

4、EPPlus:http://epplus.codeplex.com

在2010年NPOI还不支持OOXML的时候,个人感觉EPPlus是最好的.xlsx文件处理的类库,其仅有几百KB,非常轻量,对于zip文件的读取,这个类库没有选择SharpZipLib或者DotNetZip,老版本需要.NET Framework 3.0就行,刚看了下新版本得需要.NET Framework 3.5才可以。

5、ExcelDataReader:http://exceldatareader.codeplex.com

也是一个非常轻量并且好用的库,同时支持读取.xls和.xlsx,当年在使用EPPlus之前使用的这个类库,记不得是因为什么问题替换成了EPPlus,也不知道这个问题现在解决了没有。这个类库的好处是仅需要.NET CLR 2.0,并且支持.NET CF,只不过现在已经不需要开发Windows Mobile的应用了。

【六、相关链接】

1、OpenXMLDeveloper.orghttp://openxmldeveloper.org

2、如何:从 Office Open XML 文档检索段落:http://msdn.microsoft.com/zh-cn/library/bb669175.aspx

3、如何操作 Office Open XML 格式文档:http://www.microsoft.com/china/msdn/library/office/office/howManipulateOfficexml.mspx

4、如何实现...(打开 XML SDK):http://msdn.microsoft.com/zh-cn/library/bb491088.aspx

【后记】

终于到了最后一篇,这个系列就到这结束了,感谢大家的捧场,我也终于实现了两年前的心愿。说实话,我确实没想到第一篇会有那么多的访问和推荐,因为需要解析Office文档的毕竟是少数的。写这四篇文章也希望起到抛砖引玉的作用,起码可以对Office文档有个最基础的了解,而之后如果想深入了解下去也会容易得多,这也是我要把这些内容写出来的原因。

【补遗】

在写完这四篇文章后,我偶然发现微软关于这方面竟然有中文文档,泪奔了,为什么之前我没有找到。所以在此附上几篇常用的链接。

1、了解 Office 二进制文件格式:http://msdn.microsoft.com/zh-cn/library/gg615407(v=office.14).aspx

2、了解 Word MS-DOC 二进制文件格式:http://msdn.microsoft.com/zh-CN/library/gg615596

3、了解 PowerPoint MS-PPT 二进制文件格式:http://msdn.microsoft.com/zh-CN/library/gg615594

4、了解采用 Office 二进制文件格式的图形:http://msdn.microsoft.com/zh-CN/library/gg985447

5、在二进制 PowerPoint MS-PPT 文件中查找图形:http://msdn.microsoft.com/zh-CN/library/hh244173

相关推荐
m0_547486661 小时前
《大数据技术与应用:Hadoop和PySpark实现》全套PPT课件2026
大数据·hadoop
小此方1 小时前
Linux网络(十):HTTP请求是如何被服务器解析的?从请求报文、路径处理到响应状态码与异常处理
linux·服务器·网络
风骏时光牛马1 小时前
AI提示词工程:高效构建精准指令的方法论
前端
少司府1 小时前
Linux系统篇(一)指令篇·一:初识Linux及其基本指令
linux·运维·服务器·网络·阿里云·linux系统
葡萄城技术团队1 小时前
一个好的 BI Dashboard 到底应该怎么设计?
前端
估值探索者1 小时前
【AI+量化实战 #05】财报季的信息洪流:用LLM给业绩预告分类+算事件窗口收益
java·c语言·c++·人工智能·python·分类·数据挖掘
热爱编程的小李2 小时前
基于若以开发的新版情怀麻将后台
java
starzy19902 小时前
Flink高级之富函数类深度剖析:执行时序、状态注册与序列化陷阱
大数据·flink
一切如来心秘密2 小时前
智驾数据闭环湖仓实战: Apache Paimon 分层建模实践
大数据·智驾数据闭环