詳解Java API之正則表達式

     正則表達式描述的是一種規則,符合這種限定規則的字符串我們認爲它某種滿足條件的,是我們所需的。在正則表達式中,主要有兩種字符,一種描述的是普通的字符,另一種描述的是元字符。其中元字符是整個正則表達式的核心,並由它完成規則的制定工作。本篇文章主要從Java這門程序設計語言的角度理解正則表達式的應用,主要涉及以下內容:

  • 基本正則表達式的理論基礎
  • Java中用於正則表達式匹配的類
  • 幾種常用的正則表達式使用實例

一、正則表達式的理論基礎

     1、普通字符的表示
     我們說正則表達式主要由普通字符和元字符組成,那麼我們首先先看看普通字符該如何表示。大部分普通字符由字符本身即可表示,例如:’s’,’i’,’n’,’g’,’l’,’e’等。除此之外,也有一些特殊的表示方式。
     以/0開頭,後面緊跟1-3位數字,表示的是一個八進制數。這個數的十進制值對應於ASCII編碼中的相應字符。
     以/x或者/X開頭,後面緊跟兩位字符,表示的是一個十六進制的數。該數的十進制的值對應於ASCII編碼中相應的字符。
     以/u開頭,後面緊跟四位字符,表示一個Unicode編號。該編號對應於Unicode字符集中的一個具體字符。
     另外還有一些元字符,雖然它們具有特殊的含義,但是往往在某種特殊情況下,需要將這些元字符當做普通字符使用,我們使用 ‘/’+元字符,表示轉移該元字符,此後該元字符將表示一個普通字符。例如:’//’,’/^’,它們分別表示的是 ‘/’和 ‘^’,不再具有特殊含義了。下面我們開始逐漸介紹正則表達式語法中的元字符的特殊含義。

     2、字符組匹配單個字符
     我們用一對中括號([…..])表示字符組,整個字符組中會有多個字符位列其中,該字符組表示的含義是:匹配任意一個字符,該字符是位列字符組中的。例如:[single]匹配的是字符’s’,’i’,’n’,’g’,’l’,’e’中的任意一個字符。以上我們簡單介紹了字符組的基本概念以及它所能匹配的內容,其實有時候爲了表述連續的字符,我們會結合元字符 ‘-’ 一起來操作字符組。例如:[0123456789],匹配的是0到9之間的任意一個數字,對於這種情況我們可以選擇這樣來簡化操作:[0-9]。其實兩者表述的含義是一樣的,爲了簡化起見,如果遇到連續的字符表述,可以選擇使用元字符來簡化。同樣的還有[a-z],它匹配任意一個小寫字母。對於元字符 ‘-’ 還需要說明一點的是:該字符只有出現在兩個字符之間才具有特殊含義,單獨出現在字符組的所有字符之前或者之後只能表述普通字符 ‘-’ 。下面介紹有關字符組的一些其他相關的元字符。

元字符 ‘^’ 表示排除的意思,和元字符 ‘-’ 類似,只有放在所有字符的最前面才具有特殊含義,否則只能表示普通字符。例如:[^1234],該字符組匹配一個字符,但是不是1或2或3或4。當然,[c^yy],匹配的是四個普通字符,’c’,’^’,’y’,’y’。此外,需要注意一點的是,除了以上介紹的幾種元字符必須置放於指定位置上才能起作用以外,其餘所有元字符在字符組中統統被視作普通字符,不再具有特殊含義。

除此之外,字符組還支持嵌套使用。例如:[0-9[a-z]],該字符組匹配一個數字或者一個字母。我們也可以使用&&加強限定規則。例如:[0-9&&[^0123]],該字符組匹配的是0到9之間任意一個數字,但是該數字不能是0到3中任意一個,也就是隻能匹配4到9之間任意一個數字。最後和字符組有關的內容還是涉及一個預定義字符組,所謂預定義字符組就是對字符組的適當封裝,對於一些簡單的組合使用簡介的調用方式。例如:

  • \d:等同於字符組 [0-9],表示任意一個數字字符
  • \w:較爲常見,等同於字符組[0-9a-zA-Z],表示任意一個world(單詞字符)
  • \s:等同於[ \t\n\x0B\f\r],匹配的是一個空格字符(space)

