JS中的正则表达式及应用

243 阅读6分钟

正则表达式简介

正则表达式(regular expression)是一种非常高效的处理字符串的方式,几乎每种编程语言都实现了正则表达式,JavaScript也不例外。在js中,我们可以通过特定的语法声明一个正则表达式,来实现:

  • 匹配搜索文本中的字符串
  • 替换文本中匹配的字符串
  • 从字符串中提取信息
  • ...

下面我们就看看如何使用正则表达式。

如何定义一个正则表达式

在 JavaScript 中,正则表达式是一个对象,我们可以通过两种方式来定义一个它:

  1. 使用RegExp构造函数实例化新的 RegExp 对象:
const regExp = new RegExp('hello')
  1. 使用正则表达式文字形式来定义正则表达式:
const regExp = /hello/

如何使用正则表达式

我们上面定义了 regExp 的正则表达式是一个非常简单的表达式,可以在一段文字中去搜索匹配是否包含hello,没有任何限制条件,无论hello出现在这段文字中的哪里,它都会满足正则表达式的规则。那如何知道一段文字是否匹配正则表达式的规则呢,很简单,我们可以使用 RegExp.test(String) 方法来测试,这个方法会返回一个布尔值:

regExp.test('hello')                     //✅
regExp.test('123  blablabla 456 hello blablabla') //✅

regExp.test('he')        //❌
regExp.test('blablabla') //❌

在上面的示例中,我们检查了“hello”是否满足在 regExp 中定义的正则表达式规则。

这是最简单的方法,现在我们已经初步知道正则表达式的概念和应用了。

^$

/hello/表达式用来匹配hello,无论它在字符串中的哪个位置。但是如果要匹配以 hello 开头的字符串,请使用 ^ 运算符(必须要在正则表达式最前面使用):

/^hello/.test('hello bla')     //✅
/^hello/.test('bla hello') //❌

如果要匹配以 hello 结尾的字符串,请使用 $ 运算符(必须要在正则表达式最后面使用):

/hello$/.test('hello')     //✅
/hello$/.test('bla hello') //✅
/hello$/.test('hello bla') //❌

如果我们同时使用这两个符号,那这个正则表达式只能完全匹配的字符串 hello

/^hello$/.test('hello') //✅
/^hello$/.test('bla hello') //❌
/^hello$/.test('hello bla') //❌

要匹配以某个子字符串开头并以另一个子字符串结尾的字符串,可以使用 .*,它可以重复匹配 0 次或n次的任何字符:

/^hello.*world$/.test('hello world')             //✅
/^hello.*world$/.test('helloworld')              //✅
/^hello.*world$/.test('hello bla bla $$ world')  //✅
/^hello.*world$/.test('hello world!')            //❌

匹配范围

我们可以通过[]来选择匹配范围内的任何字符,而不是匹配特定字符串,例如:

/[a-z]/ //匹配所有的小写字母 a, b, c, ... , x, y, z
/[A-Z]/ //匹配所有的大写字母 A, B, C, ... , X, Y, Z
/[a-c]/ //匹配字母 a, b, c
/[0-9]/ //匹配数字 0, 1, 2, 3, ... , 8, 9

这些正则表达式匹配包含以下范围内至少一个字符的字符串:

/[a-z]/.test('a')  //✅
/[a-z]/.test('1')  //❌
/[a-z]/.test('A')  //❌

/[a-c]/.test('d')  //❌
/[a-c]/.test('dc') //✅
/[a-c]/.test('abcd') //✅

同样的,我们可以组合不同的匹配范围,如下:

/[A-Za-z0-9]/ //匹配所有的大写字母、小写字母及数字
/[A-Za-z0-9]/.test('a') //✅
/[A-Za-z0-9]/.test('1') //✅
/[A-Za-z0-9]/.test('A') //✅

我们可以检查字符串是否包含一个字符,并且只有一个字符,方法是正则表达式以 ^ 开头,并以 $ 字符结尾:

/^[A-Z]$/.test('A')  //✅
/^[A-Z]$/.test('AB') //❌
/^[A-Z]$/.test('Ab') //❌
/^[A-Za-z0-9]$/.test('1')  //✅
/^[A-Za-z0-9]$/.test('A1') //❌

否定模式

正则表达式开头的 ^ 字符表示字符串以某个字符开头来匹配,但是如果 ^[]中使用时,代表着排除范围内的字符:

