正则表达式

在笔试又被考到了,感觉还是自己写一遍比较靠谱。
文末附上在 IM 开发中遇到需要使用正则匹配的实际应用。
原文地址:正则表达式30分钟入门教程

元字符

代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意空字符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

元字符 \b^$ 都匹配一个位置。如果允许处理多行,则 ^$ 的意义变为匹配行的开始处和结束处。

字符转义

查找元字符本身需要使用 \ 转义,如 \\\.

重复

代码 / 语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复 n 次
{n,} 重复 n 次或更多次
{n,m} 重复 n 到 m 次
1
2
3
4
// 匹配 window 后面跟一个或多个数字
/window\d+/
// 匹配第一个单词
/^\w+/

字符类

在方括号中列出自负集合可指定字符范围

1
2
3
4
// 元音字母
/[aeiou]/
// 英文字母及数字
/[a-z0-9A-Z]/

分支条件

使用 | 将不同的规则分隔开,如果满足其中任意一种规则就当做匹配。满足一种规则后将忽略后面的规则

1
2
// 美国邮箱
/d{5}-d{4}|d{5}/

分组

重复多个字符,可以用小括号指定子表达式(分组)

1
2
3
4
// 匹配 hahaha
/(ha){3}/
// 匹配 IP 地址
/((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/

反义

代码 / 语法 说明
\W 匹配任意非字母,数字,下划线,汉字的字符
\S 匹配任意非空字符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^xyz] 匹配除字符集 [xyz] 以外的字符
1
2
3
4
// 匹配不包含空白符的字符串
/\S+/
// 匹配尖括号括起来的以 a 开头的字符串
/<a[^>]+>/

后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。具体如下:

  • 分组0对应整个正则表达式
  • 组号分配过程需要从左向右扫描两遍:第一遍只给未命名组分配,第二遍只给命名组分配。因此所有命名组的组号都大于未命名的组号
  • 可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权

后向引用用于重复搜索前面某个分组匹配的文本。例如, \1 代表分组1匹配的文本

也可以通过 (?<name>\w+) 或者 (?'name'\w+) 指定组名,通过 \k<name> 反向引用。Javascript 不支持

1
2
3
4
// 匹配重复的单词,如 "ha ha"
/\b(\w+)\b\s+\1\b/
// 等效于
/(?<words>\w+)\b\s+\k<words>\b)/

