第11章 使用正則表達式的模式匹配(一)

正則表達式 (regular expression) 是一個描述字符模式的對象。JavaScript 的 RegExp 類表示正則表達式,而 String 和 RegExp 都定義了使用正則表達式進行強大的模式匹配和文本檢索與替換的函數。

ECMAScript v3 對 JavaScript 正則表達式進行了標準化。JavaScript 1.2 實現了 ECMAscript v3 要求的正則表達式特性的子集,JavaScript 1.5 實現了完整的標準。JavaScript 的正則表達式完全以 Perl 程序設計語言的正則表達式工具爲基礎。粗略地說, JavaScript 1.2 實現了 Perl 4 的正則表達式,JavaScript 1.5 實現了 Perl 5 的正則表達式的大型子集。

本章定義了正則表達式用來描述文本模式的語法。它還介紹了使用正則表達式的 String 與 RegExp 方法。

11.1 正則表達式的定義

在 JavaScript 中,正則表達式由 RegExp 對象表示。當然,可以使用 RegExp() 構造函數創建 RegExp 對象,不過通常還是用特殊的直接量語法來創建 RegExp 對象。就像字符串直接量被定義爲包含在引號內的字符一樣,正則表達式直接量也被定義爲包含在一對斜槓 (/) 之間的字符。所以,JavaScript 可能會包含如下的代碼:

var pattern = /s$/;

這行代碼創建了一個新的 RegExp 對象,並且將它賦給了變量 pattern 。這個特殊的 RegExp 對象和所有的以字母 “s” 結尾的字符串都匹配。用構造函數 RegExp() 也可以定義一個等價的正則表達式,其代碼如下:

var pattern = new RegExp("s$");

無論使用正則表達式直接量還是用構造函數 RegExp() ,創建一個 RegExp 對象都比較容易。較爲困難的是用正則表達式語法來描述所需的字符的模式。JavaScript 採用的是 Perl 語言使用的正則表達式語法的相當完整的子集,因此,如果讀者是一個經驗豐富的 Perl 程序員,那麼就會知道在 JavaScript 中如何描述模式。

正則表達式的模式規範是由一系列字符構成的。大多數的字符 (包含所有的字母數字字符) 描述的都是按照直接量進行匹配的字符這樣說來,正則表達式 /java/就和所有的包含子串 "java" 的字符串相匹配。雖然正則表達式中的其他字符不是按照直接量進行匹配的,但是它們都具有特殊的意義。例如,正則表達式 /s$/ 包含兩個字符。第一個字符 "s" 按照直接量與自身相匹配。第二個字符 "$" 是一個特殊元字符,它所匹配的是字符串的結尾所以正則表達式 /s$/ 匹配的就是以字母 "s" 結尾的字符串

接下來的幾節介紹了用於 JavaScript 正則表達式的各種字符和元字符。但是注意,有關正則表達式語法的完整介紹遠遠超出了本書的範圍。

11.1.1 直接量字符

正如前面所提到的,在正則表達式中所有的字母字符和數字都是按照直接量與自身相匹配的。JavaScript 的正則表達式語法還通過以反斜槓 (\) 開頭的轉義序列支持某些非字母的字符。例如,序列 "\n" 在字符串中匹配的是直接量換行符。下表列出了這些字符。


在正則表達式中,許多標點符合具有特殊的含義。它們是:


在接下來的幾節中,我們將學習這些符號的含義。某些符號只在正則表達式的特殊環境中才具有特殊的含義,在其他環境中則被按照直接量進行處理。但是,作爲一個通用的原則,如果在正則表達式中按照直接量使用這些標點符合,就必須加前綴 \ 。其他標點符號 (如引號和 @) 沒有特殊含義,在正則表達式中只按照直接量匹配自身。

11.1.2 字符類

將單獨的直接量字符放進方括號內就可以組合成字符類 (character class)。一個字符類和它所包含的任何字符都匹配所以正則表達式 /[abc]/ 就和字母 "a" ,"b" ,"c" 中的任何一個字母都匹配。另外,還可以定義否定字符類,這些類匹配的是不包含在方括號之內的所有字符。定義否定字符類的時候,要將一個 ^ 符合作爲左方括號後的第一個字符。正則表達式 /[^abc]/ 匹配的是 "a" 、“b” 、"c" 之外的所有字符。字符類可以使用連字符來表示一個字符範圍。要匹配拉丁字母集中的任何小寫字符,可以使用 /[a-z]/,要匹配拉丁字母集中任何字母數字字符,則使用 /[a-zA-Z0-9]/ 。

