正規表達式

Regular Expressions

Date: Nov. 17th, 2019

Lecturer: Chia

Outline

  • 什麼是 Regular Expressions?
  • 創建一個正規表達式 (Creating a regular expression)
  • 比對測試 (Testing for matches)
  • 字元的集合 (Sets of characters)
  • 重複部分的模式 (Repeating parts of a pattern)
  • 群組子表達式 (Grouping subexpressions)

Outline

  • 匹配與群組(Matches and groups)
  • 日期類 (The Date class)
  • 單詞和字串的邊界 (Word and string boundaries)
  • 選擇模式 (Choice patterns)
  • 匹配機制 (The mechanics of matching)
  • 回溯 (Backtracking)
 

Outline

  • 取代方法 (The replace method)
  • 貪婪式比對 (Greed)
  • 動態創建RegExp物件 (Dynamically creating RegExp objects)
  • 搜尋方法 (The search method)
  • lastIndex屬性 (The lastIndex property)
  • 解析一個INI文件 (Parsing an INI file)
  • 國際字符 (International characters)
 

什麼是 Regular Expressions?

  • 正規表達式
    • 被用來匹配字串中字元組合的模式。
    • 一個描述字串資料之模式(patterns)的方式。
let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

創建一個正規表達式

  • 正規表達式的型別(type)為物件(object)
  • 創建方式:
// (方式一) 使用 RegExp()
let re1 = new RegExp("abc");
// (方式二) 使用 正斜杠 / 包覆所描述的模式
let re2 = /abc/;

創建一個正規表達式

? 和 + 的前面添加反斜線 \,代表將之當成一般字元

意即表示字元本身,沒有特殊用途。

let eighteenPlus = /eighteen\+/;
// + 原本是特殊字元,但這裡要當成非特殊字元

比對測試

  • test() 方法
    • 回傳布林值,說明該字串是否符合欲比對的模式。
    • 若正規表達式中只包含非特殊字元比對字元本身
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

字元的集合

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

若將一組字符放在 [ ] 內,表示匹配[ ]內的所有字符

意即只要符合 [ ] 內所列出的字符,就算匹配成功。

字元的集合

  • 內建快捷表達方式(built-in shortcuts)
    • 如:\d[0-9] 含義相同(匹配所有數字)。
特殊字元 解說
\d 任何數字
\w 任何字母和數字(“文字字符”)
\s 任何空白字符(space, tab, newline…
\D 非數字的字符
\W 非字母和數字字符
\S 非空白字符
. 除換行字符以外的任何單一字元。

字元的集合

let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;

console.log(dateTime.test("01-30-2003 15:20"));
console.log(dateTime.test("30-jan-2003 15:20"));
let notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true     1100100010`2`00110
  • ^ 存在於 [ ] 內,表示匹配非方括號內的字符。

重複部分的模式

console.log(/'\d+'/.test("'123'"));
console.log(/'\d+'/.test("''"));

console.log(/'\d*'/.test("'123'"));
console.log(/'\d*'/.test("''"));
  • 如何匹配一個或多個數字的序列?
+ 該字符重複出現至少1次
* 該字符重複出現0次以上
? 該字符重複出現出現0次或1次
let neighbor = /neighbou?r/;

console.log(neighbor.test("neighbour"));
console.log(neighbor.test("neighbor"));

重複部分的模式

let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("1-30-2003 8:45"));
// → true
  • 為了精確匹配該字符的出現次數,使用 { } 放在某個字符後面
{4} 該字符需出現4次,即符合。
{2,4} 該字符出現至少2次、最多4次,即符合。
{5,} 該字符出現至少5次,即符合。(開放式範圍)

群組子表達式

let cartoonCrying = /boo+(hok+)+/i;

console.log(cartoonCrying.test("BoohoooohooHooo"));
console.log(cartoonCrying.test("Boohokoohokhook"));

>>> `i`是指`不區分大小寫`
  • 括號 ( ) 用於驗證字符串,以及提取字串的一部分。
    • ​若一次要對多個字符使用特殊符號(+ 或 *),則必須使用 ( )
    • 沒有使用 ( ) 包起來,則代表其是針對前面的一個字符。 