當然,它們也有相對應的大寫形式,但是表示的意思卻是截然相反的。

  • \D:等同於[^0-9],表示一個任意非數字字符
  • \W:等同於[^0-9a-zA-Z],表示任意一個非單詞字符,往往會是一些特殊符號
  • \S:等同於[^\t\n\x0B\f\r],匹配一個任意非空格的字符

     3、用於指定字符多次出現的量詞
          所謂的量詞主要是三個元字符,它們主要用於指定量詞前面的字符在匹配時可以多次出現,具體區別接下來會介紹。首先我們需要知道,這三個元字符是:+ ,*, ?。下面描述它們各自作用及相互之間的區別:

  • +:該元字符指定位於元字符前面的普通字符可以出現一次或者多次。例如:se+cyy這個正則表達式,字符secyy,seeeecyy都是可以匹配的,但是scyy是不能匹配的,前面的字符是必須出現的。
  • *:該元字符指定位於元字符前面的普通字符可以出現零次或多次。例如:
se*cyy

     對於該正則表達式而言,secyy,seecyy都是可匹配的,並且scyy也是可以匹配的。這就是和元字符 + 的簡單區別。

  • ?:該元字符指定位於元字符前面的普通字符可以出現也可以不出現,但是不能多次出現。例如:se?cyy,對於該正則表達式,secyy,scyy等都是可匹配的,但是seeeecyy則是不能匹配的。它指定你前面的一個字符要麼出現,要麼不出現,不允許多次出現。

在這裏我們要申明一個誤區,這裏的三個元字符量詞作用的是緊鄰該元字符前面的一個字符,並不是作用與元字符前面所有的字符,這裏是需要注意的,包括筆者當初也都是誤以爲此的。

以上我們介紹了簡單量詞的概念,但是它們只能用於表示模糊的次數。可以出現多次,但是多次是多少卻沒有定論。對於要求字符出現精確次數的情況,我們可以使用通用量詞來解決。{m,n}是通用量詞的最基本形式,它指定前面的字符出現的次數在m到n之間。看幾個例子:

  • se{0,10}cyy:其中e可以出現0-10次
  • se{9}cyy:其中e必須出現9次
  • se{0,}cyy:其中e可以出現0-無窮大次,等同於se*cyy。

     4、分組劃分組別
          在介紹分組之前,無論是使用量詞還是字符組都是針對的一個字符。而分組針對的就是一串字符,我們也可以對分組使用量詞,控制該分組出現的次數。我們使用()括號表示分組,例如:

sing(le)de(cyy)

其中le和cyy分別是一個分組,對於一個完整的正則表達式,從頭開始,每個分組都是有編號的,按照出現的次序,以1爲基數遞增。至於爲什麼要有編號,下文說。對於分組我們依然是可以使用量詞控制其出現次數的,例如:

sing(le)+cccc:在該正則表達式中,分組le可以出現一次或者多次
sing(le)*cccc:在該正則表達式中,分組le可以出現零次或者多次

結合元字符 ‘| ‘,可以實現和字符組一樣的功效,例如:

(happy|cyy|single)

該正則表達式可以匹配三個字符子串,happy,cyy,single。但是這裏需要注意的是,元字符 | 如果用於字符組中就不再具有特殊含義,將會被作爲普通字符來匹配。(這一點其實在介紹字符組的時候已經強調過)

下面解決一個上文遺留問題,分組的編號到底有什麼作用。爲分組編號其實是爲了重新捕獲和使用分組,每個分組按照出現的次序從1開始遞增,我們使用 +分組編號進行引用。例如:

<(\w+)>(.*)</\1>:該正則表達式等效於:<(\w+)>(.*)</\w+>

(\w+)表示任意個字符(字母或數字),(.*)表示任意的符號,\1則引用了分組(\w+)。所以在這裏,html中所有非單標籤元素都是能匹配的。當然,如果我們不想使用默認的編號來引用分組,我們其實也是可以在定義分組的時候爲分組命名。爲分組命名的語法格式爲:(?<name>X),引用分組的語法格式爲:\k<name>。例如:

<(?<num1>a)>(.*)</\k<num1>>:等效於:<a>(.*)</a>

上述正則表達式定義了一個名爲num1的分組,並後續進行了引用。下面介紹正則表達式的最後一塊理論基礎,邊界匹配。

     5、邊界匹配
          以上我們所介紹的所有內容主要還是針對單個字符或者多個字符組成的分組,我們可以限制他們的出現次數以及出現位置等。但是其實在正則表達式中,我們也是可以限制邊界必須滿足某種條件的。主要涉及的元字符有:^, $, \A, \Z, \z和\b。

