0%

Java 正则表达式(四):剖析常见表达式

1. 日常开发中,常用的正则表达式有哪些

  • 邮编
  • 电话号码,包括手机号码和固定电话号码
  • 日期和时间
  • 身份证号
  • IP 地址
  • URL
  • Email 地址
  • 中文字符

2. 写正则表达式时的细节有

  • 对于同一个目的,正则表达式往往有多种写法,大多没有唯一正确的写法
  • 写一个正则表达式,匹配希望匹配的内容往往比较容易,但让它不匹配不希望匹配的内容则往往比较困难。即,保证精确性经常是很难的
  • 很多时候,也没有必要写完全精确的表达式,需要写到多精确与需要处理的文本和需求有关
  • 正则表达式难以表达的,可以通过写程序进一步处理

3. 写一个匹配邮编的正则表达式

  • 邮编是 6 位数字

    • [0-9]{6}:如果用于查找,这个表达式是不够的

      1
      2
      3
      4
      5
      6
      //验证输入是否为邮编
      public static Pattern ZIP_CODE_PATTERN = Pattern.compile("[0-9]{6}");

      public static boolean isZipCode(String text) {
      return ZIP_CODE_PATTERN.matcher(text).matches();
      }
    • (?<![0-9])[0-9]{6}(?![0-9]):环视边界匹配

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //用于查找
      public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
      "(?<![0-9])" //左边不能有数字
      + "[0-9]{6}"
      + "(?![]0-9)" //右边不能有数字
      );

      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");
      }
  • 6 位数字不一定是邮编,如果需要更精确的验证,可以写程序进一步检查

4. 写一个匹配手机号码的正则表达式

  • [0-9]{11}:11 数字,最简单的表达式

  • 1[34578][0-9]{9}:第 1 位是 1,第 2 位取值 34578 之一

  • 1[34578][0-9]-?[0-9]{4}-?[0-9]{4}:带有两个连字符的表示形式

  • (?<![0-9])((0|\+86|0086)\s?)?1[34578][0-9]-?[0-9]{4}-?[0-9]{4}(?![0-9]):在手机号前面,可能还有 0、+86 或 0086,和手机号码之间可能还有一个空格;和邮编类似,如果为了抽取,也要在左右加环视边界匹配,左右不能是数字。用 Java 表示的代码为

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

5. 写一个匹配固定电话号码的正则表达式

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

    • 区号:0[0-9]{2,3}

    • 市号:[0-9]{7,8}

    • (\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8}:区号可能用括号包含,区号与市号之间可能有连字符,整个区号是可选的

    • 再加上左右边界环视,完整的 Java 表达式为

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

6. 写一个表示日期的正则表达式(形如 2016-11-21)

  • 年月日之间用连字符分隔,月和日可能只有一位。年一般没有限制,但月只能取值 112,日只能取值 131

  • 对于月,有两种情况: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[0-1]

  • 所以,整个表达式为:\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[0-1])

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

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

7. 写一个表示时间的正则表达式

  • 考虑 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 表示为

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

8. 写一个表示身份证号的正则表达式

  • 身份证有一代和二代之分,一代身份证号是 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 表示为

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

9. 写一个表示 IP 地址的正则表达式

  • IP 地址示例如下:192.168.3.5。点号分隔,4 段数字,每个数字范围是 0~255。最简单的表达式为:(\d{1,3}\.){3}\d{1-3}

  • \d{1,3} 太简单,没有满足 0~255 之间的约束,要满足这个约束,需要分多种情况考虑

  • 值是一位数,前面可能有 0~2 个 0,表达式为:0{0,2}[0-9]

  • 值是两位数,前面可能有一个 0,表达式为:0?[0-9]{2}

  • 值是三位数,又要分为多种情况

    • 以 1 开头的,后两位没有限制,表达式为:1[0-9]{2}
    • 以 2 开头的,如果第二位是 0 到 4,则第三位没有限制,表达式为:2[0-4][0-9]
    • 如果第二位是 5,则第三位取值为 0 到 5,表达式为:25[0-5]
  • 所以,\d{1,3} 更为精确的表示为:(0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}[0-4][0-9]|25[0-5])

  • 加上左右边界环视,IP 地址的完整 Java 表示为

    1
    2
    3
    4
    5
    6
    public static Pattern IP_PATTERN = Pattern.compile(
    "(?<![0-9])" //左边不能有数字
    + "((0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}"
    + "(0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-25])"
    + "(?![0-9])" //右边不能有数字
    );
  • (以上答案待商榷)

