Python下的正則表達式原理和優化筆記

        最近的時間內對正則表達式進行了一點點學習。所選教材是《mastering regular expressions》,也就是所謂的《精通正則表達式》。讀過一遍後,頓感正則表達式的強大和精湛之處。其中前三章是對正則表達式的基本規則的介紹和鋪墊。七章之後是對在具體語言下的應用。而核心的部分則是四五六這三章節。
        其中第四章是講了整個正則表達式的精華,即傳統引擎NFA的回溯思想。第五章是一些例子下對回溯思想的理解。第六章則是對效率上的研究。根源也是在回溯思想上的引伸和研究。
        這篇文章是我結合python官方re模塊的文檔以及這本書作一個相應的總結。
        其中官方的文檔: http://docs.python.org/3.3/library/re.html
        因爲我都是在python上聯繫和使用的,因此後面的問題基本都是在python上提出來的,因此這本書中的其它正則流派我均不涉及。依書中,python和perl風格差很少,屬於傳統NFA引擎,也就是以「表達式主導「,採用回溯機制,匹配到即中止( 順序敏感,不一樣於POSIX NFA等採用匹配最左最長的結果)。
        對於回溯部分,以及談及匹配的時候,將引擎的位置老是放在字符和字符之間,而不是字符自己。好比^對應的是第一個字符以前的那個」空白「位置。

基礎規則的介紹

python中的轉義符號干擾

        python中,命令行和腳本等,裏面都會對轉義符號作處理,此時的字符串會和正則表達式的引擎產生衝突。即在python中字符串'\n'會被認爲是換行符號,這樣的話傳入到re模塊中時便再也不是‘\n’這字面上的兩個符號,而是一個換行符。因此,咱們在傳入到正則引擎時,必須讓引擎單純的認爲是一個'\'和一個'n',因此須要加上轉義符成爲'\\n',針對這個狀況,python中使用raw_input方式,在字符串前加上r,使字符串中的轉義符再也不特殊處理(即python中不處理,通通丟給正則引擎來處理),那麼換行符就是r'\n' html

基本字符

.        #普通模式下,匹配除換行符外的任意字符。(指定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.
而放在字符集開頭或者結尾,或者被'\'轉義時候,則只是表示特指'-'這個符號
最後,當在開頭的地方使用'^',表示排除型字符組. python

括號的相關內容

普通型括號

(...)    普通捕獲型括號,能夠被\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

express

傳統型NFA中的順序問題

NFA是基於表達式主導的引擎,同時,傳統型NFA引擎會在找到第一個符合匹配的狀況下當即中止:即獲得匹配以後就中止引擎。
而POSIX NFA 中不會馬上中止,會在全部可能匹配的結果中尋求最長結果。這也是有些bug在傳統型NFA中不會出現,可是放到後者中,會暴露出來。
引伸一點,NFA學名爲」非肯定型有窮自動機「,DFA學名爲」肯定型有窮自動機「
這裏的非肯定和肯定均是對被匹配的目標文本中的字符來講的,在NFA中,每一個字符在一次匹配中即便被檢測經過,也不能肯定他是否真正經過,由於NFA中會出現回溯!甚至不止一兩次。圖例見後面例子。而在DFA中,因爲是目標文本主導,全部對象字符只檢測一遍,到文本結束後,過就是過,不過就不過。這也就是」肯定「這個說法的緣由。 數組

回溯/備用狀態

備用狀態

當出現可選分支時,會將其餘的選項存儲起來,做爲備用狀態。當前的匹配失敗時,引擎進行回溯,則會回到最近的備用狀態。
匹配的狀況中,匹配優先與忽略優先某種意義上是一致的,只是順序上有所區別。當存在多個匹配時,兩種方式進行的狀況極可能是不一樣的,可是當不存在匹配時,他們倆的狀況是一致的,即必然嘗試了全部的可能。

