1. 程式人生 > 實用技巧 >正則表示式詳解(上)

正則表示式詳解(上)

簡單來說,正則表示式是根據一定的語法規則組合而成的用來匹配具有某種模式的文字的字串。

維基百科定義如下:

正則表示式,又稱正規表示式正規表示法、正規運算式、規則運算式、常規表示法(英語:Regular Expression,在程式碼中常簡寫為 regex、regexp 或 RE),電腦科學的一個概念。正則表示式使用單個字串來描述、匹配一系列符合某個句法規則的字串。在很多文字編輯器裡,正則表示式通常被用來檢索、替換那些符合某個模式的文字。

正則表示式在臺灣又譯作正規表示式,英文名稱表示 “某種規則的表示式” 的意思,目前主流的文字編輯器(source insight/sublimtext/ultra edit/emacs/notepad++/vim) 和主流計算機語言

(perl/python/PHP/java/.NET/tcl/c/c++)都支援正則表示式。它簡單,優美,功能強大,妙用無窮。大資料時代的到來,因其快捷強大的文字處理能力必然在資料探勘處理中發揮越來越重要的作用。

正則表示式由一般字元和特殊字元組成,一般字元指常見的字元本身,比如123Aaf0=;~#@%這些簡單字元。通過簡單地排列組合這些字元可以實現對複雜字串的精確匹配。先放一張 python 的正則字元列表,下面逐一介紹。

  • 元字元

    除了一般字元之外,python 規定了.^$*+?{}[]()\|這 14 個元字元,它們分別具有特殊的含義,有的代表量詞,有的代表分組,有的代表邏輯等等(具體可參見上表)。正則表示式使用\

    作為轉義字元,例如 \ s 代表空格,使用 \ t 代表 tab 等,我們將轉義字元與其組合歸類到一般字元

    注意字符集[],它匹配內容是括號中的任何一個字元,比如[a0d]表示匹配 a 或者 0 或者 d,而不是匹配a00d或者a0d。關於字符集需要注意如下兩點:

  1. 字符集中的特殊字元都失去了它們本身代表的涵義,而成為符號自身,比如[a\.0]匹配dlkll\fd,因為其中有\
  2. 字符集前面最前面為^表示不匹配字符集中的任意字元,比如[^\da-f]表示不匹配數字及字母 a~f 中的任意字元。

與 python 原生的字串方法不同的是,正則表示式可以使用量詞,位置匹配,字元組合,分組捕獲等更強大的功能實現更復雜的字元處理功能。

  • 量詞
    特殊字元中的+*?三個字元是量詞字元,描述在它們之前緊挨著它們的字元連續重複的數量。比如量詞a{100}就代表 a 連續重複 100 次,而a+表示 a 出現至少一次,其他的具體的內容可參見表格。

  • 位置字元
    正則表示式不僅可以匹配字元也可以匹配位置,這些字元包括^$\b\A\Z\B,比如匹配以 Atom 開頭的行,就是^Atom,其他字元的具體含義可以參見表格。

  • 邏輯
    |表示或,所有的字元中優先順序最低,比如girl|boy表示匹配 girl 或者 boy,而不是girloy或者girboy

初識正則表示式

有了以上的基礎知識,就可以完成大部分簡單的正則表示式了,比如官方文件中的例子,嘗試用正則表示式a[bcd]*b去匹配abcbd。具體的匹配步驟如下表所示

Step Matched Explanation
1 a 正則表示式中的 a
2 abcbd 引擎匹配 [bcd]*,匹配儘可能多的字元直到目標字串結尾
3 失敗 引擎嘗試匹配 b,但是目前已經到了字串結尾,所以無字元匹配,失敗 /
4 abcb 回溯一個字元(即d),因此 [bcd]* 少匹配一個字元
5 失敗 再次嘗試匹配 b ,但是當前未匹配字元只有 'd',再次失敗
6 abc 再次回溯一個字元(即b), 因而 [bcd]*僅僅匹配bc
6 abcb 再次嘗試匹配 b 。 這次未匹配字元當前位置就是 'b',因此成功

從以上的匹配過程可以看到,正則表示式匹配過程就是引擎從左到右逐個搜尋目標字串,匹配正則表示式中所有字元代表的模式,如果搜尋完整個字串仍然沒有找到就失敗。推薦使用網站 regex101.com 選擇 python 語言,練習正則表示式。網站截圖如下所示,它用藍色底塊標識了表示式匹配的內容,直觀好用。

