技术开发 频道

Office文件格式突变,促使Java和Office更完美集成



【IT168 专稿】Office是大家非常熟悉的办公软件。它的强大的文字和表格的处理能力几乎无人能敌。而其中的Excel除了被应用在传统的表格处理中,也常被广大程序员“抓”来充当报表系统的前端。如我们使用的Java Web程序,在后台生成一个带报表数据的Excel文件,然后将这个文件送到前台浏览器(必须是IE),这样这个Excel文件就可以嵌入到IE中。
 
这样处理报表既可以利用Excel强大的表格处理功能,又可以使程序员省了很多的麻烦。但这么做有一个限制。就是服务器的Java生成Excel报表时,一般不会使用COM来调用Excel组件,而是按着Excel文件的二进制格式直接生成Excel文件。但由于Office是商业软件,微软并没有将Office的文件格式完全公开(就算公开,也不是最新版本的),这样就给生成Excel文件带来了一定的困难。虽然一些比较新的Office文档可以保存成XML格式,但这种文件格式只是一种简陋的替代,再说前端的IE并不认XML格式的Excel文档。也许是微软意识到了这个问题,在新推出的Office2007中彻底解决了这个问题。
 
在以前老版本Office中,由于法律或技术原因,脱离Office来处理Office文档将会遇到非常多的问题。也许是开源给微软带来的压力,或许是其他的原因,微软对Office2007的格式做了和以前完全不同的处理。以前的Office文档是100%的二进制格式。第三方的工具操作起来非常不方便,而Office2007从整体上都是基于XML格式的,这里并不是说Office2007文档可以保存成XML格式。而是Office2007默认的文档格式就是XML的(Word的docx、Excel的xlsx等)。也许有人会感到奇怪,用文本编辑器打开docx后,显示的仍然是二进制格式,并不是什么XML。其实docx并不是普通的XML格式,当然,也不只是一个XML文件,docx本质上是一个zip文件,里面有一系列的xml、目录和其他的文件。如果我们将docx改成zip。就可以用winzip等软件将其解开(从这一点我们可以看出,docx中的x就是指XML)。如图1是一个word2007文档解开后的目录结构。



1 docx文档解开后的目录结构

   

目录中的XML都是基于OpenXML格式的,这个规范微软已经提供提交给了ECMA,因此,这已经成为公共的标准,我们可以自由地使用它们。由于Office2007文档全部使用了压缩的xml格式,因此,只有支持读取zip格式和xml格式的语言都可以操作Office2007的文档,当然,Java也不例外。而使用Java和Office联合操作报表将更加容易。在本文中将演示如何使用Java来操作Office2007的文档,这里以word2007为例。
下面是一个简单的Word文档,如图2如示:


2 一个简单的word文档

 



     我们将这个文档保存为test.docx。注意,不要保存成向后兼容的word文档格式,也不要保存成Office2003或更好的Office的WordML格式。这个文档将是一个压缩的zip格式,如果将test.docx改成test.zip,解开后的目录结构如图1如示:
 
    从上面解开的文件结构可以非常清楚地了解test.docx的保存结构。在Java中我们可以使用java.util.zip包来解开test.docx。从这个目录结构我们可以很容易地猜出文档的主要内容保存在document.xml中。而其他的xml文件将保存不同的信息。如字体信息将保存在fontTable.xml中,而Office主题将保存在theme.xml和theme1.xml中。
 
    下面我们开使使用Java对这个文件进行操作。首先我们使用JUnit4来确定test.docx是否存在,以及是否可以对其进行读写,代码如下:

@Test public void verifyFile() { assertTrue(new File("test.docx").exists()); assertTrue(new File("test.docx").canRead()); assertTrue(new File("test.docx").canWrite()); }
下面的代码将简单地验证java.util.zip.ZipFile类是否可以打开test.docx

@Test public void openFile() throws IOException, ZipException { ZipFile docxFile = new ZipFile(new File("test.docx")); assertEquals(docxFile.getName(), "test.docx"); }
经过测试,ZipFile完全可以操作test.docx。看来很多人都迫不急待了,下面就让我们来从test.docx中来读取数据吧。首先应该打开document.xml文件。代码如下:

Test public void listContents() throws IOException, ZipException { boolean documentFound = false; ZipFile docxFile = new ZipFile(new File("test.docx")); Enumeration entriesIter = docxFile.entries(); while (entriesIter.hasMoreElements()) { ZipEntry entry = entriesIter.nextElement(); if(entry.getName().equals("document.xml")) documentFound = true; } assertTrue(documentFound); }
但是运行上面的代码将抛出一个异常,好象说明document.xml并不存在,事实上并不是如此。而是ZipFile API需要一个完整的文件或目录名,因此,需要将上面的路径变成word/document.xml。
下一步我们将通过ZipFile得到一个ZipEntry对象,并通过这个对象来看看xml中有什么,代码如下:

@Test public void getDocument() throws IOException, ZipException { ZipFile docxFile = new ZipFile(new File("test.docx")); ZipEntry documentXML = docxFile.getEntry("word/document.xml"); assertNotNull(documentXML); }


