正規表示式是個強大的工具,定義了明確的定串比對規則,來幫助使用者把梳字串,在很多程式都有支援,如 Linux shell 的grep, awk, sed,vim的搜尋取代功能等, C語言的scanf其實也有支援,可惜正規表示法規則甚多,一時之間難以記全。

這篇主要記錄正規表示法的規則,整理自"Practical Programming in Tcl and Tk"第十一章,給自己做個參考:

一般字元與跳脫字元:

如果要比對字串,最簡單的規則就是:把字元寫出來,寫 a 就會比對出a,夠簡單吧。

但這樣就沒啥變化了,如果我要比對a, aa 或aaaaa…aa呢?
如果全部的東西都是直接比對,就少了這種彈性,所以正規表示式定義下例幾個字元為跳脫字元,平時做為特殊處理之用,要比對他們需要特殊規則,包括: .*+?()|[]^$\ 在比對中寫下這些符號的功能之後會一一介紹到,如果是要比對這些符號,就用backslash \ 跳脫。
例如要比對\,就需要寫\\而非只有\

字組(character sets)

如果我們要寫一個讓使用者輸入y/n的程式,使用者可能會填y/Y/n/N裡面一個,因此就有了字組的概念,字組利用跳脫字元[],讓某一個位置有多重選項:

  • [yY],則y或Y都可以match

[]裡面可以使用"[x-y]“的規則,來比對某區間的字,例如:

  • [0-9]比對所有數字
  • [a-z]比對小寫的英文

這裡要注意的是,隨著語系的選擇,[x-y]會有不同的範圍,不過一般都是以LANG=C的情況下去設計,也就是0-9A-Za-z的排序。

子規則1:反向用[^]

  • 比對"不是<",就用[^<]
  • 如果是[^a-z],就會排除所有小寫英文

子規則2:

比對跳脫字元除了 ] 需要在[]要排第一位、\仍要跳脫外,其他在[]就不用跳脫,直接寫就可以,例如:

  • 比對出所有的跳脫字符 [][.*+?()|\\]

另外則是 {} 這個直接寫就是直接match,但加上 \{,\} 後則是跳脫為特殊意思。

數量(quantifiers)比對與比對順序:

我們只要使用者輸入「o某字z」,他要orz, otz,omz 我才不管呢?
首先使用 . 任意一個字元,跟shell的不太一樣,這要小心。

上面的比對,我們可以寫 o.z 那針對不同的數量,我們就需要數量字符:

char quatity
* 0~∞次
+ 1~∞次
? 0~1次
{m,n} m~n次,其中n可以不填,表示∞

所以說,上面的比對可以用 a+ 完成。
重複的可以用()包起來,[]裡面的重複也OK,將數量字符直接加在下括號的後面即可。 比較重要的是 * 這個符號用起來要小心,這個比對表示任意字符重複0~n次,有時會造成不預期的結果。這就牽扯到正規表示法的比對規則了,如果一個match在字串中有多個相符的比對時,其規則如下:

  • 各相符比對,最接近開頭者優先
  • 如果是一樣的開頭,預設最長者優先,這個可以改換為nongreedy,即較短者優先

所以如果用 [a-z]* 去比對 123abc,一般可能會預期取出abc,但不對,在123前的開頭的空字串才是優先match的對象,一般都建議用+強制一定要有match來避免這樣的狀況。

字集(character classes)

字集為一些設定好的字元組合,寫法為:[:identifier:] 整理可選用的identifier如下:

identifier 比對 等同表示法
lower 英文小寫 [a-z]
upper 英文大寫 [A-Z]
alpha 英文字母 [A-Za-z]
digit 數字 [0-9]
xdigit 十六進位數字 [0-9a-fA-F]
alnum/print 以上總集合 [0-9a-zA-Z]
punct 標點符號例如 '"/<>之類
graph 非控制字符或空白類字符,其實就是以上總集合 我猜可以寫成[[:alnum:][:punct:]]
blank 空白和tab [ \t]
space 空白類字元:空白, \n, \r, tab, vertial tab, form feed 有人知道後兩個是蝦毀嗎… \s
cntrl 控制字符:ASCII 0~31
< > 分別比對單定開頭與結尾

