25.4 剖析常见表达式
25.4 剖析常见表达式
本节来讨论和分析一些常用的正则表达式,具体包括:
- 邮编。
- 电话号码,包括手机号码和固定电话号码。
- 日期和时间。
- 身份证号。
- IP地址。
- URL。
- Email地址。
- 中文字符。
对于同一个目的,正则表达式往往有多种写法,大多没有唯一正确的写法,本节的写法主要是示例。此外,写一个正则表达式,匹配希望匹配的内容往往比较容易,但让它不匹配不希望匹配的内容则往往比较困难,也就是说,保证精确性经常是很难的,不过,很多时候,也没有必要写完全精确的表达式,需要写到多精确与需要处理的文本和需求有关。另外,正则表达式难以表达的,可以通过写程序进一步处理。这么描述可能比较抽象,下面,我们会具体讨论分析。
1.邮编
邮编比较简单,就是6位数字,所以表达式可以为:
1 | [0-9]{6} |
这个表达式可以用于验证输入是否为邮编,比如:
1 | public static Pattern ZIP_CODE_PATTERN = Pattern.compile("[0-9]{6}"); |
但如果用于查找,这个表达式是不够的,看个例子:
1 | public static void findZipCode(String text) { |
文本中只有一个邮编,但输出却为:
1 | 100013 |
这怎么办呢?可以使用环视边界匹配,对于左边界,它前面的字符不能是数字,环视表达式为:
1 | (?<![0-9]) |
对于右边界,它右边的字符不能是数字,环视表达式为:
1 | (?![0-9]) |
所以,完整的表达式可以为:
1 | (?<![0-9])[0-9]{6}(?![0-9]) |
使用这个表达式,将ZIP_CODE_PATTERN改为:
1 | public static Pattern ZIP_CODE_PATTERN = Pattern.compile( |
就可以输出期望的结果了。6位数字就一定是邮编吗?答案当然是否定的,所以,这个表达式也不是精确的,如果需要更精确的验证,可以写程序进一步检查。
2.手机号码
中国的手机号码都是11位数字,所以,最简单的表达式就是:
1 | [0-9]{11} |
不过,目前手机号第1位都是1,第2位取值为3、4、5、7、8之一,所以更精确的表达式是:
1 | 1[34578][0-9]{9} |
为方便表达手机号,手机号中间经常有连字符(即减号’-‘),形如:
1 | 186-1234-5678 |
为表达这种可选的连字符,表达式可以改为:
1 | 1[34578][0-9]-? [0-9]{4}-? [0-9]{4} |
在手机号前面,可能还有0、+86或0086,和手机号码之间可能还有一个空格,比如:
1 | 018612345678 |
为表达这种形式,可以在号码前加如下表达式:
1 | ((0|\+86|0086)\s? )? |
和邮编类似,如果为了抽取,也要在左右加环视边界匹配,左右不能是数字。所以,完整的表达式为:
1 | (? <! [0-9])((0|\+86|0086)\s? )?1[34578][0-9]-? [0-9]{4}-? [0-9]{4}(? ! [0-9]) |
用Java表示的代码为:
1 | public static Pattern MOBILE_PHONE_PATTERN = Pattern.compile( |
3.固定电话号码
不考虑分机,中国的固定电话一般由两部分组成:区号和市内号码,区号是3到4位,市内号码是7到8位。区号以0开头,表达式可以为:
1 | 0[0-9]{2,3} |
市内号码表达式为:
1 | [0-9]{7,8} |
区号可能用括号包含,区号与市内号码之间可能有连字符,如以下形式:
1 | 010-62265678 |
整个区号是可选的,所以整个表达式为:
1 | (\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8} |
再加上左右边界环视,完整的Java表示为:
1 | public static Pattern FIXED_PHONE_PATTERN = Pattern.compile( |
4.日期
日期的表示方式有很多种,我们只看一种,形如:
1 | 2017-06-21 |
年月日之间用连字符分隔,月和日可能只有一位。最简单的正则表达式可以为:
1 | \d{4}-\d{1,2}-\d{1,2} |
年一般没有限制,但月只能取值1~12,日只能取值1~31,怎么表达这种限制呢?
对于月,有两种情况,1月到9月,表达式可以为:
1 | 0?[1-9] |
10月到12月,表达式可以为:
1 | 1[0-2] |
所以,月的表达式为:
1 | (0?[1-9]|1[0-2]) |
对于日,有三种情况:
- 1到9号,表达式为:
0?[1-9]
。 - 10号到29号,表达式为:
[1-2][0-9]
。 - 30号和31号,表达式为:
3[01]
。
所以,整个表达式为:
1 | \d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[01]) |
加上左右边界环视,完整的Java表示为:
1 | public static Pattern DATE_PATTERN = Pattern.compile( |
5.时间
考虑24小时制,只考虑小时和分钟,小时和分钟都用固定两位表示,格式如下:
1 | 10:57 |
基本表达式为:
1 | \d{2}:\d{2} |
小时取值范围为0~23,更精确的表达式为:
1 | ([0-1][0-9]|2[0-3]) |
分钟取值范围为0~59,更精确的表达式为:
1 | [0-5][0-9] |
所以,整个表达式为:
1 | ([0-1][0-9]|2[0-3]):[0-5][0-9] |
加上左右边界环视,完整的Java表示为:
1 | public static Pattern TIME_PATTERN = Pattern.compile( |
6.身份证号
身份证有一代和二代之分,一代身份证号是15位数字,二代身份证号是18位数字,都不能以0开头。对于二代身份证号,最后一位可能为x或X,其他是数字。一代身份证号表达式可以为:
1 | [1-9][0-9]{14} |
二代身份证号表达式可以为:
1 | [1-9][0-9]{16}[0-9xX] |
这两个表达式的前面部分是相同的,二代身份证号表达式多了如下内容:
1 | [0-9]{2}[0-9xX] |
所以,它们可以合并为一个表达式,即:
1 | [1-9][0-9]{14}([0-9]{2}[0-9xX])? |
加上左右边界环视,完整的Java表示为:
1 | public static Pattern ID_CARD_PATTERN = Pattern.compile( |
符合这个要求的就一定是身份证号吗?当然不是,身份证号还有一些更为具体的要求,本书就不探讨了。
7. IP地址
IP地址示例如下:
1 | 192.168.3.5 |
点号分隔,4段数字,每个数字范围是0~255。最简单的表达式为:
1 | (\d{1,3}\.){3}\d{1-3} |
\d{1,3}
太简单,没有满足0~255之间的约束,要满足这个约束,需要分多种情况考虑。
值是1位数,前面可能有0~2个0,表达式为:
1 | 0{0,2}[0-9] |
值是两位数,前面可能有一个0,表达式为:
1 | 0?[0-9]{2} |
值是三位数,又要分为多种情况。以1开头的,后两位没有限制,表达式为:
1 | 1[0-9]{2} |
以2开头的,如果第二位是0到4,则第三位没有限制,表达式为:
1 | 2[0-4][0-9] |
如果第二位是5,则第三位取值为0到5,表达式为:
1 | 25[0-5] |
所以,\d{1,3}更为精确的表示为:
1 | (0{0,2}[0-9]|0? [0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5]) |
所以,加上左右边界环视,IP地址的完整Java表示为:
1 | public static Pattern IP_PATTERN = Pattern.compile( |
8. URL
URL的格式比较复杂,其规范定义在https://tools.ietf.org/html/rfc1738,我们只考虑HTTP协议,其通用格式是:
1 | http://<host>:<port>/<path>? <searchpart> |
开始是http://
,接着是主机名,主机名之后是可选的端口,再之后是可选的路径,路径后是可选的查询字符串,以?开头。看一些例子:
1 | http://www.example.com |
主机名中的字符可以是字母、数字、减号和点号,所以表达式可以为:
1 | [-0-9a-zA-Z.]+ |
端口部分可以写为:
1 | (:\d+)? |
路径由多个子路径组成,每个子路径以/开头,后跟零个或多个非/的字符,简单地说,表达式可以为:
1 | (/[^/]*)* |
更精确地说,把所有允许的字符列出来,表达式为:
1 | (/[-\w$.+! *'(), %; :@&=]*)* |
对于查询字符串,简单地说,由非空字符串组成,表达式为:
1 | \?[\S]* |
更精确的,把所有允许的字符列出来,表达式为:
1 | \?[-\w$.+!*'(),%;:@&=]* |
路径和查询字符串是可选的,且查询字符串只有在至少存在一个路径的情况下才能出现,其模式为:
1 | (/<sub_path>(/<sub_path>)*(\?<search>)?)? |
所以,路径和查询部分的简单表达式为:
1 | (/[^/]*(/[^/]*)*(\? [\S]*)?)? |
精确表达式为:
1 | (/[-\w$.+!*'(),%;:@&=]*(/[-\w$.+!*'(),%;:@&=]*)*(\?[-\w$.+!*'(),%;:@&=]*)?)? |
HTTP的完整Java表达式为:
1 | public static Pattern HTTP_PATTERN = Pattern.compile( |
9. Email地址
完整的Email规范比较复杂,定义在https://tools.ietf.org/html/rfc822,我们先看一些实际中常用的。比如新浪邮箱:
1 | abc@sina.com |
对于用户名部分,它的要求是:4~16个字符,可使用英文小写、数字、下画线,但下画线不能在首尾。怎么验证用户名呢?可以为:
1 | [a-z0-9][a-z0-9_]{2,14}[a-z0-9] |
新浪邮箱的完整Java表达式为:
1 | public static Pattern SINA_EMAIL_PATTERN = Pattern.compile( |
我们再来看QQ邮箱,它对于用户名的要求为:
1)3~18个字符,可使用英文、数字、减号、点或下画线;
2)必须以英文字母开头,必须以英文字母或数字结尾;
3)点、减号、下画线不能连续出现两次或两次以上。
如果只有第1条,可以为:
1 | [-0-9a-zA-Z._]{3,18} |
为满足第2条,可以改为:
1 | [a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9] |
怎么满足第3条呢?可以使用边界环视,左边加如下表达式:
1 | (?![-0-9a-zA-Z._]*(--|\.\.|__)) |
完整表达式可以为:
1 | (?![-0-9a-zA-Z._]*(--|\.\.|__))[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9] |
QQ邮箱的完整Java表达式为:
1 | public static Pattern QQ_EMAIL_PATTERN = Pattern.compile( |
以上都是特定邮箱服务商的要求,一般的邮箱是什么规则呢?一般而言,以@作为分隔符,前面是用户名,后面是域名。用户名的一般规则是:
- 由英文字母、数字、下画线、减号、点号组成;
- 至少1位,不超过64位;
- 开头不能是减号、点号和下画线。
比如:
1 | h_llo-abc.good@example.com |
这个表达式可以为:
1 | [0-9a-zA-Z][-._0-9a-zA-Z]{0,63} |
域名部分以点号分隔为多个部分,至少有两个部分。最后一部分是顶级域名,由2~3个英文字母组成,表达式可以为:
1 | [a-zA-Z]{2,3} |
对于域名的其他点号分隔的部分,每个部分一般由字母、数字、减号组成,但减号不能在开头,长度不能超过63个字符,表达式可以为:
1 | [0-9a-zA-Z][-0-9a-zA-Z]{0,62} |
所以,域名部分的表达式为:
1 | ([0-9a-zA-Z][-0-9a-zA-Z]{0,62}\.)+[a-zA-Z]{2,3} |
完整的Java表示为:
1 | public static Pattern GENERAL_EMAIL_PATTERN = Pattern.compile( |
10.中文字符
中文字符的Unicode编号一般位于\u4e00~\u9fff之间,所以匹配任意一个中文字符的表达式可以为:
1 | [\u4e00-\u9fff] |
Java表达式为:
1 | public static Pattern CHINESE_PATTERN = Pattern.compile( |
11.小结
本节详细讨论和分析了一些常见的正则表达式。在实际开发中,有些可以直接使用,有些需要根据具体文本和需求进行调整。完整的代码在Github上,地址为https://github.com/swiftma/program-logic ,位于包shuo.laoma.regex.c90下。
至此,关于正则表达式就介绍完了。下一章,我们探讨Java 8中的函数式编程。