計算機程序的思維邏輯 (90) - 正則表達式 (下 - 剖析常見表達式)

介紹了正則表達式的語法,介紹了正則表達式相關的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

     

    這怎么辦呢?可以使用介紹的環視邊界匹配,對于左邊界,它前面的字符不能是數字,環視表達式為:

    (?<![0-9])

     

    對于右邊界,它右邊的字符不能是數字,環視表達式為:

    (?![0-9])

     

    所以,完整的表達式可以為:

    (?<![0-9])[1-9][0-9]{5}(?![0-9])

     

    使用這個表達式,也就是說,將ZIP_CODE_PATTERN改為:

    public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
            "(?<![0-9])" // 左邊不能有數字
            + "[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?)?

     

    和郵編類似,如果為了抽取,也要在左右加環視邊界匹配,左右不能是數字。所以,完整的表達式為:

    (?<![0-9])((0|\+86|0086)\s?)?1[3|4|5|7|8|][0-9]-?[0-9]{4}-?[0-9]{4}(?![0-9])

     

    用Java表示的代碼為:

    public static Pattern MOBILE_PHONE_PATTERN = Pattern.compile(
            "(?<![0-9])" // 左邊不能有數字
            + "((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-9])" // 左邊不能有數字
            + "(\\(?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(
            "(?<![0-9])" // 左邊不能有數字
            + "\\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-9])" // 左邊不能有數字
            + "([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(
            "(?<![0-9])" // 左邊不能有數字
            + "[1-9][0-9]{14}" // 一代身份證
            + "([0-9]{2}[0-9xX])?" // 二代身份證多出的部分
            + "(?![0-9])"); // 右邊不能有數字

     

    符合這個要求的就一定是身份證號碼嗎?當然不是,身份證還有一些更為具體的要求,本文就不探討了。

    IP地址

    IP地址格式如下:

    192.168.3.5

     

    點號分隔,4段數字,每個數字范圍是0到255。最簡單的表達式為:

    (\d{1,3}\.){3}\d{1-3}

     

     \d{1,3}太簡單,沒有滿足0到255之間的約束,要滿足這個約束,就要分多種情況考慮。

    值是1位數,前面可能有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}|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-5])"
            + "(?![0-9])"); // 右邊不能有數字

     

    URL

    URL的格式比較復雜,其規范定義在https://tools.ietf.org/html/rfc1738,我們只考慮http協議,其通用格式是:

    //<host>:<port>/<path>?<searchpart>

     

    開始是//,接著是主機名,主機名之后是可選的端口,再之后是可選的路徑,路徑后是可選的查詢字符串,以?開頭。

    一些例子:

    //www.example.com
    //www.example.com/ab/c/def.html
    //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]*)?)?

     

    精確表達式為:

    (/[-\w$.+!*'(),%;:@&=]*(/[-\w$.+!*'(),%;:@&=]*)*(\?[-\w$.+!*'(),%;:@&=]*)?)?

     

    HTTP的完整Java表達式為:

    public static Pattern HTTP_PATTERN = Pattern.compile(
            "//" + "[-0-9a-zA-Z.]+" // 主機名
            + "(:\\d+)?" // 端口
            + "(" // 可選的路徑和查詢 - 開始
                + "/[-\\w$.+!*'(),%;:@&=]*" // 第一層路徑
                + "(/[-\\w$.+!*'(),%;:@&=]*)*" // 可選的其他層路徑
                + "(\\?[-\\w$.+!*'(),%;:@&=]*)?" // 可選的查詢字符串
            + ")?"); // 可選的路徑和查詢 - 結束

     

    Email地址

    完整的Email規范比較復雜,定義在https://tools.ietf.org/html/rfc822,我們先看一些實際中常用的。

    比如新浪郵箱,它的格式如:

    [email protected]

     

    對于用戶名部分,它的要求是: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字符,可使用英文、數字、減號、點或下劃線

  • 必須以英文字母開頭,必須以英文字母或數字結尾

  • 點、減號、下劃線不能連續出現兩次或兩次以上

  • 如果只有第一條,可以為:

    [-0-9a-zA-Z._]{3,18}

     

    為滿足第二條,可以改為:

    [a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]

     

    怎么滿足第三條呢?可以使用邊界環視,左邊加如下表達式:

    (?![-0-9a-zA-Z._]*(--|\.\.|__))

     

    完整表達式可以為:

    (?![-0-9a-zA-Z._]*(--|\.\.|__))[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]

     

    QQ郵箱的完整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位

  • 開頭不能是減號、點號和下劃線

  • 比如:

    [email protected]

     

    這個表達式可以為:

    [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}

     

    所以,域名部分的表達式為:

    ([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}"); // 頂級域名

     

    中文字符

    中文字符的Unicode編號一般位于\u4e00和\u9fff之間,所以匹配任意一個中文字符的表達式可以為:

    [\u4e00-\u9fff]

     

    Java表達式為:

    public static Pattern CHINESE_PATTERN = Pattern.compile(
            "[\\u4e00-\\u9fff]");

     

    小結

    本節詳細討論和分析了一些常見的正則表達式,在實際開發中,有些可以直接使用,有些需要根據具體文本和需求進行調整。

    至此,關于正則表達式,我們就介紹完了,相信你對正則表達式一定有了一個更為清晰透徹的理解!

    在之前的章節中,我們都是基于Java 7討論的,從下節開始,我們探討Java 8的一些特性,尤其是函數式編程。

    (與其他章節一樣,本節所有代碼位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.regex.c90下)

    ----------------

    未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。

    來源:itnose

上一篇: 求教如何用for循環實例一堆對象

下一篇: spring MVC如何獲取session傳值到前臺

分享到: 更多
抢庄牛牛棋牌 黑马计划软件pk拾 吉林时时票助手 时时彩后二和尾稳赚 神奇的七星彩万能码 鱼丸游戏 奔驰宝马 足球比分新浪 北京pk十开奖历史记录 投注单打印软件 吉子棋牌龙虎怎么赢 百人炸金花手机版下载 pk10群计划群 组六组三全包技巧 报数21的游戏规则 pk10高手群计划 飞艇一期计划软件 app