第十九章:正則表達式的模式匹配


正則表達式,是一個描述字符模式的對象。javascript的RegExp類表示正則表達式,String和RegExp都定義了方法,后者使用正則表達式進行強大的模式匹配文本檢索與替換功能。javascript的正則表達式是perl5的這種表達式語法的大小子集,所以多有perl編程經驗的程序員來說,學習javascript的正則表達式是小菜一碟。

本章首先介紹用以描述“文本模式”的正則表達式語法。隨后講解了使用表達式String和RegExp方法

1.正則表達式的定義

javascript中的正則表達式用RegExp對象表示,可以使用RegExp()構造函數來創建RegExp對象,不過RegExp對象更多是通過一種特殊的直接語法量來創建;就像通過引號包裹字符的方式來定義字符串常量一樣,這種表達式直接量定義為包含在一堆斜杠(/)之間的字符。例如:

var patten = /s$/;

運行這段代碼創建一個新的RegExp對象,並將它賦值給patten.這個特殊的RegExp對象用來匹配以"s"結尾的字符串。用構造函數也RegExp()也可以定義個與之等價的正則表達式,代碼如下:

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

RegExp直接量和對象的創建

就像字符串和數字一樣,程序中每個取值相同的原始類型直接量均表示相同的值,這是顯而易見的。程序運行時每次遇到對象直接量(初始化表達式)諸如{}和[]的時候都會創建新對象。比如,如果在循環體中寫 var a = [],則每次遍歷都會創建一個新的空數組

正則表達式直接量與此不同,ECMAscript3規定,一個正則表達式直接量會在執行到它時轉化為一個RegExp對象,則同一段代碼所表示正則表達式直接量的每次運算都返回同一個對象。ECMAScript5做了相反的規定,同一段代碼所表示的這種表達式直接量每次運算都返回新對象。IE一直都是按照ECMAScript5規范實現的。

 

正則表達的模式規則是由一個字符序列組成的。包括所有字母和數字在內,大多數的字符都是按照直接量僅描述待匹配的字符的。比如說來,正則表達式/java/可以匹配任何包含“java”子串的字符串。除此之外,正則表達式中還有其他具有特殊語義的字符,這些字符並不按照字面含義進行匹配,比如正則表達式/s$/包含兩個字符,第一個字符“s”按照字面含義匹配,第二個字符$是一個具有特殊意義的元字符。用以匹配字符串的結束。因此,這個正則表達式表示可以以任何“s”結尾的字符串。

接下來幾節,講述javascript正則表達式中使用各種字符和元字符。

i.直接量字符

正如上文所提到的,正則表達式中所有字符和數字都是按照字面含義進行匹配的。javascript正則表達式語法也支持飛字母的字符匹配,這些字符需要通過反斜線(\)作為前綴進行轉轉義。比如,轉義符 \n用以匹配換行符。 下面李處了這些轉義字符。

javascript正則表達式中的直接量字符

字符 匹配
字母和數字字符 自身
\o NUL字符(\u0000)
\t 制表符(\u0009)
\n 換行符(\u000A)
\v 垂直制表符(\u000B)
\f 換頁符(\u000C)
\r 回車符(\u000D)
\xnn 由十六進制數指定的拉丁字符,例如\x0A等價於\n
\uxxxx 由十六進制數xxxx指定的Unicode字符,例如\u0009等價於\t
\cX 控制字符^X,例如 ,\cJ等價於換行符\n

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

^ $ . * + ? = ! : | \ / ( ) [ ] { }

在接下來的幾節里,我們將學習這些符號的含義,這些些符號只有在正則表達式的某些上下文才有特殊含義。在其他上下文中則被當成直接量處理。然而想在正則表達式中使用這些字符的直接量進行匹配,則必須使用前綴\,這是一條通行規則。其他標點符號(比如@和引號)沒有特殊含義,在正則表達式中按照字面量含義進行匹配。

如果不記得那些標點符號需要反斜線轉義,可以在每個標點符號前都加上反斜線。另外需要注意,許多字面和數字在反斜線前做前綴也有特殊含義,所以,對於想按照直接量進行匹配的字面和數字,盡量不要用反斜線進行轉義。當前,想要在正則表達式中按照直接量匹配反斜線本身,則必須使用反斜線將其轉義。比如,正則表達式“/\\/”用以匹配任何包含反斜線的字符串

ii.字符類

將直接量字符單獨放進方括號內疚成了字符類(character class)。一個字符類可以匹配它包含的任意字符。

因此,正則表達式/[abc]/就和字母"a"、“b”、“c”中的任意一個都匹配。