首先看元字符 ^ ,在字符組中,該元字符表示否定的意思,此處匹配正則表達式首部位置邊界。例如:^abc匹配一個以abc開頭的字符串。

元字符 $匹配的字符串的尾部邊界,它規定被匹配的字符串必須以什麼結束。例如:

abc$:dabc,abc,abc/n都是可匹配的

實際上,如果被匹配字符串是以指定字符結尾或者指定字符之後跟換行符,都是可匹配的。此處需要注意尾部邊界匹配時的表述格式。(不同於首部匹配)

\b匹配的是單詞邊界,所謂的單詞邊界指的就是:當一邊是字符,一邊是非字符的時候,此處即爲單詞邊界。也就是單詞結束的那個位置。還有一些邊界,例如:\A,\b,\Z等,各自匹配的邊界如下圖所示;
這裏寫圖片描述

當然,對於邊界匹配最通用的一種方式就是環視。它不侷限於整個表達式的開頭和結尾,它可以出現在表達式中的任何位置,既可以向前匹配,也可以向後匹配。主要分爲以下四種情況:

  • 肯定順序環視:它要求表達式的右邊字符串必須滿足某種約定,語法(?=….)。例如:single(?=cyy),字符e的右邊即爲邊界並且要求必須爲cyy,所以該表達式只能匹配singlecyy。
  • 否定順序環視:它要求表達式的右邊字符串必須不能滿足某種約定,和上一中情況是相反的,語法格式爲:(?!…)。
  • 肯定逆序環視:它要求表達式的左邊必須滿足某種約束,語法格式爲:(?<=…)。
  • 否定逆序環視:它要求表達式的左邊必須不能滿足某種約束,語法格式爲:(?<!…)。(此處爲了消除!在MarkDown編輯器中的特殊樣式,加了空格,望讀者注意)

雖然看起來有四種不同的環視類型,但是實際上分爲兩種,一種是向左看,一種是向右看。以上有關正則表達式的基本內容大致介紹完結,下面主要看看如何在Java中驗證我們上述的這些理論。

二、Java API對正則表達式的支持
     在Java中,對正則表達式的支持,主要還是java.util.regex這個包,我們常用的是其中的Pattern和Matcher這兩個類。其中Pattern綁定了一個正則表達式,也就是代表了一個規則,Matcher綁定了一個Pattern和一個被處理的字符串,我們可以利用Matcher中的一些方法來完成匹配工作。此外,Java中所有的正則表達式都是以字符串的形式出現的,所以自然離不開String這個類,該類中的很多方法的參數都是基於正則表達式的,下文將詳細介紹。我們首先看Pattern這個類。

Pattern主要用於編譯一個正則表達式,也就是創建一個Pattern對象,該對象與實際的一個正則表達式想綁定,它僅僅代表一個規則,與實際要匹配的字符串無關。例如:

String str = "//w";
Pattern p = Pattern.compile(str);

Pattern的compile方法將str這個正則表達式編譯成一種內部結構,然後以Pattern實例的形式返回,至於這種內部結構是什麼樣子的,此處暫時不涉及。我們只需要知道,此時返回的pattern實例是綁定了一個正則表達式的。當然,Pattern還有一個compile重載,可顯式指定匹配模式。

public static Pattern compile(String regex, int flags) {
    return new Pattern(regex, flags);
}

此處主要有四種匹配模式可選,單行模式,多行模式,無視大小寫模式,無視元字符模式(該模式下,所有元字符將會失效)。各自對應的常量:Pattern.DOTALL,Pattern.MULTILINE,Pattern.CASE_INSENSITIVE,Pattern.LITERAL。這些常量的值如下:

