Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

正则手记——语法篇 #113

Open
yanyue404 opened this issue Dec 5, 2019 · 0 comments
Open

正则手记——语法篇 #113

yanyue404 opened this issue Dec 5, 2019 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Dec 5, 2019

前言

本文纪录 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>'
] */

参考链接

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant