6日 十二月 2019

集合和范围 [...]

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

集合

比如说,[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]
  • 然后匹配 V

所以可以匹配上 Vola 或者 Vila

范围

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

比如说,[a-z] 会匹配从 az 范围内的字母,[0-5] 表示从 05 的数字。

在下面的示例中,我们会查询首先匹配 "x" 字符,再匹配两个数字或者位于 AF 范围内的字符。

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

Here [0-9A-F] has two ranges: it searches for a character that is either a digit from 0 to 9 or a letter from A to F.

If we’d like to look for lowercase letters as well, we can add the range a-f: [0-9A-Fa-f]. Or add the flag i.

We can also use character classes inside […].

For instance, if we’d like to look for a wordly character \w or a hyphen -, then the set is [\w-].

Combining multiple classes is also possible, e.g. [\s\d] means “a space character or a digit”.

Character classes are shorthands for certain character sets

For instance:

  • \d – is the same as [0-9],
  • \w – is the same as [a-zA-Z0-9_],
  • \s – is the same as [\t\n\v\f\r ], plus few other rare unicode space characters.

Example: multi-language \w

As the character class \w is a shorthand for [a-zA-Z0-9_], it can’t find Chinese hieroglyphs, Cyrillic letters, etc.

We can write a more universal pattern, that looks for wordly characters in any language. That’s easy with unicode properties: [\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}].

Let’s decipher it. Similar to \w, we’re making a set of our own that includes characters with following unicode properties:

  • Alphabetic (Alpha) – for letters,
  • Mark (M) – for accents,
  • Decimal_Number (Nd) – for digits,
  • Connector_Punctuation (Pc) – for the underscore '_' and similar characters,
  • Join_Control (Join_C) – two special codes 200c and 200d, used in ligatures, e.g. in Arabic.

An example of use:

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

Of course, we can edit this pattern: add unicode properties or remove them. Unicode properties are covered in more details in the article Unicode: flag "u" and class \p{...}.

Unicode properties aren’t supported in Edge and Firefox

Unicode properties p{…} are not yet implemented in Edge and Firefox. If we really need them, we can use library XRegExp.

Or just use ranges of characters in a language that interests us, e.g. [а-я] for Cyrillic letters.

排除范围

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

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

比如说:

  • [^aeyo] —— 匹配任何除了 'a''e''y' 或者 'o' 之外的字符。
  • [^0-9] —— 匹配任何除了数字之外的字符,也可以使用 \D 来表示。
  • [^\s] —— 匹配任何非空字符,也可以使用 \S 来表示。

下面的示例查询除了字母,数字和空格之外的任意字符:

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

在 […] 中不转义

通常当我们的确需要查询点字符时,我们需要把它转义成像 \. 这样的形式。如果我们需要查询一个反斜杠,我们需要使用 \\

在方括号表示中,绝大多数特殊字符可以在不转义的情况下使用:

  • 表示一个点符号 '.'
  • 表示一个加号 '+'
  • 表示一个括号 '( )'
  • 在开头或者结尾表示一个破折号(在这些位置该符号表示的就不是一个范围) `pattern:’-’。
  • 在不是开头的位置表示一个插入符号(在开头位置该符号表示的是排除)'^'
  • 表示一个开口的方括号符号 '['

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

一个在方括号中的点符号 "." 表示的就是一个点字符。查询模式 [.,] 将会寻找一个为点或者逗号的字符。

在下面的示例中,[-().^+] 会查找 -().^+ 的其中任意一个字符:

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

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

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

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

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

Ranges and flag “u”

If there are surrogate pairs in the set, flag u is required for them to work correctly.

For instance, let’s look for [𝒳𝒴] in the string 𝒳:

alert( '𝒳'.match(/[𝒳𝒴]/) ); // shows a strange character, like [?]
// (the search was performed incorrectly, half-character returned)

The result is incorrect, because by default regular expressions “don’t know” about surrogate pairs.

The regular expression engine thinks that [𝒳𝒴] – are not two, but four characters:

  1. left half of 𝒳 (1),
  2. right half of 𝒳 (2),
  3. left half of 𝒴 (3),
  4. right half of 𝒴 (4).

We can see their codes like this:

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

So, the example above finds and shows the left half of 𝒳.

If we add flag u, then the behavior will be correct:

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

The similar situation occurs when looking for a range, such as [𝒳-𝒴].

If we forget to add flag u, there will be an error:

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

The reason is that without flag u surrogate pairs are perceived as two characters, so [𝒳-𝒴] is interpreted as [<55349><56499>-<55349><56500>] (every surrogate pair is replaced with its codes). Now it’s easy to see that the range 56499-55349 is invalid: its starting code 56499 is greater than the end 55349. That’s the formal reason for the error.

With the flag u the pattern works correctly:

// look for characters from 𝒳 to 𝒵
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴

任务

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

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

·答案:没有,是的

  • 在脚本 Java 中它并不会匹配到任何字符串,因为 [^script] 表示的是“除了给定的字符之外的任何字符”。所以这个正则会查找 "Java" 之后是否有匹配这个规则的符号,但是这已经是整个字符串的结尾了,在其之后并没有任何符号。

    alert( "Java".match(/Java[^script]/) ); // null
  • 是的,因为正则表达式是大小写敏感的,[^script] 部分匹配到了字符 "S"

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

时间可以通过 hours:minutes 或者 hours-minutes 格式来表示。小时和分钟都有两个数字:09:00 或者 21-30

写一个正则表达式来找到时间:

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

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

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

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

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

教程路线图

评论

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