{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 正则表达式入门" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 基本概念" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**正则表达式**(*regular expression*)是一个罕见的例子,它既有极其深刻的理论背景,又成为了一种极其常用的工具。\n", "\n", "Wikipedia 上对正则表达式的说明如下:\n", "\n", "> **正则表达式**(英语:Regular Expression,在代码中常简写为 *regex*、*regexp* 或 *RE*),又称*正规表示式*、*正规表示法*、*正规运算式*、*规则运算式*、*常规表示法*,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。\n", "\n", "英文里 *regular* 本是“正规”的意思,在翻译 *regular expression* 时,可能是为了和太过通用的“正规”一词区分开来,使用了“正则”作为技术名词特有的译法。\n", "\n", "在正则表达式中,还有两个词儿需要了解:\n", "\n", "* 一个是 **pattern**,一个写出来的正则表达式就是一个 *pattern*,也就是前面定义里的“用于匹配的语法规则”;\n", "* 一个是 **match**,可以用作动词和名词,作动词是就是拿上述 *pattern* 去字符串中寻找符合规则的子串,作名词就是找到的那个子串。\n", "\n", "所有主流的程序设计语言都内置对 *regex* 的支持,实际上就是内置了一个“正则表达式引擎”,这个引擎能够拿着我们写好的规则(*pattern*)去任何字符串里寻找匹配的子串,还提供找到之后替换等功能。\n", "\n", "我们来看看 Python 里的例子:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import re" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['row', 'fox', 'dog']" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'The quick brown fox jumps over the lazy dog'\n", "pattern = re.compile(r'\\wo\\w')\n", "re.findall(pattern, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "首先引入 Python 自带的正则表达式模块 `re`,然后用 `re` 提供的 `compile()` 函数把我们书写的正则规则 `\\wo\\w` 编译成一个 *pattern*,这个规则里的 `\\w` 表示任意字母数字或者下划线,而 `o` 就是字母 ‘o’。\n", "\n", "> 这个规则字符串前面的 `r` 表示 *raw string*,Python 不会对其中的 `\\` 做转义处理。\n", "\n", "最后调用 `re` 提供的 `findall()` 函数用这个 *pattern* 去寻找 `str` 中所有满足这个规则的匹配(*matches*)。\n", "\n", "输出结果是所有 *matches* 组成的列表。\n", "\n", "正则引擎不仅可以寻找匹配,还可以**捕获**(*capture*)匹配中的一个片段,可以将其**替换**(*replace* 或 *substitute*)成别的字符串。我们来看下面一个例子:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The black dog wears a white hat.'" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'The white dog wears a black hat.'\n", "\n", "pattern = r'The (white|black) dog wears a (white|black) hat.'\n", "repl = r'The \\2 dog wears a \\1 hat.'\n", "re.sub(pattern, repl, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "和 `re.find()` 或者 `re.findall()` 不一样,`re.sub()` 有三个参数,第一个是用来匹配的规则 `pattern`,第二个是找到 *matches* 之后执行替换的目标模板 `target`,第三个是被操作的字符串。这里需要留意的是:\n", "* 正则规则 `pattern` 中有两个小括号 `()`,这两个小括号的作用就是“捕获”匹配到的内容,规则中第一对小括号捕获的内容会被存在临时变量 `$1` 中,第二对小括号对应的内容则存在 `$2` 中,依此类推;\n", "* 替换目标模板 `target` 中有一个 `\\1` 和一个 `\\2`,代表这里要换成 `$1` 和 `$2` 的值。\n", "\n", "于是上面的代码就把原句中的 *white* 和 *black* 交换了位置。\n", "\n", "试试看下面的代码,能预测它的输出吗?" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The white dog wears a white hat.'" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "repl = r'The \\1 dog wears a \\1 hat.'\n", "re.sub(pattern, repl, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "小结一下:\n", "* 我们可以按照正则表达式(*regex*)的语法书写正则规则(*pattern*);\n", "* 正则引擎可以用这个 *pattern* 去匹配(*match*)指定字符串;\n", "* 输出所有匹配(*matches*);\n", "* 这个过程中还可以捕获匹配中的特定部分,并进行替换。\n", "\n", "使用 *regex* 的最主要场景有二:\n", "* 用规则去匹配字符串,确认字符串是不是包含符合规则定义的子串,通常用来确认字符串是不是符合特定“格式”;\n", "* 用规则去匹配字符串,捕获并替换其中的特定片段。\n", "\n", "顺便,在自学的过程中,想尽一切办法把一切术语用简单直白的“人话”重新表述,是特别有效的促进进步的行为模式,也可以检验你是不是真的搞懂学会了。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 准备工作" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "虽然基本概念并不复杂,但 *regex* 还是一种比较抽象的语言,其语法远不如 Python 那么直观易懂,所以在继续学习前我们先介绍两个工具,在学习 *regex* 的过程中会很有帮助。\n", "\n", "第一个是正则规则可视化工具,可以对输入的规则给出可视化的解析,比如 [Regexper](https://regexper.com)。\n", "\n", "第二个是正则规则测试工具,可以对输入的规则给出说明,并对给出的测试字符串运行规则给出结果,比如 [regex101](https://regex101.com)。\n", "\n", "你现在就可以试试这两个工具,将前面例子里的规则填进去,看看效果,还可以试着写点别的规则玩玩,反正也不怕出错。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "另外,我们还需要个测试文本文件,用来当作下面练习使用正则表达式时的字符串,因为有时长一点才能说明一些问题,所以存在一个文件中比直接写在代码里好。我们已经把这个文件放在 `assets` 自目录下,文件名是 `sample.txt`。你可以打开这个文件看看,里面有很多用来测试 *regex* 的字符串,是个不错的测试基底。\n", "\n", "下面的代码就拿这个文件做测试:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['begin', 'began', 'begun', 'begins', 'begin']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "with open('assets/sample.txt', 'r') as f:\n", " str = f.read()\n", "pattern = r'beg[iau]ns?'\n", "re.findall(pattern, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "第一行是 Python 操作文件的标准方法,内置函数 `open` 按照指定方式(`'r'` 表示只读模式)打开指定文件(第一个参数指定路径),并用一个文件对象(`f`)的方式给我们使用;下面用 `f.read()` 来读取文件的内容并赋给 `str` 变量。\n", "\n", "这里我们使用的 `pattern` 比前面复杂多了,你可以把 `beg[iau]ns?` 分别贴到 [Regexper](https://regexper.com/#beg%5Biau%5Dns%3F) 和 [regex101](https://regex101.com) 看看。你会看到可视化解析能很清晰的告诉我们这个规则是什么意思。所以在下面每个例子中你都可以这么做。\n", "\n", "最后 `findall()` 将所有 *matches* 输出成一个列表。 " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 操作符,原子与优先级" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "学到这里的你已经不是“啥都不懂”的人了,你已经知道一个事实:编程语言无非是用来运算的。\n", "\n", "所谓的运算,就有**操作符**(*operators*)和**操作元**(*operands*),而操作符肯定是有优先级的,不然的话,那么多操作元和操作符放在一起,究竟先操作哪个呢?就和四则运算(括号由内向外处理、先乘除后加减、同级别从左到右等)是一个道理。\n", "\n", "*Regex* 本身就是个迷你语言(*mini language*),它也有很多操作符,操作符也有优先级;而它的操作元有个专门名称,叫做**原子**(*atom*)。\n", "\n", "看看下面列出的 *regex* 操作符优先级,你会对它有相当不错的了解:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "| 排列 | 原子与操作符优先级 |(从高到低)|\n", "|---|-----------------------------------|------------------------|\n", "| 1 | 转义符号 (Escaping Symbol) | `\\` |\n", "| 2 | 分组、捕获 (Grouping or Capturing) | `(...)` `(?:...)` `(?=...)` `(?!...)` `(?<=...)` `(?<!...)` |\n", "| 3 | 数量 (Quantifiers) | `a*` `a+` `a?` `a{n, m}` |\n", "| 4 | 序列与定位(Sequence and Anchor)| `abc` `^` `$` `\\b` `\\B` |\n", "| 5 | 或(Alternation)| <code>a|b|c</code> |\n", "| 6 | 原子 (Atoms) | `a` `[^abc]` `\\t` `\\r` `\\n` `\\d` `\\D` `\\s` `\\S` `\\w` `\\W` `.` |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "下面我们来看看所有这些东西到底是什么。" ] }, { "cell_type": "markdown", "metadata": { "toc-hr-collapsed": false }, "source": [ "## 原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Regex pattern* 中最基本的元素被称为**原子**(*atom*),包括下面这些类型:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 本义字符" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "最基本的原子,就是本义字符,它们都是单个字符。\n", "\n", "本义字符包括从 `a` 到 `z`,`A` 到 `Z`,`0` 到 `9`,还有下划线 `_`,它们所代表的就是它们的字面值。\n", "\n", "相当于 Python 中的 `string.ascii_letters`、`string.digits` 及 `_`。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "以下字符在 *regex* 中都有特殊含义:`\\` `+` `*` `.` `?` `-` `^` `$` `|` `(` `)` `[` `]` `{` `}` `<` `>`。\n", "\n", "所以你在写规则时,如果需要匹配这些字符,建议都在前面加上转义符 `\\`,比如,你想匹配 `$`,那你就写 `\\$`,或者想匹配 `|` 那就写 `\\|`。\n", "\n", "跟过往一样,所有的细节都很重要,它们就是需要花时间逐步熟悉到牢记。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 集合原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "原子的集合还是原子。\n", "\n", "*Regex* 使用方括号 `[]` 来标示集合原子,`[abc]` 的意思就是这个位置匹配 `a` 或者 `b` 或者 `c`,即 `abc` 中任一字符。\n", "\n", "如下面的例子 [`beg[iau]n`](https://regexper.com#beg[iau]n) 能够匹配 `begin`、`began` 和 `begun`:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['begin', 'began', 'begun', 'begin']" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'begin began begun bigins begining'\n", "re.findall(r'beg[iau]n', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在集合原子中,我们可以使用两个操作符:`-`(表示一个区间)和 `^`(表示排除)。\n", "\n", "* `[a-z]` 表示从小写字母 `a` 到 `z` 中的任意一个字符。\n", "* `[^abc]` 表示 `abc` 以外的其它任意字符;注意,**一个集合原子中,`^` 符号只能用一次,只能紧跟在 `[` 之后**,否则不起作用。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 类别原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "类别原子,是指能够代表“一类字符”的原子,类别有特殊转义符定义,包括下面这些:\n", "* `\\d` 任意数字;等价于 `[0-9]`\n", "* `\\D` 任意非数字;等价于 `[^0-9]`\n", "* `\\w` 任意本义字符;等价于 `[a-zA-Z0-9_]`\n", "* `\\W` 任意非本义字符;等价于 `[^a-zA-Z0-9_]`\n", "* `\\s` 任意空白;相当于 `[ \\f\\n\\r\\t\\v]`(注意,方括号内第一个字符是空格符号)\n", "* `\\S` 任意非空白;相当于 `[^ \\f\\n\\r\\t\\v]`(注意,紧随 `^` 之后的是一个空格符号)\n", "* `.` 除 `\\r` `\\n` 之外的任意字符;相当于 `[^\\r\\n]`\n", "\n", "类别原子挺好记的,如果你知道各个字母是哪个词的首字母的话:\n", "* `d` 是 *digits*;\n", "* `w` 是 *word characters*;\n", "* `s` 是 *spaces*。\n", "\n", "另外,在空白的集合 `[ \\f\\n\\r\\t\\v]` 中:`\\f` 是分页符;`\\n` `\\r` 是换行符;`\\t` 是制表符;`\\v` 是纵向制表符(很少用到)。各种关于空白的转义符也同样挺好记忆的,如果你知道各个字母是那个词的首字母的话:\n", "* `f` 是 *flip*;\n", "* `n` 是 *new line*;\n", "* `r` 是 *return*;\n", "* `t` 是 *tab*;\n", "* `v` 是 *vertical tab*。" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['542-', '270-']" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = '<dl>(843) 542-4256</dl> <dl>(431) 270-9664</dl>'\n", "re.findall(r'\\d\\d\\d\\-', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 边界原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "边界原子用来指定“边界”,有时也叫“定位操作符”:\n", "* `^` 匹配被搜索字符串的开始位置;\n", "* `$` 匹配被搜索字符串的结束位置;\n", "* `\\b` 匹配单词的边界;[`er\\b`](https://regexper.com#er%5Cb),能匹配 `coder` 中的 `er`,却不能匹配 `error` 中的 `er`;\n", "* `\\B` 匹配非单词边界;[`er\\B`](https://regexper.com#er%5CB),能匹配 `error` 中的 `er`,却不能匹配 `coder` 中的 `er`。" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['er', 'er', 'er', 'er']" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'never ever verb never however everest'\n", "re.findall(r'er\\b', str)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['er', 'er']" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'er\\B', str)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['nev', 'nev']" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'nev', str)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['nev']" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'^nev', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`^` 和 `$` 在 Python 语言中也可以用 `\\A` `\\Z`。事实上,每种语言或多或少都对 *regex* 有自己的定制,不过本章讨论的绝大多数细节,都是通用的。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 组合原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "我们可以用圆括号 `()` 将多个单字符原子组合成一个原子,这么做的效果是 `()` 内的字符串将被当作一整个原子,可以被随后我们要讲解的数量操作符操作。这个语法叫做**分组**或者**组合**(*grouping*)。\n", "\n", "另外,`()` 这个操作符,除了用于 *grouping*,还用于**捕获**(*capturing*),前面看到过,后面还会讲。\n", "\n", "所以现在你应该可以分清楚,[`er`](https://regexper.com#er)、[`[er]`](https://regexper.com#[er]) 和 [`(er)`](https://regexper.com#(er) 含义各不相同。\n", "\n", "> * `er` 是两个原子,`'e'` 和紧随其后的 `'r'`;\n", "> * `[er]` 是一个原子,或者 `'e'` 或者 `'r'`;\n", "> * `(er)` 是一个原子,`'er'`。\n", "\n", "下一节中讲到数量操作符的时候,会再次强调这点。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 数量操作符" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "数量操作符有这几个:`+` `?` `*` `{n, m}`。\n", "\n", "它们是用来限定位于它们之前的原子出现的个数;不加数量操作符代表出现一次且仅出现一次,而加上数量操作符的含义分别是:\n", "\n", "* `+` 代表前面的原子必须至少出现一次,即 出现次数 ≧ 1;例如,[`go+gle`](https://regexper.com#go+gle)可以匹配 `google` `gooogle` `goooogle` 等;\n", "* `?` 代表前面的原子最多只可以出现一次,即 0 ≦ 出现次数 ≦ 1;例如,[`colou?red`](https://regexper.com#colou?red)可以匹配 `colored` 或者 `coloured`;\n", "* `*` 代表前面的原子可以不出现,也可以出现一次或者多次,即 出现次数 ≧ 0;例如,[`520*`](https://regexper.com#520*)可以匹配 `52` `520` `52000` `5200000` `520000000000` 等;\n", "* `{n}` 代表前面的原子确定出现 n 次;`{n,}` 代表前面的原子出现至少 n 次;`{n, m}` 代表前面的原子出现至少 `n` 次,至多 `m` 次;例如,[`go{2,5}gle`](https://regexper.com#go%7B2,5%7Dgle),能匹配 `google` `gooogle` `goooogle` 或 `gooooogle`,但不能匹配 `gogle` 和 `gooooooogle`。\n", "\n", "如下例:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['google', 'gooogle', 'goooogle', 'goooooogle']" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "with open('assets/sample.txt', 'r') as f:\n", " str = f.read()\n", "\n", "re.findall(r'go+gle', str)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['google', 'gooogle', 'goooogle']" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'go{2,5}gle', str)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['coloured', 'colored']" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'colou?red', str)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['520', '52000', '5200000', '520000000', '520000000000']" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'520*', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "数量操作符是对它之前的原子进行操作的,换言之,数量操作符的操作元是操作符之前的原子。\n", "\n", "上一节提到,要注意区别:`er`、`[er]` 和 `(er)` 各不相同。\n", "\n", "> * `er` 是两个原子,`'e'` 之后 `'r'`\n", "> * `[er]` 是一个原子,或者 `'e'` 或者 `'r'`;\n", "> * `(er)` 是一个原子,`'er'`" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['er', 'er', 'er', 'er']" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'error wonderer severeness'\n", "\n", "re.findall(r'er', str)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['e', 'r', 'r', 'r', 'e', 'r', 'e', 'r', 'e', 'e', 'r', 'e', 'e']" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'[er]', str)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['er', 'er', 'er', 'er']" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'(er)', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "这里我们还看不出 `er` 和 `(er)` 的区别,但是,加上数量操作符就不一样了——因为数量操作符只对它之前的那**一个**原子进行操作:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['err', 'er', 'er', 'er']" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'er+', str)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['err', 'r', 'erer', 'e', 'ere', 'e']" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'[er]+', str)" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['er', 'er', 'er']" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "re.findall(r'(er)+', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 或操作符" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "或操作符 `|` 是所有操作符中优先级最低的,数量操作符的优先级比它高,所以,在 `|` 前后的原子被数量操作符(如果有的话)操作之后才交给 `|` 操作。简单理解就是规则被 `|` 分成若干段,各段里出现一种就匹配成功。\n", "\n", "下面例子里,[`begin|began|begun`](https://regexper.com#begin%7Cbegan%7Cbegun) 能够匹配 `begin` 或 `began` 或 `begun`。" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['begin', 'began', 'begun', 'begin', 'begin']" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'begin began begun begins beginn'\n", "re.findall(r'begin|began|begun', str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**注意**:前面讲的集合原子方括号中的 `|` `()` 不会被当作特殊符号,而是被当作字符本身,没有或操作符和分组的作用。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 匹配并捕获" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**捕获**(*capture*)使用的是圆括号 `()`。使用圆括号可以捕获匹配中的特定片段,并将其值暂存进一个带有索引的列表,第一个片段是 `$1`,第二个是 `$2`,依此类推。随后,我们可以在替换的过程中使用 `\\1` `\\2` 中来引用所保存的值。\n", "\n", "比如前面我们讲过的例子:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The black dog wears a white hat.'" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'The white dog wears a black hat.'\n", "\n", "pattern = r'The (white|black) dog wears a (white|black) hat.'\n", "repl = r'The \\2 dog wears a \\1 hat.'\n", "re.sub(pattern, repl, str)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The white dog wears a white hat.'" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "repl = r'The \\1 dog wears a \\1 hat.'\n", "re.sub(pattern, repl, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "有时,你并不想捕获圆括号中的内容,在那个地方你使用括号的目的只是分组(*grouping*),而非捕获,所以不希望捕获那里的值占用一个 `$` 标号,那么就在圆括号内最开头加上 `?:`:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['black']" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str = 'The white dog wears a black hat.'\n", "pattern = r'The (?:white|black) dog wears a (white|black) hat.'\n", "re.findall(pattern, str)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The black dog wears a black hat.'" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "repl = r'The \\1 dog wears a \\1 hat.' # 之前的一处捕获,在替换时可被多次引用\n", "re.sub(pattern, repl, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "实际上在 Python 代码中使用正则表达式匹配和捕获以及随后的替换,有更灵活的方式,因为可以对那些值直接编程,`re.sub()` 中的 `repl` 参数甚至可以接收另外一个函数作为参数,具体可以查阅 Python 的[官方文档](https://docs.python.org/3/library/re.html)。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "非捕获匹配,还有几个**预查**(*look ahead*, *look behind*)操作符。\n", "\n", "所谓 *look ahead* 和 *look behind*,意思就是匹配一个部分时看看这个部分前后是不是符合某个规则,前后的部分只进行规则检查但不参与匹配:\n", "\n", "* `(?=pattern)`:*look ahead positive assert*,例如 [`Windows(?=95|98|NT|2000)`](https://regexper.com#%60Windows(?=95%7C98%7CNT%7C2000)%60) 能匹配 `Windows2000` 中的 `Windows`(注意匹配的只有 `Windows`,而不是 `Windows2000`),但不能匹配 `Windows3.1` 中的 `Windows`;\n", "* `(?!pattern)`:*look ahead negative assert*,例如 [`Windows(?!95|98|NT|2000)`](https://regexper.com#Windows(?=95%7C98%7CNT%7C2000)) 能匹配 `Windows3.1` 中的 `Windows`,但不能匹配 `Windows2000` 中的 `Windows`;\n", "* `(?<=pattern)`:*look behind positive assert*,这是检查匹配部分之前的字符,例如 [`(?<=95|98|NT|2000)Windows`](https://regexper.com#(?%3C=95%7C98%7CNT%7C2000)Windows) 能匹配 `2000Windows` 中的 `Windows`,但不能匹配 `3.1Windows` 中的 `Windows`;\n", "* `(?<!pattern)`:*look behind negative assert*,例如 `(?<!95|98|NT|2000)Windows` 能匹配 `3.1Windows` 中的 `Windows`,但不能匹配 `2000Windows` 中的 `Windows`。\n", " \n", "再次提醒,这几个都是非捕获的。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 控制标记" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在 Python 中控制标记(*flag*)是模块 `re` 下的一组布尔值,通过设置其真假会影响正则引擎的匹配和替换逻辑。\n", "\n", "另外也可以在 *pattern* 最开始写一个特殊的 `(?)` 结构来只影响当前这一个 *pattern*(这叫 *inline flag*)。\n", "\n", "下面是所有标记的简要介绍,供参考:\n", " \n", "`A`/`ASCII`,默认为 `False`\n", "* `\\d`, `\\D`, `\\w`, `\\W`, `\\s`, `\\S`, `\\b`, 和 `\\B` 等只限于 ASCII 字符\n", "* Inline flag:`(?a)`\n", "* Flag in `re` module:`re.A` `re.ASCII`\n", " \n", "`I`/`IGNORECASE`,默认为 `False`\n", "* 忽略字母大小写\n", "* Inline flag:`(?i)`\n", "* Flag in `re` module:`re.I` `re.IGNORECASE`\n", "\n", "`G`/`GLOBAL`,默认为 `True`\n", "* 找到第一个 match 之后不返回\n", "* Inline flag:`(?g)`\n", "* Flag in `re` module:不能更改,默认为 TRUE\n", " \n", "`L`/`LOCALE`,默认为 `False`\n", "* 由本地语言设置决定 `\\d`, `\\D`, `\\w`, `\\W`, `\\s`, `\\S`, `\\b`, 和 `\\B` 等等的内容\n", "* Inline flag:`(?L)`\n", "* Flag in `re` module:`re.L` `re.LOCALE`\n", " \n", "`M`/`MULTILINE`,默认为 `True`\n", "* 使用本标志后,`^` 和 `$` 匹配行首和行尾时,会增加换行符之前和之后的位置。\n", "* Inline flag:`(?m)`\n", "* Flag in `re` module:`re.M` `re.MULTILINE`\n", " \n", "`S`/`DOTALL`,默认为 `False`\n", "* 使 `.` 完全匹配任何字符,包括换行;没有这个标志,`.` 匹配除了 `n` `r` 之外的任何字符。\n", "* Inline flag:`(?s)`\n", "* Flag in `re` module:`re.S` `re.DOTALL`\n", " \n", "`X`/`VERBOSE`,默认为 `False`\n", "* 当该标志被指定时,Pattern 中的的空白符会被忽略,除非该空白符在圆括号或方括号中,或在反斜杠 `\\ ` 之后。这样做的结果是允许将注释写入 Pattern,这些注释会被 Regex 解析引擎忽略。注释用 `#` 号来标识,不过该符号不能在字符串或反斜杠之后。\n", "* Inline flag:`(?x)`\n", "* Flag in `re` module:`re.X` `re.VERBOSE`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 常用规则" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "前面快速的过了一遍 *regex* 的主要语法和用法,其实很多常用的 *regex* 规则都不需要自己的写,可以直接在网上找到,比如[这里](https://digitalfortress.tech/tricks/top-15-commonly-used-regex/),[这里](https://www.keycdn.com/support/regex-cheatsheet),还有[这里](https://code.tutsplus.com/tutorials/8-regular-expressions-you-should-know--net-6149)。\n", "\n", "有个特别经常被问而且特别难写完美的,是用来校验 Email 合法性的规则,有人甚至专门为这个建了个站点叫 [Email Address Regular Expression That 99.99% Works](https://emailregex.com/),里面有个难得一见的怪兽级 *regex*,你可以去瞻仰一下。\n", "\n", "p.s. 大部分情况下并不需要追求那么全面完整,大致够用就可以了,我们在介绍[字符串数据](../p4/6-data-1.ipynb)的一章有一些例子。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 附:趣事一则" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[ELIZA](https://en.wikipedia.org/wiki/ELIZA) 是人类史上第一个“聊天机器人”程序,上个世纪六十年代(1966)实现的,核心就是使用正则表达式匹配对方语句中的关键词汇,再经过替换,形成看似有意义的“回应”——事实上,ELIZA 完全不知道自己在说什么……\n", "\n", "几年后,就是七十年代初(1972),另外一个聊天机器人 [Parry](https://en.wikipedia.org/wiki/PARRY) 被写了出来,随后第二年,这两个聊天机器人在 ARPANET “碰面”好好地聊了一下:\n", "\n", "> ELIZA: Good Evening. Tell me your problems. Please terminate input with a period or a question mark.\n", ">\n", "> PARRY: People get on my nerves sometimes.\n", ">\n", "> ELIZA: I am not sure I understand you fully.\n", ">\n", "> PARRY: You should pay more attention.\n", ">\n", "> ELIZA: Suppose you should pay more attention.\n", ">\n", "> PARRY: You're entitled to your own opinion.\n", ">\n", "> ELIZA: What makes you think I am entitled to my own opinion?\n", ">\n", "> PARRY: It bothers me just to be around people in general.\n", ">\n", "> ELIZA: Please go on.\n", ">\n", "> PARRY: I went to the track at Bay Meadows a while back.\n", ">\n", "> ELIZA: What does that suggest to you?\n", "> \n", "> PARRY: I went to the races.\n", ">\n", "> ......" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "这俩的完整聊天记录可以在[这里](https://tools.ietf.org/html/rfc439)看到,不知道你看了是什么感觉……" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "不少后人用 Python 重新实现了 ELIZA,比如[这个](https://github.com/jezhiggins/eliza.py),你可以执行它,然后试着阅读和理解其实现,作为之前我们自己做[练习](../p2/b-oo-4.ipynb)的持续演进。" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 4 }