学习笔记:正则表达式

正则表达式是文本处理方面功能最强大的工具之一。正则表达式语言用来构造正则表达式,最终构造出来的字符串就称为正则表达式,正则表达式用来完成搜索和替换操作。

本文参考《正则表达式必知必会(修订版)》《Learning Regular Expressions》Ben Forta

1 正则表达式

1.1 正则表达式的用途

用一些精心构造的语句,或者说一些由文本和特殊指令构成的高度简练的字符,串用来完成搜索和替换操作。

1.2 如何使用正则表达式

用正则表达式进行搜索

用正则表达式进行替换

1.3 什么是正则表达式

简单地说,正则表达式是一些用来匹配和处理文本的字符串。正则表达式是用正则表达式语言创建的,这种语言专门就是为了解决我们前面所描述的种种问题的。与其他程序设计语言一样,正则表达式语言也有必须要学习的特殊语法和指令。

正则表达式语言并不是一种完备的程序设计语言,它甚至算不上是一种能够直接安装并运行的程序或实用工具。更准确地说,正则表达式语言是内置于其他语言或软件产品里的"迷你"语言。

正则表达式语言虽然也被称为一种语言,但它与人们对语言的印象相去甚远。

2 匹配单个字符

2.1 匹配普通文本

正则表达式可以包含普通文本(甚至可以只包含普通文本)。当然,这种正则表达式纯粹就是一 种浪费。

2.2 匹配任意字符

在正则表达式里,特殊字符(或字符集合)用来标示要搜索的东西。**.字符(英文句号)**可以匹配任意单个字。

.字符可以匹配任意单个字符、字母、数字甚至是.字符本身。

在同一个正则表达式里允许使用多个.字符,它们既可以共同出现(一个接着一个------..将匹配连续的任意两个字符),也可以分别出现在模式的不同位置。

2.3 匹配特殊字符

.字符在正则表达式里有着特殊的含义。如果模式里需要一个.,就要想办法来告诉正则表达式你需要的是.字符本身而不是它在正则表达式里的特殊含义。为此,你必须在.的前面加上一个\(反斜杠)字符来对它进行转义。\是一个元字符(metacharacter,表示"这个字符有特殊含义,代表的不是字符本身")。因此,.表示匹配任意单个字符,\.表示匹配.字符本身。

在正则表达式里,\字符总是出现在具有特殊含义字符序列的开头,这个序列可以由一个或多个字符构成。

2.4 小结

正则表达式,也被称为模式,其实是一些由字符构成的字符串。这些字符可以是字面字符(普通文本)或元字符(有特殊含义的字符)。在这一章里,介绍了如何使用普通文本和元字符去匹配单个字符。.可以匹配任意单个字符\用来对字符进行转义。在正则表达式里,有特殊含义的字符序列总是以\字符开头。

3 匹配一组字符

3.1 匹配多个字符中的某一个

在正则表达式里,我们可以使用元字符**[和]**来定义一个字符集合。在使用[和]定义的字符集合里,出现在[和]之间的所有字符都是该集合的组成部分,必须匹配其中某个成员(但并非全部)。

3.2 利用字符集合区间

在使用正则表达式的时候,会频繁地用到一些字符区间(0~9、A~Z等)。为了简化字符区间的定义,正则表达式提供了一个特殊的元字符:可以用**-(连字符)**来定义字符区间。

字符区间的首、尾字符可以是ASCII字符表里的任意字符。但在实际工作中,最常用的字符区间还是数字字符区间和字母字符区间。

提示 在定义一个字符区间的时候,一定要避免让这个区间的尾字符小于它的首字符(例如[3-1])。这种区间是没有意义的,而且往往会让整个模式失效。

注意-(连字符)是一个特殊的元字符,它只有出现在[和]之间的时候才是元字符。在字符集合以外的地方,-只是一个普通字符,只能与-本身相匹配。因此,在正则表达式里,-字符不需要被转义。

