正則手記——語法篇

RainBow發表於2022-11-22

前言

本文紀錄正規表示式的語法學習實踐。

正則常見使用場景:

  • 資料驗證,例如檢查時間字串是否符合格式;
  • 資料抓取,以特定順序抓取包含特定文字或內容的網頁;
  • 資料包裝,將資料從某種原格式轉換為另外一種格式;
  • 字串解析,例如捕獲所擁有 URL 的 GET 引數,或捕獲一組圓括弧內的文字;
  • 字串替代,將字串中的某個字元替換為其它字元。

線上工具輔助學習:

使用規則說明

基本語句

正規表示式(可叫作 “regexp”,或 “reg”)包擴 模式 和可選的 修飾符

有兩種建立正規表示式物件的語法。

較長一點的語法:

regexp = new RegExp("pattern", "flags");

較短一點的語法,使用斜線 "/":

regexp = /pattern/; // 沒有修飾符
regexp = /pattern/gim; // 帶有修飾符 g、m 和 i(後面會講到)

這兩種語法之間的主要區別在於,使用斜線 /.../ 的模式不允許插入表示式(如帶有 ${...} 的字串模板)。它是完全靜態的。

在我們寫程式碼時就知道正規表示式時則會使用斜線的方式 —— 這是最常見的情況。當我們需要從動態生成的字串“動態”建立正規表示式時,更經常使用 new RegExp。例如:

let tag = prompt("What tag do you want to find?", "h2");
​
let regexp = new RegExp(`<${tag}>`); // 如果在上方輸入到 prompt 中的答案是 "h2",則與 /<h2>/ 相同

修飾符

  • g(global)在第一次完成匹配後並不會返回結果,它會繼續搜尋剩下的文字。
  • i(insensitive)令整個表示式不區分大小寫(例如/aBc/i 將匹配 AbC)。
  • m(multi line)啟用多行模式,它隻影響 ^ 和 $ 的行為。在多行模式下,它們不僅僅匹配文字的開始與末尾,還匹配每一行的開始與末尾。
  • y (sticky)粘性修飾符 y 使 regexp.exec 精確搜尋位置 lastIndex,而不是“從”它開始。

m 修飾符多行模式:

在這個有多行文字的例子中,模式 /^\d/gm 將從每行的開頭取一個數字:

let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;

console.log(str.match(/^\d/gm)); // 1, 2, 3

沒有修飾符 m 時,僅會匹配第一個數字 1

修飾符 y 的搜尋:

let str = 'let varName = "value"';

let regexp = /\w+/y;

regexp.lastIndex = 3;
alert(regexp.exec(str)); // null(位置 3 有一個空格,不是單詞)

regexp.lastIndex = 4;
alert(regexp.exec(str)); // varName(在位置 4 的單詞)
注意:/xxx/gi // 修飾符可以複用,不區分大小寫+全字匹配

轉義,特殊字元

正則中存在特殊字元,這些字元在正規表示式中有特殊的含義,例如 [ ] { } ( ) \ ^ $ . | ? * +。它們用於執行更強大的搜尋。

要將特殊字元用作常規字元,請在其前面加上反斜槓:\, 這就是轉義符。

alert("Chapter 5.1".match(/\d\.\d/)); // 5.1(匹配了!)

當將字串傳遞給給 new RegExp 時,我們需要雙反斜槓 \\,因為字串引號會消耗一個反斜槓:

let regStr = "\\d\\.\\d";
alert(regStr); // \d\.\d(現在對了)

let regexp = new RegExp(regStr);

alert("Chapter 5.1".match(regexp)); // 5.1

錨點:^ 和 $

