一、正则表达式字符匹配
原书这么一句话,特别棒:正则表达式是匹配模式,要么匹配字符,要么匹配位置,要记住。
1. 两种模糊匹配
正则表达式的强大在于它的模糊匹配,这里介绍两个方向上的“模糊”:横向模糊和纵向模糊。
- 横向模糊匹配
即一个正则可匹配的字符串长度不固定,可以是多种情况。
如 /ab{2,5}c/
表示匹配: 第一个字符是 "a"
,然后是 2 - 5 个字符 "b"
,最后是字符 "c"
:
let r = /ab{2,5}c/g;
let s = "abc abbc abbbc abbbbbbc";
s.match(r); // ["abbc", "abbbc"]
- 纵向模糊匹配
即一个正则可匹配某个不确定的字符,可以有多种可能。
如 /[abc]/
表示匹配 "a", "b", "c"
中任意一个。
let r = /a[123]b/g;
let s = "a0b a1b a4b";
s.match(r); // ["a1b"]
2. 字符组
- 范围表示法
可以指定字符范围,比如 [1234abcdUVWXYZ]
就可以表示成 [1-4a-dU-Z]
,使用 -
来进行缩写。
如果要匹配 "a", "-", "z"
中任意一个字符,可以这么写: [-az]
或 [a\-z]
或 [az-]
。
- 排除字符组
即需要排除某些字符时使用,通过在字符组第一个使用 ^
来表示取反,如 [^abc]
就表示匹配除了 "a", "b", "c"
的任意一个字符。
- 常见简写形式
| 字符组 | 具体含义 |
| :—-: | —- |
|
\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]
和 [^]
任意一个。
3. 量词
量词也称重复,常用简写如下:
量词 | 具体含义 |
---|---|
{m,} |
表示至少出现 m 次。 |
{m} |
等价于 {m, m} ,表示出现 m 次。 |
? |
等价于 {0, 1} ,表示出现或不出现。 |
+ |
等价于 {1, } ,表示至少出现1次。 |
* |
等价于 {0, } ,表示出现任意次,也有可能不出现。 |
- 贪婪匹配和惰性匹配
在正则 /\d{2,4}/
,表示数字连续出现 2 - 4 次,可以匹配到 2 位、 3 位、4 位连续数字。
但是在 贪婪匹配 如 /\d{2,4}/g
,会尽可能多匹配,如超过 4 个,就只匹配 4 个,如有 3 个,就匹配 3 位。
而在 惰性匹配 如 /\d{2,4}?/g
,会 尽可能少 匹配,如超过 2 个,就只匹配 2 个,不会继续匹配下去。
let r1 = /\d{2,4}/g;
let r2 = /\d{2,4}?/g;
let s = "123 1234 12345";
s.match(r1); // ["123", "1234", "1234"]
s.match(r2); // ["12", "12", "34", "12", "34"]
惰性量词 | 贪婪量词 |
---|---|
{m,m}? |
{m,m} |
{m,}? |
{m,} |
?? |
? |
+? |
+ |
*? |
* |
4. 多选分支
即提供多个子匹配模式任选一个,使用 |
(管道符)分隔,由于分支结构也是惰性,即匹配上一个后,就不会继续匹配后续的。
格式如:(r1|r2|r3)
,我们就可以使用 /leo|pingan/
来匹配 "leo"
和 "pingan"
。
let r = /leo|pingan/g;
let s = "leo cool,pingan good.";
s.match(r);// ["leo", "pingan"]
// 多选分支的惰性表现
let r1 = /leo|leooo/g;
let r2 = /leooo|leo/g;
let s = "leooo";
s.match(r1);// ["leo"]
s.match(r2);// ["leooo"]
5. 分组命名 (ES9)
ES9 之前的分组是通过数字命名的:
const pattern = /(\d{4})-(\d{2})-(\d{2})/u;
const result = pattern.exec('2020-01-05');
console.log(result[0]); // 打印"2020-01-05"
console.log(result[1]); // 打印"2020"
console.log(result[2]); // 打印"01"
console.log(result[3]); // 打印"05"
现在可以通过指定分组的名称,增加代码可读性,便于维护:
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
const result = pattern.exec('2020-01-05');
console.log(result.groups.year); // 打印"2020-01-05"
console.log(result.groups.month); // 打印"01"
console.log(result.groups.day); // 打印"05"
分组命名还可以和 String.prototype.replace
方法结合:
const reDate = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const d = '2020-01-05';
const USADate = d.replace(reDate, '$<month>-$<day>-$<year>');
console.log(USADate); // 01-05-2020
6. 忽略分组
如果不希望捕获某些分组,在分组内加上 ?:
即可。
比如 (?:tom).(ok)
那么这里 $1
指的就是 ok
。
7. 案例分析
匹配字符,无非就是字符组、量词和分支结构的组合使用。
- 十六进制颜色值匹配
let r = /#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g;
let s = "#ffaacc #Ff00DD #fFF #01d #9Gd";
s.match(r); // ["#ffaacc", "#Ff00DD", "#fFF", "#01d"]
- 时间和日期匹配
// 时间 12:23 或 01:09
let r = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
r.test("23:25"); // true
r.test("03:05"); // true
// 时间 12:23 或 1:9
let r = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
r.test("23:25"); // true
r.test("03:05"); // true
r.test("3:5"); // true
// 日期 yyyy-mm-dd
let r = /^[0-9]{4}-(0[1-9]|[1][0-2])-(0[1-9]|[12][0-9]|3[01])$/;
r.test("2019-09-19"); // true
r.test("2019-09-32"); // false
- Windows操作系统文件路径匹配
盘符使用 [a-zA-Z]:\\
,这里需要注意 \
字符需要转义,并且盘符不区分大小写;
文件名或文件夹名,不能包含特殊字符,使用 [^\\:*<>|"?\r\n/]
表示合法字符;
并且至少有一个字符,还有可以出现任意次,就可以使用 ([^\\:*<>|"?\r\n/]+\\)*
匹配任意个 文件夹\
;
还有路径最后一部分可以是 文件夹
,即没有 \
于是表示成 ([^\\:*<>|"?\r\n/]+)?
。
let r = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
r.test("C:\\document\\leo\\a.png"); // true
r.test("C:\\document\\leo\\"); // true
r.test("C:\\document"); // true
r.test("C:\\"); // true
- id匹配
如提取 <div id="leo" class="good"></id>
中的 id="leo"
:
let r1 = /id=".*"/; // tips1
let r2 = /id=".*?"/; // tips2
let r3 = /id="[^"]*"/; // tips3
let s = '<div id="leo" class="good"></id>';
s.match(r1)[0]; // id="leo" class="good"
s.match(r2)[0]; // id="leo"
s.match(r3)[0]; // id="leo"
tips1:由于 .
匹配双引号,且 *
贪婪,就会持续匹配到最后一个双引号结束。
tips2:使用惰性匹配,但效率低,有回溯问题。
tips3:最终优化。