黑馬程序員--JavaJAVA 正則表達式 (超詳細)

 

                                                    -------android培訓java培訓、期待與您交流! ---------- 

在Sun的Java JDK 1.40版本中,Java自帶了支持正則表達式的包,本文就拋磚引玉地介紹瞭如何使用java.util.regex包。

  可粗略估計一下,除了偶爾用Linux的外,其他Linu x用戶都會遇到正則表達式。正則表達式是個極端強大工具,而且在字符串模式-匹配和字符串模式-替換方面富有彈性。在Unix世界裏,正則表達式幾乎沒有什麼限制,可肯定的是,它應用非常之廣泛。

  正則表達式的引擎已被許多普通的Unix工具所實現,包括grep,awk,vi和Emacs等。此外,許多使用比較廣泛的腳本語言也支持正則表達式,比如Python,Tcl,JavaScript,以及最著名的Perl。

  我很早以前就是個Perl方面的黑客,如果你和我一樣話,你也會非常依賴你手邊的這些強大的text-munging工具。近幾年來,像其他程序開發者一樣,我也越來越關注Java的開發。

  Java作爲一種開發語言,有許多值得推薦的地方,但是它一直以來沒有自帶對正則表達式的支持。直到最近,藉助於第三方的類庫,Java開始支持正則表達式,但這些第三方的類庫都不一致、兼容性差,而且維護代碼起來很糟糕。這個缺點,對我選擇Java作爲首要的開發工具來說,一直是個巨大的顧慮之處。

  你可以想象,當我知道Sun的Java JDK 1.40版本包含了java.util.regex(一個完全開放、自帶的正則表達式包)時,是多麼的高興!很搞笑的說,我花好些時間去挖掘這個被隱藏起來的寶石。我非常驚奇的是,Java這樣的一個很大改進(自帶了java.util.regex包)爲什麼不多公開一點呢?!

  最近,Java雙腳都跳進了正則表達式的世界。java.util.regex包在支持正則表達也有它的過人之處,另外Java也提供詳細的相關說明文檔。使得朦朦朧朧的regex神祕景象也慢慢被撥開。有一些正則表達式的構成(可能最顯著的是,在於糅合了字符類庫)在Perl都找不到。

  在regex包中,包括了兩個類,Pattern(模式類)和Matcher(匹配器類)。Pattern類是用來表達和陳述所要搜索模式的對象,Matcher類是真正影響搜索的對象。另加一個新的例外類,PatternSyntaxException,當遇到不合法的搜索模式時,會拋出例外。

  即使對正則表達式很熟悉,你會發現,通過java使用正則表達式也相當簡單。要說明的一點是,對那些被Perl的單行匹配所寵壞的Perl狂熱愛好者來說,在使用java的regex包進行替換操作時,會比他們所以前常用的方法費事些。

  本文的侷限之處,它不是一篇正則表達式用法的完全教程。如果讀者要對正則表達進一步瞭解的話,推薦閱讀Jeffrey Frieldl的Mastering Regular Expressions,該書由O’Reilly出版社出版。我下面就舉一些例子來教讀者如何使用正則表達式,以及如何更簡單地去使用它。

  設計一個簡單的表達式來匹配任何電話號碼數字可能是比較複雜的事情,原因在於電話號碼格式有很多種情況。所有必須選擇一個比較有效的模式。比如:(212) 555-1212, 212-555-1212和212 555 1212,某些人會認爲它們都是等價的。

  首先讓我們構成一個正則表達式。爲簡單起見,先構成一個正則表達式來識別下面格式的電話號碼數字:(nnn)nnn-nnnn。

  第一步,創建一個pattern對象來匹配上面的子字符串。一旦程序運行後,如果需要的話,可以讓這個對象一般化。匹配上面格式的正則表達可以這樣構成:(/d{3})/s/d{3}-/d{4},其中/d單字符類型用來匹配從0到9的任何數字,另外{3}重複符號,是個簡便的記號,用來表示有3個連續的數字位,也等效於(/d/d/d)。/s也另外一個比較有用的單字符類型,用來匹配空格,比如Space鍵,tab鍵和換行符。

  是不是很簡單?但是,如果把這個正則表達式的模式用在java程序中,還要做兩件事。對java的解釋器來說,在反斜線字符(/)前的字符有特殊的含義。在java中,與regex有關的包,並不都能理解和識別反斜線字符(/),儘管可以試試看。但爲避免這一點,即爲了讓反斜線字符(/)在模式對象中被完全地傳遞,應該用雙反斜線字符(/)。此外圓括號在正則表達中兩層含義,如果想讓它解釋爲字面上意思(即圓括號),也需要在它前面用雙反斜線字符(/)。也就是像下面的一樣:

  //(//d{3}//)//s//d{3}-//d{4}

  現在介紹怎樣在java代碼中實現剛纔所講的正則表達式。要記住的事,在用正則表達式的包時,在你所定義的類前需要包含該包,也就是這樣的一行:

  import java.util.regex.*;

  下面的一段代碼實現的功能是,從一個文本文件逐行讀入,並逐行搜索電話號碼數字,一旦找到所匹配的,然後輸出在控制檯。

  BufferedReader in;

  Pattern pattern = Pattern.compile("//(//d{3}//)//s//d{3}-//d{4}");

  in = new BufferedReader(new FileReader("phone"));

  String s;

  while ((s = in.readLine()) != null)

  {

  Matcher matcher = pattern.matcher(s);

  if (matcher.find())

  {

  System.out.println(matcher.group());

  }

  }

  in.close();

  對那些熟悉用Python或Javascript來實現正則表達式的人來說,這段代碼很平常。在Python和Javascript這些語言中,或者其他的語言,這些正則表達式一旦明確地編譯過後,你想用到哪裏都可以。與Perl的單步匹配相比,看起來多多做了些工作,但這並不很費事。

  find()方法,就像你所想象的,用來搜索與正則表達式相匹配的任何目標字符串,group()方法,用來返回包含了所匹配文本的字符串。應注意的是,上面的代碼,僅用在每行只能含有一個匹配的電話號碼數字字符串時。可以肯定的說,java的正則表達式包能用在一行含有多個匹配目標時的搜索。本文的原意在於舉一些簡單的例子來激起讀者進一步去學習java自帶的正則表達式包,所以對此就沒有進行深入的探討。

  這相當漂亮吧! 但是很遺憾的是,這僅是個電話號碼匹配器。很明顯,還有兩點可以改進。如果在電話號碼的開頭,即區位號和本地號碼之間可能會有空格。我們也可匹配這些情況,則通過在正則表達式中加入/s?來實現,其中?元字符表示在模式可能有0或1個空格符。

  第二點是,在本地號碼位的前三位和後四位數字間有可能是空格符,而不是連字號,更有勝者,或根本就沒有分隔符,就是7位數字連在一起。對這幾種情況,我們可以用(-|)?來解決。這個結構的正則表達式就是轉換器,它能匹配上面所說的幾種情況。在()能含有管道符|時,它能匹配是否含有空格符或連字符,而尾部的?元字符表示是否根本沒有分隔符的情況。

  最後,區位號也可能沒有包含在圓括號內,對此可以簡單地在圓括號後附上?元字符,但這不是一個很好的解決方法。因爲它也包含了不配對的圓括號,比如"(555" 或 "555)"。相反,我們可以通過另一種轉換器來強迫讓電話號碼是否帶有有圓括號:(/(/d{3}/)|/d{3})。如果我們把上面代碼中的正則表達式用這些改進後的來替換的話,上面的代碼就成了一個非常有用的電話號碼數字匹配器:

  Pattern pattern =

  Pattern.compile("(//(//d{3}//)|//d{3})//s?//d{3}(-|)?//d{4}");

  可以確定的是,你可以自己試着進一步改進上面的代碼。

  現在看看第二個例子,它是從Friedl的中改編過來的。其功能是用來檢查文本文件中是否有重複的單詞,這在印刷排版中會經常遇到,同樣也是個語法檢查器的問題。

  匹配單詞,像其他的一樣,也可以通過好幾種的正則表達式來完成。可能最直接的是/b/w+/b,其優點在於只需用少量的regex元字符。其中/w元字符用來匹配從字母a到u的任何字符。+元字符表示匹配匹配一次或多次字符,/b元字符是用來說明匹配單詞的邊界,它可以是空格或任何一種不同的標點符號(包括逗號,句號等)。

  現在,我們怎樣來檢查一個給定的單詞是否被重複了三次?爲完成這個任務,需充分利用正則表達式中的所熟知的向後掃描。如前面提到的,圓括號在正則表達式中有幾種不同的用法,一個就是能提供組合類型,組合類型用來保存所匹配的結果或部分匹配的結果(以便後面能用到),即使遇到有相同的模式。在同樣的正則表達中,可能(也通常期望)不止有一個組合類型。在第n個組合類型中匹配結果可以通過向後掃描來獲取到。向後掃描使得搜索重複的單詞非常簡單:/b(/w+)/s+/1/b。

  圓括號形成了一個組合類型,在這個正則表示中它是第一組合類型(也是僅有的一個)。向後掃描/1,指的是任何被/w+所匹配的單詞。我們的正則表達式因此能匹配這樣的單詞,它有一個或多個空格符,後面還跟有一個與此相同的單詞。注意的是,尾部的定位類型(/b)必不可少,它可以防止發生錯誤。如果我們想匹配"Paris in the the spring",而不是匹配"Java's regex package is the theme of this article"。根據java現在的格式,則上面的正則表達式就是:Pattern pattern =Pattern.compile("//b(//w+)//s+//1//b");

  最後進一步的修改是讓我們的匹配器對大小寫敏感。比如,下面的情況:"The the theme of this article is the Java's regex package.",這一點在regex中能非常簡單地實現,即通過使用在Pattern類中預定義的靜態標誌CASE_INSENSITIVE :

  Pattern pattern =Pattern.compile("//b(//w+)//s+//1//b",

  Pattern.CASE_INSENSITIVE);

  有關正則表達式的話題是非常豐富,而且複雜的,用Java來實現也非常廣泛,則需要對regex包進行的徹底研究,我們在這裏所講的只是冰山一角。即使你對正則表達式比較陌生,使用regex包後會很快發現它強大功能和可伸縮性。如果你是個來自Perl或其他語言王國的老練的正則表達式的黑客,使用過regex包後,你將會安心地投入到java的世界,而放棄其他的工具,並把java的regex包看成是手邊必備的利器。

