【笔记】正则表达式

432 阅读10分钟

正则表达式引擎总是返回最左边的匹配,即使稍后可以找到“更好”的匹配。

元字符 12个

  1. \ 反斜杠
  2. ^ 插入符号
  3. $ 美元符号
  4. .
  5. | 管道符号
  6. ? 问号
  7. * 星号
  8. + 加号
  9. ( 左括号
  10. ) 右括号
  11. [ 左方括号
  12. { 左大括号

简写

注意:JavaJavaScriptPCRE。这些 Unicode 风格仅将 ASCII 数字与\d匹配。

  1. \d 所有数字 是[0-9]的简写
  2. \D 非数字 是 [^\d]的简写
  3. \w 单词字符 是[A-Za-z0-9_]的简写
  4. \W 非单词字符 是[^\w]的简写

注意:包含下划线 _和数字

  1. \s 空白字符 是 [ \t\r\n\f]的简写

注意:匹配空格、制表符、回车、换行或换页

Non-Printable Characters

  1. \t 匹配制表符(ASCII 0x09)
  2. \r 匹配回车符(0x0D)
  3. \n 匹配换行符(0x0A)
  4. \a (bell, 0x07)
  5. \e (escape, 0x1B)
  6. \f (form feed, 0x0C)
  7. \R 匹配任何换行符

注意:只期待匹配 CRLF 对

Character Classes or Character Sets

使用“字符类”,也称为“字符集”,您可以告诉正则表达式引擎只匹配几个字符中的一个。只需将要匹配的字符放在方括号之间。如果要匹配 a 或 e,请使用[ae]。您可以在gr[ae]y中使用它来匹配graygray

一个字符类只匹配一个字符。gr[ae]y不匹配graaygraey或任何类似的东西。字符类中字符的顺序无关紧要。结果是相同的。

您可以在字符类中使用连字符来指定字符范围。[0-9]匹配 0 到 9 之间的单个数字。您可以使用多个范围。[0-9a-fA-F]匹配单个十六进制数字,不区分大小写。您可以组合范围和单个字符。[0-9a-fxA-FX]匹配十六进制数字或字母 X。同样,字符的顺序和范围无关紧要。

字符类是正则表达式最常用的特性之一。即使拼写错误,您也可以找到单词,例如sep[ae]r[ae]teli[cs]en[cs]e。您可以使用[A-Za-z_][A-Za-z_0-9]*在编程语言中找到标识符。你可以用0[xX][A-Fa-f0-9]+.找到一个 C 风格的十六进制数。

Negated Character Classes 否定字符类

在左方括号后键入插入符号会否定字符类。结果是字符类匹配任何不在字符类中的字符。 与dot不同,否定字符类也匹配(不可见)换行符。如果您不希望否定字符类匹配换行符,则需要在类中包含换行符。[^0-9\r\n]匹配任何不是数字或换行符的字符。

否定字符类仍然必须匹配一个字符

例: /q[^u]/g 匹配所有q字符,且后面跟着的不是u

Iraq is a country // Ira<< q  >>is a country
Iraqu // Iraqu 未匹配
Irauq u // Irau<< q  >>u

如果您希望正则表达式在两个字符串中匹配 q,并且仅匹配 q,则需要使用负前瞻q(?!u)

Metacharacters Inside Character Classes 字符类中的元字符

在大多数正则表达式风格中,字符类中唯一的特殊字符或元字符是右括号]、反斜杠\、插入符号^和连字符-。要搜索星号或加号,请使用[+*]

要在字符类中包含反斜杠作为没有任何特殊含义的字符,您必须使用另一个反斜杠对其进行转义。[\\x]匹配反斜杠或 x。右括号]、插入符号^和连字符-可以通过用反斜杠转义它们或将它们放在不具有特殊含义的位置来包含它们。

连字符可以在左括号之后,右括号之前,或者在否定插入符号之后。[-x][x-]都匹配 x 或连字符。[^-x][^x-]匹配任何不是 x 或连字符的字符。

Repeating Character Classes 重复字符类

如果您使用重复字符类 ?*+运算符,会匹配整个字符类。你不只是匹配一次就返回字符。正则表达式[0-9]+可以匹配837222

  • 例1:/a/
  • 例2:/a+/