回溯機制兩個要點

  1. 在正則引擎選擇進行嘗試仍是跳過嘗試時,匹配優先量詞和忽略優先量詞會控制其行爲。
  2. 匹配失敗時,回溯須要返回到上一個備用狀態,原則是後進先出(後生成的狀態首先被回溯到)
回溯典型舉例:

上圖能夠看到,傳統型NFA到D點即匹配結束。而在陰影中POSIX NFA的匹配流程,須要找到全部結果, 並在這些結果中取最長的結果返回

做爲對比說明,下面是目標文本不能匹配時,引擎走過的路徑:
以下圖,咱們看到此時POSIX NFA和傳統型NFA的匹配路徑是一致的緩存



以上的例子引起了一個匹配時的思考,不少時候咱們應該儘可能避免使用'.*' ,由於其老是能夠匹配到最末或者行尾,浪費資源。
既然咱們只尋求引號之間的數據,每每能夠藉助排除型數組來完成工做。
此例中,使用'[^'']*'這個來代替'.*'的做用顯而易見,咱們只匹配非引號的內容,那麼遇到第一個引號便可退出*號控制權。 學習

固化分組思想

 固化分組的思想很重要, 可是python中並不支持。使用(?>...)括號中的匹配時若是產生了備選狀態,那麼一旦離開括號便會被當即 引擎拋棄掉(從而沒法回溯!)。舉個典型的例子如:
'\w+:'

這個表達式在進行匹配時的流程是這樣的,會優先去匹配全部的符合\w的字符,假如字符串的末尾沒有':',即匹配沒有找到冒號,此時觸發回溯機制,他會迫使前面的\w+釋放字符,而且在交還的字符中從新嘗試與':'做比對
可是問題出如今這裏:    \w是不包含冒號的,顯然不管如何都不會匹配成功,但是依照回溯機制,引擎仍是得硬着頭皮往前找,這就是對資源的浪費。
因此咱們就須要避免這種回溯,對此的方法就是將前面匹配到的內容固化不令其存儲備用狀態!,那麼引擎就會由於沒有備用狀態可用而只得結束匹配過程。大大減小回溯的次數! 測試

Python模擬固化過程

雖然python中不支持,但書中提供了利用前向斷言來模擬固化過程。 優化

(?=(...))\1
自己, 斷言表達式中的結果是不會保存備用狀態的,並且他也不匹配具體字符,可是經過巧妙的 添加一個捕獲型括號來反向引用這個結果,就達到了固化分組的效果!對應上面的例子則是:
'(?=(\w+))\1:'

多選結構

多選結構在傳統型NFA中, 既不是匹配優先也不是忽略優先。而是按照順序進行的。因此有以下的利用方式
spa


  1. 在結果保證正確的狀況下,應該優先的去匹配更可能出現的結果。將可能性大的分支儘量放在靠前
  2. 不能濫用多選結構,由於當匹配到多選結構時,緩存會記錄下相應數目的備用狀態。舉例子:[abcdef]和‘a|b|c|d|e|f’這兩個表達式,雖然都能完成你的某個目的,可是儘可能選擇字符型數組,由於後者會在每次比較時創建6個備用狀態,浪費資源。

一些優化的理念和技巧

平衡法則

