POI解析word目录结构

  • 简介说明
  • 认识下Word
    • 我们先看下doc版本的word
    • 我们再看下docx版本的word(今天的主角)
  • 目录解析的原理介绍
    • 写word文档时,我们是怎么设置目录?
    • 我们看下样式
      • 准备点目录的css样式
      • 定义一个层级的目录结构对象
      • 整个目录结构的封装对象
      • 转换逻辑
      • 测试demo
  • 结语
  • 项目代码

简介说明

java使用POI转换word为Html的文章有很多,但是很少有涉及到目录解析的,包括一些付费的,开源的,甚至是office自带的另存为…功能都没有涉及到生成目录的。
本篇将介绍目录生成的基本原理和代码实现,windows,linux都可以使用。
https://zhangshaoju.me

认识下Word

我想大家都知道word有两个大的版本,一个是office97~2003,一个是office2007。最直观的区别就是一个是doc结尾的一个docx结尾的。

我们先看下doc版本的word

新建一个doc,如下图:

可以注意到,doc文件的下面有一个zip文件,这是我将上面的doc后缀,直接改成了zip。你可能会有疑问,不过没错,就是你想的那样,我们经常使用的word文档本质就是一个zip压缩包,在这里存储了word相关的一些文件,这些文件里存放的就是word里你写入的文件,进行的排版,粘贴的图片,插入的表格附件,超链接等等。我们解压这个文件,看到如下的结构:

所以到这里,我们对word文档是不是开始有了揭开面纱的认识。
当然,不好意思,doc版本的word不是我们这次解析的对象,我们解析的对象是docx版本的word。这里我只是带你看看而已,doc这里的文件都是二进制的格式,当然poi也可以读取解析,但是API完全不一样,而且这种格式已经逐渐淘汰了,另外有很多开源工具都可以将doc转换成docx,转换效果很不错。

PS:本人提供完整的docx转换html,解析范围:包括表格(合并行,列),包括超链接,包括目录结构,包括正文(粗体,字号,背景颜色,字体颜色,下滑写,删除线),包括图片,包括附件,包括矢量图,包括公式等等。可以说基本上word里你见过的都可以转换。可以联系我微信hoodlake。

我们再看下docx版本的word(今天的主角)

同样,我们新建一个docx文件,将文件后缀docx手动改成zip,解压后,如下图:

这个时候我们可以看到

  1. docx的word的解压内容更加丰富。
  2. docx的内部结构都是以xml的形式存放的。对,你想的没错,如果doc的二进制格式你还有所忌讳的话,docx的xml存储结构,自己是不是可以跃跃欲试了。说句大言不惭的话:会解析xml就能全方位掌握word了,甚至可以不用poi,任何能解析xml的语言都可以解析word。不用不好意思,相信自己,真的就是这个样子的,只是你还需要花费很多经历去分析他的xml文档结构。你将会发现使用POI的ooxml,xmlbeans对于解析word简直有如神助。
  3. 本人比较习惯使用eclipse,你也可以导入到你习惯的ide里,把所有的xml文件格式化一下(Word本身是压缩存储的),你将会更加清晰的看到word的内部结构,下图是格式化后的document.xml:

接下来将会讲述原理,以上的介绍,希望大家务必手动操作一下,这样对word有个属于自己的总结性的认识。对接下来的原理理解也会容易一点。
本篇文档内容只设计部分代码的展示,完整的目录解析代码,会以附件的形式,放在文章末尾。

目录解析的原理介绍

写word文档时,我们是怎么设置目录?

我们先回顾下这个画面,您是否自己认真写过一篇word文章,一篇满足规范的文章。如果没有的话,在这里你可以从认识段落开始,重新了解下word编辑正确的使用方式。

