序:来自公司某项目的需求,解决这个问题本身牵涉到了系统中的很多地方,这里我只记录有关正则表达式过滤文段的部分,先前一直在做 .net ,这方面接触的确实比较少,需要学习。
语言:Java;系统架构:Spring MVC + MyBatis
一、问题描述
在我们的网站系统中,客户会从 Microsoft Word 中复制文本到浏览器的富文本编辑器上,但是复制过来的时候会携带大量的 IE 注释,且多次复制后还会有无内容段落出现,使得页面上出现大段空白,严重影响显示效果,需要剔除。
二、空白段落来源
1、IE 注释
在测试中我发现,从 Word 复制文本到编辑器时,后台存储的文本数据会多出下面这样的一大段注释:
<!--[if gte mso 9]><xml>
<!-- 中间省略四千行......我真没搞懂为什么一个注释能有这么多 -->
<![endif]-->
<!--[if gte mso 10]>
......
<![endif]-->
从格式上我们可以发现这是巨硬从 IE 5 开始提供的条件注释,让我们可以针对不同的浏览器进行客制化设计。它本身没什么问题,但问题在于当文本储存到 DB 里之后,通过 Vue 的语法输出该文本时,页面上会出现大段大段的空白。究其原因还是因为这些注释每一句后面都跟着一个换行符,必须全部清除。
2、无内容段落
这里的空白段落指的是形如下方的标签组合:
<p class="MyClass"></p>
<p></p>
通过富文本编辑器保存的文本会被自动填充标签以渲染样式,而注释也被视作正文文本的一部分被段落包裹修饰,但注释无法被显示出来,而段落则会被保留,这也是页面上会有大段空白的原因。要想清除空白段落,必须先清除注释。
三、清除方法
1、清除注释
在清除空白段落通过分析 IE 注释内容,我们可以发现,尽管这些内容又臭又长,但基本是由多段 if 条件组成的,可以构成形如上文所示的代码格式。因此我决定先使用 replace 方法将文段中的注释全部清空,再观察效果。
/**
* 描述:清除content中的IE注释
*
* @param content 带有IE注释的文本
* @return String 返回清除注释后的文本,若content为空,则返回一个空字符串
*/
public String removeIeNote(String content){
if (content == null || "".equals(content)) {
return "";
}
// 设置注释开始标记与结束标记
String ieNoteBegin = "<!--[if";
String ieNoteEnd = "<![endif]-->";
// 循环查找知道
while (content.contains(ieNoteEnd) && content.contains(ieNoteBegin)) {
// 查找子串内容
String cutDown = content.substring(content.indexOf(ieNoteBegin), content.indexOf(ieNoteEnd) + ieNoteEnd.length());
content = content.replace(cutDown, "");
}
// System.out.println("\n\ncontent = [" + content + "]\n");
return content;
}
该方法是我目前能找到的最简单直接的方法,网络上也有使用正则的,但不知道为什么在文段内容偏长的时候还会把我的一部分正文内容给清除掉,故没有使用。最开始我使用的是 lastIndexOf("<![endif]–>") 方法,直接找结尾然后一段全删,但在判定的时候总会漏掉一段两段,所以我选择稳妥的方法,逐段查询,逐段删除。
2、过滤空白段落
目前已知只会有两种空白文段,一种是无样式的<p>
标签,一种是带有样式修饰的<p>
标签。清除前者十分容易,因为其格式已经固定,故我们只需要利用正则加 replaceAll()
方法进行替换即可:
// content为正文内容
String reg = "<p></p>";
content = content.replaceAll(reg, "");
麻烦的部分在于后一种带有样式修饰的<p>
标签,已知该类型文本一定是形如<p class="MyClass"></p>
的格式,故我最开始尝试以<p.*?"></p>
进行过滤筛选。以下列这段文字为测试用文本(它很长所以不用去看,如果需要测试直接三连击全选复制就好):
这段文本乍一看没什么问题,使用<p.*?></p>
可以轻松分成四段:
剔除第一段之后,使用<p.*?"></p>
继续过滤,准备剔除第三段时,我们会发现,第二句和第三句一定会贴在一起:
这是因为对于<p.*?"></p>
而言,第三句末尾的"></p>
和第二句开头<p>
刚好可以匹配上指定的表达式,使得部分正文文段被误删,因此我们必须换个方法。已知<p.*?></p>
可以正确的从文本中筛选出段落,故我们可以采用逐步筛选,按段落匹配空白文段进行删除的方式过滤输入的文本内容:
/**
* 描述:分析文段,移除该文段中无内容的p标签
*
* @param content 需要进行分析的文段
* @return String 返回一个经过分析的文段,若输入文段本身为空,则返回一个空字符串
*/
public String removeEmptyPTag(String content) {
// 先移除可能存在的IE注释
this.removeTeNote(content);
if (content == null || "".equals(content)) {
return "";
}
// 先去掉无样式的空段落
String reg = "<p></p>";
content = content.replaceAll(reg, "");
// 正文分析结果
StringBuilder result = new StringBuilder();
// 通过正则分段,将文本分割成多个段落
String pattern = "<p.*?></p>";
Pattern patternSegmented = Pattern.compile(pattern);
Matcher sentence = patternSegmented.matcher(content);
// 通过正则识别语句,若语句包含内容则置入result中
Pattern patternEmptyTag = Pattern.compile("<p.*\"></p>");
Matcher checkEmptyTag = null;
// 查找无内容语句
while (sentence.find()) {
String str = sentence.group();
checkEmptyTag = patternEmptyTag.matcher(str);
// 若不是无内容语句则放入result中
if (!checkEmptyTag.matches()) {
result.append(str);
}
}
if ("".equals(result.toString())) {
return content;
} else {
return result.toString();
}
}
整个方法的思路很简单,先利用正则表达式分组,将一整段文字分解成多个段落,随后对每一个段落进行比较,如果它是无内容的段落,其结尾一定是"></p>
,故在此我们使用表达式<p.*?"></p>
进行匹配。请勿使用endWith("></p>")
分号,部分形如</span></p>
的语句使用此方法会被误判。最后,在方法结尾我们必须对 result 进行审查,如果该变量为空字符串,则说明原文本身不含空段落,可以直接输出,否则输出result本身。
文中我使用了菜鸟教程的正则表达式在线工具,蛮好用的,测试的时候可以试试看效果,链接在此:正则表达式在线测试 | 菜鸟工具