用 Java 替換文檔模板中的變量
一、概述
在本教程中,我們將替換 Word 文檔不同位置的模式。我們將使用.doc
和.docx
文件。
2. Apache POI 庫
Apache POI 庫提供了 Java API,用於處理 Microsoft Office 應用程序使用的各種文件格式,例如 Excel 電子表格、Word 文檔和 PowerPoint 演示文稿。它允許以編程方式讀取、寫入和修改此類文件。
要編輯.docx
文件,我們會將最新版本的[poi-ooxml](https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml)
添加到我們的pom.xml
中:
<dependency>
<groupId>org.apache.poi</groupId>
.<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
此外,我們還需要最新版本的[poi-scratchpad](https://mvnrepository.com/artifact/org.apache.poi/poi-scratchpad)
來處理.doc
文件:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
</dependency>
3. 文件處理
我們要創建示例文件,讀取它們,替換文件中的一些文本,然後寫入結果文件。讓我們先談談與文件處理有關的一切。
3.1.示例文件
讓我們創建一個 Word 文檔。我們要將其中的Baeldung
一詞替換為Hello
一詞。因此,我們將在文件的多個位置編寫Baeldung
,尤其是在表格、各種文檔部分和段落中。我們還想使用不同的格式樣式,包括在單詞中出現一次格式更改。我們將使用同一個文檔,一次保存為.doc
文件,一次保存為.docx
:
3.2.讀取輸入文件
首先,我們需要讀取文件。我們將把它放在資源文件夾中,以使其在類路徑中可用。這樣,我們將得到一個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);
// replace text in doc and save changes
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);
// replace text in doc and save changes
doc.close();
}
}
3.3.編寫輸出文件
我們會將輸出文檔寫入同一個文件。結果,修改後的文件將位於target
文件夾中。 HWPFDocument
和XWPFDocument
類都公開了一個write()
方法來將文檔寫入OuputStream
。例如,對於.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
對象。作為第一步,讓我們嘗試遍歷所有運行:如果我們在運行中檢測到要替換的文本,我們將更新運行的內容:
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
一詞。但是,我們可以看到剩下的兩個Baeldung
。
現在讓我們更深入地了解XWPFRun
是什麼。每次運行都代表具有一組通用格式屬性的連續文本序列。格式屬性包括字體樣式、大小、顏色、粗體、斜體、下劃線等。只要有格式更改,就會有新的運行。這就是為什麼不替換錶中出現的各種格式的原因:它的內容分佈在多個運行中。
然而,底部的藍色Baeldung
事件也沒有被替換。事實上,Apache POI 不保證具有相同格式屬性的字符是同一運行的一部分。簡而言之,對於最簡單的情況,樸素的實現已經足夠好了。在這種情況下值得使用此解決方案,因為它並不意味著任何復雜的決定。但是,如果我們遇到這種限制,我們將需要轉向另一種解決方案。
4.3.處理跨多個字符運行的文本傳播
為了簡單起見,我們將做出以下假設:當我們在其中找到單詞Baeldung
時,我們可以丟失段落的格式。因此,我們可以刪除段落中所有現有的運行並用一個新的替換它們。讓我們重寫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);
}
}
讓我們看一下結果文件:
正如預期的那樣,現在每個事件都被替換了。但是,大多數格式都會丟失。最後的格式不會丟失。在這種情況下,Apache POI 似乎以不同方式處理格式化屬性。
最後,請注意,根據我們的用例,我們還可以決定保留原始段落的某些格式。然後我們需要遍歷所有運行並根據需要保留或更新屬性。
5. 替換.doc
文檔中的文本
對於doc
文件,事情要簡單得多。我們確實可以訪問整個文檔上的Range
對象。然後我們可以通過其replaceText()
方法修改範圍的內容:
private HWPFDocument replaceText(HWPFDocument doc, String originalText, String updatedText) {
Range range = doc.getRange();
range.replaceText(originalText, updatedText);
return doc;
}
運行此代碼會生成以下更新文件:
正如我們所見,替換發生在整個文件中。我們還可以注意到,分佈在多個運行中的文本的默認行為是保持第一次運行的格式。
六,結論
在本文中,我們替換了 Word 文檔中的模式。在.doc
文檔中,它非常簡單。但是,在.docx
中,我們在易於實施的過程中遇到了一些限制。我們展示了一個通過簡化假設來克服此限制的示例。
與往常一樣,代碼在 GitHub 上可用。