/[^A-Za-z0-9]/ 表示匹配大写字母、小写字母及数字以外的字符
/[^A-Za-z0-9]/.test('a') //❌
/[^A-Za-z0-9]/.test('1') //❌
/[^A-Za-z0-9]/.test('A') //❌
/[^A-Za-z0-9]/.test('@') //✅

元字符

  • \d 匹配任何数字,相当于 [0-9]
  • \D 匹配任何不是数字的字符,相当于 [^0-9]
  • \w 匹配任何字母数字字符(加下划线),相当于 [A-Za-z_0-9]
  • \W 匹配任何非字母数字字符,相当于 [^A-Za-z_0-9]
  • \s 匹配任何空格字符:空格、制表符、换行符和 Unicode 空格
  • \S 匹配任何非空格字符
  • \0 匹配项 null
  • \n 匹配换行符
  • \t 匹配制表符
  • \uXXXX 匹配 XXXX 编码的 unicode 字符(需要 u 标志)
  • \. 匹配任何非换行符的字符(例如 \n)(除非使用 s 标志)
  • [^] 匹配任何字符,包括换行符。它在多行字符串上很有用

| 运算符

如果要匹配一个字符串或另一个字符串,我们可以使用 | 符号:

/hello|hey/.test('hello') //✅
/hello|hey/.test('hey')  //✅

量词

下面的正则表达式,表示匹配某个字符是否是数字,而不是其它字符:

/^\d$/

我们可以使用 ? 符号来表示该字符出现0次或1次:

/^\d?$/
/^\d?$/.test('1') //✅
/^\d?$/.test('')  //✅
/^\d?$/.test('12') //❌

但是,如果我们需要匹配多次的数字字符怎么办?这时我们就可以通过其它方式来实现,如 +*{n}{n,m}

+

匹配1次或一次以上 (>=1) 的匹配项

/^\d+$/ //匹配以数字开头,数字结尾,数字出现1次或1次以上的字符串

/^\d+$/.test('12')     //✅
/^\d+$/.test('14')     //✅
/^\d+$/.test('144343') //✅
/^\d+$/.test('')       //❌
/^\d+$/.test('1a')     //❌

*

匹配0次或0次以上 (>=0) 的匹配项

/^\d*$/ //匹配以数字开头,数字结尾,数字出现0次或0次以上的字符串

/^\d*$/.test('12')     //✅
/^\d*$/.test('14')     //✅
/^\d*$/.test('144343') //✅
/^\d*$/.test('')       //✅
/^\d*$/.test('1a')     //❌

{n}

匹配出现 n 次的匹配项

/^\d{3}$/ // 匹配以数字开头,数字结尾,出现3次数字的字符串

/^\d{3}$/.test('123')  //✅
/^\d{3}$/.test('12')   //❌
/^\d{3}$/.test('1234') //❌

/^[A-Za-z0-9]{3}$/.test('Abc') //✅  // 匹配以大写字母、小写字母或数字开头和结尾,出现3次大写字母、小写字母或数字的字符串

{n,m}

匹配出现 n ~ m 次(>=n 并且 <=m )的匹配项

/^\d{3,5}$/

/^\d{3,5}$/.test('123')    //✅
/^\d{3,5}$/.test('1234')   //✅
/^\d{3,5}$/.test('12345')  //✅
/^\d{3,5}$/.test('123456') //❌

m 可以被省略,那就表示匹配出现n次及n次以上(>=n)的匹配项:

/^\d{3,}$/

/^\d{3,}$/.test('12')        //❌
/^\d{3,}$/.test('123')       //✅
/^\d{3,}$/.test('12345')     //✅
/^\d{3,}$/.test('123456789') //✅

?

? 表示匹配出现0次或1次的匹配性,相当于{0,1}:

/^\d{3}\w?$/

/^\d{3}\w?$/.test('123')   //✅
/^\d{3}\w?$/.test('123a')  //✅
/^\d{3}\w?$/.test('123ab') //❌

分组

我们可以使用小括号来创建分组 (......),下面的例子表示匹配以三个数字开头,后面跟着一个或一个以上字母、数字字符,一直到结尾:

/^(\d{3})(\w+)$/

/^(\d{3})(\w+)$/.test('123')          //❌
/^(\d{3})(\w+)$/.test('123s')         //✅
/^(\d{3})(\w+)$/.test('123s#')        //❌
/^(\d{3})(\w+)$/.test('123something') //✅
/^(\d{3})(\w+)$/.test('1234')         //✅