^The        匹配任何以“The”開頭的字串 -> Try it! (https://regex101.com/r/cO8lqs/2)
end$        匹配以“end”為結尾的字串
^The end$   抽取匹配從“The”開始到“end”結束的字串
roar        匹配任何帶有文字“roar”的字串

邊界符:\b 和 \B

\babc\b          執行整詞匹配搜尋 -> Try it! (https://regex101.com/r/cO8lqs/25)

\b 如插入符號那樣表示一個錨點(它與$和^相同)來匹配位置,其中一邊是一個單詞符號(如\w),另一邊不是單詞符號(例如它可能是字串的起始點或空格符號)。

它同樣能表達相反的非單詞邊界「\B」,它會匹配「\b」不會匹配的位置,如果我們希望找到被單詞字元環繞的搜尋模式,就可以使用它。

\Babc\B          只要是被單詞字元環繞的模式就會匹配 -> Try it! (https://regex101.com/r/cO8lqs/26)

重複量詞符:*、+、?和 {}

abc*        匹配在“ab”後面跟著零個或多個“c”的字串 -> Try it! (https://regex101.com/r/cO8lqs/1)
abc+        匹配在“ab”後面跟著一個或多個“c”的字串
abc?        匹配在“ab”後面跟著零個或一個“c”的字串
abc{2}      匹配在“ab”後面跟著兩個“c”的字串
abc{2,}     匹配在“ab”後面跟著兩個或更多“c”的字串
abc{2,5}    匹配在“ab”後面跟著2到5個“c”的字串
a(bc)*      匹配在“a”後面跟著零個或更多“bc”序列的字串
a(bc){2,5}  匹配在“a”後面跟著2到5個“bc”序列的字串

或運算子:| 、 []

a(b|c)     匹配在“a”後面跟著“b”或“c”的字串 -> Try it! (https://regex101.com/r/cO8lqs/3)
a[bc]      匹配在“a”後面跟著“b”或“c”的字串

字元類:\d、\w、\s

\d         匹配數字型的單個字元(0-9) -> Try it! (https://regex101.com/r/cO8lqs/4)
\w         匹配單個詞字(字母數字加下劃線) -> Try it! (https://regex101.com/r/cO8lqs/4)
\s         匹配單個空格字元(包括製表符\t和換行符\n)

反向字元類:\D、\W、\S

對於每個字元類,都有一個“反向類”,用相同的字母表示,但是大寫的。

\D         匹配非數字:除 \d 以外的任何字元,例如字母。
\w         匹配非單字字元:除 \w 以外的任何字元,例如非拉丁字母或空格。
\s         匹配非空格符號:除 \s 以外的任何字元,例如字母。

通配字元:.

.          匹配“任何字元”, 它與“除換行符之外的任何字元”匹配。

中級語句

捕獲組:()

捕獲作用

  • (exp) 匹配 exp,並捕獲文字到自動命名的組裡
  • (?<name>exp) 匹配 exp,並捕獲文字到名稱為 name 的組裡,也可以寫成 (?'name'exp)
  • (?:exp)—  匹配 exp,不捕獲匹配的文字

位置指定

  • (?=exp) 匹配 exp 前面的位置
  • (?<=exp) 匹配 exp 後面的位置
  • (?!exp) 匹配後面跟的不是 exp 的位置
  • (?<!exp) 匹配前面不是 exp 的位置

集合和範圍:[]

[abc]            匹配帶有一個“a”、“ab”或“ac”的字串 -> 與 a|b|c 一樣 -> Try it! (https://regex101.com/r/cO8lqs/7)
[a-c]            匹配帶有一個“a”、“ab”或“ac”的字串 -> 與 a|b|c 一樣
[a-fA-F0-9]      匹配一個代表16進位制數字的字串,不區分大小寫 -> Try it! (https://regex101.com/r/cO8lqs/22)
[0-9]%           匹配在%符號前面帶有0到9這幾個字元的字串
[^a-zA-Z]        匹配不帶a到z或A到Z的字串,其中^為否定表示式 -> Try it! (https://regex101.com/r/cO8lqs/10)

記住在方括弧內,所有特殊字元(包括反斜槓\)都會失去它們應有的意義。

貪婪量詞和惰性量詞

數量符(* + {})是一種貪心運算子,所以它們會遍歷給定的文字,並儘可能匹配。例如,<.+> 可以匹配文字 「This is a <div> simple div</div> test」中的「<div>simple div</div>」。為了僅捕獲 div 標籤,我們需要使用「?」令貪心搜尋變得 Lazy 一點:

<.+?>            一次或多次匹配 “<” 和 “>” 裡面的任何字元,可按需擴充套件 -> Try it! (https://regex101.com/r/cO8lqs/24)

注意更好的解決方案應該需要避免使用「.」,這有利於實現更嚴格的正規表示式:

<[^<>]+>         一次或多次匹配 “<” 和 “>” 裡面的任何字元,除去 “<” 或 “>” 字元 -> Try it! (https://regex101.com/r/cO8lqs/23)

更多懶惰匹配:

*?         匹配重複任意次,但儘可能少重複
+?         匹配重複 1 次或更多次,但儘可能少重複
??         匹配重複 0 次或 1 次,但儘可能少重複
{n,m}?     匹配重複n到m次,但儘可能少重複
{n,}?      匹配重複n次以上,但儘可能少重複

總結:

量詞有兩種工作模式:

(1)貪婪模式

預設情況下,正規表示式引擎會嘗試儘可能多地重複量詞字元。例如,\d+ 會消耗所有可能的字元。當無法消耗更多(在尾端沒有更多的數字或字串)時,然後它再匹配模式的剩餘部分。如果沒有匹配,則減少重複的次數(回溯),並再次嘗試。

(2)惰性模式

透過在量詞後新增問號 ? 來啟用。正規表示式引擎嘗試在每次重複量化字元之前匹配模式的其餘部分。

正如我們所見,惰性模式並不是貪婪搜尋的“靈丹妙藥”。另一種方式是使用排除項“微調”貪婪搜尋,如模式 "[^"]+"

高階語句

前瞻斷言與後瞻斷言

  • x(?=y)—  前瞻斷言(零寬先行斷言):匹配 x,不過是隻在 x 後跟 y 時才匹配。
  • x(?!y)—  否定前瞻斷言:匹配 x,不過是隻在 x 後不跟 y 時才匹配。
  • (?<=y)x—  肯定的後瞻斷言(零寬後行斷言):匹配 x,僅在前面是 y 的情況下。
  • (?<!y)x—  否定的後瞻斷言:匹配 x,僅在前面不是 y 的情況下。

(1)前瞻斷言例子:

比如\b\w+(?=ing\b),匹配以 ing 結尾的單詞的前面部分(除了
ing 以外的部分),如果在查詢 I'm singing while you're dancing. 時,它會匹配 singdanc

(2)後瞻斷言例子:

比如(?<=\bre)\w+\b會匹配以re開頭的單詞的後半部分(除了 re 以外的部分),例如在查詢 reading a book 時,它匹配 ading

(3)下面這個例子同時使用了字首和字尾:(?<=\s)\d+(?=\s) 匹配以空白符間隔的數
字(再次強調,不包括這些空白符)。

注意:後瞻斷言的瀏覽器相容情況
請注意:非 V8 引擎的瀏覽器不支援後瞻斷言,例如 Safari、Internet Explorer。

模式中的反向引用:\N 和 \k<name>

按編號反向引用:\N

我們可以將兩種引號都放在方括號中:['"](.*?)['"],但它會找到帶有混合引號的字串,例如 "...''..."。當一種引號出現在另一種引號內,比如在字串 "She's the one!" 中時,便會導致不正確的匹配:

為了確保模式查詢的結束引號與開始的引號完全相同,我們可以將其包裝到捕獲組中並對其進行反向引用:(['"])(.*?)\1

let str = `He said: "She's the one!".`;

let regexp = /(['"])(.*?)\1/g;

alert(str.match(regexp)); // "She's the one!"

正規表示式引擎會找到第一個引號 (['"]) 並記住其內容。那是第一個捕獲組。

在模式中 \1 表示“找到與第一組相同的文字”,在我們的示例中為完全相同的引號。

與此類似,\2 表示第二組的內容,\3 —— 第三分組,依此類推。

請注意:
如果我們在捕獲組中使用 ?:,那麼我們將無法引用它。用 (?:...) 捕獲的組被排除,引擎不會記住它。

按命名反向引用:\k<name>

如果一個正規表示式中有很多括號,給它們起個名字會便於引用。

要引用命名的捕獲組,我們可以使用:\k<name>

在下面的示例中,帶引號的組被命名為 ?<quote>,因此反向引用為 \k<quote>

let str = `He said: "She's the one!".`;

let regexp = /(?<quote>['"])(.*?)\k<quote>/g;

alert(str.match(regexp)); // "She's the one!"

正則手紀 —— 方法篇預告

Replace

  1. 在駝峰命名法格式的字串中新增空格
removeCc("camelCase"); // => 應該返回 'camel Case'

思路分析:

  • 1.首先需要搜尋匹配大寫字母,使用 [A-Z]可以匹配出 C
  • 2.然後在 C 之前加入空格,需要拿到 C做變更

我們需要用捕獲括號!捕獲括號允許匹配一個值,並且記住它,這樣之後就可以用它!

用捕獲括號來記住匹配到的大寫字母
`/([A-Z])/`
之後用 $1 訪問捕獲到的值

最後實現捕獲括號呢?用字串的 .replace() 方法!我們插入 '$1' 為第二個引數(注意這裡一定要用引號)
方法 2:replace() 也可以指定一個函式作為第二個引數

// 方法 1
function removeCc(str) {
  return str.replace(/([A-Z])/g, " $1");
}
// 方法 2
function removeCc(str) {
  return str.replace(/[A-Z]/g, (match) => " " + match);
}
// test
console.log(removeCc("camelCase")); // 'camel Case'
console.log(removeCc("helloWorldItIsMe")); // 'hello World It Is Me'
  1. 大寫第一個字母
capitalize("camel case"); // => 應該返回 'Camel case'

使用 ^去命中首字母,配合 [a-z]選擇首字母中小寫的情況

function capitalize(str) {
  return str.replace(/^[a-z]/g, (match) => match.toUpperCase());
}

// test

console.log(capitalize("camel case")); // Camel case'
  1. 大寫單詞的所有首字母
capitalizeAll("camel case"); // => 應該返回 'Camel Case'

function capitalizeAll(str) {
  return str.replace(/\b[a-z]/g, (match) => match.toUpperCase());
}

// test

console.log(capitalizeAll("camel case")); // Camel Case'

Test

  1. 手機號碼的驗證

規則指定,手機號碼除 1211開頭的 11 位數字視為有效

checkType_phone("13520646171"); // 應該返回 true
checkType_phone("11520646171"); // 應該返回 false
checkType_phone("123456"); // 應該返回 false

function checkType_phone(str) {
  return /^1(3|4|5|6|7|8|9)[0-9]{9}$/.test(str);
}

// test
console.log(checkType_phone("13520646171")); //  true
console.log(checkType_phone("11520646171")); //  false
console.log(checkType_phone("123456")); // false

Match

儘可能的取出亂碼字串中的中文及有效符號

var str = `&lt;p class=&quot;MsoNormal&quot;&gt;
↵    在3182例接受磁控膠囊胃鏡檢查的無症狀體檢人群中&lt;span&gt;,共檢出&lt;/span&gt;7例胃癌,這意味著無症狀人群的胃癌檢出率為2.2‰,其中50歲以上人群胃癌檢出率高達7.4‰!這一研究成果刊發於美國消化領域權威學術期刊GIE&lt;span&gt;(&lt;/span&gt;Gastrointestinal Endoscopy,譯名《消化內鏡》&lt;span&gt;)。&lt;/span&gt;
↵&lt;/p&gt;`;

function getChineseText(str) {
  var reg = /[\u4e00-\u9fa5|0-9.\‰《》]+/g;
  return str.match(reg).join(",");
}
console.log(getChineseText(str)); // 在3182例接受磁控膠囊胃鏡檢查的無症狀體檢人群中,共檢出,7例胃癌,這意味著無症狀人群的胃癌檢出率為2.2‰,其中50歲以上人群胃癌檢出率高達7.4‰,這一研究成果刊發於美國消化領域權威學術期刊,譯名《消化內鏡》

匹配出地址:

var str = `<https://api.github.com/user/24217900/starred?page=2>; rel="next", <https://api.github.com/user/24217900/starred?page=16>; rel="last"`;

console.log(str.match(/<.+?>/g));

/* [
  '<https://api.github.com/user/24217900/starred?page=2>',
  '<https://api.github.com/user/24217900/starred?page=16>'
] */

參考連結

相關文章