系列文章:
Java 操作 Office:POI 之 word 生成
Java 操作 Office:POI 之 word 图片处理
Java 操作 Office:POI word 之网络图片处理
Java 操作 Office:POI word 之表格格式
Apache POI详解及Word文档读取示例
楔子
工作忙碌,又是好久不见。最近频繁地在与文档开发打交道,除了之前做过的文档生成,最近又在调研文档内容提取、解析相关的内容。顺手整理下来,供各位开发同学参考。
一 背景
简单来说,就是有一些文档数字化的场景。包括对word、pdf格式的文档进行内容提取,之后做格式解析,并根据具体的业务需求,还会有文本识别提取关键内容的一些动作。说起来看似简单,但仔细分析,其中会涉及ocr(pdf文档内容识别)、nlp(文本内容解析,例如标题提取、关键字解析等)等等。最简单的考虑,假设我们只对word文档做解析实现,也需要支持office api的sdk,以及支持模板配置解析的规则来实现内容解析。
再进一步缩小范围,我们先细化需求,都需要解析哪些内容?是否是word中易于识别的格式?例如标题提取,表格内容提取。如果再进一步细化,表格也分为word原生表格和内嵌excel表格。本篇就将以一个典型场景为例,抛砖引玉,给出一个实现方案。后续可以在此基础上再做深入探讨。
二 基于apache poi的内容提取
关于apache poi,基础信息介绍、jar包依赖的引入方式已经在之前的系列文章:Apache POI详解及Word文档读取示例 中做了介绍,所以这里不再赘述。我们可以使用poi提供的api来读取word的doc 和 docx格式文档,并能够获取到每个段落的格式(style),判断是目录,正文,还是标题等。
这里再强调一下,因为doc 和 docx是两种完全不同的格式,所以我们考虑把word文档的文本内容转为统一的格式,来存储格式信息,便于后续的统一处理。
2.1 文本数据结构
一个简单的结构定义如下,其中titleLevel代表标题级别(标题1-->1,正文-->-1),style为格式的中文描述,type代表内容类型(默认为文本,其他有图片、表格等),text表示文本内容,content有些冗余,表示其他非文本格式的内容(例如图片存储base64编码)。
代码语言:javascript复制import lombok.Builder;
import lombok.Data;
/**
* 段落风格和内容实体
*/
@Data
@Builder
public class StyleTextVO {
private Integer titleLevel;
private String style;
private String type;
private String text;
private String content;
}
2.2 标题识别
一些常量定义:
代码语言:javascript复制/**
* word doc的标题格式前缀,标题 styleName:标题 1,标题 2,...
* 正文 为 "正文"
*/
private static final String docTitlePrefix = "标题 ";
private static final String docText = "正文";
/**
* word docx的标题格式前缀,标题 styleName:heading 1,heading 2,...;
* 正文 为 null
*/
private static final String docXTitlePrefix = "heading ";
private static final String structTitlePrefix = "h";
private static final String structText = "p";
2.2.1 doc文档内容解析
重点:1、文档读取方式:HWPFDocument;2、格式获取:通过Range获取所有段落的数量,并逐个遍历,再通过文档的StyleSheet,获取格式名;3、根据业务需要,对格式做一些基础转换
代码语言:javascript复制public List<StyleTextVO> readDoc(String path) throws Exception {
List<StyleTextVO> styleTextVOList = new ArrayList<>();
InputStream is = new FileInputStream(path);
HWPFDocument doc = new HWPFDocument(is);
Range r = doc.getRange();
for (int i = 0; i < r.numParagraphs(); i ) {
Paragraph p = r.getParagraph(i);
int styleIndex = p.getStyleIndex();
StyleSheet style_sheet = doc.getStyleSheet();
StyleDescription style = style_sheet.getStyleDescription(styleIndex);
String styleName = style.getName();
int titleLevel = -1; // 默认为正文
if (styleName.equals(docText)){
styleName = structText;
}else{
titleLevel = styleIndex;
styleName = styleName.replace(docTitlePrefix, structTitlePrefix);
}
StyleTextVO styleTextVO = StyleTextVO.builder()
.titleLevel(titleLevel).style(styleName).text(p.text()).build();
styleTextVOList.add(styleTextVO);
}
doc.close();
return styleTextVOList;
}
2.2.2 docx文档内容解析
同2.2.1,差别在于通过XWPFDocument读取docx文档;通过paragraph.getStyleID()取得styleID。
代码语言:javascript复制/**
* 读取docx内容
* @param path
* @throws Exception
*/
public List<StyleTextVO> readDocX(String path) throws Exception {
List<StyleTextVO> styleTextVOList = new ArrayList<>();
InputStream is = new FileInputStream(path);
XWPFDocument document = new XWPFDocument(is);
List<XWPFParagraph> paragraphList = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphList){
String styleId = paragraph.getStyleID();
String styleName = "正文";
int titleLevel = -1;
if (!StringUtils.isEmpty(styleId)){
try{
titleLevel = Integer.valueOf(styleId);
XWPFStyle style = document.getStyles().getStyle(styleId);
styleName = style.getName();
}catch (Exception e){
log.error("不支持的标题格式, msg:{}", e.getMessage());
}
}
if (styleId == null){
styleName = structText;
}else{
styleName = styleName.replace(docXTitlePrefix, structTitlePrefix);
}
StyleTextVO styleTextVO = StyleTextVO.builder()
.titleLevel(titleLevel).style(styleName).text(paragraph.getText()).build();
styleTextVOList.add(styleTextVO);
}
this.close(is);
return styleTextVOList;
}
2.3 表格提取
与上面相同,doc和docx的表格实体也有所不通,所以自定义表格数据结构如下:
代码语言:javascript复制import lombok.Data;
import lombok.ToString;
import java.util.List;
@ToString
@Data
public class WordTableVO {
private int rowCnt;
private int colCnt;
private List<String> titleList;
private List<List<String>> contentTable;
}
2.3.1 doc文档表格提取
代码语言:javascript复制
/**
* 读取doc格式文档中的表格
* @param in
* @throws Exception
*/
public List<WordTableVO> getTableFromDoc(FileInputStream in) throws Exception {
List<WordTableVO> wordTableVOS = new ArrayList<>();
POIFSFileSystem pfs = new POIFSFileSystem(in);
HWPFDocument hwpf = new HWPFDocument(pfs);
Range range = hwpf.getRange();
TableIterator it = new TableIterator(range);
while (it.hasNext()) {
Table tb = it.next();
WordTableVO wordTableVO = new WordTableVO();
wordTableVO.setRowCnt(tb.numRows());
wordTableVO.setContentTable(new ArrayList<>());
for (int i = 0; i < tb.numRows(); i ) {
TableRow tr = tb.getRow(i);
if (i == 0){
wordTableVO.setColCnt(tr.numCells());
}
List<String> rowCellList = new ArrayList<>();
for (int j = 0; j < tr.numCells(); j ) {
TableCell td = tr.getCell(j);
String lineValue = "";
for(int k = 0; k < td.numParagraphs(); k ){
Paragraph para = td.getParagraph(k);
String s = para.text();
// 去除特殊符号
if(null != s && !"".equals(s)){
s = s.substring(0, s.length()-1);
}
lineValue = s.replace(" ", "");
}
rowCellList.add(lineValue);
}
wordTableVO.getContentTable().add(rowCellList);
}
wordTableVOS.add(wordTableVO);
}
return wordTableVOS;
}
2.3.2 docx文档表格提取
代码语言:javascript复制 /**
* word 2007文档解析,表格提取
* @param in
* @throws Exception
*/
public List<WordTableVO> getTableFromDocx(FileInputStream in) throws Exception{
List<WordTableVO> wordTableVOS = new ArrayList<>();
XWPFDocument xwpf = new XWPFDocument(in);
Iterator<XWPFTable> it = xwpf.getTablesIterator();
while(it.hasNext()){
XWPFTable table = it.next();
List<XWPFTableRow> rows = table.getRows();
WordTableVO wordTableVO = new WordTableVO();
wordTableVO.setRowCnt(rows.size());
wordTableVO.setContentTable(new ArrayList<>());
// 逐行读取表格
for (int i = 0; i < rows.size(); i ) {
XWPFTableRow row = rows.get(i);
List<XWPFTableCell> cells = row.getTableCells();
if (i == 0){
wordTableVO.setColCnt(cells.size());
}
List<String> rowCellList = new ArrayList<>();
for (int j = 0; j < cells.size(); j ) {
XWPFTableCell cell = cells.get(j);
rowCellList.add(cell.getText());
}
wordTableVO.getContentTable().add(rowCellList);
}
wordTableVOS.add(wordTableVO);
}
return wordTableVOS;
}
三 接下来可以做什么?
说句废话,有了结构化数据,接下来自然是可以识别我们的业务。那么业务可能是做哪些?
首先,标题通常是重要信息的摘要,那么我们就可以根据标题进行定位,定位到制定的段落,并提取相关信息。再细化一点,如下是某个系统的文档:
我们希望提取到系统的功能清单,如果是批量或者动态的解析(非人工)该怎么做?显然,可以先定位到“系统功能清单”这个章节,然后提取表格信息;再通过表头来获取各列(模块、功能清单)的内容。关于如何定位到“系统功能清单”章节,简单的场景是通过字符串匹配,稍复杂一点,可以提供关键词表(字典),来进行模式匹配,表头处理也可以用这种模式。总之,我们有了基础工具和资料,之后就可以做很多事情了。