正则表达式(regular expression) 描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

构造正则表达式的方法和创建数学表达式的方法一样。也就是用多种元字符与运算符可以将小的表达式结合在一起来创建更大的表达式。正则表达式的组件可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。

# 从一个文件批量修改开始:正则表达式深入剖析

写这篇文章的源头,是我在使用WebStrom编写样式时遇到了问题:

首先说明一下我使用的是css预处理器是stylus,编写的样式单位是rpx,但是如果使用Emmet的话,会出现两个问题

  1. rpx前面会自动添加一个空格
  2. 如果不带单位,自动补全为px

比如:

  • w100rpx -> width 100 rpx
  • w100 -> width 100px

两种都不是预期想要的,我想得到的结果是:

  • w100rpx -> width 100rpx
  • w100 -> width 100rpx

既然Emmet不支持rpx的直接输入,那或许可以换个思路:使用正则替换去除rpx前的空格,并将px转换为rpx(重点是不能匹配到rpx,否则将会替换为rrpx)

于是从网上找到了解决方案:使用 WebStrom 的 File Watchers 监听文件变动,然后执行特定的操作

但是网上使用的是sed,我还专门在Windows下使用了git中带了sed,发现并不能如愿

最开始使用的正则是:

sed -i s/[^r]px/rpx/g $FilePath$

发现倒是没有匹配 rpx,但是却匹配了一个非r开头的px,比如:

  • 100 rpx -> 不匹配
  • 100px -> 匹配 0px

于是查资料,发现零宽断言可以解决这个问题,于是改写为:

sed -i s/(?<!r)px/rpx/g $FilePath$

却发现sed居然不支持零宽断言,根本不起作用,于是,只能想其他方法,或许JS支持,于是写了一个脚本:

const fs = require('fs')
let [filename] = process.argv.slice(2)
let data = fs.readFileSync(filename, {
  encoding: 'utf-8'
})
data = data.replace(/(?<!r)px/g, 'rpx');
data = data.replace(/\s+rpx/g, 'rpx');
fs.writeFileSync(filename, data, {
  encoding: 'utf-8'
})
console.log(filename + ': 替换成功');

在FileWatcher中配置如下:

Program: node
Arguments: $ContentRoot$/tools/px2rpx.js $FilePath$
Output path to refresh: $FilePath$
Working directory: $FileDir$

ok,匹配成功,并正确修改了文件内容。这样的话, 就可以通过这个脚本执行px到rpx的替换了,同时将rpx前的空格也一并去除了

非常爽不是吗,这样我就可以开心地使用Emmet了:

  • w100rpx -> width 100rpx
  • w100 -> width 100rpx

既然搞了,就再把正则复习一遍吧,算是一个总结,加深记忆,遵循从简到难的方式梳理整个正则表达式的知识点

# 元字符

元字符,又叫字符集,就是用一些特殊符号表示特定种类的字符或位置。

一些常见的元字符:

  • . 匹配除换行符以外的任意字符
  • \w 匹配字母或数字或下划线或汉字, 等价于 [a-z0-9A-Z_]
  • \d 匹配数字, 等价于 [0-9]
  • \0hh 8进制值hh所表示的字符
  • \xhh 16进制值hh所表示的字符
  • \uhhhh 16进制值hhhh所表示的Unicode字符

# 反义元字符

通常反义元字符使用大写字母表示, 或者使用 ^ 匹配

  • \W 匹配任意不是字母,数字,下划线,汉字的字符, 等价于 [^a-z0-9A-Z_]
  • \D 匹配任意非数字的字符, 等价于 [^0-9]
  • [^x] 匹配除了x以外的任意字符
  • [^aeiou] 匹配除了aeiou这几个字母以外的任意字符

# 非打印字符

  • \s 匹配任意空白字符,包括空格、制表符、换页符等等, 等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符
  • \S 匹配任意非空白字符, 等价于 [^ \f\n\r\t\v]
  • \f 匹配一个换页符, 等价于 \x0c\cL
  • \n 匹配一个换行符, 等价于 \x0a\cJ
  • \r 匹配一个回车符, 等价于 \x0d\cM
  • \t 匹配一个制表符, 等价于 \x09\cI
  • \v 匹配一个垂直制表符, 等价于 \x0b\cK
  • \e Escape
  • \cx 匹配由x指明的控制字符。例如,\cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符

# 字符转义

如果想匹配元字符本身或者正则中的一些特殊字符,使用\转义。例如匹配*这个字符则使用\*,匹配\这个字符,使用\\

需要转义的字符:$, ^, (, ), *, +, ., [, ], ?, \, |, {, }

其他元字符将在接下来的文章中一一陈列

# 定位符

  • ^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。
  • $ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。
  • \b 匹配一个单词边界,即字与空格间的位置。
  • \B 非单词边界匹配。
  • \A 字符串开头(类似^,但不受处理多行选项的影响)
  • \z 字符串结尾(类似$,但不受处理多行选项的影响)
  • \Z 字符串结尾或行尾(不受处理多行选项的影响)
  • \G 上一个匹配的结尾(本次匹配开始)

举例:

  • \bapt 匹配 aptitude 中的字符串 apt,但不匹配 Chapter 中的字符串 apt
  • \Bapt 匹配 Chapter 中的字符串 apt,但不匹配 aptitude 中的字符串 apt
  • ^Chapter.*[0-9]$ 匹配以 Chapter 开头, 以数字结尾的行

# 量词 (限定符)

量词又叫限定符, 用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配

  • * 匹配前面的子表达式零次或多次, 等价于{0,}
  • + 匹配前面的子表达式一次或多次, 等价于{1,}
  • ? 匹配前面的子表达式零次或一次, 等价于{0,1}
  • {n} n 是一个非负整数。匹配确定的n次
  • {n,} n 是一个非负整数。至少匹配n次。
  • {n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次

举例:

  • fo*d 可以匹配: fd fod food foood
  • fo+d 可以匹配: fod food foood
  • fo?d 可以匹配: fd fod
  • fo{1}d 可以匹配: fod
  • fo{0,}d 可以匹配: fd fod food foood
  • fo{1,}d 可以匹配: fod food foood
  • fo{1,2}d 可以匹配: fod food

# 贪婪模式与懒惰模式

  • 贪婪:匹配尽可能长的字符串
  • 懒惰:匹配尽可能短的字符串

默认为贪婪模式, 懒惰模式的启用只需**在量词之后加?**既可:

  • *? 重复任意次,但尽可能少重复
  • +? 重复1次或更多次,但尽可能少重复
  • ?? 重复0次或1次,但尽可能少重复
  • {n,m}? 重复n到m次,但尽可能少重复
  • {n,}? 重复n次以上,但尽可能少重复

举例:

// 贪婪模式
'food'.match(/o+/g) // ["oo"]
// 懒惰模式
'food'.match(/o+?/g) // ["o", "o"]

# 字符簇

  • [xyz] 字符集合。匹配所包含的任意一个字符
  • [^xyz] 负值字符集合。匹配未包含的任意字符
  • [a-z] 字符范围。匹配指定范围内的任意字符
  • [^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符

举例:

  • [abc] 可以匹配 "plain" 中的 'a'
  • [^abc] 可以匹配 "plain" 中的'p'、'l'、'i'、'n'
  • [a-z] 可以匹配 'a' 到 'z' 范围内的任意小写字母字符
  • [^a-z] 可以匹配任何不在 'a' 到 'z' 范围内的任意字符
  • [0-9] 匹配任意数字, 同 ``

# 分组 (子表达式)

语法: (pattern)

匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在 JavaScript 中使用 $0…$9 属性获取。要匹配圆括号字符,可以使用 \(\)

# 反向引用

后面的表达式可以引用前面的某个分组,用\1表示,就好像分组1的值赋值给了\1这个变量,这个变量可以在后面任意位置引用。

  • \1 表示分组1匹配的文本

比如:

  • (\w+)\s+\1 相当于 (\w+)\s+(\w+), 可以匹配 Hello Hello

# 分支条件

分支条件又叫逻辑运算符,在此X和Y表示两个表达式

  • XY X紧跟Y
  • X|Y 表示X或Y,从左到右,满足第一个条件就不会继续匹配了。

# 非获取匹配

语法: (?:pattern)

分组会将匹配到的内容进行存取, 如果不需要进行存储, 则可以使用非获取匹配。

这在使用 "或" 字符 (|) 来组合一个模式的各个部分是很有用。例如, 'industr(?:y|ies) 就是一个比 'industry|industries' 更简略的表达式。

举例:

// 分组
'industry'.replace(/industr(y|ies)/g, '$1') // "y"
'industries'.replace(/industr(y|ies)/g, '$1') // "ies"
// 非获取匹配
'industry'.replace(/industr(?:y|ies)/g, '$1') // "$1"
'industries'.replace(/industr(?:y|ies)/g, '$1') // "$1"

以上示例可以看出, 分组进行了信息存储, 而非获取匹配未进行存储

# 匹配选项

  • (?i):忽略大小写(CASE_INSENSITIVE)
  • (?m):多行模式(MULTILINE)
  • (?x):忽略空格字符(COMMENTS)
  • (?s):表示所在位置右侧的表达式开启单行模式(DOTALL)
  • (?u):对Unicode符大小写不敏感(UNICODE_CASE),必须启用CASE_INSENSITIVE
  • (?d):只有'\n'才被认作一行的中止(UNIX_LINES)

# 全局匹配 g

如果不加全局匹配选项 g, 则只匹配符合条件的第一个

# 多行匹配 m

使用选项 m 开启多行匹配模式, (?m)只有在正则表达式中涉及到多行的“^”和“$”的匹配时,才使用Multiline模式。

`
food
 food
  food
`.match(/^fo+d$/g) // null
`
food
 food
  food
`.match(/^fo+d$/gm) // ["food"]
`
food
 food
  food
`.match(/fo+d$/gm) // ["food", "food", "food"]

# 零宽断言

所谓零宽断言,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。

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

作用是给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的字表达式匹配成功。

注意: 这里所说的子表达式并非只有用小括号括起来的表达式,而是正则表达式中的任意匹配单元。

以下是一些常见的零宽断言匹配模式:

  • \w+(?=ing) 匹配以ing结尾的多个字符(不包括ing)
  • \w+(?!ing) 匹配不是以ing结尾的多个字符
  • (?<=read)\w+ 匹配以read开头的多个字符(不包括read)
  • (?<!read)\w+ 匹配不是以read开头的多个字符

# 零宽度正预测先行断言

(?=exp)零宽度正预测先行断言,又叫正向肯定预查(look ahead positive assert),它断言自身出现的位置的后面能匹配表达式exp。

举例:

'product_path'.match(/(product)(?=_path)/g) // ["product"]
'product_path'.match(/\w+(?=_path)/g) // ["product"]

# 零宽度正回顾后发断言

(?<=exp)零宽度正回顾后发断言,又叫反向肯定预查(look behind positive assert),它断言自身出现的位置的前面能匹配表达式exp

举例:

'product_path'.match(/(?<=product_)(path)/g) // ["path"]
'product_path'.match(/(?<=product_)\w+/g) // ["path"]

# 零宽度负预测先行断言

(?!exp)零宽度负预测先行断言,又叫正向否定预查(look ahead negative assert),断言此位置的后面不能匹配表达式exp。

举例:

'product_path'.match(/(product)(?!_path)/g) // null
'product_path'.match(/(product)(?!_url)/g) // ["product"]

# 零宽度负回顾后发断言

  • (?<!exp)零宽度负回顾后发断言,又叫反向否定预查(look ahead positive assert),断言此位置的前面不能匹配表达式exp

举例:

'product_path'.match(/(?<!product_)(path)/g) // null
'product_path'.match(/(?<!image_)(path)/g) // ["path"]

# 示例: 匹配HTML标签内容

let reg = /(?<=>)[^<>]+(?=<)/g
let content = "<p>内容一</p> <div>内容二</div>"
content.match(reg)

匹配结果:

["内容一", " ", "内容二"]

但上面的匹配其实也不完全合理, 因为匹配的内容只要是在 >< 之间的均可匹配, 比如:

let reg = /(?<=>)[^<>]+(?=<)/g
let content = ">123<"
content.match(reg) // ["123"]

可以改进为:

let reg = /(?<=<\w+>)[^<>]+(?=<\/\w+>)/g
let content = "<p>内容一</p> <div>内容二</div>"
content.match(reg) // ["内容一", "内容二"]
content = ">123<"
content.match(reg) // null

# 示例: 匹配px并将其转换为rpx

let reg = /(?<!r)px/g
let data = "width: 100px; height: 200rpx;"
data.replace(/(?<!r)px/g, 'rpx')

替换结果:

"width: 100rpx; height: 200rpx;"

# 更多匹配示例

  • (?<!r)px 匹配以px而非rpx
  • (?<=\s)\d+(?=\s) 匹配两边是空白符的数字,不包括空白符

可视化匹配:

  • [+-]?(0|([1-9]\d*))(\.?\d*)(?=px) 匹配以px结尾的数字, 包括带小数点和正负号

# 参考资料

在线工具:

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress