正規運算式
內容摘要:
正規運算式 (regular expression) 被應用於本文搜尋, 及文句修改的進階場合上。
您可以在許多進階的編輯器、 語法分析程式、 以及程式語言中, 發現他們的蹤影。
介紹
許多進階的編輯器, 像是 vi 和 emacs, 工具程式 grep/egrep, 以及程式語言
, 像是 awk、 perl 和 sed, 都可以發現正規運算式的蹤影。
正規表示式被應用於本文搜尋, 及文句修改的進階場合上。
正規運算式就是某種樣式的制式描述, 透過該樣式, 可以比對到文章中的一段文字。
記得多年以前, 看到某人在使用正規運算式, 我當時大感神奇。
通常要花費數小時的時間, 才能完成文章編輯以及搜尋的工作,
居然可以在幾秒內就全部解決。 但是, 當我注視螢幕的運算式時,
卻連一個字也看不懂, 它們看起來像是一些點、 斜線、 星號,
以及其他字元, 所湊起來的奇怪組合。 雖然如此, 我仍舊下定決心,
要去暸解它的工作原理, 很快地, 我發現它們其實是非常容易使用的,
有一些基本的規則可循。
雖然正規運算式在 Unix 的世界中, 運用地相當廣泛, 但是還未出現過,
所謂的「標準正規運算式」, 它的情況, 很像是有許多不同的方言存在一般。
例如有二種 grep 程式: grep 與 egrep, 它們都有使用正規運算式,
只是能力稍微不同而已。 而程式語言 Perl 的正規運算式, 功能可說是最完整的了。
可喜的是, 他們都遵循相同的原則, 一旦您暸解基本觀念後,
再去學習其他方言的細微差異處, 是很容易的。
本文將為各位介紹一些基本的觀念, 同時您也可以查看不同程式的操作說明文件,
以便暸解其相異之處, 以及功能所在。
一個簡單的例子
譬如說, 您有一份公司員工的電話名單, 它看起來像是這樣:
Phone Name ID
...
...
3412 Bob 123
3834 Jonny 333
1248 Kate 634
1423 Tony 567
2567 Peter 435
3567 Alice 535
1548 Kerry 534
...
這是一個 500 人的公司, 他們把員工資料以一般的純文字檔案加以儲存。
電話號碼中, 第一位數字為 1 的人, 表示他在 1 號大廈工作。
那麼到底誰在 1 號大廈工作呢?
正規運算式可以回答這個問題:
grep '^1' phonelist.txt
或
egrep '^1' phonelist.txt
或
perl -ne 'print if (/^1/)' phonelist.txt
它的意思是說, 搜尋檔案的每一行, 看看是否有以 1 作該行開頭的。
符號 "^" 表示要比對每一行的開頭, 它強制整個正規運算式,
只比對匹配到, 以 1 作第一個字元的每一行。
語法規則
單一字元樣式
正規運算式的基本構件, 就是單一字元樣式, 它只比對匹配這個字元。
前面的例子中, 1 就是個單一字元樣式, 她只比對匹配文章中, 1 這個字元。
另一個單一字元樣式的實例是:
egrep 'Kerry' phonelist.txt
這個比對樣式, 其實還是僅由單一字元樣式所組成 (字母 K,e ...)
數個字元可以聚集起來, 放在字元集合中, 這樣的集合,
是由一對左、 右方括號, 以及在這對方括號之間的一串字元, 來做為表示。
整個字元集合, 其實也被視為單一字元樣式, 在進行樣式比對時,
這些字元集合裡, 會有一個, 而且只有一個字元, 出現在所搜尋匹配的文句中。
例如:
[abc] 是個單一字元樣式, 用來比對匹配文章裡
a、 b 或 c 當中之任一個字母
[ab0-9] 是個單一字元樣式, 用來比對匹配文章裡
a 或 b 或一個 ascii 字元集中,
範圍在 0 到 9 的數字
[a-zA-Z0-9\-] 用來比對匹配某個單一字元為
大寫或小寫字母、 阿拉伯數字或減號
讓我們試著執行:
egrep '^1[348]' phonelist.txt
這樣會搜尋到以 13 或 14 或 18 開頭的每一行。
如我們所看到的, 大部分的 ASCII 字元, 就是按照其字元原本的樣子來比對,
但是有些 ASCII 字元, 卻有特殊的含意。 例如方括號,
表示開始指定一個字元集合, 在字元集合中, "-"
的特殊含意就是「範圍」。 要取消特定字元的特殊含意, 您可以在該字元前面,
加上反斜線字元。 在 [a-zA-Z0-9\-] 中的減號, 就是一個實例。
也有某些正規運算式的「方言」, 在特定字元前面加上反斜線字元後,
會有特殊含意, 此時, 你得將前面的反斜線字元拿掉, 才有原本正常的意思。
點字元 (.) 是一個重要的特殊字元, 除了換行字元外,
它會比對匹配到任何一個字元。 例如:
grep '^.2' phonelist.txt
或
egrep '^.2' phonelist.txt
這樣會搜尋匹配到, 第一個字元為任何字元, 而且第二個字元為 2 的每一行。
如果使用 "[^" 而不是 "[" 來定義字元集合,
則其所代表的含意, 與原來完全相反。 在 "[" 與 "^"
組合之後, "^" 的意義不再是代表每行的開頭,
而是代表字元集合的反義。
[0-9] 是個單一字元樣式, 會比對匹配到文章裡
一個 ascii 字元集中, 範圍在 0 到 9 的數字
[^0-9] 會比對匹配到任何一個非阿拉伯數字的字元
[^abc] 會比對匹配到任何一個非 a、 b 或 c 的字元
. 點字元會比對匹配到任何一個非換行字元的字元
它的意義與 [^\n] 相同, 其中 \n 是換行字元
要比對匹配到「不是以 1 開頭的每一行文字」, 我們可以這樣寫:
grep '^[^1]' phonelist.txt
或
egrep '^[^1]' phonelist.txt
定位字元
在前面的部份, 我們已經知道 "^" 是用來比對匹配到文章的每一行開頭。
定位字元是一種特別的正規運算式字元, 它被用來比對匹配到文章的某個位置,
而不是文章的所有字元。
^ 會比對匹配文章裡的每一行開頭
$ 會比對匹配文章裡的每一行結尾
想要在公司的員工電話名單中, 尋找 ID 編號為 567 的人, 我們可以這樣做:
egrep '567$' phonelist.txt
這樣子會尋找到「以數字 567 結尾」的每一行。
倍數字元
倍數字元用來決定, 單一字元樣式在文章中必須出現幾次。
說明 |
grep |
egrep |
perl |
vi |
vim |
vile |
elvis |
emacs |
零或多次 |
* |
* |
* |
* |
* |
* |
* |
* |
一或多次 |
\{1,\} |
+ |
+ |
|
\+ |
\+ |
\+ |
+ |
零或一次 |
\? |
? |
? |
|
\= |
\? |
\= |
? |
n 到 m 次 |
\{n,m\} |
|
{n,m} |
|
|
|
\{n,m\} |
\{n,m\} |
注意事項: 各類 VI 編輯程式要與上表的動作一致有 magic 設定選項可用.
舉一個電話名單的例子:
....
1248 Kate 634
....
1548 Kerry 534
....
要比對匹配以 1 開頭, 接著有一些阿拉伯數字, 然後
至少有一個以上的間隔字元以及人名是以 K 開始的每一行
我們可以這樣寫:
grep '^1[0-9]\{1,\} \{1,\}K' phonelist.txt
或使用 * 來重複比對匹配 [0-9] 與間隔字元:
grep '^1[0-9][0-9]* *K' phonelist.txt
或
egrep '^1[0-9]+ +K' phonelist.txt
或
perl -ne 'print if (/^1[0-9]+ +K/)' phonelist.txt
倍數字元會將前面的單一字元樣式出現的次數加倍.
所以 "23*4" 不表示說她會比對匹配 " 2 然後 3 任何字元 4"
(這其實是 "23.*4" 的含義). 她會比對匹配 " 2 一次 然後 3
可能許多次以及 4 一次"
還有一個你也須要注意的重點就是這些倍數字元很貪心.
也就是說在比對樣式中第一個出現的倍數字元會盡可能地向
每行文字的右邊延伸所要匹配的字元.
運算式 ^1.*4
將會比對匹配到一整行
1548 Kerry 534
從開頭一直到最後面的 4.
但是她就是不會只匹配到 154.
這個特性對 grep 工具程式並沒有什麼影響但是對文字的編輯與代換
卻非常重要.
括號字元有
記憶能力
括號字元有記憶能力的機制並沒有改變運算式的比對方法但是
她卻能夠將包含在括號的樣式所匹配到的文字部分記憶起來,所以
在運算式的後面可以再參考到她.
這個被記憶的內容可以經由變數來取得. 第一組括號字元所記憶的內容
可以經由變數一來取得, 第二組括號字元所記憶的內容可以經由變數二來取得
依此類推.
程式名稱 | 括號字元語法 | 變數語法 |
grep | \(\) | \1 |
egrep | () | \1 |
perl | () | \1 or ${1} |
vi,vim,vile,elvis | \(\) | \1 |
emacs | \(\) | \1 |
例如:
運算式 [a-z][a-z] 將會
比對匹配二個小寫字母.
現在我們可以使用變數來搜尋比對像是 'otto' 這類的文字:
egrep '([a-z])([a-z])\2\1'
變數 \1 的內容是字母 o
而變數 \2 的內容是字母 t.
這個運算式也能夠比對匹配到 anna 這個名字
但是 yxyx 就不可以了.
在尋找像是 otto 和 anna 這類名字時並不常用到括號字元的記憶能力
倒是文字的編輯與代換時會常用到她的記憶能力.
應用正規運算式的能力來編輯文字
想在編輯文字時借用正規運算式的能力你需要一個像是 vi, emacs 之類的編輯器
或者你也可以使用像是 perl 之類的程式語言.
在 emacs 編輯器中你可以使用 M-x 來下 query-replace-regexp 命令
或是將 query-replace-regexp 命令設定在一些功能鍵上. 亦或是你也可以
使用 replace-regexp 命令. 不過 query-replace-regexp 是採交談的方式,
而後者則否.
在 vi 編輯器中文字的代換則是使用 :%s/ / /gc 這樣的命令. 其中
百分比字元是指搜尋 '整個檔案' 的範圍然而你也可以取代成你想要的
範圍. 在 vim 編輯器中你可以按下 shift-v 鍵來標示一個區域然後你
可以只在標示的區域上使用代換文字的命令. 在這裡我並不對 vim 的
使用多作說明你可以參考該編輯器所附的自習指導文件. 而代換命令中
的 'gc' 是指採用交談的方式. 不使用交談的方式可以下 s/ / /g 這樣
的命令.
所謂交談的方式是指在每次比對匹配到時編輯器會提示詢問你
是否要執行代換文字的動作.
在 perl 程式語言中你可以使用
perl -pe 's/ / /g'
讓我們看一點例子. 例如現在我們公司的編號計劃已經做了改變
就是凡電話號碼是 1 開頭的人在第二位阿拉伯數字之後都會插入一
個 2 .
也就是說如果原先電話號碼是 1423 則應該變成 14223.
舊的名單:
Phone Name ID
...
3412 Bob 123
3834 Jonny 333
1248 Kate 634
1423 Tony 567
2567 Peter 435
3567 Alice 535
1548 Kerry 534
...
這裡是各種處理的方法:
vi: s/^\(1.\)/\12/g
emacs: ^\(1.\) replaced by \12
perl: perl -pe 's/^(1.)/${1}2/g' phonelist.txt
現在新的電話名單看起來像是這樣:
Phone Name ID
...
3412 Bob 123
3834 Jonny 333
12248 Kate 634
14223 Tony 567
2567 Peter 435
3567 Alice 535
15248 Kerry 534
...
Perl 程式語言可以處理超過 \1 到 \9 的記憶變數因此 \12 將會指到
第 12 個記憶變數此時這個變數的內容當然是空的. 要解決這個問題我們
可以使用 ${1} 的方式.
現在名單的排列方式有點不整齊. 要如何修正他?你可以這樣子試試看
如果在第五個位置是空白字元你就再插入一個空白字元:
vi: s/^\(....\) /\1 /g
emacs: '^\(....\) ' replaced by '\1 '
perl: perl -pe 's/^(....) /${1} /g' phonelist.txt
現在電話名單看起來像是這樣
Phone Name ID
...
3412 Bob 123
3834 Jonny 333
12248 Kate 634
14223 Tony 567
2567 Peter 435
3567 Alice 535
15248 Kerry 534
...
現在有同事以人工的方式編輯這個名單並且偶爾會在文字行的開頭
插入一些空白字元. 我們要如何作才能夠將之除去?
Phone Name ID
...
3412 Bob 123
3834 Jonny 333
12248 Kate 634
14223 Tony 567
2567 Peter 435
3567 Alice 535
15248 Kerry 534
...
這個問題應該如此解決:
vi: s/^ *// (當我們不使用 + 時必須使用 2 個空白字元)
emacs: '^ +' replaced by the empty string
perl: perl -pe 's/^ +//' phonelist.txt
當你在撰寫程式時使用了 temp 與 temporary 二個變數. 現在你想要
將變數 temp 的變數名稱改為 counter. 如果你所作的只是代換 temp
這個字串則變數 temporary 會變成 counterorary 這當然不是你想要的
結果.
正規運算式可以解決這個問題. 你只要將 temp([^o]) 代換成 counter\1
即可. 也就是說, 只代換 temp 並且不含字母 o 的字串. (另一個解決方式
就是使用邊界字元但是我們並不在這類的定位字元樣式中討論她.)
我希望這篇文章能夠激發起你對正規運算式的興趣. 現在你可能必須去
看一下你所使用編輯器的操作說明與文件以便仔細暸解其正規運算式的功能.
還有許多特殊字元未能提到, 像是交替字元" | "
他具有 "或者" 的意思以及前面所說的邊界字元.
最後敬祝各位愉快, 並能夠用得充滿樂趣。
|