专栏名称: 老马说编程
从入门到高级, 深入浅出, 老马和你一起探索编程及计算机技术的本质, 篇篇原创, 用心写作。
目录
相关文章推荐
码农翻身  ·  漫画 | 世界上最厉害的编程语言终于出现了! ·  23 小时前  
OSC开源社区  ·  用智能体管住大模型的“嘴” ·  4 天前  
51好读  ›  专栏  ›  老马说编程

(90) 正则表达式 (下) / 计算机程序的思维逻辑

老马说编程  · 公众号  · 程序员  · 2017-06-08 21:56

正文

88节 介绍了正则表达式的语法, 上节 介绍了正则表达式相关的Java API,本节来讨论和分析一些常用的正则表达式,具体包括:

  • 邮编

  • 电话号码,包括手机号码和固定电话号码

  • 日期和时间

  • 身份证

  • IP地址

  • URL

  • Email地址

  • 中文字符


对于同一个目的,正则表达式往往有多种写法,大多没有唯一正确的写法,本节的写法主要是示例。此外,写一个正则表达式,匹配希望匹配的内容往往比较容易,但让它不匹配不希望匹配的内容,则往往比较困难,也就是说,保证精确性经常是很难的,不过,很多时候,我们也没有必要写完全精确的表达式,需要写到多精确与你需要处理的文本和需求有关,另外,正则表达式难以表达的,可以通过写程序进一步处理。这么描述可能比较抽象,下面,我们会具体讨论分析。

邮编
邮编比较简单,就是6位数字,首位不能是0,所以表达式可以为:

[1-9][0-9]{5}


这个表达式可以用于验证输入是否为邮编,比如:

public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
"[1-9][0-9]{5}");

public static boolean isZipCode(String text) {
return ZIP_CODE_PATTERN.matcher(text).matches();
}


但如果用于查找,这个表达式是不够的,看个例子:

public static void findZipCode(String text) {
Matcher matcher = ZIP_CODE_PATTERN.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group());
}
}

public static void main(String[] args) {
findZipCode("邮编 100013,电话18612345678");
}


文本中只有一个邮编,但输出却为:

100013
186123


这怎么办呢?可以使用 88节 介绍的环视边界匹配,对于左边界,它前面的字符不能是数字,环视表达式为:

(?


对于右边界,它右边的字符不能是数字,环视表达式为:

(?![0-9])


所以,完整的表达式可以为:

(?


使用这个表达式,也就是说,将ZIP_CODE_PATTERN改为:

public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
"(?        + "[1-9][0-9]{5}"
+ "(?![0-9])"); // 右边不能有数字


就可以输出期望的结果了。

非0开头的6位数字就一定是邮编吗?答案当然是否定的,所以,这个表达式也不是精确的,如果需要更精确的验证,可以写程序进一步检查。

手机号码

中国的手机号码都是11位数字,所以,最简单的表达式就是:

[0-9]{11}


不过,目前手机号第1位都是1,第2位取值为3、4、5、7、8之一,所以,更精确的表达式是:

1[3|4|5|7|8|][0-9]{9}


为方便表达手机号,手机号中间经常有连字符(即减号'-'),形如:

186-1234-5678


为表达这种可选的连字符,表达式可以改为:

1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}


在手机号前面,可能还有0、+86或0086,和手机号码之间可能还有一个空格,比如:

018612345678
+86 18612345678
0086 18612345678


为表达这种形式,可以在号码前加如下表达式:

((0|\+86|0086)\s?)?


和邮编类似,如果为了抽取,也要在左右加环视边界匹配,左右不能是数字。所以,完整的表达式为:

(?


用Java表示的代码为:

public static Pattern MOBILE_PHONE_PATTERN = Pattern.compile(
"(?        + "((0|\\+86|0086)\\s?)?" // 0 +86 0086
+ "1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}" // 186-1234-5678
+ "(?![0-9])"); // 右边不能有数字


固定电话
不考虑分机,中国的固定电话一般由两部分组成:区号和市内号码,区号是3到4位,市内号码是7到8位。区号以0开头,表达式可以为:

0[0-9]{2,3}


市内号码表达式为:

[0-9]{7,8}


区号可能用括号包含,区号与市内号码之间可能有连字符,如以下形式:

010-62265678
(010)62265678


整个区号是可选的,所以整个表达式为:

(\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8}


再加上左右边界环视,完整的Java表示为:

public static Pattern FIXED_PHONE_PATTERN = Pattern.compile(
"(?        + "(\\(?0[0-9]{2,3}\\)?-?)?" // 区号
+ "[0-9]{7,8}"// 市内号码
+ "(?![0-9])"); // 右边不能有数字


日期
日期的表示方式有很多种,我们只看一种,形如:

2017-06-21
2016-11-1


年月日之间用连字符分隔,月和日可能只有一位。

最简单的正则表达式可以为:

\d{4}-\d{1,2}-\d{1,2}


年一般没有限制,但月只能取值1到12,日只能取值1到31,怎么表达这种限制呢?

对于月,有两种情况,1月到9月,表达式可以为:

0?[1-9]


10月到12月,表达式可以为:

1[0-2]


所以,月的表达式为:

(0?[1-9]|1[0-2])


对于日,有三种情况:

  • 1到9号,表达式为:0?[1-9]

  • 10号到29号,表达式为:[1-2][0-9]

  • 30号和31号,表达式为:3[01]


所以,整个表达式为:

\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[01])


加上左右边界环视,完整的Java表示为:

public static Pattern DATE_PATTERN = Pattern.compile(
"(?        + "\\d{4}-" // 年
+ "(0?[1-9]|1[0-2])-" // 月
+ "(0?[1-9]|[1-2][0-9]|3[01])"// 日
+ "(?![0-9])"); // 右边不能有数字


时间
考虑24小时制,只考虑小时和分钟,小时和分钟都用固定两位表示,格式如下:

10:57


基本表达式为:

\d{2}:\d{2}


小时取值范围为0到23,更精确的表达式为:

([0-1][0-9]|2[0-3])


分钟取值范围为0到59,更精确的表达式为:

[0-5][0-9]


所以,整个表达式为:

([0-1][0-9]|2[0-3]):[0-5][0-9]


加上左右边界环视,完整的Java表示为:

public static Pattern TIME_PATTERN = Pattern.compile(
"(?        + "([0-1][0-9]|2[0-3])" // 小时
+ ":" + "[0-5][0-9]"// 分钟
+ "(?![0-9])"); // 右边不能有数字


身份证
身份证有一代和二代之分,一代是15位数字,二代是18位,都不能以0开头,对于二代身份证,最后一位可能为x或X,其他是数字。

一代身份证表达式可以为:

[1-9][0-9]{14}


二代身份证可以为:

[1-9][0-9]{16}[0-9xX]


这两个表达式的前面部分是相同的,二代身份证多了如下内容:
[0-9]{2}[0-9xX]

所以,它们可以合并为一个表达式,即:

[1-9][0-9]{14}([0-9]{2}[0-9xX])?


加上左右边界环视,完整的Java表示为:

public static Pattern ID_CARD_PATTERN = Pattern.compile(
"(?        + "[1-9][0-9]{14}" // 一代身份证
+ "([0-9]{2}[0-9xX])?" // 二代身份证多出的部分
+ "(?![0-9])"); // 右边不能有数字







请到「今天看啥」查看全文