Java解析word,获取文档中图片位置
前言(背景介绍):
Apache POI是Apache基金会下一个开源的项目,用来处理office系列的文档,能够创建和解析word、excel、ppt格式的文档。
其中对word文档的处理有两个技术,分别是HWPF(.doc)和XWPF(.docx)。如果你对这两个技术熟悉的话,就应该能明白使用java解析word文档的痛楚所在。
其中两个最大的问题在于:
第一是这两个类并没有统一的父类和接口(隔壁的XSSF和HSSF投过来鄙视的眼光),所以没法进行同一格式的接口式编程;
第二是官方API中并没有文档中图片相对位置的接口,这就导致了虽然你能获得文档中的所有图片,但是你并不能知道这些图片是在哪里,将来要展示图片就没法插入到正确的位置。
对于第一点,我是没什么办法,可以研究下其他相关技术,比如jacob,doc4j等看看有没有其他的解决方案,不过doc4j这货貌似只能处理2007文档(.docx)。
对于第二点,本文将给出笔者的解决方案,实际上,这也是我写本文的目的所在。
注意:简单求快的同学看第二章和第三章就行了;
一、预备知识
1.word文档的两种格式对应两种不同的存储方式
众所周知,word文档有两种存储格式:doc和docx
doc:习惯上称为Word2003,使用二进制储存数据;这个不是我们今天讨论的重点.
docx:word2007,使用xml来存储数据和格式.
可能你会问了,明明是docx结尾的文档,怎么成了xml格式了?
很简单:你随便选择一个docx文件,右键使用压缩工具打开,就能得到一个这样的目录结构:

所以你以为docx是一个完整的文档,其实它只是一个压缩文件。(docx:?_?)
2.Word文档中xml的定义格式:
从前面我们知道了docx文档使用压缩文件也就是xml来描述数据,那么word文档中的数据具体是怎么定义的呢?
出于篇幅的关系,这里不会详细地描述整个压缩的文档,这里只简单介绍下两个文件/文件夹:
一是word目录下的documen.xml文件,这个就是整个文档内容的定义;
二是word目录下的media文件夹,看名字也能猜出来这个文件夹里面是文档中的多媒体内容:


