網站製作學習誌

記錄學習製作網站的一切

PHP 小實驗 (2) - 正規表示式

也是在 Tim Club 回答的問題,記在這裡自己備忘用。

一位網友問到,想把以下的文字:

1
2
3
4
[code:07318bb522][/code:07318bb522]
[color=blue:8841ec98ad][/color:a76c137a60]
[b:8841ec98ad] [/b:6e4e964058]
[quote="Anonymous"] [/quote:6e4e964058]

換成:

1
2
3
4
[code][/code]
[color=blue][/color]
[b] [/b]
[quote] [/quote]

其實這個就是所謂的 BBCode ,只是我很少看過第一種表示形式。

怎麼轉換呢?

首先可以先想到的,是用替換的方式,也就是 str_replace() 。不過這裡有個問題,那就是標籤名稱後的冒號與英數混合字串是非固定的,所以我們就不能用 str_replace() 這種單一的替代函式,要另外想辦法解決。

雖然說是不固定的文字,但總是能找到一點脈絡,其組合大概能分成以下幾種:

1
[標籤名稱:英數混合字串]中間的文字[/標籤名稱:英數混合字串]
1
[標籤名稱=屬性值:英數混合字串]中間的文字[/標籤名稱:英數混合字串]
1
[標籤名稱="屬性值"]中間的文字[/標籤名稱:英數混合字串]

轉換後就要變成:

1
[標籤名稱]中間的文字[/標籤名稱]

不過 color 標籤比較特別,要轉換成:

1
[標籤名稱=屬性]中間的文字[/標籤名稱]

像這樣有規則可循的替換,我們就能使用 Regular Expression (正規表示式) 來完成它。

Regular Expression 裡,替換函式是 preg_replace() ,所以我寫了以下的程式測試:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$document = <<<EOD
[code:07318bb522][/code:07318bb522]
[color=blue:8841ec98ad][/color:a76c137a60]
[b:8841ec98ad] [/b:6e4e964058]
[quote="Anonymous"] [/quote:6e4e964058]
EOD;
$pattern = array(
"/\[([a-z]+)([=\w]*):[\w]*\](.*?)\[\/\\1:[\w]*\]/i",
"/\[([a-z]+)=\"[\w]*\"\](.*?)\[\/\\1:[\w]*\]/i",
);
$replace = array(
"[\\1\\2]\\3[/\\1]",
"[\\1]\\2[/\\1]",
);
$text = preg_replace($pattern, $replace, $document);
print $text;
?>

preg_replace() 第一個參數是要搜尋的 Pattern ,可以是一個字串,也可以是陣列;第二個參數則是要替換的文字,一樣可為字串或陣列。當兩個參數皆使用陣列時, preg_replace() 會幫我們以一對一的方式將 $pattern 陣列中比對到的文字,用 $replace 陣列來替換掉。

回到上面的程式,第一個 Pattern 是:

1
/\[([a-z]+)([=\w]*):[\w]*\](.*?)\[\/\\1:[\w]*\]/i

我先比對前面的標籤好了,首先是左方括號 \[ ,因為它屬於特殊字元,所以要用反斜線將它脫逸 (Escape) ;然後是標籤 ([a-z]+) ,再比對可有可無的屬性 ([=\w]*) ,接著是冒號加上英數混合字串 [\w]* ,最後是右方括號 \] ;這樣就會比對出 [code:07318bb522] 、 [color=blue:8841ec98ad] 及 [b:8841ec98ad] 。

而中間的文字是可有可無的,我就用 (.*?) 來將它比對出來。

最後是後面的標籤,方括號、冒號及英數混合字串是和上面一樣的比對方式;然而這裡用到一個 \1 ,指的是把前面第一個小括號裡所比對到的文字拿來再比一次,這就是所謂的 Back references (中文我不會翻) ;也就是說,如果我前面比對到的標籤名稱是 color ,那麼 \1 就會再用 color 來比對,以達到兩兩對稱的目的。

那第二個 Pattern 是作什麼用的?不是還有一個 quote 標籤還沒比到嗎?我比較笨,在第一個 Pattern 中我並沒辦法完全比對到 quote="Anonymous" ,所以我只好透過第二個 Pattern 來將它比對出來。

1
/\[([a-z]+)=\"[\w]*\"\](.*?)\[\/\\1:[\w]*\]/i

只看前面的標籤就行了,後面的標籤和第一個 Pattern 是一樣的。略過方括號不提,先用 ([a-z]+) 比對標籤名稱,然後是等號 = ,接著是 \"[\w]*\" 比對出帶有雙引號的屬性。

比對完之後,接著就是要將它替換掉了。在替換的字串裡,我們不需要再對特殊字元做脫逸,不過 Back references 仍然有用。 Back references 會對應到 Pattern 內有小括號的地方,然後依次替換,所以對第一個 Pattern ,我就用以下的字串來替換:

1
[\\1\\2]\\3[/\\1]

這時候 \1 就是標籤名稱, \2 則是等號及屬性, \3 當然就是中間的文字。

而第二個 Pattern 就用以下字串替換:

1
[\\1]\\2[/\\1]

原理就和上面是一樣的。

當然以上的替換方式還不是很周全,應該有很多狀況無法處理。不過我想基本原理掌握後,應該能夠舉一反三吧。

請大家指教。

Comments