另外,可以通過"^"來定義否定字符類,它匹配不包含在方括號內的字符。定義否定字符類時,將一個"^"作為左方括號內的第一個字符。正則表達式/[^abc]/匹配是的“a”、“b”、“c”之外的所有字符。

字符類可以使用連字符來表示字符的范圍。要匹配拉丁字母表中的小寫字母,可以使用/[a-z]/,要匹配拉丁字母表中任何字母和數字,則使用/[a-zA-Z0-9]/

由於某些字符類非常常用,因此在javascript的正則表達式語法中,使用這些特殊字符的轉義來表示它們。例如,\s匹配空格符、制表符和其它Unicode空白符。\S匹配的是非Unicode空白符的字符。下面列出了這些字符,並且總計了字符類的語法。(注意,有些字符類轉義只能匹配ASCII字符,還沒有擴展到可以處理Unicode類字符,例如/[\u0400-\u04FF]/用以匹配所有的Cyrillic字符(是一種斯拉夫語字母))。

正則表達式的字符類

字符  匹配 
 [...] 方括號內的任意字符 
 [^...]  不在方括號內的任意字符
 . 除換行符和其它Unicode行終止符之外的任意字符 
 \w 任何ASCII字符組成的單詞,等價於[a-zA-Z0-9] 
 \W 任何不是ASCII字符組成的單詞,等價於[^a-zA-Z0-9] 
 \s 任何Unicode空白符 
 \S 任何非Unicode空白符的字符,注意\w和\S不同 
 \d 任何ASCII數字,等價於[0-9] 
\D    除了ASCII數字之外的任何字符,等價於[^0-9]
 [\b]  退格直接量(特例)

 注意,在方括號之內也可以寫成這些特殊轉義字符。比如,由於\s匹配所有的空白字符,\d匹配的是所有數字,因此,/[\s\d]/就是匹配任意空白或數字。

注意,這里有一個特例,我們將會看到,轉義符\b具有特殊含義,當用在字符類中時,它表示的是退格符,所以要在正則表達式中按照直接量表示一個退格符,只需要使用具有一個元素的字符類/[\b]/ 

iii.重復
可以把兩位數描述成/\d\d/,四位數描述成/\d\d\d\d/.但到目前為止,還沒有一種方法可以用來描述多位數字,或者描述三個字母和一個數字構成的字符串。這些正則表達式語法中較為復雜的模式都提到了正則表達式中某元素的“重復出現次數”。

我們在正則模式之后跟隨用以指定字符重復的標記。由於某些重復種類非常常用,因此,就有一些專門用於表示種植情況的特殊字符。例如:“+”用以匹配前一個或多個副本。下面的表總結了表示重復的正則語法。

正則表達式的重復字符語法

字符 含義
 {n,m} 匹配前一項至少n次嗎,但不能超過m次 
 {n,} 匹配前一項n次或者更多次   
 {n} 匹配前一項n次   
 ? 匹配前一項0次或1次,也就是說前一項是可選的,等價於{0,1} 
+   匹配前一項1次或多次,等價於{1,}
 *  匹配前一項0次或多次,等價於{0,}

這里有一些例子:

    /\d{2,4}/         //匹配2~4個數字
    /\w{3}\d?/        //精確匹配三個單詞和一個可選的數字
    /\s+java\s+/      //匹配前后帶一個或多個空格的字符串“java”
    /[^(]*/           //匹配一個或多個非左括號的字符

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

非貪婪的重復

上表中匹配重復字符是盡可能多地匹配,而且允許后繼的正則表達式繼續匹配。因此,我們稱之為“貪婪的”匹配。我們同樣可以使用這種表達式進行“非貪婪”的匹配。只需在待匹配的字符后跟隨一個問號即可:“??”、“+?”、"*?"或"{1,5}"。比如,正則表達式/a+/可以匹配一個或多個連續的字母a。當使用"aaa"作為匹配的字符串時,正則表達式會匹配它的三個字符。但是/a+?/也可以匹配一個或多個連續字母a,但它是盡可能少的匹配。我們同樣將"aaa"作為匹配字符串,但最后一個模式只能匹配第一個a。


使用非貪婪的匹配模式所得到的結果可能和期望的並不一致。考慮以下正則表達式/a+b/,它可以匹配一個或多個a,以及一個b。當使用它來匹配"aaab"時,你期望他能匹配一個a和最后一個b。但實際上,這個模式卻匹配了整個字符串。現在再試一下非貪婪的匹配版本/a+?b/,它匹配盡可能少的a和一個b,當它用來匹配"aaab"時,你期望它能匹配一個a和最后一個b。但實際上,這個模式卻匹配了整個字符串,和該模式的貪婪模式一模一樣。這是因為正則表達式的匹配模式匹配總是會尋找字符串中第一個可能匹配的位置。由於該匹配是從字符串的第一個字符開始的,因此在這里不考慮它的子串中更短的匹配。

iiii.選擇、分組和引用
正則表達式的語法還包括指定選擇項、子表達式分組和引用前一子表達式的特殊字符。字符"|"用於分隔供選擇的字符。例如/ab|cd|ef/可以匹配字符串"ab",也可以匹配字符串"cd",還可以匹配字符串"ef"。/d{3}|[a-z]{4}/是三位數字或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/

在本章的最后幾節中,我們會看到不用創建帶數字編碼的引用,也可以對子表達式進行分組。它不是以"("和")"進行分組,比如,考慮下面的這個模式

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

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

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

正則表達式的選擇、分組和引用字符

 字符  含義
 | 選擇,匹配的是該符號左邊的子表達式或右邊的子表達式 
 (...)  組合,將幾個項組合成一個單元,這個單元可通過"*"、“+”、“?”和"1"等符號加以修飾,而且可以記住和這個組合相匹配的字符串以供此后的引用和使用
 (?:...)  只組合,將項目合並到一個單元,但不記憶與改組相匹配的字符
 \n  和第n個分組第一次匹配的字符相匹配,組是圓括號中子表達式(也有可能是嵌套的),組索引是從左到右的左括號數,"(?:"形式的分組不編碼。

iiiii.指定匹配的位置

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

 例如,要匹配單詞“JavaScirpt”,可以使用正則表達式/^JavaScript$/。如果想匹配"Java"這個單詞本身(不像在“JavaScript”做單詞的前綴),可以使用/\sJavas/,可以匹配前后都有空格的單詞“Java”。但是這樣做有兩個問題,第一。如果"Java"出現在字符串的開始或者結尾,就匹配不成功,除非開始和結尾處各有一個空格。第二個問題是,當找到了與之匹配的字符串時,它返回的匹配字符串的前端和后端都有空格。因此,我們使用單詞的邊界\b來代替真正的空格符\s進行匹配(或定位)。這樣的正則表達式就寫成了/\bJava\b/。元素\B把匹配的錨點定位在不是單詞的邊界之處。因此,正則表達式/\B[Ss]cript/與"JavaScript"和"postscript"匹配,但不予"script"和“Scripting”匹配。

任意正則表達式都可以作為錨點條件。如果在符號"(?="和")"之間加入一個表達式,它就是一個先行斷言,用以說明圓括號內的表達式必須正確匹配,但不是真正意義上的匹配,可以使用/[Jj]ava([Ss]cript)?(?=\)/。這個正則表達式可以匹配“Jvascript: The Definitive Guide”中的javascript,但不能匹配"Java in NutShell"中的"Java",因為它后面沒有冒號。

正則表達式中的錨字符

字符 含義
 ^  匹配字符串的開頭,在多行檢索中,匹配一行的開頭
$ 匹配字符串的結尾,在多行檢索中,匹配一行的結尾
\b 匹配一個單詞的邊界,簡而言之,就是位於字符\w和\W之間的位置,或位於字符\w和字符串開頭或者結尾之間的位置。(但需要注意,[\b]匹配的是退格符)
\B 匹配非單詞邊界的位置
(?=p) 零寬正向先行斷言,要求接下來的字符都與p匹配,但不包括匹配p的那些字符
(?!p)  零寬負向先行斷言 ,要求接下來的字符不與p匹配

iiiiii.修飾符

正則表達式的最后一個知識點,即正則表達式的修飾符,用以說明高級匹配模式的規則。和之前討論的正則表達式語法不同,修飾符是放在"/"符號之外的。也就是說,它們不是出現在兩條斜線之間。而是兩條斜線之后。Javascript支持三個修飾符,修飾符"i"用於說明匹配是不區分大小寫的修飾符'g'說明模式匹配應該是全局的,也就是說,應該找出被檢索字符串中所有的匹配修飾符"m"用以在多行模式中執行匹配。在這種模式下,如果待檢索的字符有多行,那么^和$錨字符除了匹配整個字符串的開始和結尾之外,還能匹配每行的開始和結尾。比如,/java$/im,可以匹配java,也可以匹配"Java\nis Fun".

  這些修飾符可以任意組合,比如,要想不區分大小寫匹配字符串中的第一個單詞"java"(或"Java"或"JAVA"等),可以使用不區分大小寫的修飾符來定義正則表達式/\bjava\b/i ,如果想要匹配字符串中所有的單詞,則需要添加修飾符g. /\bjava\b/gi

2.用於模式匹配的String方法

  到目前位置,本章已經討論過正則表達式的語法,但還沒有嘗試在javascript使用這些正則表達式。本節將討論String對象的一些用以執行正則表達式模式匹配和檢索替換操作的方法,后續幾節還會討論如歌使用javascript正則表達式的模式匹配,不過車種與RegExp的對象和它的方法及屬性。

 String支持4種使用正則表達式的方法。最簡單的是search()。它的參數是一個正則表達式,返回第一個與之 匹配的子串的位置,如果找不到匹配的子串,它將返回-1。比如下面的調用返回值為4:

"javascript".search(/script/i)

如果search()的參數不是正則表達式,則首先會通過RegExp構造將它轉換成正則表達式,search()方法不支持全局檢索,因為它忽略正則表達式參數中的修飾符g.

replace()方法用於執行檢索與替換操作。其中第一個參數是正則表達式,第二個參數是要進行替換的字符串。這個方法會對調用它的字符串進行檢索,使用指定的模式來匹配。如果正則表達式中設置了修飾符g,那么源字符串中所有與模式匹配的自傳都將替換成第二個參數指定的字符串;不帶修飾符g,則只替換所有匹配的第一個子串。如果replace()的第一個參數是字符串而不是正則表達式,則replace()將直接搜索這個字符串,而不是像search()一樣先通過RegExp()將它轉換為正則表達式。比如,可以使用下面的方法,利用replace()將文本中所有的javascript(不區分大小寫)統一替換為"JavaScript":

text.replace(/javascript/gi,"JavaScript");

但replace()的功能遠不至於如此。正則表達式中使用的圓括號括起來的子表達式是帶有從左到右的索引編號的 ,而且正則表達式會記憶與每個字表達式匹配的文本。如果在替換字符串中出現了這兩個字符串。這是一個非常有用的特性。比如,可以用它將一個字符串中的英文引號替換為中文半引號。

//一段引用文本始於引號,結束於引號,中間的內容不能包含引號
var quote = /"(^"*)"/g;
// 用中文半引號替換英文引號,同時要保持引號之間的內容(存儲在$1)沒有被修改
text.replace(quote,'"$1"');

