【PHP】 UTF-8 multi byte in preg (regex) function
問題:
PHP 為了支援常見編碼 (如 UTF-8) 上 multi byte 的 characters,提供了 mb_* 家族類 function 專門處裡這些 multi byte characters。
但並不是每一個字串相關 function 都有相對應的 mb_* function,今天要分享的就是 PHP 在 preg_* 家族類 function,也就是專門處裡 regex (正規表達式) 相關的 function 預設也是不支援 UTF-8 multi byte 的。
$string = "【武器】長劍";
preg_match('/^【([^】]*)】/', $string, $match);
var_dump($match);
$string = "【技能】雙劍斬擊";
preg_match('/^【([^】]*)】/', $string, $match);
var_dump($match);
|
舉個例子,假設某 RPG 遊戲中的專有名詞前方都會有一組 "【】" 符號內表示該名詞的所屬分類,而我們想做的就是使用 preg_match 提取該分類名稱。
array(2) {
[0] => string(12) "【武器】"
[1] => string(6) "武器"
}
array(0) {
}
|
看起來結果與預期不同,"【技能】雙劍斬擊" 這一筆資料無法被 preg_match 提取成功。接下來會解釋這個結果是怎麼運算出來的。
發生原因:
解答前要先講講 UTF-8 編碼方式,在 【PHP】trim not support multi byte & UTF-8 encode 文章中有講解到 UTF-8 的編碼方式,在這邊只放重點,有興趣的可以看該篇文章較詳細的說明。
UTF-8 本身是編碼長度是不固定的,其解碼方式如下:
- 第一個 byte 的開頭是 0,代表這是一個 1 byte 長度的字元。
- 第一個 byte 的開頭是連續 N 個 1,代表這是一個 N byte 長度的字元。
- 同時多 byte 字元,第一個以外的 byte 都會使用 10 作為開頭。
- 剩下的 bit 就照順序組合後對應 Unicode 萬國碼的編碼。
但 preg_* 家族類 function 預設是不支援 UTF-8 多 byte 的,所以剛剛範例中的中文字通通 preg_match 被當成各三個 character 在處理,其中 "技" 與 "】" 的 UTF-8 為:
var_dump(bin2hex('技')); // string(6) "e68a80"
var_dump(bin2hex('】')); // string(6) "e38091"
|
所以其實我們剛剛執行的 regex 規則是:
^【 |
【開頭 |
([^】]*) |
不包含 e3 80 91 任意 character |
】 |
e3 80 91 三個連續的 character 結尾 |
而 e6 8a 80 (技) 當中就包含 80,不符合規則,自然就不會被 preg_match 提取出來。當然其他的中文字與特殊符號也都被當成對應的多個 character 在處理,只是剛好都符合規則所以看似正常運作而已。
解答:
要解決這個問題只要使用 preg_* 家族類 function Pattern Modifiers 中的 PCRE_UTF8 即可支援 UTF-8。
u (PCRE_UTF8) This modifier turns on additional functionality of PCRE that is incompatible with Perl. Pattern and subject strings are treated as UTF-8. An invalid subject will cause the preg_* function to match nothing; an invalid pattern will trigger an error of level E_WARNING. Five and six octet UTF-8 sequences are regarded as invalid. |
來源:PHP: Possible modifiers in regex patterns - Manual
$string = "【技能】雙劍斬擊";
preg_match('/^【([^】]*)】/u', $string, $match);
var_dump($match);
/*
array(2) {
[0] => string(12) "【技能】"
[1] => string(6) "技能"
}
*/
|
在 regex 後方加上 u modifier 就可以順利將 "【技能】雙劍斬擊" 中的 "技能" 分類提取出來啦。
參考資料:
[1] PHP: Possible modifiers in regex patterns - Manual
https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php
[2] PHP: bin2hex - Manual
https://www.php.net/manual/en/function.bin2hex.php
[3] PHP: Multibyte String - Manual
https://www.php.net/manual/en/book.mbstring.php