目录
正则表达式生成
功能描述
为了满足系统需要对一定数量的String生成正则表达式,要求缩短正则表达式的长度,尽量提高正则表达式的匹配效率。
流程逻辑
给定一批需要生成的字符串,以及用来排除的其他字符串集合
设计正则表达式的优化从前缀出发,优先将共同前缀的字符串进行整合优化
- 收集需要生成正则表达式的字符串集合
- 调用getCommonFixStr方法,针对字符串集合进行最大公共前缀子串提取,存入以公共前缀为key,符合此前缀的List为value的Map中
- 接着调用dealWithPreFix方法,对每一个前缀中的List集合进行最大后缀子串的提取,其可有多个公共后缀,并将前后缀通过“-#-”的连接符连接在一起生成新的key,以及符合其前后缀的字符串集合为value,存入新的map中
- 最后通过generate方法,根据Map中的数据生成正则表达式
- 最后在testRegex方法中进行测试。正则表达式的当前业务字典匹配测试,确认是否会匹配到其他不在此范围内的字符串,通过则返回true,不通过返回false,
getCommonFixStr方法详解
getCommonFixStr方法主要通过提取最大公共前缀子串,对所有的字符串分类存在Map中,key为前缀,value为对应的list集合。
该方法中依赖两层for循环对list中每一个字符串与其他字符找寻最大公共前缀子串(longestCommonPrefix方法),整理完成后调用sortMapByValueSize方法对map进行排序,排序的规则是,优先排前缀长的,在前缀长的里面又安装list的size进行排序(这样有助有将存在合并可能的结果先处理,有利于缩短正则表达式的长度)。
最大公共前缀子串的提取
鉴于真实业务场景下,如果字符串多由相同的开头前缀组成,因此最大公共前缀子串提取是有效缩短正则表达式长度的一种方式。最大公共前缀子串的提取在正则表达式的任意一个子表达式中都作为第一优化思路。但是最大公共前缀子串仅适用于整个正则表达式只有一个分支的情况
eg:43000000,43000001,43000002 ->4300000[012]
dealWithPrefix方法详解
dealWithPrefix方法是根据已经由前缀分类的Map,依次对每个前缀下的list集合进行后缀提取分类,并将前缀和后缀组合起来,通过“-#-”的连接符连接(用于后面的生成分割处理)。
通过getCommonSubFixStr方法获取后缀及其对应的list集合。
最大公共后缀子串的提取
在前缀的基础上,增加后缀的子串提取,用于处理以下情况
eg:43000000000,43000001000,43000002000 -> 4300000(0000|1000|2000) -> 4300000(0|1|2)000
generate方法详解
generate方法对已经处理好的前缀后的map进行处理,主要是通过“-#-”的连接符切分前后缀表达式,将list中的各个字符串去除掉前后缀剩余的部分,通过“|”的正则表达式的或连接符拼接到一起,形成前缀(不通用的部分1|不通用的部分2|....|不通用的部分n)后缀
的正则表达式,各个不同前后缀的表达式也是通过“|”连接,拼完之后,就会对具有0-9单独字符串的表达式进行优化,变成最简单的\d
,这样达到最短字符串的标准。
连续数字/连续字母提取
如果遇到类似(0|1|2|3|4|5|6|7|8|9)
的正则表达式内容,应该优化\d
,后期如果遇到可优化的,也可增加一些其他针对特殊的字符应该有特定的优化。
testRegex方法详解
testRegex主要负责对生成的正则表达式的测试和验证。他要求给两个集合,一个是不匹配的集合,一个是要匹配的集合。这样确保正则表达式首先满足能匹配全部匹配到必须匹配的集合,同时也要满足在不匹配的范围内匹配不到任意一个。
验证测试分两个方面:
-
验证测试以指定非在匹配需求的字符串集合为范围,是否会匹配到不在匹配需求内的其他字典项,验证正则表达式的排他性。
-
验证当前正则表达式是否匹配需要匹配的所有字符串,验证正则表达式的完全包含性
正则表达式规范
- 对无公共前缀子串的子表达式,使用小括号+|连接。例如:
(子表达式1)|(子表达式2)
,如无必要,建议不要使用,影响匹配效率 - 单个字符串作为一个表达式,为了具有良好的排他性,统一使用
(^子表达式$)
,注意^在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合 - 连续相同的数字或字符善用*和+
- 善用非捕获匹配,优化生成的正则表达式中的|情况,例如:
industry|industries
可以被优化成industr[y|ies]
工具类代码
下方为具体的正则表达式实现代码的工具类代码
package prism.common.utils;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.util.Strings;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 正则表达式优化工具类
* 通过一批字符串,生成对应的正则表达式
* 并提供测试验证功能
* @author zt
*/
public class RegularExpressionOptimizationUtil {
/**
* 前后缀的分隔符标志
*/
private static final String PRE_SUB_SPLIT_SIGN = "-#-";
/**
* 空字符串
*/
private static final String EMPTY_STRING = "";
/**
* 比较器:根据key的长度降序
*/
static Comparator<Map.Entry<String, List<String>>> valueComparator = (o1, o2) -> o2.getKey().length() - o1.getKey().length();
/**
* 比较器:根据value的size降序
*/
static Comparator<Map.Entry<String, List<String>>> valueComparator2 = (o1, o2) -> o2.getValue().size() - o1.getValue().size();
private RegularExpressionOptimizationUtil(){
}
/**
* 获取通过测试验证的正则表达式
* @param need 需要被验证符合正则表达式的字符串集合
* @param testNot 需要被验证必须不能满足正则表达式匹配的字符串集合
* @return 正则表达式,可为空字符串
*/
public static String getRegexByTest(List<String> need, List<String> testNot) {
String generate = EMPTY_STRING;
if(CollectionUtils.isNotEmpty(need)){
//获取最大公共前缀子串
Map<String, List<String>> commonPreFixStr = getCommonFixStr(need);
//针对各个最大公共前缀子串在进行最大公共后缀的处理
Map<String, List<String>> stringListMap = dealWithPrefix(commonPreFixStr);
generate = generate(stringListMap, need);
if (!testRegex(generate, need, testNot)) {
generate = EMPTY_STRING;
}
}
return generate;
}
/**
* 获取指定字符串集合生成的正则表达式
* @param need
* @return
*/
public static String getRegex(List<String> need) {
String generate = EMPTY_STRING;
if(CollectionUtils.isNotEmpty(need)) {
//获取最大公共前缀子串
Map<String, List<String>> commonPreFixStr = getCommonFixStr(need);
//针对各个最大公共前缀子串在进行最大公共后缀的处理
Map<String, List<String>> stringListMap = dealWithPrefix(commonPreFixStr);
generate = generate(stringListMap, need);
}
return generate;
}
/**
* 测试生成优化正则表达式的方法时候符合匹配所有,判断成功的
*
* @param generate 根据need生成的正则表达式
* @param need need集合
* @param all 在同一个字典中排除need后剩余的集合
* @return boolean 是否测试通过,true-通过,false-失败
*/
private static boolean testRegex(String generate, List<String> need, List<String> all) {
Pattern NEED_TO_TEST = Pattern.compile(generate);
//默认true
boolean result = true;
//遍历匹配need,如遇到一个匹配不到,result=false
for (String isTrue : need) {
Matcher matcher = NEED_TO_TEST.matcher(isTrue);
if (!matcher.find()) {
result = false;
break;
}
}
//如果need都匹配成功,在进行测试非need的集合是否存在任意一个能匹配到,如能,则result=false
if (result) {
for (String isFalse : all) {
Matcher matcher = NEED_TO_TEST.matcher(isFalse);
if (matcher.find()) {
result = false;
break;
}