正则表达式
由 普通字符(例如字母、数字等)、特殊字符(有特殊含义的,例如.\等)两种字符组成,可以与字符串进行匹配的模板,处理字符串增删查改更加灵活。
常用正则表达式
| 描述 | 正则表达式 | 备注 |
|---|
| 数字 | ^[0-9]*$ | |
| n 位数字 | ^\d{n}$ | |
| 至少 n 位数字 | ^\d{n, }$ | |
| m~n 位数字 | ^\d{m, n}$ | |
| 整数 | ^(-?[1-9]\d*)$ | 非 0 开头,包括正整数和负整数 |
| 正整数 | ^[1-9]\d*$ | |
| 负整数 | ^-[1-9]\d*$ | |
| 非负整数 | ^(([1-9]\d*)|0)$ | |
| 非正整数 | ^((-[1-9]\d*)|0)$ | |
| 浮点数 | ^-?(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0.0+|0)$ | 包括正浮点数和负浮点数 |
| 正浮点数 | ^(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ | |
| 负浮点数 | ^-(?:[1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ | |
| 非正浮点数 | ^(?:-(?:[1-9]\d*\.\d+|0\.\d*[1-9]\d*)|0.0+|0)$ | 包含 0 |
| 非负浮点数 | ^(?:[1-9]\d*.\d+|0\.\d+|0\.0+|0)$ | 包含 0 |
| 仅一位小数 | ^-?(?:0|[1-9][0-9]*)\.[0-9]{1}$ | |
| 最少一位小数 | ^-?(?:0|[1-9][0-9]*)\.[0-9]{1, }$ | |
| 最多两位小数 | ^-?(?:0|[1-9][0-9]*)\.[0-9]{1, 2}$ | |
| 连续重复的数字 | ^(\d)\1+$ | 例如:111,222 |
| 中文 | ^[\u4E00-\u9FA5]+$ | |
| 全角字符 | ^[\uFF00-\uFFFF]+$ | |
| 半角字符 | ^[\u0000-\u00FF]+$ | |
| 英文字符串(大写) | ^[A-Z]+$ | |
| 英文字符串(小写) | ^[a-z]+$ | |
| 英文字符串(不区分大小写) | ^[A-Za-z]+$ | |
| 中文和数字 | ^(?:[\u4E00-\u9FA5]{0,} | \d)+$ |
| 英文和数字 | ^[A-Za-z0-9]+$ | |
| 数字、英文字母或者下划线组成的字符串 | ^\w+$ | |
| 中文、英文、数字包括下划线 | ^[\u4E00-\u9FA5\w]+$ | |
| 不含字母的字符串 | ^[^a-za-z]*$ | |
| 连续重复的字符串 | ^(.)\1+$ | 例如:aa,bb |
| 长度为 n 的字符串 | ^.{n}$ | |
| ASCII | ^[ -~]$ | |
| 日期 | ^\d{1,4}-(?:1[0-2]|0?[1-9])-(?:0?[1-9]|[1-2]\d|30|31)$ | 弱校验,例如:2022-06-12 |
| 时间 | ^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$ | 12 小时制,例如:11:21:31 |
| 时间 | ^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$ | 24 小时制,例如:23:21:31 |
| 中文名 | ^[\u4E00-\u9FA5·]{2,16}$ | |
| 英文名 | ^[a-zA-Z][a-za-z\s]{0,20}[a-zA-Z]$ | |
| 火车车次 | ^[GCDZTSPKXLY1-9]\d{1,4}$ | 例如:G1234 |
| 手机号 | ^(?:(?:\+|00)86)?1[3-9]\d{9}$ | 弱匹配 |
| 固话号码 | ^(?:(?:\d{3}-)?\d{8}|^(?:\d{4}-)?\d{7,8})(?:-\d+)?$ | |
创建正则表达式对象
const regex = /hello/;
const char = 'hello';
const regex = eval(`/${char}/`);
const regex = new RegExp('o', 'g');
正则表达式速查表
1. 字符
| 表达式 | 描述 |
|---|
| [abc] | 字符集。匹配集合中所含的任一字符 |
| [^abc] | 否定字符集。匹配任何不在集合中的字符 |
| [a-z] | 字符范围。匹配指定范围内的任意字符 |
| . | 匹配除换行符以外的任何单个字符 |
| \ | 转义字符 |
| \w | 匹配任何字母数字,包括下划线(等价于[A-Za-z0-9_]) |
| \W | 匹配任何非字母数字(等价于[^a-za-z0-9_]) |
| \d | 数字。匹配任何数字 |
| \D | 非数字。匹配任何非数字字符 |
| \s | 空白。匹配任何空白字符,包括空格、制表符等 |
| \S | 非空白。匹配任何非空白字符 |
| \p | 检测字符属性 |
2. 分组和引用
| 表达式 | 描述 |
|---|
| (expression) | 分组。匹配括号里的整个表达式符 |
| (?:expression) | 非捕获分组。匹配括号里的整个字符串但不获取匹配结果,拿不到分组引用 |
| \num | 对前面所匹配分组的引用。 |
| (?<name>expression) | 给分组命名为 name |
3. 锚点/边界
| 表达式 | 描述 |
|---|
| ^ | 匹配字符串或行开头 |
| $ | 匹配字符串或行结尾 |
| \b | 匹配单词边界,一边字符是\w,一边字符不是\w |
| \B | 匹配非单词边界,两边都为\w 或者 \W |
4. 数量表示
| 表达式 | 描述 |
|---|
| ? | 匹配前面的表达式 0 个或 1 个。即表示可选 |
| + | 匹配前面的表达式至少 1 个 |
| * | 匹配前面的表达式 0 个或多个 |
| | | 或运算符。并集,可以匹配符号前后的表达式 |
| {m} | 匹配前面的表达式 m 个 |
| {m, } | 匹配前面的表达式最少 m 个 |
| {m, n} | 匹配前面的表达式最少 m 个,最多 n 个 |
5. 断言/环视
后行断言中必须使用定长的量词,若出现分支,要求分支长度一致。(因为,后行断言时,无法确定需要回溯多少步)。
注意:现在后行断言是ECMAScript 2018规范的一部分。截止2019年末,谷歌的Chrome浏览器是唯一支持后行断言的。所以如果项目对浏览器的兼容性要求很高,就不要在JavaScript中使用后行断言。
| 表达式 | 描述 |
|---|
| (?=) | 零宽正向先行断言,a(?=b) 匹配 a 的右侧是 b |
| (?!) | 零宽反向先行断言,a(?!b) 匹配 a 的右侧不是 b |
| (?<=) | 零宽正向后行断言,(?<=b)a 匹配 a 左侧是 b |
| (?<!) | 零宽反向后行断言,(?<!b)a 匹配 a 左侧不是 b |
6. 模式
| 表达式 | 描述 |
|---|
| /.../i | 忽略大小写 |
| /.../g | 全局匹配 |
| /.../m | 多行修饰符。用于多行匹配 |
| /.../s | 视为单行 |
| /.../u | 允许 unicode 匹配,针对多字节(汉字),配合 \p 使用 |
| /.../y | 连续检索,下一个不匹配则结束 |
console.log(/a.b/.test('a😹b'));
console.log(/a.b/u.test('a😹b'));
console.log(/\u{41}/u.test('A'));
console.log(/\u{41}/iu.test('a'));
var str = '11111,2222,3333xxxxx44444aaaaa5555';
var str1 = 'xxxxxx,11111,2222,3333xxxxx44444aaaaa5555';
const arr = [...str.matchAll(/(?<num>\d+),?/g)];
const arr_y = [...str.matchAll(/(?<num>\d+),?/gy)];
const arr1 = [...str1.matchAll(/(?<num>\d+),?/g)];
const arr_y1 = [...str1.matchAll(/(?<num>\d+),?/gy)];
console.log('arr', arr);
console.log('arr_y', arr_y);
console.log('arr1', arr1);
console.log('arr_y1', arr_y1);
正则表达式使用
1. test —— 测试是否匹配,返回 true 或 false
const str = 'hello world 2022!';
console.log('test', /world/.test(str));
2. exec —— 查找字符串中匹配的 String ,返回一个数组(未匹配到则返回 null)
const str = 'hello world 2022!';
const regex = /o/g;
console.log('exec', regex.exec(str));
console.log('exec', regex.exec(str));
let resultArr = [];
let count = 0;
while ((resultArr = regex.exec(str)) !== null) {
count++;
console.log(`exec-${count}`, resultArr, regex.lastIndex);
}
3. match —— 查找匹配的 String ,它返回一个数组,在未匹配到时会返回 null。match 不会记录上一次成功匹配的结果
const str = 'hello world 2022!';
const regex = /o/;
console.log('match', str.match(regex));
4. matchAll —— 执行查找所有匹配的 String,它返回一个迭代器(iterator)。正则必须是设置了全局模式 g 的形式,替代在 exec 方法中使用循环获取结果
const str = 'abaabbabaab';
const regex = /ab/g;
const array = [...str.matchAll(regex)];
console.log('matchAll', array);
const regex1 = /(a){2}b/g;
const array1 = [...str.matchAll(regex1)];
console.log('matchAll', array1);
5. search —— 测试匹配的 String 方法,返回匹配到的位置索引,或者在失败时返回 -1
const str = 'hello world 2022!';
const regex = /o/g;
console.log(str.search(regex));
6. replace —— 查找匹配的 String,并且使用替换字符串替换掉匹配到的子字符串
const str = 'hello world 2022!';
const regex = /o/g;
console.log(str.replace(regex, 'mm'));
const date = '2022/02/12 2101/04/23';
const regex = /(\d{4}).([0-1]?\d).([0-2]?\d)/g;
const rplStr = date.replace(regex, (m, m1, m2, m3) => {
return `${m1}-${m2}-${m3}`;
});
console.log(rplStr);
7. split —— 使用正则表达式或者一个固定字符串分隔一个字符串,返回数组
const str = 'hello world 2022!';
const regex = /o/g;
console.log(str.split(regex));
const names = 'Harry Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand . aaa#bbb';
const regex1 = /\s*(?:;|\.|#)\s*/g;
const nameList = names.split(regex1);
console.log(nameList);
避免正则表达式因回溯过多导致的内存溢出:
console.time('test');
/^((((((.*).)*.)*.)*.)*.)*x$/.exec('123456789012345!');
console.timeEnd('test');
js 的正则引擎是 NFA(非确定有限状态自动机),在匹配目标字符串时,它从左到右逐个测试表达式的组成部分,看是否能找到匹配项。在遇到量词时,需要决定何时尝试匹配更多字符。在遇到分支时,必须从可选项中选择一个尝试匹配。每当正则做类似的决定时,如果有必要,都会记录其他选择(作为备选),以便匹配不成功时进行回溯,到最后一个决策点,再重新进行匹配。
1. 量词回溯 (贪婪量词:' + ' ' * ',懒惰量词:' ? ' )
比如 a?bc 匹配 ac
? 表示 0 或 1 次,所以在进行匹配时,两种情况都要考虑到,先进行 1 次匹配,将 0 次的情况放入备选状态,若匹配失败,就要进行回溯。比如,使用 /ab?c/ 匹配 ac 。当正则的位置在 b 时,因为有量词修饰,所以将 0 次纳入备选状态。开始匹配,ab --> ac 匹配失败;取出备选状态,ac --> ac 匹配成功
比如 /^(a+)+$/ 匹配 aaaaa!
| 步骤 | 模式位置 | 字符串位置 | 结果 |
|---|
| 1 | ^ | 'aaaaa!' | 可能匹配 |
| 2 | ^a | 'aaaaa!' | 可能匹配 |
| 3 | ^(a+) | '!' | 可能匹配 |
| 4 | ^(a+)+ | '!' | 可能匹配 |
| 5 | ^(a+)+$ | '!' | 可能的可能匹配 |
| 6 | ^(a+) | 'a!' | 可能匹配 |
| 7 | ^(a+)+ | 'a!' | 可能匹配 |
| 8 | ^(a+)+$ | 'a!' | 可能的可能匹配 |
| 9 | ^(a+) | 'aa!' | 可能匹配 |
| 10 | ^(a+)+ | 'aa!' | 可能匹配 |
| 11 | ^(a+)+$ | 'aa!' | 可能的可能匹配 |
| 12 | ^(a+) | 'aaa!' | 可能匹配 |
| 13 | ^(a+)+ | 'a!' | 可能匹配 |
| 14 | ^(a+)+$ | 'a!' | 可能的可能匹配 |
| 15 | ^(a+)+ | 'aaa!' | 可能匹配 |
| 16 | ^(a+)+$ | 'aaa!' | 可能的可能匹配 |
| 17 | ^(a+) | 'aaaa!' | 可能匹配 |
| 18 | ^(a+)+ | '!' | 可能匹配 |
| 19 | ^(a+)+$ | '!' | 可能的可能匹配 |
| 20 | ^(a+)+ | 'a!' | 可能匹配 |
| 21 | ^(a+)+$ | 'a!' | 可能的可能匹配 |
| 22 | ^(a+)+ | 'aa!' | 可能匹配 |
| 23 | ^(a+)+$ | 'aa!' | 可能的可能匹配 |
| 24 | ^(a+)+ | 'aaa!' | 可能匹配 |
| 25 | ^(a+)+$ | 'aaa!' | 可能的可能匹配 |
| 26 | ^(a+)+ | 'aaaa!' | 可能匹配 |
| 27 | ^(a+)+$ | 'aaaa!' | 可能的可能匹配 |
2. 分支回溯
/h(ello|appy) everyday/ 匹配 ("hello everyone, happy everyday")
| 步骤 | 模式位置 | 字符串位置 | 结果 |
|---|
| 1 | h | 'hello everyone, happy everyday' | 可能匹配 |
| 2 | he | 'ello everyone, happy everyday' | 可能匹配 |
| 3 | hel | 'llo everyone, happy everyday' | 可能匹配 |
| 4 | ... | ... | 可能匹配 |
| 5 | hello everyd | 'hello everyo' | 可能匹配失败 |
| 6 | h | 'ello everyone, happy everyday' | 可能匹配失败 |
| 7 | h | 'llo everyone, happy everyday' | 可能匹配失败 |
| 8 | h | 'lo everyone, happy everyday' | 可能匹配失败 |
| 9 | ... | ... | 可能匹配失败 |
| 10 | h | happy everyday | 可能匹配 |
| 11 | ha | appy everyday | 可能匹配 |
| 12 | hap | ppy everyday | 可能匹配 |
| 13 | ... | ... | 可能匹配 |
| 14 | happy everyday | happy everyday | 可能匹配 |
3. 利用先行断言和分组引用避免大量回溯
先行断言是原子性的,即意味着只有这种情况,引擎不会尝试回溯其他排列。
console.time('test');
console.log(/^([0-9]+)*$/.test('1234567890!'));
console.timeEnd('test');
console.time('test1');
console.log(/^(?=([0-9]+))\1*$/.test('1234567890!'));
console.timeEnd('test1');
总结
- 降低正则表达式的复杂度, 尽量少用分组
- 严格限制用户输入的字符串长度(特定情况下)
- 使用单元测试、regex101 或者其他测试工具保证安全
参考链接:
- 常用正则示例
- 正则表达式在线测试网站
常用校验梳理
1、校验身份证号码
export const checkCardNo = (value) => {
let reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
return reg.test(value)
}
2、校验是否包含中文
export const haveCNChars = (value) => {
return /[\u4e00-\u9fa5]/.test(value);
};
3、校验是否为中国大陆的邮政编码
export const isPostCode = (value) => {
return /^[1-9][0-9]{5}$/.test(value.toString())
}
4、校验是否为 IPv6 地址
export const isIPv6 = (str) => {
return Boolean(
str.match(/:/g)
? str.match(/:/g).length <= 7
: false && /::/.test(str)
? /^([\da-f]{1,4}(:|::)){1,6}[\da-f]{1,4}$/i.test(str)
: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i.test(str),
);
};
5、校验是否为邮箱地址
export const isEmail = (value) => {
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(value)
}
6、校验是否为中国大陆手机号
export const isTel = (value) => {
return /^1[3,4,5,6,7,8,9][0-9]{9}$/.test(value.toString())
}
7、校验是否包含 emoji 表情
export const isEmojiCharacter = (value) => {
value = String(value)
for (let i = 0
const hs = value.charCodeAt(i)
if (0xd800 <= hs && hs <= 0xdbff) {
if (value.length > 1) {
const ls = value.charCodeAt(i + 1)
const uc = (hs - 0xd800) * 0x400 + (ls - 0xdc00) + 0x10000
if (0x1d000 <= uc && uc <= 0x1f77f) {
return true
}
}
} else if (value.length > 1) {
const ls = value.charCodeAt(i + 1)
if (ls == 0x20e3) {
return true
}
} else {
if (0x2100 <= hs && hs <= 0x27ff) {
return true
} else if (0x2b05 <= hs && hs <= 0x2b07) {
return true
} else if (0x2934 <= hs && hs <= 0x2935) {
return true
} else if (0x3297 <= hs && hs <= 0x3299) {
return true
} else if (
hs == 0xa9 ||
hs == 0xae ||
hs == 0x303d ||
hs == 0x3030 ||
hs == 0x2b55 ||
hs == 0x2b1c ||
hs == 0x2b1b ||
hs == 0x2b50
) {
return true
}
}
}
return false
}