yanyue404/blog

正则手记——语法篇

Opened this issue · 0 comments

前言

本文纪录 Javascript 正则表达式的语法学习实践。

正则常见使用场景:

  • 数据验证,例如检查时间字符串是否符合格式;
  • 数据抓取,以特定顺序抓取包含特定文本或内容的网页;
  • 数据包装,将数据从某种原格式转换为另外一种格式;
  • 字符串解析,例如捕获所拥有 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

按编号反向引用:\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>'
] */

参考链接