天天看點

正規表達式-1什麼是正規表達式

什麼是正規表達式

一個正規表達式,就是用某種模式去比對一類字元串的一個公式。很多人因為它們看上去比較古怪而且複雜是以不敢去使用——很不幸,這篇文章也不能夠改變這一 點,不過,經過一點點練習之後我就開始覺得這些複雜的表達式其實寫起來還是相當簡單的,而且,一旦你弄懂它們,你就能把數小時辛苦而且易錯的文本處理工作 壓縮在幾分鐘(甚至幾秒鐘)内完成。正規表達式被各種文本編輯軟體、類庫(例如Rogue Wave的tools.h++)、腳本工具(像awk/grep/sed)廣泛的支援,而且像Microsoft的Visual C++這種互動式IDE也開始支援它了。

我們将在如下的章節中利用一些例子來解釋正規表達式的用法,絕大部分的例子是基于vi中的文本替換指令和grep檔案搜尋指令來書寫的,不過它們都是比較典型的例子,其中的概念可以在sed、awk、perl和其他支援正規表達式的程式設計語言中使用。你可以看看不同工具中的正規表達式這一節,其中有一些在别的工具中使用正規表達式的例子。還有一個關于vi中文本替換指令(s)的簡單說明附在文後供參考。

正規表達式基礎

正規表達式由一些普通字元和一些 元字元(metacharacters)組成。普通字元包括大小寫的字母和數字,而元字元則具有特殊的含義,我們下面會給予解釋。

在最簡單的情況下,一個正規表達式看上去就是一個普通的查找串。例如,正規表達式"testing"中沒有包含任何元字元,,它可以比對"testing"和"123testing"等字元串,但是不能比對"Testing"。

要想真正的用好正規表達式,正确的了解元字元是最重要的事情。下表列出了所有的元字元和對它們的一個簡短的描述。

元字元 描述
. 比對任何單個字元。例如正規表達式r.t比對這些字元串:rat、rut、r t,但是不比對root。
$ 比對行結束符。例如正規表達式weasel$ 能夠比對字元串"He's a weasel"的末尾,但是不能比對字元串"They are a bunch of weasels."。
^ 比對一行的開始。例如正規表達式^When in能夠比對字元串"When in the course of human events"的開始,但是不能比對"What and When in the"。
* 比對0或多個正好在它之前的那個字元。例如正規表達式.*意味着能夠比對任意數量的任何字元。
/ 這是引用府,用來将這裡列出的這些元字元當作普通的字元來進行比對。例如正規表達式/$被用來比對美元符号,而不是行尾,類似的,正規表達式/.用來比對點字元,而不是任何字元的通配符。

[ ]

[c1-c2]

[^c1-c2]

比對括号中的任何一個字元。例如正規表達式r[aou]t比對rat、rot和rut,但是不比對ret。可以在括号中使用連字元-來指定字元的區間,例如正規表達式[0-9]可以比對任何數字字元;還可以制定多個區間,例如正規表達式[A-Za-z]可以比對任何大小寫字母。另一個重要的用法是“排除”,要想比對除了指定區間之外的字元——也就是所謂的補集——在左邊的括号和第一個字元之間使用^字元,例如正規表達式[^269A-Z] 将比對除了2、6、9和所有大寫字母之外的任何字元。
/< /> 比對詞(word)的開始(/<)和結束(/>)。例如正規表達式/<the能夠比對字元串"for the wise"中的"the",但是不能比對字元串"otherwise"中的"the"。注意:這個元字元不是所有的軟體都支援的。
/( /) 将 /( 和 /) 之間的表達式定義為“組”(group),并且将比對這個表達式的字元儲存到一個臨時區域(一個正規表達式中最多可以儲存9個),它們可以用 /1 到/9 的符号來引用。
| 将兩個比對條件進行邏輯“或”(Or)運算。例如正規表達式(him|her) 比對"it belongs to him"和"it belongs to her",但是不能比對"it belongs to them."。注意:這個元字元不是所有的軟體都支援的。
+ 比對1或多個正好在它之前的那個字元。例如正規表達式9+比對9、99、999等。注意:這個元字元不是所有的軟體都支援的。
? 比對0或1個正好在它之前的那個字元。注意:這個元字元不是所有的軟體都支援的。

/{i/}

/{i,j/}

