2006年3月9日 星期四

Regular Expression 簡介

何謂 Regular Expression?

Regular expression (後文簡稱 RE)常見用來在某段文字中搜尋需要的字串。而 RE 的作法就是藉由“樣版(pattern)”來做比對。

RE 是一種由字元組成的樣式,用來比對資料,看看究竟符合或不符合這個樣式,然後可做進一步的處理。比如說:電子郵件位址的樣式,可寫成: .+\@.+。

RE 也可以稱為是樣式(Pattern),本身自成一種小型的程式語言。

例子

熟悉以前 DOS 作業系統的人都知道,‘*’‘?’字元在顯示目錄的命令中分別用來代表零或多個任意字元,以及單一一個任意字元(一定要有一個)

所以當我們使用像是“text?.*”的樣版時,下面列出的檔案都匹配該樣版:

  • textf.txt
  • text1.asp
  • text9.html

而下列的檔案名稱則不符合:

  • text.txt
  • text.asp
  • text.html

上面的例子展現了 RE 的運作模式。

為何要使用 RE

常見的 RE 應用範圍包括了:

  • 將某份 HTML 檔案中的某些特定的標籤移除。
  • 檢查電子郵件的位址是否合法。

基本上我們可以對字串進行下列的 RE 操作:

  • 驗證某個樣版:在一串字串中搜尋,檢查是否該樣版符合某部分子字串;回傳 true 或 false。
  • 從字串中取出子字串:在字串中搜尋某個字串,並將它取出。
  • 取代某個子字串:在字串中搜尋滿足範本的字串,並將它以其它字串取代之。

哪些地方在應用 RE

在文件處理方面,RE 經常可以發揮強大的功能。只要談到 RE,大家第一個聯想到的多半是 Perl 這個語言。RE 為 Perl 語言的基礎,因此其內建就支援 RE。其它的程式語言也可以藉由使用外部函式庫來支援使用 RE:

  • VBScript(5.x 以上):可藉由 RegExp 物件來使用 RE。
  • JScript(Version 5.x 以上):也是藉由 RegExp 物件來使用 RE。
  • C++ 經由 Regex++ 函式庫和 PCRE(Perl Compatible Regular Expression)函式庫來支援。
  • Java 內建支援 RE。
  • Microsoft .NET framework 內建支援 RE(經由使用 System.Text.RegularExpression 命名空間)。
  • PHP 內建 Perl 相容的函式,或是使用 POSIX 延伸的 RE 函式。

本文的程式碼以 Perl 語言為基準,至於其它語言因為它們彼此設計上的不同,語法稍有差異,但是大多大同小異。

在 Perl 中使用 RE

這裡我們簡單的說明要如何在 Perl 中使用 RE。

依樣版搜尋字串

expression =~ m/pattern/[switches]

在字串表示式中搜尋符合‘pattern’的子字串出現位置,並回傳該子字串(分別儲存於變數 $1}、$2$3...中)。其中的“m”代表“match”

例子

$test = "this is just one test";
$test =~ m/(o.e)/;

會回傳“one”於變數 $1 中。

替換字串

expression =~ s/pattern/new text/[switches]

在 expression 字串中搜尋符合 pattern 的子字串,並以 new text 替換找尋到的子字萬,其中的 s 代表 substitute。

例子

$test = "this is just one test";
$test =~ s/one/my/

將會以 my 替換 one,因此會將 this is just my test 字串存放於 $test 變數中。

Regular Expression 基本語法

類似於 C++ 中的跳脫字元,RE 的 meta character 必須以 \ 加以跳脫,比如若是要指定中括弧([),我們必須以 \[ 表示(這裡提到的跟你使用的程式語言有關;這裡是針對 Perl 來說)。

重要的 Meta Character 列表

Character描述
\標示下一個字元為一個特殊的字元(special character)、字母(literal)、backreference 或是 octal escape。比如,‘n’相當於去比對字元“n”。‘\n’則代表比對換行字元,‘ ’會配對到“\”等等。
.除了換行字元(\n)外,比對所有字元。若是要包含換行字元
,則使用下面這個樣式:[.\n]

Character 種類(Class)

一個字元種類(character class)是由一個或多的字元所組成的群組,這些群組內的字元被以包含於 [...] 來表示。比如說,“B[iu]rma”將會配對到 Birma 或是 Burma;也就是說,B 後面跟隨著 i 或是 u,後面再加上 rma。

換言之,字元種類代表:配對任何在該種類中的單一字元(match any single character of that class)。

RE 中也存在一些相反的字元種類:negotiated character class;其代表,配對任何一種不存在於該種類中的單一字元。比如說‘[^1-6]’將會比對任意字元,除了數字 1 到 6。

數量詞(Quantifier)

假如我們無法確定到底會有多少個字元,我們可以使用數量詞來指明某個字元可以出現的次數;我們可以使用像“Hel+o”,來代表 He 後面接著一或多個 l,後面再加上一個 o。

字元描述
*出現零次或多次以上;比如‘zo*’會配對到 z 或 zoo;* 相等於 ‘{0,}’。
+出現一次或多次;比如‘zo+’配對到‘zo’和 zoo,但是不包括 z;+ 相等於‘{1,}’。
?出現零次或一次。比如‘do(es)?’配對在 do 或 does 中的 do;?; 相當於‘{0,1}’。
{n}n 為非負整,代表確實的配對次數;比如‘o{2}’不會配對到位於 Bob 中的 o,而會配對到位於 foo 中的兩個 o。
{n,}n 為非負整,至少配對到 n 次;比如‘o{2,}’不會配對到位於 Bob 中的 o,但是會配對到 fooooooood 中所有的 o。‘o{1,}’相當於‘o+’;‘o{0,}’相當於‘o*’。
{n,m}m 和 n 皆為非負整數,其中 n 要小於等於 m;配對至少 n 次,至多 m 次。比如‘o{1,3}’配對到 fooooood 中的前面三個 o;‘o{0,1}’相當於‘o?’。要注意的是,在逗號和數字之間,請不要出現任何的空白。

Greedy

要注意到的一件事是,‘*’和‘+’是 greedy;它們會盡量去配對,配對出越多的會優先選取。比如說:

$test = "hello out there, how are you";
$test =~ m/h.*o/

表示:找出以 h 開頭,後面跟著多個任意的字元,最後以 o 結束。你也許會認為它會配對到“hello”,但是事實上,它配對到的是“hello out there, how are yo”,因為 RE 的組態是 greedy,所以它會搜尋直到最後一個“o”,而,這個例子中,就是在 you 中“o”。

你可以在表示式加上一個“?”,明確的指示要使用“ungreedy”方法。下面是一個例子:

$test = "hello out there, how are you";
$test =~ m/h.*?o/

上面的例子會尋找到“hello”;因為這個樣版的意義是去尋找一個“h”,後面接著多個任意字元,直到遇到第一次出現的“o”。

錨點(Anchor)

行起頭和行結尾

要檢查某一行的起始或結尾(或是字串),你可以使用 ^$ 這兩個 meta character。比如說,“^thing”,會配對到某個以“thing”起始的行。而“thing$”則會配對到某個以 “thing”結尾的行。

字邊界(Word Boundary)

\b’和‘\B’分別用來測試字邊界,以及非字邊界。下面這個例子:

$test =~ m/out/

對於“speak out loud”這個句子而言,會配對到“out”,但是也會配對到在“please don't shout at me”句子中的 out。若是要避免這種情況出現,你可以在樣式之前加上一個字邊界的錨點:

$test =~ m/\bout/

這樣,這個樣式只會尋找到以字邊界起始的“out”,而不包含在某個字中的“out”。

置換(alternation)和群組(grouping)

置換允許使用‘|’字元來在兩個或多個可替換的選擇中做選擇。在圓括弧中使用“(...|...|...)”,其允許你群組多個置換。

圓括弧本身用來抓取某個子字串,以便在其後對其做處理,並將它們儲存於 Perl 內建的 $1$2、... 及 $9 變數。

下面是一個例子:

$test = "I like apples a lot";
$test =~ m/like (apples|pines|bananas)/

將會配對成功,因為“apples”是三個置換人選中的其中之一,因此,會尋找到“like apples”。另外,圓括弧也會抓取到“apples”,並將它儲存於 $1 變數中,作為一個後參考(backreference)。

後參考(backreference)、往前看(lookahead-condition)和往後看(lookbehind-condition)

後參考

RE 一個重要的特性就是,它可以儲存之前配對到的子字串於某個變數中,以便之後處理。這是藉由在圓括弧中放置子字串來達到。這些抓取到的字串會被儲存於 Perl 的內建變數 $1 $2、... $9。

假如你不需要抓取某個子字串,但是需要用到圓括弧來群組字串,你可以使用“?:”來避免抓取。

例子:

$test = "Today is monday the 18th.";
$test =~ m/([0-9]+)th/

會將“18”儲存於 $1 變數中,而

$test = "Today is monday the 18th.";
$test =~ m/[0-9]+th/

$1 將不會儲存任何東西,因為並未使用圓括弧。

$test = "Today is monday the 18th.";
$test =~ m/(?:[0-9]+)th/

也不會儲存任何東西在 $1 變數中,因為在圓括弧中,有使用“?:”。另一個例子可以使用在置換中:

$test = "Today is monday the 18th.";
$test =~ s/ the ([0-9]+)th/, and the day is $1/

儲存於 $test 變數中的會是“Today is monday, and the day is 18.”。

你也可以藉由使用 \1\2、...\9 在尋找(query)中使用後參考(backreference),參考到之前尋找到的子字串。比如說,下面的例子會移除掉重複的字:

$test = "the house is is big";
$test =~ s/\b(\S+)\b(\s+\1\b)+/$1/

會在 $test 變數中儲存“the house is big”。

往前看和往後看狀態

有時候,我們需要下面幾個例子的配對:「配對它,但是只有在它不是處在另一個東西之前」或是「配對它,但是只有在它並非跟隨在某個東西之後」。若是只有考慮單一一個字元,你可以使用 negotiated character 類別:[^...]。

但是當它有多個字元時,你需要使用所謂的往前看狀態或是往後看狀態。總共有四種可能的類型:

  • Positive lookahead-condition '(?=re)' 只配對後面跟隨著 re RE。
  • Negative lookahead-condition '(?!re)' 只配對後面不跟隨著 re RE。
  • Positive lookbehind-condition '(?<=re)' 只配對前面跟隨著 re RE。
  • Negative lookbehind-condition '(?re RE。

範例:

$test = "HTML is a document description-language and not a programming-language";
$test =~ m/(?<=description-)language/ 

會配對到第一個“language”(description-language),因為前面跟隨著的是“description-”,所以

$test = "HTML is a document description-language and not a programming-language";
$test =~ m/(? 

會配對到第二個“language”(description-language),因為其並未跟隨在“description”。

更多例子

下面列出更多真實應用的例子。

置換前兩個字:

s/(\S+)(\s+)(\S+)/$3$2$1/

尋找 name=value 對:

m/(\w+)\s*=\s*(.*?)\s*$/

現在 name 儲存於 $1 中,而 value 則存於 $2

讀取 YYYY-MM-DD 格式的日期資料:

m/(\d{4})-(\d\d)-(\d\d)/

$1 中儲存的是 YYYY,在 $2中儲存的是 MM,DD 則存放於 $3

移除檔名前面的路徑:

s/^.*\///

總結

要注意的是,要處理相同的一件事,RE 可以用多種方式來呈現範本,有的表示法執行的速度可能很快,有的可能可讀性較高。

參考

沒有留言: