2022年8月6日

集合和范围 [...]

在方括号 […] 中的几个字符或者字符类表示“搜索给定字符中的任意一个”。

集合

例如,[eao] 表示以下 3 个字符中的任何一个:'a''e''o'

这就是所谓的 集合。在正则表达式中,可以将集合和常规字符一起使用。

// 查找 [t 或 m],然后匹配 "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"

请注意,虽然集合中有多个字符,但它们在匹配中只会对应其中的一个。

所以在下面的示例中并没有匹配项:

// 查找 "V",然后匹配 [o 或 i],之后匹配 "la"
alert( "Voila".match(/V[oi]la/) ); // null,无匹配项

这个模式会搜索:

  • V
  • 然后匹配其中的 一个字符 [oi]
  • 然后匹配 la

所以可以匹配上 Vola 或者 Vila

范围

方括号也可以包含 字符范围

例如,[a-z] 表示从 az 范围内的字符,[0-5] 表示从 05 的数字。

在下面的示例中,我们将搜索首先是 "x",然后有两位数或两个在 AF 范围内的字符紧随其后的字符串。

alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF

[0-9A-F] 中有两个范围:它搜索一个字符,该字符要么是在 09 范围内的数字,要么是从 AF 的字母。

如果我们还想查找小写字母,则可以添加范围 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) —— 两个特殊代码 200c200d,用于连字,例如阿拉伯语。

使用示例:

let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;

let str = `Hi 你好 12`;

// 找出所有字母和数字:
alert( str.match(regexp) ); // H,i,你,好,1,2

当然,我们可以编辑这个模式:添加 Unicode 属性或删除它们。Unicode:修饰符 "u" 和类 \p{...} 一文更详细地介绍了 Unicode 属性。

IE 浏览器不支持 Unicode 属性

IE 浏览器未实现 Unicode 属性 p{...}。如果我们真的需要它们,可以使用库 XRegExp

或者只是使用我们感兴趣的语言中的字符范围,例如西里尔字母范围 [а-я]

排除范围

除了普通的范围匹配,还有像这样 [^…] 的“排除”范围匹配。

通过在开头添加插入符号 ^ 来表示匹配所有 除了给定的字符 之外的任意字符。

例如:

  • [^aeyo] —— 匹配除了 'a''e''y''o' 之外的任何字符。
  • [^0-9] —— 匹配除了数字之外的任何字符,与 \D 作用相同。
  • [^\s] —— 匹配任何非空格字符,与 \S 作用相同。

下面的示例搜索除了字母、数字和空格之外的任何字符:

alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ and .

[…] 中的转义

通常当我们想要准确地找到一个特殊字符时,我们需要像 \. 这样对其进行转义。如果我们需要反斜杠,那么我们需要使用 \\,等等。

在方括号,我们可以使用绝大多数特殊字符而无需转义:

  • 符号 . + ( ) 无需转义。
  • 在开头或结尾(未定义范围)的连字符 - 不会被转义。
  • 插入符号 ^ 仅在开头会被转义(表示排除)。
  • 右方括号 ] 总是会被转义(如果我们需要寻找那个符号)。

换句话说,除了在方括号中有特殊含义的字符外,其它所有特殊字符都是允许不转义的。

方括号中的点 . 表示的就是一个点。模式 [.,] 将会搜索字符之一:点或逗号。

在下面的示例中,正则表达式 [-().^+] 查找 -().^+ 中的任何字符:

// 不需要转义
let reg = /[-().^+]/g;

alert( "1 + 2 - 3".match(reg) ); // 匹配 +,-

……但是如果你为了“以防万一”转义了它们,这也不会有任何问题:

// 转义其中的所有字符
let reg = /[\-\(\)\.\^\+]/g;

alert( "1 + 2 - 3".match(reg) ); // 仍能正常工作:+,-

范围和修饰符 “u”

如果集合中有代理对(surrogate pairs),则需要标志 u 才能使它们正常工作。

例如,让我们在字符串 𝒳 中查找 [𝒳𝒴]

alert( '𝒳'.match(/[𝒳𝒴]/) ); // 显示了一个奇怪的字符,像 [?]
//(搜索执行不正确,返回了半个字符)

结果不正确,因为默认情况下正则表达式“不知道”代理对。

正则表达式引擎认为 [𝒳𝒴] —— 不是两个字符,而是四个字符:

  1. 𝒳 的左半部分 (1)
  2. 𝒳 的右半部分 (2)
  3. 𝒴 的左半部分 (3)
  4. 𝒴 的右半部分 (4)

我们可以看到它们的编码,如下所示:

for(let i=0; i<'𝒳𝒴'.length; i++) {
  alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500
};

因此,上面的示例查找并显示了 𝒳 的左半部分。

如果我们添加了修饰符 u,那么行为就正常了:

alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳

当我们查找范围时也会出现类似的情况,就像 [𝒳-𝒴]

如果我们忘记添加修饰符 u,则会出现错误:

'𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression

因为,没有修饰符 u 时,代理对将被视为两个字符,所以 [𝒳-𝒴] 被理解为 [<55349><56499>-<55349><56500>](每个代理对都替换为其代码)。现在很容易看出范围 56499-55349 是无效的:其起始代码 56499 大于终止代码 55349。这就是错误的原因。

带有修饰符 u 时,该模式就可以正常匹配了:

// 查找从 𝒳 到 𝒵 的字符
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴

任务

我们有一个正则表达式 /Java[^script]/

它会和字符串 Java 中的任何一部分匹配吗?JavaScript 呢?

·答案:不会,会

  • 不会。在字符串 Java 中,它不会匹配任何内容,因为 [^script] 表示“除了给定的字符之外的任何字符”。因此,这个正则表达式会查找 "Java" 后面是否有匹配这个规则的字符,但是这已经是整个字符串的结尾了,后面没有任何字符。

    alert( "Java".match(/Java[^script]/) ); // null
  • 会。因为 [^script] 部分匹配到了字符 "S"。它不是 script。由于正则表达式区分大小写(没有 i 修饰符),因此它将 "S" 视为与 "s" 不同的字符。

    alert( "JavaScript".match(/Java[^script]/) ); // "JavaS"

时间可以通过 hours:minuteshours-minutes 格式来表示。小时和分钟都有两位数:09:0021-30

写一个正则表达式来找到它们:

let regexp = /your regexp/g;
alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30

P.S. 在这个任务中,我们假设时间总是正确的,不需要过滤像 “45:67” 这样错误的时间字符串。稍后我们也会处理这个问题。

答案:\d\d[-:]\d\d

let regexp = /\d\d[-:]\d\d/g;
alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30

请注意,破折号 '-' 在方括号中有特殊含义,但只有当它位于其它字符之间而不是开头或结尾时这个含义才会起作用,所以我们不需要对其进行转义。

教程路线图

评论

在评论之前先阅读本内容…
  • 如果你发现教程有错误,或者有其他需要修改和提升的地方 — 请 提交一个 GitHub issue 或 pull request,而不是在这评论。
  • 如果你对教程的内容有不理解的地方 — 请详细说明。
  • 使用 <code> 标签插入只有几个词的代码,插入多行代码可以使用 <pre> 标签,对于超过 10 行的代码,建议你使用沙箱(plnkrJSBincodepen…)