放在右括号后面的+符号,表示这一组匹配出现1次或1次以上:

/^(\d{2})+$/

/^(\d{2})+$/.test('12')   //✅
/^(\d{2})+$/.test('123')  //❌
/^(\d{2})+$/.test('1234') //✅

提取匹配的字符串

上面我们已经了解了如何检测字符串并看它们是否匹配正则表达式的规则。正则表达式还提供了一个非常强大的功能,能够在字符串中搜索匹配的子串,并返回一个包含匹配结果的数组。它是基于正则表达式进行模式匹配的,并且可以非常灵活地进行字符串搜索与提取。

现在我们不使用 RegExp.test(String) 来进行正则表达式匹配,因为它就返回了一个布尔值,我们用下面的两个方法:

  • String.match(RegExp)
  • RegExp.exec(String)

这两种方式结果完全相同,都是返回一个 Array,是一个数组,其中第一个元素是匹配到的整个子串,后续元素是每个括号内的匹配结果。如果没有匹配的,则返回 null:

'123s'.match(/^(\d{3})(\w+)$/)  //Array [ "123s", "123", "s" ]

/^(\d{3})(\w+)$/.exec('123s')   //Array [ "123s", "123", "s" ]

'hey'.match(/(hey|ho)/)   //Array [ "hey", "hey" ]

/(hey|ho)/.exec('hey')    //Array [ "hey", "hey" ]

/(hey|ho)/.exec('ha!')    //null

当一个组匹配到多次时,只有最后一个匹配项被放入到结果数组中:

'123456789'.match(/(\d)+/)
//Array [ "123456789", "9" ]

可选分组

如果分组后面加了?,比如(...)?,那么如果该分组未找到匹配项,则返回的数组中将包含 undefined

/^(\d{3})(\s)?(\w+)$/.exec('123 s') //Array [ "123 s", "123", " ", "s" ]
/^(\d{3})(\s)?(\w+)$/.exec('123s') //Array [ "123s", "123", undefined, "s" ]

分组引用

每个匹配的组都会分配一个编号。“1”是指第一个,“1”是指第一个,“2”是指第二个,依此类推。后面我们讨论替换字符串的某些部分时,这将会很有用。

命名分组

这是一个新特性,可以将组分配给一个名称,而不仅仅是在返回结果数组中分配一个插槽:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const result = re.exec('2015-01-02')

// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

image.png

使用不带组的 match 和 exec

使用不带组的"match"和"exec"的不同之处在于:数组中的第一项不是整个匹配的字符串,而是直接匹配的字符串:

/hello|hey/.exec('hello')   // [ "hello" ]

/(hello).(ho)/.exec('hello ho') // [ "hello ho", "hello", "ho" ]

忽略组

正常情况下,每个组都会被正则表达式识别到并返回,但是某些情况下我们可能想在返回数组中忽略这个组的结果,这个时候我们我们可能需要这样去写(?:...)

'123s'.match(/^(\d{3})(?:\s)(\w+)$/)    //null
'123 s'.match(/^(\d{3})(?:\s)(\w+)$/)   //Array [ "123 s", "123", "s" ]

标志符号

我们可以在正则表达式中使用下面的标志符号:

  • g: 多次匹配模式
  • i: 使用正则表达式不区分大小写
  • m: 启用多行模式。在此模式下,^$匹配整个字符串的开头和结尾。如果没有这个,使用多行字符串,它们会匹配每行的开头和结尾。
  • u: 启用对 Unicode 的支持(在 ES6/ES2015 中引入)
  • s: 新增的功能,单行匹配,single line的缩写,它会导致.也匹配换行符

标志可以组合使用,它们会在正则表达式字符串的末尾以文字的形式添加:

/hello/ig.test('HEllo') //✅

或作为 RegExp 对象构造函数的第二个参数:

new RegExp('hello', 'ig').test('HEllo') //✅

检查正则表达式

给定一个正则表达式,您可以检查其属性:

  • source,正则表达式的字符串
  • multilinem 标志是否为 true
  • globalg 标志是否为true
  • ignoreCasei 标志是否为true
  • lastIndex
/^(\w{3})$/i.source     //"^(\\w{3})$"
/^(\w{3})$/i.multiline  //false
/^(\w{3})$/i.lastIndex  //0
/^(\w{3})$/i.ignoreCase //true
/^(\w{3})$/i.global     //false

