没有一个特别熟练的语言是要吃大亏的,以前python只停留在表面,现在得深入研究一下python的各个特性和用法。按照书《python核心编程》来做笔记,如有读者,欢迎指正
正则表达式定义
正则表达式是包含文本和特殊字符的字符串,该字符串描述一个可以识别各种字符串的模式。
特殊符号和字符
下表列出了最常见的字符串和其正则表达式的表示方法
正则表达式字符表示法大全
“或”表示:择一匹配的管道符号(|)
表示择一匹配的管道符号(|),也就是键盘上的竖线,表示一个“从多个模式中选择其一”的操作。它用于分割不同的正则表达式。
有了这个符号,就能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不仅仅只是一个字符串。择一匹配有时候也称作并(union)
或者逻辑或(logical OR
)。
匹配任意单个字符(.)
点号或者句点(.)符号匹配除了换行符\n 以外的任何字符(Python 正则表达式有一个编译标记[S 或者 DOTALL],该标记能够推翻这个限制,使点号能够匹配换行符)。无论字母、数字、空格(并不包括“\n”换行符)、可打印字符、不可打印字符,还是一个符号,使用点号都能够匹配它们
要显式匹配一个句点符号本身,必须使用反斜线转义句点符号的功能
,例如“.”。
从字符串起始(^)或者结尾($)或者单词边界(\b \B)匹配
还有些符号和相关的特殊字符用于在字符串的起始和结尾部分指定用于搜索的模式。
- 如果要匹配字符串的开始位置,就必须使用脱字符(^)或者特殊字符\A(反斜线和大写字母 A)。
- 后者主要用于那些没有脱字符的键盘(例如,某些国际键盘)。同样,美元符号($)或者\Z将用于匹配字符串的末尾位置。
再次说明,如果想要逐字匹配这些字符中的任何一个(或者全部),就必须使用反斜线进行转义。例如,如果你想要匹配任何以美元符号结尾的字符串,一个可行的正则表达式方案就是使用模式.
*\$$
。
特殊字符\b 和\B 可以用来匹配字符边界。而两者的区别在于\b 将用于匹配一个单词的边界,这意味着如果一个模式必须位于单词的起始部分,就不管该单词前面(单词位于字符串中间)是否有任何字符(单词位于行首)。同样,\B 将匹配出现在一个单词中间的模式(即,不是单词边界)。下面为一些示例。
创建字符集([])
尽管句点可以用于匹配任意符号,但某些时候,可能想要匹配某些特定字符。正因如此,发明了方括号。该正则表达式能够匹配一对方括号中包含的任何字符。下面为一些示例。
关于[cr][23][dp][o2]
这个正则表达式有一点需要说明:如果仅允许“r2d2”或者“c3po”作为有效字符串,就需要更严格限定的正则表达式。因为方括号仅仅表示逻辑或的功能
,所以使用方括号并不能实现这一限定要求。唯一的方案就是使用择一匹配,例如,r2d2|c3po
然而,对于单个字符的正则表达式,使用择一匹配和字符集是等效的。例如,我们以正则表达式“ab”作为开始,该正则表达式只匹配包含字母“a”且后面跟着字母“b”的字符串,如果我们想要匹配一个字母的字符串,例如,要么匹配“a”,要么匹配“b”,就可以使用正则表达式[ab],因为此时字母“a”和字母“b”是相互独立的字符串。我们也可以选择正则表达式 a|b。然而,如果我们想要匹配满足模式“ab”后面且跟着“cd”的字符串,我们就不能使用方括号,因为字符集的方法只适用于单字符的情况。这种情况下,唯一的方法就是使用 ab|cd,这与刚才提到的 r2d2/c3po 问题是相同的。
限定范围和否定(^ -)
除了单字符以外,字符集还支持匹配指定的字符范围。方括号中两个符号中间用连字符(-)连接,用于指定一个字符的范围;例如,A-Z、a-z 或者 0-9 分别用于表示大写字母、小写字母和数值数字。这是一个按照字母顺序的范围,所以不能将它们仅仅限定用于字母和十进制数字上。另外,如果脱字符(^)紧跟在左方括号后面,这个符号就表示不匹配给定字符集中的任何一个字符
使用闭包操作符实现存在性和频数匹配(+ * ?)
本节介绍最常用的正则表达式符号,即特殊符号*、+和?,所有这些都可以用于匹配一个、多个或者没有出现的字符串模式。
- 星号或者星号操作符(*)将匹配其左边的正则表达式出现零次或者多次的情况(在计算机编程语言和编译原理中,该操作称为 Kleene 闭包)。
- 加号(+)操作符将匹配一次或者多次出现的正则表达式(也叫做正闭包操作符)
- 问号(?)操作符将匹配零次或者一次出现的正则表达式。
- 大括号操作符({}),里面或者是单个值或者是一对由逗号分隔的值。这将最终精确地匹配前面的正则表达式 N 次(如果是{N})或者一定范围的次数;例如,{M,N}将匹配 M~N 次出现。这
些符号能够由反斜线符号转义;*匹配星号,等等。
注意,在之前的表格中曾经多次使用问号(重载),这意味着要么匹配 0 次,要么匹配 1次,或者其他含义:如果问号紧跟在任何使用闭合操作符的匹配后面,它将直接要求正则表达式引擎匹配尽可能少的次数。
“尽可能少的次数”是什么意思?当模式匹配使用分组操作符时,正则表达式引擎将试图“吸收”匹配该模式的尽可能多的字符。这通常被叫做贪婪匹配。问号要求正则表达式引擎去“偷懒”,如果可能,就在当前的正则表达式中尽可能少地匹配字符,留下尽可能多的字符给后面的模式(如果存在)。本章末尾将用一个典型的示例来说明非贪婪匹配是很有必要的。现在继续查看闭包操作符
表示字符集的特殊字符(\d \w \D \s)
我们还提到有一些特殊字符能够表示字符集。与使用“0-9”这个范围表示十进制数相比,可以简单地使用 d 表示匹配任何十进制数字。另一个特殊字符(\w)能够用于表示全部字母数字的字符集,相当于[A-Za-z0-9_]的缩写形式,\s 可以用来表示空格字符。这些特殊字符的大写版本表示不匹配;例如,\D 表示任何非十进制数(与[^0-9]相同),等等。使用这些缩写,可以表示如下一些更复杂的示例。
使用圆括号指定分组( () )
现在,我们已经可以实现匹配某个字符串以及丢弃不匹配的字符串,但有些时候,我们可能会对之前匹配成功的数据更感兴趣。我们不仅想要知道整个字符串是否匹配我们的标准,而且想要知道能否提取任何已经成功匹配的特定字符串或者子字符串。答案是可以,要实现这个目标,只要用一对圆括号包裹任何正则表达式。
当使用正则表达式时,一对圆括号可以实现以下任意一个(或者两个)功能:
• 对正则表达式进行分组;
• 匹配子组。
关于为何想要对正则表达式进行分组的一个很好的示例是:当有两个不同的正则表达式而且想用它们来比较同一个字符串时。另一个原因是对正则表达式进行分组可以在整个正则表达式中使用重复操作符(而不是一个单独的字符或者字符集)。使用圆括号进行分组的一个副作用就是,匹配模式的子字符串可以保存起来供后续使用。
这些子组能够被同一次的匹配或者搜索重复调用,或者提取出来用于后续处理。
为什么匹配子组这么重要呢?主要原因是在很多时候除了进行匹配操作以外,我们还想要提取所匹配的模式。例如,如果决定匹配模式\w+-\d+,但是想要分别保存第一部分的字母和第二部分的数字,该如何实现?我们可能想要这样做的原因是,对于任何成功的匹配,我们可能想要看到这些匹配正则表达式模式的字符串究竟是什么。如果为两个子模式都加上圆括号,例如(\w+)-(\d+),然后就能够分别访问每一个匹配子组。我们更倾向于使用子组,这是因为择一匹配通过编写代码来判断是否匹配,然后执行另一个单独的程序(该程序也需要另行创建)来解析整个匹配仅仅用于提取两个部分。为什么不让 Python 自己实现呢?这是 re 模块支持的一个特性,所以为什么非要重蹈覆辙呢
扩展表示法(?....)
我们还没介绍过的正则表达式的最后一个方面是扩展表示法,它们是以问号开始(?…)。我们不会为此花费太多时间,因为它们通常用于在判断匹配之前提供标记,实现一个前视(或者后视)匹配,或者条件检查。尽管圆括号使用这些符号,但是只有(?P<name>)表述一个分组匹配。所有其他的都没有创建一个分组。然而,你仍然需要知道它们是什么,因为它们可能最适合用于你所需要完成的任务
正则表达式和Python语言
在了解了关于正则表达式的全部知识后,开始查看 Python 当前如何通过使用 re 模块来支持正则表达式
re 模块支持更强大而且更通用的 Perl 风格(Perl 5 风格)的正则表达式,该模块允许多个线程共享同一个已编译的正则表达式对象,也支持命名子组
re 模块:核心函数和方法
表列出了来自 re 模块的更多常见函数和方法。它们中的大多数函数也与已经编译的正则表达式对象(regex object)和正则匹配对象(regex match object)的方法同名并且具有相同的功能。本节将介绍两个主要的函数/方法——match()和 search(),以及 compile()函数。下一节将介绍更多的函数,但如果想进一步了解将要介绍或者没有介绍的更多相关信息,请查阅 Python 的相关文档。
核心提示:编译正则表达式(编译还是不编译?)
我们指定 eval()或者 exec(在 2.x 版本中或者在 3.x 版本的 exec()中)调用一个代码对象而不是一个字符串,性能上会有明显提升。这是由于对于前者而言,编译过程不会重复执行。换句话说,使用预编译的代码对象比直接使用字符串要快,因为解释器在执行字符串形式的代码前都必须把字符串编译成代码对象。
同样的概念也适用于正则表达式—在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象。由于正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译。而且,既然正则表达式的编译是必需的,那么使用预编译来提升执行性能无疑是明智之举。re.compile()
能够提供此功能。其实模块函数会对已编译的对象进行缓存,所以不是所有使用相同正则表达式模式的search()
和match()
都需要编译。即使这样,你也节省了缓存查询时间,并且不必对于相同的字符串反复进行函数调用。在不同的 Python 版本中,缓存中已编译过的 正则表达式对象的数目可能不同,而且没有文档记录。purge()
函数能够用于清除这些缓存。
使用 compile()函数编译正则表达式
后续将扼要介绍的几乎所有的 re 模块函数都可以作为 regex 对象的方法。注意,尽管推荐预编译,但它并不是必需的。如果需要编译,就使用编译过的方法;如果不需要编译,就使用函数。幸运的是,不管使用函数还是方法,它们的名字都是相同的(也许你曾对此感到好奇,这就是模块函数和方法的名字相同的原因,例如,search()、match()等)。因为这在大多数示例中省去一个小步骤,所以我们将使用字符串替代。我们仍将会遇到几个预编译代码的对象,这样就可以知道它的过程是怎么回事。
对于一些特别的正则表达式编译,可选的标记可能以参数的形式给出,这些标记允许不区分大小写的匹配,使用系统的本地化设置来匹配字母数字,等等。它们可以通过按位或操作符(|)合并。
匹配对象以及 group()和 groups()方法
当处理正则表达式时,除了正则表达式对象之外,还有另一个对象类型:匹配对象。这些是成功调用 match()或者 search()返回的对象。匹配对象有两个主要的方法:group()和groups()。
- group()要么返回整个匹配对象,要么根据要求返回特定子组。
- groups()则仅返回一个包含唯一或者全部子组的元组。
如果没有子组的要求,那么当group()仍然返回整个匹配时,groups()返回一个空元组。
使用 match()方法匹配字符串
match()是将要介绍的第一个 re 模块函数和正则表达式对象(regex object)方法。match()函数试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象;如果匹配失败,就返回 None,匹配对象的 group()方法能够用于显示那个成功的匹配。下面是如何运用 match()(以及 group())的一个示例
模式“foo”完全匹配字符串“foo”,我们也能够确认 m 是交互式解释器中匹配对象的示例。
如下为一个失败的匹配示例,它返回 None
因为上面的匹配失败,所以 m 被赋值为 None,而且以此方法构建的 if 语句没有指明任何操作。对于剩余的示例,如果可以,为了简洁起见,将省去 if 语句块,但在实际操作中,最好不要省去以避免 AttributeError 异常(None 是返回的错误值,该值并没有 group()属性[方法])
只要模式从字符串的起始部分开始匹配,即使字符串比模式长,匹配也仍然能够成功。
例如,模式“foo”将在字符串“food on the table”中找到一个匹配,因为它是从字符串的起始部分进行匹配的
可以看到,尽管字符串比模式要长,但从字符串的起始部分开始匹配就会成功。子串“foo”是从那个比较长的字符串中抽取出来的匹配部分。
甚至可以充分利用 Python 原生的面向对象特性,忽略保存中间过程产生的结果。
注意,在上面的一些示例中,如果匹配失败,将会抛出 AttributeError 异常
使用 search()在一个字符串中查找模式(搜索与匹配的对比)
其实,想要搜索的模式出现在一个字符串中间部分的概率,远大于出现在字符串起始部分的概率。这也就是 search()派上用场的时候了。search()的工作方式与 match()完全一致,不同之处在于 search()会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回 None。
我们将再次举例说明 match()和 search()之间的差别。以匹配一个更长的字符串为例,这次使用字符串“foo”去匹配“seafood”
可以看到,此处匹配失败。match()试图从字符串的起始部分开始匹配模式;也就是说,模式中的“f”将匹配到字符串的首字母“s”上,这样的匹配肯定是失败的。然而,字符串“foo”确实出现在“seafood”之中(某个位置),所以,我们该如何让 Python 得出肯定的结果呢?答案是使用 search()函数,而不是尝试匹配。search()函数不但会搜索模式在字符串中第一次出现的位置,而且严格地对字符串从左到右搜索。
此外,match()和 search()都使用可选的标记参数。最后,需要注意的是,等价的正则表达式对象方法使用可选的 pos 和 endpos 参数来指定目标字符串的搜索范围。
本节后面将使用 match()和 search()正则表达式对象方法以及 group()和 groups()匹配对象方法,通过展示大量的实例来说明 Python 中正则表达式的使用方法。我们将使用正则表达式语法中几乎全部的特殊字符和符号。
匹配多个字符串(|)
我们在正则表达式 bat|bet|bit 中使用了择一匹配(|)符号。如下为在 Python中使用正则表达式的方法
匹配任何单个字符(.)
我们展示了点号(.)不能匹配一个换行符\n 或者非字符(空字符).
下面的示例在正则表达式中搜索一个真正的句点(小数点),而我们通过使用一个反斜线对句点的功能进行转义:
创建字符集([])
前面详细讨论了[cr][23][dp][o2],以及它们与 r2d2|c3po 之间的差别。下面的示例将说明对于 r2d2|c3po 的限制将比[cr][23][dp][o2]更为严格。
重复、特殊字符以及分组( * + ? () )
正则表达式中最常见的情况包括特殊字符的使用、正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达式(\w+@\w+.com)。或许我们想要匹配比这个正则表达式所允许的更多邮件地址。为了在域名前添加主机名称支持,例如 www.xxx.com,仅仅允许 xxx.com 作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名(后面跟着一个句点),使用“?”操作符来表示该模式出现零次或者一次,然后按照如下所示的方式,插入可选的正则表达式到之前的正则表达式中:\w+@(\w+.)?\w+.com。从下面的示例中可见,该表达式允许.com 前面有一个或者两个名称
接下来,用以下模式来进一步扩展该示例,允许任意数量的中间子域名存在。请特别注意细节的变化,将“?”改为“. : \w+@(\w+.)\w+.com”
之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,而不是确定一个正则表达式匹配之后,在一个单独的子程序里面手动编码来解析字符串。此前还特别讨论过一个简单的正则表达式模式\w+-\d+,它由连字符号分隔的字母数字字符串和数字组成,还讨论了如何添加一个子组来构造一个新的正则表达式 (\w+)-(\d+)来完成这项工作。下面是初始版本的正则表达式的执行情况。
在上面的代码中,创建了一个正则表达式来识别包含 3 个字母数字字符且后面跟着 3 个数字的字符串。使用 abc-123 测试该正则表达式,将得到正确的结果,但是使用 abc-xyz 则不能。现在,将修改之前讨论过的正则表达式,使该正则表达式能够提取字母数字字符串和数字。如下所示,请注意如何使用 group()方法访问每个独立的子组以及 groups()方法以获取一个包含所有匹配子组的元组
由以上脚本内容可见,group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。可以使用 groups()方法来获取一个包含所有匹配子字符串的元组。如下为一个简单的示例,该示例展示了不同的分组排列,这将使整个事情变得更加清晰。
匹配字符串的起始和结尾以及单词边界
如下示例突出显示表示位置的正则表达式操作符。该操作符更多用于表示搜索而不是匹配,因为 match()总是从字符串开始位置进行匹配。
读者将注意到此处出现的原始字符串。你可能想要查看本章末尾部分的核心提示“Python中原始字符串的用法”(Using Python raw strings),里面提到了在此处使用它们的原因。通常情况下,在正则表达式中使用原始字符串是个好主意。读者还应当注意其他 4 个 re 模块函数和正则表达式对象方法:findall()、sub()、subn()和split()
使用 findall()和 finditer()查找每一次出现的位置
findall()查询字符串中某个正则表达式模式全部的非重复出现情况。这与 search()在执行字符串搜索时类似,但与 match()和 search()的不同之处在于,findall()总是返回一个列表。如果 findall()没有找到匹配的部分,就返回一个空列表,但如果匹配成功,列表将包含所有成功的匹配部分(从左向右按出现顺序排列)。
子组在一个更复杂的返回列表中搜索结果,而且这样做是有意义的,因为子组是允许从单个正则表达式中抽取特定模式的一种机制,例如匹配一个完整电话号码中的一部分(例如区号),或者完整电子邮件地址的一部分(例如登录名称).对于一个成功的匹配,每个子组匹配是由 findall()返回的结果列表中的单一元素;对于多个成功的匹配,每个子组匹配是返回的一个元组中的单一元素,而且每个元组(每个元组都对应一个成功的匹配)是结果列表中的元素。这部分内容可能第一次听起来令人迷惑,但是如果你尝试练习过一些不同的示例,就将澄清很多知识点。
finditer()函数是在 Python 2.2 版本中添加回来的,这是一个与 findall()函数类似但是更节省内存的变体。两者之间以及和其他变体函数之间的差异(很明显不同于返回的是一个迭代器还是列表)在于,和返回的匹配字符串相比,finditer()在匹配对象中迭代。如下是在单个字符串中两个不同分组之间的差别。
在下面的示例中,我们将在单个字符串中执行单个分组的多重匹配。
注意,使用 finditer()函数完成的所有额外工作都旨在获取它的输出来匹配 findall()的输出。
最后 ,与 match()和 search()类似,findall()和 finditer()方法的版本支持可选的 pos 和 endpos参数,这两个参数用于控制目标字符串的搜索边界,这与本章之前的部分所描述的类似
使用 sub()和 subn()搜索与替换
有两个函数/方法用于实现搜索和替换功能:sub()和 subn()。两者几乎一样,都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,但它也可能是一个函数,该函数返回一个用来替换的字符串。subn()和 sub()一样,但 subn()还返回一个表示替换的总数,替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回
前面讲到,使用匹配对象的 group()方法除了能够取出匹配分组编号外,还可以使用\N,其中 N 是在替换字符串中使用的分组编号。下面的代码仅仅只是将美式的日期表示法MM/DD/YY{,YY}格式转换为其他国家常用的格式 DD/MM/YY{,YY}
在限定模式上使用 split()分隔字符串
re 模块和正则表达式的对象方法 split()对于相对应字符串的工作方式是类似的,但是与分割一个固定字符串相比,它们基于正则表达式的模式分隔字符串,为字符串分隔功能添加一些额外的威力。如果你不想为每次模式的出现都分割字符串,就可以通过为 max 参数设定一个值(非零)来指定最大分割数。
如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么 re.split()与str.split()的工作方式相同,如下所示(基于单引号分割)
这是一个简单的示例。如果有一个更复杂的示例,例如,一个用于 Web 站点(类似于Google 或者 Yahoo! Maps)的简单解析器,该如何实现?用户需要输入城市和州名,或者城市名加上 ZIP 编码,还是三者同时输入?这就需要比仅仅是普通字符串分割更强大的处理方式,具体如下
上述正则表达式拥有一个简单的组件:使用 split 语句基于逗号分割字符串。更难的部分是最后的正则表达式,可以通过该正则表达式预览一些将在下一小节中介绍的扩展符号。在普通的英文中,通常这样说:如果空格紧跟在五个数字(ZIP 编码)或者两个大写字母(美国联邦州缩写)之后,就用 split 语句分割该空格。这就允许我们在城市名中放置空格。
通常情况下,这仅仅只是一个简单的正则表达式,可以在用来解析位置信息的应用中作为起点。该正则表达式并不能处理小写的州名或者州名的全拼、街道地址、州编码、ZIP+4(9 位 ZIP 编码)、经纬度、多个空格等内容(或者在处理时会失败)。这仅仅意味着使用 re.split()能够实现 str.split()不能实现的一个简单的演示实例。
我们刚刚已经证实,读者将从正则表达式 split 语句的强大能力中获益;然而,记得一定在编码过程中选择更合适的工具。如果对字符串使用 split 方法已经足够好,就不需要引入额外复杂并且影响性能的正则表达式
扩展符号
Python 的正则表达式支持大量的扩展符号。让我们一起查看它们中的一些内容,然后展示一些有用的示例。
通过使用 (?iLmsux) 系列选项,用户可以直接在正则表达式里面指定一个或者多个标记,而不是通过 compile()或者其他 re 模块函数。下面为一些使用 re.I/IGNORECASE 的示例,最后一个示例在 re.M/MULTILINE 实现多行混合:
在前两个示例中,显然是不区分大小写的。在最后一个示例中,通过使用“多行”,能够在目标字符串中实现跨行搜索,而不必将整个字符串视为单个实体。注意,此时忽略了实例“the”,因为它们并不出现在各自的行首。下一组演示使用 re.S/DOTALL。该标记表明点号(.)能够用来表示\n 符号(反之其通常用于表示除了\n 之外的全部字符)
re.X/VERBOSE 标记非常有趣;该标记允许用户通过抑制在正则表达式中使用空白符(除了在字符类中或者在反斜线转义中)来创建更易读的正则表达式。此外,散列、注释和井号也可以用于一个注释的起始,只要它们不在一个用反斜线转义的字符类中
(?:…)符号将更流行;通过使用该符号,可以对部分正则表达式进行分组,但是并不会保存该分组用于后续的检索或者应用。当不想保存今后永远不会使用的多余匹配时,这个符号就非常有用。
读者可以同时一起使用 (?P<name>) 和 (?P=name)符号。前者通过使用一个名称标识符而不是使用从 1 开始增加到 N 的增量数字来保存匹配,如果使用数字来保存匹配结果,我们就可以通过使用\1,\2 ...,\N \来检索。如下所示,可以使用一个类似风格的\g<name>来检索它们。
使用后者,可以在一个相同的正则表达式中重用模式,而不必稍后再次在(相同)正则表达式中指定相同的模式。例如,在本示例中,假定让读者验证一些电话号码的规范化。如下所示为一个丑陋并且压缩的版本,后面跟着一个正确使用的 (?x),使代码变得稍许易读
读者可以使用 (?=...) 和 (?!…)符号在目标字符串中实现一个前视匹配,而不必实际上使用这些字符串。前者是正向前视断言,后者是负向前视断言。在后面的示例中,我们仅仅对姓氏为“van Rossum”的人的名字感兴趣,下一个示例中,让我们忽略以“noreply”或者“postmaster”开头的 e-mail 地址。
第三个代码片段用于演示 findall()和 finditer()的区别;我们使用后者来构建一个使用相同登录名但不同域名的 e-mail 地址列表(在一个更易于记忆的方法中,通过忽略创建用完即丢弃的中间列表)