CharSequence

JDK 1.4定義了一個新的接口,叫CharSequence。它提供了StringStringBuffer這兩個類的字符序列的抽象:

interface CharSequence {
  charAt(int i);
  length();
  subSequence(int start, int end);
  toString();
}

爲了實現這個新的CharSequence接口,StringStringBuffer以及CharBuffer都作了修改。很多正則表達式的操作都要拿CharSequence作參數。

PatternMatcher

先給一個例子。下面這段程序可以測試正則表達式是否匹配字符串。第一個參數是要匹配的字符串,後面是正則表達式。正則表達式可以有多個。在Unix/Linux環境下,命令行下的正則表達式還必須用引號。

//: c12:TestRegularExpression.java

// Allows you to easly try out regular expressions.
// {Args: abcabcabcdefabc "abc+" "(abc)+" "(abc){2,}" }
import java.util.regex.*;
publicclass TestRegularExpression {
publicstaticvoid main(String[] args) {
if(args.length < 2) {
      System.out.println("Usage:/n" +
"java TestRegularExpression " +
"characterSequence regularExpression+");
      System.exit(0);
    }
    System.out.println("Input: /"" + args[0] + "/"");
for(int i = 1; i < args.length; i++) {
      System.out.println(
"Regular expression: /"" + args[i] + "/"");
      Pattern p = Pattern.compile(args[i]);
      Matcher m = p.matcher(args[0]);
while(m.find()) {
        System.out.println("Match /"" + m.group() +
"/" at positions " +
          m.start() + "-" + (m.end() - 1));
      }
    }
  }
} ///:~

