一、概述
正则表达式就是使用字符串来匹配一系列符合某个语法规则的字符串,在Java中正则表达式主要用到java.util.regex
包中的Pattern
类和Matcher
类,主要分为四种类型的正则操作,分别是匹配、替换、切割和获取。其特点是书写简单,使用方便,也有一些弊端,如规则阅读困难,对于复杂的规则不易于理解等。
二、String
中的正则操作
通过一个简单的实例来演示一下正则表达式的方便之处,如果用普通的方式验证一串字符串是否为一个合法的手机号码时,大致做法会如下,代码如下:
class RegexDemo {
public static void main(String[] args) {
String number = "18015501550";
boolean is = isPhoneNumber(number);
System.out.println(number + (is?"是":"不是") + "手机号");
}
/** * 验证是否为手机号 */
private static boolean isPhoneNumber(String number) {
if(number.length() != 11) {
return false;
}
if(number.charAt(0) == '0') {
return false;
}
char[] chs = number.toCharArray();
for(int i=0; i<chs.length; i++) {
if(chs[i] < '0' || chs[i] > '9') {
return false;
}
}
return true;
}
}
如果使用正则表达式来验证,代码只需要一句如下,正则表达式的优点不言而喻:
boolean is = number.matches("[1-9][0-9]{10}");
String
的replaceAll(String,String)
是将符合匹配的内容替换为指定内容,也是一种正则表达式。split(String)
是一种切割字符串功能,其返回String[]
,即是切割的分段结果。
三、正则表达式的基本语法
虽然开始已经说了正则操作分为四种,分别是匹配、替换、切割和获取,但是实质上都是匹配,如替换则是先匹配到待替换的内容,然后再将其替换掉;同样切割则是先匹配到用于切割的分隔符,然后再将其切割;获取则是匹配到的内容。
那么正直的基本匹配语法是哪些呢?String的例子已经说明了一些,如[]
,{}
等。
[]
:其表示一个字符,内部可以书写一定的规则,如[abc]
表示这个字符可以使a、b或者c中的一个;[^abc]
则表示这个字符不是a,也不是b,也不是c;其他的还有[1-9]
、[a-z]
、[a-zA-Z]
或者[a-d[m-p]]
嵌套式的形式,总之[]
是一个字符的规则。
{}
:其可以作为一些数量的规则,跟在一个字符的规则后面,如[1-9]{10}
表示[1-9]
规则重复10次,{10,}
10次以上,{10,20}
10次到20次。
()
:其表示分组,分组的功能是为了让后面的规则可以获取前面规则的结果,使用示例会更加容易理解,我们想使用aa、hh、kk作为分隔符,那么我们应该怎么做?"[a-z][a-z]"
?显然不可以,因为后一个字符和前一个字符要有一定的关系,这就是分组的功能所在,()
为一个分组,编号会自动从1开始,所以"([a-z])"
的编号为1,后一个字符就是分组1,如何写?编号
表示取得编号
的结果,因为
需要转义,所以这里写了两次,代码如下:
class RegexDemo {
public static void main(String[] args) {
String str = "1aadswghh,sdlfkksdf";
String[] strs = str.split("([a-z])1");
for(String s : strs) {
System.out.println(s);
}
}
}
1
dswg,sdlf
sdf
d
:其实和[0-9]
的规则一样,是一种简写方式。
+
:表示前一个规则有一个或者多个。
*
:表示前一个规则有0个或者多个。
- 反斜线、转义和引用:反斜线字符 (‘’) 用于引用转义构造,如上表所定义的,同时还用于引用其他将被解释为非转义构造的字符。因此,表达式 与单个反斜线匹配,而 { 与左括号匹配。在不表示转义构造的任何字母字符前使用反斜线都是错误的;它们是为将来扩展正则表达式语言保留的。可以在非字母字符前使用反斜线,不管该字符是否非转义构造的一部分。
字符类
字符类可以出现在其他字符类中,并且可以包含并集运算符(隐式)和交集运算符 (&&)。并集运算符表示至少包含其某个操作数类中所有字符的类。交集运算符表示包含同时位于其两个操作数类中所有字符的类。字符类运算符的优先级如下所示,按从最高到最低的顺序排列:
1 字面值转义 x
2 分组 [...]
3 范围 a-z
4 并集 [a-e][i-u]
5 交集 [a-z&&[aeiou]]
注意,元字符的不同集合实际上位于字符类的内部,而非字符类的外部。例如,正则表达式 . 在字符类
内部就失去了其特殊意义,而表达式 - 变成了形成元字符的范围。
行结束符:行结束符 是一个或两个字符的序列,标记输入字符序列的行结尾。以下代码被识别为行结束符:
新行(换行)符 ('n')、
后面紧跟新行符的回车符 ("rn")、
单独的回车符 ('r')、
下一行字符 ('u0085')、
行分隔符 ('u2028') 或
段落分隔符 ('u2029)。
如果激活 UNIX_LINES 模式,则新行符是唯一识别的行结束符。
组和捕获:捕获组可以通过从左到右计算其开括号来编号。例如,在表达式 ((A)(B(C))) 中,存在四个这样的组:
1 ((A)(B(C)))
2 A
3 (B(C))
4 (C)
组零始终代表整个表达式。之所以这样命名捕获组是因为在匹配中,保存了与这些组匹配的输入序列的每个子序列。捕获的子序列稍后可以通过 Back 引用在表达式中使用,也可以在匹配操作完成后从匹配器获取。与组关联的捕获输入始终是与组最近匹配的子序列。如果由于量化的缘故再次计算了组,则在第二次计算失败时将保留其以前捕获的值(如果有的话)例如,将字符串 “aba” 与表达式 (a(b)?)+ 相匹配,会将第二组设置为 “b”。在每个匹配的开头,所有捕获的输入都会被丢弃。以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。
四、正则表达式操作
正则表达式的四种操作,除了String
类中的简易匹配、替换、切割、获取等操作外,真正的正则操作使用的是Pattern
和Matcher
,String
的内部的一些操作也是使用的这两个类。他们的基本使用格式如下:
import java.util.regex.*;
class RegexDemo {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("[a-z]");
Matcher matcher = pattern.matcher("a");
System.out.println(matcher.matches());
matcher.reset();
System.out.println(matcher.find());
System.out.println(matcher.group());
}
}
一般可以归纳为以下步骤:
- 获取正则表达式封装类
Pattern p = Pattern.compile(regex)
。需要指定一种规则。
- 用待匹配的字符串作为参数获取匹配器
Matcher
,Matcher matcher = p.matcher(str)
。
- 做相应的处理,如判断是否匹配
matcher.matches()
,替换matcher.replaceAll(replacement)
,切割或者获取(借助find()
和group()
)。
1、匹配
用一个邮箱验证实例来演示一下匹配的操作,[a-zA-Z0-9_]
表示接收字母和数值还有下划线,后面跟一个+
表示前面的字符规则可以使一个或者一个以上,(.[a-zA-Z]+)+
表示点加上字母,然后分为一组,这个组可以一个或者一个以上如.com.cn
,具体代码如下:
import java.util.regex.*;
class RegexDemo {
public static void main(String[] args) {
String regex = "[a-zA-Z0-9_]+@[a-zA-Z0-9]+(.[a-zA-Z]+)+";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher("abc@163.com");
System.out.println(matcher.matches());
}
}
2、替换
先有两个问题,一是将一句结结巴巴的话回复成正常语句,首先将.
全部清除,.+
表示一个或多个点,然后将叠词变为一个字,那么就需要用到替换中的另一种组用法,就是在前一个字符串的分组,被后一个字符串用到,(.)1+
表示任意一个字符重叠大于1次,使用$1
替换,这里的$
表示前面正则字符串中的分组,即(.)
满足条件的那个字符。所以这样便恢复出了正常语句;二是将一串ip地址按照分段排序,思路便是将ip的每一段调整为等长的字符数字,然后按照字典序排序即可,先添加两个0,然后化成等长3个字符,最后去掉多余的0,具体代码如下:
import java.util.regex.*;
import java.util.*;
class RegexDemo {
public static void main(String[] args) {
replace1();
replace2();
}
private static void replace1() {
String str = "我我...我我...我要..要要...要要..."
+ "学学学....学学...编编编...编程..程.程程...程...程";
str = str.replaceAll(".+","").replaceAll("(.)1+","$1");
System.out.println(str);
}
private static void replace2() {
String ips = "192.68.1.254 102.49.23.013 10.0.10.10 2.2.2.2 8.109.90.30";
ips = ips.replaceAll("(d+)","00$1").replaceAll("0*(d{3})","$1");
System.out.println(ips);
String[] arr = ips.split(" ");
TreeSet<String> tset = new TreeSet<String>();
for(String s : arr) {
tset.add(s);
}
for(String s : tset) {
System.out.println(s.replaceAll("0*(d+)","$1"));
}
}
}
我要学编程
192.068.001.254 102.049.023.013 010.000.010.010 002.002.002.002 008.109.090.030
2.2.2.2
8.109.90.30
10.0.10.10
102.49.23.13
192.68.1.254
3、切割
切割与获取的功能有一定的重合,获取是获取符合匹配的,而切割则是获取符合匹配之外的内容,一般切割都是使用直接使用String
的split(regex)
方法,如按照多个空格切割,代码如下:
class RegexDemo {
public static void main(String[] args) {
String str = "ada sadasd asdas asd as d ad s";
String[] strs = str.split(" +");
for(String s : strs) {
System.out.println(s);
}
}
}
ada
sadasd
asdas
asd
as
d
ad
s
4、获取
从一个文本文件读取内容,然后从中提取邮箱,也可以将其变成从网络页面上获取邮箱的功能,如果同一个邮箱的网页爬虫一样,示例代码如下:
import java.io.*;
import java.util.regex.*;
import java.net.*;
import java.util.*;
class RegexTest {
public static void main(String[] args) throws Exception {
BufferedReader reader = new BufferedReader(new FileReader("mail.txt"));
String regex = "[a-zA-Z0-9_]+@[a-zA-Z0-9]+(.[a-zA-Z]+)+";
Pattern pattern = Pattern.compile(regex);
String line = null;
while((line=reader.readLine())!=null) {
Matcher matcher = pattern.matcher(line);
while(matcher.find()) {
System.out.println(matcher.group());
}
}
}
}
abc0@sina.com
abc1@sina.com
abc2@sina.com
abc3@sina.com
ab4c@sina.com
abc5@sina.com
abc6@sina.com
abc7@sina.com
abc8@sina.com
abc11@sina.com
abc9@sina.com
abc12@sina.com
abc55@sina.com
abc123@sina.com
abc1234@sina.com
qqq@sina.com
附录
下面列出了一些正则中的特殊字符或者规则。