10. 写一个表示 URL 的正则表达式

  • URL 的格式比较复杂,其规范定义在 https://tools.ietf.org/html/rfc1738。这里只考虑 HTTP 协议,其通用格式是:http://:/?

  • 开始是 http://,接着是主机名,主机名之后是可选的端口,再之后是可选的路径,路径后是可选的查询字符串,以 ? 开头。比如,http://www.example.com:8080/ab/c/def?q1=abc&q2=def

  • 主机名中的字符可以是字母、数字、减号和点号,所以表达式可以为:[-0-9a-zA-Z.]+

  • 端口部分可以写为:(:\d+)?

  • 路径由多个子路径组成,每个子路径以 / 开头,后跟零个或多个非 / 的字符。简单地说,表达式可以为:(/[^/]*)*;更精确地说,把所有允许的字符列出来,表达式为:(/[-\w$.+!*'(),%;:@&=]*)*

  • 对于查询字符,简单地说,由非空字符串组成,表达式为:\?[\S]*;更精确地说,把所有允许的字符列出来,表达式为:\?[-\w$.+!*'(),%;:@&=]*

  • 路径和查询字符串是可选的,且查询字符串只有在至少存在一个路径的情况下才能出现,其模式为:(/<sub_path>(/<sub_path>)*(\?<search>)?)?。所以,路径和查询部分的简单表达式为:(/[^/]*(/[^/]*)*(\?[\S]*)?)?;精确表达式为:(/[-\s$.+!*'(),%;:@&=]*(/[-\w$.+!*'(),%;:@&=]*)*(\?[-\w$.+!*'(),%;:@&=]*)?)?

  • HTTP 正则表达式的完整 Java 表达式为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static Pattern HTTP_PATTERN = Pattern.compile(
    "http://" + "[-0-1a-zA-Z.]+" //主机名
    + "(:\\d+)?" //端口
    + "(" //可选的路径和查询 - 开始
    + "/[-\\w$.+!*'(),%;:@&=]*" //第一层路径
    + "(/[-\\w$.+!*'(),%;:@&=]*)*" //可选的其他层路径
    + "(\\?[-\\w$.+!*'(),%;:@&=]*)?" //可选的查询字符串
    + ")?" //可选的路径和查询 - 结束
    );

11. 写一个 Email 地址的正则表达式

  • 完整的 Email 规范比较复杂,定义在 https://tools.ietf.org/html/rfc822

  • 新浪邮箱

    • 举例:abc@sina.com

    • 对于用户名部分,它的要求是 4~16 个字符,可使用英文小写、数字、下划线,但下划线不能在首尾

    • 验证用户名,可以为:[a-z0-9][a-z0-9_]{2,14}[a-z0-9]

    • 完整的 Java 表达式为

      1
      2
      3
      4
      5
      public static Pattern SINA_EMAIL-PATTERN = Pattern.compile(
      "[a-z0-9]"
      + "[a-z0-9_]{2,14}"
      + "[a-z0-9]@sina\\.com"
      );
  • QQ 邮箱

    • 3~18 个字符,可使用英文、数字、减号、点号或下划线

    • 必须以英文字母开头,必须以英文字母或数字结尾

    • 点号、减号、下划线不能连续出现两次或两次以上

    • 如果只有第 1 条,可以为:[-0-9a-zA-Z._]{3,18}

    • 为满足第 2 条,可以改为:[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]

    • 使用边界环视满足第 3 条,左边加如下表达式:(?![-0-9a-zA-Z.]*(–|..|_))

    • 完整表达式可以为:(?![-0-9a-zA-Z.]*(–|..|_))[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]

    • 完整的 Java 表达式为

      1
      2
      3
      4
      5
      6
      7
      public static Pattern QQ_EMAIL_PATTERN = Pattern.compile(
      //点号、减号、下划线不能连续出现两次或两次以上
      "(?![-0-9a-zA-Z._]*(--|\\.\\.|__))"
      + "[a-zA-Z]" //必须以英文字母开头
      + "[-0-9a-zA-Z._]{1,16}" //3~18位英文、数字、减号、点号、下划线组成
      + "[a-zA-Z0-9]@qq\\.com" //由英文字母、数字结尾
      );
  • 一般的邮箱的规则是:以 @ 作为分隔符,前面是用户名,后面是域名

    • 用户名的一般规则是

      • 由英文字母、数字、下划线、减号、点号组成
      • 至少 1 位,不超过 64 位
      • 开头不能是减号、点号和下划线
      • 比如,h_llo-abc.good@example.com,表达式可以为:[0-9a-zA-Z][-._0-9a-zA-Z]{0,63}
    • 域名部分以点号分隔为多个部分,至少有两个部分

      • 最后一部分是顶级域名,由 2~3 个英文字母组成,表达式可以为:[a-zA-Z]{2,3}

      • 对于域名的其他点号分隔的部分,每个部分一般由字母、数字、减号组成,但减号不能在开头,长度不能超过 63 个字符,表达式可以为:[0-9a-zA-Z][-0-9a-zA-Z]{0,62}.)+[a-zA-Z]{2,3}

      • 完整的 Java 表示为

        1
        2
        3
        4
        5
        6
        public static Pattern GENERAL_EMAIL_PATTERN = Pattern.compile(
        "[0-9a-zA-Z][-._0-9a-zA-Z]{0,63}" //用户名
        + "@"
        + "([0-9a-zA-Z][-0-9a-zA-Z]{0,62}\\.)+" //域名部分
        + "[a-zA-Z]{2,3}" //顶级域名
        );

12. 写一个匹配中文字符的正则表达式

  • 中文字符的 Unicode 编号一般位于 \u4e00~\u9fff 之间,所以匹配任意一个中文字符的表达式可以为:[\u4e00-\u9fff]
  • Java 表达式为:public static Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fff]");
-------------------- 本文结束感谢您的阅读 --------------------