Java的正則表達式是由java.util.regexPatternMatcher類實現的Pattern對象表示經編譯的正則表達式。靜態的compile( )方法負責將表示正則表達式的字符串編譯成Pattern對象。正如上述例程所示的,只要給Patternmatcher( )方法送一個字符串就能獲取一個Matcher對象。此外,Pattern還有一個能快速判斷能否在input裏面找到regex

staticboolean matches(?regex, ?input)

以及能返回String數組的split( )方法,它能用regex把字符串分割開來。

只要給Pattern.matcher( )方法傳一個字符串就能獲得Matcher對象了。接下來就能用Matcher的方法來查詢匹配的結果了。

boolean matches()
boolean lookingAt()
boolean find()
boolean find(int start)

matches( )的前提是Pattern匹配整個字符串,而lookingAt( )的意思是Pattern匹配字符串的開頭。

find( )

Matcher.find( )的功能是發現CharSequence裏的,與pattern相匹配的多個字符序列。例如:

//: c12:FindDemo.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
import java.util.*;
publicclass FindDemo {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) {
    Matcher m = Pattern.compile("//w+")
      .matcher("Evening is full of the linnet's wings");
while(m.find())
      System.out.println(m.group());
int i = 0;
while(m.find(i)) {
      System.out.print(m.group() + " ");
      i++;
    }
    monitor.expect(new String[] {
"Evening",
"is",
"full",
"of",
"the",
"linnet",
"s",
"wings",
"Evening vening ening ning ing ng g is is s full " +
"full ull ll l of of f the the he e linnet linnet " +
"innet nnet net et t s s wings wings ings ngs gs s "
    });
  }
} ///:~

"//w+"的意思是"一個或多個單詞字符",因此它會將字符串直接分解成單詞。find( )像一個迭代器,從頭到尾掃描一遍字符串。第二個find( )是帶int參數的,正如你所看到的,它會告訴方法從哪裏開始找——即從參數位置開始查找。

Groups

Group是指裏用括號括起來的,能被後面的表達式調用的正則表達式。Group 0 表示整個表達式,group 1表示第一個被括起來的group,以此類推。所以;

A(B(C))D

裏面有三個group:group 0是ABCD, group 1是BC,group 2是C

你可以用下述Matcher方法來使用group:

public int groupCount( )返回matcher對象中的group的數目。不包括group0。

public String group( ) 返回上次匹配操作(比方說find( ))的group 0(整個匹配)

public String group(int i)返回上次匹配操作的某個group。如果匹配成功,但是沒能找到group,則返回null。

public int start(int group)返回上次匹配所找到的,group的開始位置。

public int end(int group)返回上次匹配所找到的,group的結束位置,最後一個字符的下標加一。

