Java魔法堂:深入正则表达式API
目录
正则表达式作为文本处理的利器,早已成为各大语言的必要装备,但各语言对其的实现程度(功能语法支持程度)和API设计均有所差异,本篇将目光投向java原生类库中提供的正则表达式API—— java.util.regex包 和 java.lang.String实例方法 ,和所支持的功能语法上。
正则表达式一般用于处理如下诉求,本篇后续内容将以这些诉求为基础检验相关的原生API是否提供方便有效(code less,do more)的操作方式。
1. 匹配字符串:全字符串匹配、部分匹配(也就是包含关系)
2. 替换字符串
3. 萃取字符串
4. 拆分字符串
从jdk1.5开始正则表达式相关API就集中存放在该包下,且为其他类中表达式相关方法提供基础处理能力。
1. java.util.regex.Pattern类 :模式类,用于对正则表达式进行编译。
类方法:
/* * 对正则表达式进行编译,并返回Pattern实例 * 入参flag作为表达式属性,启用多个表达式属性时,采用管道符(|)连接多个表达式属性。除了通过入参的方式设置表达式属性,还可以使用嵌入式标识来设置表达式属性,格式为:(?表达式属性1表达式属性2)正则表达式,示例——不区分大小写和全局匹配abcd:(?ig)abcd */ Pattern compile(String regex); Pattern compile(String regex, int flag); // 字符串完整匹配表达式的快捷方式,内部依然是 // Pattern p = Pattern.compile(regex); // p.matcher(input).matches(); boolean matches(String regex, CharSequence input); // 返回可以配置入参s的字面量模式。注意格式为\\Q表达式\\E。表达式中的元字符将当作普通字符处理 String quote(String s);
表达式属性:
// 以\n作为换行符,内嵌为(?d) Pattern.UNIX_LINES // US-ASCII编码字符不区分大小写,内嵌为(?i) Pattern.CASE_INSENSITIVE // 忽略空格和注释(注释为以#开头直到出现换行符),内嵌为(?x) Pattern.COMMENTS // 启动多行模式,^和$匹配换行符或字符串起始位置。默认为单行模式,^和$仅匹配字符串起始位置。内嵌为(?m) Pattern.MULTILINE // 字面量模式,将元字符当作普通字符处理,没有内嵌方式,但可以通过"\\Q正则表达式\\E"的方式实现 Pattern.LITERAL // 元字符.将匹配换行符。默认情况下,元字符.不匹配换行符。内嵌为(?s) Pattern.DOTALL // UNICODE编码字符不区分大小写,内嵌为(?u) Pattern.UNICODE_CASE // 当且仅当正则分解匹配时才配置成功。 Pattern.CANON_EQ // 启用Unicode版本的预定义字符类和POSIX字符类,内嵌为(?U) Pattern.UNICODE_CHARACTER_CLASS
实例方法:
// 返回正则表达式 String pattern(); // 使用正则表达式匹配的字符串切割入参input // 入参limit用于设置返回数组长度的最大值,设置为0时则不限制最大值。 String[] split(CharSequence input); String[] split(CharSequence input, int limit); // 获取匹配类 Matcher matcher(CharSequence input);
2. java.util.regex.Matcher类 :匹配类,用于存储模式实例匹配某字符串后所产生的结果。
静态方法:
// 将入参s中的\和$元字符转换为普通字符,并返回处理后的s字符串。 String quoteReplacement(String s)
实例方法:
// 获取匹配子字符串的起始索引 int start(); // 获取匹配子字符串的结束索引 int end(); // 从字符串的end+1位置开始搜索下一个匹配的字符串 boolean find(); boolean find(int start); // 通过分组索引获取分组内容,若入参group超出分组数量则抛异常 String group(); String group(int group); // 通过分组名称获取分组内容,若没有相应的分组则返回null String group(String name); // 重置匹配实例内部的状态属性 Matacher reset(); // 重置匹配实例内部的状态属性,并重置被匹配的字符串 Matacher reset(CharSequence input); // 重置模式实例,这导致group信息丢失,但注意:start等信息依旧保留不变。 Matcher usePattern(Pattern newPattern); // 从字符串起始位开始将匹配成功的子字符串均用入参replacement替换掉 String replaceAll(String replacement); // 从字符串起始位开始将第一个匹配成功的子字符串均用入参replacement替换掉 String replaceFirst(String replacement); // 将从字符串起始位开始到最后一匹配的子字符串最后一个字符的位置的字符串复制到sb中,并用入参replacement替换sb中匹配的内容 String appendReplace(StringBuffer sb, String replacement); // 将剩余的子字符串复制到sb中 String appendTail(StringBuffer sb); // 示例: sb为one dog two dog Matcher m = p.matcher("one cat two cats in the yard"); StringBuffer sb = new StringBuffer(); while (m.find()) { m.appendReplacement(sb, "dog"); } // 字符串从头到尾匹配表达式 boolean matches(); // 从字符串起始位置开始匹配表达式,但不要字符串从头到尾匹配表达式 boolean lookingAt();
实例方法:
/**
* 若要将\替换为\\,则需要写成 replaceAll("\\\\", "\\\\\\\\")
* 由于参数regex和replacement都被正则表达式引擎识别使用,因此书写 "\\\\" 首先会被Java编译器识别为字符串 "\\",然后被正则表达式引擎识别为 "\"。
*/
String replaceAll(String regex, String replacement); String replaceFirst(String regex, String replacement);
/**
* 将字符串中的某一字符全部替换为指定的字符。
*/
String replace(char oldChar, char newChar); String[] split(String regex); String[] split(String regex, int limit); boolean matches(String regex)
五、最短路径实现诉求
final class RegExp{ // 全字符串匹配 public static boolean isMatch(String regex, String input){ if (null == input) return false; return input.matches(regex); } // 包含子字符串 public static boolean contains(String regex, String input){ Pattern r = Pattern.compile(regex); return r.matcher(input).find(); } // 实现indexOf public static int indexOf(String regex, String input){ Pattern r = Pattern.compile(regex); Matcher m = r.matcher(input); int index = -1; if(m.find()) index = m.start(); return index; } // 实现lastIndexOf public static int lastIndexOf(String regex, String input){ Pattern r = Pattern.compile(regex); Matcher m = r.matcher(input); int index = -1; while(m.find()) index = m.start(); return index; } // 替换全部匹配字符串 public static String replaceAll(String regex, String input, String replacement){ if (null == regex || regex.isEmpty()) return input; return input.replaceAll(regex, replacement); } // 替换第N个匹配字符串 public static String replaceSome(String regex, String input, String replacement, int n){ if (null == regex || regex.isEmpty()) return input; if (0 == n) return input.replaceFirst(regex, replacement); Pattern r = Pattern.compile(regex); Matcher m = r.matcher(input); int i = 0; StringBuffer buffer = new StringBuffer(); while (i <= n && m.find()){ if (i == n){ m.appendReplacement(buffer, replacement); m.appendTail(buffer); } ++i; } if (0 == buffer.length()) buffer.append(input); return buffer.toString(); } // 萃取字符串 public static String extract(String regex, String input){ String ret = ""; Pattern r = Pattern.compile(regex); Matcher m = r.matcher(input); if (m.find()) ret = m.group(); return ret; } // 拆分字符串 public static String[] split(String regex, String input, int limit){ if (null == input || input.isEmpty() || null == regex || regex.isEmpty()) return new String[]{input}; return input.split(regex, limit); } }
实际应用时当然不会像上面那么简单了。
本节内容仅针对正则表达式的高级功能语法进行叙述,而各语言的正则实现也就是这部分有所差异而已。
1. 分组及反向引用
[a]. (子表达式) ,自动命名分组(从1开始以数字自动为分组命名),后续表达式中可通过反向引用来获取该分组的内容。例如匹配字符串“so so”的正则表达式可以是 ^(\w{2})\s(\1)$ ,其中 \1 就是反向引用。
[b]. (?:子表达式) ,非捕获分组,该类型的分组将不纳入匹配对象的group属性中,并且无法通过反向引用在表达式的后续部分获取该分组的内容。通常是配合 | 使用。例如匹配字符串"so easy"和"so hard"的正则表达式可以是 so\s(?:easy|hard)
[c]. (?<name>子表达式) ,命名分组,该类型的分组将纳入匹配对象的group属性中,并且可以在group属性值中通过name值来获取该分组的值。
[d]. (?#注释) ,注释分组,该类型分组的内容将被正则表达式编译器忽略,仅供码农查阅而已。
2. 零宽先行断言
零宽先行断言初看之下有点不知所云的感觉, 那么我们拆开来分析一下它的意思吧!
零宽——意思是匹配的子表达式将不被纳入匹配结果,仅作为匹配条件而已。
先行——意思是子表达式匹配的是后续字符串的内容。
并且其细分为两类:
[a]. 子表达式B(?=子表达式A) ,零宽正向先行断言(也称为预搜索匹配)。例如匹配字符串"abcd"中的a和b的正则表达式可以是 \w(?=\w{2})
[b]. 子表达式B(?!子表达式A) ,零宽负向先行断言(也称为预搜索不匹配)。例如匹配字符串"abcd"中的c和d的正则表达式可以是 \w(?!\w{2})
3. 零宽后行断言
后行——意思是子表达式匹配的是前面字符串的内容。
[a]. (?<=子表达式A)子表达式B ,零宽正向后行断言(也称为反向搜索匹配)。例如匹配字符串"abcd"中的c和d的正则表达式可以是 (?<=\w{2})\w
[b]. (?<!子表达式A)子表达式B ,零宽负向后行断言(也称为反向搜索不匹配)。例如匹配字符串"abcd"中的a和b的正则表达式可以是 (?<!\w{2})\w
4. 平衡组
作用:用于匹配左右两边开始、结束符号数量对等的字符串。
示例——萃取"<div>parent<div>child</div></div></div>"的子字符串"<div>parent<div>child</div></div>"
失败的正则表达式: <div>.*</div> ,匹配结果为"<div>parent<div>child</div></div></div>"。
成功的正则表达式: ((?'g'<div>).*?)+(?'-g'</div>)+ ,匹配结果为"<div>parent<div>child</div></div>"。
在分析上述示例前,我们要认识一下平衡组相关的语法。
(?'name'子表达式A) ,若成功匹配子表达式A,则往名为name的栈空间压一个元素。
(?'-name'子表达式A) ,若成功匹配子表达式A,则弹出名为name的栈空间的栈顶元素,弹出元素后若栈空间为空则结束匹配。
(?(name)yes表达式|no表达式) ,若名为name的栈空间非空,则使用yes表达式进行匹配,否则则使用no表达式进行匹配。
(?(name)yes表达式) ,若名为name的栈空间非空,则使用yes表达式进行匹配。
(?!) ,由于没有后缀表达式,因此总会导致匹配失败并结束匹配。
下面我们一起来分析 ((?'g'<div>).*?)+(?'-g'</div>)+ 的匹配流程吧!
<div>parent # 步骤1,((?'g'<div>).*?)匹配成功,然后向g栈压入一个元素 <div>child # 步骤2,((?'g'<div>).*?)匹配成功,然后向g栈压入一个元素,现在栈含2个元素 </div> # 步骤3,(?'-g'</div>)匹配成功,然后弹出g栈的栈顶元素,现在栈含1个元素 </div> # 步骤4,(?'-g'</div>)匹配成功,然后弹出g栈的栈顶元素,现在栈含0个元素 # 步骤5,由于g栈为空因此结束匹配,返回<div>parent<div>child</div></div>
从该例子我们可以知道平衡组可以解决一些棘手的文本处理问题。但遗憾的是直到JDK1.7的原生API依旧不支持平衡组的功能语法,其余功能语法均被支持。而.Net的Regex类则支持平衡组,在这方面显然全面一些。当然比js连零宽后行断言都不支持要强不少了。
2015/10/30追加
注意:若正则表达式仅含/()/、/(?:)/或/(?=)/,则匹配任何字符串均返回匹配成功,且配结果为空字符串。而JS中 RegExp('') 所生成的是无捕获分组 /(?:)/ 。
而仅含/(?!)/,则匹配任务字符串均返回匹配失败。
console.log(RegExp('').test("12345")) // 显示true console.log((?:)/.test("12345")) // 显示true console.log(/(?=)/.test("12345")) // 显示true console.log(/()/.test("12345")) // 显示true console.log(/(?!)/.test("12345")) // 显示false
到这里我们已经对Java对正则表达式的支持程度有一定程度的掌握,虽然不支持平衡组但已经为我们提供强大的文本处理能力了。不过我依旧不满意那个碍眼的转义符 \ ,假如我们要写正则表达式 \w\\\{\} 但实际运用时却要写成 \\w\\\\\\{\\} ,倘若能够像JS的正则表达式字面量一样使用,那就舒畅不少了!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4098623.html ^_^肥仔John
http://deerchao.net/tutorials/regex/regex-1.htm
http://www.cnblogs.com/kissdodog/archive/2013/04/25/3043122.html
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!