定位字符(anchoring):

這部分兩個定位就是^和$,一行的開頭和結尾

反斜線跳脫:

利用反斜線的跳脫字符,可以match一些特殊的字符,主要分為四類:

  • 特殊符號類
  • 與字集相同的設定,可以match一個字集
  • 定位字符類
  • 數字指定類

整理如下:

特殊符號類:

表示法 比對 等同符號表示
\a Alert character
\b Backspace character \u0008
\e Escape character \u001B
\n Newline \u000A
\B Backslash \\
\0 NULL \u0000
\r Carriage return \u000D
\t Horizontal tab \u0009
\f Form feed \u000C
\v Vertical tab \u000B
\cx Control-x

字集類:

表示法 比對 等同表示法
\d 數字 [[:digit:]]
\D 非數字 [^[:digit:]]
\s 空白類字符 [[:space:]]
\S 非空白類字符 [^[:space:]]
\w 字母+數字+底線 [[:alnum:]]
\W 非以上組合 [^[:alnum:]]

字位字符類:

表示法 比對
\A 字串開頭
\Z 字串結尾
\m 單字開頭 [[:<:]]
\M 單字結尾 [[:>:]]
\y 單字開頭或單字結尾
\Y 非\y

數字指定類:

表示法 比對
\unnnn 16-bits Unicode character code
\Unnnnnnnn 32-bits Unicode character code
\xhh Consumes all hex digits after \x, 8-bit hexadecimal character code
\x, \xy, \xyz xyz為數字,這個可以是back reference,或者是8-bit octal character code

Nongreedy(非貪婪?這怎麼翻(yay)):

之前提過RE的規則是愈長愈好,設定Nongreedy即是變成愈短愈好,使用的是? 加在第三項的數量(quantifiers)比對後面。
比如說我們要match整個輸入中的第一行,如果寫: .+\n 這樣會match整個輸入,直到最後一個\n符號,其中包括很多很多換行符號。 .+?\n 設定+為nongreedy,則遇到第一個\n就會結束比對

或者我們要抓中間的文字: <title>(.*?)</title>

所有的數量比對加?在後面都會變成non-greedy,??代表0次優先,{1,3}?則會以1,2,3的順序去match。

Back reference:

這個規則比較重在要把內容存到暫存裡的狀況,一般的狀況下似乎不太用到。

在正規表示式中除了直接match的結果外,還可以用小括號()來記錄子比對的結果:例如(.*?),除了match整段文字外,可以取出兩個tag中間的文字內容。
要取出這個這個比對結果,可以用Back reference,使用的方式是 \1, \2, \3,依數字對應到各比對結果,比對結果的排序為"外到內,左到右”,如下所示:
(1(2))(3),注意 \1 是包括自己跟 \2 的內容。

look ahead:

另一方面,如果我們只想要比對,卻懶得管一些比對的內容呢?
這就需要look ahead,我想到最需要誕個功能的地方是rename,我要改掉所有txt檔,卻只要取出檔名的部分,這時候就需要look ahead。
例如我要取出副檔名為.txt的文件名稱:
^.*\.txt$,可以,但記錄下來的是:lalala.txt

  • .txt是不需要記錄的 positive look ahead:^.*(?=\.txt)$,記錄的就只有lalala
  • negative look ahead:^.*(?!\.txt)$,這反過來,比對出非.txt結尾的檔案名稱。

還有一個是(?:pattern),同樣也是不記錄pattern中的內容,但跟look-ahead的差異點在哪我就不是很確定了。

其實主要的功能就上述這些,我提的那本書還有兩個功能,一個是collating element [.identifier.],只是書裡沒說得很清楚,我也還沒試出幹什麼用的…; 另一個是Equivalent class [=char=],只有比對Unicode例如 [=o=]可以比對 o, ō, ŏ,但平常好像也用不到。

如果是有關C語言中,有關scanf的部分,可以參考scanf的整理,一些基本的正規表示法語法在scanf中其實是有支援的,請見:
scanf整理