replace()方法還有一些其他的重要特性,這些特性將在第三部分的String.replace()的主題頁中進行介紹。最值得注意的是,replace()方法的第二個參數可以是函數,該函數能夠動態地計算替換字符串。

match()方法是最常用的String正則表達式方法。它唯一的參數就是一個正則表達式(或通過RegExp()構造函數將其轉換為正則表達式),返回的是一個由匹配結構鋼組成的數組。如果該正則表達式設置了修飾符g,則該方法返回的數組包含字符串中所有匹配的結果。例如:

    "1 plus 2 equals 3".match(/\d+/g)    //返回 ["1", "2", "3"]

如果這個正則表達式沒有設置修飾符g,match()就不會進行全局檢索,它只檢索第一個匹配。即使match()執行的不是全局檢索,它也返回一個數組。在這種情況下,數組的第一個元素就是匹配的字符串,余下的元素則是正則表達式中用圓括號括起來的字表達式。因此,如果match()返回一個數組a,那么a[0]存放的是完整匹配,a[1]存放的則是與第一個用圓括號括起來的表達式相匹配的子串,以此類推。為了和方法replace()保持一致。a[n]存放的是$n的內容

例如,使用如下的代碼來解析一個URL;

    var url = /(\w+):\/\/([\w.]+)\/(\S*)/;
    var text = "Visit my blog at http://www.example.com/~david";
    var result = text.match(url);
    if(result != null){
        var fullurl = result[0];
        var protocol = result[1];
        var host = result[2];
        var path = result[3];
    }

值得注意的是,給字符串的match()方法傳入一個非全局的正則表達式,實際上和給這個正則表達式exec()方法傳入的字符串是一模一樣的,它返回的數組帶有兩個屬性:index和input,接下來exec()方法的討論中會提到:

String對象的最后一個和正則表達式相關的方法是split()。這個方法用以將調用它的字符串拆分為一個子串組成的數組,使用的分割符是split()的參數,例如:

"123,456,789".split(","); //返回["123","456","789"]

split()方法的參數也可以是一個正則表達式,這使得split()方法異常強大。例如,可以支持分割符,允許兩邊可以留任意多的空白符

"1, 2, 3, 4, 5".split(/\s*,\S*/) //=> ["1", " 2", " 3", " 4", " 5"]

(本文完結)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM