如何使用Apache POI获取Word文档样式及其属性以实现高质量DOCX转HTML
我完全懂你的困境——要在没有任何Office套件的生产服务器上,把DOCX转换成和Word原生导出一模一样的HTML,试了DocConv、docx4j等一堆工具效果都达不到要求,想用POI却卡在了获取完整样式属性这一步,尤其是那些定义在样式本身而非段落或Run对象里的格式(比如样式自带的加粗效果)。
其实用POI完全可以拿到Word文档里的所有样式及其完整属性,核心是要直接操作POI的样式表对象,还要处理样式的继承关系(因为Word里很多样式是基于基础样式扩展的,比如Normal样式)。下面我一步步给你讲怎么做,再附上修改后的代码示例:
第一步:获取文档的完整样式表
首先,从XWPFDocument中获取XWPFStyles对象,这是文档所有样式的容器,包括段落样式、字符样式、表格样式等。你可以通过xdoc.getStyles()直接拿到它。
第二步:遍历样式并提取属性
每个样式在POI里对应XWPFStyle对象,你需要区分段落样式(XWPFParagraphStyle)和字符样式(XWPFCharStyle),因为Word里这两种样式的属性逻辑是分开的。要获取样式的底层完整属性,得通过XWPFStyle.getCTStyle()拿到CTStyle对象,从这里可以读取所有定义的格式,包括:
- 样式ID/名称
- 段落对齐方式
- 字体族、字号
- 加粗、倾斜、下划线等字符格式
- 段落缩进、行间距等段落格式
还要注意样式继承:很多样式会基于父样式(比如Normal),所以需要递归获取父样式的属性,合并到当前样式中,避免遗漏继承的格式。
第三步:把样式转换成CSS规则
在生成HTML的<style>标签里,把每个Word样式转换成对应的CSS类,这样后面段落/文本引用class时,就能自动应用样式里的所有格式,包括那些定义在样式而非段落/Run里的属性(比如样式自带的加粗)。
修改后的完整代码示例
下面是在你原有代码基础上修改后的版本,加入了获取样式并生成CSS的逻辑,解决了样式属性缺失的问题:
import org.apache.poi.xwpf.usermodel.*; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.xwpf.model.XWPFStyles; import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.List; public class Start2 { public static void main(String[] args) throws InvalidFormatException, IOException { String inputDoc = "D:/TestOutput/OriginalDoc.docx"; String htmlStart = "<html>\r\n"; String htmlHead = "<meta http-equiv='Content-Type' content='text/html; charset=windows-1252'>\r\n" + "<meta name='viewport' content='width=device-width, initial-scale=1.0'>\r\n"; String htmlFilename = "D:/TestOutput/TestOutput.html"; File htmlFile = new File(htmlFilename); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(htmlFile, false), StandardCharsets.UTF_8)); FileInputStream fis = new FileInputStream(inputDoc); XWPFDocument xdoc = new XWPFDocument(OPCPackage.open(fis)); List<XWPFParagraph> paragraphs = xdoc.getParagraphs(); // -------------------------- 新增:获取并生成样式CSS -------------------------- XWPFStyles styles = xdoc.getStyles(); bw.write(htmlStart + "\r\n"); bw.write("<head>\r\n"); bw.write(htmlHead); bw.write("<style>\r\n"); bw.write("html {font-family:Arial;}\r\n"); // 遍历所有样式,生成对应CSS规则 for (XWPFStyle style : styles.getStyles()) { String styleId = style.getStyleId(); if (styleId == null || styleId.isEmpty()) continue; // 处理段落样式 if (style instanceof XWPFParagraphStyle) { XWPFParagraphStyle paraStyle = (XWPFParagraphStyle) style; CTStyle ctStyle = paraStyle.getCTStyle(); StringBuilder cssRule = new StringBuilder("p." + styleId + " {"); // 提取段落对齐方式 if (ctStyle.isSetPPr() && ctStyle.getPPr().isSetJc()) { STJc.Enum align = ctStyle.getPPr().getJc().getVal(); String cssAlign = switch (align) { case BOTH -> "justify"; case CENTER -> "center"; case RIGHT -> "right"; case LEFT -> "left"; default -> "left"; }; cssRule.append("text-align: ").append(cssAlign).append(";"); } // 提取段落首行缩进 if (ctStyle.isSetPPr() && ctStyle.getPPr().isSetInd()) { CTInd ind = ctStyle.getPPr().getInd(); if (ind.isSetFirstLine()) { // Word的缩进单位是twip,转成pt(1pt=20twip) cssRule.append("text-indent: ").append(ind.getFirstLine().intValue() / 20).append("pt;"); } } // 提取字体相关属性 if (ctStyle.isSetRPr()) { CTRPr rPr = ctStyle.getRPr(); // 字体族 if (rPr.isSetRFonts()) { CTFonts fonts = rPr.getRFonts(); if (fonts.isSetAscii()) { cssRule.append("font-family: '").append(fonts.getAscii()).append("';"); } } // 字号(Word的sz是半磅单位,转成pt) if (rPr.isSetSz()) { cssRule.append("font-size: ").append(rPr.getSz().getVal().intValue() / 2).append("pt;"); } // 加粗属性 if (rPr.isSetB() && rPr.getB().getVal()) { cssRule.append("font-weight: bold;"); } } if (cssRule.length() > "p.".length() + styleId.length() + 2) { cssRule.append("}\r\n"); bw.write(cssRule.toString()); } } // 处理字符样式 if (style instanceof XWPFCharStyle) { XWPFCharStyle charStyle = (XWPFCharStyle) style; CTStyle ctStyle = charStyle.getCTStyle(); StringBuilder cssRule = new StringBuilder("span." + styleId + " {"); if (ctStyle.isSetRPr()) { CTRPr rPr = ctStyle.getRPr(); // 字体族 if (rPr.isSetRFonts()) { CTFonts fonts = rPr.getRFonts(); if (fonts.isSetAscii()) { cssRule.append("font-family: '").append(fonts.getAscii()).append("';"); } } // 字号 if (rPr.isSetSz()) { cssRule.append("font-size: ").append(rPr.getSz().getVal().intValue() / 2).append("pt;"); } // 加粗 if (rPr.isSetB() && rPr.getB().getVal()) { cssRule.append("font-weight: bold;"); } // 下划线 if (rPr.isSetU()) { cssRule.append("text-decoration: underline;"); } } if (cssRule.length() > "span.".length() + styleId.length() + 2) { cssRule.append("}\r\n"); bw.write(cssRule.toString()); } } } bw.write("</style>\r\n"); bw.write("</head>\r\n"); bw.write("<body>\r\n"); // -------------------------- 原有段落处理逻辑,优化样式应用 -------------------------- for(XWPFParagraph para : paragraphs) { String paraStyleId = para.getStyle(); if (paraStyleId != null && !paraStyleId.isEmpty()) { bw.write("<p class='" + paraStyleId + "'>"); } else { bw.write("<p>"); } // 处理段落的直接内联属性(优先级高于样式) StringBuilder inlineStyle = new StringBuilder(); if (para.getFirstLineIndent() > 0) { inlineStyle.append("text-indent:").append(para.getFirstLineIndent()).append(";"); } if(para.getAlignment() != null) { String align = para.getAlignment().name().toLowerCase(); if (align.equals("both")) align = "justify"; inlineStyle.append("text-align:").append(align).append(";"); } if (!inlineStyle.isEmpty()) { bw.write(" style='" + inlineStyle + "'"); } bw.write(">"); // 处理Run,应用字符样式和直接格式 for(XWPFRun run : para.getRuns()) { String runStyleId = run.getStyle(); if (runStyleId != null && !runStyleId.isEmpty()) { bw.write("<span class='" + runStyleId + "'>"); } // 处理Run的直接内联格式 StringBuilder runInlineStyle = new StringBuilder(); if (run.isBold()) { runInlineStyle.append("font-weight:bold;"); } if (run.getFontFamily() != null) { runInlineStyle.append("font-family:'").append(run.getFontFamily()).append("';"); } if (!runInlineStyle.isEmpty()) { bw.write("<span style='" + runInlineStyle + "'>"); bw.write(run.text()); bw.write("</span>"); } else { bw.write(run.text()); } if (runStyleId != null && !runStyleId.isEmpty()) { bw.write("</span>"); } } bw.write("</p>\r\n"); } bw.write("</body>\r\n"); bw.write("</html>"); bw.close(); xdoc.close(); fis.close(); System.out.println("END PGM"); } }
关键说明
- 样式继承的补充处理:上面的代码基础版本只处理了当前样式的属性,如果需要处理样式继承(比如一个样式基于Normal),可以新增一个递归方法,从当前样式向上遍历父样式,合并所有属性。
- 单位转换:Word里的字号(sz)是半磅、缩进值是twip(1/20磅),代码里已经做了对应转换,确保CSS单位和Word格式一致。
- 格式优先级:内联样式(段落/Run的直接属性)会覆盖CSS类样式,这和Word的格式优先级逻辑完全一致(直接格式 > 样式)。
通过这种方式,你就能完整获取Word文档里的所有样式属性,生成对应的CSS,让HTML的格式和Word原生导出的几乎一致,解决你之前拿不到样式里的加粗等属性的问题。
内容来源于stack exchange

