Java可用org.apache.poi包来操作word文档。org.apache.poi包可于官网上下载,解压后各jar作用如下图所示:
可根据需求导入对应的jar。
一、HWPFDocument类的使用
用HWPFDocument类将数据写到指定的word文档中,基本思路是这样的:
- 首先,建立一个HWPFDocument类的实例,关联到一个临时的word文档;
- 然后,通过Range类实例,将数据写入这个word文档中;
- 接着,将这个临时的word文档通过write函数写入指定的word文档中。
- 最后,关闭所有资源。
下面详细说明各个步骤。
1.构造函数
这里要说明一下,经过试验,暂时还没找到直接在程序中新建一个word文档并读取的方法,只能先创建好temp.doc,然后在程序中读取。(用File-createNewFile和POIFSFileSystem.create创建出来的.doc文件,都不能被正确读取)
另外,其实选择哪种参数传入都是一样的,毕竟HWPFDocument关联的word文档是无法写入的,只是当作一个临时文件。所以,选择最为熟悉的InputStream较为合适。
参数1:InputStream。可将word文档用FileInputStream流读取,然后传入HWPFDocument类。主要用于读取word文档中的数据。
参数2:POIFSFileSystem。POIFSFileSystem的构造函数参数可以是(File,boolean)【这样的话file必须是已经存在的】,后者为false时可读写。这里可以写为
HWPFDocument doc = new HWPFDocument(new POIFSFileSystem(new File("temp.doc"),false));
2.Range类
(1)获取Range类实例。
HWPFDocument类中有一系列获取Range类实例以操作word文档的方法。比较常用的是getRange(),这个方法可以获取涵盖整个文档的范围,但不包括任何页眉和页脚。
Range range = doc.getRange();
此外,还有获取所有文本范围的getOverallRange()、获取所有文本框的getMainTextboxRange()等等,具体可以根据需求查阅文档。
(2)Range类操作word文档
Range类中有大量获取文档数据的方法,若有需要可以查阅文档。这里只说明与写入数据有关的方法。
1. insertBefore(String),将字符串插入到此range的开头。返回值类型:CharacterRun
2. insertAfter(String),将字符串插入到此range的结尾。返回值类型:CharacterRun
3. insertTableBefore(short列数, int行数),在此range的开头插入一个指定行列数的表。返回值类型:Table
4. text(),获取当前range的所有文本。返回值类型:String。虽然不是写入数据的方法,但是在调试过程中比较好用。
3.write方法
HWPFDocument类中的write方法有三种重载形式:(实际上可以理解为writeTo)
参数1:空参数。将本对象关联的word文档写入另一个打开的可写的POIFSFileSystem文件中。
参数2:File。将本对象关联的word文档写入指定的文件(newFile)中。如果该文件不存在,则创建;若存在,则覆盖。
参数3:OutputStream。将本对象关联的word文档写入指定的字节输出流中。
可以根据需求选择,但是最好还是选择OutputStream,因为输出流的操作空间更大。参数2的newFile不能续写,只能覆盖。
可以将其直接写入目标文件的输出流,也可以先写入一个字节数组输出流,在通过字节数组输出流写入到目标文件输出流中。
4.关闭资源
- 关闭doc.close();,也即是关闭doc所使用的资源”temp.doc”
- 关闭将数据写入指定word文档的输出流
二、代码示例
/**
* @description 将数据归档到.doc的word文档中。数据续写到原目标文件末尾。
* @param source
* 源文件(必须存在!)
* @param sourChs
* 读取源文件要用的编码,若传入null,则默认是GBK编码
* @param target
* 目标word文档(必须存在!)
*/
public static void storeDoc(File source, String sourChs, File target) {
/*
* 思路: 1.建立字符输入流,读取source中的数据。 2.在目标文件路径下new File:temp.doc
* 3.将目标文件重命名为temp.doc,并用HWPFDocument类关联(temp.doc)。
* 3.由temp.doc建立Range对象,写入source中的数据。 4.建立字节输出流,关联target。
* 5.将range中的数据写入关联target的字节输出流。
*/
if (!target.exists()) {
throw new RuntimeException("目标文件不存在!");
}
if (sourChs == null) {
sourChs = "GBK";
}
BufferedReader in = null;
HWPFDocument temp = null;
BufferedOutputStream out = null;
String path = target.getParent();
File tempDoc = new File(path, "temp.doc");
target.renameTo(tempDoc);
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(source), sourChs));
temp = new HWPFDocument(new BufferedInputStream(new FileInputStream(tempDoc)));
out = new BufferedOutputStream(new FileOutputStream(target));
Range range = temp.getRange();
String line = null;
range.insertAfter(getDate(12));
range.insertAfter("\r");
while ((line = in.readLine()) != null) {
range.insertAfter(line);
range.insertAfter("\r"); // word中\r是换行符
}
range.insertAfter("\r");
range.insertAfter("\r");
temp.write(out);
} catch (UnsupportedEncodingException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (FileNotFoundException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
if (temp != null) {
try {
temp.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
tempDoc.deleteOnExit();
}
}
三、调试记录
1.关于创建临时temp.doc的尝试
目的:在程序开始时创建一个temp.doc,程序结束后删除。
尝试1:用File-createNewFile创建出文件,然后用POIFSFileSystem构造函数打开这个文件, 然后用HWPFDocument关联到这个POIFSFileSystem类实例。
结果:org.apache.poi.EmptyFileException: The supplied file was empty (zero bytes long)。创建出的文件是0字节空文件,不能被POIFSFileSystem打开。
尝试2:用POIFSFileSystem.create(file)静态函数创建POIFSFileSystem实例,然后用 HWPFDocument关联。
结果:java.io.FileNotFoundException: no such entry: “WordDocument”, had: []。文件 是创建成功了,也用POIFSFileSystem成功载入,但是HWPFDocument无法接收这个参数。
结论:目前只能用Office软件创建的word文档才行。也就是说暂时还没找到直接在程序中新建 一个word文档的方法,只能先创建好temp.doc,然后在程序中读取。
2.无法写入temp.doc
描述:用doc.getRange()方法获取文件的整个范围,然后用range.insertAfter(String)方法 插入数据。编译运行没有任何异常,但是打开文件发现还是原样。
尝试:插入数据后用range.text()获取当前range的所有文本并显示在控制台上,发现数据的 确是成功插入到了range中,但是temp.doc依然没有任何变化。
猜测:可能是文件读取到HWPFDocument的方式不对,只读不可写入。
也有可能是range中的内容并不会改变.doc的内容,必须doc.write(*)写入到另一个文件中才 行。
尝试:通过各种方式(inputstream,poifsfilesystem,(poifs,readonly))载入temp.doc,结 果都是一样。于是开始尝试第二种猜测。
3.加入doc.write(*)方法后,运行报错,找不到需要的类文件(编译正常)。
详情:只加了这一句话,这句话报错:doc.write(out):java.lang.NoClassDefFoundError错 误
分析:NoClassDefFoundError发生在编译时对应的类可用,而运行时在Java的classpath路径 中,对应的类不可用导致的错误。
解决:要注意看报错的提示:
Exception in thread “main” java.lang.NoClassDefFoundError: org/apache/commons/collections4/bidimap/TreeBidiMap……
可以看出,是org.apache.commons.collections4包找不到导致的。导入这个包即可。
收获:使用外部jar包时,并不是只把所有代码里用到的类所在的jar包导入就万事大吉了,经 常是代码中用到的类里需要用到其他包中的类。如果在运行时报错,要注意看报错提示,根据 提示导入相关的包。
就这样一个简答的小bug卡了我半天,以后代码出错时不要只看错误类型,一定要细看报错的 描述。
4.无法写入目标文件
详情:续2,通过doc.write(out)方法将数据写到字节输出流,目标文件毫无变化。
尝试:用doc.write方法将数据写到字节数组,看看数据是否真的被输出了(如果是,就说明 是数据写入目标文件的过程中出了问题,而不是doc.write输出的问题)
结果:在输出的内容中找到了想要输出的数据。由此说明,前面的一切都没问题了,问题出在 把数据写入word文档上。
尝试:将目标文件删除,让程序创建出一个。结果,写入成功。那么问题来了:
5.目标文件无法续写
详情:由程序自己创建出的word文档可以写入,但已存在的word文档无法续写。即使是程序自 己创建出的word文档,也只能写入一次,无法续写。
分析:Range输出的数据是带有word文档的创建信息和格式数据的,这些内容对于已存在的 word文档不适用。
现在的情况是:用于创建HWPFDocument对象的temp.doc必须手动创建;目标文件必须由代码生 成,且生成后只能用代码写入一次。
将数据写入指定word文档的流程是:用getRange()方法获得临时文件数据(其实是为了获取 word文档的创建信息、格式数据),然后将源文件数据写入range,最后将range写入目标文件 的字节输出流。
既然如此,为何不直接将临时文件的来源设为目标文件呢?这样getRange所获取的range就能 同时包含目标文件中的原有文本数据,再在其后添加源文件中的内容,然后将整个range写入 由代码新创建的目标文件,不就是另一种意义上的续写吗?这样既避免了手动创建temp.doc, 又能实现续写,还能让避免产生垃圾文件(无意义的temp.doc)
解决:考虑到输入输出的冲突问题,先将目标文件重命名为temp.doc,然后由程序新创建出一 个空的目标文件。如果需要续写,就直接用getRange方法获取原来的所有数据;如果不需要续 写,就用Range(0,0,tempdoc)获取一个空的range,只带有格式和创建信息。将所有源数据写 入range后,用temp.write(out)将range中的数据写入新创建的目标文件。
6.如何不续写?
思路:由传入的参数,如果不续写,就用range.replaceText(“”,false),将整个range清空, 然后再往后插入需要的内容。
问题1:角标越界异常
分析:将整个range清空会导致无法插入,可以将整个range改为”tobedeleted",输出前再range.replaceText("{tobedeleted}”,”“)删掉标志即可。
问题2:做了上述操作后,仍然是续写,原range并没有清空。
分析:经过一系列测试,发现原因:用程序写入的文本,用range.text()读取不到,当然用 range.replaceText也无法操作了。而在程序写入后,随便手动在文件中写点东西然后保存, 再用range.text()就可以读取到了。
结论:这可能是包的固有bug之二,暂时无法解决。
四、其他
其实,poi包对于word文档来说,主要功能还是读取,写入功能很初级、不完善。poi只能操作最简单的word格式内容,当要求的样式复杂、文档长度较长时,用poi就较难完成要求。
这时,Jacob是一个更好的选择。Jacob能完整保持复杂的格式内容,操作也更为方便。但Jacob也有个缺点:只能在Windows平台下实现,无法在linux平台下实现。
此外,要生成标准格式的word文档,还有一种思路,是在另一篇博客上看到的:
先用office2003或者2007编辑好word的样式,然后另存为xml,将xml翻译为FreeMarker模板,最后用java来解析FreeMarker模板并输出Doc。经测试这样方式生成的word文档完全符合office标准,样式、内容控制非常便利,打印也不会变形,生成的文档和office中编辑文档完全一样。