{ "cells": [ { "cell_type": "markdown", "metadata": { "toc": "true" }, "source": [ "

Table of Contents

\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Python 正则表达式操作指南" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 简介" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "就其本质而言,正则表达式(或 RE)是一种小型的、高度专业化的编程语言,(在Python中)它内嵌在 Python 中,并通过 re 模块实现。 \n", "使用这个小型语言,你可以为想要匹配的相应字符串集指定规则;该字符串集可能包含英文语句、e-mail地址、TeX命令或任何你想搞定的东西。 \n", "然后你可以问诸如“这个字符串匹配该模式吗?”或“在这个字符串中是否有部分匹配该模式呢?”。 \n", "你也可以使用 RE 以各种方式来修改或分割字符串。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 简单模式" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 字符匹配" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 大多数字母和字符一般都会和自身匹配。 \n", "- 元字符:`. ^ $ * + ? { [ ] \\ | ( )`\n", "- 元字符是\"[\" 和 \"]\"。它们常用来指定一个字符类别,所谓字符类别就是你想匹配的一个字符集。字符可以单个列出,也可以用“-”号分隔的两个给定字符来表示一个字符区间。 \n", " - 元字符在类别里并不起作用。例如,`[akm$]` 将匹配字符\"a\", \"k\", \"m\", 或 \"`$`\" 中的任意一个;\"`$`\"通常用作元字符,但在字符类别里,其特性被除去,恢复成普通字符。\n", " - 可以用补集来匹配不在区间范围内的字符。其做法是把\"^\"作为类别的**首个字符**;其它地方的\"^\"只会简单匹配 \"^\"字符本身。例如,`[^5]` 将匹配除 \"5\" 之外的任意字符。\n", " - 最重要的元字符是反斜杠\"\\\"。 做为 Python 中的字符串字母,反斜杠后面可以加不同的字符以表示不同特殊意义。它也可以用于取消所有的元字符,这样你就可以在模式中匹配它们了。举个例子,如果你需要匹配字符 \"[\" 或 \"\\\",你可以在它们之前用反斜杠来取消它们的特殊意义: `\\[` 或 `\\\\`。\n", " - `\\d` 匹配任何十进制数;它相当于类 `[0-9]`。\n", " - `\\D` 匹配任何非数字字符;它相当于类 `[^0-9]`。\n", " - `\\s` 匹配任何空白字符;它相当于类 `[ \\t\\n\\r\\f\\v]`。\n", " - `\\S` 匹配任何非空白字符;它相当于类 `[^ \\t\\n\\r\\f\\v]`。\n", " - `\\w` 匹配任何字母数字字符;它相当于类 `[a-zA-Z0-9_]`。\n", " - `\\W` 匹配任何非字母数字字符;它相当于类 `[^a-zA-Z0-9_]`。\n", "- 元字符 `.` 。它匹配除了换行字符外的任何字符,在 alternate 模式(`re.DOTALL`)下它甚至可以匹配换行。\"`.`\" 通常被用于你想匹配“任何字符”的地方。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 重复" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- `*` 匹配零或更多次,所以可以根本就不出现\n", "- `+` 则要求至少出现一次\n", "- `?` 匹配一次或零次,可以认为它用于标识某事物是可选的\n", "- `{m,n}`(注意m,n之间不能有空格),其中 m 和 n 是十进制整数。该限定符的意思是至少有 m 个重复,至多到 n 个重复" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "考虑表达式 `a[bcd]*b`。它匹配字母 \"a\",零个或更多个来自类 `[bcd]` 中的字母,最后以 \"b\" 结尾。现在想一想该 RE 对字符串 \"abcbd\" 的匹配。 \n", "\n", "匹配引擎一开始会尽其所能进行匹配(贪婪匹配),如果没有匹配然后就逐步退回并反复尝试 RE 剩下来的部分。直到它退回尝试匹配 [bcd] 到零次为止,如果随后还是失败,那么引擎就会认为该字符串根本无法匹配 RE 。\n", "\n", "Step|\tMatched|\tExplanation|\n", "----|----|---\n", "1\t|a\t|a 匹配模式\n", "2\t|abcbd|\t引擎匹配 `[bcd]*`,并尽其所能匹配到字符串的结尾\n", "3\t|Failure|\t引擎尝试匹配 b,但当前位置已经是字符的最后了,所以失败\n", "4\t|abcb|\t退回,[bcd]`*` 尝试少匹配一个字符。\n", "5\t|Failure|\t再次尝次 b,但在当前最后一位字符是 \"d\"。\n", "6\t|abc\t|再次退回,`[bcd]*`只匹配 \"bc\"。\n", "7\t|abcb\t|再次尝试 b ,这次当前位上的字符正好是 \"b\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 使用正则表达式\n", "\n", "\n", "### 编译正则表达式\n", "\n", "REs 被处理成字符串是因为正则表达式不是 Python 语言的核心部分,也没有为它创建特定的语法。(应用程序根本就不需要 REs,因此没必要包含它们去使语言说明变得臃肿不堪。)而 re 模块则只是以一个 C 扩展模块的形式来被 Python 包含,就象 socket 或 zlib 模块一样。将 REs 作为字符串以保证 Python 语言的简洁。" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "re.compile('ab*', re.IGNORECASE)\n" ] } ], "source": [ "import re\n", "p = re.compile('ab*')\n", "p = re.compile('ab*', re.IGNORECASE)\n", "print (p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 反斜杠" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "为了匹配一个反斜杠,不得不在 RE 字符串中写 '`\\\\\\\\`',因为正则表达式中必须是 \"`\\\\`\",而每个反斜杠在常规的 Python 字符串实值中必须表示成 \"`\\\\`\"。在 REs 中反斜杠的这个重复特性会导致大量重复的反斜杠,而且所生成的字符串也很难懂。 \n", "\n", "解决的办法就是为正则表达式使用 Python 的 raw 字符串表示;在字符串前加个 \"r\" 反斜杠就不会被任何特殊方式处理,所以 `r\"\\n\"` 就是包含 \"`\\`\" 和 \"n\" 的两个字符,而 \"`\\n`\" 则是一个字符,表示一个换行。正则表达式通常在 Python 代码中都是用这种 raw 字符串表示。 \n", "\n", "\n", "常规字符串|\tRaw 字符串\n", "---------|--------\n", "\"`ab*`\"\t|r\"`ab*`\"\n", "\"`\\\\\\\\section`\"|\tr\"`\\\\section`\"\n", "\"`\\\\w+\\\\s+\\\\1`\"|\tr\"`\\w+\\s+\\1`\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 执行匹配" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "方法/属性|\t作用\n", "--------|-------\n", "match()\t|决定 RE 是否在字符串刚开始的位置匹配\n", "search()|\t扫描字符串,找到这个 RE 匹配的位置\n", "findall()|\t找到 RE 匹配的所有子串,并把它们作为一个列表返回\n", "finditer()|\t找到 RE 匹配的所有子串,并把它们作为一个迭代器返回 \n", "\n", "如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 `MatchObject` 实例,其中有这次匹配的信息:它是从哪里开始和结束,它所匹配的子串等等。 \n", "findall() 在它返回结果时不得不创建一个列表。在 Python 2.2中,也可以用 finditer() 方法。\n", "\n", "\n", "MatchObject 实例也有几个方法和属性;最重要的那些如下所示: \n", "\n", "方法/属性|\t作用\n", "--------|------\n", "group()\t|返回被 RE 匹配的字符串\n", "start()\t|返回匹配开始的位置\n", "end()\t|返回匹配结束的位置\n", "span()\t|返回一个元组包含匹配 (开始,结束) 的位置 \n", "\n", "\n", "在实际程序中,最常见的作法是将 `MatchObject` 保存在一个变量里,然後检查它是否为 None,通常如下所示:\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<_sre.SRE_Match object; span=(0, 5), match='tempo'>\n", "Match found: tempo\n" ] } ], "source": [ "p = re.compile('[a-z]+')\n", "m = p.match( 'tempo')\n", "print(m)\n", "if m:\n", " print ('Match found: ', m.group())\n", "else:\n", " print ('No match')" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'tempo'" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.group()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0, 5)" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.start(), m.end()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0, 5)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.span()" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "drummers\n", "(3, 11)\n", "drumming\n", "(12, 20)\n" ] } ], "source": [ "iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')\n", "for match in iterator:\n", " print(match.group())\n", " print(match.span())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 编译标志\n", "\n", "编译标志让你可以修改正则表达式的一些运行方式。在 re 模块中标志可以使用两个名字,一个是全名如 IGNORECASE,一个是缩写,一字母形式如 I。(如果你熟悉 Perl 的模式修改,一字母形式使用同样的字母;例如 re.VERBOSE的缩写形式是 re.X。) \n", "\n", "多个标志可以通过按位 OR-ing 它们来指定。如 `re.I | re.M` 被设置成 I 和 M 标志: \n", "- LOCALE 影响 \\w, \\W, \\b, 和 \\B,这取决于当前的本地化设置。 \n", " locales 是 C 语言库中的一项功能,是用来为需要考虑不同语言的编程提供帮助的。举个例子,如果你正在处理法文文本,你想用 \\w+ 来匹配文字,但 \\w 只匹配字符类 [A-Za-z];它并不能匹配 \"é\" 或 \"ç\"。如果你的系统配置适当且本地化设置为法语,那么内部的 C 函数将告诉程序 \"é\" 也应该被认为是一个字母。当在编译正则表达式时使用 LOCALE 标志会得到用这些 C 函数来处理 \\w 后的编译对象;这会更慢,但也会象你希望的那样可以用 \\w+ 来匹配法文文本。\n", "- VERBOSE\n", " 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。当该标志被指定时,在 RE 字符串中的空白符被忽略,除非该空白符在字符类中或在反斜杠之后;这可以让你更清晰地组织和缩进 RE。它也可以允许你将注释写入 RE,这些注释会被引擎忽略;注释用 \"#\"号 来标识,不过该符号不能在字符串或反斜杠之后。\n", "\n", "标志\t|含义\n", "------|----\n", "DOTALL, S\t|使 `.` 匹配包括换行在内的所有字符\n", "IGNORECASE, I\t|使匹配对大小写不敏感\n", "LOCALE, L\t|做本地化识别(locale-aware)匹配\n", "MULTILINE, M\t|多行匹配,影响 `^` 和 `$`,`^` 和 `$` 不会被解释\n", "VERBOSE, X\t|能够使用 REs 的 verbose 状态,使之被组织得更清晰易懂" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 更多模式功能" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 更多的元字符\n", "\n", "剩下来要讨论的一部分元字符是零宽界定符(zero-width assertions)。它们并不会使引擎在处理字符串时更快;相反,它们根本就没有对应任何字符,只是简单的成功或失败。举个例子,\\b 是一个在单词边界定位当前位置的界定符(assertions),这个位置根本就不会被 \\b 改变。这意味着零宽界定符(zero-width assertions)将永远不会被重复,因为如果它们在给定位置匹配一次,那么它们很明显可以被匹配无数次。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- `|`\n", " - 可选项,或者 \"or\" 操作符。如果 A 和 B 是正则表达式,A|B 将匹配任何匹配了 \"A\" 或 \"B\" 的字符串。| 的优先级非常低,是为了当你有多字符串要选择时能适当地运行。Crow|Servo 将匹配 \"Crow\" 或 \"Servo\", 而不是 \"Cro\", 一个 \"w\" 或 一个 \"S\", 和 \"ervo\"。\n", " - 为了匹配字母 \"|\",可以用 `\\|`,或将其包含在字符类中,如 `[|]`。\n", "- `^`\n", " - 匹配行首。除非设置 MULTILINE 标志,它只是匹配字符串的开始。在 MULTILINE 模式里,它也可以直接匹配字符串中的每个换行。\n", "- `$`\n", " - 匹配行尾,**行尾被定义为要么是字符串尾,要么是一个换行字符后面的任何位置**。\n", " - 匹配一个 \"`$`\",使用 `\\$` 或将其包含在字符类中,如 `[$]`。\n", "- `\\A`\n", " - 只匹配字符串首。\n", " - 当不在 MULTILINE 模式,`\\A` 和 `^` 实际上是一样的。\n", " - 然而,在 MULTILINE 模式里它们是不同的;`\\A` 只是匹配字符串首,而 `^` 还可以匹配在换行符之后字符串的任何位置。\n", "- `\\Z`\n", " - Matches only at the end of the string. \n", " - 只匹配字符串尾。\n", "- `\\b`\n", " - 单词边界。\n", " - 这是个零宽界定符(zero-width assertions)只用以匹配单词的词首和词尾。\n", " - 单词被定义为一个字母数字序列,因此词尾就是用空白符或非字母数字符来标示的。\n", "- `\\B`\n", " - 另一个零宽界定符(zero-width assertions),它正好同 \\b 相反,只在当前位置不在单词边界时匹配。" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<_sre.SRE_Match object; span=(3, 8), match='class'>\n", "None\n", "None\n" ] } ], "source": [ "p = re.compile(r'\\bclass\\b')\n", "print (p.search('no class at all'))\n", "print (p.search('the declassified algorithm'))\n", "print (p.search('one subclass is'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "当用这个特殊序列时你应该记住这里有两个微妙之处。第一个是 Python 字符串和正则表达式之间最糟的冲突。在 Python 字符串里,\"\\b\" 是反斜杠字符,ASCII值是8。如果你没有使用 raw 字符串时,那么 Python 将会把 \"\\b\" 转换成一个回退符,你的 RE 将无法象你希望的那样匹配它了。下面的例子看起来和我们前面的 RE 一样,但在 RE 字符串前少了一个 \"r\" 。 " ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "None\n", "<_sre.SRE_Match object; span=(0, 7), match='\\x08class\\x08'>\n" ] } ], "source": [ "p = re.compile('\\bclass\\b')\n", "print (p.search('no class at all'))\n", "print (p.search('\\b' + 'class' + '\\b'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "第二个在字符类中,这个限定符(assertion)不起作用,\\b 表示回退符,以便与 Python 字符串兼容。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 分组" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 组是通过 \"(\" 和 \")\" 元字符来标识的。 \"(\" 和 \")\" 有很多在数学表达式中相同的意思;它们一起把在它们里面的表达式组成一组。举个例子,你可以用重复限制符,象 `*, +, ?, 和 {m,n}`,来重复组里的内容,比如说 `(ab)*` 将匹配零或更多个重复的 \"ab\"。\n", "- 小组是从左向右计数的,从1开始。组可以被嵌套。计数的数值可以通过从左到右计算打开的括号数来确定。\n", "- `The groups()` 方法返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。\n", "- 模式中的逆向引用允许你指定先前捕获组的内容,该组也必须在字符串当前位置被找到。举个例子,如果组 1 的内容能够在当前位置找到的话,\\1 就成功否则失败。记住 Python 字符串也是用反斜杠加数据来允许字符串中包含任意字符的,所以当在 RE 中使用逆向引用时确保使用 raw 字符串。\n", " - 象这样只是搜索一个字符串的逆向引用并不常见 -- 用这种方式重复数据的文本格式并不多见 -- 但你不久就可以发现它们用在字符串替换上非常有用。\n" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(0, 10)\n" ] } ], "source": [ "p = re.compile('(ab)*')\n", "print (p.match('ababababab').span())" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ab\n", "ab\n" ] } ], "source": [ "p = re.compile('(a)b')\n", "m = p.match('ab')\n", "print(m.group())\n", "print(m.group(0))" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "abcd\n", "abc\n", "b\n", "('b', 'abc')\n" ] } ], "source": [ "p = re.compile('(a(b)c)d')\n", "m = p.match('abcd')\n", "print(m.group(0))\n", "print(m.group(1))\n", "print(m.group(2))\n", "print(m.group(2,1))" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('abc', 'b')" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m.groups()" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "['the']" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = re.compile(r'(\\b\\w+)\\s+\\1')\n", "p.findall('Paris in the the the spring')\n", "#p.search('Paris in the the the spring').group()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 无捕获组和命名组\n", "\n", "- Python 新增了一个扩展语法到 Perl 扩展语法中。如果在问号后的第一个字符是 \"P\",你就可以知道它是针对 Python 的扩展。目前有两个这样的扩展: \n", " - `(?P...)` 定义一个命名组\n", " - `(?P=name)` 则是对命名组的逆向引用\n", "- 首先,有时你想用一个组去收集正则表达式的一部分,但又对组的内容不感兴趣。你可以用一个无捕获组: `(?:...)` 来实现这项功能,这样你可以在括号中发送任何其他正则表达式。\n", " - 除了捕获匹配组的内容之外,无捕获组与捕获组表现完全一样;\n", " - 你可以在其中放置任何字符,可以用重复元字符如 \"`*`\" 来重复它,可以在其他组(无捕获组与捕获组)中嵌套它。\n", " - `(?:...)` 对于修改已有组尤其有用,因为你可以不用改变所有其他组号的情况下添加一个新组。\n", " - 捕获组和无捕获组在搜索效率方面也没什么不同,没有哪一个比另一个更快。\n", "- 其次,更重要和强大的是命名组;与用数字指定组不同的是,它可以用名字来指定。\n", " - 命令组的语法是 Python 专用扩展之一: `(?P...)`。名字很明显是组的名字。\n", " - 除了该组有个名字之外,命名组也同捕获组是相同的。`MatchObject` 的方法处理捕获组时接受的要么是表示组号的整数,要么是包含组名的字符串。命名组也可以是数字,所以你可以通过两种方式来得到一个组的信息。\n", " - 命名组是便于使用的,因为它可以让你使用容易记住的名字来代替不得不记住的数字。\n", "- 因为逆向引用的语法,象 `(...)\\1` 这样的表达式所表示的是组号,这时用组名代替组号自然会有差别。\n", " - 还有一个 Python 扩展:`(?P=name)`,它可以使叫 name 的组内容再次在当前位置发现。\n", " - 正则表达式为了找到重复的单词,`(\\b\\w+)\\s+\\1` 也可以被写成 `(?P\\b\\w+)\\s+(?P=word)`" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "('c',)\n", "()\n" ] } ], "source": [ "m = re.match(\"([abc])+\", \"abc\")\n", "print(m.groups())\n", "('c',)\n", "m = re.match(\"(?:[abc])+\", \"abc\")\n", "print(m.groups())" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "('Lots',)\n", "Lots\n" ] } ], "source": [ "p = re.compile(r'(?P\\b\\w+\\b)')\n", "m = p.search( '(((( Lots of punctuation )))' )\n", "print(m.groups('word'))\n", "print(m.group(1))" ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'the the'" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = re.compile(r'(?P\\b\\w+)\\s+(?P=word)')\n", "p.search('Paris in the the spring').group()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 前向界定符\n", "\n", "另一个零宽界定符(zero-width assertion)是前向界定符。前向界定符包括前向肯定界定符和前项否定界定符,如下所示: \n", "- `(?=...)` \n", " 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 \n", "- `(?!...)`\n", " 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功\n", "\n", "\n", "考虑一个简单的模式用于匹配一个文件名,并将其通过 \".\" 分成基本名和扩展名两部分。如在 \"news.rc\" 中,\"news\" 是基本名,\"rc\" 是文件的扩展名。\n", " - 匹配模式非常简单:`.*[.].*$`\n", " - 注意 \"`.`\" 需要特殊对待,因为它是一个元字符;我把它放在一个字符类中。另外注意后面的 `$`; \n", " - 添加这个是为了确保字符串所有的剩余部分必须被包含在扩展名中。 \n", " - 这个正则表达式匹配 \"foo.bar\"、\"autoexec.bat\"、 \"sendmail.cf\" 和 \"printers.conf\"。\n", "\n", "现在,考虑把问题变得复杂点;如果你想匹配的扩展名不是 \"bat\" 的文件名?\n", " - `.*[.](?!bat$).*$`\n", " - 前向的意思:如果表达式 bat 在这里没有匹配,尝试模式的其余部分;如果 `bat$` 匹配,整个模式将失败。\n", " - 后面的 `$` 被要求是为了确保象 \"sample.batch\" 这样扩展名以 \"bat\" 开头的会被允许。\n", " - 将另一个文件扩展名排除在外现在也容易;简单地将其做为可选项放在界定符中。下面的这个模式将以 \"bat\" 或 \"exe\" 结尾的文件名排除在外。`.*[.](?!bat$|exe$).*$`" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This text includes two Twitter handles.\n", "One for @ThePSF, and one for the author, @doughellmann.\n", "\n", "Handle: ThePSF\n", "Handle: doughellmann\n" ] } ], "source": [ "import re\n", "twitter = re.compile(\n", " '''(?<=@)([\\w\\d_]+)''',re.UNICODE | re.VERBOSE)\n", "\n", "text = '''This text includes two Twitter handles.\n", "One for @ThePSF, and one for the author, @doughellmann.\n", "'''\n", "\n", "print (text)\n", "for match in twitter.findall(text):\n", " print ('Handle:', match)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['Isaac']" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 前向\n", "# Isaac 前面有 Asimov 才能匹配\n", "test = re.compile(r'Isaac(?=Asimov)')\n", "test.findall(\"IsaacAsimov\")\n", "#test.findall(\"AsimovIsaac\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 前向\n", "# Isaac 前面有 Asimov 不能匹配\n", "test = re.compile(r'Isaac(?!Asimov)')\n", "test.findall(\"IsaacAsimov\")\n", "# test.findall(\"AsimovIsaac\")" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['Isaac']" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 后向\n", "# Isaac 后面有 Asimov 才能匹配\n", "test = re.compile(r'(?<=Asimov)Isaac')\n", "test.findall(\"AsimovIsaac\")\n", "#test.findall(\"IsaacAsimov\")" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 后向\n", "# Isaac 后面有 Asimov 不能匹配\n", "test = re.compile(r'(?...)` 语法定义的命名组。\"`\\g`\" 将通过组名 \"name\" 用子串来匹配,并且 \"`\\g`\" 使用相应的组号。所以 \"`\\g<2>`\" 等于 \"`\\2`\",但能在替换字符串里含义不清,如 \"`\\g<2>0`\"。(\"`\\20`\" 被解释成对组 20 的引用,而不是对后面跟着一个字母 \"0\" 的组 2 的引用。)\n", "- 替换也可以是一个甚至给你更多控制的函数。如果替换是个函数,该函数将会被模式中每一个不重复的匹配所调用。在每次调用时,函数会被传入一个 `MatchObject` 的对象作为参数,因此可以用这个对象去计算出替换字符串并返回它。" ] }, { "cell_type": "code", "execution_count": 80, "metadata": { "collapsed": true }, "outputs": [], "source": [ "p = re.compile('(blue|white|red)')" ] }, { "cell_type": "code", "execution_count": 81, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'colour socks and colour shoes'" ] }, "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.sub( 'colour', 'blue socks and red shoes')" ] }, { "cell_type": "code", "execution_count": 83, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'colour socks and red shoes'" ] }, "execution_count": 83, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.sub('colour', 'blue socks and red shoes', count=1)" ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'-a-b-d-'" ] }, "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = re.compile('x*')\n", "p.sub('-', 'abxd')" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'subsection{First} subsection{second}'" ] }, "execution_count": 107, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)\n", "p = re.compile('section{(\\w+)}')\n", "p.sub(r'subsection{\\1}','section{First} section{second}')" ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'subsection{First}'" ] }, "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p = re.compile('section{ (?P [^}]* ) }', re.VERBOSE)\n", "p = re.compile('section{ (?P \\w+) }', re.VERBOSE)\n", "p.sub(r'subsection{\\1}','section{First}')" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'subsection{First}'" ] }, "execution_count": 110, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.sub(r'subsection{\\g<1>}','section{First}')" ] }, { "cell_type": "code", "execution_count": 111, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'subsection{First}'" ] }, "execution_count": 111, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p.sub(r'subsection{\\g}','section{First}')" ] }, { "cell_type": "code", "execution_count": 112, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Call 0xffd2 for printing, 0xc000 for user code.'" ] }, "execution_count": 112, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def hexrepl( match ):\n", "... \"Return the hex string for a decimal number\"\n", "... value = int( match.group() )\n", "... return hex(value)\n", "...\n", "p = re.compile(r'\\d+')\n", "p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 常见问题" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 使用字符串方式" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "用一个固定字符串替换另一个 的例子,如:你可以把 \"deed\" 替换成 \"word\"。re.sub() 似乎正是胜任这个工作的函数,但还是考虑考虑 replace() 方法吧。注意 replace() 也可以在单词里面进行替换,可以把 \"swordfish\" 变成 \"sdeedfish\"。" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "另一个常见任务是从一个字符串中删除单个字符或用另一个字符来替代它。你也许可以用 re.sub('\\n',' ', s) 这样来实现,但 translate() 能够实现这两个任务,而且比任何正则表达式操作起来更快。 (translate 需要配合 string.maketrans 使用。例如:import string 后 'a1b3'.translate(string.maketrans('ab', 'cd')) )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `match() vs search()`" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "match() 函数只检查 RE 是否在字符串开始处匹配,而 search() 则是扫描整个字符串。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 贪婪 vs 不贪婪" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "不贪婪的限定符 `*?、+?、?? 或 {m,n}?`,尽可能匹配小的文本。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 使用 `re.VERBOSE`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 笔记" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- 大多数字母和字符一般都会和自身匹配\n", "- 元字符在类别里(`[]` 里)并不起作用\n", "- 可以用补集来匹配不在区间范围内的字符。其做法是把\"^\"作为**类别**的**首个字符**;其它地方的\"^\"只会简单匹配 \"^\"字符本身\n", "- 在字符串前加个 \"r\" 反斜杠就不会被任何特殊方式处理\n", "- 如果没有匹配到的话,match() 和 search() 将返回 None。如果成功的话,就会返回一个 `MatchObject` 实例,其中有这次匹配的信息:它是从哪里开始和结束,它所匹配的子串等等。findall() 在它返回结果时不得不创建一个列表。在 Python 2.2中,也可以用 finditer() 方法。\n", "- 多个标志可以通过按位 OR-ing 它们来指定。如 `re.I | re.M` 被设置成 I 和 M 标志\n", "- 组可以被嵌套。计数的数值可以通过从左到右计算打开的括号数来确定。`The groups()` 方法返回一个包含所有小组字符串的元组,从 **1** 到 所含的小组号。\n", "- 模式中的逆向引用允许你指定先前捕获组的内容,该组也必须在字符串当前位置被找到。举个例子,如果组 1 的内容能够在当前位置找到的话,\\1 就成功否则失败。\n", "\n", "- 无捕获组和命名组\n", " - `(?P...)` 定义一个命名组,`(?P=name)` 则是对命名组的逆向引用;除了用数字指定组,它可以用名字来指定,如:`(\\b\\w+)\\s+\\1` 也可以被写成 `(?P\\b\\w+)\\s+(?P=word)`\n", " - 无捕获组: `(?:...)` ,对于修改已有组尤其有用,因为你可以不用改变所有其他组号的情况下添加一个新组。\n", "\n", "- 前向界定符\n", " - `(?=...)` 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 \n", " - `(?!...)` 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功,匹配不是 bat 或 exe 后缀的:`.*[.](?!bat$|exe$).*$`\n", "\n", "- `split(string [, maxsplit = 0])`\n", " - `re.compile().split(text, num)`: num 非 0 时,最多分成 num 段\n", " - `p2 = re.compile(r'(\\W+)')`: 打印出定界符,如果不需要则为:`p = re.compile(r'\\W+')`\n", "\n", "- `sub(replacement, string[, count = 0])` \n", " - 可选参数 count 是模式匹配后替换的最大次数;count 必须是非负整数。缺省值是 0 表示替换所有的匹配。 \n", " - 空匹配只有在它们没有紧挨着前一个匹配时才会被替换掉。\n", " - 还可以指定用 `(?P...)` 语法定义的命名组。\"`\\g`\" 将通过组名 \"name\" 用子串来匹配,并且 \"`\\g`\" 使用相应的组号。\n", " - 替换也可以是一个函数,该函数将会被模式中每一个不重复的匹配所调用。\n", "- 更快的替换:`replace`\n", "- 从一个字符串中删除单个字符或用另一个字符来替代它\n", " - 可以用 `re.sub('\\n',' ', s)` 这样来实现,但 `translate()` 能够实现这两个任务,而且比任何正则表达式操作起来更快。\n", " - `translate` 需要配合` string.maketrans` 使用。例如:`import string` 后 `'a1b3'.translate(string.maketrans('ab', 'cd'))` " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "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" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "toc": { "base_numbering": 1, "nav_menu": { "height": "444px", "width": "252px" }, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": {}, "toc_section_display": "block", "toc_window_display": true } }, "nbformat": 4, "nbformat_minor": 1 }