Apache POI XWPF 爬坑指南之一文本替换

说点废话


前段时间使用Jacob做Word生成,Jacob调用COM组件生成Word文档,可以实现很多强大的功能,甚至能从无到有生成一个全新的格式全面的文档。但是,局限的是需要熟练地掌握VBA,学习成本太高,而且Jacob配置复杂,平台依赖性太大,只能运行在Windows系统上。故来研究下新的工具——Apache POI。

俗话说“Apache出品,必属精品”,POI很好的验证了这一点。POI可以操作MSOffice中常用的三件套Word、Excel、PowerPoint,并且支持2007以上的版本。因为项目针对Word,所以只研究了下POI中XWPF的一些特性,从而达到代码操作生成Word的效果。

现在都2017年了,很难想象还有人继续用着Word 2003(本科室友),未来趋势肯定是Word 2007以上版本,.docx文件成为主流。使用XWPF简单点是以一个旧的Word文档为模板,在里面做好标记,然后进行文本替换。

在进行替换之前,先讲一下一个.docx文件实质上是用XML格式存储起来的数据结构,POI就是对这个XML数据结构进行操作。

POI小贴士


本文所用POI版本为3.16,Maven坐标为


    org.apache.poi
    poi
    3.16

如果从官网下载的,主要jar包如下所示

POI中jar包

注意

  • 请将poi-ooxml-schemas-3.16.jar,替换为ooxml-schemas-1.1.jar,Maven坐标

    org.apache.poi
    ooxml-schemas
    1.1

整个项目所需的jar包Maven坐标如下

       
            org.apache.poi
            poi
            3.16
        
        
        
            org.apache.poi
            ooxml-schemas
            1.1
        

        
            org.apache.poi
            poi-contrib
            3.6
        

        
            org.apache.poi
            poi-excelant
            3.16
        
        
            org.apache.poi
            poi-scratchpad
            3.16
        

这是因为POI自带的jar包是精简版,有些底层的东西都不见了,ooxml-schemas-1.1.jar大小将近20M,可以完全满足生产需求。

基本操作


  • 1、打开、关闭、另存为.docx文档
String filapath="***.docx";
String destpath="***_dest.docx";
XWPFDocument document = new XWPFDocument(POIXMLDocument.openPackage(filepathString));
FileOutputStream outStream = null;
try {
    outStream = new FileOutputStream(destpath);
    document.write(outStream);
    outStream.flush();
    outStream.close();
} catch (IOException e) {
    e.printStackTrace();
}

其中document不关闭,因为关闭后对document执行的操作会被保存到原文件。

段落中的文本替换


以文档中每一段为最小单元进行标记文本替换

/**
 * 替换段落中的字符串
 *
 * @param xwpfParagraph
 * @param oldString
 * @param newString
 */
public static void replaceInParagraph(XWPFParagraph xwpfParagraph, String oldString, String newString) {
    Map pos_map = findSubRunPosInParagraph(xwpfParagraph, oldString);
    if (pos_map != null) {
        System.out.println("start_pos:" + pos_map.get("start_pos"));
        System.out.println("end_pos:" + pos_map.get("end_pos"));

        List runs = xwpfParagraph.getRuns();
        XWPFRun modelRun = runs.get(pos_map.get("end_pos"));
        XWPFRun xwpfRun = xwpfParagraph.insertNewRun(pos_map.get("end_pos") + 1);
        xwpfRun.setText(newString);
        System.out.println("字体大小:" + modelRun.getFontSize());
        if (modelRun.getFontSize() != -1) xwpfRun.setFontSize(modelRun.getFontSize());//默认值是五号字体,但五号字体getFontSize()时,返回-1
        xwpfRun.setFontFamily(modelRun.getFontFamily());
        for (int i = pos_map.get("end_pos"); i >= pos_map.get("start_pos"); i--) {
            System.out.println("remove run pos in :" + i);
            xwpfParagraph.removeRun(i);
        }
    }
}

/**
 * 找到段落中子串的起始XWPFRun下标和终止XWPFRun的下标
 *
 * @param xwpfParagraph
 * @param substring
 * @return
 */
public static Map findSubRunPosInParagraph(XWPFParagraph xwpfParagraph, String substring) {

    List runs = xwpfParagraph.getRuns();
    int start_pos = 0;
    int end_pos = 0;
    String subtemp = "";
    for (int i = 0; i < runs.size(); i++) {
        subtemp = "";
        start_pos = i;
        for (int j = i; j < runs.size(); j++) {
            if (runs.get(j).getText(runs.get(j).getTextPosition()) == null) continue;
            subtemp += runs.get(j).getText(runs.get(j).getTextPosition());
            if (subtemp.equals(substring)) {
                end_pos = j;
                Map map = new HashMap<>();
                map.put("start_pos", start_pos);
                map.put("end_pos", end_pos);
                return map;
            }
        }
    }
    return null;
}

在Word文档中段落的最小的操作单位是XWPFRun,正常的一个段落,会被分割成多个小的XWPFRun,这些XWPFRun组合在一起就是一个完整的段落。

通常我们在Word文档中做的标记${mark_1},在文档中会被分割成多个XWPFRun,所以我们没法使用一个XWPFRun来进行标记文本替换。在这里,我们想到一个方法,就是使用类似于找到字符串中子串下标的方法,找到段落XWPFRun中子Run下标,记录起始和终止下标,在终止下标后insertNewRun(int pos),然后再从终止下标往前xwpfParagraph.removeRun(i);到起始下标。

