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

2022-04-27 08:35:21 浏览数 (1)

系列文章:

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;
    }

三 接下来可以做什么?

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

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

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

0 人点赞