Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/self-denial-cy/regular-exercise
personal regular exercise
https://github.com/self-denial-cy/regular-exercise
Last synced: 8 days ago
JSON representation
personal regular exercise
- Host: GitHub
- URL: https://github.com/self-denial-cy/regular-exercise
- Owner: self-denial-cy
- License: mit
- Created: 2024-04-28T08:11:48.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2024-05-11T07:24:51.000Z (8 months ago)
- Last Synced: 2024-11-10T22:14:07.909Z (2 months ago)
- Language: JavaScript
- Size: 1.86 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 正则表达式攻略
## 1.字符匹配攻略
> 正则表达式是匹配模式,要么匹配字符,要么匹配位置
### 1.1 两种模糊匹配
#### 1.1.1 横向模糊匹配
横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的
譬如 `{m,n}` 表示连续出现最少 m 次,最多 n 次
```javascript
const reg = /ab{2,5}c/g;const string = 'abc abbc abbbc abbbbc abbbbbc abbbbbbc';
console.log(string.match(reg));
```> g 是正则的一个修饰符,表示全局匹配
>
> 意为在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是【所有】,而不只是【第一个】#### 1.1.2 纵向模糊匹配
纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,可以不是某个确定的字符,可以有多种可能
譬如 `[abc]` 表示该字符可以是 a、b、c 中的任何一个
```javascript
const reg = /a[123]b/g;const string = 'a0b a1b a2b a3b a4b';
console.log(string.match(reg));
```### 1.2 字符组
#### 1.2.1 范围表示法
譬如 `[123456abcdefGHIJKLM]` 可以写成 `[1-6a-fG-M]`
如果要匹配 `-` 字符时就不能写成 `[a-z]` 了,因为其表示小写字母中的任何一个字符了
可以写成 `[-az]` 或 `[az-]` 或 `[a\-z]`,要么放在开头,要么放在结尾,要么转义
#### 1.2.2 排除字符组
纵向模糊匹配,有一种情景,某位字符可以是任何东西,就是不能是 a、b、c,此时就是排除字符组的概念
譬如 `[^abc]` 表示是一个除 a、b、c 之外的任意一个字符,`^` 表示求反
#### 1.2.3 常见的简写形式
| 字符组 | 具体含义 |
| ------ | ------------------------------------------------------------ |
| `\d` | 表示`[0-9]`,表示是一位数字 |
| `\D` | 表示`[^0-9]`,表示除数字外的任意一位字符 |
| `\w` | 表示`[0-9a-zA-z_]`,表示数字、大小写字母和下划线 |
| `\W` | 表示`[^0-9a-zA-Z_]`,表示非数字、大小写字母和下划线的任意一位字符 |
| `\s` | 表示`[\t\v\n\r\f]`,表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符 |
| `\S` | 表示`[^\t\v\n\r\f]`,非空白符 |
| `.` | 表示`[^\n\r\u2028\u2029]`,通配符,表示几乎任意字符,换行符、回车符、行分隔符和段分隔符除外 |> 如果要匹配任意字符,可以使用 `[\d\D]`、`[\w\W]`、`[\s\S]`、`[^]` 中任一个
### 1.3 量词【重复】
#### 1.3.1 简写形式
| 量词 | 具体含义 |
| ------ | ------------------------------------------ |
| `{m,}` | 表示至少出现 m 次 |
| `{m}` | 等价于`{m,m}`,表示出现 m 次 |
| `?` | 等价于`{0,1}`,表示出现一次或不出现 |
| `+` | 等价于`{1,}`,表示出现至少一次 |
| `*` | 等价于`{0,}`,表示出现任意次,有可能不出现 |#### 1.3.2 贪婪匹配与惰性匹配
```javascript
const reg = /\d{2,5}/g;const string = '123 1234 12345 123456';
console.log(string.match(reg));
```上述例子中,`/\d{2,5}/g` 表示数字连续出现了 2 到 5 次,会匹配 2、3、4、5 位连续数字
但是其是贪婪的,它会尽可能多的匹配,6 位连续数字它会匹配 5 位,3 位连续数字它会匹配 3 位,反正只要在能力范围内,越多越好
```javascript
const reg = /\d{2,5}?/g;const string = '123 1234 12345 123456';
console.log(string.match(reg));
```上述例子中,`/\d{2,5}?/g` 表示虽然 2 到 5 位连续数字都行,但是当匹配到 2 位连续数字后,就不再往下尝试了
通过在量词后加个问号就能实现惰性匹配:
| 惰性量词 | 贪婪量词 |
| -------- | -------- |
| `{m,n}?` | `{m,n}` |
| `{m,}?` | `{m,}` |
| `??` | `?` |
| `+?` | `+` |
| `*?` | `*` |### 1.4 多选分支
一个模式可以实现横向和纵向模糊匹配,而多选分支可以支持多个子模式任选其一
具体形式:`(p1|p2|p3)` 中 p1、p2、p3 是子模式,用 `|` 管道符分隔,表示其中任一
```javascript
const reg = /good|nice/g;const string = 'good idea, nice try.';
console.log(string.match(reg));
```但需要注意,用 `/good|goodbye/` 去匹配 `goodbye` 字符串时,结果是 `good`
而用 `/goodbye|good/` 去匹配 `goodbye` 字符串时,结果是 `goodbye`
也就是说,分支结构是惰性的,当有一个模式匹配上了,后面的模式就不再尝试了
### 1.5 案例分析
#### 1.5.1 匹配 16 进制颜色值
```javascript
const reg = /#[0-9a-zA-Z]{6}|#[0-9a-zA-Z]{3}/g;const string = '#ffbbad #Fc01DF #FFF #ffE';
console.log(string.match(reg));
```#### 1.5.2 匹配时间
```javascript
const reg = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;console.log(reg.test('23:59'));
console.log(reg.test('02:07'));
```#### 1.5.3 匹配日期
```javascript
const reg = /^[0-9]{4}-([0][1-9]|[1][0-2])-([0][1-9]|[12][0-9])|[3][0-1]$/;console.log(reg.test('2024-04-28'));
```#### 1.5.4 匹配 Windows 操作系统文件路径
```javascript
const reg = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)$/;console.log(reg.test(process.cwd()));
```#### 1.5.5 匹配 id
```javascript
// const reg = /id=".*?"/;
const reg = /id="[^"]*"/;const string = '
';console.log(string.match(reg));
```## 2. 位置匹配攻略
### 2.1 什么是位置
位置【锚】是相邻字符之间的位置
![位置](./images/location.png)
### 2.2 如何匹配位置
在 ES5 中,共有 6 个锚:
- `^`
- `$`
- `\b`
- `\B`
- `(?=p)`
- `(?!p)`#### 2.2.1 `^` 和 `$`
- `^` 匹配开头,在多行匹配中匹配行开头
- `$` 匹配结尾,在多行匹配中匹配行结尾```javascript
const string = 'hello';const reg = /^|$/g;
console.log(string.replace(reg, '#'));
```上述例子中,用 `#` 替换开头和结尾【位置可以替换成字符】
```javascript
const string = 'I\nlove\njavascript';const reg = /^|$/gm;
console.log(string.replace(reg, '#'));
```上述例子中,使用 `m` 修饰符开启多行匹配模式,这时候 `^` 和 `$` 表示行开头和行结尾
> 需要注意,在大多数正则匹配引擎中,`\r` 和 `\n` 都会被认定开启了新行
>
> 额外,JavaScript 中模板字符串中换行符锁定为 `\n`,不管在什么操作系统中#### 2.2.2 `\b` 和 `\B`
`\b` 是单词边界,具体就是 `\w` 和 `\W` 之间的位置,也包括 `\w` 和 `^` 之间的位置和 `\w` 和 `$` 之间的位置
```javascript
const string = '[JS] Lesson_01.mp4';const reg = /\b/g;
console.log(string.replace(reg, '#'));
````\B` 就是 `\b` 反面的意思,非单词边界,在字符串所有位置中,扣掉 `\b` 的位置,剩下的位置都是 `\B` 的
具体来说,就是 `\w` 和 `\w`、`\W` 和 `\W`、`\W` 和 `^`、`\W` 和 `$` 之间的位置
```javascript
const string = '[JS] Lesson_01.mp4';const reg = /\B/g;
console.log(string.replace(reg, '#'));
```#### 2.2.3 `(?=p)` 和 `(?!p)`
`(?=p)`,其中 `p` 是一个子模式,即 `p` 之前的位置
譬如,`(?=l)` 表示 `l` 字符之前的位置:
```javascript
const string = 'hello';const reg = /(?=l)/g;
console.log(string.replace(reg, '#'));
```而 `(?!p)` 就是 `(?=p)` 的反面意思,字符串所有位置中,扣掉 `(?=p)` 的位置,剩下的位置都是 `(?!p)` 的
```javascript
const string = 'hello';const reg = /(?!l)/g;
console.log(string.replace(reg, '#'));
```> 两者学名分别是 `positive lookahead` 和 `negative lookahead`
>
> 也就是正向先行断言和负向先行断言
>
> ES6+ 版本中,支持 `positive lookbehind` 和 `negative lookbehind`
>
> 具体是 `(?<=p)` 和 `(?
> 也就是匹配 `p` 之后的位置和匹配除 `p` 之后的位置以外的所有位置### 2.3 位置的特性
对于位置的理解,可以理解成空字符
譬如,`hello` 字符串等价于以下形式:
```javascript
console.log('hello' === '' + 'h' + '' + 'e' + '' + 'l' + '' + 'l' + '' + 'o' + '');
console.log('hello' === '' + '' + 'hello' + '' + '' + '');
```因此,将 `/^hello$/` 写成 `/^^hello$$$/` 没有任何问题,甚至更复杂的也可以:
```javascript
console.log(/^^hello$$$/.test('hello'));
console.log(/(?=he)^^he(?=\w)llo$\b\b$/.test('hello'));
```### 2.4 案例分析
#### 2.4.1 不匹配任何东西的正则
```javascript
const string = 'hello';// const reg = /.^/;
const reg = /$./;console.log(string.match(reg));
```#### 2.4.2 数字的千位分隔符表示法
```javascript
const string = '123456789';const reg = /(?!^)(?=(\d{3})+$)/g;
console.log(string.replace(reg, ','));
```#### 2.4.3 验证密码问题
要求密码长度 6 到 12 位,由数字、小写字母、大写字母组成,但必须至少包括两种字符
两种思路:
1. 数字和小写字母、数字和大写字母、小写字母和大写字母
2. 不能全是数字、不能全是小写字母、不能全是大写字母```javascript
// const reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9a-zA-Z]{6,12}$/;
const reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9a-zA-Z]{6,12}$/;const string = '1qazXSW2';
console.log(string.match(reg));
```## 3. 括号的作用
### 3.1 分组和分支结构
#### 3.1.1 分组
```javascript
const reg = /(ab)+/g;const string = 'ababa abbb ababab';
console.log(string.match(reg));
```#### 3.1.2 分支结构
```javascript
const reg = /^I love (JavaScript|Regular Expression)$/;console.log(reg.test('I love JavaScript'));
console.log(reg.test('I love Regular Expression'));
```### 3.2 分组引用
> 这是括号一个重要的作用,有了它,就可以进行数据提取,以及更强大的替换操作
#### 3.2.1 提取数据
```javascript
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;const string = '2024-04-29';
console.log(string.match(reg));
```> `match` 执行返回一个数组,第一个元素是整体匹配结果,然后是各个分组【括号里】匹配的内容,然后是匹配下标,最后是输入的文本
>
> 另外,正则表达式是否有 `g` 修饰符,`match` 返回的数组格式是不一样的另外也可以使用正则实例对象的 `exec` 方法:
```javascript
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;const string = '2024-04-29';
console.log(reg.exec(string));
```还可以使用构造函数的全局属性 `$1` 至 `$9` 来获取:
```javascript
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;const string = '2024-04-29';
reg.test(string);
// reg.exec(string);
// string.match(reg);console.log(RegExp.$1);
console.log(RegExp.$2);
console.log(RegExp.$3);
```> 不推荐,该特性已弃用
#### 3.2.2 替换
```javascript
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;const string = '2024-04-29';
string.replace(reg, function () {
console.log(arguments);
});console.log(string.replace(reg, '$2/$3/$1'));
```### 3.3 反向引用
除了使用相应 API 来引用分组,也可以在正则本身里引用分组,但只能引用之前出现的分组,即反向引用
```javascript
const reg = /^\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}$/;console.log(reg.test('2024-04-29'));
console.log(reg.test('2024/04/29'));
console.log(reg.test('2024.04.29'));
console.log(reg.test('2024-04/29'));
```> 上述例子中,虽然匹配了要求的情况,但是也匹配了 `2024-04/29` 这样的错误数据
```javascript
const reg = /^\d{4}(-|\/|\.)\d{2}\1\d{2}$/;console.log(reg.test('2024-04-29'));
console.log(reg.test('2024/04/29'));
console.log(reg.test('2024.04.29'));
console.log(reg.test('2024-04/29'));
```> 上述例子中,通过 `\1` 引用了之前的分组,不管它匹配到了什么,都匹配那个同样的具体某个字符
#### 3.3.1 括号嵌套怎么办
```javascript
const reg = /^((\d)(\d(\d)))\1\2\3\4$/;const string = '1231231233';
console.log(reg.test(string)); // true
console.log(RegExp.$1); // 123
console.log(RegExp.$2); // 1
console.log(RegExp.$3); // 23
console.log(RegExp.$4); // 3
```#### 3.3.2 `\10` 表示什么
`\10` 表示第十个分组
```javascript
const reg = /^(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+$/;const string = '123456789# #######';
console.log(string.match(reg));
```> 如果要匹配 `\1` 和 `0` 的话,请使用 `(?:\1)0` 或 `\1(?:0)`
#### 3.3.3 引用不存在的分组会怎样
因为反向引用,是引用之前的分组
但在正则中引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身
譬如 `\2`,就匹配 `\2`【注意,`\2` 表示对 `2` 进行了转义】
```javascript
const reg = /^\1\2\3\4\5\6\7$/;const string = '\1\2\3\4\5\6\7';
console.log(string.split(''));
console.log(reg.test(string));
```#### 3.3.4 分组之后有量词会怎样
分组之后有量词的话,分组最终捕获到的数据是最后一次的匹配
```javascript
const reg = /^(\d)+$/;const string = '12345';
console.log(string.match(reg));
``````javascript
const reg = /^(\d)+ \1$/;const string = '12345 5';
console.log(string.match(reg));
```### 3.4 非捕获括号
之前出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们为捕获型分组和捕获型分支
如果只想要括号最原始的功能,但不会引用它,即,既不在 API 中引用,也不会在正则里反向引用
此时,可以使用非捕获括号 `(?:p)` 和 `(?:p1|p2|p3)`:
```javascript
const reg = /(?:ab)+/g;const string = 'ababa abbb ababab';
console.log(string.match(reg));
``````javascript
const reg = /^I love (?:JavaScript|Regular Expression)$/;console.log(reg.test('I love JavaScript'));
console.log(reg.test('I love Regular Expression'));
```### 3.5 案例分析
#### 3.5.1 模拟字符串 trim 方法
`trim` 方法是去掉字符串开头和结尾的空白符,有两种思路实现:
- 匹配到开头和结尾的空白符,然后替换成空字符
- 匹配整个字符串,然后用引用来提取出相应的数据```javascript
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}console.log(trim(' abc '));
``````javascript
function trim(str) {
return str.replace(/^\s*(.*?)\s*$/, '$1');
}console.log(trim(' abc '));
```#### 3.5.2 将每个单词的首字母转化为大写
```javascript
function titleCase(str) {
return str.toLowerCase().replace(/(?:^|\s+)\w/g, function (c) {
return c.toUpperCase();
});
}console.log(titleCase('my name is jerry'));
```#### 3.5.3 驼峰化
```javascript
function PascalCase(str) {
return str.replace(/[-_\s]+(.?)/g, function (match, c) {
return c ? c.toUpperCase() : '';
});
}console.log(PascalCase('-moz-transform '));
```#### 3.5.4 中划线化
```javascript
function strike(str) {
return str
.replace(/([A-Z])/g, '-$1')
.replace(/[-_\s]+/g, '-')
.toLowerCase();
}console.log(strike('MozTransform'));
```#### 3.5.5 HTML 转义和反转义
```javascript
function escape(str) {
const chars = {
'<': 'lt',
'>': 'gt',
'"': 'quot',
'&': 'amp',
"'": 'apos',
' ': 'nbsp'
};
return str.replace(new RegExp(`[${Object.keys(chars).join('')}]`, 'g'), function (match) {
return `&${chars[match]};`;
});
}console.log(escape("
this is container"));function unescape(str) {
const chars = {
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: "'",
nbsp: ' '
};
return str.replace(/&([^;]+);/g, function (match, key) {
if (key in chars) return chars[key];
return match;
});
}console.log(unescape(escape("
this is container")));
```#### 3.5.6 匹配成对标签
```javascript
const reg = /<([^>]+)>[^]*<\/\1>/g;const str1 = '
this is p
';
const str2 = 'this is p
this is div';
const str3 = `this is p
this is div`;console.log(str1.match(reg));
console.log(str2.match(reg));
console.log(str3.match(reg));
```## 4 回溯法原理
### 4.1 没有回溯的匹配
假设正则是 `/ab{1,3}c/`,当目标字符串是 `abbbc` 时,就没有所谓的回溯,其匹配过程如下:
![没有回溯的匹配](./images/mate1.png)
### 4.2 有回溯的匹配
如果目标字符串是 `abbc` 时,匹配过程中就有回溯:
![有回溯的匹配](./images/mate2.png)
再举个例子,正则是 `/ab{1,3}bbc/`,目标字符串是 `abbbc`,匹配过程如下:
![有回溯的匹配](./images/mate3.png)
再举个例子,正则是 `/".*"/`,目标字符串是 `"abc"de`,匹配过程如下:
![有回溯的匹配](./images/mate4.png)
> 可以看出 `.*` 非常影响效率,为了减少不必要的回溯,可以修改正则为 `/"[^"]*"/`
### 4.3 常见的回溯形式
正则表达式匹配字符串的这种方式,叫做回溯法
回溯法也称试探法,基本思想:从问题的某一状态【初始状态】出发,搜索从该状态出发所能达到的最深路径,当一条路走到尽头时,再后退一步或若干步,从另一种可能状态出发,继续搜索,直到所有路径都试探过
这种不断前进,不断回溯寻找解的方法,就叫做回溯法
本质上就是深度优先搜索算法,其中退到之前某一步的过程,就叫做回溯
从之前匹配过程中可以看出,当路走不通时,就会发生回溯,即尝试匹配失败时,接下来的一步通常就是回溯
#### 4.3.1 贪婪量词
贪婪量词,尝试可能的顺序是从多往少的方向去尝试
譬如,`b{1,3}` 首先会尝试 `bbb`,然后再看整个正则是否能匹配,不能匹配时,吐出一个 `b`,在 `bb` 的基础上再继续尝试,如果还不行,再吐出一个,再尝试,如果还不行,只能说匹配失败了
当多个贪婪量词挨着存在,并互相有冲突时,靠前的贪婪量词先下手为强:
```javascript
const reg = /^(\d{1,3})(\d{1,3})$/;const string = '12345';
console.log(string.match(reg));
```> 前面的 `\d{1,3}` 匹配 `123`,后面的 `\d{1,3}` 匹配 `45`
#### 4.3.2 惰性量词
惰性量词就是在贪婪量词后面加个问号,表示尽可能少的匹配
```javascript
const reg = /^(\d{1,3}?)(\d{1,3}?)$/;const string = '12345';
console.log(string.match(reg));
```> 前面的 `\d{1,3}?` 匹配 `12`,后面的 `\d{1,3}?` 无可奈何,只能匹配 `345`
虽然惰性量词不贪,但也会有回溯的现象:
![有回溯的匹配](./images/mate5.png)
#### 4.3.3 分支结构
分支结构也是惰性的
譬如,`/can|candy/` 去匹配 `candy`,得到结果 `can`
因为分支结构会一个一个去尝试,如果前面的满足了,就不会向后尝试了
分支结构,可能前面的子模式会形成局部匹配,如果接下来表达式整体不匹配,仍会继续尝试剩下的分支,这种尝试也可以看成一种回溯
譬如,用 `/^(?:can|candy)$/` 去匹配 `candy`:
![有回溯的匹配](./images/mate6.png)
> 既然有回溯的过程,那么匹配效率肯定低一些【相对那些 `DFA`【确定型有限自动机】引擎】
>
> 而 JavaScript 的正则引擎是 `NFA`【非确定型有限自动机】,大部分语言中的正则引擎都是 `NFA`
>
> 因为其虽然匹配慢,但是编译快## 5 拆分
### 5.1 结构和操作符
JavaScript 正则表达式中,有以下结构:
| 结构 | 说明 |
| ---------- | ------------------------------------------------------------ |
| 字符字面量 | 匹配一个具体字符,包括不用转义和需要转义的,比如 `a` 匹配字符 `a`,`\n` 匹配换行符,`\.` 匹配点 |
| 字符组 | 匹配一个字符,可以是多种可能之一,比如 `[0-9]` 匹配一个数字,也有 `\d` 的简写形式;另外还有反义字符组,表示除了特定字符之外任何一个字符,比如 `[^0-9]` 匹配一个非数字字符,也有 `\D` 的简写形式 |
| 量词 | 表示一个字符连续出现,比如 `a{1,3}` 表示 `a` 字符连续出现 1 到 3 次;另外还有常见的简写形式,比如 `a+` 表示 `a` 字符连续出现至少一次 |
| 锚 | 匹配一个位置,而不是字符;比如 `^` 匹配字符串开头;比如 `\b` 匹配单词边界;比如 `(?=\d)` 表示数字之前的位置 |
| 分组 | 用括号表示一个整体;比如 `(ab)+` 表示 `ab` 两个字符连续出现至少一次;也可以使用非捕获分组 `(?:ab)+` |
| 分支 | 多个子模式多选一;比如 `abc|bcd` 匹配 `abc` 或 `bcd`;反向引用 `\2` 表示引用第 2 个分组 |其中涉及到的操作符有:
| 操作符描述 | 操作符 |
| ------------ | ------------------------------------- |
| 转义符 | `\` |
| 括号和方括号 | `()`、`(?:)`、`(?=)`、`(?!)`、`[]` |
| 量词限定符 | `{m}`、`{m,n}`、`{m,}`、`?`、`*`、`+` |
| 位置和序列 | `^`、`$`、`\`元字符、一般字符 |
| 管道符 | `|` |> 上面操作符的优先级从上至下,由高到低
>
> 若正则表达式过于复杂,可借助[正则可视化网站](https://jex.im/regulex)分析结构### 5.2 注意要点
#### 5.2.1 匹配字符串整体问题
匹配整个字符串时,一般在正则前后加上锚 `^` 和 `$`
譬如,匹配字符串 `abc` 或 `bcd` 时,不小心就会写成 `/^abc|bcd$/`
而位置和字符优先级比管道符高,故其匹配结构:
![结构](./images/structure1.png)
应该修改成 `/^(abc|bcd)$/`:
![结构](./images/structure2.png)
#### 5.2.2 量词连缀问题
假设,要匹配如下要求字符串:
- 每个字符为 `a`、`b`、`c` 任选其一
- 字符串长度是 3 的倍数若写成 `/^[abc]{3}+$/`,这样会报错,说 `+` 前面没什么可重复的;应该修改成 `/^([abc]{3})+$/`:
![结构](./images/structure3.png)
#### 5.2.3 元字符转义问题
所谓元字符,就是正则中有特殊含义的字符:
- `^`
- `$`
- `.`
- `*`
- `+`
- `?`
- `|`
- `\`
- `/`
- `(`
- `)`
- `[`
- `]`
- `{`
- `}`
- `=`
- `!`
- `:`
- `-`当匹配上面字符本身时,一律可以转义:
```javascript
const reg = /^\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,$/;const string = '^$.*+?|\\/[]{}=!:-,';
console.log(reg.test(string));
```> 其中 `string` 中的 `\` 字符也需要转义
>
> 另外,在 `string` 中,也可以把每个字符转义,当然转义后结果仍是本身```javascript
const string1 = '^$.*+?|\\/[]{}=!:-,';const string2 = '\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,';
console.log(string1 === string2);
```> 那是不是每个字符都需要转义呢?否,看情况
##### 5.2.3.1 字符组中的元字符
跟字符组相关的元字符有 `[`、`]`、`^`、`-`
因此在会引起歧义的地方进行转义,例如开头的 `^` 必须转义,否则会视其为反义字符组
```javascript
const reg = /[\^$.*+?|\\/[\]{}=!:\-,]/g;const string = '^$.*+?|\\/[]{}=!:-,';
console.log(string.match(reg));
```##### 5.2.3.2 匹配字符串 `[abc]` 和 `{3,5}`
```javascript
const reg = /\[abc]/g;const string = '[abc][abc]';
console.log(string.match(reg));
``````javascript
const reg = /\{3,5}/g;const string = '{3,5}{3,5}';
console.log(string.match(reg));
```只需要在第一个方括号或花括号转义即可,因为后面的方括号和花括号构不成字符组和量词,正则不会引发歧义,自然不需要转义
另外,量词有简写形式 `{m,}`,却没有 `{,n}` 的情况,因此这种情况也不需要转义
```javascript
const reg = /{,3}/g;const string = '{,3}{,3}';
console.log(string.match(reg));
```##### 5.2.3.3 其余情况
比如 `=`、`!`、`:`、`-`、`,` 等符号,只要不在特殊结构中,均不需要转义
但是,括号需要前后都进行转义,如 `/\(123\)/`
至于剩下的 `^`、`$`、`.`、`*`、`+`、`?`、`|`、`\`、`/` 等字符,只要不在字符组中,均需要转义
### 5.3 案例分析
#### 5.3.1 身份证
```javascript
const reg = /^(\d{15}|\d{17}[\dxX])$/;const string = '511133198007151216';
console.log(string.match(reg));
```#### 5.3.2 IPV4 地址
```javascript
const reg = /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/;const string = '10.24.2.255';
console.log(string.match(reg));
```## 6 构建
### 6.1 平衡法则
构建正则需要做到以下几点的平衡:
- 匹配预期的字符串
- 不匹配非预期的字符串
- 可读性和可维护性
- 效率### 6.2 构建正则前提
#### 6.2.1 是否能使用正则
正则太强大了,以至于随便遇到一个操作字符串问题时,都会考虑使用正则
但正则也有其局限性,比如匹配类似 `101001000100001...` 这样的字符串就有心无力
#### 6.2.2 是否有必要使用正则
认识到正则的局限,不要去研究正则无法胜任的工作
同时,也不能走入另一个极端,能用字符串 API 解决的简单任务,也不该正则出马
比如,从日期中提取出年月日:
```javascript
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;const string = '2024-05-08';
console.log(string.match(reg));
console.log(string.split('-'));
```比如,判断是否有问号:
```javascript
const reg = /\?/;const string = '?id=xx&&?=search';
console.log(string.search(reg));
console.log(string.indexOf('?'));
```比如,获取子串:
```javascript
const reg = /^.{4}(.+)$/;const string = 'JavaScript';
console.log(string.match(reg));
console.log(string.slice(4));
```#### 6.2.3 是否有必要构建一个复杂的正则
比如,密码匹配问题,要求密码长度 6 到 12 位,由数字、小写字符、大写字符组成,但必须至少包括 2 种字符
其实可以使用多个小正则来做:
```javascript
const reg1 = /^[0-9a-zA-Z]{6,12}$/;
const reg2 = /^[0-9]{6,12}$/;
const reg3 = /^[a-z]{6,12}$/;
const reg4 = /^[A-Z]{6,12}$/;function checkPassword(string) {
if (!reg1.test(string)) return false;
if (reg2.test(string)) return false;
if (reg3.test(string)) return false;
if (reg4.test(string)) return false;
return true;
}console.log(checkPassword('1qazxsw2'));
```### 6.3 准确性
所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标
#### 6.3.1 匹配固定电话
比如,匹配如下格式的固定电话号码:
```tex
055188888888
0551-88888888
(0551)88888888
```- 了解各部分的模式规则
- 区号是 `0` 开头的 3 到 4 位数字【`0\d{2,3}`】
- 号码是非 `0` 开头的 7 到 8 位数字【`[1-9]\d{6,7}`】
- 三种形式
- `/^0\d{2,3}[1-9]\d{6,7}$/`
- `/^0\d{2,3}\-[1-9]\d{6,7}$/`
- `/^\(0\d{2,3}\)[1-9]\d{6,7}$/`
- 三种形式关系
- 这三种形式是或的关系,可以构建分支
- `/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}\-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/`
- 提取公共部分
- `/^(0\d{2,3}|0\d{2,3}\-|\(0\d{2,3}\))[1-9]\d{6,7}$/`
- 进一步简写
- `/^(0\d{2,3}\-?|\(0\d{2,3}\))[1-9]\d{6,7}$/`#### 6.3.2 匹配浮点数
要求匹配如下格式:
```tex
1.23
+1.23
-1.23
10
+10
-10
.2
+.2
-.2
```- 了解各部分的模式规则
- 符号【`[\+\-]`】
- 整数【`\d+`】
- 小数【`\.\d+`】
- 组合【`/^[\+\-]?(\d+)?(\.\d+)?$/`】
- 此组合看起来没问题,但这个正则也会匹配空字符
- 换种思路
- 匹配 `1.23` 这种【`/^[\+\-]?\d+\.\d+$/`】
- 匹配 `10` 这种【`/^[\+\-]?\d+$/`】
- 匹配 `.2` 这种【`/^[\+\-]?\.\d+$/`】
- 组合
- 三者是或的关系,提取公共部分
- `/^[\+\-]?(\d+\.\d+|\d+|\.\d+)$/`### 6.4 效率
保证了准确性后,才需要考虑是否要优化
大多数情况是不需要优化的,除非运行的非常慢
正则表达式的运行分为如下的阶段:
- 编译
- 设定起始位置
- 尝试匹配
- 匹配失败的话,从下一位开始继续尝试匹配
- 最终结果,匹配成功或失败```javascript
const reg = /\d+/g; // 1const string = '123abc34def'; // 2
console.log(reg.lastIndex, reg.exec(string)); // 3
console.log(reg.lastIndex, reg.exec(string)); // 4
console.log(reg.lastIndex, reg.exec(string)); // 5
console.log(reg.lastIndex, reg.exec(string)); // 6
```- 第一步,生成一个正则,引擎会对其进行编译,报错与否出现在这个阶段
- 第三步,尝试匹配,需要确定从哪一个位置开始匹配,一般情形都是字符串开头,也就是第 0 位
> 当使用 `test` 和 `exec` 方法且正则有 `g` 时,起始位置是从正则对象的 `lastIndex` 属性开始
- 第四步,第三步从第 0 位匹配到第 2 位,这一步就从第 3 位开始匹配
- 第五步,第四步从第 6 位匹配到第 7 位,这一步就从第 8 位开始匹配
- 第六步,第五步从第 8 位匹配到结尾失败,这一步从开头重新开始匹配
#### 6.4.1 使用具体型字符组来代替通配符,来消除回溯
比如,匹配双引号之间的字符,用 `/\".*\"/` 匹配 `123"abc"456` 中的 `abc`:
![有回溯的匹配](./images/mate7.png)
用惰性量词 `/\".*?\"/` 匹配:
![有回溯的匹配](./images/mate8.png)
> 因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用,注定要占用一定的内存
>
> 此时,要使用具体化的字符组,来代替通配符,以便消除不必要的字符,此时使用正则 `/\"[^\"]*\"/`#### 6.4.2 使用非捕获型分组
因为括号的作用之一是,捕获分组和分支中的数据,那么就需要内存来保存它们
当不需要使用分组数据和反向引用时,可以使用非捕获型分组
#### 6.4.3 独立出确定字符
比如,`/a+/` 可以修改成 `/aa*/`,后者比前者多确定字符 `a`,这样可以加快判断是否匹配失败
#### 6.4.4 提取分支公共部分
比如,`/^abc|^def/` 可以修改成 `/^(?:abc|def)/`
又比如,`/this|that/` 可以修改成 `/th(?:is|at)/`
#### 6.4.5 减少分支数量,缩小它们范围
比如,`/red|read/` 可以修改成 `/rea?d/`
## 7 编程
### 7.1 四种操作
正则表达式是匹配模式,不管如何使用正则表达式,都需要先匹配
有了匹配这一基本操作后,才有其它操作:验证、切分、提取、替换
进行任何操作,都需要宿主引擎相关 API 的配合使用,当然,在 JavaScript 中,相关 API 也不多
#### 7.1.1 验证
验证是正则表达式最直接的应用,比如表单验证
所谓匹配,就是看目标字符串中是否有满足匹配条件的子串,因此,匹配的本质就是查找
有没有匹配,是不是匹配上,判断是否的操作,即称为验证
比如,判断一个字符串中是否有数字:
```javascript
const reg = /\d/;const string = 'abc123';
console.log(!!~string.search(reg));
``````javascript
const reg = /\d/;const string = 'abc123';
console.log(reg.test(string));
``````javascript
const reg = /\d/;const string = 'abc123';
console.log(!!string.match(reg));
``````javascript
const reg = /\d/;const string = 'abc123';
console.log(!!reg.exec(string));
```#### 7.1.2 切分
所谓切分,就是把目标字符串,切成一段一段的
在 JavaScript 中,使用 `split`
```javascript
const reg = /,/;const string = 'html,css,javascript';
console.log(string.split(reg));
```又比如,如下的日期格式:
```tex
2017/06/26
2017.06.26
2017-06-26
``````javascript
const reg = /\D/;const string1 = '2017/06/26';
const string2 = '2017.06.26';
const string3 = '2017-06-26';console.log(string1.split(reg));
console.log(string2.split(reg));
console.log(string3.split(reg));
```#### 7.1.3 提取
虽然整体匹配上了,但有时需要提取部分匹配的数据
此时,正则通常使用分组引用【捕获】功能,还需要配合使用相关 API
```javascript
const reg = /^(\d{4})\D(\d{2})\D(\d{2})$/;const string = '2024-05-09';
console.log(string.match(reg));
``````javascript
const reg = /^(\d{4})\D(\d{2})\D(\d{2})$/;const string = '2024-05-09';
console.log(reg.exec(string));
``````javascript
const reg = /^(\d{4})\D(\d{2})\D(\d{2})$/;const string = '2024-05-09';
reg.test(string);
console.log(RegExp.$1, RegExp.$2, RegExp.$3);
``````javascript
const reg = /^(\d{4})\D(\d{2})\D(\d{2})$/;const string = '2024-05-09';
string.search(reg);
console.log(RegExp.$1, RegExp.$2, RegExp.$3);
``````javascript
const reg = /^(\d{4})\D(\d{2})\D(\d{2})$/;const string = '2024-05-09';
const arr = [];
string.replace(reg, function (match, year, month, day) {
console.log(match, year, month, day);
arr.push(year, month, day);
});console.log(arr);
```#### 7.1.4 替换
找,往往不是目的
通常下一步是为了替换
在 JavaScript 中,使用 `replace` 进行替换
比如,把日期格式 `yyyy-mm-dd` 替换成 `yyyy/mm/dd`:
```javascript
const reg = /\-/g;const string = '2024-05-09';
console.log(string.replace(reg, '/'));
```### 7.2 相关 API 注意要点
- `String#search`
- `String#split`
- `String#match`
- `String#replace`
- `RegExp#test`
- `RegExp#exec`#### 7.2.1 search 和 match 的参数问题
字符串的 4 个方法都支持正则和字符串参数
但是 `search` 和 `match` 会把字符串转换为正则:
```javascript
const string = '2024.05.10';console.log(string.search('.'));
console.log(string.search(/./));
console.log(string.search('\\.'));
console.log(string.search(/\./));console.log(string.match('.'));
console.log(string.match(/./));
console.log(string.match('\\.'));
console.log(string.match(/\./));console.log(string.split('.'));
console.log(string.split(/./));
console.log(string.split('\\.'));
console.log(string.split(/\./));console.log(string.replace('.', '/'));
console.log(string.replace(/./, '/'));
console.log(string.replace('\\.', '/'));
console.log(string.replace(/\./, '/'));
```#### 7.2.2 match 返回结果的格式问题
`match` 返回结果的格式,与正则对象是否有修饰符 `g` 有关
```javascript
const string = '2024.05.10';const reg1 = /\b(\d+)\b/;
const reg2 = /\b(\d+)\b/g;console.log(string.match(reg1));
console.log(string.match(reg2));
```没有 `g`,返回的是标准匹配格式,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串
有 `g`,返回的是所有匹配的内容
当没有匹配时,不管有无 `g`,都返回 `null`
#### 7.2.3 exec 比 match 更强大
当正则没有 `g` 时,使用 match 返回标准匹配格式
但是有 `g` 后,返回所有匹配的内容,就没有关键信息 `index` 了
而 `exec` 就能解决这个问题,它能接着上一次匹配后继续匹配
```javascript
const string = '2024.05.10';const reg1 = /\b(\d+)\b/;
const reg2 = /\b(\d+)\b/g;console.log(reg1.exec(string));
console.log(reg1.lastIndex);
console.log(reg1.exec(string));
console.log(reg1.lastIndex);
console.log(reg1.exec(string));
console.log(reg1.lastIndex);console.log(reg2.exec(string));
console.log(reg2.lastIndex);
console.log(reg2.exec(string));
console.log(reg2.lastIndex);
console.log(reg2.exec(string));
console.log(reg2.lastIndex);
console.log(reg2.exec(string));
console.log(reg2.lastIndex);
console.log(reg2.exec(string));
console.log(reg2.lastIndex);
```正则实例的 `lastIndex` 属性,表示下一次匹配开始的位置
在使用 `exec` 时,经常需要配合使用 `while` 循环:
```javascript
const string = '2024.05.10';const reg = /\b(\d+)\b/g;
let result;
while ((result = reg.exec(string))) {
console.log(result, reg.lastIndex);
}
```#### 7.2.4 修饰符 g 对 exec 和 test 的影响
字符串的四个方法,每次匹配时,都是从 0 开始,即 `lastIndex` 始终为 0
而正则实例的两个方法 `exec` 和 `test`,当正则是全局匹配时,每一次匹配完成后,都会修改 `lastIndex`:
```javascript
const reg = /a/g;console.log(reg.test('a'), reg.lastIndex);
console.log(reg.test('aba'), reg.lastIndex);
console.log(reg.test('ababc'), reg.lastIndex);
```注意,第三次调用 `test`,从 3 位置处开始匹配,自然就找不到了
> 当 `exec` 结果为 `null` 或 `test` 结果为 `false` 时,就会重置 `lastIndex` 为 0
如果没有 `g` 修饰符,`lastIndex` 始终为 0:
```javascript
const reg = /a/;console.log(reg.test('a'), reg.lastIndex);
console.log(reg.test('aba'), reg.lastIndex);
console.log(reg.test('ababc'), reg.lastIndex);
```#### 7.2.5 test 整体匹配时需要使用 ^ 和 $
`test` 是看目标字符串中是否有子串匹配正则,即有部分匹配即可
如果要整体匹配,正则前后需要添加开头和结尾
#### 7.2.6 split 相关注意事项
- 它可以有第二个参数,表示结果数组的最大长度
```javascript
const string = 'html,css,javascript';
console.log(string.split(/,/, 2));
```- 正则使用分组时,结果数组中是包含分隔符的
```javascript
const string = 'html,css,javascript';
console.log(string.split(/(,)/));
```#### 7.2.7 replace 是很强大的
`replace` 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数
当第二个参数是字符串时,如下的字符有特殊的含义:
| 属性 | 描述 |
| ------------------- | ------------------------------ |
| `$1`,`$2`,...,`$99` | 匹配第 1-99 个分组里捕获的文本 |
| `$&` | 匹配到的子串文本 |
| $` | 匹配到的子串的左边文本 |
| `$'` | 匹配到的子串的右边文本 |
| `$$` | 美元符号 |```javascript
const string = '2,3,5';console.log(string.replace(/^(\d+),(\d+),(\d+)$/, '$3=$1+$2'));
``````javascript
const string = '2,3,5';console.log(string.replace(/\d+/g, '$&$&$&'));
``````javascript
const string = '2+3=5,3+2=5';console.log(string.replace(/=/g, "$&$`$&$'$&"));
```当第二个参数是函数时:
```javascript
const string = '1234 2345 3456';string.replace(/(\d)\d{2}(\d)/g, function () {
console.log(...arguments);
});
```#### 7.2.8 使用构造函数需要注意的问题
一般不推荐使用构造函数生成正则,而应该优先使用字面量,因为使用构造函数会写很多 `\`,可读性差
```javascript
const string = '2017-06-27 2017.06.27 2017/06/27';const reg1 = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
const reg2 = new RegExp('\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}', 'g');console.log(string.match(reg1));
console.log(string.match(reg2));
```#### 7.2.9 修饰符
| 修饰符 | 描述 |
| ------ | ------------------------------------------------------------ |
| `g` | 全局匹配,即找到所有匹配的,单词是 `global` |
| `i` | 忽略字母大小写,单词是 `ignoreCase` |
| `m` | 多行匹配,只影响 `^` 和 `$`,二者变成行的概念,即行开头和行结尾,单词是 `multiline` |```javascript
const reg = /./gim;console.log(reg.global);
console.log(reg.ignoreCase);
console.log(reg.multiline);
```#### 7.2.10 source 属性
正则实例对象属性,除了 `global`、`ignoreCase`、`multiline`、`lastIndex` 之外,还有一个 `source` 属性
在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:
```javascript
const className = 'high';const reg = new RegExp('(^|\\s)' + className + '(\\s|$)');
console.log(reg.source);
```#### 7.2.11 构造函数属性
构造函数的静态属性基于所执行的最近一次正则操作而变化
除了 `$1` 到 `$9` 之外,还有几个不常用的属性【有兼容性问题】:
| 静态属性 | 描述 | 简写形式 |
| --------------------- | ----------------------------------- | -------------- |
| `RegExp.input` | 最近一次目标字符串 | `RegExp["$_"]` |
| `RegExp.lastMatch` | 最近一次匹配的文本 | `RegExp["$&"]` |
| `RegExp.lastParen` | 最近一次捕获的文本 | `RegExp["$+"]` |
| `RegExp.leftContext` | 目标字符串中 `lastMatch` 之前的文本 | RegExp["$`"] |
| `RegExp.rightContext` | 目标字符串中 `lastMatch` 之后的文本 | `RegExp["$'"]` |```javascript
const string = 'a1b2c3d4e5';const reg = /([abc])(\d)/g;
console.log(string.match(reg));
console.log(RegExp.input);
console.log(RegExp.lastMatch);
console.log(RegExp.lastParen);
console.log(RegExp.leftContext);
console.log(RegExp.rightContext);
```## 更多
请 [点击](./docs) 以阅读迷你书