匹配與群組

let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8
  • test()方法:最簡單的匹配方式,只回傳True或False。
  • exec()方法:沒有匹配時,回傳null;否則傳回匹配資訊。
    • index屬性:回傳成功匹配的字符從何處開始。
  • match()方法:處理字串的方法,與exec()方法類似。
console.log("one two 100".match(/\d+/));
console.log("one two 100".match(/\d?/));
console.log("one two 100".match(/\d*/));

匹配與群組

let quotedText = /'([^']*)'/; 
// '([^']*)' -> 字串外面使用''包裹。群組裡面沒有`'`出現0次以上。  
// 'hello' or hello
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]
const regexp = /(\w+)\.jpg/;
console.log(regexp.exec('File name: cat.jpg'));
// ["cat.jpg", "cat"]

匹配與群組

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
// 先比對 bad(ly)? 再比對 (ly)?

console.log(/bad(ly)?/.exec("badl"));
console.log(/bad(ly)?/.exec("badlyr"));
  • 當一個群組( )被成功匹配多次時,僅回傳最後一個匹配的項目,即"3"。
console.log(/(\d)+/.exec("123")); // (\d)+ 
// → ["123", "3"]

日期類

console.log(new Date());
// → Tue Nov 12 2019 10:28:37 GMT+0800 (台北標準時間)

Date 物件提供 getFullYear(), getMonth(), getDate(), getHours(), getMinutes(), and getSeconds()方法。

console.log(new Date(2019, 11, 12));
// → Thu Dec 12 2019 00:00:00 GMT+0800 (台北標準時間)
console.log(new Date(2009, 11, 12, 12, 59, 59, 999));
// 最後的四個參數為(小時,分鐘,秒和毫秒),預設皆為0。
  • 取得現在的日期和時間
  • 創建特定時間的物件(object)

日期類

console.log(new Date(2019, 10, 12).getTime());
// → 1573488000000
console.log(Date.now());
// → 1573527038896

console.log(new Date(1573527038896));
// → Tue Nov 12 2019 10:50:38 GMT+0800 (台北標準時間)
  • JavaScript的月份從0開始(所以12月是11);日期從1開始。
  • getTime()方法,可回傳從1970年至某個時間點的毫秒數。

日期類

  • 進行日期的處理
function processDate(string) {
  let [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  // 底線 _ (underscore) 忽略,用於跳過由exec返回的陣列中匹配的物件。
  return new Date(year, month - 1, day);
}
console.log(processDate("testing: 1-30-2003 in javascript."));
// → Thu Jan 30 2003 00:00:00 GMT+0800 (台北標準時間)
備註:(匹配模式不夠嚴謹,於下一節 "單詞和字串的邊界" 說明)

console.log(processDate("100-1-30000")); // 匹配 "00-1-3000"
>>> Sun Dec 01 2999 00:00:00 GMT+0800 (台北標準時間)

單詞和字串的邊界

^ 匹配字串的開頭
$ 匹配字串的結尾
  • 錯誤寫法:/x^/
/^\d+$/ 匹配開頭包含至少一個數字的字串
匹配結尾包含至少一個數字的字串
​/^!/ ​匹配開頭為!的字串

單詞和字串的邊界

\b 配對 word boundary(單詞邊界)。
單詞邊界是指一個字元的前後沒有其他字元。
let matchedResult = 'This is an apple.'.match(/\bis\b/); 
// is 這個單字才會被選到
// This 中的 is 不會被選到,因為前有其他字元。
// [ 'is', index: 5, input: 'This is an apple.' ]
console.log(/cat/.test("concatenate"));
console.log(/\bcat\b/.test("concatenate"));
console.log(/\bcat/.test("catenate"));

選擇模式

 
| 在左側模式和右側模式之間進行選擇。
(一次匹配多種可選擇的類型)
let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

匹配機制

 
let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("the 3 pigs"));
// → true

