前言
本文紀錄正規表示式的語法學習實踐。
正則常見使用場景:
- 資料驗證,例如檢查時間字串是否符合格式;
- 資料抓取,以特定順序抓取包含特定文字或內容的網頁;
- 資料包裝,將資料從某種原格式轉換為另外一種格式;
- 字串解析,例如捕獲所擁有 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.
時,它會匹配 sing
和 danc
。
(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
- 在駝峰命名法格式的字串中新增空格
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'
- 大寫第一個字母
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'
- 大寫單詞的所有首字母
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
- 手機號碼的驗證
規則指定,手機號碼除 12
和11
開頭的 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 = `<p class="MsoNormal">
↵ 在3182例接受磁控膠囊胃鏡檢查的無症狀體檢人群中<span>,共檢出</span>7例胃癌,這意味著無症狀人群的胃癌檢出率為2.2‰,其中50歲以上人群胃癌檢出率高達7.4‰!這一研究成果刊發於美國消化領域權威學術期刊GIE<span>(</span>Gastrointestinal Endoscopy,譯名《消化內鏡》<span>)。</span>
↵</p>`;
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>'
] */