写点什么

Java 操作 Office:POI word 之文档信息提取

  • 2022 年 4 月 18 日
  • 本文字数:4721 字

    阅读完需:约 15 分钟

Java 操作 Office:POI word 之文档信息提取

系列文章:

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 编码)。

import lombok.Builder;import lombok.Data;
/** * 段落风格和内容实体 */@Data@Builderpublic class StyleTextVO { private Integer titleLevel; private String style; private String type; private String text; private String content;}
复制代码

2.2 标题识别

一些常量定义:


/** * 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、根据业务需要,对格式做一些基础转换

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。

/**     * 读取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 的表格实体也有所不通,所以自定义表格数据结构如下:

import lombok.Data;import lombok.ToString;
import java.util.List;
@ToString@Datapublic class WordTableVO {
private int rowCnt;
private int colCnt;
private List<String> titleList;
private List<List<String>> contentTable;}
复制代码

2.3.1 doc 文档表格提取


   /**     * 读取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 文档表格提取

    /**     * 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; }
复制代码

三 接下来可以做什么?

说句废话,有了结构化数据,接下来自然是可以识别我们的业务。那么业务可能是做哪些?

首先,标题通常是重要信息的摘要,那么我们就可以根据标题进行定位,定位到制定的段落,并提取相关信息。再细化一点,如下是某个系统的文档:

我们希望提取到系统的功能清单,如果是批量或者动态的解析(非人工)该怎么做? 显然,可以先定位到“系统功能清单”这个章节,然后提取表格信息;再通过表头来获取各列(模块、功能清单)的内容。关于如何定位到“系统功能清单”章节,简单的场景是通过字符串匹配,稍复杂一点,可以提供关键词表(字典),来进行模式匹配,表头处理也可以用这种模式。总之,我们有了基础工具和资料,之后就可以做很多事情了。

发布于: 刚刚阅读数: 3
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
Java 操作 Office:POI word 之文档信息提取_内容审核_程序员架构进阶_InfoQ写作平台