转义

下面这些符号是特殊的:

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

z这些特殊字符在正则表达式中是具有含义的控制字符,因此,如果您想在正则表达式中将它们用作匹配字符,则需要通过在它们前面插入\来转义它们:

/^\\$/
/^\^$/ // /^\^$/.test('^') ✅
/^\$$/ // /^\$$/.test('$') ✅

字符边界

\b 和 \B 让您检查字符串是在单词的开头还是结尾:

  • \b 匹配单词开头或结尾的一组字符
  • \B 匹配不在单词开头或结尾的一组字符

例:

'I saw a bear'.match(/\bbear/)    //Array ["bear"]
'I saw a beard'.match(/\bbear/)   //Array ["bear"]
'I saw a beard'.match(/\bbear\b/) //null
'cool_bear'.match(/\bbear\b/)     //null

通过正则表达式来替换字符串

接下来我们将看看如何通过正则表达式来替换字符串,在 JavaScript 中, 'String' 对象提供了一个 replace() 方法,可以在没有正则表达式的情况下使用它来对字符串进行字符替换:

"Hello world!".replace('world', 'dog') //Hello dog!
"My dog is a good dog!".replace('dog', 'cat') //My cat is a good dog!

这个方法同样也接受正则表达式为参数:

"Hello world!".replace(/world/, 'dog') //Hello dog!

通过 g 标志,我们可以替换字符串中所有匹配到的字符:

"My dog is a good dog!".replace(/dog/g, 'cat') //My cat is a good cat!

通过分组我们可以做更多神奇的账号,比如字符串的移动位置:

"Hello, world!".replace(/(\w+), (\w+)!/, '$2: $1!!!')
// "world: Hello!!!"

你可以使用一个函数来做更花哨的事情,而不是使用字符串。它会接收许多参数,这些参数取决于String.match(RegExp)RegExp.exec(String)返回的数组:

"Hello, world!".replace(/(\w+), (\w+)!/, (matchedString, first, second) => {
  console.log(first);
  console.log(second);

  return `${second.toUpperCase()}: ${first}!!!`
})
//"WORLD: Hello!!!"

贪婪模式

正则表达式默认情况下是贪婪的。

这句话是什么意思呢?我们先看下面的例子:

/\$(.+)\s?/

这个正则表达式能够从字符串中提取美元金额:

/\$(.+)\s?/.exec('This costs $100')[1]
//100

但是如果我们在数字后面有更多的单词,它就会不正确了:

/\$(.+)\s?/.exec('This costs $100 and it is less than $200')[1]
//100 and it is less than $200

为什么会这样呢?因为 $ 符号后面的正则表达式.+匹配1次或一次以上的任何字符,并且在到达字符串末尾之前它不会停止。然后,它结束是因为\s?使结束空间成为可选的。

为了解决这个问题,我们需要告诉正则表达式执行尽可能少的匹配。我们可以在量词后面使用?符号来做到这一点:

/\$(.+?)\s/.exec('This costs $100 and it is less than $200')[1]

我们删除了“s”之后的“?”,否则它只匹配第一个数字,因为空格是可选的

因此,?根据其位置表示不同的含义,因为它既可以是量词,也可以是惰性模式指示器。

根据字符串后面的内容匹配字符串

我们使用 ?= 匹配一个特定子字符串后面跟着的某字符串:

/Roger(?=Waters)/

/Roger(?= Waters)/.test('Roger is my dog') //❌
/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //✅

?! 执行反向操作,匹配一个特定子字符串后面不跟着某字符串 

/Roger(?!Waters)/

/Roger(?! Waters)/.test('Roger is my dog') //✅
/Roger(?! Waters)/.test('Roger Waters is a famous musician') //❌

根据字符串前面的内容匹配字符串

?= 类似,我们使用 ?<= 匹配一个特定字符串前面跟着某字符串

/(?<=Roger) Waters/

/(?<=Roger) Waters/.test('Pink Waters is my dog') //❌
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //✅

反向操作时 ?<!:

/(?<!Roger) Waters/

/(?<!Roger) Waters/.test('Pink Waters is my dog') //✅
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //❌

正则表达式和 Unicode 字符

在使用 Unicode 字符串时,u 标志是必需的。如果不添加该标志,则此应匹配一个字符的简单正则表达式将不起作用,因为对于 JavaScript,emoji 表情符号在内部由 2 个字符表示。