在同一个字符集合里可以给出多个字符区间。比如说,下面这个模式可以匹配任何一个字母(无论大小写)或数字,但除此以外的其他字符(既不是数字也不是字母的字符)都不匹配: [A-Za-z0-9]

3.3 排除

不用逐个列出你要匹配的字符(如果只是要把一小部分字符排除在外的话,这种写法就太冗长了),可以使用元字符^来排除某个字符集合。

注意 ^的效果将作用于给定字符集合里的所有字符或字符区间,而不是仅限于紧跟在^字符后面的那一个字符或字符区间。

3.4 小结

元字符[和]用来定义一个字符集合,其含义是必须匹配该集合里的字符之一(各个字符之间是OR 的关系,而不是 AND 的关系)。

定义一个字符集合的具体做法有两种:一是把所有的字符都列举出来;二是利用元字符-以字符区间的方式给出。

可以用元字符^排除字符集合,强制匹配指定字符集合之外的字符。

4 使用元字符

4.1 再谈转义

元字符是一些在正则表达式里有着特殊含义的字符。任何一个元字符都可以通过在前面加上一个反斜杠字符(\)进行转义。

警告 配对的元字符(比如[或])不用作元字符时必须被转义,否则正则表达式解析器很可能会抛出一个错误。

对元字符进行转义需要用到\字符。这意味着\字符也是一个元字符,它的特殊含义是对其他元字符进行转义。在需要匹配\本身的时候,我们必须把它转义为\\。

4.2 匹配空白字符

元字符大致可以分为两种:一种是用来匹配文本的(比如.),另一种是正则表达式语法的组成部分(比如[和])。

空白元字符

|---------|--------------------------|
| [\b] | 回退(并删除)一个字符(Backspace 键) |
| \f | 换页符 |
| \n | 换行符 |
| \r | 回车符 |
| \t | 制表符(Tab 键) |
| \v | 垂直制表符 |

4.3 匹配特定的字符类型

4.3.1 匹配数字(与非数字)

|-----|-------------------------|
| \d | 任何一个数字字符(等价于[0-9]) |
| \D | 任何一个非数字字符(等价于[^0-9]) |

提示 正则表达式的写法几乎总是不止一种。挑选你自己觉得最舒服的那种即可。

4.3.2 匹配字母数字(与非字母数字)

|-----|------------------------------------------------------|
| \w | 任何一个字母数字 字符(大小写均可)或下划线字符(等价于 [a-zA-Z0-9_]) |
| \W | 任何一个非字母数字或非下划线字符(等价于[^a-zA-Z0-9_]) |

4.3.3 匹配空白字符(与非空白字符)

|-----|-------------------------------------|
| \s | 任何一个空白字符(等价于[\f\n\r\t\v]) |
| \S | 任何一个非空白字符(等价于[^\f\n\r\t\v]) |

注意 用来匹配退格字符的[\b]元字符不在\s的覆盖范范围内,\S也没有将其排除。

4.3.4 匹配十六进制或八进制数

|-----|------|
| \x | 十六进数 |
| \o | 八进制数 |

注意 有不少正则表达式实现还允许使用\c前缀来指定各种控制字符。比如说,\cZ可以匹配Ctrl-Z。不过,在实践中,极少会用到这种语法。

4.4 使用POSIX字符类