abc aabc 
// 例1 << a >>bc << a >><< a >>bc
// 例2 << a >>bc << aa >>bc 相同的a被合并匹配

如果要重复匹配的字符,而不是字符类,则需要使用反向引用。([0-9])\1+匹配222但不匹配837。当应用于字符串833337时,它匹配该字符串中间的3333。如果你不想这样,你需要使用lookaround

  • ([0-9])\1+
123456 // 无
11223334444556 // << 11 >><< 22 >><< 333 >><< 4444 >><< 55 >>6
833337 // 8<< 3333 >>7

The Dot Matches (Almost) Any Character 点匹配(几乎)任何字符

点匹配单个字符,而不关心该字符是什么。唯一的例外是换行符。

JavaScript(为了与旧版浏览器兼容)和VBScript中,您可以使用诸如[\s\S]之类的字符类来匹配任何字符。此字符匹配一个字符,该字符要么是空白字符(包括换行符),要么是不是空白字符的字符。由于所有字符都是空格或非空格,因此此字符类匹配任何字符。不要使用慢的(\s|\S)之类的交替。当然不要使用(.|\s),这会导致[](https://www.regular-expressions.info/toolong.html)灾难性的回溯,因为空格和制表符都可以匹配.\s

Line Break Characters 换行符

虽然在正则表达式风格中对点的支持是普遍的,但它们将哪些字符视为换行符存在显着差异。所有语言都将换行符\n视为换行符。

谨慎使用.

点是一个非常强大的正则表达式元字符。它让你变得懒惰。加上一个点,当您在有效数据上测试正则表达式时,一切都匹配得很好。问题是正则表达式在它不应该匹配的情况下也匹配。如果您不熟悉正则表达式,其中一些情况一开始可能并不那么明显。

让我们用一个简单的例子来说明这一点。假设我们想匹配 mm/dd/yy 格式的日期,但我们想让用户选择日期分隔符。快速解决方案是\d\d.\d\d.\d\d.乍一看还不错。它匹配像02/12/03这样的日期就好了。问题是:02512703也被这个正则表达式视为有效日期。在这场比赛中,第一个点匹配5,第二个匹配7。显然不是我们想要的。

\d\d[- /.]\d\d[- /.]\d\d是一个更好的解决方案。此正则表达式允许使用破折号、空格、点和正斜杠作为日期分隔符。请记住,点不是字符类中的元字符,因此我们不需要使用反斜杠对其进行转义。

这个正则表达式仍然远非完美。它匹配99/99/99作为有效日期。

[01]\d[- /.][0-3]\d[- /.]\d\d是领先一步,尽管它仍然匹配19/39/99。你希望你的正则表达式有多完美取决于你想用它做什么。如果您正在验证用户输入,它必须是完美的。如果您正在解析来自每次都以相同方式生成其文件的已知来源的数据文件,那么我们的最后一次尝试可能足以无错误地解析数据。您可以在示例部分找到更好的正则表达式来匹配日期

使用否定字符类而不是点

否定字符类通常比点更合适。解释重复运算符星号和加号的教程部分更详细地介绍了这一点。但是警告很重要,在这里也可以提及。再用一个例子来说明。

假设你想匹配一个双引号字符串。听起来很容易。我们可以在双引号之间包含任意数量的任意字符,因此".*"似乎可以解决问题。点匹配任何字符,星号允许点重复任意次数,包括零次。如果你在Put a "string" between double quotes上测试这个正则表达式,它匹配"string"就好了。现在继续在Houston, we have a problem with "string one" and "string two"中测试。

结果1:Put a << "string" >> between double quotes

结果2:Houston, we have a problem with << "string one" and "string two" >>

"string one" and "string two"绝对不是我们想要的。原因是星星贪心

在日期匹配示例中,我们通过将点替换为字符类来改进正则表达式。在这里,我们对否定字符类做同样的事情。我们最初对双引号字符串的定义是错误的。我们不希望引号之间有任何数量的任何字符。 我们想要任意数量的不是双引号或引号之间换行符的字符。所以正确的正则表达式是

如果你的风格支持简写 \v来匹配任何换行符,那么"[^"\r\n]*"是一个更好的解决方案。 结果:Houston, we have a problem with << "string one" >> and "string two"

