1. 概述
本教程将演示如何在Word文档的不同位置替换指定模式。我们将同时处理*.doc和.docx*两种格式的文件。
2. Apache POI库
Apache POI库提供了Java API,用于操作Microsoft Office应用程序使用的各种文件格式,例如Excel电子表格、Word文档和PowerPoint演示文稿。它支持以编程方式读取、写入和修改此类文件。
要编辑*.docx文件,我们需要在pom.xml中添加最新版本的poi-ooxml*依赖:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
此外,我们还需要最新版本的*poi-scratchpad来处理.doc*文件:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.5</version>
</dependency>
3. 文件处理
我们需要创建示例文件、读取文件、替换文本并保存结果文件。下面先讨论所有与文件处理相关的内容。
3.1 示例文件
创建一个Word文档。我们将把文档中的Baeldung替换为Hello。 因此,我们将在文件的不同位置(特别是表格、不同文档段落和节)写入Baeldung,并使用多种格式样式,包括单词内部有格式变化的情况。我们将使用同一个文档,分别保存为*.doc和.docx*格式:
3.2 读取输入文件
首先需要读取文件。我们将文件放在resources文件夹中,使其在类路径中可用。 这样我们可以获取InputStream。对于*.doc文档,我们将基于此InputStream创建POIFSFileSystem对象,最后获取要修改的HWPFDocument对象。使用try-with-resources确保InputStream和POIFSFileSystem自动关闭。但由于要修改HWPFDocument*,我们将手动关闭它:
public void replaceText() throws IOException {
String filePath = getClass().getClassLoader()
.getResource("baeldung.doc")
.getPath();
try (InputStream inputStream = new FileInputStream(filePath); POIFSFileSystem fileSystem = new POIFSFileSystem(inputStream)) {
HWPFDocument doc = new HWPFDocument(fileSystem);
// 替换文档中的文本并保存更改
doc.close();
}
}
处理*.docx文档时更简单,可以直接从InputStream派生XWPFDocument*对象:
public void replaceText() throws IOException {
String filePath = getClass().getClassLoader()
.getResource("baeldung.docx")
.getPath();
try (InputStream inputStream = new FileInputStream(filePath)) {
XWPFDocument doc = new XWPFDocument(inputStream);
// 替换文档中的文本并保存更改
doc.close();
}
}
3.3 写入输出文件
我们将输出文档写入同一文件。修改后的文件将位于target文件夹中。*HWPFDocument和*XWPFDocument类都提供了write()方法,用于将文档写入OutputStream。 例如,对于*.doc*文档:
private void saveFile(String filePath, HWPFDocument doc) throws IOException {
try (FileOutputStream out = new FileOutputStream(filePath)) {
doc.write(out);
}
}
4. 替换*.docx*文档中的文本
尝试替换*.docx文档中的Baeldung*,并分析过程中遇到的挑战。
4.1 朴素实现
已将文档解析为XWPFDocument对象。XWPFDocument由多个段落组成。文件核心部分的段落可直接访问,但表格中的段落需要遍历所有行和单元格。暂不讨论*replaceTextInParagraph()*方法的具体实现,先展示如何将其应用到所有段落:
private XWPFDocument replaceText(XWPFDocument doc, String originalText, String updatedText) {
replaceTextInParagraphs(doc.getParagraphs(), originalText, updatedText);
for (XWPFTable tbl : doc.getTables()) {
for (XWPFTableRow row : tbl.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
replaceTextInParagraphs(cell.getParagraphs(), originalText, updatedText);
}
}
}
return doc;
}
private void replaceTextInParagraphs(List<XWPFParagraph> paragraphs, String originalText, String updatedText) {
paragraphs.forEach(paragraph -> replaceTextInParagraph(paragraph, originalText, updatedText));
}
在Apache POI中,段落被划分为XWPFRun对象。作为初步尝试,遍历所有run:如果检测到目标文本,就更新run的内容:
private void replaceTextInParagraph(XWPFParagraph paragraph, String originalText, String updatedText) {
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
String text = run.getText(0);
if (text != null && text.contains(originalText)) {
String updatedRunText = text.replace(originalText, updatedText);
run.setText(updatedRunText, 0);
}
}
}
最后,更新*replaceText()*方法包含所有步骤:
public void replaceText() throws IOException {
String filePath = getClass().getClassLoader()
.getResource("baeldung-copy.docx")
.getPath();
try (InputStream inputStream = new FileInputStream(filePath)) {
XWPFDocument doc = new XWPFDocument(inputStream);
doc = replaceText(doc, "Baeldung", "Hello");
saveFile(filePath, doc);
doc.close();
}
}
通过单元测试运行此代码,查看更新后的文档截图:
4.2 局限性
如截图所示,大部分Baeldung已被替换为Hello,但仍有两处未被替换。
深入理解XWPFRun:每个run代表具有相同格式属性的连续文本序列。 格式属性包括字体、大小、颜色、粗体、斜体、下划线等。每当格式变化时,就会创建新的run。因此表格中包含多种格式的Baeldung未被替换——其内容分散在多个run中。
底部蓝色Baeldung也未被替换。实际上,Apache POI不保证相同格式的字符一定在同一个run中。简单来说,朴素实现仅适用于最简单场景。✅ 在简单场景下使用此方案是合理的,因为它不涉及复杂决策。但若遇到此限制,则需要其他解决方案。
4.3 处理跨多个run的文本
为简化实现,我们做以下假设:当段落中包含Baeldung时,可以接受丢失该段落的格式。 因此,我们可以移除段落中的所有run,并用单个新run替换。重写*replaceTextInParagraph()*方法:
private void replaceTextInParagraph(XWPFParagraph paragraph, String originalText, String updatedText) {
String paragraphText = paragraph.getParagraphText();
if (paragraphText.contains(originalText)) {
String updatedParagraphText = paragraphText.replace(originalText, updatedText);
while (paragraph.getRuns().size() > 0) {
paragraph.removeRun(0);
}
XWPFRun newRun = paragraph.createRun();
newRun.setText(updatedParagraphText);
}
}
查看结果文件:
如预期,所有Baeldung都被替换。但大部分格式丢失了(最后一种格式未丢失,似乎Apache POI对其处理方式不同)。
⚠️ 根据实际需求,也可以选择保留原始段落的部分格式。此时需要遍历所有run,按需保留或更新格式属性。
5. 替换*.doc*文档中的文本
处理*.doc文件要简单得多。我们可以直接获取整个文档的Range对象。**然后通过其replaceText()方法修改范围内容:*
private HWPFDocument replaceText(HWPFDocument doc, String originalText, String updatedText) {
Range range = doc.getRange();
range.replaceText(originalText, updatedText);
return doc;
}
运行此代码得到更新后的文件:
可见替换发生在整个文件中。对于跨多个run的文本,默认行为是保留第一个run的格式。
6. 总结
本文介绍了如何在Word文档中替换文本模式。在*.doc文档中操作非常直接,但在.docx*中,简单实现会遇到限制。我们通过简化假设展示了如何克服此限制。
完整代码可在GitHub获取。