从上图可以看出几点:

  1. 段落的样式是特定的;当你选定了一种段落样式之后,那它就是这一套样式,在这一套样式里选择一级标题,二级标题,三级标题。这是一种规范,是一种大家约定熟成的约束。这里是word默认的段落样式,在我的认知的领域里,我还见过其他的段落格式,我们这里只是介绍原理,其他的多写点点适配代码就好了
  2. 右边通过章节菜单,展示出来了,这篇文档的目录结构,我们的目的也就是把这个目录结构在html里展示出来。
  3. 我们要有意识,目录一种层级结构,是一种树状结构,所以接下来我们会用到递归的算法。
  4. 在第1点中提到的样式的概念,希望你格外留意,这是你打开编码转换思路的一把钥匙。

我们看下样式

上面的截图,我们看到了格式化后的word文档结构:document.xml,word的所有编辑内容都是存放在这里的,word的整个结构关系,内容都是在这里的。但是这只是一个文本文档,是不能存储附件,图片,公式等二进制资源的,他是采用的引用的策略,通过r:id属性来引用对应的资源。对于特定格式的使用也是通过引用的方式的。
我们可以看到,除了document.xml,还有很多其他的xml文件,比如这里我们比较关注,style.xml,格式化后截图如下:

所以可以看出来,通过style,我们就可以看到文章有目录结构是怎么样的,但这里仅仅只能看出目录结构,有多少级目录而已。一切和段落撇开的目录都是耍流氓,对吧。至少我们得指定这个目录叫啥吧,这时候就需要结合document.xml了

如上图所示,所有的目录段落上面都有w:pStyle标签,记录了引用的样式,通过引用的样式就可以知道层级关系,通过层级关系和段落内容,就可以构建一个目录了,说到这里,no bb ,show me the code。

准备点目录的css样式

显而易见,既然要转换成html,一个像样的html页面,css怎么能少得了呢,上css代码,项目里就放在类路径下的word.css文件中,程序运行时,加载加入到html页面中,也可以通过link的方式引用,看具体场景。

*{font-family: "Microsoft Yahei" , "Arial Narrow" ,Verdana, Geneva, sans-serif;-webkit-overflow-scrolling: touch;box-sizing: border-box;-moz-box-sizing: border-box; /* Firefox */-webkit-box-sizing: border-box;
}blockquote,body,dd,div,dl,dt,fieldset,form,h1,h2,h3,h4,h5,h6,li,aol,p,pre,svg,td,textarea,th,ul {margin: 0;padding: 0;font-family: "Microsoft Yahei" , "Arial Narrow" ,Verdana, Geneva, sans-serif;font-size: 12px;box-sizing: border-box;-moz-box-sizing: border-box; /* Firefox */-webkit-box-sizing: border-box;}#catalog{width: 300px;height: 100%;position: fixed;left: 0px;bottom: 0px;display: none;overflow-y:auto; background: #F6F6F6;}#catalog a{display: block;height: 25px;line-height: 25px;cursor: pointer;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;color: #333;}#catalog h1 a{border-left:2px solid #dedede;font-size: 12px;padding-left: 10px;}#catalog h1 a:hover{border-left:2px solid #1DC2DE;color: #1DC2DE}#catalog h2 a{padding-left: 20px;font-size: 12px;}#catalog h3 a{padding-left: 30px;font-size: 12px;}#catalog h4 a{padding-left: 40px;font-size: 12px;}#catalog h5 a{padding-left: 50px;font-size: 12px;}#catalog h6 a{padding-left: 60px;font-size: 12px;}#catalog a:hover{color: #1DC2DE}#catalog .act a{color: #1DC2DE;
}
#catalog p{font-size: 18px;font-weight: bold;padding: 10px;
}

定义一个层级的目录结构对象