|--------------|----------------------------------------|
| [:alnum:] | 任何一个字母或数字(等价于[a-zA-Z0-9]) |
| [:alpha:] | 任何一个字母(等价于[a-zA-Z]) |
| [:blank:] | 空格或制表符(等价于[\t]) |
| [:cntrl:] | ASCII 控制字符(ASCII 0 到 31,再加上 ASCII 127) |
| [:digit:] | 任何一个数字(等价于[0-9]) |
| [:graph:] | 和[:print:]一样,但不包括空格 |
| [:lower:] | 任何一个小写字母(等价于[a-z]) |
| :print:] | 任何一个可打印字符 |
| [:punct:] | 既不属于[:alnum:],也不属于[:cntrl:]的任何一个字符 |
| [:space:] | 任何一个空白字符,包括空格(等价于[\f\n\r\t\v ]) |
| [:upper:] | 任何一个大写字母(等价于[A-Z]) |
| [:xdigit:] | 何一个十六进制数字(等价于[a-fA-F0-9] |

注意 JavaScript不支持在正则表达式里使用POSIX字符类。

4.5 小结

这一章介绍了匹配特定字符(制表符、换行符等)或字符集合或字符类(数字、字母数字字符等)的元字符。这些简写的元字符和POSIX字符类可以用来简化正则表达式模式。

5 重复匹配

5.1 有多少个匹配

|----|----------------------------------|
| + | 匹配一个或多个字符(至少一个;不匹配零个字符的情况)。 |
| * | 匹配零个或多个字符。 |
| ? | 只能匹配某个字符(或字符集合)的零次或一次出现,最多不超过一次。 |

5.2 匹配的重复次数

正则表达式里的+、*和?解决了许多问题,但有时候光靠它们还不够。请思考以下问题。

  • +和*匹配的字符个数没有上限。我们无法为其匹配的字符个数设定一个最大值。
  • +、*和?匹配的字符最小数量是零个或一个。我们无法明确地为其匹配的字符个数另行设定一个最小值。
  • 我们无法指定具体的匹配次数。
5.2.1 具体的重复匹配

要想设置具体的匹配次数,把数字写在{和}之间即可。比如说,{3} 意味着匹配前一个字符(或字符集合)3 次。如果只能匹配 2 次,则不算是匹配成功。

5.2.2 区间范围

{}语法还可以用来为重复匹配次数设定一个区间范围,也就是匹配的最小次数和最大次数。比如说,{2,4},意味着最少重复 2 次,最多重复 4 次

5.2.3 匹配"至少重复多少次"

重复范围的最后一种用法是指定至少要匹配多少次,比如说,{3,} 表示至少重复 3 次,换句话说,就是"重复 3 次或 更多次"。

注意 +在功能上等价于{1,}。

5.3 防止过度匹配

因为*和+都是所谓的"贪婪型"(greedy) 元字符,其匹配行为是多多益善而不是适可而止。它们会尽可能地**从一段文本的开头一直匹配到末尾,而不是碰到第一个匹配时就停止。**这是有意设计 的,量词就是贪婪的。

在不需要这种"贪婪行为"的时候该怎么办?答案是使用这些量词的"懒惰型"(lazy) 版本(之所以称之为"懒惰型"是因为其匹配尽可能少的字符,而非尽可能多地去匹配)。懒惰型量词的写法是在贪婪型量词后面加上一个?。

贪婪型量词和懒惰型量词

|-------|-------|
| 贪婪型量词 | 懒惰型量词 |
| * | *? |
| + | +? |
| {n,} | {n,}? |

5.4 小结

在使用重复匹配时,正则表达式的真正威力就显现出来了。本章介绍了+(匹配字符或字符集合的一次或多次重复出现)、*(匹配字符或字符集合的零次或多次重复出现)和?(匹配字符或字符集合的零次或一次出现)的用法。要想获得更大的控制权,你可以用重复范围{}来精确地设定重复次数或是重复的最小次数和最大次数。量词分"贪婪型"和"懒惰型"两种,前者会尽可能多地匹配,后者则会尽可能少地匹配

6 位置匹配

位置匹配用于指定应该在文本中什么地方进行匹配操作。

6.1 边界

边界,就是一些用于指定模式前后位置(或边界)的特殊元字符。

6.2 单词边界

第一种边界(也是最常用到的)是由**\b**指定的单词边界。顾名思义,\b用来匹配一个单词的开头或结尾。

b 是英文 boundary(边界)的首字母。

注意 \b 到底匹配什么东西呢?正则表达式引擎不懂英语(事实上,它不懂任何人类语言),所以也不知道什么是单词边界。简单地说,\b匹配的是字符之间的一个位置:一边是单词(能够被\w匹配的字母数字字符和下划线),另一边是其他内容(能够被\W匹配的字符)。

重要的是要认识到,如果你想匹配一个完整的单词,就必须在要匹配的文本的前后都加上\b。

如果你不想匹配单词边界,那么可以使用\B。\B-\B将匹配一个前后都不是单词边界的连字符。

6.3 字符串边界

单词边界可以用来对单词位置进行匹配(单词的开头、单词的结尾、整个单词等)。字符串边界有着类似的用途,只不过用于在字符串首尾进行模式匹配。字符串边界元字符有两个:^代表字符串开头,$代表字符串结尾

多行模式

^和$通常分别匹配字符串的首尾位置。但也有例外,或者说有办法改变这种行为。

许多正则表达式都支持使用一些特殊的元字符去改变另外一些元字符的行为,(?m)就是其中之一,它可用于启用多行模式(multiline mode)。多行模式迫使正则表达式引擎将换行符视为字符串分隔符,这样一来,^既可以匹配字符串开头,也可以匹配换行符之后的起始位置(新行);$不仅能匹配字符串结尾,还能匹配换行符之后的结束位置。

在使用时,(?m)必须出现在整个模式的最前面。

6.4 小结

正则表达式不仅可以用来匹配任意长度的文本块,还可以用来匹配出现在字符串中特定位置的文本。\b 用来指定一个单词边界(\B 刚好相反)。^和用来指定字符串边界(字符串的开头和结尾)。**如果与(?m)配合使用,\^和还可以匹配换行符之前或之后的字符串。**

7 使用子表达式

本章将学习如何运用子表达式(subexpression)对表达式进行分组。

7.1 理解子表达式

?或*或{2},只是用来表明重复次数的元字符,指定的重复次数只作用于紧挨着它的前一个字符,而不能作用于更长的部分。

7.2 使用子表达式进行分组

子表达式是更长的表达式的一部分。划分子表达式的目的是为了将其视为单一的实体来使用。子表达式必须出现在字符**(和)**之间。

提示 (和)是元字符。如果需要匹配(和)本身,就必须使用转义序列\(和\)。

7.3 子表达式的嵌套

子表达式允许嵌套,还可以多重嵌套。

7.4 小结

子表达式使用(和)来定义,作用是把表达式的各个部分划分在一起。子表达式的常见用途包括:通过重复次数元字符准确地控制重复内容,正确地定义|的多项分支。如有必要,子表达式还允许嵌套使用。

8 反向引用

子表达式的另一种重要用途------反向引用(backreference)

8.1 理解反向引用

后面匹配的内容需要引用以前匹配一部分。

"反向引用"指的是这些实体引用的是先前的子表达式。

8.2 反向引用匹配

\1 到底是什么意思?它匹配模式中所使用的第一个子表达式,\2 匹配第二个子表达式、\3 匹配第三个,以此类推。

提示 可以把反向引用想象成变量。

警告 反向引用只能用来引用括号里的子表达式。

8.3 替换操作

当用到反向引用时,正则表达式的替换操作才会真正发挥作用。

大小写转换

用来进行大小写转换的元字符

|-----|------------------------|
| 元字符 | 说 明 |
| \E | 结束\L 或\U 转换 |
| \l | 把下一个字符转换为小写 |
| \L | 把\L 到\E 之间的字符全部转换为小写 |
| \u | 把下一个字符转换为大写 |
| \U | 把\U 到\E 之间的字符全部转换为大写 |

8.4 小结

子表达式用来定义字符或表达式的集合。除了可以用于重复匹配(详见第 7 章),还可以在模式的内部被引用。这种引用被称为反向引用(遗憾的是,反向引用的语法在不同的正则表达式中存在差异)。在文本匹配和替换操作中,反向引用颇为有用。

9 环视

到目前为止,我们见过的正则表达式都是用来匹配文本的,但有时你可能想用正则表达式标记要匹配的文本位置(而不是文本自身)。这就要用到环视(lookaround,能够前后查看)了,这正是本章要讨论的话题。

注意 所有主流的正则表达式实现都支持前者,但支持后者的就没那么多了。Java、.NET、PHP、Python和Perl都支持向后查看(其中一些有限制),JavaScript则不支持

9.1 环视简介

环视就是想办法构造出一种模式,该模式中包含一些不用被返回的匹配------这些匹配是为了找出正确的匹配位置,其自身不属于最终的匹配结果。换句话说,你需要进行"环视"。

9.2 向前查看

向前查看模式的语法是一个以**?=**开头的子表达式,需要匹配的文本跟在=的后面。

?=告诉正则表达式引擎:匹配文本只是为了向前查看(不用消耗该部分)。

9.3 向后查看

向后查看操作符是?<=

因为要匹配文本相对于模式的方向(对应"向前查看"的"前")与文本阅读方向正相反,记忆向后查看<号的方向容易引起误解,所以可以直接将"?<="读成"向......之后查看"。

警告 向前查看模式的长度是可变的,其中可以包含.和+等量词,所以非常灵活。向后查看模式则只能是固定长度。几乎所有的正则表达式实现都有此限制。

9.4 结合向前查看和向后查看

向前查看和向后查看可以组合在一起使用

9.5 否定式环视

向前查看和向后查看通常都是用来匹配文本,主要用于指定作为匹配结果返回的文本位置(指明所需匹配之前或之后的文本)。这种用法被称为肯定式向前查看(positive lookahead)和肯定式向 后查看(positive lookbehind)。术语"肯定式"(positive)是指要执行的是匹配操作

环视还有一种不太常见的形式叫作否定式环视(negative lookaround)。否定式向前查看(negative lookahead)会向前查看不匹配指定模式的文本,否定式向后查看(negative lookbehind)则向后查看不匹配指定模式的文本。

各种环视操作

|--------|---------|
| 种 类 | 说 明 |
| (?=) | 肯定式向前查看 |
| (?!) | 否定式向前查看 |
| (?<=) | 肯定式向后查看 |
| (?<!) | 否定式向后查看 |

提示 一般来说,凡是支持向前查看的正则表达式实现也都支持肯定式向前查看和否定式向前查看。类似地,凡是支持向后查看的正则表达式实现也都支持肯定式向后查看和否定式向后查看。

9.6 小结

环视可以更精细地控制最终的返回结果。

环视操作允许利用子表达式来指定文本匹配操作的发生位置,但同时又不会消耗匹配到的文本 (不出现在最终的匹配结果里)。

肯定式向前查看使用(?=)来定义,否定式向前查看使用(?!)来定义。

有些正则表达式实现还支持肯定式向后查看(相应的操作符是(?<=))和否定式向后查看(相应的操作符是(?<!))。

10 嵌入式条件

正则表达式语言还有一个威力强大但不经常被用到的特性:在表达式的内部嵌入条件处理。

10.1 为什么要嵌入条件

如果不使用条件处理根本无法编写的模式。

警告 并非所有的正则表达式实现都支持条件处理。

10.2 正则表达式里的条件

正则表达式里的条件要用?来定义。事实上,我们已经见过几种非常具体的条件了。

  • ?匹配前一个字符或表达式,如果它存在的话。
  • ?=和?<=匹配前面或后面的文本,如果它存在的话。

嵌入式条件语法也使用了?,这并没有什么让人感到吃惊的地方,因为嵌入式条件不外乎以下两种情况。

  • 根据反向引用来进行条件处理。
  • 根据环视来进行条件处理。
10.2.1 反向引用条件

反向引用条件仅在一个前面的子表达式得以匹配的情况下才允许使用另一个表达式。

用来定义这种条件的语法是(?(backreference)true),其中?表明这是一个条件,括号里的 backreference是一个反向引用,仅当反向引用立即出现时,才对表达式求值。

条件还可以有else表达式,仅当给定的反向引用不存在(也就是不符合条件)时才执行该表达式。用来定义这种条件的语法是(?(backreference) true|false)。此语法接受一个条件和两个分别在符合/不符合该条件时执行的表达式。

10.2.2 环视条件

环视条件允许根据向前查看或向后查看操作是否成功来决定要不要执行表达式。环视条件的语法与反向引用条件的语法大同小异,只需把反向引用(括号里的反向引用编号)替换为一个完整的环视表达式就行了。

向前查看和向后查看(肯定式和否定式皆可)都可作为条件,也可使用可选的else表达式(语法和之前看到的一样,即|expression)。

提示 环视条件用的并不是很多,因为使用更简单的方法往往可以实现差不多的结果。

10.3 小结

在正则表达式模式里可以嵌入条件,只有满足条件(或者没有满足)的时候,才执行相应的表达式。条件可以是反向引用(检查其是否存在),也可以是环视操作。

11 常见问题的正则表达式解决方案

|--------------|-----------------------------------------------------------------------------------------------------------------|
| 北美电话号码 | [\(.]?[2-9]\d\d[\).]?[ -]?[2-9]\d\d[-.]\d{4} |
| 中国固定电话号码 | ((?=\()\(0[1-9]\d{1,2}\)|0[1-9]\d{1,2}-)\d{2,4}\s?\d{3,4} |
| 美国ZIP编码 | \d{5}(-\d{4})? |
| 中国邮政编码 | \d(9|[0-7])\d{4} |
| 加拿大邮政编码 | [ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d |
| 英国邮政编码 | [A-Z]{1,2}\d[A-Z\d]? \d[ABD-HJLNP-UW-Z]{2} |
| 美国社会安全号码SSN | \d{3}-\d{2}-\d{4} |
| 中国公民身份号码 | [1-8]\d{16}[0-9X] |
| IP地址 | (((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3} ((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5])) |
| URL | https?:\/\/[-\w.]+(:\d+)?(\/([\w\/.]*)?)? |
| 完整的URL | https?:\/\/(\w*:\w*@)?[-\w.]+(:\d+)?(\/([\w\/
.]*(\?\S+)?)?)? |
| 电子邮件地址 | (\w+\.)*\w+@(\w+\.)+[A-Za-z]+ |
| HTML注释 | <!-{2,}.*?-{2,}> |
| JavaScript注释 | \/\/.* |
| 信用卡号码 | MasterCard卡 5[1-5]\d{14} |
| | Visa 卡 4\d{12}(\d{3})? |

小结

在本章,你看到了我们在前面章节里介绍的许多概念和思路在实际中的应用例子。这些例子里的模式可以随意拿来使用或改动。我们将其作为一份见面礼,欢迎大家进入令人兴奋且富有成效的正则表达式世界。

附录A 常见应用软件和编程语言中的正则表达式

相关推荐
小小洋洋44 分钟前
BLE MESH学习1-基于沁恒CH582学习
学习
m0_689618282 小时前
水凝胶发生器,不对称设计妙,医电应用前景广
笔记
Ace'2 小时前
每日一题&&学习笔记
笔记·学习
IM_DALLA2 小时前
【Verilog学习日常】—牛客网刷题—Verilog进阶挑战—VL25
学习·fpga开发·verilog学习
挥剑决浮云 -2 小时前
Linux 之 安装软件、GCC编译器、Linux 操作系统基础
linux·服务器·c语言·c++·经验分享·笔记
丶Darling.2 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
新晓·故知3 小时前
<基于递归实现线索二叉树的构造及遍历算法探讨>
数据结构·经验分享·笔记·算法·链表
魔理沙偷走了BUG3 小时前
【数学分析笔记】第4章第4节 复合函数求导法则及其应用(3)
笔记·数学分析
z樾4 小时前
Github界面学习
学习