前言
本文纪录正则表达式的语法学习实践。
正则常见使用场景:
- 数据验证,例如检查时间字符串是否符合格式;
- 数据抓取,以特定顺序抓取包含特定文本或内容的网页;
- 数据包装,将数据从某种原格式转换为另外一种格式;
- 字符串解析,例如捕获所拥有 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",则与 // 相同
修饰符
- 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,并捕获文本到自动命名的组里(?
匹配 exp,并捕获文本到名称为 name 的组里,也可以写成exp) (?'name'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 标签,我们需要使用「?」令贪心搜索变得 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 的情况下。(?— 否定的后瞻断言:匹配 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
按编号反向引用:\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
如果一个正则表达式中有很多括号,给它们起个名字会便于引用。
要引用命名的捕获组,我们可以使用:\k
。
在下面的示例中,带引号的组被命名为 ?
,因此反向引用为 \k
:
let str = `He said: "She's the one!".`;
let regexp = /(?['"])(.*?)\k/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 = `; rel="next", ; rel="last"`;
console.log(str.match(/<.+?>/g));
/* [
'',
''
] */