字符串开始和字符串结束锚点

到目前为止,我们已经了解了文字字符字符类。将其中一个放在正则表达式中会告诉正则表达式引擎尝试匹配单个字符。

锚是一个不同的品种。它们根本不匹配任何字符。相反,它们匹配字符之前、之后或之间的位置。它们可用于将正则表达式匹配“锚定”在某个位置。插入符号^匹配字符串中第一个字符之前的位置。将^a应用于abc匹配a^b根本不匹配abc,因为b不能在字符串开始之后立即匹配,由^匹配。

类似地,$匹配字符串中最后一个字符之后。c$匹配abc中的``c,而a``$根本不匹配。

测试用例

  1. abc
  2. acb
  3. bac
  4. bca
  5. cab
  6. cba
  • ^a 命中 abc acb // << a >>bc << a >>cb
  • c$ 命中 abc bac // ab<< c >> ba<< c >>
  • ^a\w+c$ 命中 abc // << abc >>

词边界

元字符\b是类似于插入符号和美元符号的锚。它匹配一个称为“单词边界”的位置。这个匹配是零长度的. \b允许您使用\b word \b形式的正则表达式执行“仅整个单词”搜索.

\B\b的否定版本。\B\b不匹配的每个位置匹配实际上,\B匹配两个单词字符之间的任何位置以及两个非单词字符之间的任何位置。

例子

  1. This island is beautiful
/\b is \b/ // This island<<  is  >>beautiful
/\b island \b/ // This<<  island  >>is beautiful
/\bisland\b/ // This << island >> is beautiful

与垂直条或管道符号交替

如果要搜索文字文本catdog,请用竖线或竖线符号分隔两个选项:cat|dog。如果您想要更多选项,只需展开列表:cat|dog|mouse|fish

交替运算符在所有正则表达式运算符中的优先级最低。

如果要限制交替的范围,则需要使用括号进行分组。如果我们想改进第一个示例以仅匹配整个单词,我们将需要使用\b(cat|dog)\b。这告诉正则表达式引擎找到一个单词边界,然后是catdog,然后是另一个单词边界。如果我们省略了括号,那么正则表达式引擎将搜索单词边界,然后是cat,或者,dog后跟一个单词边界。

例子:

  1. This island has many cats fish and dogs
/cat|dog/g // This island has many << cat >>s fish and << dog >>s
/\b(cat|dog)\b/ // This island has many cats fish and dogs
/\b(cat|dog|fish)\b/ This island has many cats << fish >> and dogs

可选项目

问号使正则表达式中的前面的标记成为可选的。colou?r匹配colourcolor

重要的正则表达式概念:贪婪

问号为正则表达式引擎提供了两种选择:尝试匹配问号适用的部分,或者不尝试匹配它。引擎总是试图匹配那个部分。只有当这导致整个正则表达式失败时,引擎才会尝试忽略问号适用的部分。

例子 1.Today is Feb 23rd, 2003 // Feb 23rd 优先于 Feb 23

/Feb 23(rd)?/ // Today is << Feb 23rd >>, 2003

使用 * 和 + 重复

已经引入了一种重复运算符或量词:问号。它告诉引擎尝试匹配前面的令牌零次或一次,使其成为可选的。

*告诉引擎尝试匹配前面的令牌零次或多次。

+告诉引擎尝试一次或多次匹配前面的令牌。

<[A-Za-z][A-Za-z0-9]*>匹配没有任何属性的 HTML 标签。匹配尖括号。[A-Za-z]匹配一个字母。[A-Za-z0-9]匹配一个字母或数字。*重复第二个字符类。因为我们使用了星号,所以如果第二个字符类不匹配也没关系。所以我们的正则表达式会匹配像<B>这样的标签。匹配<HTML>时,第一个字符类将匹配H。星号将导致第二个字符类重复三次,每一步匹配TML

限制重复

还有一个额外的量词,允许您指定一个标记可以重复多少次。语法为{ min , max },其中min为零或正整数,表示最小匹配数,max是等于或大于min的整数,表示最大匹配数。如果存在逗号但省略了max,则最大匹配数是无限的。所以{0,1}{0,}与``*相同,{1,}与``+相同。省略逗号和max告诉引擎准确地重复令牌min次。

