# 正则表达式 **Repository Path**: aronyang/regular-expression- ## Basic Information - **Project Name**: 正则表达式 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-05-26 - **Last Updated**: 2021-05-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 正则表达式 正则表达式是搜索和替换字符串的一种强大方式。 在 JavaScript 中,正则表达式通过内置的“RegExp”类的对象来实现,并与字符串集成。 > 请注意,在各编程语言之间,正则表达式是有所不同的。 ## 模式(Patterns)和修饰符(flags) 正则表达式(可叫作“regexp”或者“reg”)包含 **模式** 和可选的 **修饰符**。 ### 创建一个正则表达式 创建一个正则表达式对象有两种语法。 **较长一点的语法:** ```javascript regexp = new RegExp("pattern", "flags"); ``` **较短一点的语法**,使用斜杠 `"/"`: ```javascript regexp = /pattern/; // 没有修饰符 regexp = /pattern/gmi; // 伴随修饰符 g、m 和 i(后面会讲到) ``` 斜杠 `"/"` 会告诉 JavaScript 我们正在创建一个正则表达式。它的作用类似于字符串的引号。 ### 用法 如果要在字符串中进行搜索,可以使用 [search](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/String/search) 方法。 下面是示例: ```javascript let str = "I love JavaScript!"; // 将在这里搜索 let regexp = /love/; alert( str.search(regexp) ); // 2 ``` `str.search` 方法会查找模式 `/love/`,然后返回匹配项在字符串中的位置。我们可以猜到,`/love/` 是最简单的模式。它所做的就是简单的子字符串的查找。 上面的代码等同于: ```javascript let str = "I love JavaScript!"; // 将在这里搜索 let substr = 'love'; alert( str.search(substr) ); // 2 ``` 所以搜索 `/love/` 与搜索 `"love"` 是等价的。 但这只是暂时的。很快我们就会接触更复杂的正则表达式,其搜索功能将更强大。 > :evergreen_tree: : **配色** > >本文中的配色方案如下: > >- regexp – `red` >- string(我们要搜索的)-- `blue` >- result – `green` > :evergreen_tree: **什么时候使用 `new RegExp`?** > > 通常我们使用的都是简短语法 `/.../`。但是它不接受任何变量插入,所以我们必须在写代码的时候就知道确切的 regexp。 > > 另一方面,`new RegExp` 允许从字符串中动态地构造模式。 > > 所以我们可以找出需要搜索的字段,然后根据搜索字段创建 `new RegExp`: > > ```javascript > let search = prompt("What you want to search?", "love"); > let regexp = new RegExp(search); > > // 找到用户想要的任何东西 > alert( "I love JavaScript".search(regexp)); > ``` #### 修饰符 正则表达式的修饰符可能会影响搜索结果。 在 JavaScript 中,有 5 个修饰符: | 修饰符 | 作用 | | :----: | ------------------------------------------------------------ | | `i` | 使用此修饰符后,搜索时不区分大小写: `A` 和 `a` 没有区别(具体看下面的例子)。 | | `g` | 使用此修饰符后,搜索时会查找所有的匹配项,而不只是第一个(在下一章会讲到)。 | | `m` | 多行模式(详见章节 文章 "regexp-multiline" 未找到)。 | | `u` | 开启完整的 unicode 支持。该修饰符能够修正对于代理对的处理。更详细的内容见章节 [Unicode:修饰符 “u” 和 class \p{...}](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_009.html#regexp-unicode)。 | | `y` | 粘滞模式(详见 [下一章节](https://zh.javascript.info/regexp-methods#y-flag)) | #### “i”修饰符 最简单的修饰符就是 `i` 了,使用此修饰符后,搜索时不区分大小 示例代码如下: ```javascript let str = "I love JavaScript!"; alert( str.search(/LOVE/) ); // -1(没找到) alert( str.search(/LOVE/i) ); // 2 ``` 1. 第一个搜索返回的是 `-1`(也就是没找到),因为搜索默认是区分大小写的。 2. 使用修饰符 `/LOVE/i`,在字符串的第 2 个位置上搜索到了 `love`。 相比与简单的子字符串查找,`i` 修饰符已经让正则表达式变得更加强大了。 ### 常用方法 #### str.search() `search()` 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。 如果没有找到任何匹配的子串,则返回 -1。 **语法** `str.search(regexp)` **参数值** | 参数 | 描述 | | :------------ | :--------------------------------- | | *searchvalue* | 必须。查找的字符串或者正则表达式。 | **返回值** | 类型 | 描述 | | :----- | :----------------------------------------------------------- | | Number | 与指定查找的字符串或者正则表达式相匹配的 String 对象起始位置。 | **例子:** ```js var str="Visit Runoob!"; var n=str.search("Runoob"); // 6 var str="Mr. Blue has a blue house"; document.write(str.search(/blue/i)); // 4 ``` #### str.match() `match()` 方法可在字符串内检索指定的值,以找到一个或多个与 regexp 匹配的文本 **语法** `string.match(regexp)` **参数值** | 参数 | 描述 | | :------- | :----------------------------------------------------------- | | *regexp* | 必需。规定要匹配的模式的 RegExp 对象。如果该参数不是 RegExp 对象,则需要首先把它传递给 RegExp 构造函数,将其转换为 RegExp 对象。 | **返回值** | 类型 | 描述 | | :---- | :----------------------------------------------------------- | | Array | 存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。 如果没找到匹配结果返回 *null* 。 | **例子:** ```js var str="The rain in SPAIN stays mainly in the plain"; var n=str.match(/ain/gi); // ["ain","AIN","ain","ain"] ``` #### str.repalce() replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。 **语法** `string.replace(searchvalue,newvalue)` **参数值** | 参数 | 描述 | | :------------ | :----------------------------------------------------------- | | *searchvalue* | 必须。规定子字符串或要替换的模式的 RegExp 对象。 请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象。 | | *newvalue* | 必需。一个字符串值。规定了替换文本或生成替换文本的函数。 | **返回值** | 类型 | 描述 | | :----- | :----------------------------------------------------------- | | String | 一个新的字符串,是用 replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的。 | **例子:** ```js var str="Visit Microsoft! Visit Microsoft!"; var n=str.replace("Microsoft","Runoob"); // Visit Runoob!Visit Microsoft! var str="Mr Blue has a blue house and a blue car"; var n=str.replace(/blue/g,"red"); // Mr Blue has a red house and a red car ``` #### RegExpObject.test() test() 方法用于检测一个字符串是否匹配某个模式,如果字符串中有匹配的值返回 true ,否则返回 false。 `RegExpObject.test(string)` | 参数 | 描述 | | :----- | :--------------------- | | string | 必需。要检测的字符串。 | **例子:** ```js var str="Hello world!"; // 查找"Hello" var patt=/Hello/g; var result=patt.test(str); document.write("返回值: " + result); // 返回值: true // 查找 "Runoob" patt=/Runoob/g; result=patt.test(str); document.write("
返回值: " + result); // 返回值: false // Javascript 判断是移动端浏览器还是 PC 端浏览器: if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { document.write("移动") } else { document.write("PC") } ``` ### 总结 - 一个正则表达式包含模式和可选修饰符:`g`、`i`、`m`、`u`、`y`。 - 如果不使用我们在后面将要学到的修饰符和特殊标志,正则表达式的搜索就等同于子字符串查找。 - `str.search(regexp)` 方法返回的是找到的匹配项的索引位置,如果没找到则返回 `-1`。 ## 字符类 考虑一个实际的任务 – 我们有一个电话号码,例如 `"+7(903)-123-45-67"`,我们需要将其转换为纯数字:`79035419441`。 为此,我们可以查找并删除所有非数字的内容。字符类可以帮助解决这个问题。 **字符类(Character classes)** 是一个特殊的符号,匹配特定集中的任何符号。 ### 数字类 首先,让我们探索“数字”类。它写为 `\d`,对应于“任何一个数字”。 例如,让我们找到电话号码的第一个数字: ```javascript let str = "+7(903)-123-45-67"; let regexp = /\d/; alert( str.match(regexp) ); // 7 ``` 如果没有标志 `g`,则正则表达式仅查找第一个匹配项,即第一个数字 `\d`。 添加 `g`标志可以查找所有数字: ```javascript let str = "+7(903)-123-45-67"; let regexp = /\d/g; alert( str.match(regexp) ); // array of matches: 7,9,0,3,1,2,3,4,5,6,7 // let's make the digits-only phone number of them: alert( str.match(regexp).join('') ); // 79035419441 ``` 还有其他字符类。 最常用的是: - `\d`(“d” 来自 “digit”) 数字:从 `0` 到 `9` 的字符。 - `\s`(“s” 来自 “space”) 空格符号:包括空格,制表符 `\t`,换行符 `\n` 和其他少数稀有字符,例如 `\v`,`\f` 和 `\r`。 - `\w`(“w” 来自 “word”) “单字”字符:拉丁字母或数字或下划线 `_`。非拉丁字母(如西里尔字母或印地文)不属于 `\w`。 例如,`\d\s\w`表示“数字”,后跟“空格字符”,后跟“单字字符”,例如 `1 a`。 **正则表达式可能同时包含常规符号和字符类。** 例如,`CSS\d` 匹配字符串 `CSS` 与后面的数字: ```javascript let str = "Is there CSS4?"; let regexp = /CSS\d/ alert( str.match(regexp) ); // CSS4 ``` 我们还可以使用许多字符类: ```javascript alert( "I love HTML5!".match(/\s\w\w\w\w\d/) ); // ' HTML5' ``` 匹配项(每个正则表达式字符类都有对应的结果字符): ![](img\匹配项.svg) ### 反向类 对于每个字符类,都有一个“反向类”,用相同的字母表示,但要以大写书写形式。 “反向”表示它与所有其他字符匹配,例如: - `\D` 非数字:除 `\d` 以外的任何字符,例如字母。 - `\S` 非空格符号:除 `\s` 以外的任何字符,例如字母。 - `\W` 非单字字符:除 `\w` 以外的任何字符,例如非拉丁字母或空格。 在这一章的开头,我们看到了如何从 `+7(903)-123-45-67` 这样的字符串中创建一个只包含数字的电话号码: 找到所有的数字并将它们连接起来。 ```javascript let str = "+7(903)-123-45-67"; alert( str.match(/\d/g).join('') ); // 79031234567 ``` 另一种快捷的替代方法是查找非数字 `\D` 并将其从字符串中删除: ```javascript let str = "+7(903)-123-45-67"; alert( str.replace(/\D/g, "") ); // 79031234567 ``` ### 点(.)是匹配“任何字符” 点 `.` 是一种特殊字符类,它与 “除换行符之外的任何字符” 匹配。 例如: ```javascript alert( "Z".match(/./) ); // Z ``` 或在正则表达式中间: ```javascript let regexp = /CS.4/; alert( "CSS4".match(regexp) ); // CSS4 alert( "CS-4".match(regexp) ); // CS-4 alert( "CS 4".match(regexp) ); // CS 4 (space is also a character) ``` 请注意,点表示“任何字符”,而不是“缺少字符”。必须有一个与之匹配的字符: ```javascript alert( "CS4".match(/CS.4/) ); // null, no match because there's no character for the dot ``` ### 带有“s”标志时点字符类严格匹配任何字符 默认情况下,点与换行符 `\n` 不匹配。 例如,正则表达式 `A.B` 匹配 `A`,然后匹配 `B` 和它们之间的任何字符,除了换行符`\n`: ```javascript alert( "A\nB".match(/A.B/) ); // null (no match) ``` 在许多情况下,当我们希望用点来表示“任何字符”(包括换行符)时。 这就是标志 `s` 所做的。如果有一个正则表达式,则点 `.` 实际上匹配任何字符: ```javascript alert( "A\nB".match(/A.B/s) ); // A\nB (match!) ``` > ❗**不支持 Firefox、IE、Edge** > > 使用前可从 https://caniuse.com/#search=dotall 确认以获得最新的支持状态。在撰写本文时,它不包括 Firefox、IE、Edge。 > > 幸运的是,有一种替代方法可以在任何地方使用。我们可以使用诸如 `[\s\S]` 之类的正则表达式来匹配“任何字符”。 > > ```javascript > alert( "A\nB".match(/A[\s\S]B/) ); // A\nB (match!) > ``` > > 模式 `[\s\S]` 从字面上说:“空格字符或非空格字符”。换句话说,“任何东西”。我们可以使用另一对互补的类,例如 `[\d\D]`。甚至是 `[^]` —— 意思是匹配任何字符,除了什么都没有。 > > 如果我们希望两种“点”都使用相同的模式,也可以使用此技巧:实际的点 `.` 具有常规方式(“不包括换行符”)以及一种使用 `[\s\S]` 或类似形式匹配“任何字符”。 > ❗**注意空格** > > 通常我们很少注意空格。对我们来说,字符串 `1-5` 和 `1 - 5` 几乎相同。 > > 但是,如果正则表达式未考虑空格,则可能无法正常工作。 > > 让我们尝试查找由连字符(-)分隔的数字: > > ```javascript > alert( "1 - 5".match(/\d-\d/) ); // null, no match! > ``` > > 让我们修复一下,在正则表达式中添加空格:\ d-\ d`: > > ```javascript > alert( "1 - 5".match(/\d - \d/) ); // 1 - 5, now it works > // or we can use \s class: > alert( "1 - 5".match(/\d\s-\s\d/) ); // 1 - 5, also works > ``` > > **空格是一个字符。与其他字符同等重要。** > > 我们无法在正则表达式中添加或删除空格,并且期望能正常工作。 > > 换句话说,在正则表达式中,所有字符都很重要,空格也很重要。 ### 总结 | 字符类 | 匹配 | | :----: | ------------------------------------------------------------ | | \d | 数字 | | \D | 非数字 | | \s | 空格符号,制表符,换行符 | | \S | 除了 \s | | \w | 拉丁字母,数字,下划线 '_' | | . | 任何字符(不包括换行符`\n`),带有`s`标志时点字符类严格匹配任何字符 | ……但这还不是全部! JavaScript 用于字符串的 Unicode 编码提供了许多字符属性,例如:这个字母属于哪一种语言(如果它是一个字母)?它是标点符号吗?等等。 我们也可以通过这些属性进行搜索。这需要标志 `u`,在下一篇文章中介绍。 ## Unicode:修饰符 “u” 和 class \p{...} JavaScript 使用 [Unicode 编码](https://en.wikipedia.org/wiki/Unicode) (Unicode encoding)对字符串进行编码。大多数字符使用 2 个字节编码,但这种方式只能编码最多 65536 个字符。 这个范围不足以对所有可能的字符进行编码,这就是为什么一些罕见的字符使用 4 个字节进行编码,比如 `𝒳` (数学符号 X)或者 `😄` (笑脸),一些象形文字等等。 以下是一些字符对应的 unicode 编码: | 字符 | Unicode | unicode 中的字节数 | | :--- | :-------- | :----------------- | | a | `0x0061` | 2 | | ≈ | `0x2248` | 2 | | 𝒳 | `0x1d4b3` | 4 | | 𝒴 | `0x1d4b4` | 4 | | 😄 | `0x1f604` | 4 | 所以像 `a` 和 `≈` 这样的字符占用 2 个字节,而 `𝒳`,`𝒴` 和 `😄` 的对应编码则更长,它们具有 4 个字节的长度。 很久以前,当 JavaScript 被发明出来的时候,Unicode 的编码要更加简单:当时并没有 4 个字节长的字符。所以,一部分语言特性在现在仍旧无法对 unicode 进行正确的处理。 比如 `length` 认为这里的输入有 2 个字符: ```javascript alert('😄'.length); // 2 alert('𝒳'.length); // 2 ``` …但我们可以清楚地认识到输入的字符只有一个,对吧?关键在于 `length` 把 4 个字节当成了 2 个 2 字节长的字符。这是不对的,因为它们必须被当作一个整体来考虑。(即所谓的“代理伪字符”(surrogate pair),你可以在这里进一步阅读有关的的信息 [字符串](https://zh.javascript.info/string))。 默认情况下,正则表达式同样把一个 4 个字节的“长字符”当成一对 2 个字节长的字符。正如在字符串中遇到的情况,这将导致一些奇怪的结果。我们将很快在后面的文章中遇到 [集合和范围 [...\]](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_009.html#regexp-character-sets-and-ranges)。 与字符串有所不同的是,正则表达式有一个修饰符 `u` 被用以解决此类问题。当一个正则表达式使用这个修饰符后,4 个字节长的字符将被正确地处理。同时也能够用上 Unicode 属性(Unicode property)来进行查找了。我们接下来就来了解这方面的内容。 ### Unicode 属性(Unicode properties)\p{…} > **在 Firefox 和 Edge 中缺乏支持** > > 尽管 unicode property 从 2018 年以来便作为标准的一部分, 但 unicode 属性在 Firefox ([bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1361876)) 和 Edge ([bug](https://github.com/Microsoft/ChakraCore/issues/2969)) 中并没有相应的支持。 > > 目前 [XRegExp](http://xregexp.com/) 这个库提供“扩展”的正则表达式,其中包括对 unicode property 的跨平台支持。 Unicode 中的每一个字符都具有很多的属性。它们描述了一个字符属于哪个“类别”,包含了各种关于字符的信息。 例如,如果一个字符具有 `Letter` 属性,这意味着这个字符归属于(任意语言的)一个字母表。而 `Number` 属性则表示这是一个数字:也许是阿拉伯语,亦或者是中文,等等。 我们可以查找具有某种属性的字符,写作 `\p{…}`。为了顺利使用 `\p{…}`,一个正则表达式必须使用修饰符 `u`。 举个例子,`\p{Letter}` 表示任何语言中的一个字母。我们也可以使用 `\p{L}`,因为 `L` 是 `Letter` 的一个别名(alias)。对于每种属性而言,几乎都存在对应的缩写别名。 在下面的例子中 3 种字母将会被查找出:英语、格鲁吉亚语和韩语。 ```javascript let str = "A ბ ㄱ"; alert( str.match(/\p{L}/gu) ); // A,ბ,ㄱ alert( str.match(/\p{L}/g) ); // null(没有匹配的文本,因为没有修饰符“u”) ``` 以下是主要的字符类别和它们对应的子类别: | 字符类别 | 子类别 | | ------------------------------ | ------------------------------------------------------------ | | 字母(Letter) `L` | 小写(lowercase) `Ll`
修饰(modifier) `Lm`,
首字母大写(titlecase) `Lt`,
大写(uppercase) `Lu`,
其它(other) `Lo`。 | | 数字(Number) `N` | 十进制数字(decimal digit) `Nd`
字母数字(letter number) `Nl`
其它(other) `No`。 | | 标点符号(Punctuation) `P` | 链接符(connector) `Pc`,
横杠(dash) `Pd`,
起始引用号(initial quote) `Pi`
结束引用号(final quote) `Pf`
开(open) `Ps`,
闭(close) `Pe`,
其它(other) `Po`。 | | 标记(Mark) `M` (accents etc) | 间隔合并(spacing combining) `Mc`
封闭(enclosing) `Me`
非间隔(non-spacing) `Mn` | | 符号(Symbol)`S` | 货币(currency) `Sc`
修饰(modifier) `Sk`
数学(math) `Sm`
其它(other) `So` | | 分隔符(Separator)`Z` | 行(line) `Zl`
段落(paragraph) `Zp`
空格(space) `Zs` | | 其它(Other) `C` | 控制符(control) `Cc`
式(format) `Cf`
分配(not assigned) `Cn`
私有(private use) `Co`
代理伪字符(surrogate) `Cs`。 | 因此,比如说我们需要小写的字母,就可以写成 `\p{Ll}`,标点符号写作 `\p{P}` 等等。 也有其它派生的类别,例如: - `Alphabetic` (`Alpha`), 包含了字母 `L`, 加上字母数字 `Nl` (例如 Ⅻ – 罗马数字 12),加上一些其它符号 `Other_Alphabetic` (`OAlpha`)。 - `Hex_Digit` 包括 16 进制数字 `0-9`,`a-f`。 - …等等 Unicode 支持相当数量的属性,列出整个清单需要占用大量的空间,因此在这里列出相关的链接: - 列出一个字符的所有属性 https://unicode.org/cldr/utility/character.jsp. - 按照属性列出所有的字符 https://unicode.org/cldr/utility/list-unicodeset.jsp. - 属性的对应缩写形式:https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt. - 以文本格式整理的所有 Unicode 字符,包含了所有的属性:https://www.unicode.org/Public/UCD/latest/ucd/. ### 实例:16 进制数字 举个例子,让我们来查找 16 进制数字,写作 `xFF` 其中 `F` 是一个 16 进制的数字(0…1 或者 A…F)。 一个 16 进制数字可以表示为 `\p{Hex_Digit}`: ```javascript let regexp = /x\p{Hex_Digit}\p{Hex_Digit}/u; alert("number: xAF".match(regexp)); // xAF ``` ### 实例:中文字符 让我们再来考虑中文字符。 有一个 unicode 属性 `Script` (一个书写系统),这个属性可以有一个值:`Cyrillic`,`Greek`,`Arabic`,`Han` (中文)等等,[这里是一个完整的列表](https://en.wikipedia.org/wiki/Script_(Unicode))。 为了实现查找一个给定的书写系统中的字符,我们需要使用 `Script=`,例如对于西里尔字符:`\p{sc=Cyrillic}`, 中文字符:`\p{sc=Han}`,等等。 ```javascript let regexp = /\p{sc=Han}/gu; // returns Chinese hieroglyphs let str = `Hello Привет 你好 123_456`; alert( str.match(regexp) ); // 你,好 ``` ### 实例:货币 表示货币的字符,例如 `$`,`€`,`¥`,具有 unicode 属性 `\p{Currency_Symbol}`,缩写为 `\p{Sc}`。 让我们使用这一属性来查找符合“货币,接着是一个数字”的价格文本: ```javascript let regexp = /\p{Sc}\d/gu; let str = `Prices: $2, €1, ¥9`; alert( str.match(regexp) ); // $2,€1,¥9 ``` 之后,在文章 [量词 `+,*,?` 和 `{n}`](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_009.html#regexp-quantifiers) 中我们将会了解如何查找包含很多位的数字。 ### 总结 修饰符 `u` 在正则表达式中提供对 Unicode 的支持。 这意味着两件事: 1. 4 个字节长的字符被以正确的方式处理:被看成单个的字符,而不是 2 个 2 字节长的字符。 2. Unicode 属性可以被用于查找中 `\p{…}`。 有了 unicode 属性我们可以查找给定语言中的词,特殊字符(引用,货币)等等。 ## 锚点(Anchors):字符串开始 ^ 和末尾 $ 插入符号 `^` 和美元符号 `$` 在正则表达式中具有特殊的意义。它们被称为“锚点”。 插入符号 `^` 匹配文本开头,而美元符号 `$` - 则匹配文本末尾。 举个例子,让我们测试一下文本是否以 `Mary` 开头: ```javascript let str1 = "Mary had a little lamb"; alert( /^Mary/.test(str1) ); // true ``` 该模式 `^Mary` 的意思是:字符串开始,接着是 “Mary”。 与此类似,我们可以用 `snow$` 来测试文本是否以 `snow` 结尾: ```javascript let str1 = "it's fleece was white as snow"; alert( /snow$/.test(str1) ); // true ``` 在以上这些具体的例子中我们实际上可以用 `startsWith/endsWith` 来代替。正则表达式应该被用于更加复杂的测试中。 ### 测试完全匹配 这两个锚点 `^...$` 放在一起常常被用于测试一个字符串是否完全匹配一个模式。比如,测试用户的输入是否符合正确的格式。 让我们测试一下一个字符串是否属于 `12:34` 格式的时间。即,两个数字,然后一个冒号,接着是另外两个数字。 用正则表达式来表示就是 `\d\d:\d\d`: ```js let goodInput = "12:34"; let badInput = "12:345"; let regexp = /^\d\d:\d\d$/; alert( regexp.test(goodInput) ); // true alert( regexp.test(badInput) ); // false ``` 在这个例子中 `\d\d:\d\d` 所对应的匹配文本必须正好在文本开头 `^` 之后,而在这之后必须紧跟文本末尾 `$`。 整个字符串必须准确地符合这一个格式。如果其中有任何偏差或者额外的字符,结果将为 `false`。 当修饰符 `m` 出现时,锚点将会有不同的行为。我们将在后面学习到 > **锚点具有“零宽度”** > > 锚点 `^` 和 `$` 属于测试。它们的宽度为零。 > > 换句话来说,它们并不匹配一个具体的字符,而是让正则引擎测试所表示的条件(文本开头/文本末尾)。 ## Flag "m" — 多行模式 通过 flag `/.../m` 可以开启多行模式。 这仅仅会影响 `^` 和 `$` 锚符的行为。 在多行模式下,它们不仅仅匹配文本的开始与结束,还匹配每一行的开始与结束。 ### 行的开头 ^ 在这个有多行文本的例子中,正则表达式 `/^\d+/gm` 将匹配每一行的开头数字: ```javascript let str = `1st place: Winnie 2nd place: Piglet 33rd place: Eeyore`; alert( str.match(/^\d+/gm) ); // 1, 2, 33 // \d+ 匹配一个或多个数字 ``` 没有 flag `/.../m` 时,仅仅是第一个数字被匹配到: ```javascript let str = `1st place: Winnie 2nd place: Piglet 33rd place: Eeyore`; alert( str.match(/^\d+/g) ); // 1 ``` 这是因为默认情况下,锚符 `^` 仅仅匹配文本的开头,在多行模式下,它匹配行的开头。 正则表达式引擎将会在文本中查找以锚符 `^` 开始的字符串,我们找到之后继续匹配 `\d+` 模式。 ### 行的结尾 $ 美元符 `$` 行为也相似。 正则表达式 `\w+$ 会找到每一行的最后一个单词: ```javascript let str = `1st place: Winnie 2nd place: Piglet 33rd place: Eeyore`; alert( str.match(/\w+$/gim) ); // Winnie,Piglet,Eeyore ``` 没有 `/.../m` flag 的话,美元符 `$` 将会仅仅匹配整个文本的结尾,所以只有最后的一个单词会被找到。 ### 锚符 ^$ 对比 \n 要寻找新的一行的话,我们不仅可以使用锚符 `^` 和 `$`,也可以使用换行符 `\n`。 它和锚符 `^` 和 `$` 的第一个不同点是它不像锚符那样,它会“消耗”掉 `\n` 并且将其(`\n`)加入到匹配结果中。 举个例子,我们在下面的代码中用它来替代 `$`: ```javascript let str = `1st place: Winnie 2nd place: Piglet 33rd place: Eeyore`; alert( str.match(/\w+\n/gim) ); // Winnie\n,Piglet\n ``` 这里,我们每次匹配到的时候都会被添加一个换行符。 还有一个不同点——换行符 `\n` 不会匹配字符串结尾。这就是为什么在上面的例子中 `Eeyore` 没有匹配到。 所以,通常情况下使用锚符更棒,用它匹配出来的结果更加接近我们想要的结果。 ## 词边界:\b 词边界 `\b` 是一种检查,就像 `^` 和 `$` 一样。 当正则表达式引擎(实现搜索正则表达式的程序模块)遇到 `\b` 时,它会检查字符串中的位置是否是词边界。 有三种不同的位置可作为词边界: - 在字符串开头,如果第一个字符是单词字符 `\w`。 - 在字符串中的两个字符之间,其中一个是单词字符 `\w`,另一个不是。 - 在字符串末尾,如果最后一个字符是单词字符 `\w`。 例如,可以在 `Hello, Java!` 中找到匹配 `\bJava\b` 的单词,其中 `Java` 是一个独立的单词,而在 `Hello, JavaScript!` 中则不行。 ```js alert( "Hello, Java!".match(/\bJava\b/) ); // Java alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null ``` 在字符串 `Hello, Java!` 中,以下位置对应于 `\b`: 因此,它与模式 `\bHello\b` 相匹配,因为: 1. 字符串的开头符合第一种检查 `\b`。 2. 然后匹配了单词 `Hello`。 3. 然后与 `\b` 再次匹配,因为我们在 `o` 和一个空格之间。 模式 `\bJava\b` 也同样匹配。但 `\bHell\b`(因为 `l` 之后没有词边界)和 `Java!\b`(因为感叹号不是单词 `\w`,所以其后没有词边界)却不匹配。 ```javascript alert( "Hello, Java!".match(/\bHello\b/) ); // Hello alert( "Hello, Java!".match(/\bJava\b/) ); // Java alert( "Hello, Java!".match(/\bHell\b/) ); // null (no match) alert( "Hello, Java!".match(/\bJava!\b/) ); // null (no match) ``` `\b` 既可以用于单词,也可以用于数字。 例如,模式 `\b\d\d\b` 查找独立的两位数。换句话说,它查找的是两位数,其周围是与 `\w` 不同的字符,例如空格或标点符号(或文本开头/结尾)。 ```javascript alert( "1 23 456 78".match(/\b\d\d\b/g) ); // 23,78 alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56 ``` > ❗**边界 `\b` 不适用于非拉丁字母** > > 词边界测试 `\b` 检查位置的一侧是否匹配 `\w`,而另一侧则不匹配 “`\w`”。 > > 但是,`\w` 表示拉丁字母 `a-z`(或数字或下划线),因此此检查不适用于其他字符,如西里尔字母(cyrillic letters)或象形文字(hieroglyphs)。 ## 转义,特殊字符 正如我们所看到的,一个反斜杠 `"\"` 是用来表示匹配字符类的。所以它是一个特殊字符。 还存在其它的特殊字符,这些字符在正则表达式中有特殊的含义。它们可以被用来做更加强大的搜索。 这里是包含所有特殊字符的列表:`[ \ ^ $ . | ? * + ( )`。 现在并不需要尝试去记住它们 —— 当我们分别处理其中的每一个时,你自然而然就会记住它们。 ### 转义 如果要把特殊字符作为常规字符来使用,只需要在它前面加个反斜杠。 这种方式也被叫做“转义一个字符”。 比如说,我们需要找到一个点号 `'.'`。在一个正则表达式中一个点号意味着“除了换行符以外的任意字符”,所以如果我们想真正表示对“一个点号”查询的时候,可以在点号前加一个反斜杠。 ```javascript alert( "Chapter 5.1".match(/\d\.\d/) ); // 5.1 ``` 括号也是特殊字符,所以如果我们想要在正则中查找它们,我们应该使用 `\(`。下面的例子会查找一个字符串 `"g()"`: ```javascript alert( "function g()".match(/g\(\)/) ); // "g()" ``` 如果我们想查找反斜杠 `\`,我们就应该使用两个反斜杠来查找: ```javascript alert( "1\\2".match(/\\/) ); // '\' ``` ### 一个斜杠 斜杠符号 `'/'` 并不是一个特殊符号,但是它被用于在 Javascript 中开启和关闭正则匹配:`/...pattern.../`,所以我们也应该转义它。 下面是查询斜杠 `'/'` 的表达式: ```javascript alert( "/".match(/\//) ); // '/' ``` 从另一个方面看,如果使用另一种 `new RegExp` 方式就不需要转义斜杠: ```javascript alert( "/".match(new RegExp("/")) ); // '/' ``` ### 使用 new RegExp 创建正则实例 如果我们使用 `new RegExp` 来创建一个正则表达式实例,那么我们需要对其做一些额外的转义。 比如说,考虑下面的示例: ```javascript let reg = new RegExp("\d\.\d"); alert( "Chapter 5.1".match(reg) ); // null ``` 它并没有正常发挥作用,但是为什么呢? 原因就在于字符串转义规则。看下面的例子: ```javascript alert("\d\.\d"); // d.d ``` 在字符串中的反斜杠表示转义或者类似 `\n` 这种只能在字符串中使用的特殊字符。这个引用会“消费”并且解释这些字符,比如说: - `\n` —— 变成一个换行字符, - `\u1234` —— 变成包含该码位的 Unicode 字符, - 。。。其它有些并没有特殊的含义,就像 `\d` 或者 `\z`,碰到这种情况的话会把反斜杠移除。 所以调用 `new RegExp` 会获得一个没有反斜杠的字符串。 如果要修复这个问题,我们需要双斜杠,因为引用会把 `\\` 变为 `\`: ```javascript let regStr = "\\d\\.\\d"; alert(regStr); // \d\.\d (correct now) let regexp = new RegExp(regStr); alert( "Chapter 5.1".match(regexp) ); // 5.1 ``` ### Summary - 要在字面(意义)上搜索特殊字符 `[ \ ^ $ . | ? * + ( )`,我们需要在它们前面加上反斜杠 `\`(“转义它们”)。 - 如果我们在 `/.../` 内部(但不在 `new RegExp` 内部),还需要转义 `\`。 - 传递一个字符串(参数)给 `new RegExp` 时,我们需要双倍反斜杠 `\\`,因为字符串引号会消费其中的一个。 ## 集合和范围 [...] 在方括号 `[…]` 中的几个字符或者字符类意味着“**搜索给定的字符中的任意一个**”。 ### 集合 比如说,`[eao]` 意味着查找在 3 个字符 `'a'`、`'e'` 或者 `‘o’ 中的任意一个。 这被叫做一个**集合**。集合可以在正则表达式中和其它常规字符一起使用。 ```javascript // 查找 [t 或者 m],然后再匹配 “op” alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top" ``` 请注意尽管在集合中有多个字符,但它们在匹配中只会对应其中的一个。 所以下面的示例并不会匹配上: ```javascript // 查找 “V”,然后匹配 [o 或者 i],之后再匹配 “la” alert( "Voila".match(/V[oi]la/) ); // null,并没有匹配上 ``` 这个模式会做以下假设: - `V`, - 然后匹配其中的**一个字符** `[oi]`, - 然后匹配 `la`, 所以可以匹配上 `Vola` 或者 `Vila`。 ### 范围 方括号也可以包含**字符范围**。 比如说,`[a-z]` 会匹配从 `a` 到 `z` 范围内的字母,`[0-5]` 表示从 `0` 到 `5` 的数字。 在下面的示例中,我们会查询首先匹配 `"x"` 字符,再匹配两个数字或者位于 `A` 到 `F` 范围内的字符。 ```javascript alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF ``` `[0-9A-F]` 表示两个范围:它搜索一个字符,满足数字 `0` 到 `9` 或字母 `A` 到 `F`。 如果我们还想查找小写字母,则可以添加范围 `a-f`:`[0-9A-Fa-f]`。或添加标志 `i`。 我们也可以在 `[…]` 里面使用字符类。 例如,如果我们想要查找单词字符 `\w` 或连字符 `-`,则该集合为 `[\w-]`。 也可以组合多个类,例如 `[\s\d]` 表示 “空格字符或数字”。 > ❗**字符类是某些字符集的简写** > > 例如: > > - **\d** —— 和 `[0-9]` 相同, > - **\w** —— 和 `[a-zA-Z0-9_]` 相同, > - **\s** —— 和 `[\t\n\v\f\r ]` 外加少量罕见的 unicode 空格字符相同。 ### 示例:多语言 \w 由于字符类 `\w` 是简写的 `[a-zA-Z0-9_]`,因此无法找到中文象形文字,西里尔字母等。 我们可以编写一个更通用的模式,该模式可以查找任何语言中的文字字符。这很容易想到就 Unicode 属性:`[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]`。 让我们理解它。类似于 `\w`,我们在制作自己的一套字符集,包括以下 unicode 字符: - `Alphabetic` (`Alpha`) —— 字母, - `Mark` (`M`) —— 重读, - `Decimal_Number` (`Nd`) —— 数字, - `Connector_Punctuation` (`Pc`) —— 下划线 `'_'` 和类似的字符, - `Join_Control` (`Join_C`) —— 两个特殊代码 `200c` and `200d`,用于连字,例如阿拉伯语。 使用示例: ```javascript let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu; let str = `Hi 你好 12`; // finds all letters and digits: alert( str.match(regexp) ); // H,i,你,好,1,2 ``` 当然,我们可以编辑此模式:添加 unicode 属性或删除它们。文章 [Unicode:修饰符 “u” 和 class \p{...}](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_009.html#regexp-unicode) 中包含了更多 Unicode 属性的细节。 > ❗ **Edge 和 Firefox 不支持 Unicode 属性** > > Edge 和 Firefox 尚未实现 Unicode 属性 `p{…}`。如果确实需要它们,可以使用库 [XRegExp](http://xregexp.com/)。 > > 或者只使用我们想要的语言范围的字符,例如西里尔字母 `[а-я]`。 ### 排除范围 除了普通的范围匹配,还有类似 `[^…]` 的“排除”范围匹配。 它们通过在匹配查询的开头添加插入符号 `^` 来表示,它会匹配所有**除了给定的字符**之外的任意字符。 比如说: - `[^aeyo]` —— 匹配任何除了 `'a'`、`'e'`、`'y'` 或者 `'o'` 之外的字符。 - `[^0-9]` —— 匹配任何除了数字之外的字符,也可以使用 `\D` 来表示。 - `[^\s]` —— 匹配任何非空字符,也可以使用 `\S` 来表示。 下面的示例查询除了字母,数字和空格之外的任意字符: ```javascript alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @,. ``` ### 在 […] 中不转义 通常当我们的确需要查询点字符时,我们需要把它转义成像 `\.` 这样的形式。如果我们需要查询一个反斜杠,我们需要使用 `\\`。 在方括号表示中,绝大多数特殊字符可以在不转义的情况下使用: - 表示一个点符号 `'.'`。 - 表示一个加号 `'+'`。 - 表示一个括号 `'( )'`。 - 在开头或者结尾表示一个破折号(在这些位置该符号表示的就不是一个范围) `pattern:’-’。 - 在不是开头的位置表示一个插入符号(在开头位置该符号表示的是排除)`'^'`。 - 表示一个开口的方括号符号 `'['`。 换句话说,除了在方括号中有特殊含义的字符外,其它所有特殊字符都是允许不添加反斜杠的。 一个在方括号中的点符号 `"."` 表示的就是一个点字符。查询模式 `[.,]` 将会寻找一个为点或者逗号的字符。 在下面的示例中,`[-().^+]` 会查找 `-().^+` 的其中任意一个字符: ```javascript // 并不需要转义 let reg = /[-().^+]/g; alert( "1 + 2 - 3".match(reg) ); // 匹配 +,- ``` 但是如果你为了“以防万一”转义了它们,这也不会有任何问题: ```javascript //转义其中的所有字符 let reg = /[\-\(\)\.\^\+]/g; alert( "1 + 2 - 3".match(reg) ); // 仍能正常工作:+,- ``` #### 范围和标志“u” 如果集合中有代理对(surrogate pairs),则需要标志 `u` 以使其正常工作。 例如,让我们在字符串 `𝒳` 中查找 `[𝒳𝒴]`: : ```javascript alert( '𝒳'.match(/[𝒳𝒴]/) ); // 显示一个奇怪的字符,像 [?] //(搜索执行不正确,返回了半个字符) ``` 结果不正确,因为默认情况下正则表达式“不知道”代理对。 正则表达式引擎认为 `[𝒳𝒴]` —— 不是两个,而是四个字符: 1. `𝒳` `(1)` 的左半部分, 2. `𝒳` `(2)` 的右半部分, 3. `𝒴` `(3)` 的左半部分, 4. `𝒴` `(4)` 的右半部分。 我们可以看到它们的代码,如下所示: ```javascript for(let i=0; i<'𝒳𝒴'.length; i++) { alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500 }; ``` 因此,以上示例查找并显示了 `𝒳` 的左半部分。 如果我们添加标志 `u`,那么行为将是正确的: ```javascript alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳 ``` 当我们查找范围时也会出现类似的情况,就像 `[𝒳-𝒴]`。 如果我们忘记添加标志 `u`,则会出现错误: ```javascript '𝒳'.match(/[𝒳-𝒴]/); // 错误:无效的正则表达式 ``` 原因是,没有标志 `u` 的代理对被视为两个字符,因此 `[𝒳-𝒴]` 被解释为 `[<55349><56499>-<55349><56500>]`(每个代理对都替换为其代码)。现在很容易看出范围 `56499-55349` 是无效的:其起始代码 `56499` 大于终止代码 `55349`。这就是错误的原因。 使用标志 `u`,该模式可以正常匹配: ```javascript // 查找字符从 𝒳 到 𝒵 alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴 ``` ## 量词 `+,*,?` 和 `{n}` 假设我们有一个字符串 `+7(903)-123-45-67`,并且想要找到它包含的所有数字。但与之前不同的是,我们对单个数字不感兴趣,只对全数感兴趣:`7, 903, 123, 45, 67`。 数字是一个或多个 `\d` 的序列。用来形容我们所需要的数量的词被称为**量词**。 ### 数量 {n} 最明显的量词便是一对引号间的数字:`{n}`。在一个字符(或一个字符类等等)后跟着一个量词,用来指出我们具体需要的数量。 它有更高级的格式,用一个例子来说明: #### 确切的位数:`{5}` `\d{5}` 表示 5 位的数字,如同 `\d\d\d\d\d`。接下来的例子将会查找一个五位数的数字:`alert( "I'm 12345 years old".match(/\d{5}/) ); // "12345"`我们可以添加 `\b` 来排除更多位数的数字:`\b\d{5}\b`。 #### 某个范围的位数:`{3,5}` 我们可以将限制范围的数字放入括号中,来查找位数为 3 至 5 位的数字:`\d{3,5}``alert( "I'm not 12, but 1234 years old".match(/\d{3,5}/) ); // "1234"`我们可以省略上限。那么正则表达式 `\d{3,}` 就会查找位数大于或等于 3 的数字:`alert( "I'm not 12, but 345678 years old".match(/\d{3,}/) ); // "345678"` 对于字符串 `+7(903)-123-45-67` 来说,我们如果需要一个或多个连续的数字,就使用 `\d{1,}`: ```javascript let str = "+7(903)-123-45-67"; let numbers = str.match(/\d{1,}/g); alert(numbers); // 7,903,123,45,67 ``` ### 缩写 大多数常用的量词都可以有缩写: | 缩写 | 功能 | | :--: | ------------------------------------------------------------ | | `+` | 代表“一个或多个”,相当于 `{1,}`。例如,`\d+` 用来查找所有数字 | | `?` | 代表“零个或一个”,相当于 `{0,1}`。换句话说,它使得符号变得可选。 | | `*` | 代表着“零个或多个”,相当于 `{0,}`。也就是说,这个字符可以多次出现或不出现。 | **`+`** 代表“一个或多个”,相当于 `{1,}`。 例如,`\d+` 用来查找所有数字: ```javascript let str = "+7(903)-123-45-67"; alert( str.match(/\d+/g) ); // 7,903,123,45,67 ``` **`?`** 代表“零个或一个”,相当于 `{0,1}`。换句话说,它使得符号变得可选。 例如,模式 `ou?r` 查找 `o`,后跟零个或一个 `u`,然后是 `r`。 所以他能够在 `color` 中找到 `or`,以及在 `colour` 中找到 `our`: ```javascript let str = "Should I write color or colour?"; alert( str.match(/colou?r/g) ); // color, colour ``` **`*`** 代表着“零个或多个”,相当于 `{0,}`。也就是说,这个字符可以多次出现或不出现。 接下来的例子将要寻找一个后跟任意数量的 0 的数字: ```javascript alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1 ``` 将它与 `'+'`(一个或多个)作比较: ```javascript alert( "100 10 1".match(/\d0+/g) ); // 100, 10 ``` ### 更多示例 量词是经常被使用的。它们是构成复杂的正则表达式的主要模块之一,我们接着来看更多的例子。 **正则表达式“浮点数”(带浮点的数字):`\d+\.\d+`** 实现: ```javascript alert( "0 1 12.345 7890".match(/\d+\.\d+/g) ); // 12.345 ``` **正则表达式“打开没有属性的 HTML 标记”,比如 `` 或 `

`:`/<[a-z]+>/i`** 实现: ```javascript alert( " ... ".match(/<[a-z]+>/gi) ); // ``` 我们查找字符 `'<'` 后跟一个或多个英文字母,然后是 `'>'`。 **正则表达式“打开没有属性的HTML标记”(改进版):`/<[a-z][a-z0-9]*>/i`** 更好的表达式:根据标准,HTML 标记名称可以在除了第一个位置以外的任意一个位置有一个数字,比如 `

`。 ```javascript alert( "

Hi!

".match(/<[a-z][a-z0-9]*>/gi) ); //

``` **正则表达式“打开没有属性的HTML标记”:`/<\/?[a-z][a-z0-9]*>/i`** 我们在标记前加上了一个可选的斜杆 `/?`。必须用一个反斜杠来转义它,否则 JavaScript 就会认为它是这个模式的结束符。 ```javascript alert( "

Hi!

".match(/<\/?[a-z][a-z0-9]*>/gi) ); //

,

``` > 📑 **更精确意味着更复杂** > > 我们能够从这些例子中看到一个共同的规则:正则表达式越精确 —— 它就越长且越复杂。 > > 例如,HTML 标记能用一个简单的正则表达式:`<\w+>`。 > > 因为 `\w` 代表任意英文字母或数字或 `'_'`,这个正则表达式也能够匹配非标注的内容,比如 `<_>`。但它要比 `<[a-z][a-z0-9]*>` 简单很多。 > > 我们能够接受 `<\w+>` 或者我们需要 `<[a-z][a-z0-9]*>`? > > 在现实生活中,两种方式都能接受。取决于我们对于“额外”匹配的宽容程度以及是否难以通过其他方式来过滤掉它们。 ## 贪婪量词和惰性量词 量词,看上去十分简单,但实际上它可能会很棘手。 如果我们打算寻找比 `/\d+/` 更加复杂的东西,就需要理解搜索工作是如何进行的。 以接下来的问题为例。 有一个文本,我们需要用书名号:`«...»` 来代替所有的引号 `"..."`。在许多国家,它们是排版的首选。 例如:`"Hello, world"` 将会变成 `«Hello, world»`。 一些国家偏爱 `„Witam, świat!”`(波兰语)甚至 `「你好,世界」`(汉语)引号。对于不同的语言环境,我们可以选择不同的替代方式,但它们都是一样的,那我们就以书名号 `«...»` 开始。 为了进行替换,我们首先要找出所有被引号围起来的子串。 正则表达式看上去可能是这样的:`/".+"/g`。这个表达式的意思是:我们要查找这样一个句子,一个引号后跟一个或多个字符,然后以另一个引号结尾。 …但如果我们试着在一个如此简单的例子中去应用它… ```javascript let reg = /".+"/g; let str = 'a "witch" and her "broom" is one'; alert( str.match(reg) ); // "witch" and her "broom" ``` …我们会发现它的运行结果与预期不同! 它直接找到了一个匹配结果:`"witch" and her "broom"`,而不是找到两个匹配结果 `"witch"` 和 `"broom"`。 这可被称为“贪婪是万恶之源”。 ### 贪婪搜索 为了查找到一个匹配项,正则表达式引擎采用了以下算法: - 对于字符串中的每一个字符 - 用这个模式来匹配此字符。 - 若无匹配,移至下一个字符 这些简单的词语没有说清楚为什么这个正则表达式匹配失败了,因此,让我们详细说明一下模式 `".+"` 是如何进行搜索工作的。 1. 该模式的第一个字符是一个引号 `"`。 正则表达式引擎企图在字符串 `a "witch" and her "broom" is one` 的第一个位置就匹配到目标,但这个位置是 subject:a,所以匹配失败。 然后它进行下一步:移至字符串中的下一个位置,并试图匹配模式中的第一个字符,最终在第三个位置匹配到了引号: ![img](img\贪婪搜索1.svg) 2. 检测到了引号后,引擎就尝试去匹配模式中的剩余字符。它试图查看剩余的字符串主体是否符合 `.+"`。 在我们的用例中,模式中的下一个字符为 `.`(一个点)。它表示匹配除了换行符之外的任意字符,所以将会匹配下一个字符 `'w'`: ![img](img\贪婪搜索2.svg) 3. 然后因为量词 `.+`,模式中的点(.)将会重复。正则表达式引擎逐一读取字符,当该字符可能匹配时就用它来构建匹配项。 …什么时候会不匹配?点(.)能够匹配所有字符,所以只有在移至字符串末尾时才停止匹配: ![img](img\贪婪搜索3.svg) 4. 现在引擎完成了对重复模式 `.+` 的搜索,并且试图寻找模式中的下一个字符。这个字符是引号 `"`。但还有一个问题,对字符串的遍历已经结束,已经没有更多的字符了! 正则表达式引擎明白它已经为 `.+` 匹配了太多项了,所以开始**回溯**了。 换句话说,它去掉了量词的匹配项的最后一个字符: ![img](img\贪婪搜索4.svg) 现在它假设在结束前,`.+` 会匹配一个字符,并尝试匹配剩余的字符。 如果出现了一个引号,就表示到达了末尾,但最后一个字符是 `'e'`,所以无法匹配。 5. …所以引擎会再去掉一个字符,以此来减少 `.+` 的重复次数: ![img](img\贪婪搜索5.svg) `'"'` 并不会匹配 `'n'`。 6. 引擎不断进行回溯:它减少了 `'.'` 的重复次数,直到模式的其它部分(在我们的用例中是 `'"'`)匹配到结果: ![img](img\贪婪搜索6.svg) 7. 匹配完成。 8. 所以,第一次匹配是 `"witch" and her "broom"`。接下来的搜索的起点位于第一次搜索的终点,但在 `is one` 中没有更多的引号了,所以没有其它的结果了。 这可能不是我们所想要的,但这就是它的工作原理。 **在贪婪模式下(默认情况下),量词都会尽可能地重复多次。** 正则表达式引擎尝试用 `.+` 去获取尽可能多的字符,然后再一步步地筛选它们。 对于这个问题,我们想要另一种结果,这也就是懒惰量词模式的用途。 ### 懒惰模式 懒惰模式中的量词与贪婪模式中的是相反的。它想要“重复最少次数”。 我们能够通过在量词之后添加一个问号 `'?'` 来启用它,所以匹配模式变为 `*?` 或 `+?`,甚至将 `'?'` 变为 `??`。 这么说吧:通常,一个问号 `?` 就是一个它本身的量词(0 或 1),但如果添加**另一个量词(甚至可以是它自己)**,就会有不同的意思 —— 它将匹配的模式从贪婪转为懒惰。 正则表达式 `/".+?"/g` 正如预期工作:它找到了 `"witch"` 和 `"broom"`: ```javascript let reg = /".+?"/g; let str = 'a "witch" and her "broom" is one'; alert( str.match(reg) ); // witch, broom ``` 为了更清楚地理解这个变化,我们来一步步解析这个搜索过程。 1. 第一步依然相同:它在第三个位置开始 `'"'`: ![img](img\懒惰模式1.svg) 2. 下一步也是类似的:引擎为 `'.'` 找到了一个匹配项: ![img](img\懒惰模式2.svg) 3. 接下来就是搜索过程出现不同的时候了。因为我们对 `+?` 启用了懒惰模式,引擎不会去尝试多匹配一个点,并且开始了对剩余的 `'"'` 的匹配: ![img](img\懒惰模式3.svg) 如果有一个引号,搜索就会停止,但是有一个 `'i'`,所以没有匹配到引号。 4. 接着,正则表达式引擎增加对点的重复搜索次数,并且再次尝试: ![img](img\懒惰模式4.svg) 又失败了。然后重复次数一次又一次的增加… 5. …直到模式中的剩余部分找到匹配项: ![img](img\懒惰模式5.svg) 6. 接下来的搜索工作从当前匹配结束的那一项开始,就会再产生一个结果: ![img](img\懒惰模式6.svg) 在这个例子中,我们看到了懒惰模式 `+?` 是怎样工作的。量词 `*?` 和 `??` 也有类似的效果 —— 只有在模式的剩余部分无法在给定位置匹配时,正则表达式引擎才会增加重复次数。 **懒惰模式只能够通过带 `?` 的量词启用** 其它的量词依旧保持贪婪模式。 例如: ```javascript alert( "123 456".match(/\d+ \d+?/g) ); // 123 4 ``` 1. 模式 `\d+` 尝试匹配尽可能多的数字(贪婪模式),因此在它找到 `123` 时停止,因为下一个字符为空格 `' '`。 2. 匹配到一个空格。 3. 由于 `\d+?`。量词是出于懒惰模式的,所以它匹配一个数字 `4` 并且尝试去检测模式的剩余部分是否匹配。 。。。但是在 `\d+?` 之后没有其它的匹配项了。 懒惰模式不会在不必要的情况下重复任何事情。模式结束,所以我们找到了匹配项 `123 4`。 4. 接下来的搜索工作从字符 `5` 开始。 > 📑 **Optimizations** > > 当代的正则表达式引擎会通过优化内部算法来提升效率。所以它们的工作流程和所描述的算法可能略有不同。 > > 但如果只是为了理解正则表达式是如何工作以及如何构建的,我们不需要知道这些,它们仅用于内部优化。 > > 复杂的正则表达式是难以优化的,所以搜索的过程可能会完全按照描述进行。 ### 替代方法 在正则表达式中,通常有多种方法来达到某个相同目的。 在用例中,我们能够在不启用懒惰模式的情况下用 `"[^"]+"` 找到带引号的字符串: ```javascript let reg = /"[^"]+"/g; let str = 'a "witch" and her "broom" is one'; alert( str.match(reg) ); // witch, broom ``` `"[^"]+"` 得到了正确的答案,因为它查找一个引号 `'"'`,后跟一个或多个非引号字符 `[^"]`,然后是结束的引号。 当引擎寻找 `[^"]+` 时,它会在匹配到结束的引号时停止重复,这样就完成了。 请注意,这个逻辑并不能取代惰性量词! 这是不同的,我们有时需要这一个,有时却需要另一个。 让我们再来看一个使用惰性量词失败而使用这种方式正确的例子。 例如,我们想要找到 `` 形式的链接,或是任意 `href`。 该使用哪个正则表达式呢? 首先可能会想到:`//g`。 验证一下: ```javascript let str = '......'; let reg = //g; // Works! alert( str.match(reg) ); // ``` …但如果文本中有多个链接呢? ```javascript let str = '...... ...'; let reg = //g; // Whoops! Two links in one match! alert( str.match(reg) ); // ... ``` 现在这个结果和我们的 “witches” 用例结果的错误原因是一样的。量词 `.*` 占用太多字符了。 匹配结果如下: ```markup ... ``` 让我们启用惰性量词 `.*?` 来修改模式: ```javascript let str = '...... ...'; let reg = //g; // 有效! alert( str.match(reg) ); // , ``` 现在能成功了,有两个匹配项: ```markup ... ``` 它的工作原理是 —— 在上述的解释之后,这应该是显而易见的。所以我们不停留在这些细节上,来再尝试一个例子: ```javascript let str = '......

...'; let reg = //g; // 错误! alert( str.match(reg) ); // ...

``` 我们会发现,这个正则表达式不仅匹配了一个链接,还匹配了包含 `` 的一段文本。 为什么? 1. 首先,正则表达式发现一个链接标签:``。 …在哪里可以找到它呢?我们如果查看文本,就可以看到唯一的 `class="doc">` 是在链接之后的,在 `

` 中。 3. 所以有了如下匹配项: ```markup ...

``` 所以,懒惰模式在这里不起作用。 我们需要寻找 ``,但贪婪和懒惰模式都有一些问题。 正确的做法应该是这样的:`href="[^"]*"`。它会获取 href 属性中的所有字符,正好符合我们的需求。 一个实例: ```javascript let str1 = '......

...'; let str2 = '...... ...'; let reg = //g; // Works! alert( str1.match(reg) ); // 没有匹配项,是正确的 alert( str2.match(reg) ); // , ``` ### 总结 量词有两种工作模式: - **贪婪模式** 默认情况下,正则表达式引擎会尝试尽可能多地重复量词。例如,`\d+` 检测所有可能的字符。当不可能检测更多(没有更多的字符或到达字符串末尾)时,然后它再匹配模式的剩余部分。如果没有匹配,则减少重复的次数(回溯),并再次尝试。 - **懒惰模式** 通过在量词后添加问号 `?` 来启用。在每次重复量词之前,引擎会尝试去匹配模式的剩余部分。 正如我们所见,懒惰模式并不是针对贪婪搜索的灵丹妙药。另一种方式是“微调”贪婪搜索,我们很快就会见到更多的例子。 ## 捕获组 模式的一部分可以用括号括起来 `(...)`。这称为“捕获组(capturing group)”。 这有两个影响: 1. 它允许将匹配的一部分作为结果数组中的单独项。 2. 如果我们将量词放在括号后,则它将括号视为一个整体。 ### 示例:gogogo 不带括号,模式 `go+` 表示 `g` 字符,其后 `o` 重复一次或多次。例如 `goooo` 或 `gooooooooo`。 括号将字符组合,所以 `(go)+` 匹配 `go`,`gogo`,`gogogo`等。 ```javascript alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo" ``` 例如: ```javascript mail.com users.mail.com smith.users.mail.com ``` 正如我们所看到的,一个域名由重复的单词组成,每个单词后面有一个点,除了最后一个单词。 在正则表达式中是 `(\w+\.)+\w+`: ```javascript let regexp = /(\w+\.)+\w+/g; // 一个或多个( 字母 + '.' ) + 一个或多个 字母 alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com ``` 搜索有效,但是该模式无法匹配带有连字符的域名,例如 my-site.com,因为连字符不属于 `\w` 类。 我们可以通过用 `[\w-]` 替换 `\w` 来匹配除最后一个的每个单词:`([\w-]+\.)+\w+`。 ```js let regexp = /([\w-]+\.)+\w+/; // 一个或多个( 一个或多个(字母 或 '-' ) + '.') + 一个或多个 字母 ``` ### 示例:email 前面的示例可以扩展。我们可以基于它为电子邮件创建一个正则表达式。 email 格式为:`name@domain`。名称可以是任何单词,可以使用连字符和点。在正则表达式中为 `[-.\w]+`。 模式: ```javascript let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g; alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk ``` 该正则表达式并不完美的,但多数情况下都可以工作,并且有助于修复意外的错误类型。唯一真正可靠的 email 检查只能通过发送 email 来完成。 ### 匹配括号中的内容 括号从左到右编号。正则引擎会记住它们各自匹配的内容,并允许在结果中获得它。 方法 `str.match(regexp)`,如果 `regexp` 没有 `g` 标志,将查找第一个匹配并将它作为一个数组返回: 1. 在索引 `0` 处:完全匹配。 2. 在索引 `1` 处:第一个括号的内容。 3. 在索引 `2` 处:第二个括号的内容。 4. …等等… 例如,我们想找到 HTML 标记 `<.*?>` 并进行处理。这将很方便的把标签内容(尖括号内的内容)放在单独的变量中。 让我们将内部内容包装在括号中,像这样:`<(.*?)>`。 现在,我们能在结果数组中获取标签的整体 `

` 及其内容 `h1`: ### 嵌套组 括号可以嵌套。在这种情况下,编号也从左到右。 例如,在搜索标签 `` 时我们可能会对以下内容感兴趣: 1. 整个标签内容:`span class="my"`。 2. 标签名称:`span`。 3. 标签属性:`class="my"`。 让我们为它们添加括号:`<(([a-z]+)\s*([^>]*))>`。 这是它们的编号方式(从左到右,由左括号开始): ![](img\嵌套组.svg) 实际上: ```javascript let str = ''; let regexp = /<(([a-z]+)\s*([^>]*))>/; // < + (多个a-z之间的字母 + 零个或多个空格 + 零个或多个非 < 字符 ) + > let result = str.match(regexp); alert(result[0]); // alert(result[1]); // span class="my" alert(result[2]); // span alert(result[3]); // class="my" ``` `result` 的零索引始终保持完全匹配。 然后按左括号将组从左到右编号。第一组返回为 `result[1]`。它包含了整个标签内容。 然后 `result[2]` 从第二个开始的括号中进入该组 `([a-z]+)` —— 标签名称,然后在 `result[3]` 标签中:`([^>]*)`。 字符串中每个组的内容: ![](img\嵌套组.svg) ### 可选组 即使组是可选的并且在匹配项中不存在(例如,具有数量词 `(...)?`),也存在相应的 `result` 数组项,并且等于 `undefined`。 例如,让我们考虑正则 `a(z)?(c)?`。它寻找 `"a"` ,然后是可选的 `"z"`,然后是可选的 `"c"`。 如果我们在单个字母的字符串上运行 `a`,则结果为: ```js let match = 'a'.match(/a(z)?(c)?/); alert( match.length ); // 3 alert( match[0] ); // a(完全匹配) alert( match[1] ); // undefined alert( match[2] ); // undefined ``` 数组的长度为 `3`,但所有组均为空。 这是字符串的一个更复杂的匹配 `ac`: ```javascript let match = 'ac'.match(/a(z)?(c)?/) alert( match.length ); // 3 alert( match[0] ); // ac(完全匹配) alert( match[1] ); // undefined,因为 (z)? 没匹配项 alert( match[2] ); // c ``` ### 搜索所有具有组的匹配项:matchAll > 📑 **`matchAll` 是一个新方法,可能需要使用 polyfill** > > 旧的浏览器不支持 `matchAll`。 > > 可能需要一个 polyfill,例如 https://github.com/ljharb/String.prototype.matchAll. 当我们搜索所有匹配项(标志 `g`)时,`match` 方法不会返回组的内容。 例如,让我们查找字符串中的所有标签: ```javascript let str = '

'; let tags = str.match(/<(.*?)>/g); alert( tags ); //

,

``` 结果是一个匹配数组,但没有每个匹配项的详细信息。但是实际上,我们通常需要在结果中获取捕获组的内容。 在使用 `match` 很长一段时间后,它作为“新的改进版本”被加入到 JavaScript 中。 就像 `match` 一样,它寻找匹配项,但有 3 个区别: 1. 它返回的不是数组,而是一个可迭代的对象。 2. 当标志 `g` 存在时,它将每个匹配组作为一个数组返回。 3. 如果没有匹配项,则不返回 `null`,而是返回一个空的可迭代对象。 例如: ```javascript let results = '

'.matchAll(/<(.*?)>/gi); // results - is not an array, but an iterable object alert(results); // [object RegExp String Iterator] alert(results[0]); // undefined (*) results = Array.from(results); // let's turn it into array alert(results[0]); //

,h1 (1st tag) alert(results[1]); //

,h2 (2nd tag) ``` 我们可以看到,第一个区别非常重要,如 `(*)` 行所示。我们无法获得 `results[0]` 的匹配内容,因为该对象是伪数组。我们可以使用 `Array.from` 把它变成一个真正的 `Array`。在 Iterable(可迭代对象)[Iterable object(可迭代对象)](https://zh.javascript.info/iterable)一文中有关于伪数组和可迭代对象的更多详细信息。 如果我们不需要遍历结果,则 `Array.from` 没有必要: ```javascript let results = '

'.matchAll(/<(.*?)>/gi); for(let result of results) { alert(result); // 第一个结果:

,h1 // 第二个结果:

,h2 } ``` ……或使用解构: ```javascript let [tag1, tag2] = '

