由於我都是在python上聯系和使用的,所以后面的問題基本都是在python上提出來的,所以這本書中的其它正則流派我均不涉及。依書中,python和perl風格差不多,屬於傳統NFA引擎,也就是以“表達式主導“,采用回溯機制,匹配到即停止( 順序敏感,不同於POSIX NFA等采用匹配最左最長的結果)。
對於回溯部分,以及談及匹配的時候,將引擎的位置總是放在字符和字符之間,而不是字符本身。比如^對應的是第一個字符之前的那個”空白“位置。
基礎規則的介紹
python中的轉義符號干擾
python中,命令行和腳本等,里面都會對轉義符號做處理,此時的字符串會和正則表達式的引擎產生沖突。即在python中字符串'\n'會被認為是換行符號,這樣的話傳入到re模塊中時便不再是‘\n’這字面上的兩個符號,而是一個換行符。所以,我們在傳入到正則引擎時,必須讓引擎單純的認為是一個'\'和一個'n',所以需要加上轉義符成為'\\n',針對這個情況,python中使用raw_input方式,在字符串前加上r,使字符串中的轉義符不再特殊處理(即python中不處理,統統丟給正則引擎來處理),那么換行符就是r'\n'
基本字符
. #普通模式下,匹配除換行符外的任意字符。(指定DOTALL標記以匹配所有字符)
量詞限定符
* #匹配前面的對象0個或多個。千萬不要忽略這里的0的情況。 + #匹配前面的對象1個或多個。這里面的重點是至少有一個。 ? #匹配前面的對象0個或1個。 {m} #匹配前面的對象m次 {m,n} #匹配前面的對象最少m次,最多n次。
錨點符
^ #匹配字符串開頭位置,MULTILINE標記下,可以匹配任何\n之后的位置 $ #匹配字符串結束位置,MULTILINE標記下,可以匹配任何\n之前的位置
正則引擎內部的轉義符號
\m m是數字,所謂的反向引用,即引用前面捕獲型括號內的匹配的對象。數字是對應的括號順序。 \A 只匹配字符串開頭 \b 可以理解一個錨點的符號,此符號匹配的是單詞的邊界。這其中的word定義為連續的字母,數字和下划線。 准確的來說,\b的位置是在\w和\W的交界處,當然還有字符串開始結束和\w之間。 \B 和\b對應,本身匹配空字符,但是其位置是在非"邊界"情況下. 比如r'py\B'可以匹配'python',但不能匹配'py,','py.' 等等 \d 匹配數字 \D 匹配非數字 \s 未指定UNICODE和LOCALE標記時,等同於[ \t\n\r\f\v](注意\t之前是一個空格,表示也匹配空格) \S 與\s相反 \w 未指定UNICODE和LOCALE標記時,等同於[a-zA-Z0-9_] \W 和\w相反 \Z 只匹配字符串結尾 其他的一些python支持的轉移符號也都有支持,如前面的'\t'
字符集
[]
尤其注意,這個字符集最終 只匹配一個字符(既不是空,也不是一個以上!),所以前面的一些量詞限定符,在這里失去了原有的意義。
另外,'-'符號放在兩個字符之間的時候,表示ASCII字符之間的所有字符,如[0-9],表示0到9.
而放在字符集開頭或者結尾,或者被'\'轉義時候,則只是表示特指'-'這個符號
最后,當在開頭的地方使用'^',表示排除型字符組.
括號的相關內容
普通型括號
(...) 普通捕獲型括號,可以被\number引用。
擴展型括號
(?aiLmsx) a re.A i re.I #忽略大小寫 L re.L m re.M s re.S #點號匹配包括換行符 x re.X #可以多行寫表達式 如: re_lx = re.compile(r'(?iS)\d+$') re_lx = re.compile(r'\d+',re.I|re.S) #這兩個編譯表達式等價 (?:......) #非捕獲型括號,此括號不記錄捕獲內容,可節省空間 (?P<name>...) #此捕獲型括號可以使用name來調用,而不必依賴數字。使用(?P=name)調用。 (?#...) #注釋型括號,此括號完全被忽略 (?=...) #positive lookahead assertion 如果后面是括號中的,則匹配成功 (?!...) #negative lookahead assertion 如果后面不是括號中的,則匹配成功 (?<=...) #positive lookbehind assertion 如果前面是括號中的,則匹配成功 (?<!...) #negative lookbehind assertion 如果前面不是括號中的,則匹配成功 #以上四種類型的斷言,本身均不匹配內容,只是告知正則引擎是否開始匹配或者停止。 #另外在后兩種后項斷言中,必須為定長斷言。 (?(id/name)yes-pattern|no-pattern) #如有由id或者name指定的組存在的話,將會匹配yes-pattern,否則將會匹配no-pattern,通常情況下no-pattern可以省略。
匹配優先/忽略優先符號
如'??'會先匹配沒有的情況,然后才是1個對象的情況。而{m,n}?則是優先匹配m個對象,而不是占多的n個對象。
相關進階知識
python屬於perl風格,屬於傳統型NFA引擎,與此相對的是POSIX NFA和DFA等引擎。所以大部分討論都針對傳統型NFA
傳統型NFA中的順序問題
NFA是基於表達式主導的引擎,同時,傳統型NFA引擎會在找到第一個符合匹配的情況下立即停止:即得到匹配之后就停止引擎。
而POSIX NFA 中不會立刻停止,會在所有可能匹配的結果中尋求最長結果。這也是有些bug在傳統型NFA中不會出現,但是放到后者中,會暴露出來。
引申一點,NFA學名為”非確定型有窮自動機“,DFA學名為”確定型有窮自動機“
這里的非確定和確定均是對被匹配的目標文本中的字符來說的,在NFA中,每個字符在一次匹配中即使被檢測通過,也不能確定他是否真正通過,因為NFA中會出現回溯!甚至不止一兩次。圖例見后面例子。而在DFA中,由於是目標文本主導,所有對象字符只檢測一遍,到文本結束后,過就是過,不過就不過。這也就是”確定“這個說法的原因。
回溯/備用狀態
備用狀態
回溯機制兩個要點
- 在正則引擎選擇進行嘗試還是跳過嘗試時,匹配優先量詞和忽略優先量詞會控制其行為。
- 匹配失敗時,回溯需要返回到上一個備用狀態,原則是后進先出(后生成的狀態首先被回溯到)