public static final int DOTALL = 0x20;(32public static final int MULTILINE = 0x08;(8public static final int CASE_INSENSITIVE = 0x02;(2public static final int LITERAL = 0x10;(16

當然,我們沒必要記住他們各自所對應的常量的值,在使用的時候直接調用它們的常量名即可。下面通過介紹String的幾個基本方法,瞭解正則表達式在Java中的基本使用情況。

首先我們看split方法,該方法用於分割字符串,返回一個String數組。

public String[] split(String regex, int limit)

第一個參數接受的是一個正則表達式,第二參數用於限定分割次數。其實從其源代碼中我們大致可以知曉該方法作用原理:首先利用indexOf方法找到分割符首次出現的位置,將該位置以前所有字符保存,拿到剩餘子串的所有內容,一樣的操作。最後得到的數組就是按照分隔符分割的結果。limit只不過強制限定了分割次數,達到次數上限,即使後面仍有分隔符可匹配,也選擇放棄。(打包後面所有內容爲一個分組),看個例子:

public static void main(String [] args){
    String str = "cyy,single.abc/https";
    String[] results = str.split("[,./]");
    for(String s : results){
        System.out.print(s+"  ");
    }
}
輸出結果:cyy  single  abc  https

此處有人可能會有疑問,說好的Pattern和Matcher纔是正則表達式的主要操作類,怎麼沒見到他們。其實在split內部調用的就是Pattern的相關方法。

return Pattern.compile(regex).split(this, limit);

這是String類中split方法的最後一行代碼,String中的split方法除了最後一行代碼,其餘代碼處理的都是regex爲普通單個字符的情況,而對於多個字符乃至包含元字符的時候都是由Pattern中split方法處理的,該方法中會創建Matcher類並調用其中find等方法進行匹配查找,代碼量比較多,此處不再贅述。

下面看String的一個匹配校驗的方法。

public boolean matches(String regex) {
    return Pattern.matches(regex, this);
}

顯然,該方法內部調用的是Pattern的matches方法,

public static boolean matches(String regex, CharSequence input) {
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(input);
    return m.matches();
}

這是一個非常標準的對正則表達式的處理流程,首先編譯(綁定)正則表達式字符串獲取Pattern實例,然後調用Pattern的matcher方法獲取Matcher實例,接着就可以利用Matcher實例完成大量工作。此處調用matches方法完成對已綁定的正則表達式和預處理字符串的匹配工作,返回值爲boolean。

最後看String的ReplaceAll方法:

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

該方法實際上還是依賴的Matcher中的replaceAll方法,由於一個Matcher實例是同時綁定一個正則表達式和一個被匹配字符串的。所以在Matcher內部的replaceAll方法在進行搜索匹配的時候就無需傳入額外參數。具體代碼,大家可以自行查看,此處節約篇幅不再贅述。

三、常見正則表達式的案例
     接下來我們主要從日常較爲普遍使用的一些案例中來深刻理解上述所有內容。

     1、Email地址
     通常我們的Email地址的格式主要是:

  • 3-18字符,可使用英文、數字、減號、點或下劃線
  • 必須以英文字母開頭,必須以英文字母或數字結尾
  • 點、減號、下劃線不能連續出現兩次或兩次以上

以上是騰訊QQ郵箱的要求,相對而言已算是較爲複雜,接下來我麼看如何實現它。首先,第一條要求:

[-._a-z0-9A-Z]{3,18}

滿足第二條要求:

[a-zA-Z][-._a-z0-9A-Z]{1,16}/w

滿足第三個條件:

(?![-0-9a-zA-Z._]*(--|\.\.|__))[a-zA-Z][-._a-z0-9A-Z]{1,16}/w

至於最後一個條件的匹配,我們使用否定順序環視來實現,它要求右邊界所有內容不能是如下的形式:0個或者多個(英文、數字、減號、點或下劃線)加上兩個連續減號或者點或者下劃線。也就是說,右邊如果由多個字符或者一個減號,點或者下劃線,那是沒事的,可一旦出現連續的減號,點或者下劃線,那麼就將立馬被否定順序環視匹配,進而不滿足條件結束。

其實上述對郵箱用戶名的匹配算是比較嚴格的,一般用於匹配郵箱用戶名的正則表達式則沒這麼嚴格,具體要求如下:

  • 由英文字母、數字、下劃線、減號、點號組成
  • 至少1位,不超過64位
  • 開頭不能是減號、點號和下劃線

由於比較簡單,此處直接寫出結果:

[^-._][-._a-zA-Z0-9]{0,63}

     2、手機號碼
     在看一個手機號碼的正則表達式匹配情況,具體要求如下:

  • 中國的手機號碼都是11位數字
  • 目前手機號第1位都是1,第2位取值爲3、4、5、7、8之一

最終的表述結果爲:

1[34578][0-9]{9}

還有很多案例,此處爲了使篇幅不至過長,到此爲止。更多案例還有待大家自行測試。

本文如有錯誤,望大家指出,相互學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章