正則表示式的真正威力(0)
我在瀏覽StackOverflow上關於PHP的問題時經常看到有人提問如何使用正則表示式解析HTML。對於該問題的回答通常是這樣的:
你不能使用正則表示式解析HTML,因為HTML不是正則的。使用XML解析器吧。
這種回答——在該問題的語境下——是有誤導性的甚至是完全錯誤的。接下來我會展示現代正則表示式是多麼強大。
“正則”到底是什麼意思
在形式語言理論中,說一個東西是“正則(regular)”那麼其語法的產生式規則必須滿足如下形式之一:
B -> a
B -> aC
B -> ε
這些帶箭頭的規則可以這樣讀,“左邊可以被右邊替換”。所以第一條規則是“B可以用a替換”,第二條是“B可以用aC替換”,第三條是“B可以用空字串替換”(ε是代表空字串的符號)。
B
,C
和a
又代表什麼呢?根據慣例,大寫字母表示“非終結符”——可以進一步分解的符號,小寫字母表示“終結符”——不能進一步分解的符號。
這麼說可能有點抽象,我們來看一個例子:自然數的語法定義如下。
N -> 0 N -> 1 N -> 2 N -> 3 N -> 4 N -> 5 N -> 6 N -> 7 N -> 8 N -> 9 N -> 0N N -> 1N N -> 2N N -> 3N N -> 4N N -> 5N N -> 6N N -> 7N N -> 8N N -> 9N
這個語法表達了下面的意義:
自然數N是
... 0到9的任意一個數字
或者
... 0到9的任意一個數字後面再加上一個自然數
這個例子中數字0到9就是終結符(不能進一步分解)並且N是唯一的非終結符(可以進一步分解)。
如果將上面自然數的語法規則和正則語法的定義比較一下,你就會發現自然數語法規則滿足標準:前面十條規則符合形式B -> a
並且接下來的十條規則符合形式B -> aC
。因此定義自然數的語法是正則的。
你可能還注意到了,定義這樣一個簡單的東西的語法就顯得很臃腫。如何用更加簡潔的方式來表達相同的概念呢?
這就是正則表示式派上用場的時候了:上面的語法等同於正則表示式[0-9]+
正則表示式可以匹配什麼
那麼問題就來了,正則表示式只可以匹配正則的語法嗎?答案是肯定也是否定的:
按照形式語法中的定義來看正則表示式就只是用於解析正則的語法。然而當程式設計師談論“正則表示式”時,不是說的形式語法,指的是他們所使用的程式語言實現的正則表示式變體。並且這些正則實現和原始的正則式概念沒多大關係。
任何一個現代正則表示式實現能匹配的語言遠超過正則語言。接下來就要討論正則的能力界限到底在哪兒。
為了方便討論,我將會專注於PCRE的正則實現,因為我對其瞭解最多(PHP使用該實現)。大部分其他正則實現是相似的,所以下面的討論也同樣適用。
語言層級
為了分析正則表示式能匹配什麼和不能匹配什麼,先了解一下除了正則語言還存在哪些其他型別的語言。下面是喬姆斯基譜系:
Chomsky hierarchy:
/-------------------------------------------\
| |
| Recursively enumerable languages | Type 0
| |
| /-----------------------------------\ |
| | | |
| | Context-sensitive languages | | Type 1
| | | |
| | /---------------------------\ | |
| | | | | |
| | | Context-free languages | | | Type 2
| | | | | |
| | | /-------------------\ | | |
| | | | Regular languages | | | | Type 3
| | | \-------------------/ | | |
| | \---------------------------/ | |
| \-----------------------------------/ |
\-------------------------------------------/
喬姆斯基將語言劃分為四種類型:
正則語言(型別3)是最弱的,然後是上下文無關語言(型別2),上下文相關語言(型別1)以及最後無所不能的遞迴可列舉語言(型別0)。
喬姆斯基譜系是一個包含性的層級,上圖中小盒子完全包括在更大的盒子裡面。例如每一個正則語言同時也是一個上下文無關語言(反之則不成立)。
因此我們在層級中向上走一級:我們已經知道正則表示式可以匹配任意正則語言。然而也可以匹配上下文無關語言嗎?
(請注意:我在說“正則表示式”的時候是從程式設計師的視角來看的,而不是形式語言理論的視角。)