作為對比說明,下面是目標文本不能匹配時,引擎走過的路徑:
如下圖,我們看到此時POSIX NFA和傳統型NFA的匹配路徑是一致的。

以上的例子引發了一個匹配時的思考,很多時候我們應該盡量避免使用'.*' ,因為其總是可以匹配到最末或者行尾,浪費資源。
既然我們只尋求引號之間的數據,往往可以借助排除型數組來完成工作。
此例中,使用'[^'']*'這個來代替'.*'的作用顯而易見,我們只匹配非引號的內容,那么遇到第一個引號即可退出*號控制權。
固化分組思想
'\w+:'
這個表達式在進行匹配時的流程是這樣的,會優先去匹配所有的符合\w的字符,假如字符串的末尾沒有':',即匹配沒有找到冒號,此時觸發回溯機制,他會迫使前面的\w+釋放字符,並且在交還的字符中重新嘗試與':'作比對。
但是問題出現在這里: \w是不包含冒號的,顯然無論如何都不會匹配成功,可是依照回溯機制,引擎還是得硬着頭皮往前找,這就是對資源的浪費。
所以我們就需要避免這種回溯,對此的方法就是將前面匹配到的內容固化,不令其存儲備用狀態!,那么引擎就會因為沒有備用狀態可用而只得結束匹配過程。大大減少回溯的次數!
Python模擬固化過程
雖然python中不支持,但書中提供了利用前向斷言來模擬固化過程。
(?=(...))\1
本身, 斷言表達式中的結果是不會保存備用狀態的,而且他也不匹配具體字符,但是通過巧妙的 添加一個捕獲型括號來反向引用這個結果,就達到了固化分組的效果!對應上面的例子則是:
'(?=(\w+))\1:'
多選結構
多選結構在傳統型NFA中, 既不是匹配優先也不是忽略優先。而是按照順序進行的。所以有如下的利用方式
- 在結果保證正確的情況下,應該優先的去匹配更可能出現的結果。將可能性大的分支盡可能放在靠前。
- 不能濫用多選結構,因為當匹配到多選結構時,緩存會記錄下相應數目的備用狀態。舉例子:[abcdef]和‘a|b|c|d|e|f’這兩個表達式,雖然都能完成你的某個目的,但是盡量選擇字符型數組,因為后者會在每次比較時建立6個備用狀態,浪費資源。
一些優化的理念和技巧
平衡法則
- 只匹配期望的文本,排除不期望的文本。(善於使用非捕獲型括號,節省資源)
- 必須易於控制和理解。避免寫成天書。。
- 使用NFA引擎,必須要保證效率(如果能夠匹配,必須很快地返回匹配結果,如果不能匹配,應該在盡可能短的時間內報告匹配失敗。)
處理不期望的匹配
在處理過程中,我們總是習慣於使用星號等非硬性規定的量詞(其實是個不好的習慣),
這樣的結果可能導致我們使用的匹配表達式中沒有必須匹配的字符,例子如下:
'[0-9]?[^*]*\d*' #只是舉個例子,沒有實際意義。
上面的式子就是這種情況,在目標文本是“理想”時,可能出現不了什么問題,但是如果本身數據有問題。那么這個式子的匹配結果就完全不可預知。
原因就在於他沒有一部分是必須的!它匹配任何內容都是成功的。。。
對數據的了解和假設
引擎中一般存在的優化項
編譯緩存
其他技巧和補充內容
過度回溯問題
消除指數級匹配
(\w+)*
這種情況的表達式,在匹配長文本的時候會遇到什么問題呢,如果在文本匹配失敗時(別忘了,如果失敗,則說明已經回溯了 所有的可能),想象一下,*號退一個狀態,里面的+號就包括其余的 所有狀態,驗證都失敗后,回到外面,*號 退到倒數第二個備用狀態,再進到括號內,+號又要回溯一邊比上一輪差1的 備用狀態數,當字符串很長時, 就會出現指數級的回溯總數。系統就會'卡死'。甚至當有匹配時,這個匹配藏在回溯總數的中間時,也是會造成卡死的情況。所以,使用NFA的引擎時,必須要注意這個問題!
我們采用如下思路去避免這個問題:
占有優先量詞(python中使用前向斷言加反向引用模擬)
道理很簡單,既然龐大的回溯數量都是被儲存的備用狀態導致的,那么我們直接使引擎放棄這些狀態。說到底是擺脫(regex*)* 這種形式。
import re re_lx = re.compile(r'(?=(\w+))\1*\d')
效率測試代碼
在測試表達式的效率時,可借助以下代碼比較所需時間。在兩個可能的結果中擇期優者。
import re import time re_lx1 = re.compile(r'your_re_1') re_lx2 = re.compile(r'your_re_2') starttime = time.time() repeat_time = 100 for i in range(repeat_time): s='test text'*10000 result = re_lx1.search(s) time1 = time.time()-starttime print(time1) starttime = time.time() for i in range(repeat_time): s='test text'*10000 result = re_lx2.search(s) time2 = time.time()-starttime print(time2)
量詞等價轉換
綜上,python中總體上使用量詞不如簡單的列出來!(與書中不同!)
錨點優化的利用
下面這個例子假設出現匹配的內容在字符串對象的結尾,那么下面的第一個表達式是快於第二個表達式的,原因在於前者有錨點的優勢。
re_lx1 = re.compile(r'\d{5}$') re_lx2 = re.compile(r'\d{5}') #前者快,有錨點優化
排除型數組的利用
繼續,假設我們要匹配一段字符串中的5位數字,會有如下兩個表達式供選擇:
經過分析,我們發現\w是包含\d的,當使用匹配優先時,前面的\w會包含數字,之所以能匹配成功,或者確定失敗,是后面的\d迫使前面的量詞交還一些字符。
知道這一點,我們應該盡量避免回溯,一個順其自然的想法就是不讓前面的匹配優先量詞涉及到\d
re_lx1 = re.compile(r'^\w+(\d{5})') re_lx2 = re.compile(r'^[^\d]+\d{5}') #優於上面的表達式
總體來說,在我們沒有時間去深入研究模塊代碼的時候,只能通過嘗試和反復修改來得到最終的復合預期的表達式。
常識優化措施
“取巧”的修改又可能會關閉或者避開了這些優化,所以結果也許會令我們很失望。
避免重新編譯(循環外創建對象)
使用非捕獲型括號(節省捕獲時間和回溯時狀態的數量)
善用錨點符號
不濫用字符組
提取文本和錨點。將他們從可能的多選分支結構中提取出來,會提取速度。
最可能的匹配表達式放在多選分支前面
一個很好用的核心公式
- special部分和normal部分匹配的開頭不能重合。一定保證這兩部分在任何情況下不能匹配相同的內容,不然在無法出現匹配時遍歷所有情況,此時引擎的路徑就不能確定。
- normal部分必須匹配至少一個字符
- special部分必須是固定長度的
舉個例子:
[^\\"]+(\\.[^\\"]+)* #匹配兩個引號內的文本,但是不包括被轉義的引號
from:http://www.codeweblog.com/python%E4%B8%8B%E7%9A%84%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%8E%9F%E7%90%86%E5%92%8C%E4%BC%98%E5%8C%96%E7%AC%94%E8%AE%B0/