//: c12:Groups.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
publicclass Groups {
privatestatic Test monitor = new Test();
staticpublicfinal String poem =
"Twas brillig, and the slithy toves/n" +
"Did gyre and gimble in the wabe./n" +
"All mimsy were the borogoves,/n" +
"And the mome raths outgrabe./n/n" +
"Beware the Jabberwock, my son,/n" +
"The jaws that bite, the claws that catch./n" +
"Beware the Jubjub bird, and shun/n" +
"The frumious Bandersnatch.";
publicstaticvoid main(String[] args) {
    Matcher m =
      Pattern.compile("(?m)(//S+)//s+((//S+)//s+(//S+))___FCKpd___6quot;)
        .matcher(poem);
while(m.find()) {
for(int j = 0; j <= m.groupCount(); j++)
        System.out.print("[" + m.group(j) + "]");
      System.out.println();
    }
    monitor.expect(new String[]{
"[the slithy toves]" +
"[the][slithy toves][slithy][toves]",
"[in the wabe.][in][the wabe.][the][wabe.]",
"[were the borogoves,]" +
"[were][the borogoves,][the][borogoves,]",
"[mome raths outgrabe.]" +
"[mome][raths outgrabe.][raths][outgrabe.]",
"[Jabberwock, my son,]" +
"[Jabberwock,][my son,][my][son,]",
"[claws that catch.]" +
"[claws][that catch.][that][catch.]",
"[bird, and shun][bird,][and shun][and][shun]",
"[The frumious Bandersnatch.][The]" +
"[frumious Bandersnatch.][frumious][Bandersnatch.]"
    });
  }
} ///:~

這首詩是Through the Looking Glass的,Lewis Carroll的"Jabberwocky"的第一部分。可以看到這個正則表達式裏有很多用括號括起來的group,它是由任意多個連續的非空字符('/S+')和任意多個連續的空格字符('/s+')所組成的,其最終目的是要捕獲每行的最後三個單詞;'$'表示一行的結尾。但是'$'通常表示整個字符串的結尾,所以這裏要明確地告訴正則表達式注意換行符。這一點是由'(?m)'標誌完成的(模式標誌會過一會講解)。

start( )和end( )

如果匹配成功,start( )會返回此次匹配的開始位置,end( )會返回此次匹配的結束位置,即最後一個字符的下標加一。如果之前的匹配不成功(或者沒匹配),那麼無論是調用start( )還是end( ),都會引發一個IllegalStateException。下面這段程序還演示了matches( )lookingAt( )

//: c12:StartEnd.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
publicclass StartEnd {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) {
    String[] input = new String[] {
"Java has regular expressions in 1.4",
"regular expressions now expressing in Java",
"Java represses oracular expressions"
    };
    Pattern
      p1 = Pattern.compile("re//w*"),
      p2 = Pattern.compile("Java.*");
for(int i = 0; i < input.length; i++) {
      System.out.println("input " + i + ": " + input[i]);
      Matcher
        m1 = p1.matcher(input[i]),
        m2 = p2.matcher(input[i]);
while(m1.find())
        System.out.println("m1.find() '" + m1.group() +
"' start = "+ m1.start() + " end = " + m1.end());
while(m2.find())
        System.out.println("m2.find() '" + m2.group() +
"' start = "+ m2.start() + " end = " + m2.end());
if(m1.lookingAt()) // No reset() necessary
        System.out.println("m1.lookingAt() start = "
          + m1.start() + " end = " + m1.end());
if(m2.lookingAt())
        System.out.println("m2.lookingAt() start = "
          + m2.start() + " end = " + m2.end());
if(m1.matches()) // No reset() necessary
        System.out.println("m1.matches() start = "
          + m1.start() + " end = " + m1.end());
if(m2.matches())
        System.out.println("m2.matches() start = "
          + m2.start() + " end = " + m2.end());
    }
    monitor.expect(new String[] {
"input 0: Java has regular expressions in 1.4",
"m1.find() 'regular' start = 9 end = 16",
"m1.find() 'ressions' start = 20 end = 28",
"m2.find() 'Java has regular expressions in 1.4'" +
" start = 0 end = 35",
"m2.lookingAt() start = 0 end = 35",
"m2.matches() start = 0 end = 35",
"input 1: regular expressions now " +
"expressing in Java",
"m1.find() 'regular' start = 0 end = 7",
"m1.find() 'ressions' start = 11 end = 19",
"m1.find() 'ressing' start = 27 end = 34",
"m2.find() 'Java' start = 38 end = 42",
"m1.lookingAt() start = 0 end = 7",
"input 2: Java represses oracular expressions",
"m1.find() 'represses' start = 5 end = 14",
"m1.find() 'ressions' start = 27 end = 35",
"m2.find() 'Java represses oracular expressions' " +
"start = 0 end = 35",
"m2.lookingAt() start = 0 end = 35",
"m2.matches() start = 0 end = 35"
    });
  }
} ///:~

注意,只要字符串裏有這個模式,find( )就能把它給找出來,但是lookingAt( )matches( ),只有在字符串與正則表達式一開始就相匹配的情況下才能返回truematches( )成功的前提是正則表達式與字符串完全匹配,而lookingAt( )成功的前提是,字符串的開始部分與正則表達式相匹配。

匹配的模式(Pattern flags)

compile( )方法還有一個版本,它需要一個控制正則表達式的匹配行爲的參數:

Pattern Pattern.compile(String regex, int flag)
flag的取值範圍如下:
編譯標誌 效果
Pattern.CANON_EQ 當且僅當兩個字符的"正規分解(canonical decomposition)"都完全相同的情況下,才認定匹配。比如用了這個標誌之後,表達式"a/u030A"會匹配"?"。默認情況下,不考慮"規範相等性(canonical equivalence)"。
Pattern.CASE_INSENSITIVE
(?i)
默認情況下,大小寫不明感的匹配只適用於US-ASCII字符集。這個標誌能讓表達式忽略大小寫進行匹配。要想對Unicode字符進行大小不明感的匹配,只要將UNICODE_CASE與這個標誌合起來就行了。
Pattern.COMMENTS
(?x)
在這種模式下,匹配時會忽略(正則表達式裏的)空格字符(注:不是指表達式裏的"//s",而是指表達式裏的空格,tab,回車之類)。註釋從#開始,一直到這行結束。可以通過嵌入式的標誌來啓用Unix行模式。
Pattern.DOTALL
(?s)
在這種模式下,表達式'.'可以匹配任意字符,包括表示一行的結束符。默認情況下,表達式'.'不匹配行的結束符。
Pattern.MULTILINE
(?m)
在這種模式下,'^'和'$'分別匹配一行的開始和結束。此外,'^'仍然匹配字符串的開始,'$'也匹配字符串的結束。默認情況下,這兩個表達式僅僅匹配字符串的開始和結束。
Pattern.UNICODE_CASE
(?u)
在這個模式下,如果你還啓用了CASE_INSENSITIVE標誌,那麼它會對Unicode字符進行大小寫不明感的匹配。默認情況下,大小寫不明感的匹配只適用於US-ASCII字符集。
Pattern.UNIX_LINES
(?d)
在這個模式下,只有'/n'才被認作一行的中止,並且與'.','^',以及'$'進行匹配。

在這些標誌裏面,Pattern.CASE_INSENSITIVEPattern.MULTILINE,以及Pattern.COMMENTS是最有用的(其中Pattern.COMMENTS還能幫我們把思路理清楚,並且/或者做文檔)。注意,你可以用在表達式裏插記號的方式來啓用絕大多數的模式。這些記號就在上面那張表的各個標誌的下面。你希望模式從哪裏開始啓動,就在哪裏插記號。

可以用"OR" ('|')運算符把這些標誌合使用:

//: c12:ReFlags.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
publicclass ReFlags {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) {
    Pattern p =  Pattern.compile("^java",
      Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
    Matcher m = p.matcher(
"java has regex/nJava has regex/n" +
"JAVA has pretty good regular expressions/n" +
"Regular expressions are in Java");
while(m.find())
      System.out.println(m.group());
    monitor.expect(new String[] {
"java",
"Java",
"JAVA"
    });
  }
} ///:~

這樣創建出來的正則表達式就能匹配以"java","Java","JAVA"...開頭的字符串了。此外,如果字符串分好幾行,那它還會對每一行做匹配(匹配始於字符序列的開始,終於字符序列當中的行結束符)。注意,group( )方法僅返回匹配的部分。

split( )

所謂分割是指將以正則表達式爲界,將字符串分割成String數組。

String[] split(CharSequence charseq)
String[] split(CharSequence charseq, int limit)

這是一種既快又方便地將文本根據一些常見的邊界標誌分割開來的方法。

//: c12:SplitDemo.java
import java.util.regex.*;
import com.bruceeckel.simpletest.*;
import java.util.*;
publicclass SplitDemo {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) {
    String input =
"This!!unusual use!!of exclamation!!points";
    System.out.println(Arrays.asList(
      Pattern.compile("!!").split(input)));
// Only do the first three:
    System.out.println(Arrays.asList(
      Pattern.compile("!!").split(input, 3)));
    System.out.println(Arrays.asList(
"Aha! String has a split() built in!".split(" ")));
    monitor.expect(new String[] {
"[This, unusual use, of exclamation, points]",
"[This, unusual use, of exclamation!!points]",
"[Aha!, String, has, a, split(), built, in!]"
    });
  }
} ///:~

第二個split( )會限定分割的次數。

正則表達式是如此重要,以至於有些功能被加進了String類,其中包括split( )(已經看到了),matches( )replaceFirst( )以及replaceAll( )。這些方法的功能同PatternMatcher的相同。

替換操作

正則表達式在替換文本方面特別在行。下面就是一些方法:

replaceFirst(String replacement)將字符串裏,第一個與模式相匹配的子串替換成replacement

replaceAll(String replacement),將輸入字符串裏所有與模式相匹配的子串全部替換成replacement

appendReplacement(StringBuffer sbuf, String replacement)sbuf進行逐次替換,而不是像replaceFirst( )replaceAll( )那樣,只替換第一個或全部子串。這是個非常重要的方法,因爲它可以調用方法來生成replacement(replaceFirst( )replaceAll( )只允許用固定的字符串來充當replacement)。有了這個方法,你就可以編程區分group,從而實現更強大的替換功能。

調用完appendReplacement( )之後,爲了把剩餘的字符串拷貝回去,必須調用appendTail(StringBuffer sbuf, String replacement)

下面我們來演示一下怎樣使用這些替換方法。說明一下,這段程序所處理的字符串是它自己開頭部分的註釋,是用正則表達式提取出來並加以處理之後再傳給替換方法的。

//: c12:TheReplacements.java
import java.util.regex.*;
import java.io.*;
import com.bruceeckel.util.*;
import com.bruceeckel.simpletest.*;
/*! Here's a block of text to use as input to
    the regular expression matcher. Note that we'll
    first extract the block of text by looking for
    the special delimiters, then process the
    extracted block. !*/
publicclass TheReplacements {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) throws Exception {
    String s = TextFile.read("TheReplacements.java");
// Match the specially-commented block of text above:
    Matcher mInput =
      Pattern.compile("///*!(.*)!//*/", Pattern.DOTALL)
        .matcher(s);
if(mInput.find())
      s = mInput.group(1); // Captured by parentheses
// Replace two or more spaces with a single space:
    s = s.replaceAll(" {2,}", " ");
// Replace one or more spaces at the beginning of each
// line with no spaces. Must enable MULTILINE mode:
    s = s.replaceAll("(?m)^ +", "");
    System.out.println(s);
    s = s.replaceFirst("[aeiou]", "(VOWEL1)");
    StringBuffer sbuf = new StringBuffer();
    Pattern p = Pattern.compile("[aeiou]");
    Matcher m = p.matcher(s);
// Process the find information as you
// perform the replacements:
while(m.find())
      m.appendReplacement(sbuf, m.group().toUpperCase());
// Put in the remainder of the text:
    m.appendTail(sbuf);
    System.out.println(sbuf);
    monitor.expect(new String[]{
"Here's a block of text to use as input to",
"the regular expression matcher. Note that we'll",
"first extract the block of text by looking for",
"the special delimiters, then process the",
"extracted block. ",
"H(VOWEL1)rE's A blOck Of tExt tO UsE As InpUt tO",
"thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE'll",
"fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr",
"thE spEcIAl dElImItErs, thEn prOcEss thE",
"ExtrActEd blOck. "
    });
  }
} ///:~

TextFile.read( )方法來打開和讀取文件。mInput的功能是匹配'/*!' 和 '!*/' 之間的文本(注意一下分組用的括號)。接下來,我們將所有兩個以上的連續空格全都替換成一個,並且將各行開頭的空格全都去掉(爲了讓這個正則表達式能對所有的行,而不僅僅是第一行起作用,必須啓用多行模式)。這兩個操作都用了StringreplaceAll( )(這裏用它更方便)。注意,由於每個替換隻做一次,因此除了預編譯Pattern之外,程序沒有額外的開銷。

