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

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

答:

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

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

答:

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

3. 【笔试题】手写一个匹配邮编的正则表达式?

答:

  • 邮编是 6 位数字
  • [0-9]{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]):环视边界匹配:

    //用于查找
     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 位取值 3``4``5``7``8 之一。
  • 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``+860086,和手机号码之间可能还有一个空格;和邮编类似,如果为了抽取,也要在左右加环视边界匹配,左右不能是数字。用 Java 表示的代码为:

    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 开头,34 位;市号是 78

  • 区号:0[0-9]{2,3}
  • 市号:[0-9]{7,8}
  • (\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8}:区号可能用括号包含,区号与市号之间可能有连字符,整个区号是可选的。
  • 再加上左右边界环视,完整的 Java 表达式为:

    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)?

答:

  • 年月日之间用连字符分隔,月和日可能只有一位。年一般没有限制,但月只能取值 1~12,日只能取值 1~31
  • 对于月,有两种情况1 月到 9 月,表达式可以为:0?[1-9]10 月到 12 月,表达式可以为:1[0-2]。所以,月的表达式为:(0?[1-9]|1[0-2])
  • 对于日,有三种情况19 号,表达式为: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 表示为:

    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 表示为:

    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 开头。对于二代身份证,最后一位可能为 xX,其他是数字(符合这个要求的不一定就是身份证号,身份证号还有一些更为具体的要求)。
  • 一代身份证号表达式可以为:[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(
        "(?<![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 之间的约束,要满足这个约束,需要分多种情况考虑。
  • 值是 1 位数,前面可能有 0~20,表达式为:0{0,2}[0-9]
  • 值是两位数,前面可能有一个 0,表达式为:0?[0-9]{2}
  • 值是三位数,又要分为多种情况:
    • 1 开头的,后两位没有限制,表达式为:1[0-9]{2}
    • 2 开头的,如果第二位是 04,则第三位没有限制,表达式为:2[0-4][0-9]
    • 如果第二位是 5,则第三位取值为 05,表达式为: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 表示为

    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://<host>:<port>/<path>?<searchpart>
  • 开始是 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 表达式为

    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 表达式为:

      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 表达式为:

      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 表示为:

        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]");
-------------本文结束感谢您的阅读-------------