正则表达式的基本使用
一、为什么要使用正则表达式
1.1 背景:
在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。比如邮箱是否合法,表单的校验..... 正则表达式就是用于描述这些规则的工具。
1.2 定义:
正则表达式就是匹配模式,要么匹配字符,要么匹配位置。
它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。
1.3 作用
在编程语言中,正则常常用来简化文本处理的逻辑。它的应用极其广泛,主要包括以下几类:
- 校验数据的有效性
- 比如校验手机号、邮箱
- 查找符合要求的文本内容
- 比如从查找符合某规则的号码
- 对文本进行切割、替换等操作
- 比如用逗号切分字符串,将逗号替换为 & 符号
二、基本使用
2.1 基础语法
修饰符
| 修饰符 | 含义 | 描述 |
|---|---|---|
| i | ignore - 不区分大小写 | 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。 |
| g | global - 全局匹配 | 查找所有的匹配项。 |
| m | multi line - 多行匹配 | 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。 |
普通字符
方括号:
方括号用于查找某个范围内的字符:
| 表达式 | 描述 |
|---|---|
| [abc] | 查找方括号之间的任何字符。 |
| [[^abc]](www.w3school.com.cn/jsref/jsref…) | 查找任何不在方括号之间的字符。 |
| [0-9] | 查找任何从 0 至 9 的数字。 |
| [a-z] | 查找任何从小写 a 到小写 z 的字符。 |
| [A-Z] | 查找任何从大写 A 到大写 Z 的字符。 |
| [A-z] | 查找任何从大写 A 到小写 z 的字符。 |
| [adgk] | 查找给定集合内的任何字符。 |
| [^adgk] | 查找给定集合外的任何字符。 |
常用元字符:
元字符表示正则表达式功能的最小单位
| 代码 | 说明 |
|---|---|
| . | 匹配除换行符以外的任意字符 |
| \w | 匹配字母或数字或下划线或汉字 |
| \s | 匹配任意的空白符 |
| \d | 匹配数字 |
| \b | 匹配单词的开始或结束 |
| 匹配字符串的开始 | |
| $ | 匹配字符串的结束 |
常用的限定符:
限定匹配字符数量
| 代码/语法 | 说明 |
|---|---|
| * | 重复零次或更多次 等价{0,} |
| + | 重复一次或更多次 等价{1,} |
| ? | 重复零次或一次 等价{0,1} |
| {n} | 重复n次 |
| {n,} | 重复n次或更多次 |
| {n,m} | 重复n到m次 |
常用的反义代码:
| 代码/语法 | 说明 |
|---|---|
| \W | 匹配任意不是字母,数字,下划线,汉字的字符 |
| \S | 匹配任意不是空白符的字符 |
| \D | 匹配任意非数字的字符 |
| \B | 匹配不是单词开头或结束的位置 |
| [^x] | 匹配除了x以外的任意字符 |
| [^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
常用的懒惰限定表:
如果我们希望正则尽量少地匹配字符,那么就可以在表示数字的符号后面加上一个?,组成如下的形式。
| 代码/语法 | 说明 |
|---|---|
| *? | 重复任意次,但尽可能少重复 |
| +? | 重复1次或更多次,但尽可能少重复 |
| ?? | 重复0次或1次,但尽可能少重复 |
| {n,m}? | 重复n到m次,但尽可能少重复 |
| {n,}? | 重复n次以上,但尽可能少重复 |
需要转义的特殊字符:
$ ( ) * + . ? [ \ ^ {
在文本中遇到 这几种特殊字符想转为文本,需要通过反斜杠\转义:
$ ( ) * + . ? [ \ ^ {
断言
正则表达式的先行断言和后行断言一共有 4 种形式:
- (?=pattern) 零宽正向先行断言(zero-width positive lookahead assertion)
- (?!pattern) 零宽负向先行断言(zero-width negative lookahead assertion)
- (?<=pattern) 零宽正向后行断言(zero-width positive lookbehind assertion)
- (?<!pattern) 零宽负向后行断言(zero-width negative lookbehind assertion)
先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为 "零宽" 。所谓位置,是指字符串中(每行)第一个字符的左边、最后一个字符的右边以及相邻字符的中间。
(?=pattern) 正向先行断言
(?!pattern) 负向先行断言
const str = 'hi hello'
// 匹配hello的h不匹配hi的h
const reg1 = /h(?=ello)/
const reg2 = /h(?!i)/
console.log(reg1.exec(str));
// [ 'h', index: 3, input: 'hi hello', groups: undefined ]
console.log(reg2.exec(str));
// [ 'h', index: 3, input: 'hi hello', groups: undefined ]
(?<=pattern)正向后行断言
(?<!pattern)负向后行断言
// 后行断言
const str = 'get post'
// 匹配post的t不匹配get的t
const reg1 = /(?<=pos)t/
const reg2 = /(?<!ge)t/
console.log(reg1.exec(str));
// [ 't', index: 7, input: 'get post', groups: undefined ]
console.log(reg2.exec(str));
// [ 't', index: 7, input: 'get post', groups: undefined ]
2.2 案例分析
验证字符串是否是数字:
let regex = /^[0-9]+$/g; // 优化:/^\d+$/
let number = '123';
let string = 'abc';
regex.test(number); // true
regex.test(string); // false
验证是否为字母:
let regex = /^[a-zA-Z]+$/;
let number = '123';
let string = 'abc';
regex.test(number); // false
regex.test(string); // true
验证是否为手机号:
let regex = /^[a-zdA-Z]+$/;
let number = '123';
let string = 'abc';
regex.test(number); // false
regex.test(string); // true
三、匹配原理
3.1 有穷状态自动机
概念:
正则采用了 有穷状态自动机来处理负责文本 ,有穷状态自动机的具体实现就称为正则引擎。
不同的语言里的正则引擎的工作方式不同,主要有 DFA 和 NFA 两种,其中 NFA 又分为传统的 NFA 和 POSIX NFA(根据NFA引擎出的规范版本,应用较少在这里不做重点讲解)。
DFA:确定性有穷自动机(Deterministic finite automaton)
NFA:非确定性有穷自动机(Non-deterministic finite automaton)
DFA引擎 和 NFA引擎 的区别就在于:在没有编写正则表达式的前提下,是否能确定字符执行顺序!
感兴趣的大家可以自己去了解下具体的DFA,这里我们就只明确的讲下 JavaScript 中的正则引擎的工作方式。JavaScript 正则引擎的工作方式是 NFA 方式。
文本定义:
- 确定型与非确定型:假设有一个字符串(text=abc)需要匹配,在没有编写正则表达式的前提下,就直接可以确定字符匹配顺序的就是确定型,不能确定字符匹配顺序的则为非确定型。
- 有穷:有穷即表示有限的意思,这里表示有限次数内能得到结果。
- 自动机:自动机便是自动完成,在我们设置好匹配规则后由引擎自动完成,不需要人为干预!
3.2 NFA 工作机制
NFA 的工作机制:先看正则,再看字符串,以正则为主导,反复测试字符串。
const reg = /ab(c|d)/
const str = 'abd'
匹配步骤:
- 正则中第一个字符是 a,NFA 引擎在字符串中查找 a,接着匹配其后是否为 b,如果是继续查找.
- 再根据正则看字符串后面是不是 c,发现不是,此时 c 分支淘汰,文本回退。
- 接着看其他的分支,看字符串部分是不是 d,然后匹配上。
可以看出 NFA 引擎的匹配方式会造成的一个现象是,字符串中的同一部分,有可能会被反复测试很多次。NFA 引擎就是使用贪心匹配回溯算法实现,这就引出了一个概念 回溯。
3.3 回溯原理
本质上就是深度优先搜索算法。其中退到之前的某一步这一过程 ,我们称为“回溯”。
只有在正则中出现量词或者多选分支结构时,才可能会发生回溯。
分支结构
分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分支。这种尝试也可以看成一种回溯。
const reg = /^(can|candy)$/
const str = 'candy'
贪婪量词
回溯过程,后面匹配不上,会吐出已匹配的,递减的去重新匹配;
const reg = /ab{1,3}c/
const str = "abbc"
惰性量词
回溯过程,后面匹配不上,会继续递增的去匹配更长;
const reg = /^\d{1,3}?\d{1,3}$/
const str = '12345'
四、JS中相关API
有两种方法可以创建一个 RegExp 对象:一种是字面量,另一种是构造函数。
/ab+c/i; //字面量形式
new RegExp('ab+c', 'i'); // 首个参数为字符串模式的构造函数
new RegExp(/ab+c/, 'i'); // 首个参数为常规字面量的构造函数
RegExp
-
exec
- RegExp.exec()该方法用于检索字符串中的正则表达式的匹配。该函数的参数是一个字符串,该函数返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
-
test
- RegExp.test()的参数是一个字符串,返回结果为布尔值。如果传入的字符串与正则表达式匹配,返回true,反之返回false。
String
-
match
- 该方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。返回的结果与 exec 函数的结果比较类似。 调用match之后,可以通过RegExp对象上的 $N(N为1到9的数字)属性,来访问到匹配到的捕获组。
const paragraph = 'The quick brown fox jumps over the lazy dog. It barked.';
const regex = /[A-Z]/g;
const found = paragraph.match(regex);
console.log(found);
// expected output: Array ["T", "I"]
-
matchAll
- matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。
const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';
const array = [...str.matchAll(regexp)];
console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]
console.log(array[1]);
// expected output: Array ["test2", "e", "st2", "2"]
-
replace
- replace 函数用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。该方法接受两个参数,第一个参数可以是字符串或对象,第二个参数可以是字符串或函数。
const p = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';
const regex = /Dog/i;
console.log(p.replace(regex, 'ferret'));
// expected output: "The quick brown fox jumps over the lazy ferret. If the dog reacted, was it really lazy?"
-
search
- search()参数为一个正则表达式,返回值为第一个匹配的index,如果没有任何匹配,则返回-1
const paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?';
// any character that is not a word character or whitespace
const regex = /[^\w\s]/g;
console.log(paragraph.search(regex));
// expected output: 43
-
split
- split()方法的参数可以是一个正则表达式,也可以是一个字符;split()方法按照给定的正则表达式或者字符分割字符串,返回一个包含分割后的子串的数组。
var myString = "Hello 1 word. Sentence number 2.";
var splits = myString.split(/(\d)/);
console.log(splits);
// [ "Hello ", "1", " word. Sentence number ", "2", "." ]'
五、课后习题
匹配Email地址的正则表达式:/^\w+@\w+.\w+/
匹配网址URL的正则表达式:[a-zA-z]+://[^s]*
匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
匹配国内电话号码:\d{3}-\d{8}|\d{4}-\d{7}
注:匹配形式如 0511-4405222 或 021-87888822
匹配腾讯QQ号:[1-9][0-9]{4,}
注:腾讯QQ号从10000开始
匹配身份证 /\d{15}|\d{18}/
注:中国的身份证为15位或18位
匹配ip地址:/((2[0-4]\d|25[0-5]|[01]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/
驼峰——>下划线
const reg = /[A-Z]/g
const str1 = 'lastLoginIp'
const str2 = 'lastLoginTime'
const replace = (reg, str) => {
return str.replace(reg, function (matchStr) {
return "_" + matchStr.toLowerCase();
})
}
console.log(replace(reg, str1));
下划线——>驼峰
const reg = /_([a-z])/g
const str = 'last_login_ip'
console.log(str.replace(reg,($0,$1)=>{
return $1.toUpperCase()
}));
时间格式校验
let reg = /^\d{4}-(0[1-9]|1[12])-(0[1-9]|[12]\d|3[01])/
let str = '2000-12-31'
console.log(reg.test(str));