package com.bigbrain.converter.docx2html;
import java.util.ArrayList;
import java.util.List;
/*** @author big brain*    目录条目封装类*/
public class DocCatalog {/*** 上级目录*/private DocCatalog upperCatalog = null;/*** 子目录*/private List<DocCatalog> subCatalogs = new ArrayList<DocCatalog>();/*** 节点排序,从1开始,第几个目录条目*/private int sort;/*** 节点唯一编码:由上级节点编码和当前节点编码构成*/private String code;/*** 节点名称*/private String text;/*** 节点层级,根节点为1,依次累加*/private int level;public int getSort() {return sort;}public void setSort(int sort) {this.sort = sort;}public DocCatalog getUpperCatalog() {return upperCatalog;}public void setUpperCatalog(DocCatalog upperCatalog) {this.upperCatalog = upperCatalog;}public List<DocCatalog> getSubCatalogs() {return subCatalogs;}public String getText() {return text;}public void setText(String text) {this.text = text;}/*** 获取全标题(输出当前目录节点的html代码)* @return*/public String getFullTitle() {return "<span class=\"p_title_num\">"+this.code+"</span><span class=\"p_title\">"+this.text+"</span>";}/*** 是否拥有子目录* @return*/public boolean hasSub() {return subCatalogs.size()>0;}public int getLevel() {return level;}public void setLevel(int level) {this.level = level;}/*** 添加子目录* @param item*/public void addSubCatalog(DocCatalog item) {this.subCatalogs.add(item);}public String getCode() {return code;}public void setCode(String code) {this.code = code;}
}

整个目录结构的封装对象

