PHP中PRGE正則函數的學習
正則表達式的作用想必不用我多說了,大家在日常的開發中或多或少都會接觸到。特別是對于一些登錄(郵箱、手機號)以及網頁爬蟲來說,正則表達式就是神器一般的存在。在 PHP 中,有兩種處理正則表達式的函數,今天我們就來學習其中的一種。
PCRE 與 POSIX
前面說到,有兩種處理正則的函數庫,一個是 POSIX 為主的 ereg_xxx 這種函數,不過它們已經被淘汰了,并不是很推薦使用。而另一種就是基于 PCRE 的以 preg_xxx 開頭的這種函數庫。今天我們主要學習的就是這類型的正則處理函數庫。
POSIX 類型的正則函數庫不是二進制安全的,并且對 utf8 的支持也不好,所以從 PHP5.3 開始如果使用 ereg_xxx 這類的函數就會報一個 E_DEPRECATED 錯誤。PCRE 的函數庫對 perl 支持非常友好,同時,它也是支持 POSIX 擴展語法的正則表達式。具體的正則語法規則和模式修飾符相關的信息可以在文末的鏈接中查閱。關于模式修飾符的作用這里就不多說了,不清楚的小伙伴自己查找相關的資料哦。
另外,PCRE 與 POSIX 和 perl 也是有一些不同的,這些內容也都在文末的官方文檔鏈接中可以看到。
正則匹配
好了,話不多說,我們直接進入主題,其實大部分內容相信不少同學都是接觸過的,我們就來一一演示學習一下。
$str?=?"a@qq.com,b@sina.COM,c@yahoo.com,一堆測試數據。Test?Txt.";preg_match_all("/(.*)@(.*)\.(.*),/iU",?$str,?$out);
print_r($out);
//?Array
//?(
//?????[0]?=>?Array
//?????????(
//?????????????[0]?=>?a@qq.com,
//?????????????[1]?=>?b@sina.com,
//?????????????[2]?=>?c@yahoo.com,
//?????????)//?????[1]?=>?Array
//?????????(
//?????????????[0]?=>?a
//?????????????[1]?=>?b
//?????????????[2]?=>?c
//?????????)//?????[2]?=>?Array
//?????????(
//?????????????[0]?=>?qq
//?????????????[1]?=>?sina
//?????????????[2]?=>?yahoo
//?????????)//?????[3]?=>?Array
//?????????(
//?????????????[0]?=>?com
//?????????????[1]?=>?com
//?????????????[2]?=>?com
//?????????)//?)preg_match_all("/(.*)@(.*)\.(.*),/iU",?$str,?$out,?PREG_SET_ORDER);
print_r($out);
//?Array
//?(
//?????[0]?=>?Array
//?????????(
//?????????????[0]?=>?a@qq.com,
//?????????????[1]?=>?a
//?????????????[2]?=>?qq
//?????????????[3]?=>?com
//?????????)//?????[1]?=>?Array
//?????????(
//?????????????[0]?=>?b@sina.COM,
//?????????????[1]?=>?b
//?????????????[2]?=>?sina
//?????????????[3]?=>?COM
//?????????)//?????[2]?=>?Array
//?????????(
//?????????????[0]?=>?c@yahoo.com,
//?????????????[1]?=>?c
//?????????????[2]?=>?yahoo
//?????????????[3]?=>?com
//?????????)//?)preg_match_all("/(.*)@(.*)\.(.*),/iU",?$str,?$out,?PREG_OFFSET_CAPTURE);
print_r($out);
//?Array
//?(
//?????[0]?=>?Array
//?????????(
//?????????????[0]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?a@qq.com,
//?????????????????????[1]?=>?0
//?????????????????)//?????????????[1]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?b@sina.COM,
//?????????????????????[1]?=>?9
//?????????????????)//?????????????[2]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?c@yahoo.com,
//?????????????????????[1]?=>?20
//?????????????????)//?????????)//?????[1]?=>?Array
//?????????(
//?????????????[0]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?a
//?????????????????????[1]?=>?0
//?????????????????)//?????????????[1]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?b
//?????????????????????[1]?=>?9
//?????????????????)//?????????????[2]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?c
//?????????????????????[1]?=>?20
//?????????????????)//?????????)//?????[2]?=>?Array
//?????????(
//?????????????[0]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?qq
//?????????????????????[1]?=>?2
//?????????????????)//?????????????[1]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?sina
//?????????????????????[1]?=>?11
//?????????????????)//?????????????[2]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?yahoo
//?????????????????????[1]?=>?22
//?????????????????)//?????????)//?????[3]?=>?Array
//?????????(
//?????????????[0]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?com
//?????????????????????[1]?=>?5
//?????????????????)//?????????????[1]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?COM
//?????????????????????[1]?=>?16
//?????????????????)//?????????????[2]?=>?Array
//?????????????????(
//?????????????????????[0]?=>?com
//?????????????????????[1]?=>?28
//?????????????????)//?????????)//?)
preg_match_all() 函數用于完全的匹配,也就是文本中的內容全都匹配出來,并且將結果放到一個引用數組中。注意它最后的那個可選參數,默認情況下,數組的 0 下標是所有匹配到的字符內容,而剩下的索引內容是括號內部匹配的結果,可以對應到后面我們學習的替換函數中的 $1
、$2
這些插值中。
如果將最后一個參數設置為 PREG_SET_ORDER ,那么數據會以分組的形式展示,一級數組中就是每一個匹配到的內容,二級數組的 0 下標就是這個完全的文本內容,而后面的數據就是對應于這個完全匹配內容的括號內部匹配數據。
設置為 PREG_OFFSET_CAPTURE 的話,在格式上其實和默認情況下是一樣的,只是每個數組內部又多了一個表示匹配位置的數字下標值。
說實話,這三個屬性原來還真的沒有了解過,很多時候需要這些功能的時候反而是自己又重新去寫算法進行操作,這下也算是開了眼界。
preg_match("/(.*)@(.*)\.(.*),/iU",?$str,?$out);
print_r($out);
//?Array
//?(
//?????[0]?=>?a@qq.com,
//?????[1]?=>?a
//?????[2]?=>?qq
//?????[3]?=>?com
//?)preg_match("/(.*)@(.*)\.(.*),/iU",?$str,?$out,?PREG_OFFSET_CAPTURE,?2);
print_r($out);
//?Array
//?(
//?????[0]?=>?Array
//?????????(
//?????????????[0]?=>?qq.com,b@sina.COM,
//?????????????[1]?=>?2
//?????????)//?????[1]?=>?Array
//?????????(
//?????????????[0]?=>?qq.com,b
//?????????????[1]?=>?2
//?????????)//?????[2]?=>?Array
//?????????(
//?????????????[0]?=>?sina
//?????????????[1]?=>?11
//?????????)//?????[3]?=>?Array
//?????????(
//?????????????[0]?=>?COM
//?????????????[1]?=>?16
//?????????)//?)
preg_match() 函數就比較簡單了,它只返回第一個與正則相匹配的數據。當然,它也有一些可選的參數。最后一個可選參數的作用就是偏移量,我們從第 2 個字符以后開始匹配,這里匹配到的數據和第一條中的就不一樣了。
字符串分割
就像 explode() 和 str_split() 函數一樣,正則中也有將字符串分割為數組的函數,它一般會作用于更復雜的分割條件。
print_r(preg_split("/@(.*)\.(.*),/iU",?$str));
//?Array
//?(
//?????[0]?=>?a
//?????[1]?=>?b
//?????[2]?=>?c
//?????[3]?=>?一堆測試數據。Test?Txt.
//?)print_r(preg_split("/@(.*)\.(.*),/iU",?$str,?2,?PREG_SPLIT_OFFSET_CAPTURE));
//?Array
//?(
//?????[0]?=>?Array
//?????????(
//?????????????[0]?=>?a
//?????????????[1]?=>?0
//?????????)//?????[1]?=>?Array
//?????????(
//?????????????[0]?=>?b@sina.COM,c@yahoo.com,一堆測試數據。Test?Txt.
//?????????????[1]?=>?9
//?????????)//?)
這里我們是通過 @xxx.xxx, 來作為分隔符,所以分隔后的結果就是不包含這個分隔符的數組數據。preg_split() 這個函數的默認使用也是比較簡單的,它同樣有一些可選參數,比如第二條,第三個可選參數的作用是限制分割的數量,這里我們限制只分割成兩個數組,所以文本后面的內容都會放到一個數組中,并且通過最后一個參數來指定返回查找到的數據的位置在字符串中的下標。
正則替換
關于替換的內容就比較多了,可以說,除了第一個我們介紹的 preg_match_all() 之外,最常用的就是 preg_replace() 這個函數了。它的作用當然也是和 str_replace() 類似的,只不過使用正則的話條件能夠更豐富,也更加強大。
普通替換
echo?preg_replace("/@(.*)\.(.*),/iU",?'@$1.$2.cn,?',$str),?PHP_EOL;
//?a@qq.com.cn,?b@sina.COM.cn,?c@yahoo.com.cn,?一堆測試數據。Test?Txt.echo?preg_replace("/[\x{4E00}-\x{9FFF}]+/u",?'Many?Test?Info.',$str,?-1,?$count),?PHP_EOL;
echo?$count,?PHP_EOL;
//?a@qq.com,b@sina.COM,c@yahoo.com,Many?Test?Info.。Test?Txt.
//?3echo?preg_replace("/@(.*)\.(.*),/iU",?'@$1.$2.cn,?',$str,?2,?$count),?PHP_EOL;
echo?$count,?PHP_EOL;
//?a@qq.com.cn,?b@sina.COM.cn,?c@yahoo.com,一堆測試數據。Test?Txt.
//?2
普通的 preg_replace() 函數也是非常簡單的,它的可選參數其實和 str_replace() 也是類似的,第 4 個參數指定替換數量,比如第二條設置為 -1 也就是默認值,這樣就是全部替換,而第三條就是設置為 2 ,只會替換兩條匹配的內容。最后一個參數是返回匹配替換的數量,它是一個引用參數,也就是文本中我們一共替換掉了多少內容,或者說是我們匹配到了多少條信息。
另外還有一個函數和 preg_replace() 非常類似。我們直接來看它和 preg_replace() 的區別。
echo?preg_filter("/@(.*)\.(.*),/iU",?'@$1.$2.cn,?',$subStr),?PHP_EOL;$subject?=?array('1',?'a',?'2',?'b',?'3',?'A',?'B',?'4');?
$pattern?=?array('/\d/',?'/[a-z]/',?'/[1a]/');?
$replace?=?array('A:$0',?'B:$0',?'C:$0');?echo?"preg_filter?的結果:",?PHP_EOL;
print_r(preg_filter($pattern,?$replace,?$subject));?
//?preg_filter?的結果:
//?Array
//?(
//?????[0]?=>?A:C:1
//?????[1]?=>?B:C:a
//?????[2]?=>?A:2
//?????[3]?=>?B:b
//?????[4]?=>?A:3
//?????[7]?=>?A:4
//?)echo?"preg_replace?的結果:",?PHP_EOL;
print_r(preg_replace($pattern,?$replace,?$subject));
//?preg_replace?的結果:
//?Array
//?(
//?????[0]?=>?A:C:1
//?????[1]?=>?B:C:a
//?????[2]?=>?A:2
//?????[3]?=>?B:b
//?????[4]?=>?A:3
//?????[5]?=>?A
//?????[6]?=>?B
//?????[7]?=>?A:4
//?)
從上面的代碼中可以看出,preg_filter() 函數最后返回的結果會是匹配到結果的內容,而 preg_replace() 如果字符中沒有匹配到結果,也會返回原始的內容。它們兩個的參數是完全相同的。
在這段測試代碼中,我們使用了數組作為替換的前三個參數,它們的匹配規則是 pattern 對應 replace 的一個一個去匹配。也就是說,0 號下標的 /\d/
對應的匹配規則是 A:$0
,如果缺少了替換或者匹配規則的話,不會報錯,但替換的結果可能就不是你想要的了。
注意,只有替換類的函數是可以這樣接收數組作為參數的。
回調替換
除了上面的替換之外,PRGE 的函數庫中還有回調式替換的函數,也就是能讓我們自定義替換之后的返回結果。
print_r(preg_replace_callback($pattern,?function($matches){print_r($matches);return?strtolower($matches[0]);
},?$subject));
//?Array
//?(
//?????[0]?=>?1
//?)
//?Array
//?(
//?????[0]?=>?1
//?)
//?Array
//?(
//?????[0]?=>?a
//?)
//?Array
//?(
//?????[0]?=>?a
//?)
//?Array
//?(
//?????[0]?=>?2
//?)
//?Array
//?(
//?????[0]?=>?b
//?)
//?Array
//?(
//?????[0]?=>?3
//?)
//?Array
//?(
//?????[0]?=>?4
//?)
//?Array
//?(
//?????[0]?=>?1
//?????[1]?=>?a
//?????[2]?=>?2
//?????[3]?=>?b
//?????[4]?=>?3
//?????[5]?=>?A
//?????[6]?=>?B
//?????[7]?=>?4
//?)print_r(preg_replace_callback('/(.*)@(.*)\.(.*),/iU',?function($matches){return?strtoupper($matches[0]);
},?$str));
//?A@QQ.COM,B@SINA.COM,C@YAHOO.COM,一堆測試數據。Test?Txt.
preg_replace_callback() 的第二個參數其實就是相當于把 preg_replace() 中的替換字符串換成一個匿名回調函數了。這個函數中的參數就是匹配到的結果,上面的測試代碼中我們全部打印了出來。然后給這個函數一個 return 返回值,就是對應地去把替換的結果返回到原值中。
preg_replace_callback() 最終的返回值是根據傳遞給它的原始數據來確定的,如果是數組就返回數組,如果是字符串就返回的字符串。
另外還有一種更復雜的回調函數。
print_r(preg_replace_callback_array(['/(.*)@(.*)\.(.*),/iU'?=>?function?($matches)?{echo?'one:',?$matches[0],?PHP_EOL;return?strtoupper($matches[0]);},'/Test?Txt./iU'?=>?function?($matches)?{echo?'two:',?$matches[0],?PHP_EOL;return?strtoupper($matches[0]);}],$str
));
//?one:a@qq.com,
//?one:b@sina.COM,
//?one:c@yahoo.com,
//?two:Test?Txt.
//?A@QQ.COM,B@SINA.COM,C@YAHOO.COM,一堆測試數據。TEST?TXT.
沒錯,在一個函數中進行兩種正則模式的匹配。是不是感覺很高大上。這個函數的使用場景就不多了,而且需要注意的是,如果第一條正則匹配到數據了,第二條正則就不會有匹配的結果了,這個大家可以自己測試一下。
匹配驗證及字符串模式格式轉換
匹配驗證就是驗證我們的正則表達式是否能匹配到的內容。
print_r(preg_grep("/\d/",?[$str]));
//?Array
//?(
//?)print_r(preg_grep("/\d/",?[$str],?PREG_GREP_INVERT));
//?Array
//?(
//?????[0]?=>?a@qq.com,b@sina.COM,c@yahoo.com,一堆測試數據。Test?Txt.
//?)
它只返回能夠匹配到的數據,也就是第二個參數。這個參數必須是一個數組,可以驗證多條數據是否能夠通過這個正則匹配到內容,但不返回具體的匹配內容信息。可以用作在正式的 preg_match_all() 或者替換、分割操作之前的判斷驗證。它的最后一個參數如果設置為 PREG_GREP_INVERT 的話,就是反向地獲取不能和正則匹配的數據。
print_r(preg_quote("(.*).(.*),"));
//?\(\.\*\)\.\(\.\*\),
preg_quote() 函數其實是有點類似于 addslashes() 函數,它是針對正則中的特殊符號添加轉義斜杠的。
錯誤信息
最后我們再看看錯誤信息的展示,對于正則匹配的錯誤,在 PHP8 之前僅有一個錯誤號,作用不大。
preg_match("///",?$str);print_r(preg_last_error());?
//?Warning:?preg_match():?Delimiter?must?not?be?alphanumeric?or?backslash?in?/Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/03/source/6.PHP中PRGE正則函數的學習.php?on?line?332
//?1
//?print_r(preg_last_error_msg());??//?php8
而在 PHP8 之后,新增加了一個 preg_last_error_msg() 可以返回錯誤信息。不過我的電腦上還沒有安裝 PHP8 所以這塊內容就不展示了。
總結
PHP 中正則操作的函數就這些,但正則真正的精髓其實是在于正則表達式怎么寫這一塊。不好的正則可能會產生嚴重的回溯導致性能的急劇下降,所以在做業務開發的時候,能不用正則其實還是盡量不要用的。不過相對來說,像是登錄的用戶驗證之類的功能,正則簡直不要太好用,這個就完全可以讓正則好好發揮啦!另外,用好模式修飾符也是能夠有效地提升正則效率的,這些都是值得我們深入去研究的東西,有興趣的小伙伴多多閱讀官方文檔,一定能找到讓你驚喜的地方。
測試代碼:
https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/03/source/6.PHP%E4%B8%ADPRGE%E6%AD%A3%E5%88%99%E5%87%BD%E6%95%B0%E7%9A%84%E5%AD%A6%E4%B9%A0.php
參考文檔:
https://www.php.net/manual/zh/book.pcre.php