比對指定數目的字元,這些字元是在它之前的表達式定義的。例如正規表達式A[0-9]/{3/} 能夠比對字元"A"後面跟着正好3個數字字元的串,例如A123、A348等,但是不比對A1234。而正規表達式[0-9]/{4,6/} 比對連續的任意4個、5個或者6個數字字元。注意:這個元字元不是所有的軟體都支援的。

最簡單的元字元是點,它能夠比對任何單個字元(注意不包括新行符)。假定有個檔案test.txt包含以下幾行内容:

  • he is a rat

    he is in a rut

    the food is Rotten

    I like root beer

我們可以使用grep指令來測試我們的正規表達式,grep指令使用正規表達式去嘗試比對指定檔案的每一行,并将至少有一處比對表達式的所有行顯示出來。指令

  • grep r.t test.txt

在test.txt檔案中的每一行中搜尋正規表達式 r.t,并列印輸出比對的行。正規表達式 r.t比對一個 r接着任何一個字元再接着一個 t。是以它将比對檔案中的 rat和 rut,而不能比對 Rotten中的 Rot,因為正規表達式是大小寫敏感的。要想同時比對大寫和小寫字母,應該使用字元區間元字元(方括号)。正規表達式 [Rr]能夠同時比對 R和 r。是以,要想比對一個大寫或者小寫的 r接着任何一個字元再接着一個 t就要使用這個表達式: [Rr].t。

要想比對行首的字元要使用抑揚字元(^)——又是也被叫做插入符。例如,想找到text.txt中行首"he"打頭的行,你可能會先用簡單表達式he,但是這會比對第三行的the,是以要使用正規表達式^he,它隻比對在行首出現的h。

有時候指定“除了×××都比對”會比較容易達到目的,當抑揚字元(^)出現在方括号中是,它表示“排除”,例如要比對he ,但是排除前面是t or s的情性(也就是the和she),可以使用:[^st]he。

可以使用方括号來指定多個字元區間。例如正規表達式[A-Za-z]比對任何字母,包括大寫和小寫的;正規表達式[A-Za-z][A-Za-z]* 比對一個字母後面接着0或者多個字母(大寫或者小寫)。當然我們也可以用元字元+做到同樣的事情,也就是:[A-Za-z]+ ,和[A-Za-z][A-Za-z]*完全等價。但是要注意元字元+ 并不是所有支援正規表達式的程式都支援的。

要指定特定數量的比對,要使用大括号(注意必須使用反斜杠來轉義)。想比對所有100和1000的執行個體而排除10和10000,可以使用:10/{2,3/},這個正規表達式比對數字1後面跟着2或者3個0的模式。在這個元字元的使用中一個有用的變化是忽略第二個數字,例如正規表達式0/{3,/} 将比對至少3個連續的0。

簡單的例子

這裡有一些有代表性的、比較簡單的例子。

vi 指令 作用
:%s/ */ /g 把一個或者多個空格替換為一個空格。
:%s/ *$// 去掉行尾的所有空格。
:%s/^/ / 在每一行頭上加入一個空格。
:%s/^[0-9][0-9]* // 去掉行首的所有數字字元。
:%s/b[aeio]g/bug/g 将所有的bag、beg、big和bog改為bug。
:%s/t/([aou]/)g/h/1t/g 将所有tag、tog和tug分别改為hat、hot和hug(注意用group的用法和使用/1引用前面被比對的字元)。

中級的例子(神奇的咒語)

例1

将所有方法foo(a,b,c)的執行個體改為foo(b,a,c)。這裡a、b和c可以是任何提供給方法foo()的參數。也就是說我們要實作這樣的轉換:

之前 之後
foo(10,7,2) foo(7,10,2)
foo(x+13,y-2,10) foo(y-2,x+13,10)
foo( bar(8), x+y+z, 5) foo( x+y+z, bar(8), 5)

下面這條替換指令能夠實作這一魔法:

  • :%s/foo(/([^,]*/),/([^,]*/),/([^)]*/))/foo(/2,/1,/3)/g

現在讓我們把它打散來加以分析。寫出這個表達式的基本思路是找出foo()和它的括号中的三個參數的位置。第一個參數是用這個表達式來識别的::/([^,]*/),我們可以從裡向外來分析它:

[^,] 除了逗号之外的任何字元
[^,]* 0或者多個非逗号字元
/([^,]*/) 将這些非逗号字元标記為/1,這樣可以在之後的替換模式表達式中引用它
/([^,]*/), 我們必須找到0或者多個非逗号字元後面跟着一個逗号,并且非逗号字元那部分要标記出來以備後用。

