java中实现word(doc、docx)中完美提取文字、表格为结构化数据

目的

对于word中的数据,我们可能存在将其抽取为结构化数据的需求。

好处

  • 将数据存储于数据库中,将数据从word繁杂的以手工编辑的格式媒介中抽离出来,便于做大数据分析、ai数据集准备等后续操作。
  • 提供在网页等其它媒介中方便地展示、编辑、再储存等,可自由定制数据展示方式,而不需依赖word客户端组件。

概述及依赖

Word包括docx和doc,其中doc源文件为二进制流文件,可读性较差。docx为xml文件,可读性较强。
想要使用全套的poi解析word,引用的maven包如下:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>ooxml-schemas</artifactId>
    <version>1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>3.17</version>
</dependency>
<dependency>
    <groupId>org.apache.xmlbeans</groupId>
    <artifactId>xmlbeans</artifactId>
    <version>2.6.0</version>
</dependency>

相信我,使用这套引用没有问题,特别全!甚至你版本号也不需要改!如果你需要改动版本号,还需要注意不同包之间的版本关系。具体查看网址如下:
Maven Repository: org.apache.poi » poi
进入这个网址后点击对应版本号往下拉可以看到对应依赖版本配置,以保证没有依赖错误。

开始

在poi中,doc与docx使用的是完全不同的类,方法内部逻辑也不相同,需要各自开发。
docx主要使用的类:XWPFDocument(WordExtractor类是其子集,不需要它),相关规范
doc主要使用的类:HWPFDocument

抽取

核心思想:

对整个word文档从上至下扫描,并对其中的文字和表格进行区分处理,优点:

  • 可以记录文字和表格的顺序,而其它网站上的抽取方法很可能会丢失页面文字和表格的顺序。
  • 其它网站的方法很可能在抽文字时会把表格中的文字一并抽出,而我这里可以自由选择是否抽取出表格中的文字。

常量定义

/**
 * word表格默认高度
 */
private static final int DEFAULT_HEIGHT = 500;

/**
 * word表格默认宽度
 */
private static final int DEFAULT_WIDTH = 1000;

/**
 * word表格转换参数 默认为/1 可以根据需求调整
 */
private static final int DEFAULT_DIV = 1;

/**
 * 目前没有提取word的字体大小 默认为12
 */
private static final Float DEFAULT_FONT_SIZE = 12.0F;

/**
 * word的全角空格 以及\t 制表符
 */
private static final String WORD_BLANK = "[\u00a0|\u3000|\u0020|\b|\t]";

/**
 * word的它自己造换行符 要换成string的换行符
 */
private static final String WORD_LINE_BREAK = "[\u000B|\r]";

/**
 * word table中的换行符和空格
 */
private static final String WORD_TABLE_FILTER = "[\\t|\\n|\\r|\\s+| +]";

/**
 * 计算表格行列信息时设置的偏移值
 */
private static final Float TABLE_EXCURSION = 5F;

/**
 * 抽取文字时去掉不必须字符正则
 */
private static final String splitter = "[\\t|\\n|\\r|\\s+|\u00a0+]";

private static final String regexClearBeginBlank = "^" + splitter + "*|" + splitter + "*$";

结构化javabean类:

WordTableCell类:
@Data
public class WordTableCell {

    private Float x;

    private Float y;

    private Float width;

    private Float height;

    private String text;

    /**
     * 默认为12
     */
    private Float fontSize;

    /**
     * 行号 0开始
     */
    private Integer row;

    /**
     * 列号 0开始
     */
    private Integer col;

    /**
     * 行跨度 从1开始
     */
    private Integer rowspan;

    /**
     * 列跨度 从1开始
     */
    private Integer colspan;
}
WordTable类:
@Data
public class WordTable {
    
    private List<WordTableCell> wordTableCellList;
    
    private Float width;
    
    private Float height;
}
WordContent类(包括word抽取出的文字和表格结构)
@Data
public class WordContent {

    /**
     * text包括段落文字(不包括表格文字,改成包括表格文字也很简单)
     */
    private String text;

    /**
     * 抽取的表格对象
     */
    private List<WordTable> wordTableList;
}