图3:word/document.xml(定义文档内容) 图4:word/media文件夹下的内容
以下是document.xml文档的部分关键内容:
A:document整体结构定义:
<w:document mc:ignorable="w14 w15 wp14" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpscustomdata="http://www.wps.cn/officeDocument/2013/wpsCustomData">
<w:body>
<w:p>
<w:ppr>
<w:pstyle w:val="2">
</w:pstyle>
<w:keepnext w:val="0">
</w:keepnext>
<w:keeplines w:val="0">
</w:keeplines>
<w:widowcontrol>
</w:widowcontrol>
<w:suppresslinenumbers w:val="0">
</w:suppresslinenumbers>
<w:pbdr>
<w:top w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:top>
<w:left w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:left>
<w:bottom w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:bottom>
<w:right w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:right>
</w:pbdr>
B:文档段落内容:
<w:p>
<w:ppr>
<w:pstyle w:val="2">
</w:pstyle>
<w:keepnext w:val="0">
</w:keepnext>
<w:keeplines w:val="0">
</w:keeplines>
<w:widowcontrol>
</w:widowcontrol>
<w:suppresslinenumbers w:val="0">
</w:suppresslinenumbers>
<w:pbdr>
<w:top w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:top>
<w:left w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:left>
<w:bottom w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:bottom>
<w:right w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:right>
</w:pbdr>
<w:shd w:fill="FAFAFA" w:val="clear">
</w:shd>
<w:spacing w:after="150" w:afterautospacing="0" w:before="150" w:beforeautospacing="0" w:line="378" w:linerule="atLeast">
</w:spacing>
<w:ind w:firstline="0" w:left="0" w:right="0">
</w:ind>
<w:rpr>
<w:rfonts w:ascii="Verdana" w:cs="Verdana" w:hansi="Verdana" w:hint="default">
</w:rfonts>
<w:i w:val="0">
</w:i>
<w:caps w:val="0">
</w:caps>
<w:color w:val="404040">
</w:color>
<w:spacing w:val="0">
</w:spacing>
<w:sz w:val="21">
</w:sz>
<w:szcs w:val="21">
</w:szcs>
</w:rpr>
</w:ppr>
<w:r>
<w:rpr>
<w:rfonts w:ascii="Verdana" w:cs="Verdana" w:hansi="Verdana" w:hint="default">
</w:rfonts>
<w:i w:val="0">
</w:i>
<w:caps w:val="0">
</w:caps>
<w:color w:val="404040">
</w:color>
<w:spacing w:val="0">
</w:spacing>
<w:sz w:val="21">
</w:sz>
<w:szcs w:val="21">
</w:szcs>
<w:bdr w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:bdr>
<w:shd w:fill="FAFAFA" w:val="clear">
</w:shd>
</w:rpr>
<w:t>
作者: Brian Dear
</w:t>
</w:r>
</w:p>
C:图片内容定义:
<w:r>
<w:rpr>
<w:rfonts w:ascii="Verdana" w:cs="Verdana" w:hansi="Verdana" w:hint="default">
</w:rfonts>
<w:i w:val="0">
</w:i>
<w:caps w:val="0">
</w:caps>
<w:color w:val="404040">
</w:color>
<w:spacing w:val="0">
</w:spacing>
<w:sz w:val="21">
</w:sz>
<w:szcs w:val="21">
</w:szcs>
<w:bdr w:color="auto" w:space="0" w:sz="0" w:val="none">
</w:bdr>
<w:shd w:fill="FAFAFA" w:val="clear">
</w:shd>
</w:rpr>
<w:drawing>
<wp:inline distb="0" distl="114300" distr="114300" distt="0">
<wp:extent cx="5543550" cy="5543550">
</wp:extent>
<wp:effectextent b="0" l="0" r="0" t="0">
</wp:effectextent>
<wp:docpr descr="IMG_256" id="1" name="Picture 1">
</wp:docpr>
<wp:cnvgraphicframepr>
<a:graphicframelocks nochangeaspect="1" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
</a:graphicframelocks>
</wp:cnvgraphicframepr>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicdata uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvpicpr>
<pic:cnvpr descr="IMG_256" id="1" name="Picture 1">
</pic:cnvpr>
<pic:cnvpicpr>
<a:piclocks nochangeaspect="1">
</a:piclocks>
</pic:cnvpicpr>
</pic:nvpicpr>
<pic:blipfill>
<a:blip r:embed="rId4">
</a:blip>
<a:stretch>
<a:fillrect>
</a:fillrect>
</a:stretch>
</pic:blipfill>
<pic:sppr>
<a:xfrm>
<a:off x="0" y="0">
</a:off>
<a:ext cx="5543550" cy="5543550">
</a:ext>
</a:xfrm>
<a:prstgeom prst="rect">
<a:avlst>
</a:avlst>
</a:prstgeom>
<a:nofill>
</a:nofill>
<a:ln w="9525">
<a:nofill>
</a:nofill>
</a:ln>
</pic:sppr>
</pic:pic>
</a:graphicdata>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
有兴趣的童鞋可以看一下上面三段xml代码,我这里直接给结论了:
word文档shema文件:xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
文档根节点:<w:document> 定义了整个文档的开始
<w:body>是document的子节点,文档的主体内容
<w:p>body子节点,一个段落,就是word文档中的段落
<w:r>P元素的子节点,一个Run定义了段落中具有相同格式的一段内容
<w:t>Run元素节点的子节点,就是文档的内容.
<w:drawing> run元素的子节点,定义了一张图片:
<w:inline> drawing子节点,具体应用也没有深入研究
<a:graphic> 定义图片内容
<pic:blipfill>这个是graphic文档的子节点,定义了图片内容的索引,具体来说,poi能根据这个名称拿到图片所对应的资源,而获取文档图片位置的关键也就在这里

