首先简单介绍一下我开发时的需求,做的是一个考试系统,负责的功能模块是试题导入,导入分为excel导入和word导入,个人认为excel导入很方便,word相对来说麻烦一点,用户体验相对较差,但是怎么办呢,领导要求的啊,别人有的我们也要有,没错,就是这么强势!!!言归正传,需求就是,给用户提供一个word模板,用户在模板里面按照特定的格式将题目信息填写到docx中,上传到系统中,系统将word中的一堆文字解析成对应的实体,存入数据库,其中的难点包括:
1.如何将整个文档的所有文字图片信息分隔成一个个单个的实体数据;
2.如何知道文档中的图片属于哪段文字也就是属于分隔之后的哪个实体信息;
3.就算知道是哪个实体信息中的图片之后,如果有多张图片,如何知道其具体位置;
最开始的需求并不是要求一道题目可以添加多张图片,也就是开始的需求是一道题目只允许用户上传一张图片,作为题干的图片拼接在题干的最后面;后来又改成了不仅题干允许用户自由发挥插入多张图片,选项也是同样需要支持多张图片,这里是最坑的,导致后面代码改动比较大;先说说前者的实现思路吧;
1.首先考虑到docx的格式是主流,所以系统中只支持docx文件格式的word上传,在实现这块功能的时候最开始使用的是poi技术,后面发现poi在excel方面解析还可以,word方面不是很好用,后来选择使用了docx4j,
File file = new File(wordTempPath);
InputStream in = new FileInputStream(file);
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(in);
//获取word所有内容
List<Object> list= wordMLPackage.getMainDocumentPart().getContent();
//将文件中的内容分隔成每一道题
List<Map<String, String>> questionMapList = getSubjectList(list);
getSubjectList方法根据特定的规则将整段的信息分隔成片段
/**
* 将word中的题目分割成list
private List<Map<String, String>> getSubjectList(List<Object> list){
List<Map<String, String>> subjectList = new ArrayList<Map<String,String>>();
StringBuffer subjectItem =new StringBuffer();
int count =0;
int qNum = 0;
//划分题目
//以数字开头并且包含.表示一个新的题目开始
String regex = "^\\d{1,100}\\.";
Pattern pattern = Pattern.compile(regex);
Matcher m = null;
Map<String, String> tempMap = new HashMap<String, String>();
String qtype ="";
String oldQtype = "";
String line ="";
for (int i = 0; i < list.size(); i++) {
line = list.get(i).toString();
m = pattern.matcher(line);
if(m.find()){//题干
count++;
if(qNum>0){//不是文件文件第一个题干,将之前的buffer保存
tempMap = new HashMap<String, String>();
tempMap.put("qtype",oldQtype);
tempMap.put("content", subjectItem.toString());
subjectList.add(tempMap);
oldQtype=qtype;
subjectItem = new StringBuffer();
subjectItem.append(line);
}else{//文件第一个题干,创建新的buffer,并将题干放入buffer
subjectItem = new StringBuffer();
subjectItem.append(line);
}
qNum++;
}else if(line.startsWith("【单选题】")){
qtype = "1";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【多选题】")){
qtype = "2";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【判断题】")){
qtype = "3";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【不定项选择题】")){
qtype = "4";
if(count==0){
oldQtype=qtype;
}
}else{
subjectItem.append(line);
}
List<String> resList = DocxUtils.getShortStr(subjectItem.toString());
/*if(i==list.size()-1){
tempMap = new HashMap();
tempMap.put("qtype",oldQtype);
tempMap.put("content", subjectItem.toString());
tempMap.put("imgMsgList", resList);
subjectList.add(tempMap);
}*/
}
return subjectList;
}
上述步骤完成之后需要将信息解析成具体的实体,然后再进行格式校验,如果格式校验正确那么最后再处理图片问题,毕竟图片处理之后需要上传到服务器,如果格式或者内容不符合导入要求,那么上传图片之后浪费资源;
图片处理步骤:
1.获取所有存在图片的题目的的标号
如上图所示,获取的编号为2
2.根据题目编号将一个word文档切割成多个word文档,这样处理的原因是如果想要获取word中的图片信息,只找到一次性获取所有的图片信息的方法,那么就导致图片信息和题目无法对应上,也就是你获得了图片信息,但是你不知道是哪个题目的图片;所以将整个文档根据题目分割成多个word文档,再进行获取图片信息,那么两者就能对应上了;
/**
* 将word切割
* @param in
* @return
* @throws Docx4JException
*/
private void splitWord(HttpServletRequest request) throws Docx4JException{
InputStream in = null;
try {
in = new FileInputStream(new File(request.getSession().getAttribute("wordRootPath").toString()));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(in);
//获取word所有内容
List<Object> list= wordMLPackage.getMainDocumentPart().getContent();
WordprocessingMLPackage doc1 =null;
int count =0;
for (int i = 0; i < list.size(); i++) {
String tempFileName=null;
String regex = "^\\d{1,100}\\.";
Pattern pattern = Pattern.compile(regex);
Matcher m = pattern.matcher(list.get(i).toString());
if(m.find()){//判断当前内容是否为题干
count++;
if(count==1){//第一题开始
doc1 = WordprocessingMLPackage.createPackage();
doc1.getMainDocumentPart().addObject(list.get(i));
}else{//非第一题
tempFileName=count-1+"";
doc1.save(new File(request.getSession().getAttribute("wordUUIDFileName")+"/"+tempFileName+".docx"));
doc1 = WordprocessingMLPackage.createPackage();
doc1.getMainDocumentPart().addObject(list.get(i));
}
}else{
if(count>0){
doc1.getMainDocumentPart().addObject(list.get(i));
}
}
if(i==list.size()-1){
tempFileName=count+"";
doc1.save(new File(request.getSession().getAttribute("wordUUIDFileName")+"/"+tempFileName+".docx"));
}
}
}
word中有5题,所以分隔成了5个文件,最后一个是用户上传的源文件;
做到这里的时候觉得就快大功告成了,很开心,但是,打开一个切分后的子文件你就会发现,文件的图片只有位置不显示图片;
经过一番研究终于找到了原因:
docx文件存储的本质是xml文件,其中图片的信息的存储是这样的,图片插入的位置是一个图片标签,这个标签里面有个对应的属性唯一标识一张图片,相当于html中标签的id属性,而所有的图片实际信息也就是二进制数据统一存储在文档的某个位置,然后标签中的id与其一一对应
当你切分word文档的时候只是将图片标签信息切分过去了,但是实际的二进制数据并没有切分过去,所以导致打开切分后的word文件图片只有位置,不显示
于是乎开动大脑,我在切分后的word文件中获取唯一标识,再到用户上传的源文件中获取二进制数据,哈哈哈,有点麻烦,但是我也很无奈啊;撸起袖子加油干!
package com.mxexam.utils.question;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
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 com.microsoft.schemas.vml.CTShape;
/**
* 获取2007word中的图片索引(例:Ird4,Ird5)
* @author zcy
*
*/
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();
//如果子元素是这样的形式,使用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();
// 如果子元素是这样的形式
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;
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;
}
}
/**
* 获取word中图片上传到文件服务器
* @return
* @throws FileNotFoundException
*/
public static List<Map<String,String>> getImgUrl(List<Map<String,String>> imgMsgList,HttpServletRequest request) throws Exception{
/* * 实现思路
* 1、根据段落docx获取图片索引
* 2、根据获取到的图片数据标识,在总的docx中获取图片data数据
* 3.上传图片返回访问路径;*/
//未分割之前的总文件地址
ResourceBundle resource = ResourceBundle.getBundle("URL");
String imgLocalPath = resource.getString("imgLocalPath");
String Indexdocx =request.getSession().getAttribute("wordRootPath").toString();
//读取总文件
InputStream in = new FileInputStream(Indexdocx);
XWPFDocument xwpfDocumentIndex = new XWPFDocument(in);
in.close();
List<XWPFPictureData> list = xwpfDocumentIndex.getAllPackagePictures();
//需要获取数据的图片名称
String paraPicName = "";
//总文档中的图片名称
String pictureName ="";
//上传到图片服务器之后的图片名称
//图片索引rId1/rId2/rId3..
String id ="";
String uuidName = "";
String endName = "";
byte[] bd = null;
//方法返回的List包含,题目序号,上传之后图片名称
List<Map<String,String>> resMapList = new ArrayList<Map<String,String>>();
Map<String, String> imgUploadNameMap = new HashMap<String,String>();
for (XWPFPictureData xwpfPictureData : list) {
uuidName = UUID.randomUUID().toString();
id = xwpfPictureData.getParent().getRelationId(xwpfPictureData);
pictureName = xwpfPictureData.getFileName();
endName = pictureName.substring(pictureName.lastIndexOf("."));
bd = xwpfPictureData.getData();
FileOutputStream fos = new FileOutputStream(new File(imgLocalPath+uuidName+endName));
fos.write(bd);
fos.flush();
fos.close();
ImageSizer.imageZip(new File(imgLocalPath+uuidName+endName), new File(imgLocalPath+uuidName+"-e"+endName), "", 130, 130, 1);
imgUploadNameMap.put(id, uuidName+endName);
}
//遍历参数
String tempPicName = "";
String tempValue ="";
for (Map<String, String> map : imgMsgList) {
tempPicName = map.get("pictureName");
tempValue = imgUploadNameMap.get(tempPicName);
if(tempValue!=null){
map.put("pictureName", tempValue);
}else{
map.put("pictureName", "");
}
resMapList.add(map);
}
return resMapList;
}
最后上传之后的图片信息设置到对应的实体中再保存到数据库就OK了
做到这里,本以为大功告,谁知长路漫漫,需求又改了!抹掉两行泪,将袖子撸起更高,加油!!!
新需求:题干和选项都要支持添加图片,而且位置不固定,需要识别一段文字中的多张图片的准确位置;
本来想着从xml文件下手,研究很久,始终无果,后来得到一位技术大牛相助,指点迷津;思路是:将docx转换成html然后读取指定标签,就可以实现我的需求;大牛说的很笼统,但是改变了我思考的方向,实践证明,大牛见多识广果然厉害!话不多说,上码
//1.将docx转换成HTML
DocxUtils.docx2html( rootpath+"/"+userpath+"/"+newpath, rootpath+"/"+userpath+"/"+"root.html");
//2.解析HTML,获取question字符串列表
List<String> questionStringList = DocxUtils.readHTML(rootpath+"/"+userpath+"/"+"root.html");
//3.解析字符串列表获取question信息列表
List<Map<String, String>> questionList = DocxUtils.getQuestionsStrList(questionStringList);
//4.解析上面列表,生成question实体及相关信息列表
questionMsgList = getQuestions(questionList, request);
//5.检查试题格式
flag = checkQuestion(questionMsgList,request);
//6.获取题目中的图片信息
imgIndexList = DocxUtils.getShortStr(itemQuestion.getQcontent());
//7.获取图片信息
List<Map<String,Object>> imgMagList = getImgUrl(imgMsgMapList, request);
//8.将图片信息设置到对应的试题中去
List<Questionoption> itemOptionList = new ArrayList<Questionoption>();
Map<String,String> imgMap = new HashMap<String,String>();
for (Map<String, Object> map : imgMagList) {
detailes = (int) map.get("details");
imgMap = (Map<String, String>) map.get("imgIndexs");
itemQues = (Question) questionMsgList.get((Integer)map.get("qIndex")).get("question");
for (Entry<String, String> entry : imgMap.entrySet()) {
itemString2 = "&^&"+entry.getKey()+"&^&";
if(detailes==666){//题干
itemString = itemQues.getQcontenttext().replace(itemString2, "");
itemQues.setQcontenttext(itemString);
itemString = itemQues.getQcontent().replace(itemString2, "");
itemQues.setQcontent(itemString);
questionMsgList.get((Integer)map.get("qIndex")).put("question",itemQues);
}else if(detailes==999){//解析
itemAnalysis = itemQues.getAnalysis();
itemString = itemAnalysis.replace(itemString2, "");
itemQues.setAnalysis(itemString);
questionMsgList.get((Integer)map.get("qIndex")).put("question",itemQues);
}else{
itemOptionList = itemQues.getOptionList();
itemString = itemOptionList.get(detailes).getOptdes().replace(itemString2, "+imgLoadPath+entry.getValue()+"\" style=\"max-width:50%\">");
itemOptionList.get(detailes).setOptdes(itemString);
itemOptionList = itemQues.getOptionList();
itemQues.setOptionList(itemOptionList);
questionMsgList.get((Integer)map.get("qIndex")).put("question",itemQues);
}
}
}
package com.mxexam.controller.question;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.docx4j.Docx4J;
import org.docx4j.Docx4jProperties;
import org.docx4j.convert.out.HTMLSettings;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.mxexam.entity.question.Question;
import com.mxexam.entity.question.Questionoption;
import com.mxexam.utils.question.DesUtil;
import javassist.expr.NewArray;
public class DocxUtils {
/**
* 将docx转换成html
* @param docx
* @param html
* @throws FileNotFoundException
* @throws Docx4JException
*/
public static void docx2html(String docx,String html) throws FileNotFoundException, Docx4JException{
//1.生成的HTML文件需要提前创建好
File file = new File(html);
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
WordprocessingMLPackage wordMLPackage= Docx4J.load(new java.io.File(docx));
HTMLSettings htmlSettings = Docx4J.createHTMLSettings();
String imageFilePath=html.substring(0,html.lastIndexOf("/")+1)+"/images";
htmlSettings.setImageDirPath(imageFilePath);
htmlSettings.setImageTargetUri( "images");
htmlSettings.setWmlPackage(wordMLPackage);
String userCSS = "html, body, div, span,font, h1, h2, h3, h4, h5, h6, p, a, img, ol, ul, li, table, caption, tbody, tfoot, thead, tr, th, td " +
"{ margin: 0; padding: 0; border: 0;}" +
"body {line-height: 1;} ";
htmlSettings.setUserCSS(userCSS);
OutputStream os;
os = new FileOutputStream(html);
Docx4jProperties.setProperty("docx4j.Convert.Out.HTML.OutputMethodXML", true);
Docx4J.toHTML(htmlSettings, os, Docx4J.FLAG_EXPORT_PREFER_XSL);
}
/**
* 读取HTML中文字内容和图片信息
* @param HTMLPath
* @return
* @throws IOException
*/
public static List<String> readHTML(String HTMLPath) throws IOException{
List<String> allList = new ArrayList<>();
File input = new File(HTMLPath);
Document doc = Jsoup.parse(input, "UTF-8", "");
Elements links = doc.select("span.2,img");
/*Elements links = doc.select("p,img");*/
String item = "";
boolean flag = false;
for (Element link : links) {
String itemContent = link.text();
if("【".equals(itemContent)){
flag = true;
item = item+itemContent;
}else{
if(flag){
item = item+itemContent;
if(itemContent.contains("】")){
allList.add(item);
item="";
flag = false;
}
}else{
allList.add(itemContent);
}
}
if(!"".equals(link.attr("id"))){
allList.add("&^&"+link.attr("id")+"&^&");
}
}
return allList;
}
/**
* 去除listh中的重复数据
* @param list
* @return
*/
public static List removeDuplicate(List list){
List listTemp = new ArrayList();
for(int i=0;i<list.size();i++){
if(!listTemp.contains(list.get(i))){
listTemp.add(list.get(i));
}
}
return listTemp;
}
/**
* 获取分隔后的question字符串列表
* @param list
* @return
*/
public static List<Map<String, String>> getQuestionsStrList(List<String> list){
List<Map<String, String>> subjectList = new ArrayList<Map<String,String>>();
StringBuffer subjectItem =new StringBuffer();
int count =0;
int qNum = 0;
//划分题目
//以数字开头并且包含.表示一个新的题目开始
String regex = "^\\d{1,100}\\.";
Pattern pattern = Pattern.compile(regex);
Matcher m = null;
Map<String, String> tempMap = new HashMap<String, String>();
String qtype ="";
String oldQtype = "";
String line ="";
for (int i = 0; i < list.size(); i++) {
line = list.get(i).toString();
m = pattern.matcher(line);
if(m.find()){//题干
count++;
if(qNum>0){//不是文件文件第一个题干,将之前的buffer保存
tempMap = new HashMap<String, String>();
tempMap.put("qtype",oldQtype);
tempMap.put("content", subjectItem.toString());
subjectList.add(tempMap);
oldQtype=qtype;
subjectItem = new StringBuffer();
subjectItem.append(line);
}else{//文件第一个题干,创建新的buffer,并将题干放入buffer
subjectItem = new StringBuffer();
subjectItem.append(line);
}
qNum++;
}else if(line.startsWith("【单选题】")){
qtype = "1";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【多选题】")){
qtype = "2";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【判断题】")){
qtype = "3";
if(count==0){
oldQtype=qtype;
}
}else if(line.startsWith("【不定项选择题】")){
qtype = "4";
if(count==0){
oldQtype=qtype;
}
}else{
subjectItem.append(line);
}
if(i==list.size()-1){
tempMap = new HashMap<String, String>();
tempMap.put("qtype",oldQtype);
tempMap.put("content", subjectItem.toString());
subjectList.add(tempMap);
}
}
return subjectList;
}
/**
* 获取字符串中所有的子串的位置
* @param str
* @return
*/
public static String getAllSonstrIndex(String str,String sonStr){
StringBuffer buffer = new StringBuffer();
int index = str.indexOf(sonStr);
while(index!=-1){
buffer.append(index+",");
index = str.indexOf(sonStr,index+1);
}
String string = buffer.toString();
if(string.indexOf(",")!=-1){
string = string.substring(0, string.lastIndexOf(","));
}
return string;
}
/**
* 获取图片地址列表
* @param str:总的字符串
* @return
*/
public static List<String> getShortStr(String str){
List<String> resList = new ArrayList<String>();
String indexsStr = getAllSonstrIndex(str,"&^&");
String[] indexs = indexsStr.split(",");
int start = 0;
int end = 0;
for (int i = 0; i < indexs.length-1; i=i+2) {
start=Integer.parseInt(indexs[i]);
end=Integer.parseInt(indexs[i+1]);
resList.add(str.substring(start+3,end));
}
return resList;
}
}
好了,本人开发小白,不对的或者不好的地方望大家批评指正,不清楚的地方欢迎加微信学习讨论,但拒绝恶意吐槽哦!!!
觉得内容对您有帮助,欢迎打赏哦!!!嘻嘻嘻!!!