docx核心方法解析:

  1. 概述:
    每个docx中都对应着一个xml文件,样式示例如下:
					<w:tbl>
						<w:tblPr>
							<w:tblStyle w:val="3"/>
							<w:tblW w:type="auto" w:w="0"/>
							<w:jc w:val="center"/>
							<w:tblBorders>
								<w:top w:color="auto" w:space="0" w:sz="12" w:val="single"/>
								<w:left w:color="auto" w:space="0" w:sz="12" w:val="single"/>
								<w:bottom w:color="auto" w:space="0" w:sz="12" w:val="single"/>
								<w:right w:color="auto" w:space="0" w:sz="12" w:val="single"/>
								<w:insideH w:color="auto" w:space="0" w:sz="4" w:val="single"/>
								<w:insideV w:color="auto" w:space="0" w:sz="4" w:val="single"/>
							</w:tblBorders>
							<w:tblLayout w:type="fixed"/>
							<w:tblCellMar>
								<w:top w:type="dxa" w:w="0"/>
								<w:left w:type="dxa" w:w="108"/>
								<w:bottom w:type="dxa" w:w="0"/>
								<w:right w:type="dxa" w:w="108"/>
							</w:tblCellMar>
						</w:tblPr>
						<w:tblGrid>
							<w:gridCol w:w="1185"/>
							<w:gridCol w:w="1664"/>
							<w:gridCol w:w="1336"/>
							<w:gridCol w:w="1364"/>
							<w:gridCol w:w="1816"/>
							<w:gridCol w:w="1380"/>
						</w:tblGrid>
						<w:tr>
							<w:tblPrEx>
								<w:tblBorders>
									<w:top w:color="auto" w:space="0" w:sz="12" w:val="single"/>
									<w:left w:color="auto" w:space="0" w:sz="12" w:val="single"/>
									<w:bottom w:color="auto" w:space="0" w:sz="12" w:val="single"/>
									<w:right w:color="auto" w:space="0" w:sz="12" w:val="single"/>
									<w:insideH w:color="auto" w:space="0" w:sz="4" w:val="single"/>
									<w:insideV w:color="auto" w:space="0" w:sz="4" w:val="single"/>
								</w:tblBorders>
							</w:tblPrEx>
							<w:trPr>
								<w:trHeight w:hRule="atLeast" w:val="630"/>
								<w:jc w:val="center"/>
							</w:trPr>
							<w:tc>
								<w:tcPr>
									<w:tcW w:type="dxa" w:w="1185"/>
									<w:vMerge w:val="restart"/>
									<w:shd w:color="auto" w:fill="C0C0C0" w:val="clear"/>
									<w:vAlign w:val="center"/>
								</w:tcPr>
								<w:p>
									<w:r>
										<w:t>申请人</w:t>
									</w:r>
									<w:r>
										<w:br w:type="textWrapping"/>
									</w:r>
									<w:bookmarkStart w:id="0" w:name="_GoBack"/>
									<w:r>
										<w:t>信用等级</w:t>
									</w:r>
									<w:bookmarkEnd w:id="0"/>
								</w:p>
							</w:tc>
						</w:tr>
					</w:tbl>

大体对标签字段解释如下:

<w:tbl>:表格开始
<w:tblPr> :表格属性定义
<w:tblGrid>:表格单元格定义,里面定义着从左至右每个最小单元格的宽度,如果某个单元格span为2,则会在cell中定义<w:gridSpan w:val=“2”/>
<w:trPr>:表格每一行的属性定义
<w:tc>:表格中某个单元格的属性定义
<w:vMerge w:val=“restart”/>:表示这个单元格跨行,并且是跨行的第一个cell
<w:vMerge w:val=“continue”/>:表示这个单元格跨行,但不是跨行的第一个cell
<w:p>:word中的一个段落
<w:r>:段落中的一个格式一致的文本块
我们使用poi包中的方法对xml文件中的字段进行解析,抽取出文件中的结构,并根据位置信息填充必要的结构信息。

  1. 抽取文字元素
