doc文档内容分析检错

举报
Amrf 发表于 2019/08/05 14:39:56 2019/08/05
【摘要】 java环境下可以使用:jacob是java调用com; poi属于独立解析;docx4j只支持docx,需要先将doc转换成docx转换;试了试直接在doc里面写一些vba,感觉分析效率太低了;考虑到jacob很这种方式机制上比较相似,目前考虑先使用poi看看能不能达到预期;

一个问题记录

获取标题列表编号的时候与实际值不符,查了一下发现aspose 19.8的发行说明里有一条,


WORDSNET-18903当段落具有修订时,ListLabel.LabelString返回不正确的值

而我项目里面使用的是aspose版本是19.7

具体参考:

Implemented an option that allows specifying whether to work with the original or revised version of a document

image.png


doc文档分析回顾

  • NAME检查:[分析:]Name会出现在两种类型的地方:第一种,遍历所有段落,当段落样式名包含"标题",由于Name是不包含中文的,使用[\u4e00-\u9fa5]+过滤掉包含中文的,然后进一步处理;另一种,出现在表头为"标示"的表格中

  1. 检查NAME包含多表格名称是否出现2个及以上连续_(下划线),如有可自动删除,只保留一个[分析:]当段文字中包含两个下划线时继续处理(para.getRange().replace)            [注意:]para.getText()与para.toString(SaveFormat.TEXT).trim()的不同之处在于,举个例子,当你获取一个标题的文字的时候,前者只会获得标题编号后的文字,而后者则会包含标题的编号

  2. 检查NAME包含多表格名称字符间不能出现空格,如有可自动删除[分析:]当段文字中包含两个空格时继续处理

  3. 检查NAME多格式大小写X和x不能混用,比如x,只能一种;[分析:]当段文字中包含"Xx"或者"xX"时继续处理

  • ID检查:[分析:]Name会出现在两种类型的地方:第一种,Paragraph以"标示[:/:]"或"标识[:/:]"开始的非表头格式的后面部分;另一种,出现在表头为"标示"的表格中的第一列除了第一行的所有单元格内容

  1. 检查“标识”或"ID"关键词下是否缺失16进制或10进制ID(0x----)[分析:]当不包含"0x"的时候,正则查找[0-9]+获得其十进制的值后,String.format("0x%X(%d)",dex,dex)进行替换

  2. 检查关键词"标识"是否正确,其他别字(如标示)均报错,可自动修改为"标识"[分析:]使用para.getRange().replace进行归一化处理

  3. 检查“标识:0x1234()”,结尾不能出现其他符号,如有可自动删除多余符号[分析:]首先将中文括号归一化为英文括号,然后当字段包含括号时,如果包含"(0x"则可能是(0xccc)111这种错误样式,采用正则"\\((0x[A-Z0-9]+)\\)([0-9]+)"进行匹配,替换为m.group(1)+"("+m.group(2)+")",忽略包含顿号和短横的多ID并列的情况,由于前面已经移除了空格,所以现在只要判断括号后面有没有部分标点,使用正则"\\)[,.;,。;]+"进行查找;另外一种情况字段不包含括号,同样忽略包含顿号或者横杆的范围并列的情况,原来只包含十六进制的情况,采用"(0x[A-Z0-9]+)[,.;,。;]+"查找并消除后面的其他标点,然后通过"0x([A-Z0-9]+)"替换成"0x"+m.group(1)+"("+Integer.toString(hex)+")"补上十进制的字段

  4. 检查16进制ID是否大小写混用,如“0xAbC7”,可自动修改为大写[分析:]0x不要求大写,首先将"0X"替换成"0x",移除"0x"后自身与其转大写不相等则继续处理,for(Run run:para.getRuns()) {String ot = run.getText();run.setText(ot.toUpperCase());},最后把"0X"替换成"0x"

  5. 支持检查格式:word[分析:]限定到doc

  6. 报错机制:在错误处进行标记(批注形式)[分析:]添加批注的逻辑前面我是调用的时候即时就添加了,但是其实这样会带来一些问题,因为comment中也是添加了Paragraph的,这样就有可能影响一些分析后来的改进是不即时添加,

    	//先记录
    	public static void addComment(Document doc,int[] commentId,Paragraph para,String text){
    		CComment ccomment = new CComment(para,text);
    		pendingComment.put(commentId[0],ccomment);
    		commentId[0]+=1;
    	}
    	//最后集中添加批注
        public static void addComment(Document doc) {
    		for(Map.Entryentry:pendingComment.entrySet()) {		
    			Node[] runs = entry.getValue().Paragraph.getChildNodes(NodeType.RUN, true).toArray();	
    			NodeCollection coms = entry.getValue().Paragraph.getChildNodes(NodeType.COMMENT, true);
    			if(coms.getCount()>0) {
    				Comment oldComment = (Comment)coms.get(0);
    				oldComment.getFirstParagraph().getRuns().add(new Run(doc, "and err:"+entry.getValue().text.replace('\r', '\0').replace('\n', '\0')));
    			}else {
    				if(runs==null || runs.length==0) {
    					Comment comment = new Comment(doc, "amrf000", "DH", new Date());
    					entry.getValue().Paragraph.appendChild(comment);
    					comment.getParagraphs().add(new Paragraph(doc));
    			        comment.getFirstParagraph().getRuns().add(new Run(doc, "err:"+entry.getValue().text.replace('\r', '\0').replace('\n', '\0')));
    				}else {
    					Comment comment = new Comment(doc, "amrf000", "DH", new Date());
    					/*CommentRangeStart start = new CommentRangeStart(doc, entry.getKey());
    					CommentRangeEnd end = new CommentRangeEnd(doc, entry.getKey());
    					runs[0].getParentNode().insertBefore(start,runs[0]);
    					runs[runs.length-1].getParentNode().insertAfter(end,runs[runs.length-1]);
    					end.getParentNode().insertBefore(comment,end);*/
    					entry.getValue().Paragraph.appendChild(comment);
    					comment.getParagraphs().add(new Paragraph(doc));
    			        comment.getFirstParagraph().getRuns().add(new Run(doc, "err:"+entry.getValue().text.replace('\r', '\0').replace('\n', '\0')));
    				}
    			}
    		}

               


  7. 支持文件夹下多word文档检查(至少5个)[分析:]这个没什么好说的在Swingworker中遍历处理就行,如果要采用并行提升处理效率的话,下一步在分析

  • 表格检查[分析:]doc.getChildNodes(NodeType.TABLE, true)获取文档中的所有表格,通过工具类PageNumberFinder,finder.GetPage(table.getFirstChild())获取表格的起始页码

  1. 表格下空行检查检查文档中的表格下是否有且只有一个空行[分析:]table.getPreviousSibling();table.getNextSibling();获取表格相邻两个节点,如果next是段落并且toString(SaveFormat.TEXT).trim()不为空则异常;next.getNextSibling(),获取下下个节点,如果连续两个都为空,则异常

  2. 表格标题行重复属性未设置表格表头是否设置标题行重复属性[分析:]通过finder.GetPage(table.getLastChild())获取表格结束页码,当开始不等于结束页时即表格是跨页的,这个时候如果table.getFirstRow().getRowFormat().getHeadingFormat()为false则标题行重复属性未设置

  3. 表注位置检查表注位于表格上方[分析:]首先前面检测表后非空行已经一定程度上包含了这一点,另外当没有表注的时候也是要求判定正常的,遍历所有字段,如果字段格式为"Figure Description"或"Table Description"并且非空串,获取相邻的段落,如果为空则位置异常

  • 图形检查[分析:]doc.getChildNodes(NodeType.SHAPE, true)获取所有图形

  1. 图注位置检查图注位于图片上方[分析:]如果前一个段落样式为"Figure Description"则为图注,finder.GetPage(pre)!=finder.GetPage(shape)则图形段落与上下同页未勾选;如果下一行是段落且段落内容为空,并且下下段落不为空(包含Shape也为不空)则正常,ImageData ida = shape.getImageData()如果ida.getTitle()不是说明和警告等则图后面非空行;遍历所有字段,如果字段格式为"Figure Description"或"Table Description"并且非空串,获取相邻的段落,如果为空则位置异常

  • 标识文本[分析:]如果ida.getTitle()等于说明或者警告等

  1. 说明样式检查"检查文档中“说明”“窍门”上、下无空行,内容不跨页;[分析:]shape.getParentNode().getPreviousSibling()或者shape.getParentNode().getNextSibling()获取前后的段落,如果是说明类的上下任意为空则异常,如果是警告类的上面为空或警告后不为空则异常,如果警告类的后面的空行多于1则异常

  2. “警告”“注意”“危险”上空1行,下不空行,内容不跨页"[分析:]循环获取后面的节点如果样式不等于"Notes Text"/"Notes Heading"/"Notes Text List"/"CAUTION Text"/"CAUTION Heading"/"CAUTION Text List"则该区段结束,获取结束的页码如果不等于开始则跨页了,如果结束后面的一个字段为空则异常(包含除了Heading的段落)

  • 标题

  1. 标题不带标点符号1~3级标题不带标点符号(/~-+&除外)[分析:]样式名包含标题且Pattern.compile("[\\pP]+").matcher(para.text.replaceAll("/|~|-|&|_|\\.", "")).find()则异常

  2. Block Label样式标题检查Block Label样式标题下必须要有内容

  3. 标题编号检查检查文档中标题编号方式是否与模板相同,具体包括1~4级标题、Item step样式,图号,表号是否连续 [分析:]para.Paragraph.getListFormat().getListLevel().getNumberStyle()!=NumberStyle.ARABIC则异常

  • 文档内容

  1. 重复或错字词检查检查重复的字和词,如的的、功能功能[分析:]遍历错词作为匹配条件查找

  2. 成对符号检查检查中文状态的圆括号()和书名《》号是否成对出现[分析:]在段落中分别对各个符号进行while(m.find()) {++op;}计数,应该成对的不相等时则异常

  3. 数值范围单位检查文档中表示数值范围时,数字带有国际单位和表示量的数词时,两个数字都要加上单位,例如:错误“1~/-5Hz”,正确“1Hz~5Hz”[分析:]正则"\\d+[~|-]\\dHz"

  4. 空白页检查页面不应有多余空白页[分析:]通过工具类PageNumberFinder将所有段落分页缓存起来,doc.getPageCount()获取总页数,从1开始遍历段落缓存,循环该页段落,叠加段落正文直到trim()非空或者都循环完(当段落包含shape子时,正文也不为空),循环结束,如果叠加结果为空则异常

  • 文档样式

  1. 页眉页脚检查[分析:]doc.getChildNodes(NodeType.HEADER_FOOTER, true)获取所有的页眉页脚,finder.GetPage(para.getParentNode())获取父节点的页码,页眉页脚是对于section的,获取的页码也是section的

  •                 封面无页眉页脚(文档前2页)[分析:],通过isHeader()可以获取当前section属于页眉的,页脚偶数页取HeaderFooterType.FOOTER_EVEN,奇数页取HeaderFooterType.FOOTER_PRIMARY,如果页码小于等于2时对应的页眉页脚非空则异常

  •                 前言及正文页眉处为1级标题名称[分析:]通过h.getRange().getFields()遍历,当类型为FieldType.FIELD_STYLE_REF时并且样式不为"Heading1 no Number"或"Contents"或"1"时,即前言及正文页眉处不为1级标题名称

  •                 前言及目录(包含表目录,图目录)页脚的页码使用希腊数字i、ii、iii….[分析:]section.getPageSetup().getPageNumberStyle()获取页码数字样式,正文前面的章节应该是NumberStyle.LOWERCASE_ROMAN后面的章节应该是NumberStyle.ARABIC

  •                 第1章节开始,页码使用数字1、2、3…"[分析:]通过doc.getChildNodes(NodeType.SECTION, true)获取所有章节,section.getBody().getFirstParagraph()获取章节正文第一个段落的样式名,如果等于"标题1"说明后面的其他章节包括本章是doc的正文部分,正文的第一个章节的页码需要重新计数检查section.getPageSetup().getRestartPageNumbering()是否为true

图表跨页检查检查文档中图表标注和图表本身是否在同一页[分析:]如果表前段落样式为"Table Description",finder.GetPage(pre)!=finder.GetPage(table.getFirstChild())则表格的属性段落与上下同页未勾选,同理对于图片前段落为"Figure Description"且finder.GetPage(shape)!=finder.GetPage(pre)则其与上下同页未勾选

  • 链接[分析:]doc.getRange().getFields()获取所有域,field.getStart().getParentParagraph()可以获得域的父段落

  1. 链接正确性检查检查文档中所有链接是否正确(包含总目录、章节目录、文中的超链接及交叉引用)[分析:]field.isDirty()说明改域未更新,对于FieldType.FIELD_REF,获取其引用getBookmarkName()==>doc.getRange().getBookmarks().get(bookmarkName)如果结果为null说明引用的目标已经被删除了

  • 归档检查

  1. 修订标记归档时不允许出现修订标记[分析:]doc.getRevisions().getCount()>0则原文存在修订标记,doc.stopTrackRevisions()doc.setTrackRevisions(false)可以关闭修订

  2. 批注归档时不允许出现批注[分析:]doc.getChildNodes(NodeType.COMMENT, true).getCount()>0则原文存在批注


=============================================================================

背景:

java环境下可以使用:

jacob是java调用com; 

poi属于独立解析;

docx4j只支持docx,需要先将doc转换成docx转换;


试了试直接在doc里面写一些vba,感觉分析效率太低了;

考虑到jacob很这种方式机制上比较相似,目前考虑先使用poi看看能不能达到预期;


==>由于poi不方便在doc格式下添加批注(docx可以)


后来采用了aspose,这个是个有些商业属性的包


关于去除aspose功能限制,首先运行下面的初始化代码:

    public static void doInit(){
		 InputStream is = xxxx.class.getClassLoader().getResourceAsStream("\\license.xml");
		 License aposeLic = new License();
		 try {
			aposeLic.setLicense(is);
		} catch (Exception e) {
			e.printStackTrace();
		}
    }

运行后可以定位到报错的类和方法

我不是用的javassist进行修改,aspose最近的版本javassit修改结果有问题,

使用jd-gui打开后找到对应的类复制出来,在项目中的对应于该类文件创建一个一样的类,粘贴进去,修改掉一些语法错误,

将target中生成的.class用压缩工具拷贝回jar中对应的位置,

删除jar中的META-INF文件夹

参考:

aspose.word.19.3

aspose.words

Aspose-words 18.8

下面这个是个很有意思的问题:

现象p.getRange().replace似乎越界了,也像连续replace导致迭代器失效,下面这个方法可以解决这个问题

https://forum.aspose.com/t/range-replace-how-to-replace-first-occurence-only/40275

https://webcache.googleusercontent.com/search?q=cache:0WkLaYf_T4oJ:https://forum.aspose.com/t/range-replace-how-to-replace-first-occurence-only/40275+&cd=2&hl=zh-CN&ct=clnk

https://docs.aspose.com/display/wordsnet/Find+and+Replace

https://apireference.aspose.com/java/words/com.aspose.words/IReplacingCallback

https://blog.csdn.net/lqzkcx3/article/details/71414693

https://blog.csdn.net/WuLex/article/details/81702812

其他参考:

https://forum.aspose.com/t/multiple-lines-of-text-in-replace-command/126960

https://webcache.googleusercontent.com/search?q=cache:8jdDEL02sLQJ:https://docs.aspose.com/display/wordsjava/Find%2Band%2BReplace+&cd=2&hl=zh-CN&ct=clnk

https://forum.aspose.com/t/identifying-the-paragraph-and-edit-replace-delete/53097/2

https://www.cnblogs.com/ggjucheng/p/3423731.html

https://blog.csdn.net/weixin_34409703/article/details/85974476

测试doc内容

0xDA00
0xDA01
0xDA02
0xDA03
0xDA04#
0xDA05
0xDA06(1256);.
dddd
0xDA00
0xDA01(7788)
dddd
0xDA02(3256)
0xDA03
0xDA04
0xDA05

处理

class ReplaceFirstOccourance implements IReplacingCallback{
  • 记录两个比较低级的问题

  1. InputStream is=xxx.class.getClassLoader().getResourceAsStream("license.xml");

    文件放在了resources目录下,在eclipse中调试启动可以正常找到到,打包成可执行jar找不到这个文件了,

    image.png

    需要将resources路径添加到Source中,这样打包后才会出现在类路径里

  2. java 反射传递变长参数的例子

    class PWorker extends SwingWorker<Boolean, Map>
    {
    	private String InDir;
        private String OutDir;
        private JButton buttonStart;
        private JProgressBar jpb;
        private JTextArea area;
        private boolean isAddCom;
    	public PWorker(String in,String out,JButton button,JProgressBar pb,JTextArea ea,boolean addCom) {
    		...
    	}
    	protected Boolean doInBackground() throws Exception {
    		try {
    			Method[] methods = this.getClass().getSuperclass().getDeclaredMethods();
    		    Method method = this.getClass().getSuperclass().getDeclaredMethod("publish",Object[].class);
    			method.setAccessible(true);
    			xxxx(InDir, OutDir,this,method,isAddCom);
    		}catch(Exception e) {
    			e.printStackTrace();
    		}
    		return true;
    	}
    	@Override
    	protected void done() {
    		buttonStart.setEnabled(true);
    	}
    	@Override
    	protected void process(List<Map> chunks) {
    		Map i = chunks.get(chunks.size()-1);
    		jpb.setValue(Integer.valueOf(i.get("int").toString()));
    		area.append(i.get("str").toString());
    	}
    }
    try {
    	Map pu = new HashMap<String,Object>();
    	pu.put("int", pindex);
    	pu.put("str", processInfo);
    	Map[] apd = new Map[] {pu};
    	method.invoke(target, new Object[] {apd} );
    } catch (Exception e) {		
    	e.printStackTrace();
    }
  3. jar运行路径下如果放了和jar中类路径相同的.class文件,会导致java.lang.vertify的异常

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200