replaceFirst( )只替換第一個子串。此外,replaceFirst( )replaceAll( )只能用常量(literal)來替換,所以如果每次替換的時候還要進行一些操作的話,它們是無能爲力的。碰到這種情況,得用appendReplacement( ),它能在進行替換的時候想寫多少代碼就寫多少。在上面那段程序裏,創建sbuf的過程就是選group做處理,也就是用正則表達式把元音字母找出來,然後換成大寫的過程。通常你得在完成全部的替換之後才調用appendTail( ),但是如果要模仿replaceFirst( )(或"replace n")的效果,你也可以只替換一次就調用appendTail( )。它會把剩下的東西全都放進sbuf

你還可以在appendReplacement( )replacement參數裏用"$g"引用已捕獲的group,其中'g' 表示group的號碼。不過這是爲一些比較簡單的操作準備的,因而其效果無法與上述程序相比。

reset( )

此外,還可以用reset( )方法給現有的Matcher對象配上個新的CharSequence

//: c12:Resetting.java
import java.util.regex.*;
import java.io.*;
import com.bruceeckel.simpletest.*;
publicclass Resetting {
privatestatic Test monitor = new Test();
publicstaticvoid main(String[] args) throws Exception {
    Matcher m = Pattern.compile("[frb][aiu][gx]")
      .matcher("fix the rug with bags");
while(m.find())
      System.out.println(m.group());
    m.reset("fix the rig with rags");
while(m.find())
      System.out.println(m.group());
    monitor.expect(new String[]{
"fix",
"rug",
"bag",
"fix",
"rig",
"rag"
    });
  }
} ///:~

如果不給參數,reset( )會把Matcher設到當前字符串的開始處。

 

如果你曾經用過Perl或任何其他內建正則表達式支持的語言,你一定知道用正則表達式處理文本和匹配模式是多麼簡單。如果你不熟悉這個術語,那麼“正則表達式”(Regular Expression)就是一個字符構成的串,它定義了一個用來搜索匹配字符串的模式。
許多語言,包括Perl、PHP、PythonJavaScriptJScript,都支持用正則表達式處理文本,一些文本編輯器用正則表達式實現高級“搜索-替換”功能。那麼Java又怎樣呢?本文寫作時,一個包含了用正則表達式進行文本處理的Java規範需求(Specification Request)已經得到認可,你可以期待在JDK的下一版本中看到它。
然而,如果現在就需要使用正則表達式,又該怎麼辦呢?你可以從Apache.org下載源代碼開放的Jakarta-ORO庫。本文接下來的內容先簡要地介紹正則表達式的入門知識,然後以Jakarta-ORO API爲例介紹如何使用正則表達式。
一、正則表達式基礎知識
我們先從簡單的開始。假設你要搜索一個包含字符“cat”的字符串,搜索用的正則表達式就是“cat”。如果搜索對大小寫不敏感,單詞“catalog”、“Catherine”、“sophisticated”都可以匹配。也就是說:
1.1 句點符號
假設你在玩英文拼字遊戲,想要找出三個字母的單詞,而且這些單詞必須以“t”字母開頭,以“n”字母結束。另外,假設有一本英文字典,你可以用正則表達式搜索它的全部內容。要構造出這個正則表達式,你可以使用一個通配符——句點符號“.”。這樣,完整的表達式就是“t.n”,它匹配“tan”、“ten”、“tin”和“ton”,還匹配“t#n”、“tpn”甚至“t n”,還有其他許多無意義的組合。這是因爲句點符號匹配所有字符,包括空格、Tab字符甚至換行符:
1.2 方括號符號
爲了解決句點符號匹配範圍過於廣泛這一問題,你可以在方括號(“[]”)裏面指定看來有意義的字符。此時,只有方括號裏面指定的字符才參與匹配。也就是說,正則表達式“t[aeio]n”只匹配“tan”、“Ten”、“tin”和“ton”。但“Toon”不匹配,因爲在方括號之內你只能匹配單個字符:
1.3 “或”符號
如果除了上面匹配的所有單詞之外,你還想要匹配“toon”,那麼,你可以使用“|”操作符。“|”操作符的基本意義就是“或”運算。要匹配“toon”,使用“t(a|e|i|o|oo)n”正則表達式。這裏不能使用方擴號,因爲方括號只允許匹配單個字符;這裏必須使用圓括號“()”。圓括號還可以用來分組,具體請參見後面介紹。
1.4 表示匹配次數的符號
表一顯示了表示匹配次數的符號,這些符號用來確定緊靠該符號左邊的符號出現的次數:

假設我們要在文本文件中搜索美國的社會安全號碼。這個號碼的格式是999-99-9999。用來匹配它的正則表達式如圖一所示。在正則表達式中,連字符(“-”)有着特殊的意義,它表示一個範圍,比如從0到9。因此,匹配社會安全號碼中的連字符號時,它的前面要加上一個轉義字符“/”。

圖一:匹配所有123-12-1234形式的社會安全號碼

假設進行搜索的時候,你希望連字符號可以出現,也可以不出現——即,999-99-9999和999999999都屬於正確的格式。這時,你可以在連字符號後面加上“?”數量限定符號,如圖二所示:

圖二:匹配所有123-12-1234和123121234形式的社會安全號碼