/^.$/.test('a') //✅
/^.$/.test('🐶') //❌
/^.$/u.test('🐶') //✅

Unicode, 和普通字符一样,也有处理范围:

/[a-z]/.test('a')  //✅
/[1-9]/.test('1')  //✅

/[🐶-🦊]/u.test('🐺')  //✅
/[🐶-🦊]/u.test('🐛')  //❌

JavaScript 会检查 Unicode 内部代码表示,因为  \u1F436 < \u1F43A < \u1F98A,所以 🐶 < 🐺 < 🦊。我们可以通过full Emoji list来获取这些Unicode符号的代码并找出顺序。(提示:macOS中 表情符号选择器有一些表情符号的顺序是混合的)

Unicode 属性转义

正如我们在上面所看到的,正则表达式中,我们可以使用\d来匹配任何数字,使用\s来匹配任何非空格的字符,使用\w来匹配任何字母数字字符等等...

Unicode 属性转义是 ES2018 的一项功能,它非常强大,将这个概念扩展到所有引入p{}和否定含义的\P{}的Unicode 字符 。

任何 unicode 字符都具有一组属性。例如,Script 确定语言系列,ASCII 是一个布尔值,确定是否是 ASCII 字符,等等。我们可以将这些属性放在{}括号中,正则表达式将检查该属性是否为 true:

/^\p{ASCII}+$/u.test('abc')   //✅
/^\p{ASCII}+$/u.test('ABC@')  //✅
/^\p{ASCII}+$/u.test('ABC🙃') //❌

ASCII_Hex_Digit 是另一个布尔属性,用于检查字符串是否仅包含有效的十六进制数字:

/^\p{ASCII_Hex_Digit}+$/u.test('0123456789ABCDEF') //✅
/^\p{ASCII_Hex_Digit}+$/u.test('h')                //❌

还有许多其他布尔属性,您只需通过在图形括号中添加它们的名称来检查它们,包括UppercaseLowercaseWhite_SpaceAlphabeticEmoji等:

/^\p{Lowercase}$/u.test('h') //✅
/^\p{Uppercase}$/u.test('H') //✅

/^\p{Emoji}+$/u.test('H')   //❌
/^\p{Emoji}+$/u.test('🙃🙃') //✅

除了这些二进制属性之外,还可以检查任何 unicode 字符属性以匹配特定值。在此示例中,我检查字符串是用希腊字母还是拉丁字母书写的:

/^\p{Script=Greek}+$/u.test('ελληνικά') //✅
/^\p{Script=Latin}+$/u.test('hey') //✅

更多属性信息可以参考链接.

示例

接下来是一些正则表达式的使用示例:

1. 从字符串中提取数字

'Test 123123329'.match(/\d+/)
// Array [ "123123329" ]

2. 匹配电子邮件地址

一种简单的方法是使用 \S 检查 @ 符号前后的非空格字符:

/(\S+)@(\S+)\.(\S+)/

/(\S+)@(\S+)\.(\S+)/.exec('copesc@gmail.com')
//["copesc@gmail.com", "copesc", "gmail", "com"]

但是这是一个简单的例子,许多无效的电子邮件仍然可以被这个正则表达式所匹配。

3. 在双引号之间捕获文本

假设有一个包含双引号内容的字符串,那如何去提取出来该内容呢?一种比较好的方法是通过组来提取,因为我们知道是以"开头和结尾,我们可以很容易地定位它,但我们也要从结果中删除这些引号,最后我们将会发现 result[1]中就是我们的结果:

const hello = 'Hello "nice flower"'
const result = /"([^']*)"/.exec(hello)
//Array [ ""nice flower"", "nice flower" ]

4.获取 HTML 标签中的内容

例如,获取 span 标签内的内容,并且允许标签内任意数量的参数:

/<span\b[^>]*>(.*?)<\/span>/

/<span\b[^>]*>(.*?)<\/span>/.exec('test')
// null

/<span\b[^>]*>(.*?)<\/span>/.exec('<span>test</span>')
// ["<span>test</span>", "test"]

/<span\b[^>]*>(.*?)<\/span>/.exec('<span class="x">test</span>')
// ["<span class="x">test</span>", "test"]

总结

本文中比较详细的介绍了正则表达式相关的概念和基础知识,如如何定义正则表达式、正则表达式的特殊字符、量词以及正则表达式的使用示例,希望能够帮助大家对正则表达式有个相对全面的认识。

文中如有错误,敬请指正!