'.matchAll(/<(.*?)>/gi); ``` 由 `matchAll` 所返回的每个匹配,其格式与不带标志 `g` 的 `match` 所返回的格式相同:它是一个具有额外的 `index`(字符串中的匹配索引)属性和 `input`(源字符串)的数组: ```javascript let results = '

'.matchAll(/<(.*?)>/gi); let [tag1, tag2] = results; alert( tag1[0] ); //

alert( tag1[1] ); // h1 alert( tag1.index ); // 0 alert( tag1.input ); //

``` > 📑 **为什么 `matchAll` 的结果是可迭代对象而不是数组?** > > 为什么这个方法这样设计?原因很简单 — 为了优化。 > > 调用 `matchAll` 不会执行搜索。相反,它返回一个可迭代的对象,最初没有结果。每当我们对它进行迭代时才会执行搜索,例如在循环中。 > > 因此,这将根据需要找到尽可能多的结果,而不是全部。 > > 例如,文本中可能有 100 个匹配项,但是在一个 `for..of` 循环中,我们已经找到了 5 个匹配项,然后觉得足够了并做出一个 `break`。这时引擎就不会花时间查找其他 95 个匹配。 ### 命名组 用数字记录组很困难。对于简单模式,它是可行的,但对于更复杂的模式,计算括号很不方便。我们有一个更好的选择:给括号起个名字。 这是通过在开始括号之后立即放置 `?` 来完成的。 例如,让我们查找 “year-month-day” 格式的日期: ```javascript let dateRegexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/; let str = "2019-04-30"; let groups = str.match(dateRegexp).groups; alert(groups.year); // 2019 alert(groups.month); // 04 alert(groups.day); // 30 ``` 如您所见,匹配的组在 `.groups` 属性中。 要查找所有日期,我们可以添加标志 `g`。 如您所见,匹配的组在 `.groups` 属性中。 要查找所有日期,我们可以添加标志 `g`。 We’ll also need `matchAll` to obtain full matches, together with groups: 我们还需要 `matchAll` 获取完整的组匹配: ```javascript let dateRegexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/g; let str = "2019-10-30 2020-01-01"; let results = str.matchAll(dateRegexp); for(let result of results) { let {year, month, day} = result.groups; alert(`${day}.${month}.${year}`); // 第一个 alert:30.10.2019 // 第二个:01.01.2020 } ``` ### 替换捕获组 方法 `str.replace(regexp, replacement)` 用 `replacement` 替换 `str` 中匹配 `regexp` 的所有捕获组。这使用 `$n` 来完成,其中 `n` 是组号。 例如, ```javascript let str = "John Bull"; let regexp = /(\w+) (\w+)/; alert( str.replace(regexp, '$2, $1') ); // Bull, John ``` 对于命名括号,引用为 `$`。 例如,让我们将日期格式从 “year-month-day” 更改为 “day.month.year”: ```javascript let regexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/g; let str = "2019-10-30, 2020-01-01"; alert( str.replace(regexp, '$.$.$') ); // 30.10.2019, 01.01.2020 ``` ### 非捕获组 ?: 有时我们需要括号才能正确应用量词,但我们不希望它们的内容出现在结果中。 可以通过在开头添加 `?:` 来排除组。 例如,如果我们要查找 `(go)+`,但不希望括号内容(`go`)作为一个**单独的数组项**,则可以编写:`(?:go)+`。 在下面的示例中,我们仅将名称 `John` 作为匹配项的单独成员: ```javascript let str = "Gogogo John!"; // ?: 从捕获组中排除 'go' let regexp = /(?:go)+ (\w+)/i; let result = str.match(regexp); alert( result[0] ); // Gogogo John(完全匹配) alert( result[1] ); // John alert( result.length ); // 2(数组中没有更多项) ``` ### 总结 括号将正则表达式的一部分组合在一起,以便量词可以整体应用。 括号组从左到右编号,可以选择用 `(?...)` 命名。 可以在结果中获得按组匹配的内容: - 方法 `str.match` 仅当不带标志 `g` 时返回捕获组。 - 方法 `str.matchAll` 始终返回捕获组。 如果括号没有名称,则匹配数组按编号提供其内容。命名括号还可使用属性 `groups`。 我们还可以使用 `str.replace` 来替换括号内容中的字符串:使用 `$n` 或者名称 `$`。 可以通过在组的开头添加 `?:` 来排除编号组。当我们需要对整个组应用量词,但不希望将其作为结果数组中的单独项时这很有用。我们也不能在替换字符串时引用此类括号。 ## 模式中的反向引用:\N 和 \k\ 我们不仅可以在结果或替换字符串中使用捕获组 `(...)` 的内容,还可以在模式本身中使用它们。 ### 按编号反向引用:\N 可以使用 `\N` 在模式中引用一个组,其中 `N` 是组号。 为了弄清那为什么有帮助,让我们考虑一项任务。 我们需要找到带引号的字符串:单引号 `'...'` 或双引号 `"..."`– 应匹配两种变体。 如何找到它们? 我们可以将两种引号放在方括号中:`['"](.*?)['"]`,但它会找到带有混合引号的字符串,例如 `"...'` 和 `'..."`。当一种引号出现在另一种引号内,比如在字符串 `"She's the one!"` 中时,便会导致不正确的匹配: ```javascript let str = `He said: "She's the one!".`; let regexp = /['"](.*?)['"]/g; // 不是我们想要的结果 alert( str.match(regexp) ); // "She' ``` 如我们所见,该模式找到了一个开头的引号 `"`,然后文本被匹配,直到另一个引号 `'`,该匹配结束。 为了确保模式查找的结束引号与开始的引号完全相同,我们可以将其包装到捕获组中并对其进行反向引用:`(['"])(.*?)\1`。 这是正确的代码: ```javascript let str = `He said: "She's the one!".`; let regexp = /(['"])(.*?)\1/g; alert( str.match(regexp) ); // "She's the one!" ``` 现在可以了!正则表达式引擎会找到第一个引号 `(['"])` 并记住其内容。那是第一个捕获组。 `\1` 在模式中进一步的含义是“查找与第一(捕获)分组相同的文本”,在我们的示例中为完全相同的引号。 与此类似,`\2` 表示第二(捕获)分组的内容,`\3` – 第三分组,依此类推。 > 📑 **请注意:** > > 如果我们在组中使用 `?:`,那么我们将无法引用它。用 `(?:...)` 捕获的组被排除,引擎不会存储。 > 📑 **不要搞混了: 在模式中用 `\1`,在替换项中用:`$1`** > > 在替换字符串中我们使用美元符号:`$1`,而在模式中 – 使用反斜杠 `\1`。 ### 按命名反向引用:`\k` 如果正则表达式中有很多括号对(注:捕获组),给它们起个名字方便引用。 要引用命名组,我们可以使用:`\k`。 在下面的示例中引号组命名为 `?`,因此反向引用为 `\k`: ```javascript let str = `He said: "She's the one!".`; let regexp = /(?['"])(.*?)\k/g; alert( str.match(regexp) ); // "She's the one!" ``` ## 选择(OR)| 选择是正则表达式中的一个术语,实际上是一个简单的“或”。 在正则表达式中,它用竖线 `|` 表示。 例如,我们需要找出编程语言:HTML、PHP、Java 或 JavaScript。 对应的正则表达式为:`html|php|java(script)?`。 用例如下: ```javascript let reg = /html|php|css|java(script)?/gi; let str = "First HTML appeared, then CSS, then JavaScript"; alert( str.match(reg) ); // 'HTML', 'CSS', 'JavaScript' ``` 我们已知的一个相似符号 —— 方括号。就允许在许多字符中进行选择,例如 `gr[ae]y` 匹配 `gray` 或 `grey`。 选择符号并非在字符级别生效,而是在表达式级别。正则表达式 `A|B|C` 意思是命中 `A`、`B` 或 `C` 其一均可。 例如: - `gr(a|e)y` 严格等同 `gr[ae]y`。 - `gra|ey` 匹配 “gra” or “ey”。 我们通常用圆括号把模式中的选择部分括起来,像这样 `before(XXX|YYY)after`。 选择是正则表达式中的一个术语,实际上是一个简单的“或”。 在正则表达式中,它用竖线 `|` 表示。 例如,我们需要找出编程语言:HTML、PHP、Java 或 JavaScript。 对应的正则表达式为:`html|php|java(script)?`。 用例如下: ```javascript let reg = /html|php|css|java(script)?/gi; let str = "First HTML appeared, then CSS, then JavaScript"; alert( str.match(reg) ); // 'HTML', 'CSS', 'JavaScript' ``` 我们已知的一个相似符号 —— 方括号。就允许在许多字符中进行选择,例如 `gr[ae]y` 匹配 `gray` 或 `grey`。 选择符号并非在字符级别生效,而是在表达式级别。正则表达式 `A|B|C` 意思是命中 `A`、`B` 或 `C` 其一均可。 例如: - `gr(a|e)y` 严格等同 `gr[ae]y`。 - `gra|ey` 匹配 “gra” or “ey”。 我们通常用圆括号把模式中的选择部分括起来,像这样 `before(XXX|YYY)after`。 ### 时间正则表达式 在之前的章节中有个任务是构建用于查找形如 `hh:mm` 的时间字符串,例如 `12:00`。但是简单的 `\d\d:\d\d` 过于模糊。它同时匹配 `25:99`。 如何构建更优的正则表达式? 我们可以应用到更多的严格匹配结果中: - 首个匹配数字必须是 `0` 或 `1`,同时其后还要跟随任一数字。 - 或者是数字 `2` 之后跟随 `[0-3]`。 构建正则表达式:`[01]\d|2[0-3]`。 接着可以添加冒号和分钟的部分。 分钟的部分必须在 `0` 到 `59` 区间,在正则表达式语言中含义为首个匹配数字 `[0-5]` 其后跟随任一数字 `\d`。 把它们拼接在一起形成最终的模式 `[01]\d|2[0-3]:[0-5]\d`。 快大功告成了,但仍然存在一个问题。选择符 `|` 在 `[01]\d` 和 `2[0-3]:[0-5]\d` 之间。这是错误的,因为它只匹配符号左侧或右侧任一表达式。 ```javascript let reg = /[01]\d|2[0-3]:[0-5]\d/g; alert("12".match(reg)); // 12 (matched [01]\d) ``` 这个错误相当明显,但也是初学正则表达式的常见错误。 我们需要添加一个插入语用于匹配时钟:`[01]\d` 或 `2[0-3]`。 以下为正确版本: ```javascript let reg = /([01]\d|2[0-3]):[0-5]\d/g; alert("00:00 10:10 23:59 25:99 1:2".match(reg)); // 00:00,10:10,23:59 ``` ## 前瞻断言与后瞻断言 有时候我们需要匹配后面跟着特定模式的一段模式。比如,我们要从 `1 turkey costs 30€` 这段字符中匹配价格数值。 我们需要获取 `€` 符号前面的数值(假设价格是整数)。 那就是前瞻断言要做的事情。 ### 前瞻断言 语法为:`x(?=y)`,它表示 “匹配 `x`, 仅在后面是 `y` 的情况"” 那么对于一个后面跟着 `€` 的整数金额,它的正则表达式应该为:`\d+(?=€)`。 ```javascript let str = "1 turkey costs 30€"; alert( str.match(/\d+(?=€)/) ); // 30 (正确地跳过了单个的数字 1) ``` 让我们来看另一种情况:这次我们想要一个数量,它是一个不被 `€` 跟着的数值。 这里就要用到前瞻否定断言了。 语法为:`x(?!y)`,意思是 “查找 `x`, 但是仅在不被 `y` 跟随的情况下匹配成功”。 ```javascript let str = "2 turkeys cost 60€"; alert( str.match(/\d+(?!€)/) ); // 2(正确地跳过了价格) ``` ### 后瞻断言 前瞻断言允许添加一个“后面要跟着什么”的条件判断。 后瞻断言也是类似的,只不过它是在相反的方向上进行条件判断。也就是说,它只允许匹配前面有特定字符串的模式。 语法为: - 后瞻肯定断言:`(?<=y)x`, 匹配 `x`, 仅在前面是 `y` 的情况。 - 后瞻否定断言:`(? 📑 **请注意:** > > 这些文章中有更多关于占有型量词和前瞻断言的的内容:[Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead](http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead) 和 [Mimicking Atomic Groups](http://blog.stevenlevithan.com/archives/mimic-atomic-groups)。 我们现在用前瞻断言重写第一个例子中的正则来防止回溯吧: ```javascript let regexp = /^((?=(\w+))\2\s?)*$/; alert( regexp.test("A good string") ); // true let str = "An input string that takes a long time or even makes this regex to hang!"; alert( regexp.test(str) ); // false,执行得很快! ``` 这里我们用 `\2` 代替 `\1`,因为这里附加了额外的外部括号。为了防止数字产生混淆,我们可以给括号命名,例如 `(?\w+)`。 ```javascript // 括号被命名为 ?,使用 \k 来引用 let regexp = /^((?=(?\w+))\k\s?)*$/; let str = "An input string that takes a long time or even makes this regex to hang!"; alert( regexp.test(str) ); // false alert( regexp.test("A correct string") ); // true ``` 本文所描述的问题称作“灾难性回溯(catastrophic backtracking)”,又译作“回溯陷阱”。 我们有 2 种处理它的思路: - 重写正则表达式,尽可能减少其中排列组合的数量。 - 防止回溯。 ## 粘性标志 "y",在位置处搜索 `y` 标志允许在源字符串中的指定位置执行搜索。 为了掌握 `y` 标志的用例,看看它有多好,让我们来探讨一个实际的用例。 regexps 的常见任务之一是"词法分析":比如我们在程序设计语言中得到一个文本,然后分析它的结构元素。 例如,HTML 有标签和属性,JavaScript 代码有函数、变量等。 编写词法分析器是一个特殊的领域,有自己的工具和算法,所以我们就不深究了,但有一个共同的任务:在给定的位置读出一些东西。 例如,我们有一个代码字符串 `let varName = "value"`,我们需要从其中读取变量名,这个变量名从位置 `4` 开始。 我们用 regexp `\w+` 来查找变量名。实际上,JavaScript 的变量名需要更复杂的 regexp 来进行准确的匹配,但在这里并不重要。 调用 `str.match(/\w+/)` 将只找到该行中的第一个单词。或者是所有带标记 `g` 的单词。但我们只需要在位置 `4` 的一个词。 要从给定位置搜索,我们可以使用方法 `regexp.exec(str)`。 如果 `regexp` 没有标志 `g` 或 `y`,那么这个方法就可以寻找字符串 `str` 中的第一个匹配,就像 `str.match(regexp)` 一样。这种简单的无标志的情况我们在这里并不感兴趣。 如果有标志 `g`,那么它就会在字符串 `str` 中执行搜索,从存储在 `regexp.lastIndex` 属性中的位置开始。如果发现匹配,则将 `regexp.lastIndex` 设置为匹配后的索引。 当一个 regexp 被创建时,它的 `lastIndex` 是 `0`。 因此,连续调用 `regexp.exec(str)` 会一个接一个地返回匹配。 一个例子(用标志 `g` ): ```javascript let str = 'let varName'; let regexp = /\w+/g; alert(regexp.lastIndex); // 0(最初 lastIndex=0) let word1 = regexp.exec(str); alert(word1[0]); // let(第一个单词) alert(regexp.lastIndex); // 3(匹配后的位置) let word2 = regexp.exec(str); alert(word2[0]); // varName (第二个单词) alert(regexp.lastIndex); // 11(匹配后的位置) let word3 = regexp.exec(str); alert(word3); // null(没有更多的匹配) alert(regexp.lastIndex); // 0(搜索结束时重置) ``` 每个匹配都会以数组形式返回,包含分组和附加属性。 我们可以在循环中得到所有的匹配。 ```javascript let str = 'let varName'; let regexp = /\w+/g; let result; while (result = regexp.exec(str)) { alert( `Found ${result[0]} at position ${result.index}` ); // 在位置 0 发现 let, 然后 // 在位置 4 发现 varName } ``` `regexp.exec` 是 `str.matchAll` 方法的替代方法。 与其他方法不同,我们可以设置自己的 `lastIndex`,从给定位置开始搜索。 例如,让我们从位置 `4` 开始寻找一个单词。 ```javascript let str = 'let varName = "value"'; let regexp = /\w+/g; // 如果没有标志 "g",属性 lastIndex 会被忽略 regexp.lastIndex = 4; let word = regexp.exec(str); alert(word); // varName ``` 我们从位置 `regexp.lastIndex = 4` 开始搜索 `w+`。 请注意:搜索从位置 `lastIndex` 开始,然后再往前走。如果在 `lastIndex` 位置上没有词,但它在后面的某个地方,那么它就会被找到: ```javascript let str = 'let varName = "value"'; let regexp = /\w+/g; regexp.lastIndex = 3; let word = regexp.exec(str); alert(word[0]); // varName alert(word.index); // 4 ``` ……所以,用标志 `g` 属性 `lastIndex` 设置搜索的起始位置。 **标记 `y` 使 `regexp.exec` 正好在 `lastIndex` 位置,而不是在它之前,也不是在它之后。 下面是使用标志 `y` 进行同样的搜索。 ```javascript let str = 'let varName = "value"'; let regexp = /\w+/y; regexp.lastIndex = 3; alert( regexp.exec(str) ); // null(位置 3 有一个空格,不是单词) regexp.lastIndex = 4; alert( regexp.exec(str) ); // varName(在位置 4 的单词) ``` 我们可以看到,regexp `/\w+/y` 在位置 `3` 处不匹配(不同于标志 `g` ),而是在位置 `4` 处匹配。 想象一下,我们有一个长的文本,而里面根本没有匹配。那么用标志 `g` 搜索将一直到文本的最后,这将比用标志 `y` 搜索要花费更多的时间。 在像词法分析这样的任务中,通常在一个确切的位置会有很多搜索。使用标志 `y` 是获得良好性能的关键。 ## 正则表达式(RegExp)和字符串(String)的方法 在本文中,我们将深入探讨与正则表达式配合使用的各种方法。 ### str.match(regexp) `str.match(regexp)` 方法在字符串 `str` 中找到匹配 `regexp` 的字符。 它有 3 种模式: 1. 如果 `regexp` 不带有 `g` 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 `index`(匹配项的位置)、`input`(输入字符串,等于 `str`): ```javascript let str = "I love JavaScript"; let result = str.match(/Java(Script)/); alert( result[0] ); // JavaScript(完全匹配) alert( result[1] ); // Script(第一个分组) alert( result.length ); // 2 // 其他信息: alert( result.index ); // 7(匹配位置) alert( result.input ); // I love JavaScript(源字符串) ``` 2. 如果 `regexp` 带有 `g` 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息。 ```javascript let str = "I love JavaScript"; let result = str.match(/Java(Script)/g); alert( result[0] ); // JavaScript alert( result.length ); // 1 ``` 3. 如果没有匹配项,则无论是否带有标记 `g` ,都将返回 `null`。 这是一个重要的细微差别。如果没有匹配项,我们得到的不是一个空数组,而是 `null`。忘记这一点很容易出错,例如: ```javascript let str = "I love JavaScript"; let result = str.match(/HTML/); alert(result); // null alert(result.length); // Error: Cannot read property 'length' of null ``` 如果我们希望结果是一个数组,我们可以这样写: ```javascript let result = str.match(regexp) || []; ``` ### str.matchAll(regexp) **A recent addition** This is a recent addition to the language. Old browsers may need polyfills. 方法 `str.matchAll(regexp)` 是 `str.match` “新改进的”变体。 它主要用来搜索所有组的所有匹配项。 与 `match` 相比有 3 个区别: 1. 它返回包含匹配项的可迭代对象,而不是数组。我们可以用 `Array.from` 从中得到一个常规数组。 2. 每个匹配项均以包含分组的数组形式返回(返回格式与不带 `g` 标记的 `str.match` 相同)。 3. 如果没有结果,则返回的不是 `null`,而是一个空的可迭代对象。 用法示例: ```javascript let str = '

