學習Linux,101:使用正規表示式搜尋文字檔案

maojunxu發表於2013-04-01

概述

本文深入介紹基礎的 Linux 程式管理技術。您將學習如何:

  • 建立簡單的正規表示式
  • 使用正規表示式搜尋檔案和檔案系統
  • 使用正規表示式和 sed

本文幫助您準備 Linux Professional Institute`s Junior Level Administration (LPIC-1) 考試的 103 主題下的 103.7 考核目標。該考核目標的權值為 2。

先決條件

為了從本文獲得最大的收益,您應該具備基礎的 Linux 知識,並且具有一個能夠正常執行的 Linux 系統,以便練習本文討論的命令。不同版本的程式輸出的結果的格式可能不同,因此您的結果可能與本文圖片和清單所示的結果有所不同。本文以之前的文章 “學習 Linux,101:文字流和過濾器” 中討論的概念為基礎。


回頁首

設定示例

聯絡 Ian

  Ian 是我們最受歡迎並且很多產的作者之一。檢視
Ian 的個人資料
並與他和 My developerWorks 上的其他作者和讀者聯絡。

在本文中,我們將使用文章 “學習 Linux,101:文字流和過濾器” 中建立的檔案練習命令。如果您沒有學習該文或者沒有儲存得到的檔案,您可以在名為 lpi103-7 的子目錄中新建一個子目錄,並建立必要的檔案。開啟文字視窗,使用主目錄作為當前目錄。然後,將清單 1 中的內容複製到視窗執行命令,這將建立 lpi103-7 子目錄以及您將使用的檔案。

清單 1. 建立示例檔案

				
mkdir -p lpi103-7 && cd lpi103-7 && {
echo -e "1 apple
2 pear
3 banana" > text1
echo -e "9	plum
3	banana
10	apple" > text2
echo "This is a sentence. " !#:* !#:1->text3
split -l 2 text1
split -b 17 text2 y; 
cp text1 text1.bkp
mkdir -p backup
cp text1 backup/text1.bkp.2
}
			

您的視窗應類似於清單 2,當前目錄現在是 lpi103-7 目錄中新建的目錄。

清單 2. 建立示例檔案——輸出

				
ian@attic4:~$ mkdir -p lpi103-7 && cd lpi103-7 && {
> echo -e "1 apple
2 pear
3 banana" > text1
> echo -e "9	plum
3	banana
10	apple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; 
> cp text1 text1.bkp
> mkdir -p backup
> cp text1 backup/text1.bkp.2
> }
ian@attic4:~/lpi103-7$ 


回頁首

正規表示式

正規表示式在計算機語言理論中有很長的歷史。大部分計算機學科的學生都知道,可以使用正規表示式表示的語言與有限時序機(finite automata)可以接受的語言一樣。本文中的正規表示式所代表的含義更為複雜,與您在電腦科學課堂上學到的內容可能不同,雖然傳承是一樣的。

正規表示式(也稱為 “regex” 或 “regexp”)是描述文字字串的一種方式或者一種模式,程式可以根據任何文字字串匹配 該模式,以提供強大的搜尋功能。grep(正規表示式處理程式的縮寫)是 Linux 或 UNIX® 程式設計師或管理員的標準裝備,他們可以在檔案搜尋或命令輸出中使用正規表示式。在文章 “學習
Linux,101:文字流和過濾器
” 中,我們介紹了 sed(流編輯器的縮寫),這是使用正規表示式在檔案或文字流中查詢和替換文字的另一個標準工具。本文將幫助您更好地理解
grepsed 使用的正規表示式。使用正規表示式的另一個程式是 awk

結合本系列文章中的其他部分您會發現,整本書都是以正規表示式和計算機語言理論為基礎的。更多建議請參見
參考資料

根據您對正規表示式的瞭解,您可能發現正規表示式語法與 “學習 Linux,101:檔案和目錄管理” 中討論的萬用字元語法有類似之處。但這種相似之處只是表面現象。


回頁首

基本的構建塊

大部分 Linux 系統中的 GNU 程式可以使用兩種常規表示式語法:basicextended。使用 GNU                grep,功能上沒有不同之處。本文將介紹基本的語法,以及它和擴充套件語法之間的不同之處。

正規表示式通過元字元 加強的字元操作符 構建。大部分字元與自身匹配,大部分元字元必須使用反斜槓()進行轉義。基本的操作包括:

連線
連線兩個正規表示式建立一個更長的正規表示式。例如,正規表示式 a 匹配字串 abcdcba 兩次,正規表示式
b 也是一樣。但是,ab 將只匹配 abcdcba,而
ba
將只匹配 abcdcba
重複
Kleene * 或重複操作符將匹配 0 次或多次前一個正規表示式。因此像 a*b 之類的表示式將匹配以任何以
a
開頭以 b 結尾的字串,包括 b 本身。Kleene * 不用轉義,因此希望匹配字面值星號(*)的表示式必須讓星號轉義。這裡使用的 * 與萬用字元中使用的 * 不同,萬用字元中的 * 號匹配任何字串。
交替
交替操作符(|)匹配前置或後置表示式。它必須匹配前一個或後一個表示式之一。在基本語法中它必須轉義。例如,表示式 a*|b*c 匹配由任何數量的
ab 組成(但不是同時)且以一個 c 結尾的字串。同樣,單個字元
c 也是匹配的。

儘量不要引用正規表示式以避免 shell 膨脹。


回頁首

搜尋檔案和檔案系統

我們將之前的示例中建立的使用文字檔案(參見 “設定示例”)。研究清單 3 中的示例。注意
grep 使用一個正規表示式作為引數,還有 0 個或多個要搜尋的檔案。如果沒有給定檔案,grep 將搜尋 stdin,這讓它成為一個可以在管道中使用的過濾器。如果沒有匹配任何行,則
grep 沒有輸出,儘管可以測試它的退出程式碼。

清單 3. 簡單的正規表示式

				
ian@attic4:~/lpi103-7$ grep p text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep pea text1
2 pear
ian@attic4:~/lpi103-7$ grep "p*" text1
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ grep "pp*" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "x" text1; echo $?
1
ian@attic4:~/lpi103-7$ grep "x*" text1; echo $?
1 apple
2 pear
3 banana
0
ian@attic4:~/lpi103-7$ cat text1 | grep "l|n"
1 apple
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an 
s* here" | grep "s*"
s* here

從上例中可以看出,有時候會得到出乎意料的結果,尤其是使用重複的時候。您可能預期 p* 或者 pp* 能夠匹配幾個帶
p 的字串,但是 p*x* 能匹配檔案的所有行,因為 * 操作符匹配
0 次或多次前一個正規表示式。

有兩個示例演示了從 grep 退出的程式碼。如果找到匹配,則返回值 0。如果發生錯誤,比如要搜尋的檔案不存在,則返回大於 1 的值(GNU grep 總是返回 2)。

快捷鍵

現在可以使用 grep 和基本的正規表示式構建塊了,以下是一些方便的快捷鍵。

+
+ 操作符類似於 * 操作符,但是它匹配一次或多次前一個正規表示式。基本表示式中它必須轉義。
?
? 表示前一個表示式是可選的,因此它表示匹配 0 次或 1 次。這與萬用字元中使用的 ? 不同。
.
.(句點)是表示任何字元的元字元。最常使用的方式是 .*,該表示式匹配包含任何字元(或沒有字元)的任意長度的字串。不用說您就明白,這一般在一個較長的表示式中使用。比較句點與萬用字元中使用的 ?,.* 與萬用字元中使用的 *。

清單 4. 更多正規表示式

				
ian@attic4:~/lpi103-7$ grep "pp+" text1 # at least two p`s
1 apple
ian@attic4:~/lpi103-7$ grep "pl?e" text1
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "pl?e" text1 # pe with optional l between
1 apple
2 pear
ian@attic4:~/lpi103-7$ grep "p.*r" text1 # p, some string then r
2 pear
ian@attic4:~/lpi103-7$ grep "a.." text1 # a followed by two other letters
1 apple
3 banana

