正则表达式很简单,但要多用方可掌握。
常用字符
正则中很多需要强记的字符,这里列出常用的字符和其表达的含义:
常用匹配字符 | 含义 |
---|---|
[0-9] | 匹配单个数字0-9 |
[a-z] | 匹配单个小写字母 |
[A-Z] | 匹配单个大写字母 |
\s | 匹配所有空白字符,如空格、换行等 |
\n | 匹配所有换行符 |
\b | 匹配单词之间的边界(匹配连续的字母、数字的两边) |
\w | 匹配任意字母、数字、下划线 |
\d | 等同于[0-9] |
注:\S \B \W \D
分别是对应 \s \b \w \d
的反义。
特殊字符 | 含义 | 用法 |
---|---|---|
^ | 1. 匹配输入字符串的开始位置 2. 用在[]中时表示 非 | 1. /^http/ 匹配以http开头的字符串 2. /[^a-zA-Z]/ 匹配非字母 |
$ | 匹配输入字符串的结尾位置 | /.com$/ 匹配以.com结尾的字符串 |
| | 二选一,表示 或 | /a|b/ 匹配a或者b |
. | 小数点匹配换行符\n之外的任何单个字符 | /./ 匹配换行符之外的其他字符 |
[] | 中括号匹配一个字符 | /[aeiou]/ 匹配字母 aeiou 中的一个 |
() | 小括号表示一个子表达式分组 | 匹配的子表达式可以用于以后使用 |
{} | 大括号表示限定一个表达式多少次 | {n} 匹配n次; {n,} 匹配最少n次; {n, m} 匹配n-m次 |
+ | 匹配前面的子表达式一次或多次 | /[0-9]+/ 匹配一个数字或多个数字 |
* | 匹配前面的子表达式零次或多次 | /[0-9]*/ 匹配0次数字或多个数字 |
? | 1. 匹配前面的子表达式零次或一次 2. 指明一个非贪婪限定符 | 1. /[0-9]?/ 2. /<.*?>/ 匹配一个标签如<p> |
匹配特殊字符本身时需要转义,共有以下几个:
* . ? + $ ^ [ ] ( ) { } | \ /
- 其中
/
在字面量中需要转义,在构造函数中不需要,如下匹配一个斜杠/
。
const reg = /\//
const reg = new RegExp('/')
- 在字面量中带一个转义符
\
的用构造函数写要带两个转义符\\
,如下匹配一个字符串.
。
const reg = /\./
const reg = new RegExp('\\.')
- 在中括号
[]
内需要转义的特殊字符-
,^
,\
,[
,]
/[a\-z]/.test('-') // true,匹配字符串 -
/\^/.test('^') // true,^放在开头需要加转义符
/a^b/.test('^') // true,^ 放在中间不需要加转义符
/\\/.test('\\') // true,转义符本身需要转义,而且在字符串中也需要转义
常用方法
js 中的正则表达式分为字面量和构造函数两种:
// 字面量
const reg = /[0-9a-z]/g
// 构造函数
const reg = new RegExp('[0-9a-z]', 'g')
其中字面量中不能包含变量,构造函数中可以使用变量:
const name = '幻灵尔依'
const reg = new RegExp(`我的名字叫${name}`)
经常会用 reg.test(str)
方法来判断字符串中是否匹配到了正则表达式:
const reg = /[0-9]/
const str = '文本中有没有数字1234等'
if (reg.test(str)) {
...
}
也经常用str.replace(reg, '')
方法来替换字符串中的内容:
const reg = /[0-9]/g
const str = '文本中的数字1234全部替换成x'
const newStr = str.replace(reg, 'x')
也会用到 str.match(reg)
方法来获取匹配到的内容(也可以用reg.exec(str)
):
const reg = /[0-9]+[.][0-9]+[.][0-9]+/g
const str = '这里有个表名字叫做 11.11.11'
str.match(reg) // ['11.11.11']
- match 中的正则表达式如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
- 如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。 在这种情况下,返回的项目将具有如下所述的其他属性。
老子曰:下面才是精华
贪婪&非贪婪
* 和 + 限定符都是贪婪的,它们会尽可能多的匹配文字。在它们的后面加上一个 ? 就可以实现非贪婪或最小匹配。
- 贪婪(默认都是贪婪的)
const str = '<h1>正则表达式</h1>'
const reg = /<.*>/
str.match(reg) // ['<h1>正则表达式</h1>']
- 非贪婪
const str = '<h1>正则表达式</h1>'
const reg = /<.*?>/
str.match(reg) // ['<h1>']
捕获分组和回溯引用
小括号 ()
匹配到的子表达式会被缓存为一个个组,方便后面对其引用。假设要获取html中的 h1 标签:
- 在正则表达式中使用
\n
可以引用第n个捕获组
const str = '<p>正则表达式</p><h1>正则表达式</h1><h2>正则表达式</h2>'
const reg = /<(h1)>.+?<\/\1>/
str.match(reg) // ['<h1>正则表达式</h1>']
- 在正则表达式外使用
$n
引用第n个捕获组(RegExp.$n)
const str = 'abc'
const reg = /(abc)/
reg.test(str)
RegExp.$1 // 'abc'
str.replace(reg, '$1$1') // 'abcabc'
非捕获分组和限定查找
因为捕获组 ()
会将每个捕获到的结果缓存下来以便引用,所以会造成内存使用增加。如果只是想用分组的原始功能,而不需要缓存,则可以使用非捕获分组 (?:)
const str = 'abc'
const reg = /(?:abc)/
reg.test(str)
RegExp.$1 // ''
非捕获分组还有 (?=)、(?<=)、(?!)、(?<!)
,他们比 (?:)
多了限定作用,即只匹配而不会被输出。
前向查找
前向查找是用来限制后缀的。
(?=)
: 即查找符合限定条件(?=)
的前面的匹配项(输出内容不包括(?=)
中的匹配项)
const str = 'a.png b.jpg c.gif d.svg'
// 查找所有 边界开头的、 .svg 前面的 小写字母。
const reg = /\b[a-z](?=.svg)/g
str.match(reg) // ['d']
(?!)
: 即查找 不符合 限定条件(?!)
的前面的匹配项(输出内容不包括(?!)
中的匹配项)
const str = 'a.png b.jpg c.gif d.svg'
// 查找所有边界开头的、 非.svg 前面的、 `.[a-z]{3}` 前面的 小写字母。
const reg = /\b[a-z](?!.svg)(?=\.[a-z]{3})/g
str.match(reg) // ['a', 'b', 'c']
后向查找
后向查找是用来限制前缀的。
- 查找符合限定条件
(?<=)
的后面的匹配项(输出内容不包括(?<=)
中的匹配项)
const str = '1. 1111; 2. 2222; 3. 3333; 4. 4444。'
// 查找所有 序号 后面的项。
const reg = /(?<=\b[0-9]+\.\s).+?[;。]/g
str.match(reg) // ["1111;", "2222;", "3333;", "4444。"]
- 查找 不符合 限定条件
(?<!)
的后面的匹配项(输出内容不包括(?<!)
中的匹配项)
const str = 'a.png b.jpg c.gif d.svg'
// 查找前缀不为 a b c 的后面的项
const reg = /\b(?<![abc]\.)[a-z]{3}/g
str.match(reg) // ['svg']
来个能吃的栗子
一般稍微复杂的正则都是多种规则同时使用的,下面来几个例子吧:
前向查找和后向查找齐用:
假设要获取 <img crossorigin src="https://abcdefg.com" data-img-url="https://test.com">
中的 data-img-url
属性中的链接。可以确定的是链接左边一定是 data-img-url="
,右边一定是紧贴着 "
(非贪婪)。
const str = '<img crossorigin src="https://abcdefg.com" data-img-url="https://test.com">'
const dataImgUrl = 'data-img-url'
const reg = new RegExp(`(?<=${dataImgUrl}=").+?(?=")`, 'g')
str.match(reg) // ['https://test.com']
回溯引用和非贪婪并用
假如我要获取一段html中的文本,但是我又不想要加了 not-show-in-text
标记的标签中的文本,可以这样:
const notShowInText = 'not-show-in-text'
const html = `
<p>test1</p>
<p ${notShowInText} style="text-align: center;">
<b>表 1.4.4 测试表格</b>
</p>
<p>test2</p>
`
const reg = new RegExp(`<([a-z][a-z1-6]*?)[^>]+${notShowInText}[\\s\\S]+?</\\1>`, 'g')
const text = html.replace(reg, '').replace(/<[^>]+>/g, '')
其中最关键的是要匹配到 not-show-in-text
所在的整个标签。([a-z][a-z1-6]*?)
匹配了一个非贪婪的标签名,[^>]
保证了 <
到 >
是一个半个完整的标签,</\\1>
匹配一个闭合的标签, [\\s\\S]+?
匹配了标签见可能出现的任意元素且是非贪婪的。
replace第二个参数可以是回调函数
比如,想把 yyyy-mm-dd
格式,替换成 mm/dd/yyyy
怎么做?
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); // "06/12/2017"
其中 replace 第二个参数里用$1、$2、$3
指代相应的分组。等价于如下的形式:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); // "06/12/2017"
也等价于:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match, year, month, day) {
return month + "/" + day + "/" + year;
});
console.log(result); // "06/12/2017"
鄙人公司的一道面试题
实现千分位转换:12345678 转换后 12.345.678
刚开始遇到这个问题的时候,我没想到用正则一行代码就能实现,而是先转数组然后reverse,然后三个三个的分割,最后再reverse后转为字符串。这样虽然也能实现,但是代码冗余了很多。
后来无意中看到公司的面试题里面有这道题,而且答案贼简单。无比汗颜-_-||
-
12345678..toLocaleString('de-DE')
// 德国本地化以‘ . ’分割,正常是‘ , ’分割 -
'12345678'.replace(/\B(?=(\d{3})+(?!\d))/g, '.')
拆解:
-
\b匹配单词边界,\B 匹配非单词边界,数字之间就属于非单词边界。'1234'.replace(/\B/g, '.')结果是'1.2.3.4'
-
(?=) 前向查找,括号中为限制条件
-
(\d{3})+ 匹配三个连接的字符一次或多次
-
(?!\d) 限制 (\d{3})+ 的后面不能为数字 如本例中匹配的 678 和 345678 后面一定是非数字
第四步可以换成 \b 或者 $ 等,限制结尾, ,即 '12345678'.replace(/\B(?=(\d{3})+\b)/g, '.')
'12345678'.replace(/(\d)(?=(\d{3})+\b)/g, '$1.')
这个方法是匹配三个连接字符的前一个数字,并替换为在其后面加个 ' . '