好的正則表達式需尋求以下平衡:
  1. 只匹配指望的文本,排除不指望的文本。(善於使用非捕獲型括號,節省資源)
  2. 必須易於控制和理解。避免寫整天書。。
  3. 使用NFA引擎,必需要保證效率(若是可以匹配,必須很快地返回匹配結果,若是不能匹配,應該在儘量短的時間內報告匹配失敗。

處理不指望的匹配

在處理過程當中,咱們老是習慣於使用星號等非硬性規定的量詞(實際上是個很差的習慣),
這樣的結果可能致使咱們使用的匹配表達式中沒有必須匹配的字符,例子以下:

'[0-9]?[^*]*\d*'    #只是舉個例子,沒有實際意義。
上面的式子就是這種狀況,在目標文本是「理想」時,可能出現不了什麼問題,可是若是自己數據有問題。那麼這個式子的匹配結果就徹底不可預知。
緣由就在於他沒有一部分是必須的!它匹配任何內容都是成功的。。。

對數據的瞭解和假設

其實在處理不少數據的時候,咱們的操做數據狀況都是不同的, 有時會很規整,那麼咱們能夠省掉考慮複雜表達式的狀況, 可是反過來,當來源很雜亂的時候,就須要思考多一些,對各類可能的情形作相應的處理。

引擎中通常存在的優化項

編譯緩
反覆使用編譯對象時,應該在使用前,使用re.compile()方法來進行編譯,這樣在後面調用時沒必要每次從新編譯。節省時間。尤爲是在循環體中反覆調用正則匹配時。
錨點優化
配合一些引擎的優化,應儘可能將錨點單獨凸顯出來。對比^a|^b,其效率便不如^(a|b)
一樣的道理,系統也會處理行尾錨點優化。因此在寫相關正則時,若是有可能的話,將錨點使用出來。
量詞優化
引擎中的優化,會對如.* 這樣的量詞進行統一對待,而不是按照傳統的回溯規則,因此,從理論上說'(?:.)*' 和'.*'是等價的,不過具體到引擎實現的時候,則會對'.*'進行優化。速度就產生了差別。
消除沒必要要括號以及字符組
這個在python中是否有 未知。只是在支持的引擎中,會對如[.]中轉化成\.,由於顯而後者的效率更高(字符組處理引發額外開銷)

以上是一些引擎帶的優化,天然其實是咱們沒法控制的的,不過了解一些後,對咱們後面的一些處理和使用有很大幫助。

其餘技巧和補充內容

過分回溯問題

消除指數級匹配

形以下面:
(\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)

量詞等價轉換

如今來看看大括號量詞的效率問題
1,當大括號修飾的對象是相似於字符數組或者\d這種 非肯定性字符時,使用大括號效率高於重複疊加對象。即:
\d{5}優於\d\d\d\d\d
經測試在python中後者優於前者。會快不少.
2,可是當重複的字符時肯定的某一個字符時,則簡單的重複疊加對象的效率會高一些。這是由於引擎會對單純的字符串內部優化(雖然咱們不知道具體優化是如何作到的)
aaaaa 優於a{5}
整體上說'\d' 確定是慢於'1'
我使用的python3中的re模塊,經測試,不使用量詞會快。
綜上,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}')    #優於上面的表達式

整體來講,在咱們沒有時間去深刻研究模塊代碼的時候,只能經過嘗試和反覆修改來獲得最終的複合預期的表達式。

常識優化措施

然而咱們利用可能的提高效果去嘗試修改的時候頗有可能 拔苗助長 由於某些咱們看來緩慢的回溯在正則引擎內部會進行必定的優化
「取巧」的修改又可能會關閉或者避開了這些優化,因此結果也許會令咱們很失望。
如下是書中提到的一些 常識性優化措施:
避免從新編譯(循環外建立對象)
使用非捕獲型括號(節省捕獲時間和回溯時狀態的數量)
善用錨點符號
不濫用字符組
提取文本和錨點。將他們從可能的多選分支結構中提取出來,會提取速度。
最可能的匹配表達式放在多選分支前面


一個很好用的核心公式

’opening normal*(special normal*)* closing‘

這個公式 特別用來對於匹配在兩個特殊分界部分(可能不是一個字符)內的normal文本,special則是處理當分界部分也許和normal部分混亂的狀況。
有以下的三點避免這個公式無休止匹配的發生。
  1. special部分和normal部分匹配的開頭不能重合。必定保證這兩部分在任何狀況下不能匹配相同的內容,否則在沒法出現匹配時遍歷全部狀況,此時引擎的路徑就不能肯定。
  2. normal部分必須匹配至少一個字符
  3. special部分必須是固定長度的

舉個例子:

[^\\"]+(\\.[^\\"]+)* #匹配兩個引號內的文本,可是不包括被轉義的引號
相關文章
相關標籤/搜索