匹配一行的開始或結束

^(脫字元號)匹配一行的開始,$(美元符號)匹配行的結束。^..b 匹配行開始處任何後跟 b 的兩個字元,而
ar$ 匹配任何以 ar 結束的行。正規表示式 ^$ 匹配空行。

更復雜的表示式

到目前為止,我們已經學習了用於單個字元的重複。如果希望搜尋一個或多個多字元字串,比如 bananan 出現了兩次,那麼可以使用圓括號,在基本語法中必須轉義。類似地,您可能希望搜尋一些字元,但又不想使用 . 這麼通用或者交替這麼囉嗦的表示式。那麼,您可以使用方括號([])將交替情況括起來,常規語法需要轉義。方括號中的表示式構成了一個字元類。使用方括號還可以減少轉義特殊字元(比如 . 和 *)的需求,例外情況見後文。

清單 5. 圓括號和字元類

				

ian@attic4:~/lpi103-7$ grep "(an)+" text1 # find at least 1 an
3 banana
ian@attic4:~/lpi103-7$ grep "an(an)+" text1 # find at least 2 an`s
3 banana
ian@attic4:~/lpi103-7$ grep "[3p]" text1 # find p or 3
1 apple
2 pear
3 banana
ian@attic4:~/lpi103-7$ echo -e "find an
s* here
somewhere." | grep "s[.*]"
s* here
ian@attic4:~/lpi103-7$ echo -e "find an
 * in position 2." | grep ".[.*]"
 * in position 2.