总体看来:XWPF解析docx文档就是做了xml文档的解析,将所有的节点保存下来,然后转换成更加好用的属性,提供API出来供用户使用.
所以我们就能用POI提供给我们的接口拿到文档内容,自己去解析文档中的数据,就能获取到图片是在哪一个段落里了,当然你也可以得知图片是位于哪一个Run元素的后面.
二、实现
package com.szdfhx.reportStatistic.util;
import com.microsoft.schemas.vml.CTShape;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPictureData;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.drawingml.x2006.main.CTGraphicalObject;
import org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture;
import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import java.util.ArrayList;
import java.util.List;
import java.util.Map; public class XWPFUtils { //获取某一个段落中的所有图片索引
public static List<String> readImageInParagraph(XWPFParagraph paragraph) {
//图片索引List
List<String> imageBundleList = new ArrayList<String>(); //段落中所有XWPFRun
List<XWPFRun> runList = paragraph.getRuns();
for (XWPFRun run : runList) {
//XWPFRun是POI对xml元素解析后生成的自己的属性,无法通过xml解析,需要先转化成CTR
CTR ctr = run.getCTR(); //对子元素进行遍历
XmlCursor c = ctr.newCursor();
//这个就是拿到所有的子元素:
c.selectPath("./*");
while (c.toNextSelection()) {
XmlObject o = c.getObject();
//如果子元素是<w:drawing>这样的形式,使用CTDrawing保存图片
if (o instanceof CTDrawing) {
CTDrawing drawing = (CTDrawing) o;
CTInline[] ctInlines = drawing.getInlineArray();
for (CTInline ctInline : ctInlines) {
CTGraphicalObject graphic = ctInline.getGraphic();
//
XmlCursor cursor = graphic.getGraphicData().newCursor();
cursor.selectPath("./*");
while (cursor.toNextSelection()) {
XmlObject xmlObject = cursor.getObject();
// 如果子元素是<pic:pic>这样的形式
if (xmlObject instanceof CTPicture) {
org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture picture = (org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture) xmlObject;
//拿到元素的属性
imageBundleList.add(picture.getBlipFill().getBlip().getEmbed());
}
}
}
}
//使用CTObject保存图片
//<w:object>形式
if (o instanceof CTObject) {
CTObject object = (CTObject) o;
System.out.println(object);
XmlCursor w = object.newCursor();
w.selectPath("./*");
while (w.toNextSelection()) {
XmlObject xmlObject = w.getObject();
if (xmlObject instanceof CTShape) {
CTShape shape = (CTShape) xmlObject;
imageBundleList.add(shape.getImagedataArray()[0].getId2());
}
}
}
}
}
return imageBundleList;
} }
首先要提出来是XWPF对xml元素的封装:
<w:document> 对应XWPFDocument类
<w:run>对应XWPFRun类
基本上只对应到Run这一层,因为run的子元素有很多,所以没有再往下面的层次封装和定义了,
所以我们使用API只能拿到所有的XWPFRun对象转成它的xml的定义:CTR对象。最后利用CTR去读取和解析的Run元素中的内容,获取图片的索引。
其次要谈的则是整个XML元素的定义:
我们可以看到POI使用的是Apache下的xmlbeans这个技术解析的XML,相关的技术不做深谈,关键要明白两点:
1:xml文档中的所有元素经过xmlbean是封装后都继承了一个XMLObject的接口,所以可以用这个类来接收获取到的子元素;
2:元素遍历是通过XmlCursor来做的,具体获取子元素是根据XmlCursor对象的selectPath属性来控制,当selectPath为"./*"时就定义为遍历子元素;
所以写成了如下的代码:能遍历当前元素的子元素,并且检验子元素的类型:
CTR ctr = run.getCTR();
//对子元素进行遍历
XmlCursor c = ctr.newCursor();
//这个就是拿到所有的子元素:
c.selectPath("./*");
while (c.toNextSelection()) {
XmlObject o = c.getObject();
//如果子元素是<w:drawing>这样的形式,使用CTDrawing保存图片
if (o instanceof CTDrawing) {
CTDrawing drawing = (CTDrawing) o;
最后你可能会有疑问,不是说<w:drawing>这个元素定义了一张图片吗?
那么
if (o instanceof CTObject) {
CTObject object = (CTObject) o;
...
}
这个第二个判断条件是用来干嘛的?
聪明的你应该已经猜到了
没错!docx文档中的xml定义图片的方式除了<w:drawing>这一种之外,还可以运用<w:object>元素去定义,
为什么只有这两种?
因为我只使用第一种方式解析,发现有些图片丢失了,于是发现了第二种方式.......也许不止两种?我也不知道,反正对于目前的我来说已经没有问题了.
或许聪明的你在实践中还遇到了更多种情况?
那么运用上面提到的xml解析方式,相信你也能正确读取,得到自己想要的索引值.
再拓宽一点,如果POI还有其他没有提供的API,我们是不是也能通过XML解析的技术自己实现呢?这个就需要我们在实践中去探索了,相信时间会给我们答案
好了,现在我们拿到了索引值,那么如何去拿到图片资源呢?
POI提供了现成的方法:
XWPFDocument类中有getPictureDataByID(String picture);方法可以拿到XWPFPictrueDate对象,这个就是图片的资源了.
具体的操作可以参阅相关的博文和API,这里就不详细介绍了.
三、测试:
使用Junit4测试的代码:
package com.szdfhx.reportStatistic.util; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFPictureData;
import org.junit.Test; import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List; import static org.junit.Assert.*; public class XWPFUtilsTest { @Test
public void readImageInParagraph() throws IOException {
InputStream in = new FileInputStream("D:\\Document\\我的博客\\Java解析word,获取文档中图片位置\\示例.docx");
XWPFDocument xwpfDocument = new XWPFDocument(in);
List<XWPFParagraph> paragraphList = xwpfDocument.getParagraphs();
System.out.println("图片的索引\t|图片名称\t|图片上一段文字的内容\t");
System.out.pringln("------------------------------------------");
for(int i = 0;i < paragraphList.size();i++){
List<String> imageBundleList = XWPFUtils.readImageInParagraph(paragraphList.get(i));
if(CollectionUtils.isNotEmpty(imageBundleList)){
for(String pictureId:imageBundleList){
XWPFPictureData pictureData = xwpfDocument.getPictureDataByID(pictureId);
String imageName = pictureData.getFileName();
String lastParagraphText = paragraphList.get(i-1).getParagraphText();
System.out.println(pictureId +"\t|" + imageName + "\t|" + lastParagraphText);
}
}
}
} }
展示结果:

这里使用图片名称指代表明我拿到了对应的资源,实际上 如果你对前文的内容还熟悉的话,会发现图片的名称实际上就是word/media文件夹下的所有图片的全名称。
在对应的XWPFPictureData对象中,图像的二进制数据可以通过getData()属性来拿到,这样你就可以保存到数据库或者是你本地的文件夹中了!
四、其他:
谈到这里,开头提到的第二个问题这里就已经解决了。
那么,第一个问题怎么办呢?
如果你的系统对速度要求不高的话,那么我给你的建议是,把doc文档转化成docx文档来解析--POI就有成熟的API来做
如果要考虑性能的话,那就只好写两套方法去解析文档。
那么......doc类型的word文档怎么获取图片的相对位置呢?
我也不知道········或者,你来告诉我?
参考:
POI官网:https://poi.apache.org/
Apache POI Word - 快速指南 :https://www.w3cschool.cn/apache_poi_word/apache_poi_word_quick_guide.html
Java解析word,获取文档中图片位置的更多相关文章
- php解析word,获得文档中的图片
背景 前段时间在写一个功能:用原生php将获得word中的内容并导入到网站系统中.因为文档中存在公式,图片,表格等,因此写的比较麻烦. 思路 大体思路是先将word中格式为doc的文档转化为docx, ...
- java POI往word文档中指定位置插入表格
1.Service demo import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.a ...
- jquery获取元素在文档中的位置信息以及滚动条位置(转)
jquery获取元素在文档中的位置信息以及滚动条位置 http://blog.csdn.net/qq_34095777/article/details/78750886 原文链接 原创 201 ...
- javaScript获取文档中所有元素节点的个数
HTML+JS 代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...
- html中如何获取元素在文档中的位置
html中如何获取元素在文档中的位置 一.总结 一句话总结: $("#elem").offset().top $("#elem").offset().left ...
- dom4j解析xml报"文档中根元素后面的标记格式必须正确"
今天,在写个批量启动报盘机的自动化应用,为了简化起见,将配置信息存储在xml中,格式如下: <?xml version="1.0" encoding="UTF-8& ...
- 使用compareDocumentPosition比较两个元素在文档中的位置
PS:尊重原创,转载请注明来自http://www.cnblogs.com/Raoh/p/js_compareDocumentPosition_between_two_node.html 使用comp ...
- 可以粘贴Word文档中图片的编辑器
Chrome+IE默认支持粘贴剪切板中的图片,但是我要发布的文章存在word里面,图片多达数十张,我总不能一张一张复制吧?Chrome高版本提供了可以将单张图片转换在BASE64字符串的功能.但是无法 ...
- Java POI Word 写文档
package apache.poi; import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import ...
随机推荐
- debug断点调试
debug断点调试 1,虫子启动2,F6 执行断点的下一步,下一个语句 F5 进入方法 F8 执行到结束 查看表达式的值:选中查看的表达式,接着按 ctrl ...
- Less变量
Less变量 定义变量 Less 中的变量和其他编程语言一样,可以实现值的复用,同样它也有作用域(scope).简单的讲,变量作用域就是局部变量和全局变量的概念. Less 中,变量作用域采用的是就近 ...
- jquery类数组结构学习笔记
大家都知道我们使用$()产生的jquery对象可以使用下标去获取对应下标的元素. 举个栗子,一个页面有5个div标签,我们使用$("div")去获取到这些元素,然后我们就可以使用$ ...
- 自己动手编写IOC框架(四)
终于到了激动人心的时刻了,首先感谢小伙伴们的阅读,如果能多点评论,多点探讨就更好了,没有交流让我觉得我写的东西只有标题有点价值,内容只是在浪费大家的时间.为了泪滴下周能写下一个框架orm,请小伙伴们能 ...
- poj 2528 Mayor's posters 线段树+离散化技巧
poj 2528 Mayor's posters 题目链接: http://poj.org/problem?id=2528 思路: 线段树+离散化技巧(这里的离散化需要注意一下啊,题目数据弱看不出来) ...
- 初窥c++11:lambda函数及其用法
转载于:点击打开链接 为什么需要lambda函数 匿名函数是许多编程语言都支持的概念,有函数体,没有函数名.1958年,lisp首先采用匿名函数,匿名函数最常用的是作为回调函数的值.正因为有这样的需求 ...
- 前端测试框架Jest系列教程 -- Mock Functions
写在前面: 在写单元测试的时候有一个最重要的步骤就是Mock,我们通常会根据接口来Mock接口的实现,比如你要测试某个class中的某个方法,而这个方法又依赖了外部的一些接口的实现,从单元测试的角度来 ...
- 可以在手机上看电脑本地html步骤,我自己总结的哦!
1.打开控制面板 2.打开程序和功能 3.打开或关闭功能 4.internet信息服务展开后里面所有的都要选中 5.回到桌面,然后右键计算机,选择'管理' 6.先在E盘或者D盘创建一个文件夹,自己随意 ...
- NodeJS爬虫入门
1. 写在前面 往常都是利用 Python/.NET 语言实现爬虫,然现在作为一名前端开发人员,自然需要熟练 NodeJS.下面利用 NodeJS 语言实现一个糗事百科的爬虫.另外,本文使用的部分代码 ...
- 【NOIP2014提高组】飞扬的小鸟
https://www.luogu.org/problem/show?pid=1941 从某一点开始飞直到飞出地图最少点击屏幕的次数,显然只和该点的坐标唯一相关,没有后效性,考虑用DP解.令f(i,j ...