這篇文章通過提取html元素介紹了Regex中模式修飾符、貪婪匹配與非貪婪匹配、Unicode模式、環視等知識點。
在閱讀這篇文章前最好把同系列文章php實戰Regex(一):驗證手機號先仔細閱讀一遍。
基本提取
有這樣一個表格
| 使用者名稱 |
職業 |
| Kobe Bryant |
籃球運動員 |
| Jay Chou |
歌手、詞曲創作人、製作人、演員、導演 |
| Lionel Messi |
足球運動員 |
它的源碼如下:
| 使用者名稱 |
職業 |
| Kobe Bryant |
籃球運動員 |
| Jay Chou |
歌手、詞曲創作人、製作人、演員、導演 |
| Lionel Messi |
足球運動員 |
現在要提取第一個元素。最簡單的Regex應該是這樣:
\s+.*<\/tr>
其中
\s是php實戰Regex(一):驗證手機號介紹過的字元組簡記法中的一個,代表斷行符號符、空格、定位字元等空白字元
量詞+表示它所修飾的字元或字元組出現次數大於等於1
點號字元.在Regex中是一個特殊的元字元,它可以匹配“任一字元”
閉標籤中的斜線/在php的Regex中是模式分隔字元,所以需要轉義來表示斜線字元。
但實際上這樣一個運算式是無法從上面的中提取第一個元素的
這裡主要的問題是在預設情況下點號字元.無法匹配分行符號\n。有兩個方法可以解決這個問題:
關於模式修飾符(Pattern Modifiers),這裡需要詳細介紹一下(點擊這裡查看php支援的所有模式修飾符)。模式修飾符可以改變Regex的一些預設規則,常用的模式修飾符有i、s、U、u等,我們在後面會用到它們中的一些,這裡不展開介紹每個模式修飾符的作用,後面用到了再具體介紹。這裡主要對比一下/.../{modifier}與...(?{modifier})...兩種表示方法的區別。
| 模式修飾符 |
/.../{modifier} |
...(?{modifier})... |
| 樣本 |
/ |
.*<\/tr>/s
|
(?s).*<\/tr>
| 名稱(php手冊) |
模式修飾符 |
模式內修飾符 |
| 名稱(《正則指引》) |
預定義常量 |
模式修飾符 |
| 作用範圍 |
整個Regex |
不在分組(子運算式)中時,對它後面的全部Regex起作用;如果在分組(子運算式)中,則對它分組中的剩餘部分起作用。在沒有分組,且放在整個Regex最前面的時候相當於/.../{modifier} |
| 支援程度 |
支援所有模式修飾符 |
支援部分模式修飾符 |
| 其他程式設計語言 |
可能不支援 |
一般都支援 |
從上面的gif中可以看到提取的結果中有三個tr,而不是只有一個。這是因為Regex中量詞預設是貪婪匹配,在這裡,.*會匹配一切字元,直到最後沒有字元再向前回溯,回溯到中的最後一個時與Regex中的<\/tr>相匹配,從而完成整個匹配過程,最後的結果也就是包含了三個。
可以使用模式修飾符U指定整個Regex為非貪婪模式,也可以使用非貪婪匹配量詞指定某一個量詞為非貪婪模式:
-
指定整個Regex為非貪婪模式:
/\s+.*<\/tr>/Us
或(?Us)\s+.*<\/tr>
非貪婪量詞:
/\s+.*?<\/tr>/s
完整的貪婪量詞(匹配優先量詞)與非貪婪量詞(忽略優先量詞)見下表:
| 貪婪量詞 |
非貪婪量詞 |
限定次數 |
| * |
*? |
可能出現,可能不出現,出現次數沒有上限 |
| + |
+? |
至少出現1次,沒有上限 |
| ? |
?? |
出現0次或1次 |
| {m,n} |
{m,n}? |
出現次數大於等於m,小於等於n |
| {m,} |
{m,}? |
至少出現m次,沒有上限 |
| {0,n} |
{0,n}? |
出現0次-n次 |
提取包含指定內容的行
假設我們想把表格中有關於運動員的記錄都提取出來,我們可能會使用/.*運動員.*<\/tr>/s這樣的Regex。
這個運算式在Unicode編碼環境下可以匹配出結果,但是在GBK環境下就未必了。我們可以通過模式修飾符u來指定Unicode模式:
/.*運動員.*<\/tr>/us
在Unicode模式下,我們甚至可以使用碼值來代替漢字:
/.*\x{8fd0}\x{52a8}\x{5458}.*<\/tr>/us
php正則中使用\x{hex}的形式來表示Unicode字元的碼值,使用碼值的好處是可以結合字元組來表示一段範圍,如[\x{4e00}-\x{9fff}]表示匹配所有漢字字元。
上面的運算式可以匹配出結果,但是卻不正確。我們可以看到,它匹配了整個字串的第一個到最後一個。
直覺上,我們是想Regex先去匹配“運動員”,然後向左尋找最近的一個,向右尋找最近的一個。但事實上,Regex是從左往右匹配的,即從開始尋找,整個Regex的匹配情況見下表(空白字元沒有顯示出來)。
| 運算式 |
匹配值 |
| / |
|
|
|
| .* |
|
使用者名稱 |
職業 |
| Kobe Bryant |
籃球 |
| 運動員 |
運動員 |
| .* |
|
| Jay Chou |
歌手、詞曲創作人、製作人、演員、導演 |
| Lionel Messi |
足球運動員 |
| <\/tr> |
|
| /us |
|
這裡兩個.*匹配到的字元都比預期要多。第二個.*匹配字元比預期多的原因是Regex預設是貪婪匹配模式,它會匹配剩餘字串中的每個字元,直到字串的末尾,然後再向前回溯到最後一個,可以通過指定非貪婪匹配模式來解決這個問題。但是第一個.*匹配字元比預期多是正常現象,因為Regex是從左向右匹配的,運算式中的匹配字串中第一個,後面的.*則匹配剩餘的所有字元,直到字串的末尾,然後再向前回溯到“運動員”。
我們先看看使用非貪婪匹配時的結果:
可以看到,第二個.*匹配的字元已經是我們想要的了。那麼,對於第一個.*匹配字元比預期多這個問題怎麼解決呢?
如果僅使用到目前為止我的文章中介紹的知識,也是有方法可以解決的。我們可以先從左至右匹配出所有的行(...),方法是使用php中的preg_match_all函數結合非貪婪匹配模式;然後再遍曆每一行,過濾出其中包含“運動員”的行即可。
當然,我們也可以通過純粹的Regex來解決這個問題。如果有一定Regex使用經驗的朋友可能很容易聯想到排除型字元組,我們介紹過字元組[...],它表示在同一位置可能出現的字元。而排除型字元組則表示在同一位置不能出現的字元,它的形式是[^...],通過緊跟在開方括弧[後面的^來表示排除型字元組。例如,[^\d]表示匹配的字元是除了數字以外的任一字元。
如果有排除型子運算式,類似於(^)*,我們只需要指定第一個.*把排除就行了。但是很遺憾,Regex中沒有排除型子運算式或者說排除型分組。這種情況下,我們只能使用環視
/(.(?!))*運動員.*<\/tr>/Us
環視(look-around)不匹配任何字元,用來“停在原地,四處張望”。上面的運算式使用了否定順序環視,它的形式是(?!...)。具體對於(.(?!))*來分析,每當.匹配了一個字元後,就向右看看,如果當前匹配字元的右邊沒有出現就匹配成功。
完整的環視有:
| 名字 |
記法 |
含義 |
| 肯定順序環視 |
(?=...) |
向右看看,右邊出現了環視中的內容才匹配 |
| 否定順序環視 |
(?!...) |
向右看看,右邊不出現環視中的內容才匹配 |
| 肯定逆序環視 |
(?<=...) |
向左看看,左邊出現了環視中的內容才匹配 |
| 否定逆序環視 |
(? |
向左看看,左邊不出現環視中的內容才匹配 |
由於上面的Regex有一個分組(子運算式),所以匹配的結果除了下標0,還有下標1,這裡下標1的結果其實沒有什麼用,我們可以用之前介紹過的非捕獲分組:
/(?:.(?!))*運動員.*<\/tr>/Us
我們的真正目的是提取所有包含“運動員”的行,而上面只提取了第一個,所以需要將preg_match函數換成preg_match_all。