字元類還有幾個有趣的可能性。

範圍表示式(Range expression)
範圍表示式是使用 -(連字元)分隔的雙字元,比如數字裡面的 0-9,十六進位制裡的 0-9a-fA-F。注意,範圍與語言環境有關。
署名類(Named class)
有些署名類可以為通常使用的類提供便捷。署名類以 [: 開始,以 :] 結束,可以在括號表示式中使用。示例如下:
[:alnum:]
字母數字字元
[:blank:]
空格和製表符
[:digit:]
數字 0 到 9(等效於 0-9)
[:upper:] 和 [:lower:]
分別為大寫字母和小寫字母。
^(求反)
在字元類 [ 後的第一個字元使用時,^(脫字元號)對剩餘字元求反,因此只有類中不存在該字元時(前導^除外)才能匹配。               

瞭解了以上特殊含義後我們知道,如果希望匹配一個字元類中的字面值 -(連字元),那麼您必須將其放在第一個或最後一個。如果想匹配字面值^(脫字字元),那麼它不能是第一個字元。] 在非第一個位置時表示結束類。

字元類中,正規表示式和萬用字元類似的,但使用的否定符號不同(^ 和 !)。清單 6 展示了一些字元類示例。

清單 6. 更多字元類

				
ian@attic4:~/lpi103-7$ # Match on range 3 through 7
ian@attic4:~/lpi103-7$ echo -e "123
456
789
0" | grep "[3-7]"
123
456
789
ian@attic4:~/lpi103-7$ # Find digit followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]][^nr]*$" text1
1 apple
ian@attic4:~/lpi103-7$ # Find a digit, n, or z followed by no n or r till end of line
ian@attic4:~/lpi103-7$ grep "[[:digit:]nz][^nr]*$" text1
1 apple
3 banana

最後一個示例讓您感到奇怪嗎? 在這種情況下,第一個括號表示式匹配字串中的任何數字、 n 或 z,至少 n 後面沒有另一個 n 或 r,因此字串結尾處的 na 匹配該正規表示式。

哪些內容匹配?

如果您能夠區分高亮顯示,比如用顏色、粗體或下劃線,那麼您可以設定 GREP_COLORS 環境變數來高亮顯示匹配內容。預設設定使用粗體紅色高亮顯示匹配內容,如圖 1 所示。您會看到整個輸出的第一行都是匹配的,但是第二行只匹配最後兩個字元。

