正则表达式在实际开发过程中应用最多的场合就是验证数据格式的正确性,例如用户在一个网站注册时需要填写电子邮件地址、手机号码等信息,这些信息都有特定格式,使用正则表达式就能验证这些信息的格式是否正确。本小节将详细讲解如何使用正则表达式验证这些数据格式的正确性。
16.5.1验证手机号格式
用户在一个网站完成注册时往往需要填写手机号,本小节将讲解手机号的验证过程。手机号码都是以“1”开头的,到目前为止,我国的手机号从“13”到“19”开头的都有,但是没有“10”、“11”或者是“12”开头的,因此,以字符串形式的正则表达式表示手机号开头并把开头作为整体的正则表达式是:
String reg = ("13|14|15|16|17|18|19");
手机号第3位数字的取值范围各不相同。对于“13”和“18”开头开头的手机号,第3位数字的范围是从0到9,对于14开头的手机号,目前第3位数字只有5、7、9三种的范围了。对于“15”开头的手机号,目前第3位数字不包含4,其他的数字都有。对于“16”开头的手机号而言,目前第3位数字只有6。对于“17”开头的手机,目前第3位数字有0、1、3、5、6、7、8。对于“19”开头的手机号,它的第3位只有8和9两个数字。因此按照这个规则,可以把上面的正则表达式继续完善为:
String reg="(13[0-9]|14[579]|15[0-35-9]|16[6]|17[0135-8]|18[0-9]|19[89])";
手机号总共11位,目前正则表达式定义了前3位的格式,后面8位对于任意手机号都是从0-9的范围,因此可以直接用表示数字的通配符“\d”定义,并且需要加上出现次数的限制,因此,验证手机号的正则表达式的完整形态为:
String reg="(13[0-9]|14[579]|15[0-35-9]|16[6]|17[0135-8]|18[0-9]|19[89])\\d{8}";
下面的【例16_20】展示了使用正则表达式验证手机号正确性的过程。
【例16_20 验证手机号】
Exam16_20.java
import java.util.*;
import java.util.regex.*;
public class Exam16_20 {
public static void main(String[] args) {
String reg="(13[0-9]|14[579]|15[0-35-9]|16[6]|17[0135-8]|18[0-9]|19[89])\\d{8}";
Scanner sc = new Scanner(System.in);
System.out.println("请输入手机号:");
String input = sc.nextLine();
System.out.println("手机号格式是否正确:"+Pattern.matches(reg,input));
}
}
【例16_20】的运行结果如图16-20所示。
图16-20【例16_20】运行结果
图16-20展示的是一个正确的格式,读者也可以输入错误的手机号格式以观察检验结果。
16.5.2验证电子邮箱格式
电子邮箱地址定义成什么样子才算符合规则需要分两种情况进行讨论。第一种情况是申请开通邮箱的时候,第二种情况就填写个人信息的时候。申请开通电子邮箱时,邮箱的格式由用户申请邮箱地址的门户网站规定。例如:用户如果申请的是新浪网站的邮箱,那么要求邮箱地址@符号以前的部分必须由4到16个字符组成,可使用英文小写、数字、下划线,下划线不能在开头或者是末尾。而如果用户申请的是网易的邮箱,那么@符号之前的部分必须是6到18个字符组成,可使用字母、数字、下划线,并且必须以字母开头。如果是腾讯的QQ邮箱,那么这个邮箱地址@符号之前的部分就是用户的QQ号,它是一组纯数字,并不要求必须以字母开头。因此,各大门户网站邮箱地址的格式要求各不相同,所以在申请邮箱的时候,验证邮箱格式合法性的代码也各不相同。我们把这种某个门户网站专门编写的验证邮箱格式的程序,称之为“专用邮箱验证程序”。
第二种情况是在填写个人信息的时候,比如说在网上办理信用卡,对方要求用户留下一个电子邮箱的地址以便有什么事情能够及时把通知发送到邮箱中。这种情况下,用户留新浪邮箱或者是QQ邮箱都可以,只要这个邮箱能够收邮件就行。这种情况下对电子邮箱地址的格式验证就不能以某个门户网站的要求作为标准了,而是要设定一个通用的检验标准,这个标准能够让所有可以使用的合法邮箱都能够验证通过。我们把这种验证邮箱格式的程序称之为“通用邮箱验证程序”。下面要给读者讲解的就是“通用邮箱验证程序”。
任何一个邮箱地址的格式都可以概括为以下形式:
第一部分是x,这部分位于“@”符号之前,这部分内容是用户自己定义的。第二部分是y,是网站域名的主体,位于“@”符号和“.”之间。第三部分是z,是域名的结尾,位于“.”的后面。
网站都要求用户的电子邮箱地址不能“-”、“_”、“*”这些符号开头,一般要求都是以英文字母开头,但像QQ邮箱这样的邮箱也可以用数字开头,因此邮箱地址的第一个字符是英文字母或者是数字,定位为字符串形式的正则表达式为:
String reg="[a-z0-9A-Z]"
第一个字符之后,从第二个字符一直到第N个字符,都可以是数字、字母、“-”、“_”以及“.”。有的读者可能觉得不可思议,认为不可以在“@”符号之前出现“.”。实际上很多门户网站的邮箱当中是允许出现“.”。这些字符可以出现N次,但这个N的上限是多少呢?其实程序员没法规定这个上限,这是因为各大门户网站在用户申请邮箱的时对于邮箱地址的x部分长度的要求也是不一样的。因此,邮箱地址的x部分可以定义为:
String reg="[a-z0-9A-Z][a-z0-9A-Z._-]*";
需要注意:在以上表达式的自定义通配符当中出现了“-”、“_”以及“.”,之前讲过:“-”只要不出现在两个字符之间,表达式引擎就不会把它当作特殊符号。x部分的第2个字符一直到第N个字符,格式要求的是一样的,因此在第二个字符的后面是一个表示出现次数的“*”。
邮箱的第二部分,也就是y这一部分是域名主体部分。域名主体可以包含只有英文母、数字以及“-”,但域名的开头及结尾均不能是“-”,“-”也不能连续出现。域名最长为60个字符。因此域名主体加上前面的x部分和固定出现的“@”的验证表达式为:
String reg="[a-z0-9A-Z][a-z0-9A-Z._-]*@[a-z0-9A-Z]([-]?[a-z0-9A-Z])*";
邮箱的第三部分也就是z这一部分是域名的后缀部分。所谓域名的后缀部分,其实就是邮箱地址的“.com”或者“.cn”那一部分。现在的域名后缀除了“com”或者“cn”这样的传统形式,又出现了很多像“net”或者是“org”这样的形式,甚至还有一些更为稀有的像“top”、“xyz”、“fun”、“shop”这样的形式,并且以后还会出现更多形式。这种情况下,只能把域名的后缀部分定义成一个“字母与数字组合”的格式。域名的后缀部分可以出现多层分级。比如“.com.cn”或者是“.cn.com”等等。甚至在某种情况下,还可能出现3层分级。为了表示这种多层分级,可以把域名的后缀部分括起来,然后在后面添加一个表示出现次数的“+”。因此表示邮箱地址格式的正则表达式完整形态为:
String reg="[a-z0-9A-Z][a-z0-9A-Z._-]*@[a-z0-9A-Z]([-]?[a-z0-9A-Z])*(\\.[a-zA-Z0-9]+)+";
下面的【例16_21】展示了使用正则表达式验证手机号正确性的过程。
【例16_21验证电子邮箱】
Exam16_21.java
import java.util.*;
import java.util.regex.*;
public class Exam16_21 {
public static void main(String[] args) {
String reg =
"[a-z0-9A-Z][a-z0-9A-Z._-]*"+
"@[a-z0-9A-Z]([-]?[a-z0-9A-Z])*(\\.[a-zA-Z0-9]+)+";
Scanner sc = new Scanner(System.in);
System.out.println("请输入电子邮箱:");
String input = sc.nextLine();
System.out.println("电子邮箱格式是否正确:"+Pattern.matches(reg,input));
}
}
【例16_21】的运行结果如图16-21所示。
图16-21【例16_21】运行结果
16.5.3验证身份证号码格式
身份证号码也是一种经常需要填写的信息。本小节所讲解的身份证号码验证仅指中国大陆地区的第二代身份证号码的验证。中国大陆地区的第二代身份证总共18位,其中前面的17位都是数字,最后1位可能是数字,也可能是字母“X”。
身份证号码中包含了很多信息,并且这些信息之间具有很强的关联性,例如出生月份与出生日期就有很强的关联性,任何一个人都不会出生在4月31日。因此,程序员无法用一个简单的正则表达式把这个身份证的格式概括出来。如果希望验证一个身份证号码的正确性,需要通过多次验证,每一次验证都检验一个格式的合理性。
第二代身份证号码都是18位,并且前17位都是纯数字,第18位有可能是数字,也有可能是一个字母X。因此,首先要验证身份证号码是否符合这个最基本的格式要求,验证格式的表达式为:
String reg = "^\\d{17}(\\d|[xX])";
身份证验证的第二步是验证行政区域。我们知道:身份证号码的前6位表示发证机关所在的行政区域。其中前两位表示省份、自治区或者是直辖市。身份证编码规则当中,把中国大陆地区划分成了6个大的区域,分别用数字1-6表示这6个大的区域。这6大区域当中所包含的省份的数量并不完全相同,有的多有的少。其中以1开头的表示华北区,华北区包含5个省、自治区和直辖市。分别是:北京、天津、河北、山西和内蒙古。这些省,自治区和直辖市的代号恰好是1到5。以2开头的表示东北区域,东北区域只有3个省,分别是辽宁、吉林和黑龙江,这三个省分别用1到3表示。以3开头的表示华东及东南地区,总共有7个省。以4开头的是华中以及华南地区,总共6个省。以5开头的是西南地区,总共5个省。6开头的是西北地区,总共包含5个省。按照以上划分,表示身份证的前两位的正则表达式为:
String reg = "1[1-5]|2[1-3]|3[1-7]|4[1-6]|5[0-4]|6[1-5]";
需要说明:西南地区的第二位编号是从0开始的,这是因为重庆市原来属于四川省,后来变成了直辖市,所以重庆的身份证号码的前两位做了特殊处理,编号为50。
接下来要验证省份下面的城市编号。表示城市的编号在身份证当中是第3和第4两位。每个省当中城市的编号都是从“01”开始的,理论上编号的最大值是“99”。因此验证城市正则表达式可以写为:
String reg = "0[1-9]|[1-9][0-9] ";
可以看出:表达式用“|”定义了两种情况,这是因为:如果第3位是0,那么第4位的取值范围是1到9,如果第3位不是0,那么第4位的取值范围是0到9。各个城市下属的区县的编号是第5位和第6位,这个编号的理论范围和城市完全相同。
身份证号码的第7位开始一直到第14位结束是生日。生日的正确性很难用一个正则表达式表示出来,例如:一个人是1999年出生的,那么他的身份证号码当中表示出生月份和日期的那4位数字肯定不能是“0229”,因为1999年不是闰年。但是如果这个人是2000年出生的,那么出生月份和日期的号码就可以是“0229”。因此验证生日需要编写一个checkBirthday()方法实现,方法的参数就是8位长度的生日。而在验证生日的checkBirthday()方法中还需要调用判断是否是闰年的leapYear()方法。以下是checkBirthday()和leapYear()方法的实现过程。
boolean checkBirthday(String birthday){
boolean flag = true;
String part = "";//要进行判断的那一部分信息
part = birthday.substring(0,4);//出生年份
//判断出生于哪个世纪
int year = Integer.parseInt(part);//年份转成整型
LocalDate ld = LocalDate.now();//当前年份
flag = (year>=1880 && year<=ld.getYear());
if(flag){
part = birthday.substring(4,6);//月份
int month = Integer.parseInt(part);//月份转成整型
//判断月份的格式正确性
flag = (month>=1 && month<=12);
if(flag){
part = birthday.substring(6,8);//出生日期
int date = Integer.parseInt(part);//出生日期转成整型
if(date>31){//日期大于31一律算错
return false;
}
if((mnotallow==4||mnotallow==6||mnotallow==9||mnotallow==11) && date>30){//处理小月
return false;
}
boolean leapYear = leapYear(year);
if(leapYear==false && mnotallow==2 && date>28){//平年2月
return false;
}
if(leapYear==true && mnotallow==2 && date>29){//闰年2月
return false;
}
}
return flag;
}
/**判断参数所指定的年份是否是闰年*/
private boolean leapYear(int year){
if(year%100!=0&&year%4==0 || year%400==0){
return true;
}else{
return false;
}
}
如果两个人出生在同一个行政区域,又是同一天出生的,那么他们的身份证号码的前14位都是相同的。为区分这两个人,派出所会根据他们登记户口的时间先后,给这两个人分配两个不同的号码,这个号码就是顺序码。顺序码位于身份证号码的第15和16两位,范围是从00一直到99。
顺序码之后的第17位就是性别代码。派出所会给男性分配一个奇数作为性别代码,而给女性分配一个偶数作为性别代码,因此性别代码的范围是0-9。
身份证号码的最后1位是根据前面的17位计算出来的。它的作用是为了防止有人胡编乱造一个身份证号码而专门增加的一个校验码。校验码的计算过程需要一个中间变量S,具体计算流程如下:
- S=第1位号码*第1位加权因子+第2位号码*第2位加权因子+…+第17位号码*第17位加权因子
- 第n位加权因子 =(218-n)%11
- 计算校验码数组下标i
- i = S%11
- 校验码数组{"1","0","X","9","8","7","6","5","4","3","2"}
- 根据i的值从校验码数组中得到校验码
从以上流程可以看出:校验码的验证过程非常复杂,所以需要编写一个checkLastNum()方法来实现。下面的【例16_22】展示了验证身份证号码的全过程,其中也包含checkLastNum()方法的实现。
【例16_22 验证身份证号码】
Exam16_22.java
import java.util.*;
import java.time.*;
public class Exam16_22 {
//加权因子
static int[] weight = {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2};
//校验码数组
static String[] checkDigit = {"1","0","X","9","8","7","6","5","4","3","2"};
/**检验身份证号码是否正确*/
static boolean checkIDCard(String IDCard){
boolean flag = false;
flag = IDCard.matches("^\\d{17}(\\d|[xX])$");
String part = "";
if(flag){
part = IDCard.substring(0,2);
flag = part.matches("1[1-5]|2[1-3]|3[1-7]|4[1-6]|5[0-4]|6[1-5]");
if(flag){
part = IDCard.substring(2,4);
flag = part.matches("0[1-9]|[1-9][0-9]");
if(flag){
part = IDCard.substring(4,6);
flag = part.matches("0[1-9]|[1-9][0-9]");
if(flag){
part = IDCard.substring(6,14);
flag = checkBirthday(part);
if(flag){
flag = checkLastNum(IDCard);
}
}
}
}
}
return flag;
}
/**测试参数所指定的出生日期是否合理*/
static boolean checkBirthday(String birthday){
boolean flag = true;
String part = "";//要进行判断的那一部分信息
part = birthday.substring(0,4);//出生年份
//判断出生于哪个世纪
int year = Integer.parseInt(part);//年份转成整型
LocalDate ld = LocalDate.now();//当前年份
flag = (year>=1880 && year<=ld.getYear());
if(flag){
part = birthday.substring(4,6);//月份
int month = Integer.parseInt(part);//月份转成整型
//判断月份的格式正确性
flag = (month>=1 && month<=12);
if(flag){
part = birthday.substring(6,8);//出生日期
int date = Integer.parseInt(part);//出生日期转成整型
if(date>31){//日期大于31一律算错
return false;
}
if((month==4||month==6||month==9||month==11) && date>30){//处理小月
return false;
}
boolean leapYear = leapYear(year);
if(leapYear==false && month==2 && date>28){//平年2月
return false;
}
if(leapYear==true && month==2 && date>29){//闰年2月
return false;
}
}
}
return flag;
}
/**判断参数所指定的年份是否是闰年*/
static boolean leapYear(int year){
if(year%100!=0&&year%4==0 || year%400==0){
return true;
}else{
return false;
}
}
/**检验最后1位号码的正确性*/
static boolean checkLastNum(String IDCard){
String lastNum = IDCard.substring(17);//截取身份证号的最后一位
//用截取出的最后一位与计算出的最后一位比较
boolean flag = lastNum.equalsIgnoreCase(getLastNum(IDCard));
return flag;
}
/**根据前17位计算最后1位*/
static String getLastNum(String IDCard){
String lastNum = "";
int sum = 0;
for(int i=0;i<17;i++){
String strNum = IDCard.substring(i,i+1);
int num = Integer.parseInt(strNum);
sum = sum +(num*weight[i]);
}
lastNum = checkDigit[sum%11];
return lastNum;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入身份证号码:");
String IDCard = sc.nextLine();
boolean flag = checkIDCard(IDCard);
System.out.println("身份证号码格式是否正确:"+flag);
}
}
【例16_22】的运行结果如图16-22所示。
图16-22【例16_22】运行结果
需要说明:本例中出生最早的出生年定义为1880年,程序认为早于1880年的生日是错误的。此外,一个格式正确的身份证号码并不一定真实存在。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。