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坐标为

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.16</version>
</dependency>

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

POI中jar包

注意

  • 请将poi-ooxml-schemas-3.16.jar,替换为ooxml-schemas-1.1.jar,Maven坐标
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>ooxml-schemas</artifactId>
    <version>1.1</version>
</dependency>

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

       <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.16</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.poi/ooxml-schemas -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>ooxml-schemas</artifactId>
            <version>1.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-contrib</artifactId>
            <version>3.6</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-excelant</artifactId>
            <version>3.16</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>3.16</version>
        </dependency>

这是因为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<String, Integer> 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<XWPFRun> 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<String, Integer> findSubRunPosInParagraph(XWPFParagraph xwpfParagraph, String substring) {

    List<XWPFRun> 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<String, Integer> 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<XWPFParagraph> xwpfParagraphList, Map<String, String> 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<String, Integer> 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<XWPFRun> 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<String, Integer> findSubRunPosInParagraph(XWPFParagraph xwpfParagraph, String substring) {

    List<XWPFRun> 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<String, Integer> 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<XWPFTable> xwpfTableList, Map<String, String> params) {
    for (XWPFTable table : xwpfTableList) {
        replaceInTable(table, params);

    }
}

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


/**
 * 替换表格中的一行
 *
 * @param rows
 * @param params
 */
public static void replaceInRows(List<XWPFTableRow> rows, Map<String, String> 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<XWPFTableCell> xwpfTableCellList, Map<String, String> params) {
    for (XWPFTableCell cell : xwpfTableCellList) {
        replaceInCell(cell, params);
    }
}

/**
 * 替换表格中每一行中的每一个单元格中的所有段落
 *
 * @param cell
 * @param params
 */
public static void replaceInCell(XWPFTableCell cell, Map<String, String> params) {
    List<XWPFParagraph> 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<String, String> map = new HashMap<String, String>();
    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<XWPFParagraph> parasList = document.getParagraphs();
    replaceInAllParagraphs(parasList, map);

   /**
     * 对表格中的标记进行替换
     */
    List<XWPFTable> 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();
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容