下面我們再來看另外一個例子。美國汽車牌照的一種格式是四個數字加上二個字母。它的正則表達式前面是數字部分“[0-9]{4}”,再加上字母部分“[A-Z]{2}”。圖三顯示了完整的正則表達式。

圖三:匹配典型的美國汽車牌照號碼,如8836KV

1.5 “否”符號
“^”符號稱爲“否”符號。如果用在方括號內,“^”表示不想要匹配的字符。例如,圖四的正則表達式匹配所有單詞,但以“X”字母開頭的單詞除外。

圖四:匹配所有單詞,但“X”開頭的除外

1.6 圓括號和空白符號
假設要從格式爲“June 26, 1951”的生日日期中提取出月份部分,用來匹配該日期的正則表達式可以如圖五所示:

圖五:匹配所有Moth DD,YYYY格式的日期

新出現的“/s”符號是空白符號,匹配所有的空白字符,包括Tab字符。如果字符串正確匹配,接下來如何提取出月份部分呢?只需在月份周圍加上一個圓括號創建一個組,然後用ORO API(本文後面詳細討論)提取出它的值。修改後的正則表達式如圖六所示:

圖六:匹配所有Month DD,YYYY格式的日期,定義月份值爲第一個組

1.7 其它符號
爲簡便起見,你可以使用一些爲常見正則表達式創建的快捷符號。如表二所示:
表二:常用符號

例如,在前面社會安全號碼的例子中,所有出現“[0-9]”的地方我們都可以使用“/d”。修改後的正則表達式如圖七所示:

圖七:匹配所有123-12-1234格式的社會安全號碼

二、Jakarta-ORO庫
有許多源代碼開放的正則表達式庫可供Java程序員使用,而且它們中的許多支持Perl 5兼容的正則表達式語法。我在這裏選用的是Jakarta-ORO正則表達式庫,它是最全面的正則表達式API之一,而且它與Perl 5正則表達式完全兼容。另外,它也是優化得最好的API之一。
Jakarta-ORO庫以前叫做OROMatcher,Daniel Savarese大方地把它贈送給了Jakarta Project。你可以按照本文最後參考資源的說明下載它。
我首先將簡要介紹使用Jakarta-ORO庫時你必須創建和訪問的對象,然後介紹如何使用Jakarta-ORO API。
▲ PatternCompiler對象
首先,創建一個Perl5Compiler類的實例,並把它賦值給PatternCompiler接口對象。Perl5Compiler是PatternCompiler接口的一個實現,允許你把正則表達式編譯成用來匹配的Pattern對象。
▲ Pattern對象
要把正則表達式編譯成Pattern對象,調用compiler對象的compile()方法,並在調用參數中指定正則表達式。例如,你可以按照下面這種方式編譯正則表達式“t[aeio]n”:
默認情況下,編譯器創建一個大小寫敏感的模式(pattern)。因此,上面代碼編譯得到的模式只匹配“tin”、“tan”、 “ten”和“ton”,但不匹配“Tin”和“taN”。要創建一個大小寫不敏感的模式,你應該在調用編譯器的時候指定一個額外的參數:
創建好Pattern對象之後,你就可以通過PatternMatcher類用該Pattern對象進行模式匹配。
▲ PatternMatcher對象
PatternMatcher對象根據Pattern對象和字符串進行匹配檢查。你要實例化一個Perl5Matcher類並把結果賦值給PatternMatcher接口。Perl5Matcher類是PatternMatcher接口的一個實現,它根據Perl 5正則表達式語法進行模式匹配:
使用PatternMatcher對象,你可以用多個方法進行匹配操作,這些方法的第一個參數都是需要根據正則表達式進行匹配的字符串:
· boolean matches(String input, Pattern pattern):當輸入字符串和正則表達式要精確匹配時使用。換句話說,正則表達式必須完整地描述輸入字符串。
· boolean matchesPrefix(String input, Pattern pattern):當正則表達式匹配輸入字符串起始部分時使用。
· boolean contains(String input, Pattern pattern):當正則表達式要匹配輸入字符串的一部分時使用(即,它必須是一個子串)。
另外,在上面三個方法調用中,你還可以用PatternMatcherInput對象作爲參數替代String對象;這時,你可以從字符串中最後一次匹配的位置開始繼續進行匹配。當字符串可能有多個子串匹配給定的正則表達式時,用PatternMatcherInput對象作爲參數就很有用了。用PatternMatcherInput對象作爲參數替代String時,上述三個方法的語法如下:
· boolean matches(PatternMatcherInput input, Pattern pattern)
· boolean matchesPrefix(PatternMatcherInput input, Pattern pattern)
· boolean contains(PatternMatcherInput input, Pattern pattern)
三、應用實例
下面我們來看看Jakarta-ORO庫的一些應用實例。
3.1 日誌文件處理
任務:分析一個Web服務器日誌文件,確定每一個用戶花在網站上的時間。在典型的BEA WebLogic日誌文件中,日誌記錄的格式如下:
分析這個日誌記錄,可以發現,要從這個日誌文件提取的內容有兩項:IP地址和頁面訪問時間。你可以用分組符號(圓括號)從日誌記錄提取出IP地址和時間標記。
首先我們來看看IP地址。IP地址有4個字節構成,每一個字節的值在0到255之間,各個字節通過一個句點分隔。因此,IP地址中的每一個字節有至少一個、最多三個數字。圖八顯示了爲IP地址編寫的正則表達式:

圖八:匹配IP地址

IP地址中的句點字符必須進行轉義處理(前面加上“/”),因爲IP地址中的句點具有它本來的含義,而不是採用正則表達式語法中的特殊含義。句點在正則表達式中的特殊含義本文前面已經介紹。
日誌記錄的時間部分由一對方括號包圍。你可以按照如下思路提取出方括號裏面的所有內容:首先搜索起始方括號字符(“[”),提取出所有不超過結束方括號字符(“]”)的內容,向前尋找直至找到結束方括號字符。圖九顯示了這部分的正則表達式。

圖九:匹配至少一個字符,直至找到“]”

現在,把上述兩個正則表達式加上分組符號(圓括號)後合併成單個表達式,這樣就可以從日誌記錄提取出IP地址和時間。注意,爲了匹配“- -”(但不提取它),正則表達式中間加入了“/s-/s-/s”。完整的正則表達式如圖十所示。

圖十:匹配IP地址和時間標記

現在正則表達式已經編寫完畢,接下來可以編寫使用正則表達式庫的Java代碼了。
爲使用Jakarta-ORO庫,首先創建正則表達式字符串和待分析的日誌記錄字符串:
這裏使用的正則表達式與圖十的正則表達式差不多完全相同,但有一點例外:在Java中,你必須對每一個向前的斜槓(“/”)進行轉義處理。圖十不是Java的表示形式,所以我們要在每個“/”前面加上一個“/”以免出現編譯錯誤。遺憾的是,轉義處理過程很容易出現錯誤,所以應該小心謹慎。你可以首先輸入未經轉義處理的正則表達式,然後從左到右依次把每一個“/”替換成“//”。如果要複檢,你可以試着把它輸出到屏幕上。
初始化字符串之後,實例化PatternCompiler對象,用PatternCompiler編譯正則表達式創建一個Pattern對象:
現在,創建PatternMatcher對象,調用PatternMatcher接口的contain()方法檢查匹配情況:
接下來,利用PatternMatcher接口返回的MatchResult對象,輸出匹配的組。由於logEntry字符串包含匹配的內容,你可以看到類如下面的輸出:
3.2 HTML處理實例一
下面一個任務是分析HTML頁面內FONT標記的所有屬性。HTML頁面內典型的FONT標記如下所示:
程序將按照如下形式,輸出每一個FONT標記的屬性:
在這種情況下,我建議你使用兩個正則表達式。第一個如圖十一所示,它從字體標記提取出“"face="Arial, Serif" size="+2" color="red"”。

圖十一:匹配FONT標記的所有屬性

第二個正則表達式如圖十二所示,它把各個屬性分割成名字-值對。

圖十二:匹配單個屬性,並把它分割成名字-值對

分割結果爲:
現在我們來看看完成這個任務的Java代碼。首先創建兩個正則表達式字符串,用Perl5Compiler把它們編譯成Pattern對象。編譯正則表達式的時候,指定Perl5Compiler.CASE_INSENSITIVE_MASK選項,使得匹配操作不區分大小寫。
接下來,創建一個執行匹配操作的Perl5Matcher對象。
假設有一個String類型的變量html,它代表了HTML文件中的一行內容。如果html字符串包含FONT標記,匹配器將返回true。此時,你可以用匹配器對象返回的MatchResult對象獲得第一個組,它包含了FONT的所有屬性:
接下來創建一個PatternMatcherInput對象。這個對象允許你從最後一次匹配的位置開始繼續進行匹配操作,因此,它很適合於提取FONT標記內屬性的名字-值對。創建PatternMatcherInput對象,以參數形式傳入待匹配的字符串。然後,用匹配器實例提取出每一個FONT的屬性。這通過指定PatternMatcherInput對象(而不是字符串對象)爲參數,反覆地調用PatternMatcher對象的contains()方法完成。PatternMatcherInput對象之中的每一次迭代將把它內部的指針向前移動,下一次檢測將從前一次匹配位置的後面開始。
本例的輸出結果如下:
3.3 HTML處理實例二
下面我們來看看另一個處理HTML的例子。這一次,我們假定Web服務器從widgets.acme.com移到了newserver.acme.com。現在你要修改一些頁面中的鏈接:
執行這個搜索的正則表達式如圖十三所示:

圖十三:匹配修改前的鏈接

如果能夠匹配這個正則表達式,你可以用下面的內容替換圖十三的鏈接:
注意#字符的後面加上了$1。Perl正則表達式語法用$1、$2等表示已經匹配且提取出來的組。圖十三的表達式把所有作爲一個組匹配和提取出來的內容附加到鏈接的後面。
現在,返回Java。就象前面我們所做的那樣,你必須創建測試字符串,創建把正則表達式編譯到Pattern對象所必需的對象,以及創建一個PatternMatcher對象:
接下來,用com.oroinc.text.regex包Util類的substitute()靜態方法進行替換,輸出結果字符串:
Util.substitute()方法的語法如下:
這個調用的前兩個參數是以前創建的PatternMatcher和Pattern對象。第三個參數是一個Substiution對象,它決定了替換操作如何進行。本例使用的是Perl5Substitution對象,它能夠進行Perl5風格的替換。第四個參數是想要進行替換操作的字符串,最後一個參數允許指定是否替換模式的所有匹配子串(Util.SUBSTITUTE_ALL),或只替換指定的次數。
【結束語】在這篇文章中,我爲你介紹了正則表達式的強大功能。只要正確運用,正則表達式能夠在字符串提取和文本修改中起到很大的作用。另外,我還介紹瞭如何在Java程序中通過Jakarta-ORO庫利用正則表達式。至於最終採用老式的字符串處理方式(使用StringTokenizer,charAt,和substring),還是採用正則表達式,這就有待你自己決定了。





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