// 读取docx文字部分
StringBuilder docxText = new StringBuilder();
Iterator<IBodyElement> iter = docx.getBodyElementsIterator();
int count = 0;
while (iter.hasNext()) {
    IBodyElement element = iter.next();
    if (element instanceof XWPFParagraph) {
        // 获取段落元素
        XWPFParagraph paragraph = (XWPFParagraph) element;
        String text = paragraph.getText();
        if (StringUtils.isBlank(text)) {
            continue;
        }
        // 将word中的特有字符转化为普通的换行符、空格符等
        String textWithSameBlankAndBreak = text.replaceAll(WORD_BLANK, " ").replaceAll(WORD_LINE_BREAK, "\n")
                .replaceAll("\n+", "\n");
        // 去除word特有的不可见字符
        String textClearBeginBlank = textWithSameBlankAndBreak.replaceAll(regexClearBeginBlank, "");
        // 为抽取的每一个段落加上\n作为换行符标识
        docxText.append(textClearBeginBlank).append("\n");
    } else if (element instanceof XWPFTable) {
        try {
            // 获取表格中的原始文字 默认文字中不加入表格文字 取消注释可加入
            /*String text = originTableTextList.get(count);
            docxText.append(text);*/
            count++;
        } catch (Exception e) {
            log.error("docx抽表数据与对应的表格位置不一致");
        }
    }
}
  1. 抽取表格元素
    docx抽取表格的长宽主要使用两种方法,优先采用表格边框法:
  • 表格边框法:根据[相关规范]中的<w:tblPrEx>
  • 单元格法:根据[相关规范]中的<w:tcPr>
  • span:表示跨单元格,有rowspan和colspan两种