@Test public void fromDocumentIntoDOM() throws Exception { ZipFile docxFile = new ZipFile(new File("test.docx")); ZipEntry documentXML = docxFile.getEntry("word/document.xml"); InputStream documentXMLIS = docxFile.getInputStream(documentXML); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc = dbf.newDocumentBuilder().parse(documentXMLIS); assertEquals("[w:document: null]", doc.getDocumentElement().toString()); }
 下面让我们看一下test.docx中最主要的document.xml,这个文档看上去要比一般的xml文档更丰富,它可以表示word2007的所有的格式,也面是document.xml文档的内容(删除了一些命名空间):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document><w:body><w:pw:rsidR="00A77011" w:rsidRDefault="004333E8"><w:pPr><w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr></w:pPr><w:r><w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr><w:t>这是第一个测试文档</w:t></w:r></w:p><w:sectPr w:rsidR="00A77011" w:rsidSect="00A77011"><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0"/><w:cols w:space="425"/><w:docGrid w:type="lines" w:linePitch="312"/></w:sectPr></w:body></w:document>
 
    至于document.xml中每个节点和属性的详细信息已不属于本文所讨论内容,感兴趣的读者可以参考OpenXML文档格式。虽然我们不太清楚整个文档的格式,但这个文档的核心部分是相当清楚的。如"p"表示段、"r"表示文本范围。我们还可以在document.xml中找到输入的文本"这是第一个测试文档"。
 
    现在我们已经基本了解document.xml的内容,也面我们就来编辑这些内容。在前面讲来使用ZipFile和ZipEntry来读取document.xml的数据,但用它们来写数据有一些困难。
 
除了用这两个类来操作document.xml外,我们还可以使用ZipOutputStream来向document.xml中写入数据。当然,我们还可以用其他的第三方工具来处理。但在本文中选用了ZipOutputStream,因为这个类的JDK的一部分。
 
    要想操作document.xml,必须得做以下几件事。首先,Java应用程序必须通过DOM的层次关系来枚举数据,先查找到"t"结点,然后在这个节点上插入自己的内容,并将其写回document.xml。为了更容易操作XML,开发都必须创建一个Transformer对象,并将数据包装在ByteArrayOutputStream中。


    一但上面的事情做完后,必须创建一个ZIP文件,这时我们将使用ZipOutputStream类。由于我们只是变化了document.xml中的内容,其他的xml文件将被直接复制进来,如字体、格式以及其他的东西。做到这一点也非常简单,只要使用ZipEntry对每一个实体进行扫描即可。代码如下:

@Test public void modifyDocumentAndSave() throws Exception { ZipFile docxFile =new ZipFile(new File("test.docx")); ZipEntry documentXML = docxFile.getEntry("word/document.xml"); InputStream documentXMLIS = docxFile.getInputStream(documentXML); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc = dbf.newDocumentBuilder().parse(documentXMLIS); Element docElement = doc.getDocumentElement(); assertEquals("w:document", docElement.getTagName()); Element bodyElement = (Element) docElement.getElementsByTagName("w:body").item(0); assertEquals("w:body", bodyElement.getTagName()); Element pElement = (Element) bodyElement.getElementsByTagName("w:p").item(0); assertEquals("w:p", pElement.getTagName()); Element rElement = (Element) pElement.getElementsByTagName("w:r").item(0); assertEquals("w:r", rElement.getTagName()); Element tElement = (Element) rElement.getElementsByTagName("w:t").item(0); assertEquals("w:t", tElement.getTagName()); assertEquals("这是第一个测试文档", tElement.getTextContent()); tElement.setTextContent( "这是第一个用Java写的测试文档"); Transformer t = TransformerFactory.newInstance().newTransformer(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); t.transform(new DOMSource(doc), new StreamResult(baos)); ZipOutputStream docxOutFile = new ZipOutputStream( new FileOutputStream("response.docx")); Enumeration entriesIter = docxFile.entries(); while (entriesIter.hasMoreElements()) { ZipEntry entry = entriesIter.nextElement(); if(entry.getName().equals("word/document.xml")) { byte[] data = baos.toByteArray(); docxOutFile.putNextEntry(new ZipEntry(entry.getName())); docxOutFile.write(data, 0, data.length); docxOutFile.closeEntry(); } else { InputStream incoming = docxFile.getInputStream(entry); byte[] data = new byte[1024 * 16]; int readCount = incoming.read(data, 0, data.length); docxOutFile.putNextEntry( new ZipEntry(entry.getName())); docxOutFile.write(data, 0, readCount); docxOutFile.closeEntry(); } } docxOutFile.close(); }
 3是上面代码写完后的word2007文档:


    上面的代码演示了如何使用Java的标准库来读写Word2007文档,同样,使用Java也可以读取Excel2007的相应的文档(.xlsx)。这样在使用Java生成Excel报表时就无需使用第三方的库,也不需要调用Excel2007。但在客户端必须安装Excel2007,这也许是一个不足的地方,但随着微软的Office2007系列软件的普及,我相信这将会使我们的报表家族更丰富多彩。
0
相关文章