摘要:正则表达式主要用途是文本规则的匹配和验证,在处理字符串的程序或网页时,经常需要对用户输入进行正则验证,从而简化复杂的程序编写逻辑,因此,正则表达式实际上就是一种文本规则的验证工具。

引例

在正则表达式诞生之前,通常我们对某一字符串文本进行验证的时候,需要编配大量的匹配规则,尤其出现在需要用户输入的地方,比如对用户输入的用户名,密码和邮箱进行输入验证,如果存在非法字符,则可能认为是恶意输入。下面通过Java语言介绍一个没有使用正则表达式的邮箱输入验证案例。

给定一个邮箱:aB-c_d.ef@g1_2-3h.i4_J-k, 对于该邮箱(任意邮箱),我们将其划分为3个部分,首先,邮箱一定至少包含一个”.”号且只包含一个”@”符号,以这两个符号将邮箱分成3部分,第一部分为@之前的内容,可以是任意字母,数字,中划线,下划线,点号;第二部分为@与最后一个点号之间的内容,可以是任意字母,数字,中划线,下划线,但不可以是点号;第三部分为最后一个点号一直到结尾处的内容,实际上应该是域名,为了简化,我们认为此处内容也可以是任意字母,数字,下划线,中划线,但不可以是点号。

明白以上规则之后,见代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class TestRegexp {
public static void main(String[] args) {
String emailStr = "csuzhangvae@163.com";//[\w-\.]+@[\w-]+\.[\w-]+
if(regexVerify(emailStr)){
if(verifyDNS(emailStr)){
System.out.println("email is right");
}else{
System.out.println("email is wrong");
}
}
}

public static boolean isDigit(char c){
if(c >= '0' && c <= '9'){
return true;
}else{
return false;
}
}

public static boolean isLetter(char c){
if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')){
return true;
}else{
return false;
}
}

public static boolean commVerify(String emailStr){
boolean flag = true;
if(emailStr.indexOf(".") == -1 || emailStr.indexOf("@") == -1){
flag = false;
}else{
int highIndex = emailStr.lastIndexOf(".");
String highStr = emailStr.substring(highIndex+1);
System.out.println(highStr);

char highChars[] = highStr.toCharArray();
for(int i=0;i<highChars.length;i++){
if(!isDigit(highChars[i]) && !isLetter(highChars[i]) && highChars[i] != '-' && highChars[i] != '_'){
flag = false;
break;
}
}

int middleIndex = emailStr.indexOf("@");
String middleStr = emailStr.substring(middleIndex+1,highIndex);
System.out.println(middleStr);

char middleChars[] = middleStr.toCharArray();
for(int i=0;i<middleChars.length;i++){
if(!isDigit(middleChars[i]) && !isLetter(middleChars[i]) && middleChars[i] != '-' && middleChars[i] != '_'){
flag = false;
break;
}
}
int lowIndex = 0;
String lowStr = emailStr.substring(0,middleIndex);
System.out.println(lowStr);

char lowChars[] = lowStr.toCharArray();
for(int i=0;i<lowChars.length;i++){
if(!isDigit(lowChars[i]) && !isLetter(lowChars[i]) && lowChars[i] != '-' && lowChars[i] != '_' && lowChars[i] != '.'){
System.out.println(lowChars[i]);
flag = false;
break;
}
}


}
return flag;
}
}

以上代码虽然可以实现邮箱规则的匹配,但是各种条件语句和循环语句的嵌套导致结构不清晰,代码量多。

如何改用正则表达式呢。这便是本文要介绍的问题。

正则表达式

正则表达式是一种文本规则描述的匹配工具,其规则和语法非常多,也不需要全部记住,但对最基本的一定要牢记,最基本最简单的正则表达式就是文本字符串本身, 用于匹配与该字符串相等的字符串。

[]表示选择

表达式 描述
[abc] 表示取值可以是a,可以是b,也可以是c
[^abc] 表示取值不是abc中的任意字符,即除abc以外的内容
[a-zA-Z] 表示所有字母,大写和小写,[a-z]表示小写字母,[A-Z]表示大写字母

常用元字符

表达式 描述
. 除换行符以外的任意字符
\d 所有数字
\w 所有字母,数字,下划线
\s 所有空白字符
\b 单词边界位置
^ 字符串开始
$ 字符串结尾

常用的反义字符

表达式 描述
\D 非数字
\W 非字母、数字、下划线
\S 非空白字符
\B 非单词开始或结束边界位置

表示出现的次数

表达式 描述
X? 表示出现0次或1次
X* 表示出现0次或多次
X+ 表示出现1次或多次
X{n} 表示出现n次
X{n,} 表示出现的长度大于n次
X{n,m} 表示出现n到m次

关系运算