package com.bigbrain.docx2html;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author zhangshaoju*     文档目录结构*/
public class DocCatalogList {/*** 所有根节点*/private List<DocCatalog> rootList =  new ArrayList<>();/*** 树节点个数,总的目录条目数,初始为0*/private int count = 0;/*** 存储某个层级的最后一个节点*/private Map<Integer,DocCatalog> lastCatalogInLevel = new HashMap<>();/*** 销毁对象*/public void clear() {this.rootList.clear();this.lastCatalogInLevel.clear();}/*** 添加一个新的目录节点* @param text* @param level*/public void addCatalog(String text,int level){count++;DocCatalog item = new DocCatalog();item.setLevel(level);item.setText(text);item.setSort(count);if(level == 1) {//根目录item.setUpperCatalog(null);item.setCode((rootList.size()+1)+".");rootList.add(item);}if(level > 1) {//非根目录DocCatalog upper = lastCatalogInLevel.get(level-1);if(upper != null) {//可能会解析到不规范的文档,比如一级标题在二级标内部item.setUpperCatalog(upper);item.setCode(upper.getCode()+(upper.getSubCatalogs().size()+1)+".");upper.addSubCatalog(item);}}lastCatalogInLevel.put(level, item);//设置该节点为当前层级的最后一个节点}/*** 遍历整个目录* @param processor*/public void walk(DocCatalogNodeProcessor processor) {for(DocCatalog catalog:rootList) {walk(catalog,processor);}}/*** 遍历所有目录节点* @param catalog* @param processor*/private void walk(DocCatalog catalog,DocCatalogNodeProcessor processor) {if(processor==null) {processor= new DocCatalogNodeProcessor00();__123Util.record("WARN:处理程序为空,使用默认处理程序");}processor.process(catalog);List<DocCatalog> subList = catalog.getSubCatalogs();for(DocCatalog one : subList) {if(one.hasSub()) {walk(one,processor);}else {processor.process(one);}}}
}

转换逻辑

package com.bigbrain.docx2html;import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.util.List;import org.apache.poi.xwpf.usermodel.BodyElementType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFNum;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFStyle;/*** @author big brain* docx转换html之大纲目录解析*/
public class Docx2Html_Catalog {private DocCatalogList docCatalogList = new DocCatalogList();/***  输出转换后的HTML*/private StringBuffer asHtml = new StringBuffer();/*** docx的poi文档对象*/private XWPFDocument document;/*** @param document 需要手动关闭,本转换程序不会关闭document对象*/public Docx2Html_Catalog(File docxFile) {try {this.document = new XWPFDocument(new FileInputStream(docxFile));} catch (Exception e) {//继承了RuntimeException的一个自定义异常类throw new Word2HtmlParseException(e);}convert();}public String asHtml() {return asHtml.toString();}/*** 从类路径加载css文件* * @return* @throws Exception*/private static String loadCss() {BufferedReader br = new BufferedReader(new InputStreamReader(Docx2Html_Catalog.class.getResourceAsStream("/word.css")));StringBuffer sb = new StringBuffer();String line = null;try {while ((line = br.readLine()) != null) {sb.append(line).append("\n");}br.close();} catch (IOException e) {e.printStackTrace();}return sb.toString();}/*** 返回段落样式名称* * @param document* @param p* @return*/private String getStyle(XWPFDocument document, XWPFParagraph p) {XWPFStyle style = document.getStyles().getStyle(p.getStyleID());if (style == null)return "_default";return style.getName().toLowerCase();}/*** 开始构建目录* * @param elems*/private void buildCatalog() {List<IBodyElement> elems = document.getBodyElements();for (int i = 0; i < elems.size(); i++) {IBodyElement be = elems.get(i);if (be instanceof XWPFParagraph == false) {continue;}XWPFParagraph p = (XWPFParagraph) be;String style = getStyle(document, p);if (!style.contains("heading") && !style.contains("标题")) {//两种标题模式continue;}int level = 1;try {if(style.contains("heading")) {level = Integer.parseInt(style.replace("heading", "").trim());}} catch (Exception e) {System.err.println("忽略无法解析的样式:" + style + ",所在段落:" + p.getText());continue;}String text = p.getText();docCatalogList.addCatalog(text, level);}DocCatalogNodeProcessor01 process = new DocCatalogNodeProcessor01();docCatalogList.walk(process);asHtml.append("\t\t<div id=\"catalog\" class=\"content-right-box boxSize\">").append("\n");asHtml.append("\t\t\t<p>目录</p>");asHtml.append(process.getContent()).append("\n");asHtml.append("\t\t</div>").append("\n");}/*** 获取段落类型,这个段落类型是根据技术文档书写规范自定义的一个类型,(注意)和word本身的段落类型概念不一样,目前分为以下三种:<br>* 1.head 标题段落<br>* 2.list 列表段落<br>* 3.text 普通文本段落<br>* * @param p* @return*/private String getParagraphType(XWPFParagraph p) {// 优先匹配层级段落XWPFStyle style = document.getStyles().getStyle(p.getStyleID());if (style != null) {// 检查是否是标题段落for (int i = 1; i <= 9; i++) {// word最高支持9级标题,html最高支持6级标题String styleName = "heading " + i;if (styleName.equalsIgnoreCase(style.getName())) {return styleName;}}}// 再匹配列表段落BigInteger numId = p.getNumID();// 检查是否是普通列表段落,numId=0可以认为是无效列表(之前生成的列表,后面清理了,但是没清理干净的,虽然word里看不见,但是底层xml文件里还残留)if (numId != null && numId.intValue() != 0) {XWPFNum num = this.document.getNumbering().getNum(numId);XWPFAbstractNum abstractNum = this.document.getNumbering().getAbstractNum(num.getCTNum().getAbstractNumId().getVal());String multiLevelType = abstractNum.getCTAbstractNum().getMultiLevelType().getVal().toString();if ("singleLevel".equalsIgnoreCase(multiLevelType)) {return "list";}}return "text";// 普通文本段落}/*** 转换程序,直接调用该方法进行word转换html的工作* * @return* @throws Exception*/private void convert() {asHtml.append("<!DOCTYPE html>").append("\n");asHtml.append("<html>").append("\n");asHtml.append("<head>").append("\n");asHtml.append("\t<title>docx转换html之解析目录结构</title>").append("\n");asHtml.append("\t<meta charset=\"UTF-8\"/>").append("\n");asHtml.append("\t<style>\n");asHtml.append(loadCss());asHtml.append("\t</style>\n");asHtml.append("</head>").append("\n");asHtml.append("<body class=\"bodys\">").append("\n");List<IBodyElement> elems = document.getBodyElements();asHtml.append("\t<div class=\"container\">").append("\n");buildCatalog();asHtml.append("\t\t<div class=\"content\" id=\"contentBox\">").append("\n");for (IBodyElement be : elems) {try {if (be.getElementType().equals(BodyElementType.PARAGRAPH)) {// 是段落XWPFParagraph p = (XWPFParagraph) be;String text = p.getText();String paragraphType = getParagraphType(p);if (paragraphType.contains("heading")) {// 输出标题DocCatalogNodeProcessor02 processor02 = new DocCatalogNodeProcessor02(p.getText());docCatalogList.walk(processor02);DocCatalog catalog = processor02.fetch();if (catalog == null) {continue;}String code = catalog.getCode();// 获取到目录节点代码asHtml.append("\t<a name=\"" + code + "\" id=\"" + code + "\">\n");asHtml.append("<h").append(catalog.getLevel()).append(">");asHtml.append(catalog.getFullTitle());asHtml.append("</h").append(catalog.getLevel()).append(">\n");asHtml.append("\t</a>\n");continue;} else if (paragraphType.equals("list")) {// 说明是列表段落System.out.println("忽略列表解析");} else {// 输出正文if (text == null || text.trim().equals("")) {asHtml.append("\t<p class=\"br\"></p>\n");continue;} else {String ali = p.getAlignment().name().toLowerCase();int fstIndent = p.getFirstLineIndent() <= 0 ? 0 : p.getFirstLineIndent();int lftIndent = p.getIndentFromLeft() <= 0 ? 0 : p.getIndentFromLeft();int indent = fstIndent + lftIndent;asHtml.append("\t<p style=\"text-indent:" + indent / 10 + "px;text-align:" + ali + "\">\n");asHtml.append("<span>");asHtml.append(p.getText().replaceAll(" ", "&nbsp;"));asHtml.append("</span>");asHtml.append("\t</p>\n");continue;}}} else{System.out.println("其他word元素如表格,这里忽略");}} catch (Exception e) {e.printStackTrace();continue;}}asHtml.append("\t\t</div>").append("\n");asHtml.append("\t</div>").append("\n");asHtml.append("</body>").append("\n");asHtml.append("</html>");}
}

测试demo

package com.bigbrain.docx2html;
import java.io.File;
import java.io.FileWriter;
public class DemoTest {public static void main(String[] args) throws Exception{Docx2Html_Catalog convertor = new Docx2Html_Catalog(new File("q:\\word解析\\docx版本的word.docx"));String html = convertor.asHtml();FileWriter fw = new FileWriter("q:\\word解析\\docx版本的word.html");fw.write(html);fw.close();}
}

结语

到这里,docx的word文档的目录结构解析就完成了,您可以使用更加复杂的层级结构来检验。这里只是简单说明下目录原理,要达到现实中的使用需求,还有很多地方要补充完善,比如:

  1. 通过html锚点定位到文章指定地方
  2. 好的文档结构还有很多有序或者无序列表,和目录解析的原理也差不多,后面有时间,我会分享出来。
  3. 甚至还有一些地方不是严格按段落样式来的,有夹杂的地方,很多地方还需要做好兼容性控制。

项目代码

这是一个maven项目,点击下载即可。完整的代码都在里面。如需要更多的word解析转换功能可以关注后面的文章(如果写了的话),也可以联系我微信(hoodlake)
docx2html_catalog

如何使用poi解析word生成html目录结构相关推荐

  1. poi解析word文档(解析表格,emf,wmf,svg转jpg图片)

    POI解析word文档 poi解析word的表格:   提前先准备需要的jar包:   <!-- poi --><dependency><groupId>org.a ...

  2. Java POI解析Word提取数据存储在Excel

    JavaPOI解析word提取数据到excel 一.了解POI POI以前有了解,这次需求是解析word读取其中标题,还有内容赛选获取自己想要的内容 经过两天的学习,开始熟悉Java这么读取word和 ...

  3. poi解析word中的表格

    解析word简历,使用poi解析word表格研究记录如下: package poi;import java.io.File; import java.io.FileInputStream; impor ...

  4. 生成具有目录结构的CAB文件

    一.使用makecab.exe生成CAB文件 在微软签名的过程中有时需要提交.cab格式的文件,该格式是使用微软的makecab.exe或Cabarc.exe来进行生成的. 这里主要介绍的是使用mak ...

