項目背景:
當我們對一些word文檔(該文檔包含很多的標題比如 1.1 ,1.2 , 1.2.1.1, 1.2.2.3)當我們刪除其中一項或者幾項時,需要手動的對后續的進行補充。該功能主要是對標題進行自動的補充。
具體步驟:
導入依賴:
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.2</version></dependency>
官網網址:(覺得麻煩不可也許)
Apache Poi 官方鏈接 可以看官方文檔,其實更方便的可以直接導入依賴后,下載源代碼,直接看源碼的注釋也許
跑一下代碼熟悉一下
首先把下面的代碼復制到編譯器跑一下,看看是否正常運行,順便了解基本使用
package codeByZyc;import org.apache.poi.xwpf.usermodel.*;import java.io.FileInputStream;
import java.io.IOException;public class rederWordTest {public static void main(String[] args) throws IOException {FileInputStream file = new FileInputStream("輸入你的word文檔地址");XWPFDocument document = new XWPFDocument(file);// 獲取word中的段落,無法獲取表格System.out.println("獲取到的段落");for (XWPFParagraph paragraph : document.getParagraphs()) {System.out.println(paragraph.getText());}// 這是只能獲取word中的表格System.out.println("獲取到的表給內容");for (XWPFTable table : document.getTables()) {for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {System.out.print(cell.getText() + " \t");}System.out.println();}}document.close();file.close();}}
api說明
通過上面的代碼,我們可以知道poi是用過XWPFDocument這個類去獲取的word內容。 下面從段落和表格兩部分進行代碼說明
段落api說明
對于word中的段落他的操作如下:
XWPFDocument(最大的模塊).getParagraphs->Paragraph(負責每一個段落).getRuns->Run(這是最小處理的元素)
下面是進行調式的圖片,配合圖片更好理解:
XWPFDocument:就是最大的那個模塊 信息很大
Paragraphs:這是所有的段落集合
Paragraph:存放的就是每一段了 里面的runs 是按照格式進行分割的
Runs run的集合
具體看下面
Run(最基礎的元素)
這是最重要的那個元素,他是構成所有段落和表格的最小單位。
一個段落他是如何劃分成幾個run的?
他是按照每個字的前后 是否同一個格式(字體,加粗否,大小等)必須完全一樣才能分到一個run里面。具體分割還得調式看
下圖是一些run的切割:
這個就是特殊的符號會被劃開
這個更離譜 注意 1.3.1后面的空格沒有 ,這個的空格是被劃開的。是因為空格的格式和標題不一樣
段落代碼(直接看結尾的整合代碼,寫得更詳細注釋更全面):里面有注釋 應該算比較清楚了 有問題可以下面評論
文件路徑自己填寫一下
package codeByZyc;import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class word {// 匹配數字開頭,小數點隔開 空格后接內容static Pattern headerPattern = Pattern.compile("^(\\d+(?:\\.\\d+)*)(\\s+.*)");//用于計算層級static ArrayList<Integer> headerlist= new ArrayList();//用于存放段落開頭有單個特殊符號的static ArrayList<String> kaitou =new ArrayList<>();// 匹配中文括號列表項(如 (1))
// static Pattern listItemPattern = Pattern.compile("^((\\d+))(.*)$");public static void main(String[] args) {String path="文件路徑";//初始化計算器for (int i = 0; i < 10; i++) {headerlist.add(0);}//初始化開頭特殊字符kaitou.add("★");kaitou.add("*");//執行代碼updateWord(path);}public static void updateWord(String path){try {// 1. 讀取 .docx 文件FileInputStream fis = new FileInputStream(path);XWPFDocument document = new XWPFDocument(fis);//獲取word每一個段落元素(不包含表格的)List<XWPFParagraph> paragraphs = document.getParagraphs();// //一個段落一個段落的處理for (int i = 0; i < paragraphs.size(); i++) {wordDuanluo(paragraphs.get(i));}//開始村存回去FileOutputStream out = new FileOutputStream("文件路徑代碼生成.docx");document.write(out);// 4. 關閉流document.close();fis.close();out.close();} catch (Exception e) {e.printStackTrace();}}//處理段落private static void wordDuanluo(XWPFParagraph paragraph) {//每個段落下面還有更小的元素,叫run 所以處理runList<XWPFRun> runs = paragraph.getRuns();//直接匹配第一個 是那就是標題 不是那就是正文if (runs.size()==1){wordRun(runs.get(0));}else if (runs.size()==2){//只有兩個識別開頭是不是包含特殊 時走一個通道 不是走二通道//標志是否匹配boolean flagkaitou= false;for (int i = 0; i < kaitou.size(); i++) {if (kaitou.get(i).equals(runs.get(0).text())){flagkaitou=true;break;}}if (flagkaitou==true){//證明開頭匹配走一同到wordRun(runs.get(1));}else {//不匹配wordRun(runs.get(0),runs.get(1));}}else if (runs.size()>2){//數量在三個及以上//標志是否匹配boolean flagkaitou= false;for (int i = 0; i < kaitou.size(); i++) {if (kaitou.get(i).equals(runs.get(0).text())){flagkaitou=true;break;}}if (flagkaitou==true){//證明開頭匹配wordRun(runs.get(1),runs.get(2));}else {//不匹配wordRun(runs.get(0),runs.get(1));}}}// 處理非常特殊的 標題后面跟的空格字體和標題不一樣分開了 進行拼接private static void wordRun(XWPFRun run1, XWPFRun run2) {//run是作為操作word的非常小的元素了,他會把每個段落換分成幾個run組成 具體劃分規則我也不知道 (我看案列 是按格式進行 格式一樣的情況大概率是在一起)//采用正則表達式進行匹配Matcher matcher = headerPattern.matcher(run1.text()+run2.text());if (matcher.find()) {//匹配成功//保存序號后面的文章用于拼接String contex = matcher.group(2);//按照.進行切割String[] originalParts = matcher.group(1).split("\\.");//根據長度判斷層級 一個就一級int length = originalParts.length;//文檔按照順序1 1.1 1.1.1//將子層級覆蓋掉for (int i = length; i < headerlist.size(); i++) {headerlist.set(i,0);}headerlist.set(length - 1, (headerlist.get(length - 1) + 1));StringBuffer result = new StringBuffer();//拼接正確的序號for (int i = 0; i < length; i++) {result.append(headerlist.get(i));result.append(".");}//多出一個. 進行刪除result.deleteCharAt(result.length()-1);//序號放到run1 空格+正文放到run2run1.setText(result.toString(),0);run2.setText(contex,0);}}private static void wordRun(XWPFRun run) {//run是作為操作word的非常小的元素了,他會把每個段落換分成幾個run組成 具體劃分規則我也不知道 (我看案列 是按格式進行 格式一樣的情況大概率是在一起)//采用正則表達式進行匹配Matcher matcher = headerPattern.matcher(run.text());if (matcher.find()) {//匹配成功//保存序號后面的文章用于拼接String contex = matcher.group(2);//按照.進行切割String[] originalParts = matcher.group(1).split("\\.");//根據長度判斷層級 一個就一級int length = originalParts.length;//文檔按照順序1 1.1 1.1.1//將子層級覆蓋掉for (int i = length; i < headerlist.size(); i++) {headerlist.set(i,0);}headerlist.set(length - 1, (headerlist.get(length - 1) + 1));StringBuffer result = new StringBuffer();//拼接正確的序號for (int i = 0; i < length; i++) {result.append(headerlist.get(i));result.append(".");}//多出一個. 進行刪除result.deleteCharAt(result.length()-1);result.append(contex);//將內容替換到runrun.setText(result.toString(),0);}}}
表格api說明 基本上和段落一樣 有一點點不一樣
XWPFDocument(最大的模塊).getTables->XWPFTable(負責每一個表格).getRows->Row(代表一行).getTableCells->XWPFTableCell(每一格子)[由于cell無法更改具體看下圖] 需要深入到 run
注意,我原來以為在cell就可以更改他的文本
但是看源碼可以知道 他是在尾部追加并不是覆蓋,所以還是只能追到run去覆蓋。
表格代碼:(直接看結尾的整合代碼,寫得更詳細注釋更全面)
import org.apache.poi.xwpf.usermodel.*;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class TestWord {// 匹配表格的 結尾開頭是數字 空格幾個都行static Pattern tablePattern = Pattern.compile("^(\\d+)(\\s*)$");static Integer tableCount=0;//用于存放段落開頭有單個特殊符號的static ArrayList<String> kaitou =new ArrayList<>();public static void main(String[] args) {String path="測試table.docx";//初始化開頭特殊字符kaitou.add("★");kaitou.add("*");readWord(path);}public static void readWord(String filePath){try {// 1. 讀取 .docx 文件FileInputStream fis = new FileInputStream(filePath);XWPFDocument document = new XWPFDocument(fis);//直接一層一層找一下找到 run 試過在cell進行修改 但是cell的修改是 原來的基礎上進行了新的增加 不能進行替換 直接找到run進行替換for (XWPFTable table : document.getTables()) {//一個table 清除一次計數器tableCount=0;for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph paragraph : cell.getParagraphs()) {List<XWPFRun> runs = paragraph.getRuns();//直接匹配第一行if (runs.size()==1){tableup(runs.get(0));}else if (runs.size()>1){//這邊就是 考慮到前面有個特殊符號的情況 就需要判斷了XWPFRun run1 = runs.get(0);//第一個不是特殊那就直接走第一通道boolean flag=false;for (int i = 0; i < kaitou.size(); i++) {if (kaitou.get(i).equals(run1.text())){flag=true;break;}}if (flag){tableup(runs.get(1));}else {tableup(run1);}}}}}}System.out.println("測試完成");FileOutputStream out = new FileOutputStream("生成的table.docx");document.write(out);// 4. 關閉流document.close();fis.close();out.close();} catch (Exception e) {e.printStackTrace();}}public static void tableup(XWPFRun run){Matcher matcher = tablePattern.matcher(run.text());if (matcher.find()){//匹配成功 開始更換層級String content = matcher.group(2);String originalParts = matcher.group(1);//開始覆蓋掉序號 并拼接后面的內容tableCount++;StringBuffer result=new StringBuffer();result.append(tableCount+content);//寫入run.setText(result.toString(),0);}}
}
總結
整合代碼:
package codeByZyc;import org.apache.poi.xwpf.usermodel.*;import java.io.FileInputStream;import java.io.FileOutputStream;import java.util.ArrayList;import java.util.List;import java.util.regex.Matcher;import java.util.regex.Pattern;public class Upword {// 匹配數字開頭,小數點隔開 空格后接內容static Pattern headerPattern = Pattern.compile("^(\\D?)(\\d+(?:\\.\\d+)*)(\\s+)");//用于段落計算層級static ArrayList<Integer> headerlist= new ArrayList();//用于存放段落開頭有單個特殊符號的// 匹配表格的 結尾開頭是數字 空格幾個都行static Pattern tablePattern = Pattern.compile("^(\\D?)(\\d+)(\\s*)$");//匹配表格的層級static Integer tableCount=0;// 匹配中文括號列表項(如 (1))
// static Pattern listItemPattern = Pattern.compile("^((\\d+))(.*)$");public static void main(String[] args) {String path="測試專用刪除部分標題.docx";//初始化段落層級計算器for (int i = 0; i < 10; i++) {headerlist.add(0);}//執行代碼updateWord(path);}public static void updateWord(String path){try {// 1. 讀取 .docx 文件FileInputStream fis = new FileInputStream(path);XWPFDocument document = new XWPFDocument(fis);//獲取word每一個段落元素(不包含表格的)List<XWPFParagraph> paragraphs = document.getParagraphs();// //一個段落一個段落的處理for (int i = 0; i < paragraphs.size(); i++) {wordDuanluo(paragraphs.get(i));}//獲取word的表格
// 直接一層一層找一下找到 run 試過在cell進行修改 但是cell的修改是 原來的基礎上進行了新的增加 不能進行替換 直接找到run進行替換for (XWPFTable table : document.getTables()) {wordtable(table);}//開始村存回去FileOutputStream out = new FileOutputStream("代碼生成.docx");document.write(out);// 4. 關閉流document.close();fis.close();out.close();} catch (Exception e) {e.printStackTrace();}}//處理表格private static void wordtable(XWPFTable table){//一個table 清除一次計數器tableCount=0;for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph paragraph : cell.getParagraphs()) {List<XWPFRun> runs = paragraph.getRuns();//直接匹配第一行if (runs.size()==1){wordTableRun(runs.get(0));}else if (runs.size()==2){wordTableRun(runs.get(0),runs.get(1));}}}}}//處理段落private static void wordDuanluo(XWPFParagraph paragraph) {//每個段落下面還有更小的元素,叫run 所以處理runList<XWPFRun> runs = paragraph.getRuns();/* 這里需要注意一下因為我的測試文檔的需求標題是這樣的 *1.2.1 背景展望 像這個 要是格式不一樣會被劃分成 三個部分 * 1.2.1 還有一個空格 。 但是要是格式一樣 就直接化成一個了主要是我的測試文檔是有些一樣 有些不一樣所以需要考慮的比較多。*/if (runs.size()==1){//針對只劃分一個的 那沒得說 直接走第一個wordRun(runs.get(0));}else if (runs.size()==2){//這就是兩個 那可能是兩種情況: *1.2.1 空格 或者 * 1.2.1空格 就是兩種了wordRun(runs.get(0),runs.get(1));}else if (runs.size()>2){//數量在三個及以上//這種 就是我上面說的 三個格式都不一樣wordRun(runs.get(0),runs.get(1),runs.get(2));}}private static void wordRun(XWPFRun run) {//run是作為操作word的非常小的元素了,他會把每個段落換分成幾個run組成 具體劃分規則我也不知道 (我看案列 是按格式進行 格式一樣的情況大概率是在一起)//采用正則表達式進行匹配Matcher matcher = headerPattern.matcher(run.text());if (matcher.find()) {//匹配成功String oldxuaho=matcher.group(2);//按照.進行切割String[] originalParts =oldxuaho.split("\\.");//根據長度判斷層級 一個就一級int length = originalParts.length;//文檔按照順序1 1.1 1.1.1//將子層級覆蓋掉for (int i = length; i < headerlist.size(); i++) {headerlist.set(i,0);}headerlist.set(length - 1, (headerlist.get(length - 1) + 1));StringBuffer result = new StringBuffer();//拼接正確的序號//把序號前面的放進來result.append(matcher.group(1));for (int i = 0; i < length; i++) {result.append(headerlist.get(i));result.append(".");}//多出一個. 進行刪除result.deleteCharAt(result.length()-1);//替換if(run.text().contains(oldxuaho)){//進行替換String s = run.text();s.replace(oldxuaho,result.toString());run.setText(s,0);}}}// 處理非常特殊的 標題后面跟的空格字體和標題不一樣分開了 進行拼接private static void wordRun(XWPFRun run1, XWPFRun run2) {//run是作為操作word的非常小的元素了,他會把每個段落換分成幾個run組成 具體劃分規則我也不知道 (我看案列 是按格式進行 格式一樣的情況大概率是在一起)//采用正則表達式進行匹配Matcher matcher = headerPattern.matcher(run1.text()+run2.text());if (matcher.find()) {//匹配成功String oldxuaho=matcher.group(2);//按照.進行切割String[] originalParts =oldxuaho.split("\\.");//根據長度判斷層級 一個就一級int length = originalParts.length;//文檔按照順序1 1.1 1.1.1//將子層級覆蓋掉for (int i = length; i < headerlist.size(); i++) {headerlist.set(i,0);}headerlist.set(length - 1, (headerlist.get(length - 1) + 1));StringBuffer result = new StringBuffer();//把前面的放進來result.append(matcher.group(1));//拼接正確的序號for (int i = 0; i < length; i++) {result.append(headerlist.get(i));result.append(".");}//多出一個. 進行刪除result.deleteCharAt(result.length()-1);//查找替換 具體思路看一看下面的if(run1.text().contains(oldxuaho)){//進行替換String s = run1.text();s.replace(oldxuaho,result.toString());run1.setText(s,0);}else if (run2.text().contains(oldxuaho)){String s = run2.text();s.replace(oldxuaho,result.toString());run2.setText(s,0);}}}private static void wordRun(XWPFRun run1, XWPFRun run2,XWPFRun run3) {//run是作為操作word的非常小的元素了,他會把每個段落換分成幾個run組成 具體劃分規則我也不知道 (我看案列 是按格式進行 格式一樣的情況大概率是在一起)//采用正則表達式進行匹配Matcher matcher = headerPattern.matcher(run1.text()+run2.text()+run3.text());if (matcher.find()) {//匹配成功String oldxuaho=matcher.group(2);//按照.進行切割String[] originalParts = oldxuaho.split("\\.");//根據長度判斷層級 一個就一級int length = originalParts.length;//文檔按照順序1 1.1 1.1.1//將子層級覆蓋掉for (int i = length; i < headerlist.size(); i++) {headerlist.set(i,0);}headerlist.set(length - 1, (headerlist.get(length - 1) + 1));StringBuffer result = new StringBuffer();//直接將他們拼起來result.append(matcher.group(1));//拼接正確的序號for (int i = 0; i < length; i++) {result.append(headerlist.get(i));result.append(".");}//多出一個. 進行刪除result.deleteCharAt(result.length()-1);/*下面就需要考慮 run1 2 3 如何劃分了因為我的matcher.group 把他們三個合并的 拆分成了 三部分 (數字標題前面的部分) (數字標題) 空格內容(內容可能有可能無)* 情況1: 最開始說到的 * 1.2.1 空格內容(內容可能有可能無)* 情況2: 也有可能是: *1.2.1 空格內容(內容可能有可能無) 內容情況3: 也有可能是: * 1.2.1空格內容(內容可能有可能無) 內容情況4: *1.2.1空格內容(內容可能有可能無) 內容 內容解決思路: group(2)匹配的序號 只需要 挨個便利 看看 哪個run包含 進行替換就行其他的保持不變。* *///查找if(run1.text().contains(oldxuaho)){//進行替換String s = run1.text();s.replace(oldxuaho,result.toString());run1.setText(s,0);}else if (run2.text().contains(oldxuaho)){String s = run2.text();s.replace(oldxuaho,result.toString());run2.setText(s,0);}else {String s = run3.text();s.replace(oldxuaho,result.toString());run3.setText(s,0);}}}//處理表格的單元格private static void wordTableRun(XWPFRun run){Matcher matcher = tablePattern.matcher(run.text());if (matcher.find()){//匹配成功 開始更換層級String oldxuhao = matcher.group(2);//開始覆蓋掉序號 并拼接后面的內容tableCount++;String s = run.text();s.replace(oldxuhao,tableCount.toString());//寫入run.setText(s,0);}}private static void wordTableRun(XWPFRun run1,XWPFRun run2){Matcher matcher = tablePattern.matcher(run1.text()+run2.text());if (matcher.find()){String oldxuhao=matcher.group(2);//開始覆蓋掉序號 并拼接后面的內容tableCount++;String s;if (run1.text().contains(oldxuhao)){s=run1.text();s.replace(oldxuhao,tableCount.toString());run1.setText(s,0);}else if (run2.text().contains(oldxuhao)){s=run2.text();s.replace(oldxuhao,tableCount.toString());run2.setText(s,0);}}}}
難點:
我感覺最大的難點就是對Api的熟悉,需要看看源碼或者文檔。以及利用正則表達式對標題進行匹配