这个方法可以以整个段落位单位进行标记文本替换。然后遍历文档中所有的段落进行替换。
全部代码如下:

/**
 * 替换所有段落中的标记
 *
 * @param xwpfParagraphList
 * @param params
 */
public static void replaceInAllParagraphs(List xwpfParagraphList, Map params) {
    for (XWPFParagraph paragraph : xwpfParagraphList) {
        if (paragraph.getText() == null || paragraph.getText().equals("")) continue;
        for (String key : params.keySet()) {
            if (paragraph.getText().contains(key)) {
                replaceInParagraph(paragraph, key, params.get(key));
            }
        }
    }
}

/**
 * 替换段落中的字符串
 *
 * @param xwpfParagraph
 * @param oldString
 * @param newString
 */
public static void replaceInParagraph(XWPFParagraph xwpfParagraph, String oldString, String newString) {
    Map pos_map = findSubRunPosInParagraph(xwpfParagraph, oldString);
    if (pos_map != null) {
        System.out.println("start_pos:" + pos_map.get("start_pos"));
        System.out.println("end_pos:" + pos_map.get("end_pos"));

        List runs = xwpfParagraph.getRuns();
        XWPFRun modelRun = runs.get(pos_map.get("end_pos"));
        XWPFRun xwpfRun = xwpfParagraph.insertNewRun(pos_map.get("end_pos") + 1);
        xwpfRun.setText(newString);
        System.out.println("字体大小:" + modelRun.getFontSize());
        if (modelRun.getFontSize() != -1) xwpfRun.setFontSize(modelRun.getFontSize());//默认值是五号字体,但五号字体getFontSize()时,返回-1
        xwpfRun.setFontFamily(modelRun.getFontFamily());
        for (int i = pos_map.get("end_pos"); i >= pos_map.get("start_pos"); i--) {
            System.out.println("remove run pos in :" + i);
            xwpfParagraph.removeRun(i);
        }
    }
}


/**
 * 找到段落中子串的起始XWPFRun下标和终止XWPFRun的下标
 *
 * @param xwpfParagraph
 * @param substring
 * @return
 */
public static Map findSubRunPosInParagraph(XWPFParagraph xwpfParagraph, String substring) {

    List runs = xwpfParagraph.getRuns();
    int start_pos = 0;
    int end_pos = 0;
    String subtemp = "";
    for (int i = 0; i < runs.size(); i++) {
        subtemp = "";
        start_pos = i;
        for (int j = i; j < runs.size(); j++) {
            if (runs.get(j).getText(runs.get(j).getTextPosition()) == null) continue;
            subtemp += runs.get(j).getText(runs.get(j).getTextPosition());
            if (subtemp.equals(substring)) {
                end_pos = j;
                Map map = new HashMap<>();
                map.put("start_pos", start_pos);
                map.put("end_pos", end_pos);
                return map;
            }
        }
    }
    return null;
}

对表格中标记文本的替换


有些标记做在表格单元格中,每个单元格中的内容都是一个普通的段落,所以,我们只需遍历出所有的单元格,然后遍历出每个单元格中的所有段落,再调用以上方法进行标记文本替换即可。代码如下

/**
 * 替换所有的表格
 *
 * @param xwpfTableList
 * @param params
 */
public static void replaceInTables(List xwpfTableList, Map params) {
    for (XWPFTable table : xwpfTableList) {
        replaceInTable(table, params);

    }
}

/**
 * 替换一个表格中的所有行
 *
 * @param xwpfTable
 * @param params
 */
public static void replaceInTable(XWPFTable xwpfTable, Map params) {
    List rows = xwpfTable.getRows();
    replaceInRows(rows, params);
}


/**
 * 替换表格中的一行
 *
 * @param rows
 * @param params
 */
public static void replaceInRows(List rows, Map params) {
    for (int i = 0; i < rows.size(); i++) {
        XWPFTableRow row = rows.get(i);
        replaceInCells(row.getTableCells(), params);
    }
}

/**
 * 替换一行中所有的单元格
 *
 * @param xwpfTableCellList
 * @param params
 */
public static void replaceInCells(List xwpfTableCellList, Map params) {
    for (XWPFTableCell cell : xwpfTableCellList) {
        replaceInCell(cell, params);
    }
}

/**
 * 替换表格中每一行中的每一个单元格中的所有段落
 *
 * @param cell
 * @param params
 */
public static void replaceInCell(XWPFTableCell cell, Map params) {
    List cellParagraphs = cell.getParagraphs();
    replaceInAllParagraphs(cellParagraphs, params);
}

调用方法测试

public static void main(String[] args) throws IOException, InvalidFormatException {
    // TODO Auto-generated method stub
    String filepathString = "***.docx";
    String destpathString = "***_result.docx";
    Map map = new HashMap();
    map.put("${text_1}", "I hava a pen");
    map.put("${text_2}", "I have an apple");
    map.put("${text_3}", "pen apple and pen");
    OPCPackage pack = POIXMLDocument.openPackage(filepathString);
    XWPFDocument document = new XWPFDocument(pack);

    /**
     * 对段落中的标记进行替换
     */
    List parasList = document.getParagraphs();
    replaceInAllParagraphs(parasList, map);

   /**
     * 对表格中的标记进行替换
     */
    List tables = document.getTables();
    replaceInTables(tables, map);
    FileOutputStream outStream = null;
    try {
        outStream = new FileOutputStream(destpathString);
        document.write(outStream);
        outStream.flush();
        outStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

你可能感兴趣的:(Apache POI XWPF 爬坑指南之一文本替换)