学习正则
正则很常用,或者说总是会用到,它就像躺在工具箱角落边的工具,越是生疏越要操练。有人调侃正则,“在你意识到这个问题需要正则,现在你有两个问题了”。下面是我在学习正则时的记录,如果没有特殊说明,环境是 macOS 13.0.1,语言使用 javascript,浏览器使用 Chrome 107.0.5304.110。
捕获组
关于括号、括号加量词对 match 返回结果的影响,结果中可能匹配到空字符串""
或是undefined
:
1 | 'a'.match(/a(z?)(c?)/); |
关于反向引用\N
和 \k<name>
:
- 对之前的捕获组进行反向引用
- 例如
"He said: \"She's the one!\".".match(/(['"])(.*?)\1/g)
是正确的,得到结果["\"She's the one!\""]
; - 例如
"He said: \"She's the one!\".".match(/(['"])(.*?)['"]/g)
是错误的,得到结果["\"She'"]
; \k<name>
中的 k 取自backReference
(反向引用)中的 k。
相关链接:
- 捕获组——JAVASCRIPT.INFO 教程;
- 模式中的反向引用:\N 和 \k<name>——JAVASCRIPT.INFO 教程。
贪婪模式和惰性模式
相关链接:
String.prototype.replace
第二个参数是字符串时,可以利用一些模版变量来替换。
变量名 | 解释 |
---|---|
$$ | 插入一个 “$”。 |
$& | 插入匹配的子串。 |
$` | 插入当前匹配的子串左边的内容。 |
$’ | 插入当前匹配的子串右边的内容。 |
$n | 插入第 n 个分组。 |
$<Name> | 插入有名称的分组。 |
交换分组的单词,这里用到了修饰符g
,代表会交换所有的匹配:
1 | var re = /(\w+)\s(\w+)/g; |
相关链接:
词边界和锚点
关于词边界\b
:
- 词边界指的是
\w
旁边不是\w
的位置,也就是说\b
找的是字母(或数字下划线)的旁边不是字母(或数字下划线)的位置; - 换一种说法,词边界指的是
\w
和\W
之间的位置,\w
和$
(末尾位置)之间的位置,\w
和^
(开头位置)之间的位置。
下面是词边界的例子,可以观察词边界被替换成“🤔”的位置:
1 | "So what do you wanna do, what's your point-of-view.".replace(/\b/g, () => "🤔"); |
锚点^
:紧跟每一个行终止符后的位置,字符串的开头。
锚点$
:紧靠每一个行终止符前的位置,字符串的末尾。
关于锚点的匹配:
1 | var withSpace = " begin\n between\t\n\n\n\nend "; |
观察锚点的位置:
1 | var str = '666\n\n\t\n\n'; |
可以看到^
被替换的位置,是紧跟每一个行终止符后的位置,或在字符串的开头。$
被替换的位置,是紧靠每一个行终止符前的位置,或在字符串的末尾。
提示:锚点放在[]
方括号(character class)中是无效的:
1 | /[$^]/.test(''); // false |
环视
在环视中匹配字符:
1 | '1234567'.match(/\d(?=\d(?!\d))/g); // ['6'] |
上面的代码第一行的环视部分表示,要匹配的位置后面必须是一个数字字符,这个数字字符后面不能紧跟一个数字字符。这个例子的环视里又包了一层环视,但内部的环视是作用在外部环视匹配过的字符之后的。
为什么要区分前瞻断言和后瞻断言:
1 | var reg = /<(?!\/)[^>]+(?<!\/)>/; |
上面的代码块里,可以看到第二个断言是后瞻断言,如果替换成前瞻断言,会导致成功匹配<a/>
,这是因为[^>]+
会匹配到/
,接下来的(?<!\/)
检测当前位置的后面不能是/
,而当前位置的后面是>
,所以匹配成功,所以这里要注意环视是和两边的字符相关的,下面是都使用前瞻断言的代码块:
1 | var reg = /<(?!\/)[^>]+(?!\/)>/; |
有的语言中(非 javascript),环视会导致回溯失败,有时候会引起问题,例如/(?<=(\d+))\w\1/
可能不能匹配123ab12
。javascript 可以使用非固定宽度的环视,ruby 会报错“invalid pattern in look-behind: /(?<=(\d+))\w\1/
”,python 同样报错“re.error: look-behind requires fixed-width pattern
”。
利用环视,可以实现其它语言中正则字符组的运算,下面的例子表示从字母中减去元音字母:
1 | var reg = /(?![aeiou])[a-z]/; |
RegExp.lastIndex
lastIndex 是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。 —— MDN
regexp.test
或regexp.exec
每执行一次,都会重新赋值 lastIndex,这会导致每次执行test
或exec
进行校验的时候,得到的结果不一致。另外还有一个条件是,正则表达式需要包含y
或者g
修饰符。
相关链接:
- 一个比较容易被忽略的正则问题;
- MDN-RegExp.lastIndex—— MDN 的 RegExp.lastIndex 文档。
修饰符
下面是粘性修饰符y
的例子:
1 | var str = "So, get away."; |
带y
的正则,在形式上很像每执行一次就把已经匹配的部分截去,并且给正则添加锚点^
。体现出的效果就像是被 lastIndex 粘住了。
修饰符s
:修饰符s
让字符类.
可以匹配包括换行\n
的所有字符,否则.
只能表示除换行外的所有字符。
关于修饰符u
:添加了修饰符u
之后,正则可以正确处理 4 字节长度的字符,也可以使用\p{...}
表示 unicode 属性。
1 | '😄'.match(/./); // 不用修饰符 u,可以看到获取到的是 4 字节字符代理对的左半部分“\uD83D”,而不是期望的“😄” |
关于修饰符m
:在多行模式m
的正则里,\n
和$
是有区别的,\n
会匹配除最后一行之外的每行末尾的字符\n
,而$
表示每一行末尾的位置,包括最后一行,且$
匹配的是无宽度的位置,而不是字符。
1 | var str = `- Naruto |
相关链接:
- 粘性修饰符 “y”,在位置处搜索;
- Unicode 属性 \p{…}—— Unicode 属性和它的子类别。
字符类
字符类 | 来源 | 解释 |
---|---|---|
\d |
digit | |
\w |
word | 字母、数字、下划线 |
\s |
space | 空格、制表符\t 、换行\n |
. |
点表示除换行\n 之外的任意字符 |
|
反向类 | 小写的字符类的大写形式,表示和对应的字符类互补的字符 |
类似[\d\D]
,包括大小写的字符类可以表示所有字符,[^]
也可以表示所有字符,意思是“匹配除了什么都没有之外其他情况的字符”。
编码、字符串
正则表达式在 ES6 前不能处理 UTF-16 字符码点大于 65536 的部分,例如/\uD83D/.test('💩')
会错误地得到true
,这是因为“💩
”字符被翻译成 UTF-16 的代理对是“\uD83D\uDCA9
”,可以发现/\uD83D/.test('\uD83D\uDCA9')
就匹配了,但“💩
”在我们的视角中应该是 1 个字符才对,而语言内部不是这样理解字符的,因此我们需要告诉语言如何处理这样的字符,告诉它即将要处理的字符串是一段 UTF-16 编码方式的字符串,这就是修饰符u
的作用。
加上u
后,/\uD83D/u.test('💩')
会正确地得到false
,因为程序可以理解💩
是单个字符,而不是分开的两个字符\uD83D
和\uDCA9
。
再看一个例子,“🚩
”的代理对是“\uD83D
”和“\uDEA9
”,“💩”的代理对是“\uD83D
”和“\uDCA9
”:
1 | /[🚩]/.test('💩'); // true |
修饰符u
让 js 能正确理解 UTF-16,因为理解了编码,就导致了正则的其它功能也变得有预期了,例如.
和量词{n}
都能正常工作了,可以使用 unicode 属性了:
1 | /^.$/.test('💩'); // false |
为什么'💩'.length === 2
:
- js 对于编码的处理曾经是 UCS-2,因为在 js 被创造的时候,编码方式只有 UCS-2,作为 UCS-2 的超集 UTF-16 尚未诞生,ES6 之后使用 UTF-16;
- ES6 对于非 BMP(码点大于 65536 的字符)中的字符使用代理对的方式表示,BMP 中的字符只要两字节(16 比特)表示,非 BMP 的字符需要四字节(32 比特)表示,BMP 中保留了两个部分用于表示所有的非 BMP 字符,UCS-2 固定长,而 UTF-16 克变长;
- “
💩
”就是非 BMP 中的字符,它的码点是 128169,已经超出了 BMP 的范围,js 会使用代理对表示,它的代理对是“\uD83D\uDCA9
”,这可以通过 128169 计算,也可以通过'💩'.split('')
得到; - 基于前面的事实,推测 js 的单元长度(1)是固定的 2 字节,即 UCS-2 单个字符的大小,由于 UTF-16 使用 4 字节(代理对)来表示范围更大的字符,所以长度被计算为 2。
JavaScript 中一定程度上正确获取字符串长度的方法:
1 | var string = "😄😂😣😭🥵😘"; |
仍然有很多 emoji 不能通过以上的方式正确获取长度,例如“👶🏻👦🏻👧🏻👨🏻👩🏻👱🏻♀️👱🏻👴🏻👵🏻👲🏻👳🏻♀️👳🏻👮🏻♀️👮🏻👷🏻♀️👷🏻💂🏻♀️💂🏻🕵🏻♀️👩🏻⚕️👨🏻⚕️👩🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾👨🏻🌾”的长度应该是 33,但是不能通过以上的方法得到正确结果。
一些 js 代码示例:
1 | /^.$/.test('💩'); // false |
一些 python 代码示例:
1 | import re |
码位(code points)和代理对(surrogate pairs)的转换:
1 | // 码位 C 转换成代理对<H, L> |
如何证明是浏览器在渲染的时候组合了 unicode 字符?
1 | // 首先执行,屏幕出现“问号”符号 |
关于编码的两个方法:
- 字符串变码点,codePointAt
- 码点变字符串,fromCodePoint
检测正则支持 u 修饰符吗:
1 | function hasRegExpU() { |
相关链接:
- Unicode与JavaScript详解——阮一峰文章;
- 正则的扩展——阮一峰文章;
- 字符编码笔记:ASCII,Unicode 和 UTF-8——待读;
- 硬核基础编码篇(一)烫烫烫烫烫烫——掘金系列文章,硬核基础编码篇(二)ascii、unicode 和 utf-8,淦,为什么 “𠮷𠮷𠮷”.length !== 3;
- Unicode —— 字符串内幕;
- 中点(·)在UTF-8和GBK转换中的问题——不同类型的中点号;
- 正则表达式校验汉字——总结了
\p{sc=Han}
和\u4e00-\u9fd5
的区别; - 字节序探析:大端与小端的比较——阮一峰文章;
- JavaScript’s internal character encoding: UCS-2 or UTF-16?——JavaScript 内部编码是 UCS-2 还是 UTF-16?SF 上的一篇中译文章可以查看;
- 零宽连字——
\u200D
,用于组合字符,https://codepoints.net/U+200D; - “💩”.length === 2——英文,介绍了长度大于 2 的 emoji 字符;
- Unicode symbol as text or emoji——介绍了变体选择器,改变 emoji 外观;
- “👩💻🎉”.length = 7 ??? How to count emojis with Javascript——提供了正确获取 emoji 长度的方法;
- 为什么 utf8没有字节序,utf16、utf32有字节序——解释了为什么 UTF-8 没有 BOM。
性能
由于 JavaScript 不支持占有型量词(Possessive Quantifiers),因此使用前瞻变换(lookahead transform)来模拟。
1 | var reg = /^((?=(\w+))\2\s?)*$/; // 提高可读性:/^((?=(?<word>\w+))\k<word>\s?)*$/; |
常见导致回溯的代码:
1 | // (x+x+)+y |
如果要匹配字符串开头,锚点的性能低于语言提供的“startsWith”之类方法:
1 | /^starts/.test("starts with sugar"); // true |
相关链接:
- 浅谈正则表达式原理——“abcd”使用
(.*)+\d
导致回溯的例子; - Catastrophic Backtracking——RegexBuddy 发布的文章,RegexBuddy 是一个可以查看匹配回溯情况的 windows 独占的正则软件。
可读性和性能
提取一个多选正则会提高性能,但是降低了可读性:
1 | var reg1 = /(this|that|these|those)/; |
捕获组会保存匹配内容,使用非捕获组会提高性能,但是降低了可读性:
1 | var reg1 = /(content)/; |
特殊的地方
如果要删除多行字符串的头尾空格,不能使用(^\s|\s$)
。
匹配 URL
- 检查合法性;
- 合并参数;
- 匹配参数。
检查合法性:
1 | var reg = |
获取 url 的每个部分:
1 | var reg = /(([^:\/?#]+):)?(\/\/([^\/?#]+))?([^?#]*)?(\?([^#]*))?(#(.*))?/; |
获取 url 的参数(query string):
1 | // 使用正则 |
URL 的各种形式:
1 | // 用 HTTP 协议访问 Web 服务器 |
相关链接:
- In search of the perfect URL validation regex——用案例表格衡量匹配 URL 的正则的准确度,还包括了正则的长度;
- URLSearchParams——MDN 文档;
- Getting parts of a URL (Regex);
- Capture value out of query string with regex?;
- What is the best regular expression to check if a string is a valid URL?;
- 关于URL编码。
示例
相关链接:
练习
检查是否质数(质数是除了 1 和本身,不能被其它数整除的数(没有 1 和本身以外的其它因数)):
1 | function numberSticks(n) { |
24 小时制:
1 | var reg1 = /[0-1]\d|2[0-3]/; // 方法 1 |
千分位:
1 | var reg = /(?!^)(?=(\d{3})+$)/g; // (?!) 表示否定前瞻断言,(?=)表示前瞻断言 |
URL query 参数获取:
1 | function query(name) |
删除头尾空格:
1 | function zTrim(z) |
补全非自闭合标签:
1 | const tags = /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; // 这些是自闭合标签 |
中线转驼峰:
1 | function style(element, name, value) |
把带符号的号码转换成数字:
1 | var str = "+7(903)-123-45-67"; |
提取字符串里的中文:
1 | var str = "你好吗,David?"; |
匹配空字符串:
1 | // 锚点表示位置,对于空字符串,开头就是结尾,结尾就是开头 |
查找时间,从给定的字符串中找到时间:
1 | // https://zh.javascript.info/task/find-time-hh-mm |
URL 转对象(对象转 URL,压缩 URL)
- 检查正则正确性;
- 默认对象;
- 重复参数的数组对象。
TODO
1 | // var X = '[a-z0-9]' |
url 参数转对象
非贪婪匹配,?
,例如/.+?\s/.exec("Your time is limited.")
匹配“Your ”
其它
关于回车换行:历史上使用机械打字机打字,添加字符就向后移动打印头(carriage),到了行末就必须把打印头移到开头,于是进行“回车”,回车后纸张也要向上移动,防止新字重叠,于是“换行”。
双引号会消耗斜杠,因为双引号中斜杠是转义符号,是有特殊意义的,同样在正则字面量中的斜杠也是有特殊意义的,因此从正则字面量/.../
转向用new RegExp("...")
来表示正则,其中的斜杠要转义两次。
1 | var reg = new RegExp("\\\\"); // 相当于正则字面量 /\\/,也就是一个斜杠字符 |
正则相关的方法:
- 字符串——search、match、replace、split;
- 正则——exec、test。
引用
资料:
- iHateRegex——一个常用正则表达式网站(“Now you have two problems.”);
- JS正则表达式完整教程(略长)——正则迷你书;
- oschina 正则表格;
- regex101: build, test, and debug regex——正则表达式校验网站;
- JAVASCRIPT.INFO 正则表达式—— JAVASCRIPT.INFO 的正则表达式教程;
- 正则表达式——MDN 的正则教程;
- 正则的扩展——阮一峰《ECMAScript 6 入门》;
- RegexLearn——交互式学习正则网站,中文。
书籍:
相关链接:
- javascript正则深入以及10个非常有意思的正则实战
- 实现一个简单的模板引擎
- 网站 regexper——以图形形式展示正则;
- 老姚丨洞见生产者——常发布正则相关文章的知乎账户;
- 正则表达式案例分析 (一);
- 🍩想要白嫖正则是吧?这一次给你个够!——案例列表;
- 🦕正则大全——案例列表;
- 日常开发提升效率之常用正则实例——案例列表;
- 正则扩展练习——Linux 里的正则;
- regex 101——正则可视化,当匹配失败的时候,可以查看匹配步骤,查看正则回溯的过程;
- Multi-line Regex In Javascript with Comments——给正则加注释,
new RegExp(parts.map(x => (x instanceof RegExp) ? x.source : x).join(''));
。