List<WordTable> allWordTableCellList = new ArrayList<>();
Iterator<XWPFTable> it = docx.getTablesIterator();
// 抽取表中的文字集合
List<String> originTableTextList = new ArrayList<>();
while (it.hasNext()) {
    try {
        XWPFTable table = it.next();
        WordTable wordTable = new WordTable();
        List<WordTableCell> wordTableCellList = new ArrayList<>();
        // 默认每个表格左上角的位置为(0,0)
        float x = 0.0f;
        float y = 0.0f;
        // TblGridExist是记录表格的边框 如果存在的话用它来计算单元格宽度很准 但是不一定存在 else 会使用单元格法
        boolean isTblGridExist = true;
        // 一种计算width的方式,表格边框法
        List<CTTblGridCol> tableGridColList = null;
        try {
            // 尝试读取表格网格信息
            tableGridColList = table.getCTTbl().getTblGrid().getGridColList();
        } catch (Exception e) {
            log.info("该docx表格无边框");
            isTblGridExist = false;
        }
        // 采用表格边框法
        if (isTblGridExist) {
            for (int i = 0; i < table.getNumberOfRows(); i++) {
                int colNums = table.getRow(i).getTableCells().size();
                int currentRowHeight = getDocxRowHeight(table, i) / DEFAULT_DIV;
                for (int j = 0, minCellNums = 0; j < colNums; j++) {
                    XWPFTableCell cell = table.getRow(i).getCell(j);
                    int spanNumber = 1;
                    // 表示colspan
                    BigInteger girdSpanBigInteger;
                    try {
                        girdSpanBigInteger = cell.getCTTc().getTcPr().getGridSpan().getVal();
                    } catch (Exception e) {
                        girdSpanBigInteger = null;
                    }
                    if (girdSpanBigInteger != null) {
                        spanNumber = girdSpanBigInteger.intValue();
                    }
                    int widthByGrid = 0;
                    for (int k = 0; k < spanNumber; k++) {
                        widthByGrid += tableGridColList.get(minCellNums + k).getW().intValue();
                    }
                    int width = widthByGrid / DEFAULT_DIV;
                    minCellNums += spanNumber;

                    if (!docxIsContinue(cell)) {
                        int height = this.getDocxCellHeight(table, currentRowHeight, i, j);
                        WordTableCell wordTableCell = this
                                .buildWordCellContent((float) height, (float) width, cell.getText(),
                                        DEFAULT_FONT_SIZE, x, y);
                        wordTableCellList.add(wordTableCell);
                    }
                    x += width;
                }
                if (i + 1 == table.getNumberOfRows()) {
                    wordTable.setHeight(y);
                    wordTable.setWidth(x);
                }
                x = 0.0f;
                y += currentRowHeight;
            }
        } else {
            // 另一种查看width方式,单元格法
            for (int i = 0; i < table.getNumberOfRows(); i++) {
                int colNums = table.getRow(i).getTableCells().size();
                int currentRowHeight = getDocxRowHeight(table, i) / DEFAULT_DIV;
                for (int j = 0; j < colNums; j++) {
                    XWPFTableCell cell = table.getRow(i).getCell(j);
                    int width = getDocxCellWidth(table, i, j) / DEFAULT_DIV;
                    if (width <= 0) {
                        // tableGridMethod = true;
                        width = DEFAULT_WIDTH;
                    }
                    if (!docxIsContinue(cell)) {
                        int height = this.getDocxCellHeight(table, currentRowHeight, i, j);
                        WordTableCell wordTableCell = this
                                .buildWordCellContent((float) height, (float) width, cell.getText(),
                                        DEFAULT_FONT_SIZE, x, y);
                        wordTableCellList.add(wordTableCell);
                    }
                    x += width;
                }
                if (i + 1 == table.getNumberOfRows()) {
                    wordTable.setHeight(y);
                    wordTable.setWidth(x);
                }
                x = 0.0f;
                y += currentRowHeight;
            }
        }

        wordTable.setWordTableCellList(wordTableCellList);
        allWordTableCellList.add(wordTable);
        // 以下代码为为抽取的文字中加入表格文字
        /* 
        String originTableText = "<tb>\n" + table.getText().replaceAll(WORD_TABLE_FILTER, "") + "</tb>\n";
        originTableTextList.add(originTableText);
        */
    } catch (Exception e) {
        log.error("docx表格解析错误", e);
    }
}
// 为表格加入行列信息
allWordTableCellList.forEach(this::fillSpan);
// 开始抽取doc中的文字
StringBuilder docText = new StringBuilder();
for (int i = 0; i < range.numParagraphs(); i++) {
    Paragraph paragraph = range.getParagraph(i);
    // 拿出段落中不包括表格的文字
    if (!paragraph.isInTable()) {
        String text = paragraph.text();
        if (StringUtils.isBlank(text)) {
            continue;
        }
        String textWithSameBlankAndBreak = text.replaceAll(WORD_BLANK, " ").replaceAll(WORD_LINE_BREAK, "\n");
        String clearBeginBlank = textWithSameBlankAndBreak.replaceAll(regexClearBeginBlank, "");
        docText.append(clearBeginBlank).append("\n");
    } else {
        try {
            // 寻找表格的开始位置和结束位置
            int index = i;
            int endIndex = index;
            // 拿出表格中文字
            StringBuilder tableOriginText = new StringBuilder(paragraph.text());
            for (; index < range.numParagraphs(); index++) {
                Paragraph tableParagraph = range.getParagraph(index);
                if (!tableParagraph.isInTable() || tableParagraph.getTableLevel() < 1) {
                    endIndex = index;
                    break;
                } else {
                    tableOriginText.append(tableParagraph.text());
                }
            }
            i = endIndex - 1;
            // 过滤掉表格中所有不可见符号
            String tableOriginTextWithoutBlank = tableOriginText.toString().replaceAll(WORD_TABLE_FILTER, "");
            // 默认不加入表格中字体
            // docText.append("<tb>").append(tableOriginTextWithoutBlank).append("</tb>").append("\n");
        } catch (Exception e) {
            log.error("doc抽表数据与对应的表格位置不一致");
        }
private int getDocCellToLeftWidth(Table table, int row, int col) {
    int leftWidth = 0;
    for (int i = 0; i < col; i++) {
        leftWidth += getDocCellWidth(table, row, i);
    }
    return leftWidth;
}

private int getDocCellWidth(Table table, int row, int col) {
    int width = table.getRow(row).getCell(col).getWidth() / DEFAULT_DIV;
    if (width < 0) {
        width = Math.abs(width);
        log.info("doc取出的宽度为负数");
    }
    return width == 0 ? DEFAULT_WIDTH : width;
}

private int getDocRowHeight(Table table, int row) {
    int height = table.getRow(row).getRowHeight();
    if (height < 0) {
        log.info("出现height小于0");
        height = Math.abs(height);
    }
    return height == 0 ? DEFAULT_HEIGHT : height;
}

/**
 * 只会传isRestart进来 判断往下是不是continue
 */
private int getDocContinueRowHeight(Table table, int row, int col, int rowHeight) {
    int nextRow = row + 1;
    if (nextRow >= table.numRows()) {
        return rowHeight;
    }
    int nextRowHeight = getDocRowHeight(table, nextRow) / DEFAULT_DIV;
    int nextColNums = table.getRow(nextRow).numCells();
    for (int j = 0; j < nextColNums; j++) {
        TableCell nextRowCell = table.getRow(nextRow).getCell(j);
        if (docIsContinue(nextRowCell) && getDocCellWidth(table, nextRow, j) == getDocCellWidth(table, row, col)
                && getDocCellToLeftWidth(table, nextRow, j) == getDocCellToLeftWidth(table, row, col)) {
            rowHeight += nextRowHeight;
            return getDocContinueRowHeight(table, nextRow, j, rowHeight);
        }
    }
    return rowHeight;
}

/**
 * 是否行合并单元格,但不是第一个
 */
private boolean docIsContinue(TableCell cell) {
    return cell.isVerticallyMerged() && !cell.isFirstVerticallyMerged();
}

/**
 * 行合并单元格且为第一个
 */
private boolean docIsRestart(TableCell cell) {
    return cell.isFirstVerticallyMerged();
}

先写到这里,后面看有没有人有word抽取结构化的需求再决定是否继续写下去。
相关代码已上传至github,有帮助的话希望点个star,谢谢了。有问题欢迎留言或者私信我,有空我会解答。
https://github.com/boyonger/word-extractor

  • 24
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
如果你想要从Word文档读取表格并将其数据映射到Java对象,可以使用Apache POI的XWPFTable类和XWPFTableRow类。 以下是一个示例代码,演示如何读取Word文件表格,并将其数据映射到Java对象: ```java import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.List; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableRow; public class WordTableReader { public static void main(String[] args) { try { // Read the Word file File file = new File("path/to/word/file.docx"); FileInputStream fis = new FileInputStream(file); XWPFDocument docx = new XWPFDocument(fis); // Get the first table in the document XWPFTable table = docx.getTables().get(0); // Define a list to store the table data List<TableRowData> tableData = new ArrayList<TableRowData>(); // Iterate through the rows of the table for (int i = 1; i < table.getNumberOfRows(); i++) { XWPFTableRow row = table.getRow(i); // Map the row data to a Java object TableRowData rowData = new TableRowData(); rowData.setField1(row.getCell(0).getText()); rowData.setField2(row.getCell(1).getText()); rowData.setField3(row.getCell(2).getText()); rowData.setField4(row.getCell(3).getText()); // Add the row data to the list tableData.add(rowData); } // Close the connections fis.close(); docx.close(); // Print the table data System.out.println(tableData); } catch (Exception e) { e.printStackTrace(); } } } class TableRowData { private String field1; private String field2; private String field3; private String field4; public String getField1() { return field1; } public void setField1(String field1) { this.field1 = field1; } public String getField2() { return field2; } public void setField2(String field2) { this.field2 = field2; } public String getField3() { return field3; } public void setField3(String field3) { this.field3 = field3; } public String getField4() { return field4; } public void setField4(String field4) { this.field4 = field4; } @Override public String toString() { return "TableRowData [field1=" + field1 + ", field2=" + field2 + ", field3=" + field3 + ", field4=" + field4 + "]"; } } ``` 这个示例代码假设你的Word文档包含一个名为“表格1”的表格,该表格有四列和多行(至少有一行)。示例代码使用XWPFTable类的getNumberOfRows()方法获取表格的行数,并使用XWPFTableRow类的getCell()方法获取行的单元格。然后,将每一行的数据映射到一个Java对象,该Java对象包含与表格列对应的字段。最后,将Java对象添加到列表并打印出来。 你可以根据自己的需求修改示例代码,例如更改表格名称、列数和字段名称等。
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值