本篇假設你已經學過一些正規表示式 (regular expression), 如果你對 regular expression 完全沒概念, 或者有些式子看不懂, 就要麻煩你先讀一下 JavaScript 的 regular expression 的說明.
在運用 regular expression 時, 表示式中如果有特殊義意的字元 (以下簡稱特殊字元) 需要跳脫其特殊義意回歸為不具備特殊義意的字元時, 必需在該字元前面加上脫逸字元\. 這樣已經夠麻煩了, JavaScript 還支援了二種生成 regular expression (RegExp) 物件的方法, 二個方法用的表示式字串還長得不一樣, 這使得多數人在第一次運用 regular expression 時經常時, 錯! 錯! 錯! 一錯再錯, 就是試不出來, 最後只好放棄.
這二種方法一種是表示式固定已知的字面 (literal) 表示式; 另一種則把表示式轉換為字串, 可以進行字串處理, 處理完後再拿來建立 RegExp 物件:
- 固定的字面表示式: 程式中使用/字元作為分隔字元(註一) 來括住表示式, 例如:
(意思是 'a' 後面接著 1 個以上的 'b' 再接著位於行末 'c')
var re = /ab+c$/; - 可變動表示式: 程式中使用"字元或是'字元來括住表示式, 可以執行字串串接或取代運算. 例如:
不設 flag 的例子設定了 flag "g" 的例子var x = "$"; var re = new RegExp("ab+c" + x);或者換用 ES6 的 Template Literalsvar re = new RegExp(rule.id+"\\s*:\\s*.*?"+rule.text+";?", "g");var re = new RegExp(`${rule.id}\\s*:\\s*.*?${rule.text};?`, "g");
其實第一個方法用的就是傳統表示式, 這對一般學過 regular expression 的人應該沒有問題. 而第二種方法 RegExp() 用的表示式則只是把第一個方法用的表示式字串化. 這原本不難, 之所以會令許多人困惑, 其實是初次使用大家容易把撰寫表示式和組字串二件事攪和在一起了. 事情一件一件來, 一次只處理一個就不會搞混. 因此要用 RegExp() 需要先進行撰寫傳統式的正規表示式, 完成了之後再來轉換成可以做為 RegExp() 參數的字串.
註一: 依據 PCRE, regular expression 的分隔字元 (delimiter) 可以是任意的 non-alphanumeric (非字母及數字), non-backslash (非倒斜線), non-whitespace (非空白) 字元. 我們可以把它分為二類:
- 前後一致的分隔字元, 一共有下列這 23 個: !, ", #, $, %, &, ', *, +, ,, -, ., /, :, ;, =, ?, @, ^, _, `, |, ~. 但 regular expression 與其他語言在一起工作時, 一般都需要再多排除一些字元, 例如: 在 javascript 裡 ", ' 及 ` 等三個字元必需排除. 一般比較常見的有: /, #, +, %.
- 需成對使用的分隔字元: (), {}, [], <>. 這部份因為和其他程式的運算式符號衝突, 容易造成使用者閱讀上的混淆, 所以比較少見.
二組相加一共 31 個字元.
Step1: 撰寫表示式
以下所列是個人在程式中撰寫表示式的步驟, 在此野人獻曝, 還請不否指教:
- 列出原始字串: 列出想要處理的原始字串. 還不是很熟 regular expression 的話原始字串可以有多列.
- 標記特殊字元: 標記原始字串中的特殊字元(註二), 但先不加上脫逸字元\. 意思是先找出字串中所有在 regular expression 語法中具有特殊意義的字元.
- 替換特殊字元: 以 regular expression 的規則, 把原始字串中的字串片段替換成由特殊字元組成的表示式片段 (包含合併多行原始字串).
- 檢查 greedy match: * (0次或多次), + (1次或多次), ? (0次或1次) 這 3 個表示次數的特殊字元預設會匹配儘可能多的字元(註三), 需要阻斷 greedy match 變成 non-greedy 時, 這三種字符的後面就要補一個?字元. 另外還有 {m,n} (指定發生次數) 也是喔!
- 加脫逸字元\: 進行到這個步驟時, 如果步驟 2 中標記的特殊字元還餘留在字串中則前面補一個脫逸字元\. (意即: 原本的字串含有特殊字元需要以原字元留在表示式中)
- 加上/: 表示式前後各加上一個/字元.
這樣傳統的表示式即完成.
註二: Regular expression 表示式中的特殊字元, 一共 1512+ 個, 如下: /, \, ^, $, (, ), [, ], {, }, ., *, +, ?, |, 另外再加上一個 regular expression 的結束分隔字元.
另外, 許多人在填寫 [] (字元集合) 當中的字元也時常會搞不清楚, 特殊字元是不是和[]外面的定義一樣?
答案是不一樣: [] (字元集合) 裡的特殊字元只有 5 個: ^, -, [, ], \.
同時有一點要注意: 有些以 \ 開頭的預定義字元集合是可以用在 [] 裡的, 例如: \n, \t, \s, \S, \w, \W. 這也是為何 \ 在 [] 裡是特殊字元.
註三: Greedy match 會匹配儘可能多的字元. 例如: .*是任意字元, 任意長度. 而.*;則是任意長度任意字元緊接著一個;, 但是字串中如果有多個;, 則.*會將前面的所有的;包含在內, 只留下最後一個;給.*;當中的;. 要打破這個原則, 則需要加一個?, 把.*;變成.*?;.
所以如果整個大字串中有 6 組字串用 5 個 ; 串起來, 你以為(.*;){3}會找到前 3 個;? 錯! 匹配會把所有的;全吃掉了. 第一次匹配會儘可能的長 (到第 3 組, 留下 2 個給後面的第二次及第三次), 必需改成 (.*?;){3}才是只找到前三組.
Step2: 轉換成 RegExp() 的參數
進行轉換時請把前一段 regular expression 的規則全部都忘掉, 我們現在要的只是在程式中撰寫一個字串, 字串內容是上一段還沒有用/括住的表示式 (即尚未進行步驟 6 的表示式字串). 而我們在 JavaScript 程式中組寫字串時需要注意的特殊字元就只有\和用來括住字串的"和'.
所以想要把表示式轉換成字串, 以便能當成 new RegExp() 的參數時, 只要再多下列 2 個步驟:
- 把表示式中的每一個\換成\\.
所以原始字串中的一個要保留的\字元, 在傳統表示式中會變成\\, 換成表示式字串則變成\\\\. 好可怕呀!! - 把括住表示式前後的/換掉: 換成用"或者'來括住表示式字串.
置換前先檢查表示式中有沒有"字元或'字元:
- 都沒有: 隨便選一個用
- 有": 用'來括住表示式字串
- 有': 用"來括住表示式字串
- 二個都有: 隨便選一個用, 但同時需要把表示式中所有的衝突的字元前面加脫逸字元\.
例如: 選用"來括住字串, 則原表示式中的"都要換成\".
請小心, 不要把這二個步驟次序弄反了.
另外有一點要特別注意的是: 這二個步驟是在撰寫 JavaScript 程式時用的, 是要組一個字串給 JavaScript 的語法分析器(註四)看的. 如果是想要讓使用者輸入一個 RegExp 可以處理的字串, 那就到上一段的步驟 5 就可以了. 也就是使用者輸入的內容是不需要用/括住, 直接輸入表示式字串即可. 而程式也是把輸入直接當參數送給 RegExp() 就可以了. 不必再多事把\換成\\以及在前後加".
註四: JavaScript 語法分析器的內容其實很複雜, 不過簡要一點的說 JavaScript 在檢查正規表示式定數 (regular expression literals) 時的規則如下:
- 第一個字元必需是/, 之後的內容 (必需以正規表示式的規則處理脫逸字元\的義意) 為表示式定數之內容, 直到再度出現/.
而檢查字串定數 (string literals) 時的規則如下:
- 第一個字元必需是", 之後的內容 (必需以雙括號字串的規則處理脫逸字元\的義意) 為字串定數之內容, 直到再度出現" 或者
- 第一個字元必需是', 之後的內容 (必需以單括號字串的規則處理脫逸字元\的義意) 為字串定數之內容, 直到再度出現'.
所以我們一般寫傳統的正規表示式已經滿足了正規表示式定數 (字面表示式) 的規則, 但是 RegExp() 的參數是字串, 所以就必需要再以字串定數的規則處理一次傳統的正規表示.
測試
表示式寫好之後當然要測試一下啊, 最簡單的方法就是把原始字串或者可能的輸入字串一個一個丟進去執行一下, 然後把結果 Show 出來囉.
字串物件有 4 個方法可用: match(), search(), replace(), split(), RegExp 物件有二個方法: exec(), test() 可用.
var re = /\w+\s/g;
var myArray = "fee fi fo fum".match(re);
console.log(myArray);
var re = new RegExp("\\w+\\s", "g");
var myArray = re.exec("fee fi fo fum");
console.log(myArray);
範例一
要處理的字串如下:
- .article-content-inner.zoom1 *
- .article-content-inner.zoom2 *
- .article-content-inner.zoom3 *
先找出特殊字元標記下來 (此例只有., *).
- .article-content-inner.zoom1 *
- .article-content-inner.zoom2 *
- .article-content-inner.zoom3 *
合併成一條規則:
.article-content-inner.zoom[1-3] *
加上脫逸字元\, 前後加/完成表示式
/\.article-content-inner\.zoom[1-3] \*/
如果還要轉成字串
"\\.article-content-inner\\.zoom[1-3] \\*"
var re = /\.article-content-inner\.zoom[1-3] \*/;
或者是
var re = new RegExp("\\.article-content-inner\\.zoom[1-3] \\*");
範例二
要處理的字串如下:
- http://www.abc.com/pages/example.html
- https://service.xyz.com.tw/zh-tw/pages/example.html
想要把開頭的傳輸協定字串及 domain name 部份拿掉, 所以只要匹配前段就好, 後半段需要留下來.
先找出特殊字元標記下來 (此例有/, .).
- http://www.abc.com/pages/example.html
- https://service.xyz.com.tw/zh-tw/pages/example.html
以 regular expression 規則替換 (字串開頭, 還有 doname name 的部份)
- ^http://.*/
- ^https://.*/
然後合併成一條規則:
^https?://.*/
.*需要改為 non-greedy, 因為 . (任意字元) 也包括字元 /, 以致於.*/匹配時會連同後半段要保留下來的路徑一起被括進來. 改成 non-greedy match 之後如下:
^https?://.*?/
加上脫逸字元\:
^https?:\/\/.*?\/
前後加/完成表示式:
/^https?:\/\/.*?\//
換成字串
"^https?:\\/\\/.*?\\/"
var re = /^https?:\/\/.*?\//;
var sfname = "http://www.abc.com/pages/example.html".replace(re, "");
console.log(sfname);
var re = new RegExp("^https?:\\/\\/.*?\\/");
var sfname = "http://www.abc.com/pages/example.html".replace(re, "");
console.log(sfname);
執行後 sfname 的內容應該是 "pages/example.html"
範例三
陣列變數 rules 是 DOM styleSheet 物件中的 cssRules, 目的: 想要取出每一條 rule 中的設定字串.
var rules = document.styleSheets[i].cssRules;
而 css rule 一般長這個樣子:
div.main { width: 80%; color: blue; font-weight: bold; text-align:center }
.ui-field > label { padding-left:10px; width:30%; height:2em; text-align:right; text-overflow:ellipsis }
所以是 "第一串文字{第二串文字}", 而我們想要取出的就是 "第二串文字". 所以初步的想法是:.*{.*}
需要把我們的目標:第二串文字暫存下來, 以便置換時使用
.*{(.*)}
不過這樣子空白的部份會包進.*中, 而我們想要進一步連同空白也去掉, 因此加上\s*來對應可能出現的任意長度的空白. 所以式子就變成
.*{\s*(.*)\s*}
{和}都是唯一, 不會發生 greedy match, 直接跳過. 加上脫逸字元\
.*\{\s*(.*)\s*\}
前面加上字串開頭符號^, 並且前後加/完成表示式:
/^.*\{\s*(.*)\s*\}/
換成字串
"^.*\\{\\s*(.*)\\s*\\}"
在 JavaScript 中, 運用來置換 css rule: ($1就是我們在 regular Expression 中指定要存下來的字串)
obj.cssText = rules[j].cssText.replace(/^.*\{\s*(.*)\s*\}/, "$1");