現在正是指出一個使用正規表達式常見錯誤的最佳時機。為什麼我們要使用[^,]*這樣的一個表達式,而不是更加簡單直接的寫法,例如:.*,來比對第一個參數呢?設想我們使用模式.*來比對字元串"10,7,2",它應該比對"10,"還是"10,7,"?為了解決這個兩義性(ambiguity),正規表達式規定一律按照最長的串來,在上面的例子中就是"10,7,",顯然這樣就找出了兩個參數而不是我們期望的一個。是以,我們要使用[^,]*來強制取出第一個逗号之前的部分。

這個表達式我們已經分析到了:foo(/([^,]*/),這一段可以簡單的翻譯為“當你找到foo(就把其後直到第一個逗号之前的部分标記為/1”。然後我們使用同樣的辦法标記第二個參數為/2。 對第三個參數的标記方法也是一樣,隻是我們要搜尋所有的字元直到右括号。我們并沒有必要去搜尋第三個參數,因為我們不需要調整它的位置,但是這樣的模式能 夠保證我們隻去替換那些有三個參數的foo()方法調用,在foo()是一個重載(overoading)方法時這種明确的模式往往是比較保險的。然後, 在替換部分,我們找到foo()的對應執行個體,然後利用标記好的部分進行替換,是的第一和第二個參數交換位置。

例2

假設有一個CSV(comma separated value)檔案,裡面有一些我們需要的資訊,但是格式卻有問題,目前資料的列順序是:姓名,公司名,州名縮寫,郵政編碼,現在我們希望講這些資料重新組 織,以便在我們的某個軟體中使用,需要的格式為:姓名,州名縮寫-郵政編碼,公司名。也就是說,我們要調整列順序,還要合并兩個列來構成一個新列。另外, 我們的軟體不能接受逗号前後面有任何空格(包括空格和制表符)是以我們還必須要去掉逗号前後的所有空格。

這裡有幾行我們現在的資料:

  • Bill Jones,     HI-TEK Corporation , CA, 95011

    Sharon Lee Smith, Design Works Incorporated, CA, 95012

    B. Amos   , Hill Street Cafe, CA, 95013

    Alexander Weatherworth, The Crafts Store, CA, 95014

    ...

我們希望把它變成這個樣子:

  • Bill Jones,CA 95011,HI-TEK Corporation

    Sharon Lee Smith,CA 95012,Design Works Incorporated

    B. Amos,CA 95013,Hill Street Cafe

    Alexander Weatherworth,CA 95014,The Crafts Store

    ...

我們将用兩個正規表達式來解決這個問題。第一個移動列和合并列,第二個用來去掉空格。

下面就是第一個替換指令:

  • :%s//([^,]*/),/([^,]*/),/([^,]*/),/(.*/)//1,/3 /4,/2/

這裡的方法跟例1基本一樣,第一個列(姓名)用這個表達式來比對: /([^,]*/),即第一個逗号之前的所有字元,而姓名内容被用 /1标記下來。公司名和州名縮寫字段用同樣的方法标記為 /2和 /3,而最後一個字段用 /(.*/)來比對("比對所有字元直到行末")。替換部分則引用上面标記的那些内容來進行構造。

下面這個替換指令則用來去除空格:

  • :%s/[ /t]*,[ /t]*/,/g

我們還是分解來看: [ /t]比對空格/制表符, [ /t]* 比對0或多個空格/制表符, [ /t]*,比對0或多個空格/制表符後面再加一個逗号,最後, [ /t]*,[ /t]*比對0或多個空格/制表符接着一個逗号再接着0或多個空格/制表符。在替換部分,我們簡單的我們找到的所有東西替換成一個逗号。這裡我們使用了結尾的可選的 g參數,這表示在每行中對所有比對的串執行替換(而不是預設的隻替換第一個比對串)。

例3

假設有一個多字元的片斷重複出現,例如:

Billy tried really hard

Sally tried really really hard

Timmy tried really really really hard

Johnny tried really really really really hard

而你想把"really"、"really really",以及任意數量連續出現的"really"字元串換成一個簡單的"very"(simple is good!),那麼以下指令:

:%s//(really /)/(really /)*/very /

就會把上述的文本變成:

Billy tried very hard

Sally tried very hard

Timmy tried very hard

Johnny tried very hard

表達式 /(really /)*比對0或多個連續的"really "(注意結尾有個空格),而 /(really /)/(really /)* 比對1個或多個連續的"really "執行個體。

繼續閱讀