类型 代码 / 语法 说明
捕获 (exp) 匹配 exp,并捕获文本到自动命名的组里
(?\exp) 匹配 exp,并捕获文本到名称为 name 的组里,也可以写成 (?’name’exp)
(?:exp) 匹配 exp,不捕获匹配的文本,也不给此分组分配组号
零宽断言 (?=exp) 匹配 exp 前面的位置
(?<=exp) 匹配 exp 后面的位置
(?!exp) 匹配后面跟的不是 exp 的位置
(?<!exp) 匹配前面不是 exp 的位置
注释 (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

零宽断言

零宽断言用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像 \b , ^ , $ 那样用于指定一个位置,这个位置应该满足一定的条件(即断言)

(?=exp)

零宽度正预测先行断言 ,它断言自身出现的位置的后面能匹配表达式 exp。比如 \b\w+(?=ing\b) ,匹配以 “ing” 结尾的单词的前面部分(除了 “ing “以外的部分)

1
2
3
4
// 匹配以 "ing" 结尾的单词的前面部分
var str = "I'm singing while you're dancing",
reg = /\b\w+(?=ing)/g;
str.match(reg); // => ["sing", "danc"]


(?<=exp)

零宽度正回顾后发断言 ,它断言自身出现的位置的前面能匹配表达式 exp。Javascript 不支持
比如 (?<=\bre)\w+\b 会匹配以 “re” 开头的单词的后半部分

'Please resize the viewport'
/(?<=\bre)\w+\b/
=> "size"

// 匹配不包含属性的简单 HTML 标签内的内容
/(?<=<(\w+)>).*(?=<\/\1>)/

负向零宽断言


(?!exp)

零宽度负预测先行断言 ,断言此位置的后面不能匹配表达式 exp

比如 \d{3}(?!\d) 匹配三位数字,而且这三位数字的后面不能是数字; \b((?!abc)\w)+\b 匹配不包含连续字符串abc的单词

// 匹配不包含 "ing" 的单词
var str = "singing and dancing",
    reg = /\b((?!ing)\w)+\b/g;
str.match(reg);    // => ["and"]
// 发现这样子无法匹配
reg = /\b(?!ing)\w+\b/g;
str.match(reg); // => ["singing", "and", "dancing"]


(?<!exp)

零宽度负回顾后发断言 ,断言此位置的前面不能匹配表达式 exp。Javascript 不支持

比如 (?<![a-z])\d{7} 匹配前面不是小写字母的七位数字

'Please resize the viewport'
/(?<!\bre)\w+\b/
=> ["PLease", "the", "viewport"]

注释

通过语法 (?#comment) 来包含注释

例如: 2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)

如果启用忽略模式里的空白字符,可以在编写表达式时能任意的添加空格,Tab,换行。可以这样写代码

(?<=    # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
)       # 前缀结束
.*      # 匹配任意文本
(?=     # 断言要匹配的文本的后缀
<\/\1>  # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
)       # 后缀结束

贪婪与懒惰

贪婪匹配

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。

a.*b ,它将会匹配最长的以 “a” 开始,以 “b” 结束的字符串。如果用它来搜索 “aabab” 的话,它会匹配整个字符串 “aabab”。

懒惰匹配

也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号 ? 。这样 .*? 就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。

a.*?b 匹配最短的,以 “a” 开始,以 “b” 结束的字符串。如果把它应用于 “aabab” 的话,它会匹配 “aab”(第一到第三个字符)和 “ab”(第四到第五个字符)

为什么第一个匹配是 “aab”(第一到第三个字符)而不是 “ab”(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰 / 贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权

代码 / 语法 说明
*? 匹配 exp,并捕获文本到自动命名的组里
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

处理选项

代码 / 语法 说明
IgnoreCase 匹配时不区分大小写
Multiline 更改 ^$ 的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下, $ 的精确含意是:匹配 \n 之前的位置以及字符串结束前的位置.)
Singleline 更改 . 的含义,使它与每一个字符匹配(包括换行符 \n
IgnorePatternWhitespace 忽略表达式中的非转义空白并启用由 # 标记的注释
ExplicitCapture 仅捕获已被显式命名的组

其他

代码 / 语法 说明
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII 代码中八进制代码为 nn 的字符
\xnn ASCII 代码中十六进制代码为 nn 的字符
\unnnn Unicode 代码中十六进制代码为 nnnn 的字符
\cN ASCII 控制字符。比如 \cC 代表 Ctrl+C
\A 字符串开头(类似 ^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似 $,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode 中命名为 name 的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?-exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes/no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes/no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
(?(name)yes) 同上,只是使用空表达式作为no

业务相关,举个栗子

  • URL 替换成 a

    1
    2
    3
    4
    5
    6
    7
    handleUrl: function (content) {
    if (!content) {
    return '';
    }
    const reg = /(http:\/\/|https:\/\/)((\w|=|\?|\.|\/|&|-)+)/g;
    return content.replace(reg, '<a href="$1$2" target="_blank" class="a-link">$1$2</a>');
    }
  • Emoji 替换成 img

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    handleEmoji: function (content) {
    if (!content) {
    return '';
    }
    const emojiReg = /:(.+?):/g;
    const emojiNameArr = content.match(emojiReg);
    if (emojiNameArr) {
    const emojiNameArrLength = emojiNameArr.length || 0;
    for (let i = 0; i < emojiNameArrLength; i++) {
    const curEmojiName = emojiNameArr[i].replace(/:/g, '');
    emojiNameArr[i] = ':' + curEmojiName + ':';
    const unified = isEmojiName.unified.toLowerCase();
    const fileName = unified + '.png';
    const imgLabel = [
    '<img class="emoji-img ',
    curEmojiName,
    '" src="https:',
    configs.api.emojiDownload + fileName,
    '" data-emoji="',
    curEmojiName,
    '" data-unified="',
    unified,
    '">'
    ].join('');
    content = content.replace(emojiNameArr[i], imgLabel);
    }
    }
    return content;
    }
  • @人 替换成 span

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    handleAtLabel: function (item) {
    const _item = { ...item };
    let content = _item.displayContent;
    if (_item.remindJids && _item.remindJids.users) {
    const remindJidsLength = _item.remindJids.users.length;
    for (let i = 0; i < remindJidsLength; i++) {
    const userItem = _item.remindJids.users[i];
    const jid = userItem.jid;
    const nickName = userItem.nickname;
    const atReg = new RegExp([
    '@(',
    filter.filterSpecChar(nickName),
    ')'
    ].join(''), 'g');
    content = content.replace(atReg, [
    '<span data-display="$1" ',
    'data-plain-text="$1" ',
    'data-jid="',
    jid.split('@')[0],
    '">@\ufeff$1</span>',
    ' '
    ].join(''));
    }
    }
    return content;
    }