本文簡要介紹Java的Regex及其實現方式,並通過執行個體講解Regex的具體用法。 1. Regex 1.1. 簡介
Regex(Regular Expression), 簡稱 正則, 也翻譯為 正規式, 用來表示文本搜尋模式。英文縮寫是 regex(reg-ex).
搜尋模式(search pattern)可能多種多樣, 如, 單個字元(character), 特定字串(fixed string), 包含特殊含義的複雜運算式等等. 對於給定的字串, Regex可能匹配一到多次, 也可能一次都不匹配。
Regex一般用來尋找、編輯和替換文本(text), 本質上, text(文本) 和 string(字串) 是一回事。
用Regex來分析/修改文本的過程, 稱為: 應用於文本/字串的Regex 。Regex掃描字串的順序是從左至右. 每個字元都只能被匹配成功一次, 下次匹配掃描就會從後面開始。例如, Regex aba, 匹配字串 ababababa 時, 只會掃描到兩個匹配(aba_aba__)。 1.2. 樣本
最簡單的例子是字母串。例如, Regex Hello World 能匹配的就是字串 “Hello World”。 Regex中, 點號 .(dot,英文句號)屬於萬用字元, 點號匹配任意一個字元(character); 例如, “a” 或者 “1”; 當然, 預設情況下點號不能匹配換行 \n, 需要特殊標識指定才行。
下表列舉了一些簡單的Regex,和對應的匹配模式。
Regex |
Matches |
this is text |
完全符合 “this is text” |
this\s+is\s+text |
匹配的內容為: “this”, 加上1到多個空白符(whitespace character, 如空格,tab,換行等), 加上 “is”, 加上1到多個空白符, 再加上 “text”. |
^\d+(\.\d+)? |
Regex以逸出字元 ^(小尖號)打頭, 表示這行必須以小尖號後面的字元模式開始, 才會達成匹配. \d+ 匹配1到多個數字. 英文問號 ? 表示可以出現 0~1次. \. 匹配的是字元 “.”, 小括弧(parentheses) 表示一個分組. 所以這個Regex可以匹配正整數或者小數,如: “5”, “66.6” 或者 “5.21” 等等. |
說明,中文的全形空格( )字元不屬於空白字元(whitespace characters), 可以認為其屬於一個特殊的漢字。 1.3. 程式設計語言對Regex的支援
大多數程式設計語言都支援Regex, 如 Java、Perl, Groovy 等等。但各種語言的Regex寫法略有一些不同。 2. 預備知識
本教程要求讀者具備Java語言相關的基礎知識。
下面的一些樣本通過 JUnit 來驗證執行結果。如果不想使用JUnit, 也可以改寫相關代碼。關於JUnit的知識請參考 JUnit教程: http://www.vogella.com/tutorials/JUnit/article.html。 3. 文法規則
本章介紹各種正則元素的範本, 我們會先介紹什麼是元字元(meta character)。 3.1. 通用運算式簡介
Regex |
說明 |
. |
點號(.), 匹配任意一個字元 |
^regex |
小尖號(^), 起始標識, 前面不能出現其他字元. |
regex$ |
貨幣符號($,dollar,美刀), 結束標識,後面不能再出現其他字元. |
[abc] |
字元組(set), 匹配 a 或 b 或 c. |
[abc][vz] |
字元組(set), 匹配 a 或 b 或 c,緊接著是 v 或 z. |
[^abc] |
如果小尖號(^, caret, 此處讀作 非) 出現在中括弧裡面的第一位, 則表示否定(negate). 這裡匹配: 除 a, b, c 之外的其他任一字元. |
[a-d1-7] |
範圍標記法: 匹配 a 到 d 之間的單個字元, 或者 1 到 7之間的單個字元, 整體只匹配單個字元, 而不是 d1 這種組合. |
X|Z |
匹配 X 或者 Z. |
XZ |
匹配XZ, X和Z必須按順序全部出現. |
$ |
判斷一行是否結束. |
3.2. 元字元
下面這些是預定義的元字元(Meta characters), 可用於提取通用模式, 如 \d 可以代替 [0-9], 或者[0123456789]。
Regex |
說明 |
\d |
單個數字, 等價於 [0-9] 但更簡潔 |
\D |
非數字, 等價於 [^0-9] 但更簡潔 |
\s |
空白字元(whitespace), 等價於 [ \t\n\x0b\r\f] |
\S |
非空白字元, 等價於 [^\s] |
\w |
反斜線加上小寫w, 表示單個標識符,即字母數字底線, 等價於 [a-zA-Z_0-9] |
\W |
非單詞字元, 等價於 [^\w] |
\S+ |
匹配1到多個非空白字元 |
\b |
匹配單詞外邊界(word boundary), 單詞字元指的是 [a-zA-Z0-9_] |
這些元字元主要取自於對應單詞的英文首字母, 例如: digit(數字), space(空白), word (單詞), 以及 boundary(邊界)。對應的大寫字元則用來表示取反。 3.3. 量詞
量詞(Quantifier)用來指定某個元素可以出現的次數。?, *, + 和 {} 等符號定義了Regex的數量。
Regex |
說明 |
樣本 |
* |
0到多次, 等價於 {0,} |
X* 匹配0到多個連續的X, .* 則匹配任一字元串 |
+ |
1到多次, 等價於 {1,} |
X+ 匹配1到多個連續的X |
? |
0到1次, 等價於 {0,1} |
X? 匹配0個,後者1個X |
{n} |
精確匹配 n 次 {} 前面序列出現的次數 |
\d{3} 匹配3位元字, .{10} 匹配任意10個字元. |
{m, n} |
出現 m 到 n 次, |
\d{1,4} 匹配至少1位元字,至多4位元字. |
*? |
在量詞後面加上 ?, 表示懶惰模式(reluctant quantifier). 從左至右慢慢掃描, 找到第一個滿足Regex的地方就暫停搜尋, 用來嘗試匹配最少的字串. |
|
3.4. 分組和引用
可以對Regex進行分組(Grouping), 用圓括弧 () 括起來。這樣就可以對括弧內的整體使用量詞。
當然, 在進行替換的時候, 還可以對分組進行引用。也就是擷取的群組(captures the group)。反向參考(back reference) 指向匹配中該分組所對應的字串。進行替換時可以通過 $ 來引用。
使用 $ 來引用一個擷取的群組。例如 $1 表示第一組, $2 表示第二組, 以此類推, $0則表示整個正則所匹配的部分。
例如, 想要去掉單詞後面, 句號/逗號(point or comma)前面的空格。可以把句號/逗號寫入正則中, 然後原樣輸出到結果中即可。
// 去除單詞與 `.|,` 之間的空格String pattern = "(\\w)(\\s+)([\\.,])";System.out.println(EXAMPLE_TEST.replaceAll(pattern, "$1$3"));
提取 標籤的內容:
// 提取 <title> 標籤的內容pattern = "(?i)(<title.*?>)(.+?)()";String updated = EXAMPLE_TEST.replaceAll(pattern, "$2");
3.5. 環視
環視(lookaround), 分為順序環視(Lookahead)與逆序環視(lookbehind), 屬於零寬度斷言(zero-length assertion)。 類似於行起始標識(^)和結束標識($); 或者單詞邊界(\b)一類的位置標識。
順序否定環視(Negative look ahead), 用於在匹配的同時, 排除掉某些情形。也就是說其後面不能是符合某種特徵的字串。
順序否定環視(Negative look ahead) 使用 (?!pattern) 這種格式定義。例如, 下面的正則, 只匹配後面不是 b 字母的 “a” 字母。
a(?!b)
類似的, 順序環視(look ahead), 也叫順序肯定環視。 如,只匹配a字母, 但要求後面只能是 b 字母, 否則這個 a 就不符合需要:
a(?=b)
注意,環視 是一種向前/後尋找的文法: (?=exp), 會尋找後面位置的 exp; 所環視的內容卻不包含在Regex匹配中。
環視(lookaround)是一種進階技巧, 環視的部分不會匹配到結果之中, 但卻要求匹配的字串前面/後面具備環視部分的特徵。
如果將等號換成驚嘆號, 就是環視否定 (?!exp), 變成否定語義,也就是說尋找的位置的後面不能是exp。
逆序肯定環視, (?<=exp), 表示所在位置左側能夠匹配 exp
逆序否定環視, (?<!exp), 表示所在位置左側不能匹配 exp
詳情請參考: 正則應用之——逆序環視探索: http://blog.csdn.net/lxcnn/article/details/4954134
參考: 利用Regex排除特定字串 http://www.cnblogs.com/wangqiguo/archive/2012/05/08/2486548.html 3.6. Regex的模式
在Regex開頭可以指定模式修飾符(mode modifier)。還可以組合多種模式, 如 (?is)pattern。
(?i) Regex匹配時不區分大小寫。
(?s) 單行模式(single line mode), 使點號(.) 匹配所有字元, 包括換行(\n)。
(?m) 多行模式(multi-line mode), 使 小尖號(^,caret) 和 貨幣符號($, dollar) 匹配目標字串中每一行的開始和結束。 3.7. Java中的反斜線
在Java字串中, 反斜線(\, backslash) 是逸出字元, 有內建的含義。在原始碼層級, 需要使用兩個反斜線\\來表示一個反斜線字元。如果想定義的Regex是 \w, 在 .java 檔案源碼中就需要寫成 \\w。 如果想要匹配文本中的1個反斜線, 則源碼中需要寫4個反斜線 \\\\。 4. String類正則相關的方法 4.1. String 類重新定義了正則相關的方法
Java中 String 類內建了4個支援正則的方法, 即: matches(), split(), replaceFirst() 和replaceAll() 方法。 需要注意, replace() 是純字串替換, 不支援Regex。
這些方法並沒有對效能進行最佳化。稍後我們將討論最佳化過的類。
方法 |
說明 |
s.matches("regex") |
判斷字串 s 是否能匹配正則 "regex". 只有整個字串匹配正則才返回 true . |
s.split("regex") |
用Regex "regex" 作為分隔字元來拆分字串, 返回結果是 String[] 數組. 注意 "regex" 對應的分隔字元並不包含在返回結果中. |
s.replaceFirst("regex", "replacement") |
替換第一個匹配 "regex" 的內容為 "replacement. |
s.replaceAll("regex", "replacement") |
將所有匹配 "regex" 的內容替換為 "replacement. |
下面是對應的樣本。
package de.vogella.regex.test;public class RegexTestStrings { public static final String EXAMPLE_TEST = "This is my small example " + "string which I'm going to " + "use for pattern matching."; public static void main(String[] args) { System.out.println(EXAMPLE_TEST.matches("\\w.*")); String[] splitString = (EXAMPLE_TEST.split("\\s+")); System.out.println(splitString.length);// should be 14 for (String string : splitString) { System.out.println(string); } // 將所有空白符(whitespace) 替換為 tab System.out.println(EXAMPLE_TEST.replaceAll("\\s+", "\t")); }}
4.2. 樣本
下面給出一些Regex的使用樣本。請參照注釋資訊。
If you want to test these examples, create for the Java project de.vogella.regex.string.
如果想測試這些樣本, 請將java檔案放到一個Java包下, 如 de.vogella.regex.string。
package de.vogella.regex.string;public class StringMatcher { // 如果字串完全符合 "`true`", 則返回 true public boolean isTrue(String s){ return s.matches("true"); } // 如果字串完全符合 "`true`" 或 "`True`", 則返回 true public boolean isTrueVersion2(String s){ return s.matches("[tT]rue"); } // 如果字串完全符合 "`true`" 或 "`True`" // 或 "`yes`" 或 "`Yes`", 則返回 true public boolean isTrueOrYes(String s){ return s.matches("[tT]rue|[yY]es"); } // 如果包含字串 "`true`", 則返回 true public boolean containsTrue(String s){ return s.matches(".*true.*"); } // 如果包含3個字母, 則返回 true public boolean isThreeLetters(String s){ return s.matches("[a-zA-Z]{3}"); // 當然也等價於下面這種比較土的方式 // return s.matches("[a-Z][a-Z][a-Z]"); } // 如果不以數字開頭, 則返回 true public boolean isNoNumberAtBeginning(String s){ // 可能 "^\\D.*" 更好一點 return s.matches("^[^\\d].*"); } // 如果包含了 `b` 之外的字元, 則返回 true public boolean isIntersection(String s){ return s.matches("([\\w&&[^b]])*"); } // 如果包含的某串數字小於300, 則返回 true public boolean isLessThenThreeHundred(String s){ return s.matches("[^0-9]*[12]?[0-9]{1,2}[^0-9]*"); }}
And a small JUnit Test to validates the examples.
我們通過 JUnit 測試來驗證。
package de.vogella.regex.string;import org.junit.Before;import org.junit.Test;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;public class StringMatcherTest { private StringMatcher m; @Before public void setup(){ m = new StringMatcher(); } @Test public void testIsTrue() { assertTrue(m.isTrue("true")); assertFalse(m.isTrue("true2")); assertFalse(m.isTrue("True")); } @Test public void testIsTrueVersion2() { assertTrue(m.isTrueVersion2("true")); assertFalse(m.isTrueVersion2("true2")); assertTrue(m.isTrueVersion2("True"));; } @Test public void testIsTrueOrYes() { assertTrue(m.isTrueOrYes("true")); assertTrue(m.isTrueOrYes("yes")); assertTrue(m.isTrueOrYes("Yes")); assertFalse(m.isTrueOrYes("no")); } @Test public void testContainsTrue() { assertTrue(m.containsTrue("thetruewithin")); } @Test public void testIsThreeLetters() { assertTrue(m.isThreeLetters("abc")); assertFalse(m.isThreeLetters("abcd")); } @Test public void testisNoNumberAtBeginning() { assertTrue(m.isNoNumberAtBeginning("abc")); assertFalse(m.isNoNumberAtBeginning("1abcd")); assertTrue(m.isNoNumberAtBeginning("a1bcd")); assertTrue(m.isNoNumberAtBeginning("asdfdsf")); } @Test public void testisIntersection() { assertTrue(m.isIntersection("1")); assertFalse(m.isIntersection("abcksdfkdskfsdfdsf")); assertTrue(m.isIntersection("skdskfjsmcnxmvjwque484242")); } @Test public void testLessThenThreeHundred() { assertTrue(m.isLessThenThreeHundred("288")); assertFalse(m.isLessThenThreeHundred("3288")); assertFalse(m.isLessThenThreeHundred("328 8")); assertTrue(m.isLessThenThreeHundred("1")); assertTrue(m.isLessThenThreeHundred("99")); assertFalse(m.isLessThenThreeHundred("300")); }}
5. Pattern與Matcher簡介
For advanced regular expressions the java.util.regex.Pattern and java.util.regex.Matcher classes are used.
要支援Regex的進階特性, 需要藉助 java.util.regex.Pattern 和 java.util.regex.Matcher 類。
首先建立/編譯 Pattern 對象, 用來定義Regex。對 Pattern 對象, 給定一個字串, 則產生一個對應的 Matcher 對象。通過 Matcher 對象就可以對 String 進行各種正則相關的操作。
package de.vogella.regex.test;import java.util.regex.Matcher;import java.util.regex.Pattern;public class RegexTestPatternMatcher { public static final String EXAMPLE_TEST = "This is my small example string which I'm going to use for pattern matching."; public static void main(String[] args) { Pattern pattern = Pattern.compile("\\w+"); // 如需忽略大小寫, 可以使用: // Pattern pattern = Pattern.compile("\\w+", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(EXAMPLE_TEST); // 尋找所有匹配的結果 while (matcher.find()) { System.out.print("Start index: " + matcher.start()); System.out.print(" End index: " + matcher.end() + " "); System.out.println(matcher.group()); } // 將空格替換為 tabs Pattern replace = Pattern.compile("\\s+"); Matcher matcher2 = replace.matcher(EXAMPLE_TEST); System.out.println(matcher2.replaceAll("\t")); }}
6. Regex樣本
下面列出了常用的Regex使用情景。希望讀者根據實際情況進行適當的調整。 6.1 或(Or)
任務: 編寫Regex, 用來匹配包含單詞 “Joe” 或者 “Jim” , 或者兩者都包含的行。
建立 de.vogella.regex.eitheror 包和下面的類。
package de.vogella.regex.eitheror;import org.junit.Test;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;public class EitherOrCheck { @Test public void testSimpleTrue() { String s = "humbapumpa jim"; assertTrue(s.matches(".*(jim|joe).*")); s = "humbapumpa jom"; assertFalse(s.matches(".*(jim|joe).*")); s = "humbaPumpa joe"; assertTrue(s.matches(".*(jim|joe).*")); s = "humbapumpa joe jim"; assertTrue(s.matches(".*(jim|joe).*")); }}
6.2. 匹配電話號碼
任務: 編寫Regex, 匹配各種電話號碼。
假設電話號碼(Phone number)的格式為 “7位連續的數字”; 或者是 “3位元字加空格/橫線, 再加上4位元字”。
package de.vogella.regex.phonenumber;import org.junit.Test;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;public class CheckPhone { @Test public void testSimpleTrue() { String pattern = "\\d\\d\\d([,\\s])?\\d\\d\\d\\d"; String s= "1233323322"; assertFalse(s.matches(pattern)); s = "1233323"; assertTrue(s.matches(pattern)); s = "123 3323"; assertTrue(s.matches(pattern)); }}
6.3. 判斷特定數字範圍
以下樣本用來判斷文本中是否具有連續的3位元字。
建立 de.vogella.regex.numbermatch 包和下面的類。
package de.vogella.regex.numbermatch;import java.util.regex.Matcher;import java.util.regex.Pattern;import org.junit.Test;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;public class CheckNumber { @Test public void testSimpleTrue() { String s= "1233"; assertTrue(test(s)); s= "0"; assertFalse(test(s)); s = "29 Kasdkf 2300 Kdsdf"; assertTrue(test(s)); s = "99900234"; assertTrue(test(s)); } public static boolean test (String s){ Pattern pattern = Pattern.compile("\\d{3}"); Matcher matcher = pattern.matcher(s); if (matcher.find()){ return true; } return false; }}
6.4. 校正超連結
假設需要從網頁中找出所有的有效連結。當然,需要排除 “javascript:” 和 “mailto:” 開頭的情況。
建立 de.vogella.regex.weblinks 包, 以及下面的類:
package de.vogella.regex.weblinks;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.MalformedURLException;import java.net.URL;import java.util.ArrayList;import java.util.List;import java.util.regex.Matcher;import java.util.regex.Pattern;public class LinkGetter { private Pattern htmltag; private Pattern link; public LinkGetter() {