{ "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) | `(...)` `(?:...)` `(?=...)` `(?!...)` `(?<=...)` `(?a|b|c |\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 = '