您可以使用\b[1-9][0-9]{3}\b来匹配 1000 到 9999 之间的数字。\b[1-9][0-9]{2,4}\b匹配介于 100 和 99999 之间的数字。注意单词边界的使用。

当心贪婪!

假设您想使用正则表达式来匹配 HTML 标记。您知道输入将是一个有效的 HTML 文件,因此正则表达式不需要排除任何对尖括号的无效使用。如果它位于尖括号之间,则它是一个 HTML 标记。

大多数不熟悉正则表达式的人都会尝试使用<.+>.如下 结果是<EM>first</EM> >>并不是我们想要的<EM>原因是加号是贪婪的。

查看正则表达式引擎 的解释

正则表达式中的第一个标记是<。这是一个字面意思。正如我们已经知道的,它将匹配的第一个位置是字符串中的第一个<。下一个标记是点,它匹配除换行符以外的任何字符。点由加号重复。加号是贪婪的。因此,引擎将尽可能多地重复该点。点匹配E,因此正则表达式继续尝试将点与下一个字符匹配。M匹配,并且点再次重复。下一个字符是>。你现在应该看到问题了。点匹配>,并且引擎继续重复该点。点将匹配字符串中所有剩余的字符。当引擎在字符串结束后到达空位时,点会失败。只有在这一点上,正则表达式引擎才会继续使用下一个标记:>

到目前为止,<.+已匹配<EM>first</EM> test,并且引擎已到达字符串的末尾。>此处无法匹配。引擎会记住加号重复点的次数超过了所需的次数。(请记住,加号要求点仅匹配一次。)引擎不会承认失败,而是回溯。它将加号的重复减少一,然后继续尝试正则表达式的其余部分。

所以匹配.+简化为EM>first</EM> tes。正则表达式中的下一个标记仍然是>。但是现在字符串中的下一个字符是最后一个t。同样,这些无法匹配,导致引擎进一步回溯。到目前为止,总匹配减少到<EM>first</EM> te。但是>仍然无法匹配。所以引擎继续回溯,直到匹配.+简化为EM>first</EM。现在,>可以匹配字符串中的下一个字符。正则表达式中的最后一个标记已匹配。得到结果<EM>首先</EM>

请记住,正则表达式引擎渴望返回匹配项。它不会继续进一步回溯以查看是否存在另一个可能的匹配项。它将报告找到的第一个有效匹配项。由于贪心,这是最左边最长的匹配。

懒惰代替贪婪 解决方法

