[分享] Regular Expression 新手入門 [[簡介]] Regular Expression (正規表示式,簡寫 regexp或regex) 是一個字串,可用來描述字串的模式, 例如我要表達「兩個英文大寫字母」,那麼 regexp 就是 [A-Z]{2}。 而使用這個 regexp ,就可以在一個字串內找出配合 (Match) 的部份, 例如在字串 $st = '11AA22BB33' 內,有哪些部份配合「兩個英文大寫字母」這個模式? 就會找到 'AA' 和 'BB' 兩部份了。 [[regexp 可以做什麼]] 以下是一些常見的 regexp 應用: use strict; my ($st, $pattern, $replacement, @array, @matches); ## ## 有沒有找到至少一個 Match? ## $st = '11AA22'; $pattern = 'AA'; if ($st =~ /$pattern/) { print "has at least one match\n"; } else { print "no match\n"; } ## ## 找出所有 Matches ## $st = '11AA22BB33'; $pattern = '[A-Z]{2}'; @matches = $st =~ /$pattern/g; foreach my $i (0 .. $#matches) { print "$i : $matches[$i]\n"; } ## ## 找出所有 Matches,並取代它們 ## $st = '11AA22AA33'; $pattern = 'AA'; $replacement = 'XX'; $st =~ s/$pattern/$replacement/g; print "$st\n"; ## ## 在 Matches 的位置分割 ## $st = '11AA22AA33'; $pattern = 'AA'; @array = split /$pattern/, $st; print join ',', @array; 以下是 .Net 例子(以 C# 語言) using System; using System.Text.RegularExpressions; class MainClass { public static void Main(string[] args) { string st, pattern, replacement; string[] array; MatchCollection mc; // // has at least one match? // st = "11AA22"; pattern = @"AA"; if ( Regex.IsMatch(st, pattern) ) { Console.WriteLine("has at least one match"); } else { Console.WriteLine("no match"); } // // get all matches // st = "11AA22BB33"; pattern = @"[A-Z]{2}"; mc = Regex.Matches(st, pattern); for (int i = 0; i < mc.Count; i++ ) { Console.WriteLine("{0} : {1}", i, mc[i]); } // // search and replace // st = "11AA22AA33"; pattern = @"AA"; replacement = "XX"; st = Regex.Replace(st, pattern, replacement); Console.WriteLine("{0}", st); // // split // st = "11AA22AA33"; pattern = "AA"; array = Regex.Split(st, pattern); Console.WriteLine(String.Join(",", array)); } } [[解釋 regexp 的意思]] 當您遇到一些很難看得懂的 regexp, 您可以用 YAPE::Regex::Explain 模組, 把 regexp 翻譯成英文,這對新手來說有很大幫助。 #### use YAPE::Regex::Explain; my $pattern = '[A-Z]{2}'; print YAPE::Regex::Explain->new($pattern)->explain; #### 輸出結果: [A-Z]{2} any character of: 'A' to 'Z' (2 times) 意思是「任何一個由 'A' 至 'Z' 的字元(兩次)」, 那麼它配合的東西就是: AA AB ... AZ BA BB ... ZA ZB ... ZZ 參考: Power Regexps, Part II By Simon Cozens http://www.perl.com/lpt/a/2003/07/01/regexps.html [[ 特殊字元 (Metacharacter) ]] 當您要表示一些特殊字元,例如括號,那就不能直接寫: $pattern = '('; 而要在前面加上反斜號: $pattern = '\('; 以下 regexp 的特殊字元: {}[]()^$.|*+?/ 為什麼這些是特殊字元呢?本文稍後會介紹。 例: #### $st = '11((22'; $pattern = '\(\('; @matches = $st =~ /$pattern/g; foreach my $i (0 .. $#matches) { print "$i : $matches[$i]\n"; } #### 順帶一提,應儘量用單引號來括字串, 因為它不會好像雙引號那樣會做字串插入,如果寫: $pattern = "\(\("; 那結果變成: $pattern = '(('; [[ 脫離序列 (Escape Sequence) ]] 當您要表示 tab、 newline 等不能列印的字元, 可以用 Escape Sequence。 例: $st = "11\t22\t33"; $pattern = '\t'; [[ 字元類別 (Character Class) ]] 您除了可以表示一個固定的字元外,如 $pattern = 'A'; 還可以表示一個字元集合,例如您想表示 「'A'字元或者'B'字元或者'C'字元」,可以寫 $pattern = '[ABC]'; 即把您想 match 的字元寫在中括號內。 例: $st = '11A22B33C44D'; $pattern = '[ABC]'; 會找到以下的 match: 0 : A 1 : B 2 : C 例如您想找 'AAAXX' 或者 'AABXX' 或者 'AACXX': $st = '11AACXX22AABXX33AAAXX44AAD'; $pattern = 'AA[ABC]XX'; 會找到以下的 match: 0 : AACXX 1 : AABXX 2 : AAAXX 留意一點,雖然 $pattern 內的 [ABC] 是先寫A, 但結果是先找到 'AACXX',而不是'AAAXX', 因為 Perl 是先從 $st 的左邊開始找,而不是先從右邊開始找, 而 'AACXX' 是最左邊的 match。 [[ 字元類別 - 脫離序列 ]] 在字元類別內表示一些特殊字元,也是要加反斜號, 不過字元類別內的特殊字元是: -]\^$ 本文稍後會解釋為何它們是特殊字元。 例: $st = '11A22B33-44]'; $pattern = '[AB\-\]]'; 會找到以下的 match: 0 : A 1 : B 2 : - 3 : ] [[ 字元類別 - 範圍運算子 (Range Operator) ]] 您可以用減號,來表示一段字元範圍,例如 $pattern = '[ABCDEFGHIJ]'; 可以寫成 $pattern = '[A-J]'; 這樣就更簡潔了。又例如 $pattern = '[ABCDKLMN0123]'; 可以寫成 $pattern = '[A-DK-N0-3]'; 但如果把減號寫在最頭或者最尾,例如 $pattern = '[-AB]'; 或 $pattern = '[AB-]'; 那麼減號就直接表示 '-' 字元,而不是範圍運算子了。 例: $st = '99A88L773]'; $pattern = '[A-DK-N0-3]'; 會找到以下的 match: 0 : A 1 : L 2 : 3 [[ 字元類別 - 反相字元類別 (Negated Character Class) ]] 例如要表示「'A'或者'B'或者'C'」,就寫 $pattern = '[ABC]'; 但如果要表示「'A'或者'B'或者'C'」的相反,即「不是'A'並且不是'B'並且不是'C'」 就可以寫 $pattern = '[^ABC]'; 在字元類別內,如果第一個字元是 '^',就表示這是一個「反相字元類別」。 例: $st = '11A22B33X44Y'; $pattern = '[^A-C0-9]'; 會找到以下的 match: 0 : X 1 : Y [[ 字元類別 - 簡寫 ]] Perl 提供了一些常用的字元類別簡寫,例如 $pattern = '[0-9]'; 其實可以寫成 $pattern = '\d'; 這就更簡潔了。以下列出這些簡寫: \d 等於寫 [0-9] \D 等於寫 [^0-9] ,即 \d 的相反 \w 等於寫 [0-9a-zA-Z_] ,即數字、英文大小寫字母和底線 \W 等於寫 [^0-9a-zA-Z_] ,即 \w 的相反 \s 等於寫 [\ \t\n\r\f] ,即白格字元 (Whitespace character), 包括space, tab, newline, carriage return, form feed \S 等於寫 [^\ \t\n\r\f] ,即 \s 的相反 還有一個很特別的簡寫,就是點號 '.', 因為它比較複雜,所以稍後再介紹它。 例: $st = '11A22b33_44='; $pattern = '\D'; 會找到以下的 match: 0 : A 1 : b 2 : _ 3 : = 例: $st = '11AA22A_33A044AX'; $pattern = 'A[^\dX-Z]'; 會找到以下的 match: 0 : AA 1 : A_ [[ Alternation ]] 如果想表示「'AB'字串或'KLM'字串或者'PQRST'」,可以寫 $pattern = 'AB|KLM|PQRST'; 即用直線分隔它們。 Alternation 跟 Character Class 很像, 例如以下兩者是一樣的: $pattern = '[ABC]'; $pattern = 'A|B|C'; 不過 Character Class 不能表示超過一個字元的字串。 例: $st = '11AB22KLMN33PQRS'; $pattern = 'AB|KLM|PQRST'; 會找到以下的 match: 0 : AB 1 : KLM 如果寫: $pattern = 'PQRST|KLM|AB'; 那還是會先找到 'AB',效果跟 Character Class 一樣。 [[群組 (Group)]] 如果想表示「'ABC'或者'ABKL'或者'ABPQR'」,除了可以寫: $pattern = 'ABC|ABKL|ABPQR'; 還可以這樣寫: $pattern = 'AB(C|KL|PQR)'; 即用括號來分開'AB'和接著的那三個 Alternatives, 好像數學上把 2*3 + 2*4 + 2*5 寫成 2 * (3+4+5)。 如果想連'AB'都要 match: $pattern = 'AB|ABC|ABKL|ABPQR'; 也可以把空字串都寫到括號內: $pattern = 'AB(C|KL|PQR|)'; [[群組 - 特殊變數 $1, $2, ...]] 括號還有一種特別的功能,就是把括號內 match 到的部份 放到特殊變數 $1, $2, ...。 例如您想檢查某字串是含有時間的模式,例如 HH:MM:SS, (不檢查數值範圍),同時想取得時分秒各個部份,可以寫: #### my ($time_string, $hour, $minute, $second); $time_string = '12:34:56'; $pattern = '(\d\d):(\d\d):(\d\d)'; if ($time_string =~ /$pattern/) { ($hour, $minute, $second) = ($1, $2, $3); print "hour=$hour,minute=$minute,second=$second"; } else { print 'Invalid time format'; } #### 執行結果: hour=12,minute=34,second=56 至於哪部份是 $1, $2, ...,就以開括號出現的先後順序來決定, 例如括著「時」那個開括號是第一個出現的,所以它就對應 $1。 (\d\d):(\d\d):(\d\d) 1______2______3 [[群組 - 巢狀群組]] 括號內也可以有括號,成為巢狀群組,例如: (\d\d):((\d\d):(\d\d)) 1______23______4 #### my ($time_string, $hour, $minute, $second); $time_string = '12:34:56'; $pattern = '(\d\d):((\d\d):(\d\d))'; if ($time_string =~ /$pattern/) { print "\$1=$1\n\$2=$2\n\$3=$3\n\$4=$4"; } else { print 'Invalid time format'; } #### 執行結果: $1=12 $2=34:56 $3=34 $4=56 [[群組 - 反參照 (Backreferences) \1, \2, ...]] 跟 $1, $2, ... 有很密切關係的東西,就是 Backreferences, 即 \1, \2, ... 。 它們不同之處,在於 $1 只應該用在 regexp 「以外」, 而 \1 只應該用在 regexp 「以內」。 例: #### $st = 'A_A'; $pattern = '([ABC])_\1'; if ($st =~ /$pattern/) { print "\$1=$1"; } #### 執行結果: $1=A 您可以嘗試把 $st 改成: $st = 'B_B'; $st = 'C_C'; 您會發現 \1 能夠取得之前所 match 的東西, 即應該放到 $1 的值。但要緊記,不應該在 regexp 以內寫 $1。 例: #### $st = 'ALZ_A_L_Z'; $pattern = '([ABC])([KLM])([XYZ])_\1_\2_\3'; if ($st =~ /$pattern/) { print "\$1=$1\n\$2=$2\n\$3=$3"; } #### 執行結果: $1=A $2=L $3=Z [[ 群組 - $1, $2, ... 的開始和結束位置 ]] Perl 5.6.0 提供了兩個陣列 @- 和 @+, @- 儲存了 $1, $2, ... 的開始位置,@+ 就儲存結束位置。 例: #### my ($time_string, $hour, $minute, $second); $time_string = '12:34:56'; $pattern = '(\d\d):(\d\d):(\d\d)'; $time_string =~ /$pattern/; foreach my $i (1 .. $#-) { print "Match $i='${$i}' at position ($-[$i] , $+[$i])\n"; } #### 執行結果: Match 1='12' at position (0 , 2) Match 2='34' at position (3 , 5) Match 3='56' at position (6 , 8) [[ 群組 - $` 、 $& 和 $' ]] 如果 regexp 沒有寫括號,就沒有 $1, $2, ... 可用了, 但您仍然可以用 $& 來取得 match,$` 則取得 match 前面所有東西, $' 取得 match 後面所有東西。 #### $st = '11AA22'; $pattern = 'AA'; $st =~ /$pattern/; print "\$`=$`\n\$&=$&\n\$'=$'"; #### 執行結果: $`=11 $&=AA $'=22 如果用 substr 來表示,那麼: $` 是 substr( $st, 0, $-[0] ) $& 是 substr( $st, $-[0], $+[0] - $-[0] ) $' 是 substr( $st, $+[0] ) [[ 重覆 (Repetition) ]] 如果想表示「五個 'A' 字元」,可以寫 $pattern = 'AAAAA'; 也可以用量化詞 (Quantifier) ,寫成 $pattern = 'A{5}'; 這樣就更清楚了。而量化詞可以寫在字元、字元類別或群組的後面。 例: $st = '11AAA22AAAAA'; $pattern = 'A{5}'; [[重覆 - 最多和最少]] 量化詞可分成兩類: 1) 最多配對(貪心的) Maximal Match (Greedy) 2) 最少配對(不貪心的) Minimal Match (Non-greedy) 以下列出「最多配對」: A? 表示 0 個或 1 個 'A' 字元(以最多為準) A* 表示 0 個或以上的 'A' 字元(以最多為準) A+ 表示 1 個或以上的 'A' 字元(以最多為準) A{n} 表示 n 個 'A' 字元 A{n,m} 表示最少 n 個,最多 m 個 'A' 字元(以最多為準) A{n,} 表示最少 n 個 'A' 字元(以最多為準) 以下列出「最少配對」: A?? 表示 0 個或 1 個 'A' 字元(以最少為準) A*? 表示 0 個或以上的 'A' 字元(以最少為準) A+? 表示 1 個或以上的 'A' 字元(以最少為準) A{n}? 表示 n 個 'A' 字元 (這個其實沒有所謂的「最多」還是「最少」之分, 定義這個符號只是為了保持符號一致性而己) A{n, m}? 表示最少 n 個,最多 m 個 'A' 字元(以最少為準) A{n,}? 表示最少 n 個 'A' 字元(以最少為準) 到底「最多配對」和「最少配對」有什麼分別呢?例如 $st = 'AAAAA'; $pattern1 = 'A{3,5}'; # 最多配對 $pattern2 = 'A{3,5}?'; # 最少配對 那麼 match 可能是: 1) 'AAA'(字元位置0至2) 2) 'AAAA'(字元位置0至3) 3) 'AAAAA'(字元位置0至4) 如果是「最多配對」,那麼 match 就是 'AAAAA',因為它是最多次數, 如果是「最少配對」,那麼 match 就是 'AAA',因為它是最少次數。 [[ 特殊字元 - 任何一個字元 ]] [[ 字串的開頭 ]] 如果要表示「字串的開頭」,可以用 '^',例如: $st = 'AA22'; $pattern = '^AA'; @matches = $st =~ /$pattern/g; 執行結果: 0 : AA 但 match 當中不會有 '^' 所配出來的字元, 因為 '^' 主要用來測試字串的特性(例如字串位置), 而不是配任何字元的。 為了方便理解,您可以把它「想像」成一個看得見的字元, 例如: $st = '頭AA22尾'; ## $st = 'AA22' $pattern = '頭AA'; ## $pattern = '^AA'; [[ 字串的結尾 ]] 如果要表示「字串的結尾,或者在字串最尾的 \n 和前面字元的中間」, 可以用 '$',例如: $st = "AA\nBB\n"; ## 用了雙引號 $pattern = '$'; @matches = $st =~ /$pattern/g; 結果有兩個 match ,但是'$' 與 '^' 一樣,是不會配到字元的。 您可以把它想像成: $st = "頭AA\nBB尾\n尾"; ## $st = "AA\nBB\n"; $pattern = '尾'; ## $pattern = '$'; [[ 多行 (Multi-line) ]] 一個字串內可以含有 \n,例如: $st = "AA\nAA\nAA\n"; $pattern = '^AA'; 如果我們想 '^' 配到每一行的開頭, 可以用 m 修飾詞: @matches = $st =~ /$pattern/mg; 執行結果: 0 : AA 1 : AA 2 : AA 想像成: $st = "頭AA尾\n頭AA尾\n頭AA尾\n尾"; $pattern = '頭AA'; 用了 m 修飾詞,也會令 '$' 配到每一行的結尾, $st = "AA\nAA\nAA\n"; $pattern = 'AA$'; @matches = $st =~ /$pattern/mg; 想像成: $st = "頭AA尾\n頭AA尾\n頭AA尾\n尾"; $pattern = 'AA尾'; 如果用了 m 修飾詞,您仍然可以用: \A 表示沒有 m 修飾詞的 '^' 意思 \Z 表示沒有 m 修飾詞的 '$' 意思 \z 代表字串的結尾(即 \Z ,但不包括最尾 \n 和前面字元的中間) $st = "AA\nAA\nAA\n"; $pattern1 = '\AAA'; $pattern2 = 'AA\Z'; $pattern3 = 'AA\z'; 總括來說,s 修飾詞會影響 '.' 的意思, m 修飾詞會影響 '^' 和 '$' 的意思, s 和 m 總共有四種可能: 沒有 s 和 m :預設模式 (Default Mode) /s :單行模式 (Single Line Mode) /m :多行模式 (Multi-Line Mode) /sm :清潔多行模式 (Clean Multi-Line Mode) 看看大家能否解釋以下程式的執行結果: #### $st = "A\nA\nA\n"; $pattern = '(^|$|.)'; @matches = $st =~ /$pattern/smg; #### [[ 字的邊界 Word Boundary ]] 字的邊界是指兩個連續字元的中間, 一個會配到 \w,而另一個會配到 \W。 (字串的開頭和結尾也視為配到 \W) 例如: $st = 'ABC-=+'; $pattern = 'C\b'; @matches = $st =~ /$pattern/g; 因為左邊的 'C' 會配到 \w,而右邊的 '-' 會配到 \W, 所以 \b 會配到它們的中間。 \b 不代表某個字元,而是代表某個位置的特性。 您可以想像成: $st = '界ABC界-=+'; $pattern = 'C界'; 您可以用 \B 來表示 \b 的相反,即「不是邊界」, 例如: $st = 'ABCDE'; $pattern = '\BC\B'; @matches = $st =~ /$pattern/g; 因為 'C' 本身配到 \w ,而它左邊的'B'和右邊的'D'也是配到 \w, 所以沒有邊界。 [[ 不分大小寫 (Case-insensitive) ]] 如果加入了 i 修飾詞,就會做不分大小寫的配對, 例如: $st = '11AA22BB'; $pattern = '[abAB]'; @matches = $st =~ /$pattern/g; 可以用 i 修飾詞寫成: $st = '11AA22BB'; $pattern = '[AB]'; # 或者 '[ab]' @matches = $st =~ /$pattern/ig; 因為不分大小寫,所以不必在 $pattern 把大小寫都列出來了。 [[ x 修飾詞 ]] 如果用了 x 修飾詞,就會令 regexp 解譯器忽略 $pattern 內的白格, (除了那些在被反斜號標示了的白格,及那些在字元類別內的白格) 還有在 $pattern 內的 '#' 也會有好像 Perl 一樣的單行註解作用, (除了那些在被反斜號標示了的'#',及那些在字元類別內的'#') 它的用途是讓您可以把 regexp 寫成容易閱讀的方式,及加入註解。 例子: 不用 x 修飾詞的寫法: $st = '11AA22 33\\\\44##'; $pattern = '(A| |\\\\|#)'; @matches = $st =~ /$pattern/g; 用了 x 修飾詞的寫法: $st = '11AA22 33\\\\44##'; $pattern = q{ ( # 左括號表示 Alternation 開始 A # 'A' 字元 | # 或者 [ ] # 用字元類別表示空格 | # 或者 \\\\ # 反斜號字元 | [#] # 用字元類別表示'#' ) # 右括號表示完結 }; @matches = $st =~ /$pattern/xg; [[ 伸展模式 (Extended Patterns) ]] 除了傳統的 regexp 語法外,Perl 還定義了一些額外的語法, 但在其它工具未必有這些語法,而且有些語法可能還在實驗階段, 說不定將來會被刪除,詳情請參閱說明文件。 這些語法都是這樣子的: (?char) 而 char 就表示伸展的種類。 [[ 伸展模式 - 內嵌註解 (Embedding Comments) ]] 語法: (?#text) 在 text 位置的東西會當作註解, 但要留意 text 內不可以含有右括號。 例: $pattern = '(?# THIS IS A COMMENT)(A| |\\\\|#)'; [[ 伸展模式 - 非捕捉群組 (Non-capturing groupings) ]] 語法: (?:pattern) 之前提到括號有個特別的作用,就是會把配到的東西放進 $1, $2,..., 例如: $time_string = '12:34:56'; $pattern = '(\d\d):(\d\d):(\d\d)'; if ($time_string =~ /$pattern/) { ($hour, $minute, $second) = ($1, $2, $3); print "hour=$hour,minute=$minute,second=$second"; } 但如果您不想放的話,可以用 (?:pattern): $time_string = '12:34:56'; $pattern = '(\d\d):(?:\d\d):(\d\d)'; if ($time_string =~ /$pattern/) { ($hour, $minute, $second) = ($1, $2, $3); print "hour=$hour,minute=$minute,second=$second"; } 因為「分」那部份使用了(?:pattern) ,所以就不會放到本來的 $2, 這就輪到「秒」的部份放到 $2了。 [[ 伸展模式 - 內嵌修飾詞 (Embedded Modifiers) ]] 語法: (?imsx-imsx:pattern) 之前提到可以用 i 修飾詞來做不分大小寫的配對,例如: $st = '11AB22Ab33aB44ab'; $pattern = 'AB'; @matches = $st =~ /$pattern/ig; 這樣整個 $pattern 都是不分大小寫了, 如果想某部份分辨大小寫,某部份不分辨, 可以用內嵌的寫法。 $st = '11AB22Ab33aB44ab'; $pattern = '(?i:A)(?-i:B)'; @matches = $st =~ /$pattern/g; 表示 A 不分大小寫,B 就分辨大小寫。 如果修飾詞前面是減號,表示關閉該修飾詞, 例如 (?s-i),表示啟動 s 修飾詞,關閉 i 修飾詞。 其實內嵌修飾詞的語法,是在非捕捉群組內加入修飾詞而己。 [[ 伸展模式 - 往前看(Look-ahead)和往後看(Look-behind) ]] 語法: 往前看(正)(Positive Look-ahead) (?=pattern) 往後看(正)(Positive Look-behind) (?<=pattern) 往前看(負)(Negative Look-ahead) (?!pattern) 往後看(負)(Negative Look-behind) (?