正则表达式小记

349 阅读5分钟

语法篇

元字符

( [ { \ ^ $ | ) ? * + . ] }

匹配这些字符的时候建议都加\进行转义

匹配模式 i g m u s

  1. i 表示对大小写不敏感
  2. g 查找所有匹配的字符,而不是找到第一个就不找了
  3. m 多行匹配,会改变 ^和 $ 的行为
  4. u 可以匹配4字节的unicode编码
  5. s dotAll 模式, .可以匹配换行符
console.log(/^\uD83D/.test('\uD83D\uDC2A'))     //true
console.log(/^\uD83D/u.test('\uD83D\uDC2A'))    //false

默认情况下, . 可 以匹配任意字符,除了换行符,且 . 不能匹配 unicode字符,需要使用 u 选项启用unicode模式才可以。

/foo.bar/.test('foo\nbar')    // false

/foo.bar/s.test('foo\nbar')   // 开启dotAll 模式 . 就可以匹配换行符了

类 [] 查找某个范围内的字符

  1. [abc] 查找方括号之间的任何字符
  2. [0-9] 查找任何从0 至 9 的数字

只要匹配到一次就算匹配到了。

/[abc]/.test('fdsfda')     // true

/[abc]/.test('a432423')    // true

/[abc]/.test('a')          // true 

/[0-9]/.test('9-fdsfa')      // true

/[0-9]/.test('2ewrewr')      // true

还有一些预定义类方便直接使用

预定义类等价描述
\s[\t\n\x0B\f\r]空格
\S[^\t\n\x0B\f\r]非空格
\d[0-9]数字
\D[^0-9]非数字
\w[a-zA-Z_0-9]单词字符 ( 字母、数字、下划线)
\W[^a-zA-Z_0-9]非单词字符
.[^\r\n]任意字符,除了回车与换行外所有字符
\f\x0c \cL匹配一个换页符
\n\x0a \cJ匹配一个换行符
\r\x0d \cM匹配一个回车符
\t\x09 \cI匹配一个制表符
\v\x0b \cK匹配一个垂直制表符
\xxx查找以八进制数 xxx 规定的字符
\xdd查找以十六进制数 dd 规定的字符
\uxxxx查找以十六进制数 xxxx 规定的 Unicode 字符
.匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括'\n'在内的任何字符

量词 + ? * {n,m}

量词等价描述
n *{0,}匹配零个或多个n
n ?{0,1}可选,匹配0次或一次
n +{1,}匹配至少一个 n 的字符串
{n}匹配n次
{n,m}匹配n到m次
{n,}至少匹配n次

边界 ^ $ \b \B

  1. /^xx/ 匹配 以xx开始, 在类 [] 中表示排除非的意思
/^xx/.test('xx')    // true

/^xx/.test('x')     // false
  1. $ 表示以 xx结束
/aaaxx$/.test('aaaxx')    // true

console.log(/aaaxx$/.test('xx'))    // false
  1. \b 表示单词边界 也就是指的单词和空格间的位置
/er\b/.test('never')   // true 可以匹配 never 中的 er

/er\b/.test('er sdfas')  // true 

/er\b/.test('erb')     // false 不能匹配
  1. \B 表示 匹配非单词边界
/er\B/.test('era')    // true

/er\B/.test('era fadsfa')   // true

/er\B/.test('er fadsfa')    // false

分组 ()

分组使用(),提取相匹配的字符串,使量词作用于分组。

{}

/hehe{3}/.test('heheee')     // true 是把e匹配三次,而不是整个单词

/hehe{3}/.test('heheeeeee')  // true 

()

/(hehe){3}/.test('hehehehehehe')   // true 相同的视为一个组,然后量词再作用于他


或 |

分组中可以使用 | 表示或的意思

/T(oo|ii)m/.test('Toom')  // true

/T(oo|ii)m/.test('Tiim')  // true

反向引用

使用 () 之后可以使用 $1 - $9 等来匹配 ​

'2018-02-11'.replace(/(\d{4})\-(\d{2})\-(\d{2})/g, '$1/$2/$3')  // "2018/02/11"

'2018-02-11'.replace(/(\d{4})\-(\d{2})\-(\d{2})/g, '$2/$3/$1')  // "02/11/2018"

后向引用

\n 表示后项引用, \1 是指在正则表达式中,从左往右第一个 () 中的内容;以此类推 , \2 代表第二个 () , \0 表示整个表达式。 ​

匹配多个日期格式,表达式中的 \1 代表重复 引用第一个()  既 (\-|\/|.)   

貌似重复的必须是“确定”的才行。

/\d{4}(\-|\/|.)\d{1,2}\1\d{1,2}/.test('2016-03-26')   // true

/\d{4}(\-|\/|.)\d{1,2}\1\d{1,2}/.test('2016/03/26')   // true

/\d{4}(\-|\/|.)\d{1,2}\1\d{1,2}/.test('2016.03.26')   // true
        

