正则表达式头痛散

288 阅读4分钟

前言

正则表达式匹配到底有多香?大家先看一下下面的 2 个例子:

  1. 封装具有搜索功能的下拉列表组件,实现过滤搜索关键字功能 普通实现
function isShowItem(item) {
   if (!this.searchText) return true;
   return (
     item.name.toLowerCase().indexOf(this.searchText.toLowerCase()) >= 0
     );
   }

正则实现

function isShowItem(item) {
   if (!this.searchText) return true;
   return (new RegExp(this.searchText, "i").test(item.name));
 }
  1. 获取地址参数 普通实现
function getUrlParam(name) {
  const query = window.location.search.substring(1);
  const vars = query.split("&");
  for (let i=0;i<vars.length;i++) {
       let pair = vars[i].split("=");
       if(pair[0] === name){return pair[1];}
  }
  return false
}

正则实现

function getUrlParam(name) {
  const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)') 
  const r = window.location.search.substr(1).match(reg) 
  if (r !== null) return decodeURI(r[2])
  return false 
}

普通实现的方式非常丑陋蹩脚,而用正则匹配代码非常简洁优雅,逼格瞬间提升几倍!香是香,可由于繁琐易混淆的语法让我们对它敬而远之,当不得不用到它的时候只能去网上搜索抄答案。笔者认为学好正则的一个小秘诀就是记符号的单词。

这篇文章非常简单通俗,囊括了很多日常应用到的例子,可以让你轻松上手正则表达式并且不会容易遗忘。如有不妥之处欢迎留言指正。

正则表达式

创建一个正则表达式有两种方式

const reg = /pattern/gmi
const reg = new RegExp('pattern','gmi')

通常情况下我们用第一种方式 /.../,那么什么时候用 new RegExp 呢?

答案是:当我们需要动态定义我们的正则表达式时。例如上面的例子

function isShowItem(item) {
   if (!this.searchText) return true;
   return (new RegExp(this.searchText, "i").test(item.name));
 }

new RegExp 构造允许我们传入参数 this.searchText 作为正则的模式 pattern 的字符。

字符类

字符类是指一个特殊的符号,匹配特定集合的任何符号其包括

字符解释
\ddigit - 表示从 0-9 的数字字符 与 [0-9] 相同
\D与 \d 相反,表示非数字字符与 [^0-9]
\wword - 表示字母或数字或下划线 _ 与 [a-zA-Z0-9_] 相同
\W与 \w 相反,表示除\s以外字符 与 [^a-zA-Z0-9_] 相同
\sspace - 包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,与 [\t\n\v\f\r] 相同
\S与 \s 相反,表示非空格字符
.表示除换行符 \r\n 之外的任何字符
\s\S表示任何字符

上面我说过学习正则小秘诀就是记缩写的单词,看了上面的符号单词就瞬间理解了字符的意思和作用有没有!

常用应用例子

// 获取手机号
let phone = "+7(903)-123-45-67";
phone.match(/\d/g)  // 79031234567
phone.replace(/\D/g,'')  // 79031234567

// 验证用户名有效性
const reg = /\w/g
const username = "ali_677";
const.test(username)  // true

// 去掉字符的空格
const str = 'H T M L'
str.replace(/\s/g,'') // HTML

// 匹配换行字符
"A\nB".match(/A.B/) // null
"A\nB".match(/A[\s\S]B/) // A\nB

修饰符

修饰符解释
iignore - 不区分大小写 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
gglobal - 全局匹配 查找所有的匹配项。
mmulti line - 多行匹配 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。

eg:

const str = `Google baidu juejin 
javascript baidu`; 

str.match(/Google/);   // null 区分大小写
str.match(/Google/i);  // Google 不区分大小写

str.match(/baidu/);  // baidu 只匹配一次
str.match(/baidu/g);  // baidu baidu 匹配全部

const str1 = `1、Google
2、baidu
3、juejin
4、Google`;

str1.match(/^\d+/g) // 1
str1.match(/^\d+/gm) // 1、2、3、4

锚点:字符串开始 ^ 和末尾 $

插入符号 ^ 匹配字符串开头和美元符号 $ 匹配字符串结尾。

eg:

const validTime = "12:34";
const invalidTime = "12:345";

const regexp = /^\d\d:\d\d$/;
regexp.test(validTime); // true
regexp.test(invalidTime) ; // false

注意: ^ 符号放到集合时也可以作为 ‘非’ 表达式来用

  • [^aeyo] —— 匹配任何除了 'a'、'e'、'y' 或者 'o' 之外的字符。
  • [^0-9] —— 匹配任何除了数字之外的字符,也可以使用 \D 来表示。
  • [^\s] —— 匹配任何非空字符,也可以使用 \S 来表示

