{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 正则表达式" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "正则表达式本质上是个独立的语言,短小却格外强悍 —— 乃至于,如果你竟然没学会它的话,你的之前学的编程技能干脆与残疾无异。\n", "\n", "Wikipedia 上对正则表达式的说明如下:\n", "\n", "> **正则表达式**(英语:Regular Expression,在代码中常简写为 regex、regexp 或 RE),又称*正规表示式*、*正规表示法*、*正规运算式*、*规则运算式*、*常规表示法*,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。\n", "\n", "以下是绝大多数翻译成中文的教程中对正则表达式进行讲解时所使用的描述:\n", "\n", "> 一个正则表达式(Regular Expression)通常被称为一个模式(Pattern)。\n", "\n", "我常常觉得当初要是它被翻译成 “规则表达式”,那么很可能初学者不会感到那么大的压力 —— 谁都一样,看着由 “每个都认识的字构成的词组” 却不能直观地想到它究竟是什么东西,都会感到莫名的压力。\n", "\n", "**Regular**,其实在它的众多语义中,取以下释义最符合 Regular Expression 的原意[1]:\n", "\n", "> ⑭ Linguistics 规则的 ▸ regular verbs 规则动词\n", "\n", "而 **Pattern** 这个词,在词典里有好几个对应的中文词汇:\n", "\n", "> ① 图案;② 式样;③ 图样;④ 榜样;⑤ 模式;⑥ 样品;⑦ 模子\n", "\n", "在当前语境之下,把 Pattern 翻译成 “模式”,显然不如 “模子” 更好(甚至连 “样品” 感觉都比 “模式” 更恰当)—— “模子” 这个词很直观啊,拿着一个模子去找与它一致的字符串…… “与规则一致”,英文用的是 **Match**,一般被翻译作 “匹配”。\n", "\n", "在自学编程的过程中,处处都是这种语言翻译带来的迷惑、障碍,或者耽误。既然应该把 Regular Expression 理解为 “规则表达式” 更好,那其实吧,把 Pattern 直接理解为中文的 “*规则*”,可能更直观更准确,理解上更是毫无障碍:\n", "\n", "> 一个规则表达式(Regular Expression)通常被称为一个规则(Pattern)。\n", "\n", "那么,**规则表达式**里写的是什么呢?只能是**规则**了…… 到最后好像也就 “捕获”(Capture)这个词没什么歧义。\n", "\n", "现在,我们已经把术语全部 “解密” 了,然后再看看下面的表述:\n", "\n", "> 我们可以用书写特定的规则,用来在文本中捕获与规则一致的字符串,而后对其进行操作……\n", "\n", "理解起来相当顺畅。\n", "\n", "以下的 Python 代码中,[`\\wo\\w`](https://regexper.com#%5Cwo%5Cw) 就是一个*规则表达式*(或称为*规则*);\n", "\n", "而 `re.findall(pttn, str)` 的作用就是,在 `str` 里找到所有与这个**规则**(Pattern,模式)**一致**(Match,匹配)的字符串:" ] }, { "cell_type": "code", "execution_count": 226, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['row', 'fox', 'dog']" ] }, "execution_count": 226, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import re\n", "str = 'The quick brown fox jumps over the lazy dog'\n", "pttn = re.compile(r'\\wo\\w')\n", "re.findall(pttn, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "总结一下:\n", "\n", "> **规则表达式**(Regular Expressions,通常缩写为 Regex)是最强大且不可或缺的文本处理工具 —— 它的用处就是在文本中**扫描/搜索**(Scan/Search)与某一**规则**(Pattern)**匹配**(Match,即,与规则一致)的所有实例,并且还可以按照规则**捕获**(Capture)其中的部分或者全部,对它们进行**替换**(Replace)。\n", "\n", "接下来为了避免歧义,我们干脆用 Regex 这个缩写,以及与它相关的英文单词:pattern, match, capture, replace(ment)……\n", "\n", "有时,使用 Regex 并不是为了 Replace,而是为了检查格式,比如,可以用 Regex 检查用户输入的密码是否过于简单(比如,全部都由数字构成),比如可以用来验证用户输入的电话号码、证件号码是否符合特定格式等等。\n", "\n", "另外,在自学的过程中,想尽一切办法把一切术语用简单直白的 “人话” 重新表述,是特别有效的促进进步的行为模式。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 视觉体验" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "所谓百闻不如一见。\n", "\n", "眼见为实 —— 想办法让一个陌生的概念视觉上直观,是突破大多学习障碍的最简单粗暴直接有效的方式。\n", "\n", "我们最好先直接看看 Regex 的工作过程。以下,是用微软发行的代码编辑工具 Visual Studio Code 针对一小段文本使用若干条 Regex 进行匹配的过程:\n", "\n", "\n", "\n", "Python 的项目代码仓库里有一个很简短的 Demo 程序,叫 [`redemo.py`](https://github.com/python/cpython/blob/3.6/Tools/demo/redemo.py),它使用 [Tcl/Tk](https://docs.python.org/3/library/tkinter.html) 作为图形界面,也可以用来测试正则表达式。\n", "\n", "它的代码地址是:\n", "\n", "> https://raw.githubusercontent.com/python/cpython/3.6/Tools/demo/redemo.py\n", "\n", "它运行起来长成这样:\n", "\n", "\n", "\n", "目前(2019)网上最方便的 Regex 测试器,是 [regex101.com](https://regex101.com):\n", "\n", "以下,就是在一段文本中,找出所有首写字母大写的词汇的*过程*,并将其先全部替换成小写,再将其全部替换为大写的过程;使用的正则表达式是 `([A-Z]\\w+)`,替换表达式分别是 `\\L$1` 和 `\\U$1`:\n", "\n", "\n", "\n", "这个网站太好了,所以,平日里我是用 [Nativefier](https://github.com/jiahaog/nativefier) 工具将这个网站打包为一个 Mac Desktop App 使用。不过,它也有局限,就是被搜索文件略微大点就报错,说 `timeout`……" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 准备工作\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "我们需要个文本文件,用来当作练习使用正则表达式去搜索替换的目标。这个文件保存在当前的根目录,文件名称是:`regex-target-text-sample.txt`。\n", "\n", "以下代码中,`pttn = r'beg[iau]ns?'` 这一句中的 [`beg[iau]ns?`](https://regexper.com#beg[iau]ns?) 就是 Regex 的 Pattern。\n", "\n", "**注意**:在 Python 代码中,写 Pattern 的时候,之所以要在字符串 `'...'` 之前加上 `r`,写成 `r'...'`,是因为如果不用 raw string 的话,那么,每个转义符号都要写成 `\\\\`;如果用 raw string,转义符号就可以直接使用 `\\` 本身了…… 当然,如果你想搜索 `\\` 这个符号本身的话,那么还是得写 `\\\\`。\n", "\n", "而 `re.findall(pttn, str)` 的意思是说,把 `str` 中所有与 `pttn` 这个规则一致的字符串都找出来:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['begin', 'began', 'begun', 'begin']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import re\n", "with open('regex-target-text-sample.txt', 'r') as f:\n", " str = f.read()\n", "pttn = r'beg[iau]ns?'\n", "re.findall(pttn, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "文件 `regex-target-text-sample.txt` 中的内容如下:\n", "\n", "```html\n", "
begin began begun bigins begining
google gooogle goooogle goooooogle
coloured color coloring colouring colored
never ever verb however everest
520 52000 5200000 520000000 520000000000
error wonderer achroiocythaemia achroiocythemia
The white dog wears a black hat.
Handel, Händel, Haendel
It's very very big.
\n", "Keep it simple, simple, simple!
\n", "```\n", "\n", "在以下的示例中,有时直接设定了 str 的值,而不是使用以上整个文本文件 —— 因为读者在阅读的时候,最好能直接看到被搜索的字符串。另外,如果使用整个文件,所得到的 Match 太多,也确实影响阅读。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 优先级" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "毕竟,你已经不是 “啥都不懂” 的人了。你已经知道一个事实:编程语言无非是用来运算的。\n", "\n", "所谓的运算,就有操作符(Operators)和操作元(Operands)—— 而操作符肯定是有优先级的,不然的话,那么多操作元和操作符放在一起,究竟先操作哪个呢?\n", "\n", "Regex 也一样,它本身就是个迷你语言(Mini Language)。在 Regex 中,操作符肯定也有优先级。它的操作元有个专门的名称,**原子**(Atom)。\n", "\n", "先大致看看它的操作符优先级,你就会对它有相当不错的了解:\n", "\n", "| 排列 | 原子与操作符优先级 |(从高到低)|\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` `.` |\n", "\n", "当然,你若是在之前,没有自学过、理解过 Python(或者任何其它编程语言)表达式中的操作符优先级,那么一上来就看上面的表格不仅对你没有帮助,只能让你更迷惑。\n", "\n", "—— 这就是理解能力逐步积累逐步加强的过程。" ] }, { "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", "即,相当于,`string.ascii_letters` 和 `string.digits` 以及 `_`。" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" }, { "data": { "text/plain": [ "'0123456789'" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.core.interactiveshell import InteractiveShell\n", "InteractiveShell.ast_node_interactivity = \"all\"\n", "\n", "import string\n", "string.ascii_letters\n", "string.digits" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "以下字符在 Regex 中都有特殊含义:\n", "\n", "> `\\` `+` `*` `.` `?` `-` `^` `$` `|` `(` `)` `[` `]` `{` `}` `<` `>` \n", "\n", "当你在写 Regex 的时候,如果你需要搜索的字符不是本义字符,而是以上这些特殊字符时,*建议*都直接加上转义符号 `\\` 来表示,比如,你想搜索 `'`,那你就写 `\\'`,或者你想搜索 `#` 那你就写 `\\#`(事实上,`#` 并不是 Regex 的特殊符号,所以,它之前的转义符号可有可无)—— 这对初学者来说可能是最安全的策略。\n", "\n", "跟过往一样,所有的细节都很重要,它们就是需要花时间逐步熟悉到牢记。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 集合原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "集合原子还是原子。\n", "\n", "标示集合原子,使用方括号 `[]`。`[abc]` 的意思是说,“`a` or `b` or `c`”,即,`abc` 中的任意一个字符。\n", "\n", "比如,[`beg[iau]n`](https://regexper.com#beg[iau]n) 能够代表 `begin`、`began`,以及 `begun`。" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['begin', 'began', 'begun', 'begin']" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import re\n", "\n", "str = 'begin began begun bigins begining'\n", "pttn = r'beg[iau]n'\n", "re.findall(pttn, str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在方括号中,我们可以使用两个操作符:`-`(区间)和 `^`(非)。\n", "\n", "* `[a-z]` 表示从小写字母 `a` 到小写字母 `z` 中的任意一个字符。\n", "* `[^abc]` 表示 `abc` 以外的其它任意字符,即,非 `[abc]`。\n", "\n", "注意,一个集合原子中,`^` 符号只能用一次,只能紧跟在 `[` 之后。否则不起作用。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 类别原子" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "类别原子,是指那些能够代表 “一类字符” 的原子,它们都得使用转义符号再加上另外一个符号表达,包括:\n", "\n", "`\\d` 任意数字;等价于 `[0-9]`\n", "\n", "`\\D` 任意非数字;等价于 `[^0-9]`\n", "\n", "`\\w` 任意本义字符;等价于 `[a-zA-Z0-9_]`\n", "\n", "`\\W` 任意非本义字符;等价于 `[^a-zA-Z0-9_]`\n", "\n", "`\\s` 任意空白;相当于 `[ \\f\\n\\r\\t\\v]`(注意,方括号内第一个字符是空格符号)\n", "\n", "`\\S` 任意非空白;相当于 `[^ \\f\\n\\r\\t\\v]`(注意,紧随 `^` 之后的是一个空格符号)\n", "\n", "`.` 除 `\\r` `\\n` 之外的任意字符;相当于 `[^\\r\\n]`\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", "\n", "> * `f` 是 flip\n", "> * `n` 是 new line\n", "> * `r` 是 return\n", "> * `t` 是 tab\n", "> * `v` 是 vertical tab" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['542-', '270-']" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import re\n", "\n", "str = '