由於某些字符類非常常用,所以 JavaScript 的正則表達式語法就包含了一些特殊字符和轉義序列來表示這些常用的類。例如, \s 匹配的是空格符、製表符和其他 Unicode 空白符,\S 匹配的是非 Unicode 空白符的字符。下表列出了這些字符,並且總結了字符類的語法。(注意,有些字符類轉義序列只匹配 ASCII 字符,還沒有擴展到可以處理 Unicode 字符。可以顯式地定義自己的 Unicode 字符類,例如, /[\u0400-\u04FF]/ 匹配所有的 Cyrillic 字符)。


注意,在方括號之內也可以使用這些特殊的字符轉義序列。例如 \s 匹配的是所有的空白符,\d 匹配的是所有數字,那麼 /[\s\d]/ 就匹配任意的空白符或數字。注意,這裏有一個特例。下面我們將會看到轉義序列 \b 具有特殊含義,當用在字符類中時,它表示的是退格符,所以要在正則表達式中按照直接量表示一個退格符,只需要使用具有一個元素的字符類/[\b]/

11.1.3 重複

用剛剛學過的正則表達式的語法,可以把兩位數描述成 /\d\d/ ,把四位數描述成 /\d\d\d\d/ 。但是還沒有一種方法可以用來描述具有任意多數位的數字,或描述字符串,這個字符串由三個字母以及跟隨在字母之後的一位數字構成。這些較複雜的模式使用的正則表達式語法都指定了該正則表達式中的一個元素重複出現的次數。

指定重複的字符總是出現在它們所作用的模式之後。由於某種重複類型相當常用,所有有一些特殊的字符專門用於表示這種情況。例如,+號匹配前一模式的一個或多個副本。下表總結了這些重複的語法。


下面的代碼是一些例子:

/\d{2,4}/                       // 匹配 2 到 4 個數字

/\w{3}\d?/                    // 匹配 3 個字符 和 0 或 1個數字

/\s+java\s+/                // 匹配 "java" 前後有 1 個或多個空白符

