標籤:原理 速度 完成 special 表達 空白 raw_input href normal
最近的時間內對Regex進行了一點點學習。所選教材是《mastering regular expressions》,也就是所謂的《精通Regex》。讀過一遍後,頓感Regex的強大和精湛之處。其中前三章是對Regex的基本規則的介紹和鋪墊。七章以後是對在具體語言下的應用。而核心的部分則是四五六這三章節。 其中第四章是講了整個Regex的精華,即傳統引擎NFA的回溯思想。第五章是一些例子下對回溯思想的理解。第六章則是對效率上的研究。根源也是在回溯思想上的引申和研究。 這篇文章是我結合python官方re模組的文檔以及這本書做一個相應的總結。 其中官方的文檔: http://docs.python.org/3.3/library/re.html
由於我都是在python上聯絡和使用的,所以後面的問題基本都是在python上提出來的,所以這本書中的其它正則流派我均不涉及。依書中,python和perl風格差不多,屬於傳統NFA引擎,也就是以“運算式主導“,採用回溯機制,匹配到即停止(
順序敏感,不同於POSIX NFA等採用匹配最左最長的結果)。
對於回溯部分,以及談及匹配的時候,將引擎的位置總是放在字元和字元之間,而不是字元本身。比如^對應的是第一個字元之前的那個”空白“位置。
基礎規則的介紹
python中的轉義符號幹擾
python中,命令列和指令碼等,裡面都會對轉義符號做處理,此時的字串會和Regex的引擎產生衝突。即在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對應,本身匹配Null 字元,但是其位置是在非"邊界"情況下. 比如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.Ai re.I #忽略大小寫L re.Lm re.Ms 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中,由於是目標文本主導,所有對象字元只檢測一遍,到文本結束後,過就是過,不過就不過。這也就是”確定“這個說法的原因。
回溯/備用狀態
備用狀態當出現可選分支時,會將其他的選項儲存起來,作為備用狀態。當前的匹配失敗時,引擎進行回溯,則會回到最近的備用狀態。匹配的情況中,匹配優先與忽略優先某種意義上是一致的,只是順序上有所區別。當存在多個匹配時,兩種方式進行的情況很可能是不同的,但是當不存在匹配時,他們倆的情況是一致的,即必然嘗試了所有的可能。
回溯機制兩個要點
- 在正則引擎選擇進行嘗試還是跳過嘗試時,匹配優先量詞和忽略優先量詞會控制其行為。
- 匹配失敗時,回溯需要返回到上一個備用狀態,原則是後進先出(後產生的狀態首先被回溯到)
回溯典型舉例:可以看到,傳統型NFA到D點即匹配結束。而在陰影中POSIX NFA的匹配流程,需要找到所有結果,
並在這些結果中取最長的結果返回。
作為對比說明,下面是目標文本不能匹配時,引擎走過的路徑:
如,我們看到此時POSIX NFA和傳統型NFA的匹配路徑是一致的。
以上的例子引發了一個匹配時的思考,很多時候我們應該盡量避免使用‘.*‘ ,因為其總是可以匹配到最末或者行尾,浪費資源。
既然我們只尋求引號之間的資料,往往可以藉助排除型數組來完成工作。
此例中,使用‘[^‘‘]*‘這個來代替‘.*‘的作用顯而易見,我們只匹配非引號的內容,那麼遇到第一個引號即可退出*號控制權。
固化分組思想 固化分組的思想很重要,
但是python中並不支援。使用(?>...)括弧中的匹配時如果產生了備選狀態,那麼一旦離開括弧便會被立即
引擎拋棄掉(從而無法回溯!)。舉個典型的例子如:
‘\w+:‘
這個運算式在進行匹配時的流程是這樣的,會優先去匹配所有的符合\w的字元,假如字串的末尾沒有‘:‘,即匹配沒有找到冒號,此時觸發回溯機制,他會迫使前面的\w+釋放字元,並且在交還的字元中重新嘗試與‘:‘作比對。
但是問題出現在這裡: \w是不包含冒號的,顯然無論如何都不會匹配成功,可是依照回溯機制,引擎還是得硬著頭皮往前找,這就是對資源的浪費。
所以我們就需要避免這種回溯,對此的方法就是將前面匹配到的內容固化,不令其儲存備用狀態!,那麼引擎就會因為沒有備用狀態可用而只得結束匹配過程。大大減少回溯的次數!
Python類比固化過程
雖然python中不支援,但書中提供了利用前向斷言來類比固化過程。
(?=(...))\1
本身,
斷言運算式中的結果是不會儲存備用狀態的,而且他也不匹配具體字元,但是通過巧妙的
添加一個捕獲型括弧來反向引用這個結果,就達到了固化分組的效果!對應上面的例子則是:
‘(?=(\w+))\1:‘
多選結構
多選結構在傳統型NFA中, 既不是匹配優先也不是忽略優先。而是按照順序進行的。所以有如下的利用方式
- 在結果保證正確的情況下,應該優先的去匹配更可能出現的結果。將可能性大的分支儘可能放在靠前。
- 不能濫用多選結構,因為當匹配到多選結構時,緩衝會記錄下相應數目的備用狀態。舉例子:[abcdef]和‘a|b|c|d|e|f’這兩個運算式,雖然都能完成你的某個目的,但是盡量選擇字元型數組,因為後者會在每次比較時建立6個備用狀態,浪費資源。
一些最佳化的理念和技巧
平衡法則好的Regex需尋求如下平衡:
- 只匹配期望的文本,排除不期望的文本。(善於使用非捕獲型括弧,節省資源)
- 必須易於控制和理解。避免寫成天書。。
- 使用NFA引擎,必須要保證效率(如果能夠匹配,必須很快地返回匹配結果,如果不能匹配,應該在儘可能短的時間內報告匹配失敗。)
處理不期望的匹配
在處理過程中,我們總是習慣於使用星號等非硬性規定的量詞(其實是個不好的習慣),
這樣的結果可能導致我們使用的匹配運算式中沒有必須匹配的字元,例子如下:
‘[0-9]?[^*]*\d*‘ #只是舉個例子,沒有實際意義。
上面的式子就是這種情況,在目標文本是“理想”時,可能出現不了什麼問題,但是如果本身資料有問題。那麼這個式子的匹配結果就完全不可預知。
原因就在於他沒有一部分是必須的!它匹配任何內容都是成功的。。。
對資料的瞭解和假設其實在處理很多資料的時候,我們的操作資料情況都是不一樣的,
有時會很規整,那麼我們可以省掉考慮複雜運算式的情況,
但是反過來,當來源很雜亂的時候,就需要思考多一些,對各種可能的情形做相應的處理。
引擎中一般存在的最佳化項
編譯緩衝反覆使用編譯對象時,應該在使用前,使用re.compile()方法來進行編譯,這樣在後面調用時不必每次重新編譯。節省時間。尤其是在迴圈體中反覆調用正則匹配時。
錨點最佳化配合一些引擎的最佳化,應盡量將錨點單獨凸顯出來。對比^a|^b,其效率便不如^(a|b)同樣的道理,系統也會處理行尾錨點最佳化。所以在寫相關正則時,如果有可能的話,將錨點使用出來。
量詞最佳化引擎中的最佳化,會對如.* 這樣的量詞進行統一對待,而不是按照傳統的回溯規則,所以,從理論上說‘(?:.)*‘ 和‘.*‘是等價的,不過具體到引擎實現的時候,則會對‘.*‘進行最佳化。速度就產生了差異。
消除不必要括弧以及字元組這個在python中是否有
未知。只是在支援的引擎中,會對如[.]中轉化成\.,因為顯然後者的效率更高(字元組處理引起額外開銷)
以上是一些引擎帶的最佳化,自然實際上是我們無法控制的的,不過瞭解一些後,對我們後面的一些處理和使用有很大協助。其他技巧和補充內容
過度回溯問題
消除指數級匹配形如下面:
(\w+)*
這種情況的運算式,在匹配長文本的時候會遇到什麼問題呢,如果在文本匹配失敗時(別忘了,如果失敗,則說明已經回溯了
所有的可能),想象一下,*號退一個狀態,裡面的+號就包括其餘的
所有狀態,驗證都失敗後,回到外面,*號
退到倒數第二個備用狀態,再進到括弧內,+號又要回溯一邊比上一輪差1的
備用狀態數,當字串很長時,
就會出現指數級的回溯總數。系統就會‘卡死‘。甚至當有匹配時,這個匹配藏在回溯總數的中間時,也是會造成卡死的情況。所以,使用NFA的引擎時,必須要注意這個問題!
我們採用如下思路去避免這個問題:
佔有優先量詞(python中使用前向斷言加反向引用類比)
道理很簡單,既然龐大的回溯數量都是被儲存的備用狀態導致的,那麼我們直接使引擎放棄這些狀態。說到底是擺脫(regex*)* 這種形式。
import rere_lx = re.compile(r‘(?=(\w+))\1*\d‘)
效率測試代碼
在測試運算式的效率時,可藉助以下代碼比較所需時間。在兩個可能的結果中擇期優者。
import reimport timere_lx1 = re.compile(r‘your_re_1‘)re_lx2 = re.compile(r‘your_re_2‘)starttime = time.time()repeat_time = 100for i in range(repeat_time): s=‘test text‘*10000 result = re_lx1.search(s)time1 = time.time()-starttimeprint(time1)starttime = time.time()for i in range(repeat_time): s=‘test text‘*10000 result = re_lx2.search(s)time2 = time.time()-starttimeprint(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部分混亂的情況。有如下的三點避免這個公式無休止匹配的發生。
- special部分和normal部分匹配的開頭不能重合。一定保證這兩部分在任何情況下不能匹配相同的內容,不然在無法出現匹配時遍曆所有情況,此時引擎的路徑就不能確定。
- normal部分必須匹配至少一個字元
- special部分必須是固定長度的
舉個例子:
[^\\"]+(\\.[^\\"]+)* #匹配兩個引號內的文本,但是不包括被轉義的引號
Python下的Regex原理和最佳化筆記