業務常需從 Word 試卷/合同/報告中提取結構化數據。Apache POI 支持 .doc (HWPF) 與 .docx (XWPF)。本文實現 Word → HTML → 模型化 Map/List 兩階段解析,提取標題層級、題目、選項、分值等教學結構。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
</dependency>
<!-- .docx 需額外 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
HWPFDocument:文檔入口Range:文檔文本範圍,按字符/段落/表格迭代CharacterRun:同樣式連續字符片段 (字體、大小、顏色、粗斜)Paragraph:段落,含文本、樣式、縮進Table/TableRow/TableCell:表格結構PicturesTable:圖片提取public class WordToHtml {
// 關鍵 ASCII 碼
private static final short ENTER = 13, SPACE = 32, TAB = 9;
public static String convert(String inputPath, String outputHtmlPath) throws Exception {
try (FileInputStream fis = new FileInputStream(inputPath)) {
HWPFDocument doc = new HWPFDocument(fis);
Range range = doc.getRange();
PicturesTable pics = doc.getPicturesTable();
StringBuilder html = new StringBuilder("")
.append(doc.getSummaryInformation().getTitle())
.append(" ");
// 1. 先處理表格 (記錄位置偏移,避免字符流重複處理)
TableIterator tit = new TableIterator(range);
Map tableHtmlMap = new HashMap<>();
int tblIdx = 0;
while (tit.hasNext()) {
Table table = (Table) tit.next();
int start = table.getStartOffset();
int end = table.getEndOffset();
String tblHtml = buildTableHtml(table);
tableHtmlMap.put(start, tblHtml + "|" + end); // 存 HTML + 結束偏移
tblIdx++;
}
// 2. 字符級流式處理非表格內容
StringBuilder tempRun = new StringBuilder();
for (int i = 0; i < doc.characterLength() - 1; i++) {
Range charRange = new Range(i, i + 1, doc);
CharacterRun cr = charRange.getCharacterRun(0);
// 檢查是否進入表格區域
if (tableHtmlMap.containsKey(i)) {
html.append(tempRun).append(tableHtmlMap.get(i).split("\\|")[0]);
i = Integer.parseInt(tableHtmlMap.get(i).split("\\|")[1]) - 1;
tempRun.setLength(0);
continue;
}
// 圖片處理
if (pics.hasPicture(cr)) {
html.append(tempRun).append(extractPicture(pics, cr));
tempRun.setLength(0);
continue;
}
// 字符歸一化
char c = cr.text().charAt(0);
if (c == SPACE) tempRun.append(" ");
else if (c == TAB) tempRun.append(" ");
else {
// 樣式邊界檢測:與下一字符樣式對比
Range nextRange = new Range(i + 1, i + 2, doc);
CharacterRun crNext = nextRange.getCharacterRun(0);
boolean sameStyle = sameStyle(cr, crNext);
if (sameStyle && c != ENTER) {
tempRun.append(cr.text());
} else {
// 輸出累積 run 並帶上樣式 span
String style = buildStyleSpan(cr);
html.append(style).append(tempRun).append(cr.text()).append("");
tempRun.setLength(0);
}
}
if (c == ENTER) html.append("
");
}
html.append(tempRun).append("");
// 寫入文件 (可選)
Files.write(Paths.get(outputHtmlPath), html.toString().getBytes(StandardCharsets.UTF_8));
return html.toString();
}
}
// 樣式對比、構建、圖片提取、表格構建等輔助方法省略...
}
public List
[
{
"title": "一、單選題",
"subQuestions": [
{
"title": "1、Java 基礎類是? (5分)",
"score": 5,
"options": [
{"key": "A", "content": "Object"},
{"key": "B", "content": "String"},
{"key": "C", "content": "System"},
{"key": "D", "content": "Math"}
]
}
]
}
]
XWPFDocument,API 相似但包名不同 (org.apache.poi.xwpf)docx4j 或商業庫 (Aspose.Words)SAX 流式解析 word/document.xml 避免全量 DOMCharacterEncoding 或用 ICU4J 探測Exam/Question/Option,接入業務服務層此方案已支撐在線考試系統萬級試卷導入,準確率 95%+,人工複核成本降低 80%。