正则表达式

简介

通过使用正则表达式,可以:

  • 测试字符串内的模式。例如,可以测试输入字符串,以查看字符串内是否出现电话号码模式或信用卡号码模式。这成为数据验证。
  • 替换文本。可以使用正则表达式来识别文档中的特定文本,完全删除该文本或者用其他文本替换它。
  • 基于模式匹配从字符串中提取子字符串。可以查找文档内或输入域内特定的文本。

发展历史

正则表达式的"祖先"可以一直上溯至对人类神经系统如何工作的早期研究。Warren McCulloch 和 Walter Pitts 这两位神经生理学家研究出一种数学方式来描述这些神经网络。

1956 年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为"神经网事件的表示法"的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为"正则集的代数"的表达式,因此采用"正则表达式"这个术语。

随后,发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究,Ken Thompson 是 Unix 的主要发明人。正则表达式的第一个实用应用程序就是 Unix 中的 qed 编辑器。

如他们所说,剩下的就是众所周知的历史了。从那时起直至现在正则表达式都是基于文本的编辑器和搜索工具中的一个重要部分。

语法

正则表达式是由普通字符(例如字符a到z)以及特殊字符(成为“元字符”)组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串 。正则表达式作为一个模板,将某个字符模式与搜索的字符串进行匹配。

普通字符

普通字符包括没有显示指定为元字符的所有可打印和不可打印字符。包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

非打印字符

非打印字符也可以是正则表达式的组成部分。下表是非打印字符的转义序列:

字符 描述
\cx 匹配由x指明的控制字符。例如,\cM匹配一个 Control-M 或回车符。x的值必须为 A-Z 或 a-z 之一,否则,将 c 视为一个原义的 'c' 字符
\f 匹配一个换页符。等价于 \x0c 和 \cL
\n 匹配一个换行符。等价于 \x0a 和 \cJ
\r 匹配一个回车符。等价于 \x0d 和 \cM
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [\f\n\r\t\v]
\S 匹配任何非空白字符。等价于 [^\f\n\r\t\v]
\t 匹配一个制表符。等价于 \x09 和 \cl
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK
特殊字符

下表是正则表达式中的特殊字符:

特殊字符 描述
$ 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 '\n' 或 '\r'
() 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用
* 匹配前面的子表达式零次或多次
+ 匹配前面的子表达式一次或多次
. 匹配除换行符 \n 之外的任何单字符。
[ 标记一个中括号表达式的开始
? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符
\ 将下一个字符标记为特殊字符、或原义字符、或向后引用、或八进制转义符
^ 匹配输入字符串的开始位置,在方括号表达式中使用,表示不接受该字符集合
{ 标记限定符表达式的开始
| 指明两项之间的一个选择
限定符

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

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

定位符能够将正则表达式固定到行首或行尾。
正则表达式的定位符有以下四个

字符 描述
^ 匹配输入字符串开始的位置
$ 匹配输入字符串结尾的位置
\b 匹配一个字边界,即字与空格间的位置
\B 非字边界匹配

注意:不能将限定符与定位点一起使用。由于在紧靠换行或者字边界的前面或后面不能有一个以上位置,因此不允许诸如 ^* 之类的表达式。

字边界是单词和空格之间的位置。非字边界是任何其他位置。

/\bCha/  // 匹配 Cha 开头的字符串
/ter\b/  // 匹配 ter 结尾的字符串
/\Bapt/ // 匹配含有 apt 子串的字符串 并且 apt不位于字符串边界(对于 \B 非字边界运算符,位置并不重要,因为匹配不关心究竟是单词的开头还是结尾。)
选择

用圆括号将所有选择项括起来,相邻的选择项之间用 | 分隔。使用圆括号时,相关的匹配会被缓存,可用 ?: 放在第一个选项前来消除这种副作用。
?: 是非捕获元之一,还有两个非捕获元是 ?= 和 ?!,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串。

反向引用

对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从1开始,最多可存储99个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
可以使用非捕获元字符 '?:'、'?=' 或 '?!' 来重写捕获,忽略对相关匹配的保存。

运算符优先级

正则表达式从左到右进行计算,并遵循优先级顺序。相同优先级的从左到右进行运算,不同优先级的运算先高后低。下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

运算符 描述
\ 转义符
(), (?:), (?=), [] 圆括号和方括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, \任何元字符、任何字符 定位点和序列(即位置与顺序)
| 替换,“或”操作

JavaScript RegExp对象

创建正则表达式

JavaScript有两种方式创建一个正则表达式:
第一种方法是直接通过/正则表达式/写出来,第二种方式是通过new RegExp('正则表达式')创建一个 RegExp 对象:

const re1 = /ABC\-001/;
const re2 = new RegExp('ABC\\-001');

re1; // /ABC\-001/
re2; // /ABC\-001/
test()方法

RegExp对象的test()方法用于测试给定的字符串是否符合条件。

const re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false
exex()方法

如果正则表达式中定义了组,就可以在RegExp对象上用exec()方法提取出子串来。
exec()方法在匹配成功后,会返回一个Array,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。
exec()方法在匹配失败时返回null:

const re = /^(\d{3})-(\d{3,8})$/;
re.exec('010-12345'); // ['010-12345', '010', '12345']
re.exec('010 12345'); // null

ES6 中正则的扩展

u 修饰符

ES6 对正则表达式添加了u修饰符,含义为"UniCode 模式",用来正确处理大于\uFFFF的 UniCode 字符:

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

正则实例对象新增unicode属性,表示是否设置了u修饰符:

const r1 = /hello/;
const r2 = /hello/u;
r1.unicode // false
r2.unicode // true

一旦加上u 修饰符,就会修改下面这些正则表达式的行为:
(1) 点字符
对于码点大于0xFFFF的 UniCode 字符,点字符不能识别,必须加上u修饰符:

const s = '𠮷';
/^.$/.test(s) // false
/^.$/u.test(s) // true

(2) UniCode 字符表示法
ES6 新增了使用大括号表示 UniCode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词:

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

(3) 量词
使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 UniCode 字符:

/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true

(4) 预定义模式
u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的 UniCode 字符:

/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true

上面代码的\S是预定义模式,匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符。
(5) i 修饰符
有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B\u212A都是大写的K

/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

上面代码中,不加u修饰符,就无法识别非规范的K字符。
(6) 转义
没有u修饰符的情况下,正则中没有定义的转义(如逗号的转义\,)无效,而在u模式会报错:

/\,/ // /\,/
/\,/u // 报错
y 修饰符

ES6 为正则表达式添加了y修饰符,叫做"粘连" (sticky) 修饰符。
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是"粘连"的含义:

const s = 'aaa_aa_a';
const r1 = /a+/g;
const r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]
r2.exec(s) // null

ES6 的正则实例对象多了sticky属性,表示是否设置了y修饰符:

const r = /hello\d/y;
r.sticky // true
RegExp.prototype.flags 属性

ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符:

// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"

// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
s 修饰符:dotAll 模式

匹配任意单个字符,通常有一种变通的写法:

/foo[^]bar/.test('foo\nbar')
// true

但是这种方法不太符合直觉,ES 2018 引入了 s修饰符,使得.可以匹配任意单个字符:

/foo.bar/s.test('foo\nbar') // true

这被称为dotAll模式,即点(dot)代表一切字符。
正则表达式还引入了dotAll属性,表示该正则表达式是否处于dotAll模式:

const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');

re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
后行断言

JavaScript语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES 2018 则引进了后行断言。
“先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。“先行否定断言”指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]

“后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。“后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/

/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容