貪婪模式

上面的例子裡的第二步,*儘可能地匹配符合表示式的所有字元,引擎就像一個貪婪的胖子,一口氣吞下所有可以吞下去的東西,這就是正則表示式的貪婪模式。構造正則表示式時需要特別小心地處理具有相同屬性的量詞字元?{m,n}+,不然往往會產生意想不到的結果。

比如使用正則表示式<.*>嘗試匹配<a>b<c>的中的<a>,卻匹配了整個表示式。為了僅僅匹配<a>,需要在*後加上?,即使用<.*?>就能成功。二者的具體的區別如下表所示

表示式 步驟
<.*> 先匹配<,再匹配.*,此時吞下所有的符號,最後從最後一個字元開始,吐一個字元匹配一次>,直到成功為止
<.*?> 先匹配<,再匹配.*?,此時一個字元一個字元吞,每吞一個字元就立馬匹配>,直到成功為止

在量詞之後加上?表示非貪婪模式或者最小模式,吐到第三個字元為止,因此它找到了最小的<.*>的模式。

零寬字元

正則表示式中字元\b^$\A\Z\B|不佔有任何字元,但是定義字元的邊界,它們都是零寬 (zero-width) 字元。比如\b\w+\b表示匹配一個單詞,而其中的\b表示單詞的邊界。

分組捕獲

字元處理不僅僅需要判斷是否匹配,我們常常希望提取相關模式的字串,獲得對應的資訊,比如從網頁原始碼中提取郵件地址。此時就可以利用正則表示式的分組功能捕獲字元,上面表中的的 group 就可以抓取不同的分組字元。比如,需要從下面字元中提取郵件的發件人資訊

From: [email protected]
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: [email protected]

使用^From:\s*([\w@.]+)抓取資訊,其中有 2 個括號,就是分別分組捕獲發件人和日期。從左到右以(的出現順序為序,分別是第 1 個分組第 2 個分組依次類推,使用編號就可以重複對應括號分組的模式。

舉個例子,匹配類似abba的單詞,使用正則表示式\b([a-zA-Z])([a-zA-Z])\2\1\b,其中的 \ 1 和 \ 2 就分別表示與第 1 個和第 2 個分組相同的內容,依次類推。

如果分組很多,數字編號數數會很累,也可以使用(?P<name>...)命名,之後再使用(?P=name)引用,比如下面的程式碼

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

其中的<word>表示匹配的分組名字是word,使用 group() 方法使用名字即可呼叫這個分組內容。

零寬斷言

除此之外,還有如下 5 個特殊的分組匹配符號和正常的匹配符號相似,但是它們匹配...的表示式,卻不捕獲內容

  • (?:...)。非捕獲分組,表示匹配… 表示的表示式,但是它不捕獲內容,因此不能以\1<name>的方式被引用。比如

    >>> m = re.match("([abc])+", "abc")
    >>> m.groups()
    ('c',)
    >>> m = re.match("(?:[abc])+", "abc")
    >>> m.groups()
    ()
    

第二個表示式什麼都沒有匹配。

  • (?=...)。肯定正序環視 (Positive lookahead),跟在匹配字元之後,表示接下來匹配… 的字元,比如Isaac (?=Asimov)匹配後面跟著 Asimov 的 Isaac。

  • (?!...)。否定正序環視 (Negative lookahead) 與上面的意思剛好相反,表示不匹配…。

    舉個例子匹配形如foo.txt的檔名,但是要求檔案的副檔名不是bar,就可以使用.*[.](?!bar).*$匹配。

  • (?<=...)。肯定逆序環視 (Positive lookbehind) 跟在匹配字元之後,表示之前匹配… 的字元,比如(?<=abc)def匹配abcdef,表示之前為abcdef

  • (?<!...)。否定逆序環視 (Negative lookbehind) 表示之前不匹配…,與上一條意思剛好相反。

符號優先順序

另一個需要留心的問題是正則表示式的優先順序,它表示解讀正則表示式時對一般字元及字元組(用()括起來的一般字元的組合)的粘度,最低的是|(表中未列出),具體可參考如下的優先順序列表。

運算子 描述
\ 轉義符
(), (?