表达式 描述
X或Y 表示要么是X的正则,要么是Y的正则
(X) 表示一组规范,一个分组

匹配策略

介绍了以上的正则表达式及其表示含义之后,大家都或多或少了解了一些相关知识,现在顺便介绍一下正则表达式的贪婪匹配和惰性匹配策略。

一般匹配都是遵循贪婪匹配策略,即尽可能多地匹配,而以下规则支持惰性匹配,即尽可能少地匹配。

表达式 描述
*? 重复任意次,但尽可能少地重复
*? 重复任意次,但尽可能少地重复
+? 重复1次或多次,但尽可能少地重复
?? 重复0次或1次,但尽可能少地重复
{n,m}? 重复n到m次,尽可能少地重复
{n,}? 重复至少n次,但尽可能少地重复

正则验证

下面就上面介绍地邮箱验证的例子,我们给出使用正则表达式进行规则匹配的案例,对比之下,可以发现,使用正则表达式,不但能使程序结构清晰,也大大简化了代码量和我们的逻辑判断工作。

JAVA对正则表达式有着良好地支持,自JDK1.4,它提供了java.util.regex包,主要负责正则匹配的两个类为Pattern类和Matcher类,Pattern根据指定正则表达式字符串构建匹配模式,并利用matcher方法返回Matcher类对象实例,用于完成匹配,实际的匹配是交给Matcher完成的,Pattern类对象的构造是通过静态方法compile构造。

1
2
3
4
5
6
7
8
9
public static boolean regexVerify(String emailStr){
Pattern p = Pattern.compile("[\\w-\\.]+@[\\w-]+\\.[\\w-]+");
Matcher m = p.matcher(emailStr);
if(m.matches())
//if(emailStr.matches("[\\w-\\.]+@[\\w-]+\\.[\\w-]+"))//String object has a good support to regexp
return true;
else
return false;
}

从以上代码可以看出,使用Java的正则表达式可以快速完成与大量逻辑判断相同的工作,且结构清晰,只要定义的正则表达式正确,其实在Java中一般不直接使用Pattern类和Matcher类完成正则模式匹配,而使用String自带的正则支持。因为正则匹配的对象都是文本字符串,目的就是为了验证文本规则,而String在Java中又是一个及其特殊的类,因此自从JDK1.4,引进正则表达式,String类就作了相应的修改,提供了split,matches,replaceAll,replaceFirst等方法。如上述代码注释的内容即为直接使用String对象的matches方法,其效果是完全一样的。

另外我想延伸的一点是,上述程序代码即正则表达式规则,虽然能够判别邮箱,但是邮箱通过的域名都是有限的(如163.com,qq.com),因此,本文提供精确的邮箱认证方式,即登录到邮箱服务器,验证邮箱是否存在,并返回验证结果。

该功能需要Appache 的common-net.jar和dnsjava.jar包的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public static boolean verifyDNS(String emailStr) {
String host = "";
String hostName = emailStr.split("@")[1];
Record[] result = null;
SMTPClient client = new SMTPClient();

try {
// 查找MX记录
Lookup lookup = new Lookup(hostName, Type.MX);
lookup.run();
if (lookup.getResult() != Lookup.SUCCESSFUL) {
return false;
} else {
result = lookup.getAnswers();
}

// 连接到邮箱服务器
for (int i = 0; i < result.length; i++) {
host = result[i].getAdditionalName().toString();
client.connect(host);
if (!SMTPReply.isPositiveCompletion(client.getReplyCode())) {
client.disconnect();
continue;
} else {
break;
}
}

client.login("xxxxxxxxxxxxxxxz.com");
client.setSender("xxxxxxxxxxxx@xxxxxxxxxxxxxxx.com");

client.addRecipient(emailStr);
if (250 == client.getReplyCode()) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
client.disconnect();
} catch (Exception e) {
}
}
return false;
}

这部分代码拷贝至http://www.eziep.net/details/80.html。以下是常见的正则匹配规则,参考自网上相关博客:

  • 匹配中文字符:[u4e00-u9fa5]
  • 匹配空白行: ns*r
  • 匹配html标签: :<(S?)[^>]>.?|<.? /> 或 </?[^>]+>
  • 标准的匹配Email:w+([-+.]w+)@w+([-.]w+).w+([-.]w+)*
  • 匹配Url::[a-zA-z]+://S*
  • 匹配帐号是否合法: ^[a-zA-Z][a-zA-Z0-9_]{4,15}$
  • 匹配国内电话号码: d{3}-d{8}|d{4}-d{7}
  • 匹配腾讯QQ号: [1-9][0-9]{4,}
  • 匹配中国邮政编码: [1-9]d{5}(?!d)
  • 匹配身份证:d{15}|d{18}
  • 匹配ip地址:d+.d+.d+.d+