  5. .md文件自动生成项目目录结构

    自动生成项目目录结构 1)安装mddir (-g是全局安装,可以选择不全局安装,这里因为以后都要使用所以选择的全局安装) npm install mddir -g 2)cd 到你想生成目录的工程结构, ...

  6. java如何解析word大纲_Java POI 解析word文档

    实现步骤: 1.poi实现word转html 2.模型化解析html 3.html转Map数组 Map数组(数组的操作处理不做说明) 1.导jar包. 2.代码实现 package com.web.o ...

  7. POI解析Word批注信息

    前言:报告审批后,要求解析Word批注信息获取 作者.引用正文.批注内容 等信息入库,我这边额外加了回复对象(为后续考虑) 先上代码吧: /*** @author : weiheng* @versio ...

  8. java poi实现Word生成表格并下载至本地

    首先我来讲一下需要用到的类: XWPFDocument代表一个docx文档,其可以用来读docx文档,也可以用来写docx文档 XWPFParagraph代表文档.表格.标题等种的段落,由多个XWPF ...

  9. Word生成图目录和表目录

    事实上,Word不仅能生成标题目录,还能生成图片目录.图表目录.表格目录等.无论是图片.图表还是表格等内容,生成目录的方法是一样的. 均需要通过[题注]功能添加名称,然后再插入表目录.下面以图片为例, ...

最新文章

  1. sharepoint2013- Office web app server2013详细的安装和部署
  2. SAP WebService基础知识
  3. 通过带数据盘的自定义镜像来创建使用应用程序网关的虚拟机规模集
  4. c语言案例朗读工具源码,C语言编写简单朗读小工具(有源码)
  5. webstore 与 热编译的配置冲突
  6. 为什么root下不能使用passwd命令_Linux:CentOS 7中常用的基础命令
  7. scrapy命令介绍
  8. 对外汉语偏误语料库_对外汉语偏误
  9. java io broken pipe解决_报错!!java.io.IOException: 断开的管道 中文报错?Broken pipe?这次发生的报错不影响业务,已有解决方案...
  10. 在Photoshop中设计Web 2.0标签/徽章
  11. CentOS7.5 重装 SSH 与 禁止网卡休眠
  12. 直接在pcb中查看焊盘尺寸
  13. 网络安全威胁检测与威胁溯源_网络安全威胁2017 –如何保护您的网站
  14. Joan Baez - Jackaroe
  15. 在伯克利学计算机,2019加州大学伯克利分校计算机专业申请要求
  16. SpringBoot配置拦截器导致swagger失效
  17. 嵌入式系统主要应用哪些行业?
  18. 简单demo诉说Service And Intentservice
  19. 【装机】【华硕TUF b450m-pro】新增固态硬盘之后机械盘无法显示
  20. 【§狼族少年之淳伊美女主题(oo)】

热门文章

  1. 2004年9月全国计算机等级考试二级C语言笔试试题
  2. 阿里云elasticsearch试用套路
  3. Ratione aspernatur nam dolorem vitae quia.Fumer comme créer passer ailleurs jouer lumière.
  4. 概率论例题01:设P(A)=a,P(B)=b,P(A∪B)=c,则P(A非B)=()
  5. 用计算机完成下表的视距测量计算公式,2012测量学计算题库及参考答案
  6. 开源项目 - 电子签章(移动端签名方案)
  7. 搜狗输入法自动打开问题
  8. 项目经验之Kafka压力测试以及kafka机器数量计算
  9. 论文研读 —— 6. ImageNet Classification with Deep Convolutional Neural Networks (2/3)
  10. 文件格式FAT、FAT32、exFAT之间关系和区别