Hello, world!

'; let regexp = /<(.*?)>/g; let matchAll = str.matchAll(regexp); alert(matchAll); // [object RegExp String Iterator],不是数组,而是一个可迭代对象 matchAll = Array.from(matchAll); // 现在返回的是数组 let firstMatch = matchAll[0]; alert( firstMatch[0] ); //

alert( firstMatch[1] ); // h1 alert( firstMatch.index ); // 0 alert( firstMatch.input ); //

Hello, world!

``` 如果我们用 `for..of` 来循环 `matchAll` 的匹配项,那么我们就不需要 `Array.from` 了。 ### str.split(regexp|substr, limit) 使用正则表达式(或子字符串)作为分隔符来分割字符串。 我们可以用 `split` 来分割字符串,如下所示: ```javascript alert('12-34-56'.split('-')) // 数组 ['12', '34', '56'] ``` 但同样,我们也可以用正则表达式来做: ```javascript alert('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56'] ``` ### str.search(regexp) 方法 `str.search(regexp)` 返回第一个匹配项的位置,如果未找到,则返回 `-1`: ```javascript let str = "A drop of ink may make a million think"; alert( str.search( /ink/i ) ); // 10(第一个匹配位置) ``` **重要限制:`search` 仅查找第一个匹配项。** 如果需要其他匹配项的位置,则应使用其他方法,例如用 `str.matchAll(regexp)` 查找所有位置。 ### str.replace(str|regexp, str|func) 这是用于搜索和替换的通用方法,是最有用的方法之一。它是搜索和替换字符串的瑞士军刀。 我们可以不用正则表达式来搜索和替换子字符串: ```javascript // 用冒号替换连字符 alert('12-34-56'.replace("-", ":")) // 12:34-56 ``` 不过有一个陷阱。 **当 `replace` 的第一个参数是字符串时,它仅替换第一个匹配项。** 您可以在上面的示例中看到:只有第一个 `"-"` 被 `":"` 替换了。 如要找到所有的连字符,我们不应该用字符串 `"-"`,而应使用带 `g` 标记的正则表达式 `/-/g`: ```javascript // 将连字符替换为冒号 alert( '12-34-56'.replace( /-/g, ":" ) ) // 12:34:56 ``` 第二个参数是一个替代字符串。我们可以在其中使用特殊字符: | 符号 | 替换字符串中的操作 | | :-------- | :----------------------------------------------------------- | | `$&` | 插入整个匹配项 | | `$`` | 在匹配项之前插入字符串的一部分 | | `$'` | 在匹配项之后插入字符串的一部分 | | `$n` | 如果 `n` 是一个 1 到 2 位的数字,则插入第 n 个分组的内容,详见 [捕获组](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_010.html#regexp-groups) | | `$` | 插入带有给定 `name` 的括号内的内容,详见 [捕获组](chrome-extension://bfpfpfenkimhijpdcbbhmemcimbeehcl/more_split_010.html#regexp-groups) | | `$$` | 插入字符 `$` | 例如: ```javascript let str = "John Smith"; // 交换名字和姓氏 alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John ``` **对于需要“智能”替换的场景,第二个参数可以是一个函数。** 每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。 该函数 `func(match, p1, p2, ..., pn, offset, input, groups)` 带参数调用: 1. `match` - 匹配项, 2. `p1, p2, ..., pn` - 分组的内容(如有), 3. `offset` - 匹配项的位置, 4. `input` - 源字符串, 5. `groups` - 所指定分组的对象。 如果正则表达式中没有括号,则只有 3 个参数:`func(str, offset, input)`。 例如,将所有匹配项都大写: ```javascript let str = "html and css"; let result = str.replace(/html|css/gi, str => str.toUpperCase()); alert(result); // HTML and CSS ``` 按其在字符串中的位置来替换每个匹配项: ```javascript alert("Ho-Ho-ho".replace(/ho/gi, (match, offset) => offset)); // 0-3-6 ``` 在下面的示例中,有两对括号,因此将使用 5 个参数调用替换函数:第一个是完全匹配项,然后是 2 对括号,然后是匹配位置(在示例中未使用)和源字符串: ```javascript let str = "John Smith"; let result = str.replace(/(\w+) (\w+)/, (match, name, surname) => `${surname}, ${name}`); alert(result); // Smith, John ``` 如果有许多组,用 rest 参数(…)可以很方便的访问: ```javascript let str = "John Smith"; let result = str.replace(/(\w+) (\w+)/, (...match) => `${match[2]}, ${match[1]}`); alert(result); // Smith, John ``` 或者,如果我们使用的是命名组,则带有它们的 `groups` 对象始终是最后一个对象,因此我们可以这样获得它: ```javascript let str = "John Smith"; let result = str.replace(/(?\w+) (?\w+)/, (...match) => { let groups = match.pop(); return `${groups.surname}, ${groups.name}`; }); alert(result); // Smith, John ``` 使用函数可以为我们提供终极替代功能,因为它可以获取匹配项的所有信息,可以访问外部变量,可以做任何事。 ### regexp.exec(str) `regexp.exec(str)` 方法返回字符串 `str` 中的 `regexp` 匹配项。与以前的方法不同,它是在正则表达式而不是字符串上调用的。 根据正则表达式是否带有标志 `g`,它的行为有所不同。 如果没有 `g`,那么 `regexp.exec(str)` 返回的第一个匹配与 `str.match(regexp)` 完全相同。这没什么新的变化。 但是,如果有标记 `g`,那么: - 调用 `regexp.exec(str)` 会返回第一个匹配项,并将紧随其后的位置保存在属性 `regexp.lastIndex` 中。 -下一次同样的调用会从位置 `regexp.lastIndex` 开始搜索,返回下一个匹配项,并将其后的位置保存在 `regexp.lastIndex` 中。 - …以此类推。 -如果没有匹配项,则 `regexp.exec` 返回 `null`,并将 `regexp.lastIndex` 重置为 `0`。 因此,重复调用会挨个返回所有的匹配项,属性 `regexp.lastIndex` 用来跟踪当前的搜索位置。 过去,在将 `str.matchAll` 方法添加到 `JavaScript` 之前,在循环中是通过调用 `regexp.exec` 来获取分组的所有匹配项: ```javascript let str = 'More about JavaScript at https://javascript.info'; let regexp = /javascript/ig; let result; while (result = regexp.exec(str)) { alert( `Found ${result[0]} at position ${result.index}` ); // Found JavaScript at position 11,然后 // Found javascript at position 33 } ``` 这个现在也可以使用,尽管对于较新的浏览器来说,`str.matchAll` 通常更方便。 **我们可以通过手动设置 `lastIndex`,用 `regexp.exec` 从给定位置进行搜索。** 例如: ```javascript let str = 'Hello, world!'; let regexp = /\w+/g; // 带有标记 "g",lastIndex 属性被忽略 regexp.lastIndex = 5; // 从第 5 个位置搜索(从逗号开始) alert( regexp.exec(str) ); // world ``` 如果正则表达式带有标记 `y`,则搜索将精确地在 `regexp.lastIndex` 位置执行,不会再继续了。 让我们将上例中的 `g` 标记替换为 `y`。现在没有找到匹配项了,因为在位置 `5` 处没有单词: ```javascript let str = 'Hello, world!'; let regexp = /\w+/y; regexp.lastIndex = 5; // 在位置 5 精确查找 alert( regexp.exec(str) ); // null ``` 这个方法在某些场景下很方便,例如需要用正则表达式从字符串的精确位置来“读取”字符串(而不是其后的某处)。 ### regexp.test(str) 方法 `regexp.test(str)` 查找匹配项,然后返回 `true/false` 表示是否存在。 例如: ```javascript let str = "I love JavaScript"; // 这两个测试相同 alert( /love/i.test(str) ); // true alert( str.search(/love/i) != -1 ); // true ``` 一个反例: ```javascript let str = "Bla-bla-bla"; alert( /love/i.test(str) ); // false alert( str.search(/love/i) != -1 ); // false ``` 如果正则表达式带有标记 `g`,则 `regexp.test` 从 `regexp.lastIndex` 属性中查找,并更新此属性,就像 `regexp.exec` 一样。 因此,我们可以用它从给定位置进行搜索: ```javascript let regexp = /love/gi; let str = "I love JavaScript"; // 从位置 10 开始: regexp.lastIndex = 10; alert( regexp.test(str) ); // false(无匹配) ``` > 📑 **相同的全局正则表达式在不同的源字符串上测试可能会失败** > > 如果我们在不同的源字符串上应用相同的全局表达式,可能会出现错误的结果,因为 `regexp.test` 的调用会增加 `regexp.lastIndex` 属性值,因此在另一个字符串中的搜索可能是从非 0 位置开始的。 > > 例如,这里我们在同一文本上调用 `regexp.test` 两次,而第二次调用失败了: > > ```javascript > let regexp = /javascript/g; // (新建 regexp:regexp.lastIndex=0) > > alert( regexp.test("javascript") ); // true(现在 regexp.lastIndex=10) > alert( regexp.test("javascript") ); // false > ``` > > 这正是因为在第二个测试中 `regexp.lastIndex` 不为零。 > > 如要解决这个问题,我们可以在每次搜索之前设置 `regexp.lastIndex = 0`。或者,不调用正则表达式的方法,而是使用字符串方法 `str.match/search/...`,这些方法不用 `lastIndex`。