圖 1. 使用顏色區分 grep 匹配內容

使用顏色區分上文所述的 grep 匹配內容。

如果您是正規表示式新手,或者不確定 grep 為什麼返回某一行,那麼這項技術可以幫您。


回頁首

擴充套件的正規表示式

擴充套件的正規表示式語法是 GNU 擴充套件。我們在基本語法中使用時,它不需要轉義一些字元,包括圓括號、`?`、`+`、`|`和 `{`。但缺點在於,如果您在正規表示式中將它們作為字元解釋,那麼必須進行轉義。您可以使用
-E(或者 grep 的 --extended-regexp 選項)表示您正在使用擴充套件的正規表示式語法。此外,egrep 命令也可以幫助您實現這一點。清單 7 展示了本節上文中使用的示例,以及
egrep 使用的相應擴充套件表示式。

清單 7. 擴充套件的正規表示式

				
ian@attic4:~/lpi103-7$ # Find b followed by one or more an`s and then an a
ian@attic4:~/lpi103-7$ grep "b(an)+a" text1 
3 banana
ian@attic4:~/lpi103-7$ egrep "b(an)+a" text1
3 banana


回頁首

在檔案中查詢內容

現在您瞭解了基本的命令,讓我們使用 grepfind 在檔案系統中查詢內容。示例相對比較簡單;它們使用
學習 Linux,101:文字流和過濾器 中建立的檔案或者您在 lpi103-7 目錄及其子目錄中建立的檔案。(參見 “設定示例”。)如果使用本系列之前的文章中建立的檔案,您將有一些額外的檔案,因此將看到一些額外的結果。

首先,grep 可以一次搜尋多個檔案。如果新增 -n 選項,它將告訴您匹配的行號。如果只想知道匹配多少行,可以使用
-c 選項,如果只想獲得匹配的檔案列表,可以使用 -l 選項。清單 8 展示了一些示例。

清單 8. 搜尋多個檔案

				
ian@attic4:~/lpi103-7$ grep plum *
text2:9	plum
yaa:9	plum
ian@attic4:~/lpi103-7$ grep -n banana text[1-4]
text1:3:3 banana
text2:2:3	banana
ian@attic4:~/lpi103-7$ grep -c banana text[1-4]
text1:1
text2:1
text3:0
ian@attic4:~/lpi103-7$ grep -l pear *
text1
text1.bkp
xaa

檢視清單 8 中的 -c 選項,您會看到一行 text3:0。                 您經常需要知道某個內容在檔案中出現了多少次,但是不用知道沒有出現該內容的檔案。grep 命令有一個
-v 選項,它表示只顯示匹配的行輸出。因此,我們可以使用正規表示式 :0$ 查詢以逗號和 0 結尾的行。

下一個示例是使用 find 查詢當前目錄及其子目錄中的所有常規檔案,然後使用 xargs 將檔案列表傳遞到
grep,以確定每個檔案中出現 banana 的次數。最後,通過再一次呼叫 grep 篩選該輸出,這一次使用
-v 選項查詢所有以 :0 結尾的行,只用告訴我們包含字串 banana 的檔案計數。

清單 9. 查詢至少包含一次 banana 的檔案

				
ian@attic4:~/lpi103-7$ find . -type f -print0| xargs -0 grep -c banana| grep -v ":0$"
./backup/text1.bkp.2:1
./text2:1
./text1:1
./yaa:1
./xab:1
./text1.bkp:1


回頁首

正規表示式和                sed

文章 “學習 Linux,101:文字流和過濾器” 中介紹了 sed——流編輯器,其中提到 sed 使用正規表示式。regexps 可以在地址表示式和替代表示式中使用。