回溯

 
let reg = /\b([01]+b|[\da-f]+h|\d+)\b/;
console.log(reg.test("0 856 fr2 eah 1b"));
// → true
console.log(reg.exec("0m 856 fr2 eah 1b")); // 856 eah 1b 皆符合
// → ["856", "856"]

回溯

 
let reg = /^.*x/;
console.log(reg.test("."));
console.log(reg.test(""));

console.log(reg.test("x"));
console.log(reg.test(".x"));
console.log(reg.test(".xa"));
console.log(reg.test("abcxe"));
let reg = /([01]+)+b/;
console.log(reg.test("01 123"));
console.log(reg.test("0 reg"));

console.log(reg.test("01b 123"));
console.log(reg.test("0b reg"));

取代方法

 
console.log("papa".replace("p", "m"));
// → mapa
  • replace()方法:用於將一個字串替換為另一個字串。
    • ​[ replace() 的第一個參數 ]
      • 使用g於正規表達式中,可取代所有匹配項
console.log("Borobudur".replace(/[ou]/, "j"));
// → Bjrobudur
console.log("Borobudur".replace(/[ou]/g, "j"));
// → Bjrjbjdjr

取代方法

 
console.log(
  "Liskov, Barbara\nMcCarthy, John\nWadler, Philip"
    .replace(/(\w+), (\w+)/g, "$2 $1"));
// → Barbara Liskov
//   John McCarthy
//   Philip Wadler
  • [ replace()的第二個參數 ]
    • 使用$,可針對括號( )內的內容
console.log(
  "Liskov, Barbara\nMcCarthy, John\nWadler, Philip"
    .replace(/(\w+), (\w+)/g, "$&")); // $& 指的是所有括號( )的內容
// → Liskov, Barbara
// McCarthy, John
// Wadler, Philip

貪婪式比對

 
let html = ` 
<table>
    <td>aaa</td>
</table>
<table>
    <td>bbb</td>
</table>      


`;

let reg = /<table[.\n\s\S]*<\/table>/g; 
let r = html.match(reg);
console.log(r);
  • Greedy​​:使用 *、+、? 或 { } 等重複運算符,將會使之儘可能匹配最多的字元。

貪婪式比對

 
// let html 同上

let reg = /<table[.\n\s\S]*?<\/table>/g; 
let r = html.match(reg);
console.log(r);
  • Non-Greedy:使用 *?、+?、?? 或 {}? ,將會使之儘可能匹配最少的字元。
    • 例如:在123abc中應用 /\d+/ 可匹配「123」,但使用 /\d+?/ 在相同字串上只能匹配「1」。

動態創建RegExp物件

 

使用「脫逸字元\

// 使用 RegExp()
let pp2 = new RegExp("ab\/nc");
// 使用 正斜杠 / 包覆所描述的模式
let pp1 = /ab\/nc/;

欲將特殊字元,轉為一般字串

使用「脫逸字元\

let pp1 = /ab\/nc/;
let str1 = "qaz1ab/nc";
console.log(pp1.exec(str1));
let pp2 = new RegExp("ab\/nc");
let str2 = "qaz1ab/nc";
console.log(pp2.exec(str2));

正規表達式固定已知!

動態創建RegExp物件

 
let name = "harry"; // 把 harry 當成模式
let text = "Harry is a suspicious character.";
let regexp = new RegExp("\\b(" + name + ")\\b", "gi"); 
// 符合模式

console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.

正規表達式動態未知!

  • 先把表示式轉換為字串,進行動態處理,再拿來建立 RegExp 物件

搜尋方法

 
  • indexOf() 方法
    • 回傳給定元素於陣列中第一個被找到之索引,若不存在於陣列中則回傳-1。
    • 不能用於正規表達 str.indexOf(/[abc]/ , i);
    • 優點:能從特定位置開始搜尋。
let str="Hello world!";
console.log(str.indexOf("Hello"));
console.log(str.indexOf("World"));
console.log(str.indexOf("world"));

Output:
0
-1
6

搜尋方法

 
  • search()方法
    • 可用於正規表達式。
    • 回傳給定元素於陣列中第一個被找到之索引,若不存在於陣列中則回傳-1。
    • 缺點:不能從特定位置才開始搜尋。
let str="Mr. Blue has a blue house";
console.log(str.search(/blue/i));
console.log("  word".search(/\S/)); //012
// → 2

lastIndex 屬性

 
  • exec()方法
    • 缺點:不能從特定位置才開始搜尋。
  • 正規表達式的物件,擁有以下屬性:
    • source:正規表達式。
    • lastIndex:用於控制,該從何處開始匹配。

lastIndex 屬性

 
let str =  "a0bc1";
// Indexes: 01234

let rexWithout = /\d/; 
//指定位子搜尋,對它沒用。從index=0開始搜尋
rexWithout.lastIndex = 2;
console.log(rexWithout.exec(str));
// → [ '0', index: 1, input: 'a0bc1', groups: undefined ]
let rexWithout_g = /\d/g;
//指定位子搜尋,對它有用。從index=2開始搜尋,會繼續往後搜尋
rexWithout_g.lastIndex = 2;          
console.log(rexWithout_g.exec(str));
// → [ '1', index: 4, input: 'a0bc1', groups: undefined ]

lastIndex 屬性

 
let str =  "a0bc1";
// Indexes: 01234

let rexWith_y = /\d/y;

rexWith_y.lastIndex = 2;
//指定位子搜尋,對它有用。只搜尋index=2,"不會"繼續往後搜尋
console.log(rexWith_y.exec(str));
// → null
let str =  "a0bc1";
// Indexes: 01234

let rexWith_y = /\d/y;

rexWith_y.lastIndex = 1;
console.log(rexWith_y.exec(str));

rexWith_y.lastIndex = 4;
console.log(rexWith_y.exec(str));

lastIndex 屬性

 
console.log("Banana".match(/an/g));
// → ["an", "an"]
  • match()方法 + g (global option)
    • match()方法會在字串中找到該模式的所有匹配項,並回傳包含匹配字串的陣列。

解析一個INI文件

 
searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7

; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451

[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn

解析一個INI文件

 
\r 返回符號(返回同一行的開頭,不前進到下一行)
\n 換行符號
/\r?\n/ 允許行與行之間為\n或\r\n的拆分方式。
function parseINI(string) {
  let result = {};
  let section = result;
  string.split(/\r?\n/).forEach(line => {
    let match;
    if (match = line.match(/^(\w+)=(.*)$/)) { 
      //是屬性
      section[match[1]] = match[2];
    } else if (match = line.match(/^\[(.*)\]$/)) {
      //是節標題
      section = result[match[1]] = {};
    } else if (!/^\s*(;.*)?$/.test(line)) {
      //不是節標題或屬性
      // 檢查它是註釋還是空行  // (;.*) 匹配註釋  ? 匹配空格
      throw new Error("Line '" + line + "' is not valid.");
      // 與任何形式都不匹配時,引發異常。
    }
  });
  return result;
}

console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}

國際字符

 
  • JavaScript的word character只包含26個大小寫的英文字母、十進位數字、底線。
  • 而像是éβ等字符,將不被匹配\w(文字字符),但可匹配\W(非文字字符)
  • \s (空白),可匹配Unicode標準認為的所有字符,包括不間斷空格蒙古元音分隔符之類的東西。

國際字符

 
  • JavaScript預設:
    • 處理正規表達式的單個程式碼字元,而不是處理實際的單個字符。
console.log(/🍎{3}/.test("🍎🍎🍎")); 
// → false
console.log(/<.>/.test("<🌹>"));
// → false

console.log(/<.>/u.test("<🌹>")); 
// 必須在正則表達式中添加`u`,以Unicode使其正確處理此類字符。
// → true

國際字符

 
  • u 意味以Unicode處理此類字符
  • \p是 Unicode 屬性轉義,它賦予了我們根據 Unicode 字符的屬性數據構造表達式的能力。
/* \p{Property=Value} */

console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false
/* \p{Name} */
console.log(/\p{Alphabetic}/u.test("α"));
// → true
console.log(/\p{Alphabetic}/u.test("!"));
// → false

感謝聆聽

Regular Expressions

By BessyHuang

Regular Expressions

  • 436