收起左侧

[其它] 常见正则表达式剖析

2
回复
[复制链接]
avatar
  • TA的每日心情
    qdsmile擦汗
    昨天 02:06
  • 签到天数: 2669 天

    [LV.Master]伴吧终老

    461

    主题

    1066

    帖子

    3万

    积分
    发表于 2017-6-24 18:04:46 | 显示全部楼层 |阅读模式
    常用的正则表达式,具体包括:
    • 邮编
    • 电话号码,包括手机号码和固定电话号码
    • 日期和时间
    • 身份证
    • IP地址
    • URL
    • Email地址
    • 中文字符

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

    邮编
    邮编比较简单,就是6位数字,首位不能是0,所以表达式可以为:
    1. [1-9][0-9]{5}
    复制代码
    这个表达式可以用于验证输入是否为邮编,比如:
    1. public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
    2.         "[1-9][0-9]{5}");

    3. public static boolean isZipCode(String text) {
    4.     return ZIP_CODE_PATTERN.matcher(text).matches();
    5. }
    复制代码
    但如果用于查找,这个表达式是不够的,看个例子:
    1. public static void findZipCode(String text) {
    2.     Matcher matcher = ZIP_CODE_PATTERN.matcher(text);
    3.     while (matcher.find()) {
    4.         System.out.println(matcher.group());
    5.     }
    6. }

    7. public static void main(String[] args) {
    8.     findZipCode("邮编 100013,电话18612345678");
    9. }
    复制代码
    文本中只有一个邮编,但输出却为:
    1. 100013
    2. 186123
    复制代码
    这怎么办呢?可以使用环视边界匹配,对于左边界,它前面的字符不能是数字,环视表达式为:
    1. (?<![0-9])
    复制代码
    对于右边界,它右边的字符不能是数字,环视表达式为:
    所以,完整的表达式可以为:
    1. (?<![0-9])[1-9][0-9]{5}(?![0-9])
    复制代码
    使用这个表达式,也就是说,将ZIP_CODE_PATTERN改为:
    1. public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
    2.         "(?<![0-9])" // 左边不能有数字
    3.         + "[1-9][0-9]{5}"
    4.         + "(?![0-9])"); // 右边不能有数字
    复制代码
    就可以输出期望的结果了。


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

    手机号码

    中国的手机号码都是11位数字,所以,最简单的表达式就是:
    1. [0-9]{11}
    复制代码
    不过,目前手机号第1位都是1,第2位取值为3、4、5、7、8之一,所以,更精确的表达式是:
    1. 1[3|4|5|7|8|][0-9]{9}
    复制代码
    为方便表达手机号,手机号中间经常有连字符(即减号'-'),形如:
    1. 186-1234-5678
    复制代码
    为表达这种可选的连字符,表达式可以改为:
    1. 1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}
    复制代码
    在手机号前面,可能还有0、+86或0086,和手机号码之间可能还有一个空格,比如:
    1. 018612345678
    2. +86 18612345678
    3. 0086 18612345678
    复制代码
    为表达这种形式,可以在号码前加如下表达式:
    1. ((0|\+86|0086)\s?)?
    复制代码
    和邮编类似,如果为了抽取,也要在左右加环视边界匹配,左右不能是数字。所以,完整的表达式为:
    1. (?<![0-9])((0|\+86|0086)\s?)?1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}(?![0-9])
    复制代码
    用Java表示的代码为:
    1. public static Pattern MOBILE_PHONE_PATTERN = Pattern.compile(
    2.      "(?<![0-9])" // 左边不能有数字
    3.      + "((0|\\+86|0086)\\s?)?" // 0 +86 0086
    4.      + "1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}" // 186-1234-5678
    5.      + "(?![0-9])"); // 右边不能有数字
    复制代码

    固定电话

    不考虑分机,中国的固定电话一般由两部分组成:区号和市内号码,区号是3到4位,市内号码是7到8位。区号以0开头,表达式可以为:
    1. 0[0-9]{2,3}
    复制代码
    市内号码表达式为:
    1. [0-9]{7,8}
    复制代码
    区号可能用括号包含,区号与市内号码之间可能有连字符,如以下形式:
    1. 010-62265678
    2. (010)62265678
    复制代码
    整个区号是可选的,所以整个表达式为:
    1. (\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8}
    复制代码
    再加上左右边界环视,完整的Java表示为:
    1. public static Pattern FIXED_PHONE_PATTERN = Pattern.compile(
    2.      "(?<![0-9])" // 左边不能有数字
    3.      + "(\\(?0[0-9]{2,3}\\)?-?)?" // 区号
    4.      + "[0-9]{7,8}"// 市内号码
    5.      + "(?![0-9])"); // 右边不能有数字
    复制代码

    日期

    日期的表示方式有很多种,我们只看一种,形如:
    1. 2017-06-21
    2. 2016-11-1
    复制代码
    年月日之间用连字符分隔,月和日可能只有一位。

    最简单的正则表达式可以为:
    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(
    2.      "(?<![0-9])" // 左边不能有数字
    3.      + "\\d{4}-" // 年
    4.      + "(0?[1-9]|1[0-2])-" // 月
    5.      + "(0?[1-9]|[1-2][0-9]|3[01])"// 日
    6.      + "(?![0-9])"); // 右边不能有数字
    复制代码

    时间

    考虑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(
    2.      "(?<![0-9])" // 左边不能有数字
    3.      + "([0-1][0-9]|2[0-3])" // 小时
    4.      + ":" + "[0-5][0-9]"// 分钟
    5.      + "(?![0-9])"); // 右边不能有数字
    复制代码

    身份证

    身份证有一代和二代之分,一代是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(
    2.     "(?<![0-9])" // 左边不能有数字
    3.     + "[1-9][0-9]{14}" // 一代身份证
    4.     + "([0-9]{2}[0-9xX])?" // 二代身份证多出的部分
    5.     + "(?![0-9])"); // 右边不能有数字
    复制代码
    符合这个要求的就一定是身份证号码吗?当然不是,身份证还有一些更为具体的要求,本文就不探讨了。

    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(
    2.      "(?<![0-9])" // 左边不能有数字
    3.      + "((0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}"
    4.      + "(0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])"
    5.      + "(?![0-9])"); // 右边不能有数字     
    复制代码


    URL


    URL的格式比较复杂,其规范定义在https://tools.ietf.org/html/rfc1738,我们只考虑http协议,其通用格式是:
    1. http://<host>:<port>/<path>?<searchpart>
    复制代码
    开始是http://,接着是主机名,主机名之后是可选的端口,再之后是可选的路径,路径后是可选的查询字符串,以?开头。

    一些例子:

    1. http://www.example.com
    2. http://www.example.com/ab/c/def.html
    3. http://www.example.com:8080/ab/c/def?q1=abc&q2=def
    复制代码
    主机名中的字符可以是字母、数字、减号和点号,所以表达式可以为:
    1. [-0-9a-zA-Z.]+
    复制代码
    端口部分可以写为:
    1. (:\d+)?
    复制代码
    路径由多个子路径组成,每个子路径以/开头,后跟零个或多个非/的字符,简单的说,表达式可以为:
    1. (/[^/]*)*
    复制代码
    更精确的说,把所有允许的字符列出来,表达式为:
    1. (/[-\w$.+!*'(),%;:@&=]<font color="#000000">*)*</font>
    复制代码
    对于查询字符串,简单的说,由非空字符串组成,表达式为:
    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(
    2.     "http://" + "[-0-9a-zA-Z.]+" // 主机名
    3.     + "(:\\d+)?" // 端口
    4.     + "(" // 可选的路径和查询 - 开始
    5.     + "/[-\\w$.+!*'(),%;:@&=]*" // 第一层路径
    6.     + "(/[-\\w$.+!*'(),%;:@&=]*)*" // 可选的其他层路径
    7.     + "(\\?[-\\w$.+!*'(),%;:@&=]*)?" // 可选的查询字符串
    8.     + ")?"); // 可选的路径和查询 - 结束
    复制代码


    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(
    2.      "[a-z0-9]"
    3.      + "[a-z0-9_]{2,14}"
    4.      + "[a-z0-9]@sina\\.com");  
    复制代码
    我们再来看QQ邮箱,它对于用户名的要求为:
    • 3-18字符,可使用英文、数字、减号、点或下划线
    • 必须以英文字母开头,必须以英文字母或数字结尾
    • 点、减号、下划线不能连续出现两次或两次以上

    如果只有第一条,可以为:

    1. [-0-9a-zA-Z._]{3,18}
    复制代码
    为满足第二条,可以改为:
    1. [a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]
    复制代码
    怎么满足第三条呢?可以使用边界环视,左边加如下表达式:
    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(
    2.     "(?![-0-9a-zA-Z._]*(--|\\.\\.|__))" // 点、减号、下划线不能连续出现两次或两次以上
    3.     + "[a-zA-Z]" // 必须以英文字母开头
    4.     + "[-0-9a-zA-Z._]{1,16}" // 3-18位 英文、数字、减号、点、下划线组成
    5.     + "[a-zA-Z0-9]@qq\\.com"); // 由英文字母、数字结尾      
    复制代码
    以上都是特定邮箱服务商的要求,一般的邮箱是什么规则呢?一般而言,以@作为分隔符,前面是用户名,后面是域名。

    用户名的一般规则是:
    • 由英文字母、数字、下划线、减号、点号组成
    • 至少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(
    2.     "[0-9a-zA-Z][-._0-9a-zA-Z]{0,63}" // 用户名
    3.     + "@"
    4.     + "([0-9a-zA-Z][-0-9a-zA-Z]{0,62}\\.)+" // 域名部分
    5.     + "[a-zA-Z]{2,3}"); // 顶级域名      
    复制代码

    中文字符

    中文字符的Unicode编号一般位于\u4e00和\u9fff之间,所以匹配任意一个中文字符的表达式可以为:
    1. [\u4e00-\u9fff]
    复制代码
    Java表达式为:
    1. public static Pattern CHINESE_PATTERN = Pattern.compile(
    2.     "[\\u4e00-\\u9fff]");
    复制代码


    小结
    本节详细讨论和分析了一些常见的正则表达式,在实际开发中,有些可以直接使用,有些需要根据具体文本和需求进行调整。
    学习心情好,签到少不了。
    avatar
  • TA的每日心情
    qdsmile奋斗
    2024-6-14 16:07
  • 签到天数: 206 天

    [LV.7]超级吧粉

    4

    主题

    3

    帖子

    494

    积分
    发表于 2018-8-9 14:50:38 | 显示全部楼层
    非常详细,学习了
    avatar
  • TA的每日心情
    qdsmile无聊
    2019-2-2 13:55
  • 签到天数: 1 天

    [LV.1]小吧新人

    0

    主题

    3

    帖子

    29

    积分

    发表于 2019-2-2 14:02:07 | 显示全部楼层
    签到少不了。
    您需要登录后才可以回帖 登录 | 立即注册 QQ登录

    本版积分规则