解决这个问题的快速方法是让 + 变得懒惰而不是贪婪。惰性量词有时也被称为“不贪婪”或“不情愿”。您可以通过在正则表达式中的+后放置一个?来做到这一点。你可以对*{?本身做同样的事情。所以我们的例子变成了<.+?>。让我们再看看正则表达式引擎的内部。

同样,<匹配字符串中的第一个<。下一个标记是点,这一次由一个惰性加号重复。这告诉正则表达式引擎尽可能少地重复.。最小值是一个。因此引擎将点与E匹配。满足,引擎继续>M。失败了。再次,引擎将回溯。但这一次,回溯将迫使懒惰的加号扩大而不是缩小其范围。所以匹配.+扩展为EM,引擎再次尝试继续>。现在,>匹配成功。正则表达式中的最后一个标记已匹配。引擎报告<EM>已成功匹配。这还差不多。

例子:

  1. This is a <EM>first</EM> test
/<.+>/ // This is a << <EM>first</EM> >> test
/<.+?>/ // This is a << <EM> >>first</EM> test

懒惰的替代品

在这种情况下,有比让 plus 变得懒惰更好的选择。我们可以使用贪婪的加号和否定的字符类<[^>]+>. 这更好的原因是因为回溯。当使用惰性加号时,引擎必须回溯它试图匹配的 HTML 标记中的每个字符。当使用否定字符类时,当字符串包含有效的 HTML 代码时,根本不会发生回溯。回溯会减慢正则表达式引擎的速度。

/<[^>]+>/ // This is a << <EM> >>first</EM> test

重复 \Q…\E 转义序列

\Q...\E 序列转义一串字符,将它们作为文字字符进行匹配。转义字符被视为单个字符。如果在\E之后放置一个量词,它将仅应用于最后一个字符。例如,如果您将\Q*\d+*\E+应用于*\d+**\d+*,则匹配将是*\d+**。只有星号重复。

使用括号进行分组和捕获

通过将正则表达式的一部分放在圆括号或圆括号内,您可以将正则表达式的该部分组合在一起。这允许您将量词应用于整个组或将交替限制为正则表达式的一部分。

只有括号可以用于分组。方括号定义一个字符类,大括号由具有特定限制的量词使用

括号创建编号的捕获组

除了将正则表达式的一部分组合在一起之外,括号还创建了一个编号的捕获组。它将与正则表达式部分匹配的字符串部分存储在括号内。

正则表达式Set(Value)?匹配SetSetValue。在第一种情况下,第一个(也是唯一的)捕获组保持为空。在第二种情况下,第一个捕获组匹配Value

非捕获组

如果您不需要组来捕获其匹配项,您可以将此正则表达式优化为Set(?:Value)?. 左括号后的问号和冒号是创建非捕获组的语法。左括号后的问号与正则表达式末尾的问号无关。最后一个问号是使前一个标记可选的量词. 这个量词不能出现在左括号之后,因为在组的开头没有什么是可选的。因此,问号作为使标记可选的运算符与问号作为非捕获组语法的一部分之间没有歧义,即使这起初可能令人困惑。还有其他类型的组使用(?语法与冒号以外的其他字符结合使用,本教程后面将对此进行解释。

color=(?:red|green|blue)是另一个带有非捕获组的正则表达式。此正则表达式没有量词。

支持命名捕获的正则表达式风格通常可以选择将所有未命名组转换为非捕获组

  1. red

/color:(?:red|green|blue)/g // <p style="<< color:red >>;<< color:blue >>;">red</p>

使用反向引用再次匹配相同的文本

反向引用匹配与捕获组先前匹配的相同文本。假设您要匹配一对打开和关闭 HTML 标记,以及它们之间的文本。通过将开始标签放入反向引用中,我们可以将标签的名称重用于结束标签。方法如下: <([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1>这个正则表达式只包含一对括号,它们捕获由[A-Z][A-Z0-9]*。这是开始的 HTML 标记。(由于 HTML 标记不区分大小写,因此此正则表达式需要不区分大小写的匹配。反向引用\1(反斜杠)引用第一个捕获组。\1匹配与第一个捕获组匹配的完全相同的文本。/之前的它只是我们试图匹配的结束 HTML 标记中的正斜杠。

要找出特定反向引用的数量,请从左到右扫描正则表达式。计算所有编号的捕获组的左括号。第一个括号从第一个反向引用开始,第二个是第二个等。跳过属于其他语法(例如非捕获组)的一部分的括号。这意味着非捕获括号还有另一个好处:您可以将它们插入到正则表达式中,而无需更改分配给反向引用的数字。这在修改复杂的正则表达式时非常有用。

您可以多次重复使用相同的反向引用。([a-c])x\1x\1 匹配 axaxabxbxb 和 cxcxc

大多数正则表达式支持多达 99 个捕获组和两位数的反向引用。因此,如果您的正则表达式有 99 个捕获组,则\99是一个有效的反向引用。

  1. 111<p style="color:red;color:blue;">red</p>222
  2. 1axaxa
  3. axaxa1
/<([A-Z][A-Z0-9]*)\b[^>]*>.*?<\/\1>/i // 111<< <p style="color:red;color:blue;">red</p> >>222
/([a-c])x\1x\1/ 
// 1<< axaxa >>
// << axaxa >>1

未完成的

命名组
相对反向引用
分支重置组
自由间距和注释
统一码
模式修饰符
原子分组
占有量词
前瞻与后瞻
环视,第 2 部分
将文本排除在匹配之外
条件句
平衡组
递归
子程序
无限递归
递归和量词
递归和捕获
递归和反向引用
递归和回溯
POSIX 括号表达式
零长度匹配
连续比赛