/[^"]*/                            // 匹配 0或多個不是 " 的字符

在使用重複字符 * 和 ? 時要注意,由於這些字符可能匹配前面字符的 0 個實例,所以它們允許什麼都不匹配。例如,正則表達式 /a*/ 實際上與字符串 "bbbb" 匹配,因爲這個字符串含有 0個 a 。

非貪婪的重複

上表中列出的重複字符可以匹配儘可能多的字符,而且允許接下來的正則表達式繼續匹配。因此,我們說重複是 "貪婪的" 。可以以非貪婪的方式進行重複(在 JavaScript 1.5 和其後的版本中可以做到,這是 Perl 5 的一個特性,JavaScript 1.2 沒有實現它),只需要在重複字符後加問號即可 (如 ?? 、+? 、*? ,甚至 {1,5}?)。例如,正則表達式 /a+/ 匹配一個或多個字符 a 。將其應用到字符串 "aaa" 上時,它與三個字母都匹配。但是 /a+?/只匹配一個或多個必要的字母 a 。將其應用到同樣的字符串上時,該模式只匹配第一個字母 a 。

使用非貪婪的重複生成的結果並不總是與期望一致。考慮模式 /a*b/ ,它匹配 0個或多個字母 a 後跟隨字母 b 。在應用到字符串 “aaab” 上時,它匹配整個字符串。現在使用非貪婪的重複版本 /a*?b/ ,它應該匹配字母b ,通過在字母 b 前加最少的字母 a 。在應用到同一個字符串 "aaab" 上時,讀者可能以爲它只匹配最後一個字母 b 。但事實上,這個模式也匹配整個字符串,和該模式的貪婪版本一樣。這是因爲正則表達式模式匹配是在尋找字符串中第一個可能匹配的位置。該模式的非貪婪版本在字符串的第一個字符處不匹配,所以該匹配將返回,甚至不考慮對後面的字符進行匹配。

11.1.4 選擇、分組和引用

正則表達式的語法還包括指定選擇項、對子表達式分組和引用前一子表達式的特殊字符。字符 “|” 用於分隔供選擇的字符。例如 /ab|cd|ef/ 匹配的是字符串 “ab” ,或者是字符串 "cd" ,又或者是字符串 "ef" 。/\d{3}|[a-z]{4}/ 匹配的是三位數字或四個小寫字母。

注意,選擇項是從左到右考慮,直到發現了匹配項。如果左邊的選擇項匹配,就忽略右邊的匹配項,即使它產生更好的匹配。因此,把模式 /a|ab/ 應用到字符串 "ab" 上時,它只匹配第一個字符。

在正則表達式中括號具有幾種作用。一個作用是把單獨的項目組合成子表達式,以便可以像處理一個獨立的單元那樣用 | 、*、+或 ? 等來處理它們。例如 ,/java(script)?/ 匹配的是字符串 "java" ,其後既可以有 "script" ,也可以沒有。 /(ab|cd)+|ef/ 匹配的既可以是字符串 "ef" ,也可以是字符串 "ab" 或者 "cd" 的一次或多次重複。

在正則表達式中,括號的另一個作用是在完整的模式中定義子模式。當一個正則表達式成功地和目標字符串相匹配時,可以從目標串中抽出和括號中的子模式相匹配的部分。例如,假定我們正在檢索的模式是一個或多個小寫字母后面跟隨了一位或多位數字,則可以使用模式 /[a-z]+\d+/ 。但是假定我們真正關心的是每個匹配尾部的數字,那麼如果將模式的數字部分放在括號中 (/[a-z]+(\d+)/) ,就可以從所檢索到的任何匹配中抽取數字了,之後我們會對此進行解釋。

帶括號的子表達式的另一個用途是允許我們在同一正則表達式的後部引用前面的子表達式。這是通過在字符 "\" 後面加一位或多位數字實現的。數字指定了帶括號的子表達式在正則表達式中的位置。例如,\1引用的是第一個帶括號的子表達式,\3引用的是第三個帶括號的子表達式。注意,由於子表達式可以被嵌套在別的子表達式中,所以它的位置是被計數的左括號的位置。例如,在下面的正則表達式中,嵌套的子表達式 ([Ss]cript) 就被指定爲 \2 。

/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/

對正則表達式中前一子表達式的引用所指的並不是那個子表達式的模式,而是與那個模式相匹配的文本。這樣,引用可以用於實施一條約束,即一個字符串各個單獨的部分包含的是完全相同的字符。例如,下面的正則表達式匹配的就是位於單引號或雙引號之內的0個或多個字符。但是,它並不要求開始和結束的引號匹配 (例如兩個都是雙引號或單引號):

/[' "][^' "]*[' "]/

如果要求開始和結束的引號相匹配,可以使用如下的引用:

/([' "])[^' "]*\1/

\1 匹配的是第一個帶括號的子表達式所匹配的模式。在這個例子中,它實施了一條約束,那就是開始的引號必須和結束的引號相匹配。正則表達式不允許用雙引號括起的字符串中有單引號,反之亦然。在字符類中使用引用是不合法的,所以我們不能編寫:

/([' "])[^\1]*\1/

在本章後面的小節中,我們會看到一種對帶括號的子表達式的引用,它們是對正則表達式進行檢索和替換操作的強大特性之一。

在 JavaScript 1.5 (不是 JavaScript 1.2) 中,無須創建帶編碼的引用就可以將正則表達式中的項目進行組合。它不是以 "(" 和 ")" 對項目進行分組,而以 "(?:" 和 ")" 來分組。考慮如下的模式,例如:

/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/

這裏,子表達式 (?:[Ss]cript) 僅僅用於分組,因此複製符合 "?" 可以應用到各個分組。這種改進了的括號並不生成引用,所以在這個正則表達式中,\2 引用了與 (fun\w*) 匹配的文本。

下表總結了正則表達式的選擇、分組和引用運算符。


11.1.5 指定匹配的位置

正如前面所介紹的,正則表達式中的多個元素才能夠匹配字符串中的一個字符。例如 \s 匹配的只是一個空白符。還有一些正則表達式的元素匹配的是字符之間的位置,而不是實際字符。例如 \b 匹配的是一個單字的邊界,即位於 \w (ASCII 單字字符) 字符和 \W (非單字字符) 之間的邊界,或位於一個 ASCII 單字字符與一個字符串的開頭或結尾之間的邊界。像 \b 這樣的元素不指定匹配的字符串中使用的字符,它們指定的是匹配所發生的合法位置。有時我們稱這些元素爲正則表達式的錨,因爲它們將模式定位在檢索字符串中的一個特定位置上。最常用的錨元素是 ^ ,它使模式定位在字符串的開頭,而錨元素 $ 則使模式定位在字符串的末尾。

例如,要匹配單字 "JavaScript" ,可以使用正則表達式 /^JavaScript$/ 。如果想檢索 "Java" 這個單字自身 (不像在 "JavaScript" 中那樣作爲前綴),可以使用模式 /\sJava\s/ ,它要求在單字 Java 之前和之後都要有空格。但是這樣做有兩個問題。第一,如果 "Java" 出現在一個字符串的開頭或結尾,該模式就會不與之匹配,除非在開頭處或者結尾處有一個空格。第二個問題是,當這個模式找到了一個與之匹配的字符串時,它返回的匹配字符串的前端和後端都有空格,這並不是我們想要的。因此,我們使用單字的邊界 \b 來代替真正的空格符 \s 進行匹配。結果表達式是 /\bJava\b/ 。元素 \B 將把匹配錨定在不是單字邊界的位置。因此,模式 /\B[Ss]cript/ 與 "JavaScript" 和 "postscript" 匹配,但是不與 "script" 和 "Scripting" 匹配。

在 JavaScript 1.5 中 (不是 JavaScript 1.2 ) ,還可以使用任意的正則表達式作爲錨定條件。如果在符合 "(?=" 和 ")" 之間加入一個表達式,它就是一個向前聲明,指定接下來的字符必須被匹配,但並不真正進行匹配。例如,要匹配一種常用的程序設計語言的名字,但只在其後有冒號時匹配,可以使用  /[Jj]ava([Ss]cript)?(?=\:)/ 。這個模式與 "JavaScript:  The Definitive Guide" 中的單字 "JavaScript" 匹配,但是與 "Java in a Nutshell" 中的 "Java" 不匹配,因爲其後沒有冒號。

如果用 "(?!" 引入聲明,它將是反前向聲明,指定接下來的字符都不必匹配。例如,/Java(?!Script)([A-Z]\w*)/ 匹配的是 "Java" 後跟隨一個大寫字母和任意多個 ASCII 單字字符,但是不能跟隨 "Script" 。它與 "JavaBeans" 匹配,不與 "Javanese" 匹配,與 "JavaScrip" 匹配,但不與 "JavaScript" 或 "JavaScripter" 匹配。

下表總結了正則表達式的錨


11.1.6 標誌

正則表達式的語法還有最後一個元素,即正則表達式的標誌,它說明高級模式匹配的規則。和其他的正則表達式語法不同,標誌是在 "/" 符號之外說明的,即它們不出現在兩個斜槓之間,而是位於第二個斜槓之後。JavaScript 1.2支持兩個標誌。標誌 i 說明模式匹配不區分大小寫。標誌 g 說明模式匹配應該是全局的,也就是說,應該找出被檢索的字符串中所有的匹配。這兩種標誌聯合起啦就可以執行一個全局的不區分大小寫的匹配。

例如,要執行一個不區分大小寫的檢索以找到單字 "java" (或者 “Java” 、"JAVA" 等) 的第一次出現,可以使用不區分大小寫的正則表達式 /\bjava\b/i 。如果要在一個字符串中找到所有出現的 "java" ,需要添加標誌 g ,即 /\bjava\b/gi 。

JavaScript 1.5 還支持一個標誌 m ,它以多行模式執行模式匹配。在這種模式中,如果要檢索的字符串中含有換行符, ^ 和 $ 錨除了匹配字符串的開頭和結尾外還匹配每行的開頭和結尾。例如,模式 /Java$/im 匹配 "java" 和 "Java\nis fun" 。

下表總結了這些正則表達式的標誌。注意,我們在本章後面的小節中介紹用於實際執行匹配的 String 和 RegExp 方法時還將看到更多有關標誌 g 的用法。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章