分组命名 (?)

ES2018 之前的分组是通过数字命名的

let res = /(\d{4})\-(\d{2})\-(\d{2})/.exec('2021-10-30')

console.log(res[0]) // 匹配到的所有字符串  2021-10-30

console.log(res[1]) // 2021

console.log(res[2]) // 10 

console.log(res[3]) // 30

现在可以通过指定分组名称,增加代码的可读性,以便于维护

let res = /(?<year>\d{4})\-(?<month>\d{2})\-(?<day>\d{2})/.exec('2018-10-25')

console.log(res)  
[
    "2018-10-25",
    "2018",
    "10",
    "25"
]

console.log(res.groups.year)  // 2018

console.log(res.groups.month) // 10 

console.log(res.groups.day)   // 25

还可以结合之前的 replace 方法

let reg = /(?<year>\d{4})\-(?<month>\d{2})\-(?<day>\d{2})/

let str = '2021-10-30'

let res = str.replace(reg, '$<month>/$<day>/$<year>')

console.log(res)    // 10/30/2021

忽略分组 (?:)

就是在匹配的结果里面不要这个分组

(?:tom).(ok)   // 这里的$1  就匹配的是ok 

前瞻 (?=) (?!) 非捕获组

正向前瞻 (?=) 后面要有

强调的是这个位置右边的有要求,但是右边的东西不会放到结果里面去 比如 /read(?=img)/ 去匹配 "reading", 结果是 "read" ,"ing"是不会到结果里面去的。

console.log('1a2bc*456v8'.replace(/\w(?=\d)/g, '-'))  

// 1-2bc*--6-8   

// 表示匹配 后面是数字的单词字符

1后面是a 不匹配     // 1a2bc*456v8
a后面是1 匹配       // 1-2bc*456v8
2后面是b 不匹配     // 1-2bc*456v8


console.log(/\d+(?= dollars)/u.exec('42 dollars'))

// 匹配字符串中紧跟着dollars的数字

[
    "42"    // 只匹配到42   
]

后面不要有xx

console.log('1a2bc*456v8'.replace(/\w(?!\d)/g, '-'))

//-a---*45-v-

// 表示匹配后面不是数字的单词字符 

1后面不是数字
b后面不是数字
c后面不是数字
*后面是数字    

同理
console.log(/\d+(?! dollars)/u.exec('77 pesos'))

["77"]  负向前瞻  匹配字符串中紧跟着的不是dollars的数字

eg、长度4-8个字符之间,并且必须包含一个数字

/^(?=.*\d).{4,8}$/

后顾 (?<=) (?<!)

(?<=) 前面要有xx

console.log(/(?<=\$)\d+/u.exec('333$66'))

//["66"]   匹配数字前面是$的字符串


(?<!) 前面不能有

console.log(/(?<!\$)\d+/u.exec('333$66'))

// ['333']   匹配数字前面不能是$的字符串

贪婪模式 与 非贪婪模式 ?

正则表达式在匹配的时候默认会尽可能多的匹配, 叫做贪婪模式。通过在限定符后面加 ? 可以进行非贪婪模式匹配 。

例如 \d{3,6} 会默认匹配 6 个数字 而不是三个 在量词 {} 后面添加一个 ? 就可以修改成非贪婪模式, 进而少的匹配 3 次。

console.log('12345678'.replace(/\d{3,6}/, '-'))

// -78   一次匹配6个字符串,替换成-

console.log('12345678'.replace(/\d{3,6}?/, '-'))

// -45678 // 尽可能少的匹配 3 次 , 值替换了 123



console.log('abbbb'.replace(/ab+/, '-'))   // - 全部匹配了


console.log('abbbb'.replace(/ab+?/, '-'))  // -bbb 只匹配了一次 

优先级

  1. 转义 \
  2. 括号 [] 、(?:) 、(?=) 、[]
  3. 字符和位置
  4. 或 |

RegExp 实例上的属性

  1. lastIndex 当前表达式匹配内容的最后一个字符串的下一个位置 序号从 0 开始计算
const reg = /\w/g

reg.test('a')  // true

reg.test('a')  // false

方法

test(str)

检测字符串参数中是否存在正则表达式匹配的字符串 , 如果用 g 修饰那么 lastIndex 会改变

exec(str)

使用正则表达式对字符串执行搜索,并将更新全局RegExp对象的属性以反映匹配结果 如果匹配失败,exec() 方法返回 null 如果匹配成功,exec() 方法返回一个数组,并更新正则表达式对象的属性 ​

  • 数组索引0:匹配的全部字符串
  • 数组索引1,2..n:括号中的分组捕获
  • index:属性是匹配文本的第一个字符的位置
  • input:存放被检索的字符串

要注意的是:

  1. exec()永远只返回一个匹配项(指匹配整个正则的)

  2. 如果设置了g修饰符,每次调用exec()会在字符串中继续查找新匹配项,不设置g修饰符,对一个字符串每次调用exec()永远只返回第一个匹配项。所以如果要匹配一个字符串中的所有需要匹配的地方,那么可以设置g修饰符,然后通过循环不断调用exec方法

const str = "Reading and Writing";

const pattern = /\b([a-zA-Z]+)ing\b/g;

let matches;

// 直到 匹配不上的时候返回null
while ((matches = pattern.exec(str))) {
  console.log(matches.index + " " + matches[0] + " " + matches[1]);
}
console.log(matches)


// 0 Reading Read
// 12 Writing Writ

// null

search(reg)

search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串

方法返回第一个匹配结果的index,查找不到返回-1

search() 方法不执行全局匹配,它将忽略修饰符g,并且总是从字符串的开始进行检索

split(reg)

split() 方法一般用来分割字符串成数组,也可以传入正则表达式,使用正则可以避免一些传入字符串解决不了的问题

'143134jhg432g2g324g232g'.split(/\d/)   // 用数字分割

// ["", "", "", "", "", "", "jhg", "", "", "g", "g", "", "", "g", "", "", "g"]


'a b    c'.split(' ')   

//  ["a", "b", "", "", "", "c"]    无法识别连续的字符串

'a b    c'.split(/\s*/)     

// ["a", "b", "c"]     // ko 

match(reg)

match() 方法将检索字符串,以找到一个或多个与reg相匹配的文本,reg是否有修饰符 g影响很大 返回值与 RegExp.prototype.exec的返回类似,不过只返回匹配的字符串数组

'cdbbdbsdbdbzddzdbbbd'.match(/d(b+)d/g)   

//  ["dbbd", "dbd", "dbbbd"]

如果修饰符有 g则匹配出所有匹配的数组,如果不是,则出第一个匹配的字符串,以及相应的捕获内容

'cdbbdbsdbdbzddzdbbbd'.match(/d(b+)d/) 

[
    "dbbd",
    "bb"
]

replace()

找到匹配并替换,传入string、number比较常见,这里传入回调function是比较高级的用法,这里可以参考MDN 比如一个场景,把手机号的中间4位换成*

function validateMobile(str) {
  return /^[1][0-9]{10}$/.test(str) &&
    str.replace(
    /(\d{3})(\d{4})(\d{4})/,
    (rs, $1, $2, $3) => `${$1}****${$3}`
  );
}

console.log(validateMobile('15625468976'))

// 156****8976
let str = "asss23sjdssskssa7lsssdkjsssdss";
const arr = str.split(/\s*/);
// 把字符串转换为数组
const str2 = arr.sort().join("");
// 首先进行排序,这样结果会把相同的字符放在一起,然后再转换为字符串
let value = "";
let index = 0;

str2.replace(/(\w)\1*/g, function ($0, $1) {
  //匹配字符
  if (index < $0.length) {
    index = $0.length;
    // index是出现次数
    value = $1;
    // value是对应字符
  }
});

console.log(`最多的字符: ${value} ,重复的次数: ${index}`);
let str = "asss23sjdssskssa7lsssdkjsssdss";
const arr = str.split(/\s*/);
let obj = {}
arr.forEach(item => {
  if(obj[item] === undefined) {
    obj[item] = 1
  }else {
    obj[item] = obj[item]+ 1
  }
})
let values = Object.values(obj)
let keys = Object.keys(obj)

let max = Math.max(...values)
let valueIndex = values.indexOf(max)
console.log(`最多的字符: ${keys[valueIndex]} ,重复的次数: ${max}`);

MDN replace

案例

1、贪婪模式 与非贪婪模式 取第一个span中的文本

/.+/   // 贪婪模式

/.+?/  // 非贪婪模式 
假设现在有如下的字符串,需要取字符串中第一个span的文本text1
let str = '<p><span>text1</span><span>text2</span></p>'

// 于是我们写下了 以下的正则

console.log(str.match(/<span>(.+)<\/span>/))

// 我们得到了如下的结果
[
    "<span>text1</span><span>text2</span>",
    "text1</span><span>text2"
]

// 却发现匹配到的竟然是 text1</span><span>text2
// 这是因为 我们括号中写的是 `(.+)` .为匹配任意字符, +则表示匹配一次以上。
// 当规则匹配到了`text1`的时候,还会继续查找下一个,发现`<`也命中了`.`这个规则
// 于是就持续的往后找,知道找到最后一个span,结束本次匹配。


当我们修改正则之后
console.log(str.match(/<span>(.+?)<\/span>/))

[
    "<span>text1</span>",
    "text1"
]

// 这次就能匹配到我们想要的结果了
// `?`的作为是,匹配`0~1`次规则
// 但是如果跟在`*`、`+`之类的表示数量的特殊字符后,含义就会变为匹配尽量少的字符。
// 当正则匹配到了text1后,判断后边的</span>命中了规则,就直接返回结果,不会往后继续匹配。


简单的记忆

贪婪模式:尽可能的多拿
非贪婪模式:尽可能的少拿