eg:

const str = "abcd";
str.match(/[^bc]/g) // a、d

词边界:\b

词边界 \b (boundary)是对 \w 的一种检查;

/b 与 ^ 、$ 都是检查边界,他们的区别是:^ 、$ 对字符串完全匹配

eg

"Hello, Java!".match(/\bJava\b/) // Java
"Hello, Java!".match(/\bHell\b/) )  // null l 之后没有词边界
"Hello, Java!".match(/\bJava!\b/) // null !不是单词 \w,其后没有词边界
"Java".match(/^Java$/) // Java  
"Hello, Java!".match(/^Java$/) // null

特殊字符转义

特殊字符包括 [ \ ^ $ . | ? * + ( ),当我们需要把特殊字符当作常规字符来使用时,一般情况下在字符前面加反斜杠
eg

"Chapter 5.1".match(/\d\.\d/) // 5.1
"function g()".match(/g\(\)/) // g()

斜杆

斜杆 / 不是特殊字符,当它被用于在 Javascript 中开启和关闭正则匹配:/...pattern.../ 时,也需要加转义斜, 而 new RegExp 方式就不需要转义斜杠。

eg

"/".match(/\//)  // '/'
"/".match(new RegExp("/")) '/'

new RegExp 创建正则实例

当我们用 new RegExp 创建带有转义字符正则实例时,我们还需要再做一次转义 eg

const reg = new RegExp("\d\.\d");
"Chapter 5.1".match(reg) // null

原因在于:字符串中的反斜杠表示转义或者类似 \n 这种只能在字符串中使用的特殊字符。这个引用会“消费”并且解释这些字符,比如说:

  • \n —— 变成一个换行字符,
  • \u1234 —— 变成包含该码位的 Unicode 字符,
  • 其它有些并没有特殊的含义,就像 \d 或者 \z,碰到这种情况的话会把反斜杠移除。 解决问题直接在前面多加一个斜杆 \

eg

const regStr = "\\d\\.\\d";
const regexp = new RegExp(regStr);
"Chapter 5.1".match(regexp) // 5.1

集合、范围、数量

集合

[abc] 是指查找在 3 个字符'a'、'b'或者'c' 中的任意一个
这里提醒一下同学很容易出错的地方:把 [abc] 和 /abc/ 弄混
注意:/abc/ 是需要找到字符串中的子字符串 abc,而 [abc] 是指字符串只要任何一个字符是a、b 或者 c 便能匹配上其等价于[abc]{1}

eg

"abc".match(/[abc]/)     // a
"abcd".match(/abc/)      // abc
"abc".match(/[abc]/g)    // a、b、c

范围

[a-z] 代表匹配 a-z 之间的任意字符,[0-9] 代表匹配 0-9 之间的任意字符, 另外

  • \d —— 和 [0-9] 相同,
  • \w —— 和 [a-zA-Z0-9_] 相同,
  • \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 unicode 空格字符相同。

eg

"Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) // xAF

量词 +,*,?{n}

量词代表需要匹配的字符的个数;

  • + 代表匹配个数为 1 个以上等价于 {1,}
  • * 代表匹配个数为 0 个以上等价于 {0,}
  • ? 代表匹配个数为 0 个或者 1 个
  • {n} 代表匹配个数为 n
  • {n,m} 代表匹配个数为范围为 n 到 m
  • {n,} 代表匹配个数为 n 个以上

[abc]{n} 代表匹配 abc 中任意 n 个字符组合的匹配,若 n=2,即表示可以匹配 aa、ab、ac、bb、ba、bc、cc、ca、cb

eg

"abc".match(/[abc]{3}/)  // abc
"abaccb".match(/abc/g)   // null
"abaccb".match(/[abc]{3}/g) // aba、ccb

常用应用示例

// 匹配 html 字符串
"<h1>Hi!</h1>".match(/<\/?[a-z][a-z0-9]*>/gi)  // <h1>, </h1>

// 匹配颜色值
const str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2 #12345678";
str.match(/#[0-9a-f]{6}\b/gi)// #121212,#AA00ef

在集合 [] 中不转义

在非集合字符正则表达式如需要用特殊字符普通用时比如要匹配 . ,这时我们需要加上反斜杠 . 但在集合 [] 里不需要,因为它不存在特殊字符运用,当然你要转义也是 OK 的。

eg

// 并不需要转义
const reg = /[-().^+]/g;
"1 + 2 - 3".match(reg)  // +,-

// 转义其中的所有字符
const reg = /[\-\(\)\.\^\+]/g;
"1 + 2 - 3".match(reg)  // +,-

贪婪模式和懒惰模式模式

贪婪模式

默认情况下(默认贪婪模式),量词都会尽可能地重复多次。例如,\d+ 检测所有可能的字符。当不可能检测更多(没有更多的字符或到达字符串末尾)时,然后它再匹配模式的剩余部分。如果没有匹配,则减少重复的次数(回溯),并再次尝试。

我们想匹配加双引号 “” 的单词,结果失败匹配

const reg = /".+"/g;
const str = 'a "witch" and her "broom" is one';
str.match(reg) // "witch" and her "broom"

懒惰模式

懒惰模式跟贪婪模式相反,它会尽可能地减少重复次数,实现的方式在量词后加 ?。即

  • * => *?
  • + => +?
  • ? => ??
const reg = /".+?"/g;
const str = 'a "witch" and her "broom" is one';
str.match(reg) // "witch","broom"

常用应用示例

// 匹配 html 注释
const reg = /<!--[\s\S]*?-->/g;
const str = `... <!-- My -- comment
 test --> ..  <!----> ..`;

str.match(reg) // '<!-- My -- comment \n test -->', '<!---->'

捕获组

捕获组是表达式模式用括号括起来 (...) 的部分。

  1. 它可以将匹配的一部分作为结果数组中的单独项。
  2. 如果我们将量词放在括号后,则它将括号视为一个整体。 比如 (go)+,匹配 go,gogo,gogogo 等

eg

// 没有 g 标志会返回括号匹配的 h1
const str = "<h1>Hello, world!</h1>";
const tag = str.match(/<(.*?)>/); // h1、<h1>

// 有 g 标志将不会返回 h1
const str1 = "<h1>Hello, world!</h1>";
const tag1 = str.match(/<(.*?)>/); // <h1>、</h1>

// 用 matchAll 返回所有匹配组
const str2 = "<h1>Hello, world!</h1>";
const [tag1,tag2] = str.matchAll(/<(.*?)>/);
tag1  // h1、<h1>
tag2 // /h1、</h1>

常用应用示例

// 域名匹配
const regexp = /(\w+\.)+\w+/g;
"site.com my.site.com".match(regexp) // site.com,my.site.com
// 邮箱匹配
const regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;
"my@mail.com my@site.com.uk".match(regexp) // my@mail.com, my@site.com.uk

命名组

通过在捕获组前加 ?<name> 即可给捕获组命名 name,看以下示例

eg

// 从 year-month-day 格式改 month/day/year
const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const str = "2019-04-30";

const {year,month,day} = str.match(dateRegexp).groups;
`${month}/${day}/${year}` // 04/30/2019

选择(OR)|

正则表达式 | 表示 “或” 的意思。

示例:从 "Java JavaScript PHP C++ C" 字符串中找出汇编语言

// bad
const reg = /Java|JavaScript|PHP|C|C\+\+/g;
const str = "Java, JavaScript, PHP, C, C++";
alert( str.match(reg) ); // Java,Java,PHP,C,C

// good

const reg = /Java(Script)?|C(\+\+)?|PHP/g;
const str = "Java, JavaScript, PHP, C, C++";
alert( str.match(reg) ); // Java,JavaScript,PHP,C,C++

匹配时间

const reg = /([01]\d|2[0-3]):[0-5]\d/g;
"00:00 10:10 23:59 25:99 1:2".match(reg); // 00:00,10:10,23:59

前瞻断言与后瞻断言

前瞻断言:语法为:x(?=y),它表示匹配x, 仅在后面是y的情况

后瞻断言:后瞻肯定断言:(?<=y)x, 匹配x, 仅在前面是y的情况

eg

// 前瞻断言
let str = "1 turkey costs 30€";
alert( str.match(/\d+(?=€)/) ); // 30 (正确地跳过了单个的数字 1)

// 后瞻断言
let str = "1 turkey costs $30";
alert( str.match(/(?<=\$)\d+/) ); // 30 (跳过了单个的数字 1)

image.png

常用示例:取整数

const regexp = /(?<![-\d])\d+/g;
const str = "0 12 -5 123 -18";
str.match(regexp); // 0, 12, 123

RegExp 对象方法

  1. test 检索字符串中指定的值。返回 true 或 false
  2. exec 检索字符串中指定的值。返回数组包括找到的值和其位置 用法: /正则表达式/.test('字符串')

支持正则表达式的 String 对象的方法

  1. search 检索与正则表达式相匹配的值。返回其位置索引值
  2. match 检索与正则表达式相匹配的值。返回数组包括找到的值和其位置
  3. replace 替换与正则表达式匹配的子串。
  4. split 把字符串分割为字符串数组

用法: '字符串'.match(/正则表达式/)

文章参考

现代 JavaScript 教程