如果您需要查詢某內容,那麼只需要使用 grep。如果需要從匹配行中提取搜尋字串,或者相關字串,那麼需要進一步操作,您可以選擇使用
sed。讓我們解釋一下它的工作方式。首先回憶我們的兩個示例檔案,text1 和                text2,其中包含了一個數字,後跟空格,再加一個水果的名稱,而 text3 包含重複的語句。我們在清單 10 中再看一次它的內容。

清單 10. text1、text2 和 text3 的內容

				
ian@attic4:~/lpi103-7$ cat text[1-3]
1 apple
2 pear
3 banana
9	plum
3	banana
10	apple
This is a sentence.  This is a sentence.  This is a sentence. 

首先,我們將使用 grepsed 提取以一個或多個數字開頭,且後跟空白字元(空格或製表符)的行。一般情況下,sed 在一個週期結束時列印出每個行,因此我們使用 sed 的
-n 選項禁止輸出,然後使用 sed 中的 p 命令只列印匹配我們正規表示式的行。要確認我們對這兩個工具使用的正規表示式相同,我們將其賦予一個變數。

清單 11. 搜尋 grep 和 sed

				
ian@attic4:~/lpi103-7$ grep "$oursearch" text[1-3]
text1:1 apple
text1:2 pear
text1:3 banana
text2:9	plum
text2:3	banana
text2:10	apple
ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/p"
1 apple
2 pear
3 banana
9	plum
3	banana
10	apple

注意,grep 在搜尋到多個檔案時將顯示檔名稱。因為我們使用 cat 提供 sed 的輸出,所以
sed 無法知道原始檔名。但是,匹配行是相同的,正如我們期望的那樣。

現在假設我們只需要找到的行中的第二個字。在本例中是水果的名稱,但是我們需要查詢 HTTP URL 或者檔名等等其他內容。例如,刪除我們試圖匹配的字串就足夠了,如清單 12 所示。

清單 12. 使用 sed 刪除前導數字

				
ian@attic4:~/lpi103-7$ cat text[1-3] | sed -ne "/$oursearch/s/$oursearch//p"
apple
pear
banana
plum
banana
apple
            

對於最後一個示例,假設我們的行在水果名稱之後還有些內容。我們新增了一行 “lemon pie”,檢視如何只提取 lemon。我們將對輸出排序,放棄非唯一的值,因此我們得到一個找到的水果列表,每個水果只出現一次。

清單 13 展示了兩種實現同一個任務的方式。首先,我們剔除了前導數字以及後面的空格,然後剔除第一個空格或選項卡之後的所有內容,並列印剩下的內容。在第二個示例中,我們引入了圓括號將整個行分為 3 個部分,數字和後面的空格、第二個字以及其他內容。我們使用
s 命令將整個行替換為第二個字,然後列印結果。您可以嘗試變化一下方式,忽略第三部分,(.*),看看是否能解釋發生了什麼。

清單 13. 獲取水果名

				
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] |
> sed -ne "/$oursearch/s/($oursearch)([^[:blank:]]*)(.*)/2/p" |
> sort | uniq
apple
banana
lemon
pear

有些舊版本的 sed 不支援擴充套件的正規表示式。如果您的 sed 版本不支援擴充套件的 regexps,請使用
-r 選項告訴 sed 您使用的是擴充套件語法。清單 14 展示了要對 oursearch 變數和
sed 命令進行哪些更改才能讓擴充套件的正規表示式完成清單 13 中基本正規表示式完成的任務。

清單 14. 使用擴充套件的正規表示式和                sed

				
ian@attic4:~/lpi103-7$ echo "7 lemon pie" | cat - text[1-3] |
> sed -nre "/$oursearchx/s/($oursearchx)([^[:blank:]]*)(.*)/2/p" |
> sort | uniq
apple
banana
lemon
pear
plum
            

本文介紹了您可以使用正規表示式以及 grepsed 對 Linux 命令列執行的操作,這還只是冰山的一角。使用手冊瞭解更多有關這些高價值工具的資訊。

http://www.ibm.com/developerworks/cn/linux/l-lpic1-v3-103-7/


相關文章