Python 自动化指南(繁琐工作自动化)第二版:七使用正则表达式的模式匹配
Posted 布客飞龙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 自动化指南(繁琐工作自动化)第二版:七使用正则表达式的模式匹配相关的知识,希望对你有一定的参考价值。
您可能熟悉通过按下CTRL+F
并输入您要查找的单词来搜索文本。正则表达式更进了一步:它们允许你指定文本的模式来搜索。您可能不知道某个企业的确切电话号码,但如果您住在美国或加拿大,您会知道它是三位数字,后跟一个连字符,然后是四位数字(还可以选择以三位数字的区号开头)。这就是你,作为一个人类,看到一个电话号码是怎么知道的:415-555-1234
是一个电话号码,但 4155551234 不是。
我们每天还会识别各种其他文本模式:电子邮件地址中间有@符号,美国社会保障号码有九位数字和两个连字符,网站 URL 通常有句点和正斜杠,新闻标题使用标题大小写,社交媒体标签以#
开头且不包含空格,等等。
正则表达式是有帮助的,但是很少有非程序员知道它们,即使大多数现代的文本编辑器和文字处理器,如 Microsoft Word 或 OpenOffice,都有查找和查找替换功能,可以基于正则表达式进行搜索。正则表达式不仅对软件用户来说,而且对程序员来说,都是巨大的省时工具。事实上,科技作家科利·多克托罗认为,我们应该在编程之前就教授正则表达式:
了解正则表达式可能意味着用 3 个步骤解决问题和用 3000 个步骤解决问题的区别。当你是一个书呆子的时候,你会忘记你通过几个按键解决的问题可能会花费其他人几天乏味的、容易出错的工作来完成 [1]。
在这一章中,你将首先编写一个程序,在不使用正则表达式的情况下找到文本模式,然后看看如何使用正则表达式使代码不那么臃肿。我将向您展示正则表达式的基本匹配,然后介绍一些更强大的特性,比如字符串替换和创建您自己的字符类。最后,在这一章的最后,你将编写一个程序,可以自动从文本块中提取电话号码和电子邮件地址。
不使用正则表达式查找文本模式
假设您想在一个字符串中查找一个美国电话号码。如果你是美国人,你应该知道这个模式:三个数字,一个连字符,三个数字,一个连字符,和四个数字。举个例子:415-555-4242
。
让我们使用一个名为isPhoneNumber()
的函数来检查一个字符串是否匹配这个模式,返回True
或False
。打开一个新的文件编辑器选项卡,并输入以下代码;然后将文件保存为isPhoneNumber.py
:
def isPhoneNumber(text):
if len(text) != 12: # ➊
return False
for i in range(0, 3):
if not text[i].isdecimal(): # ➋
return False
if text[3] != '-': # ➌
return False
for i in range(4, 7):
if not text[i].isdecimal(): # ➍
return False
if text[7] != '-': # ➎
return False
for i in range(8, 12):
if not text[i].isdecimal(): # ➏
return False
return True # ➐
print('Is 415-555-4242 a phone number?')
print(isPhoneNumber('415-555-4242'))
print('Is Moshi moshi a phone number?')
print(isPhoneNumber('Moshi moshi'))
运行该程序时,输出如下所示:
Is 415-555-4242 a phone number?
True
Is Moshi moshi a phone number?
False
isPhoneNumber()
函数的代码会进行几次检查,看看text
中的字符串是否是有效的电话号码。如果这些检查中有任何一项失败,该函数将返回False
。首先,代码检查字符串是否正好是 12 个字符 ➊。然后检查区号(即text
中的前三个字符)是否仅由数字字符 ➋ 组成。函数的其余部分检查字符串是否遵循电话号码的模式:号码必须在区号 ➌ 后有第一个连字符,再有三个数字字符 ➍,然后是另一个连字符 ➎,最后是四个数字 ➏。如果程序执行设法通过了所有检查,它返回True
➐。
用参数'415-555-4242'
调用isPhoneNumber()
将返回True
。用'Moshi moshi'
调用isPhoneNumber()
会返回False
;第一个测试失败了,因为'Moshi moshi'
不是 12 个字符长。
如果您想在一个更大的字符串中查找电话号码,您必须添加更多的代码来查找电话号码模式。用以下内容替换isPhoneNumber.py
中的最后四个print()
函数调用:
message = 'Call me at 415-555-1011 tomorrow. 415-555-9999 is my office.'
for i in range(len(message)):
chunk = message[i:i+12] # ➊
if isPhoneNumber(chunk): # ➋
print('Phone number found: ' + chunk)
print('Done')
当该程序运行时,输出将如下所示:
Phone number found: 415-555-1011
Phone number found: 415-555-9999
Done
在for
循环的每次迭代中,来自message
的 12 个字符的新块被分配给变量chunk
➊。比如第一次迭代,i
是0
,chunk
被赋值message[0:12]
(也就是字符串'Call me at 4'
)。在下一次迭代中,i
为1
,chunk
被赋值为message[1:13]
(字符串'all me at 41'
)。换句话说,在for
循环的每次迭代中,chunk
采用以下值:
'Call me at 4'
'all me at 41'
'll me at 415'
'l me at 415-'
- …诸如此类。
您将chunk
传递给isPhoneNumber()
以查看它是否与电话号码模式 ➋ 匹配,如果匹配,则打印大块内容。
继续循环通过message
,最终chunk
中的 12 个字符将是一个电话号码。该循环遍历整个字符串,测试每个 12 个字符的部分,并打印它找到的满足isPhoneNumber()
的任何chunk
。一旦我们完成了message
,我们打印Done
。
虽然在这个例子中message
中的字符串很短,但它可能有几百万个字符长,程序仍然会在不到一秒的时间内运行。使用正则表达式查找电话号码的类似程序也可以在不到一秒钟的时间内运行,但是正则表达式使得编写这些程序更快。
使用正则表达式查找文本模式
以前的电话号码查找程序可以工作,但是它使用大量代码来做一些有限的事情:isPhoneNumber()
函数有 17 行,但是只能找到一种电话号码模式。格式为415.555.4242
或(415) 555-4242
的电话号码呢?如果电话号码有分机,比如415-555-4242 x99
,会怎么样?isPhoneNumber()
函数将无法验证它们。您可以为这些额外的模式添加更多的代码,但是有一种更简单的方法。
正则表达式,简称为正则表达式,是对文本模式的描述。例如,正则表达式中的\\d
代表一个数字字符,即从 0 到 9 的任何一个数字。Python 使用正则\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d
来匹配与前面的isPhoneNumber()
函数相同的文本模式:一个由三个数字、一个连字符、另外三个数字、另一个连字符和四个数字组成的字符串。任何其他字符串都不会匹配\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d
正则表达式。
但是正则表达式可以复杂得多。例如,在一个模式后的大括号(3
)中添加一个3
就像说“匹配这个模式三次”。所以略短的正则表达式\\d3-\\d3-\\d4
也匹配正确的电话号码格式。
创建正则对象
Python 中所有的正则函数都在re
模块中。在交互式 Shell 中输入以下内容以导入该模块:
>>> import re
注
本章中的大多数例子都需要
re
模块,所以记得在你写的任何脚本的开头或者重启 Mu 的任何时候导入它。否则,你会得到一个NameError: name 're' undefined
的错误信息。
将表示正则表达式的字符串值传递给re.compile()
会返回一个Regex
模式对象(或者简单地说,一个Regex
对象)。
要创建一个匹配电话号码模式的Regex
对象,请在交互式 Shell 中输入以下内容。(请记住,\\d
表示“一个数字字符”,而\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d
是电话号码模式的正则表达式。)
>>> phoneNumRegex = re.compile(r'\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d')
现在phoneNumRegex
变量包含了一个Regex
对象。
匹配正则对象
一个Regex
对象的search()
方法在传递给它的字符串中搜索正则表达式的匹配项。如果在字符串中没有找到正则表达式模式,search()
方法将返回None
。如果发现模式,则search()
方法返回一个Match
对象,该对象有一个group()
方法,将从搜索的字符串中返回实际匹配的文本。(我很快会解释组。)例如,在交互式 Shell 中输入以下内容:
>>> phoneNumRegex = re.compile(r'\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d')
>>> mo = phoneNumRegex.search('My number is 415-555-4242.')
>>> print('Phone number found: ' + mo.group())
Phone number found: 415-555-4242
mo
变量名只是用于Match
对象的通用名称。这个例子初看起来可能很复杂,但是它比早期的isPhoneNumber.py
程序要短得多,并且做同样的事情。
在这里,我们将所需的模式传递给re.compile()
,并将结果Regex
对象存储在phoneNumRegex
中。然后我们调用phoneNumRegex
上的search()
,并向search()
传递我们在搜索过程中想要匹配的字符串。搜索的结果存储在变量mo
中。在这个例子中,我们知道我们的模式将在字符串中找到,所以我们知道将返回一个Match
对象。知道了mo
包含一个Match
对象而不是空值None
,我们可以调用mo
上的group()
来返回匹配。在我们的print()
函数调用中编写mo.group()
显示整个匹配,415-555-4242
。
正则表达式匹配的回顾
虽然在 Python 中使用正则表达式有几个步骤,但每个步骤都相当简单。
- 用
import re
导入正则模块。 - 用
re.compile()
函数创建一个Regex
对象。(记得使用原始字符串。) - 将您想要搜索的字符串传递到
Regex
对象的search()
方法中。这将返回一个Match
对象。 - 调用
Match
对象的group()
方法来返回实际匹配文本的字符串。
注
虽然我鼓励您将示例代码输入到交互式 Shell 中,但是您也应该使用基于 Web 的正则表达式测试器,它可以向您展示正则表达式是如何准确匹配您输入的一段文本的。我推荐
pythex.org
的测试人员。
使用正则表达式的更多模式匹配
现在您已经知道了使用 Python 创建和查找正则表达式对象的基本步骤,您已经准备好尝试一些更强大的模式匹配功能了。
用括号分组
假设您想将区号与电话号码的其余部分分开。添加括号将在正则(\\d\\d\\d)-(\\d\\d\\d-\\d\\d\\d\\d)
中创建分组。然后,您可以使用group()
match 对象方法从一个组中获取匹配的文本。
正则表达式字符串中的第一组括号将是分组1
。第二组将是组2
。通过将整数1
或2
传递给group()
匹配对象方法,可以获取匹配文本的不同部分。向group()
方法传递0
或什么都不传递将返回整个匹配的文本。在交互式 Shell 中输入以下内容:
>>> phoneNumRegex = re.compile(r'(\\d\\d\\d)-(\\d\\d\\d-\\d\\d\\d\\d)')
>>> mo = phoneNumRegex.search('My number is 415-555-4242.')
>>> mo.group(1)
'415'
>>> mo.group(2)
'555-4242'
>>> mo.group(0)
'415-555-4242'
>>> mo.group()
'415-555-4242'
如果您想一次检索所有的组,使用groups()
方法——注意名称的复数形式。
>>> mo.groups()
('415', '555-4242')
>>> areaCode, mainNumber = mo.groups()
>>> print(areaCode)
415
>>> print(mainNumber)
555-4242
由于mo.groups()
返回多个值的元组,您可以使用多重赋值技巧将每个值赋给一个单独的变量,如前面的areaCode, mainNumber = mo.groups()
行所示。
括号在正则表达式中有特殊的含义,但是如果需要在文本中匹配一个括号,该怎么办呢?例如,也许您试图匹配的电话号码在括号中设置了区号。在这种情况下,需要用反斜杠对(
和)
字符进行转义。在交互式 Shell 中输入以下内容:
>>> phoneNumRegex = re.compile(r'(\\(\\d\\d\\d\\)) (\\d\\d\\d-\\d\\d\\d\\d)')
>>> mo = phoneNumRegex.search('My phone number is (415) 555-4242.')
>>> mo.group(1)
'(415)'
>>> mo.group(2)
'555-4242'
传递给re.compile()
的原始字符串中的\\(
和\\)
转义字符将匹配实际的括号字符。在正则表达式中,下列字符具有特殊含义:
. ^ $ * + ? [ ] \\ | ( )
如果您想将这些字符检测为您的文本模式的一部分,您需要用反斜杠对它们进行转义:
\\. \\^ \\$ \\* \\+ \\? \\ \\ \\[ \\] \\\\ \\| \\( \\)
确保仔细检查,没有将转义括号\\(
和\\)
误认为正则表达式中的括号(
和)
。如果您收到有关“丢失”或“不平衡括号”的错误消息,您可能忘记了包括组的右非转义括号,如下例所示:
>>> re.compile(r'(\\(Parentheses\\)')
Traceback (most recent call last):
--snip--
re.error: missing ), unterminated subpattern at position 0
错误消息告诉您在r'(\\(Parentheses\\)'
字符串的索引0
处有一个左括号,它缺少相应的右括号。
用管道匹配多个分组
这个|
字符被称为管道。您可以在任何想要匹配众多表达式之一的地方使用它。例如,正则表达式r'Batman|Tina Fey'
将匹配'Batman'
或'Tina Fey'
。
当蝙蝠侠和蒂娜·菲都出现在搜索字符串中时,匹配文本的第一次出现将作为Match
对象返回。在交互式 Shell 中输入以下内容:
>>> heroRegex = re.compile (r'Batman|Tina Fey')
>>> mo1 = heroRegex.search('Batman and Tina Fey')
>>> mo1.group()
'Batman'
>>> mo2 = heroRegex.search('Tina Fey and Batman')
>>> mo2.group()
'Tina Fey'
注
你可以用第 171 页中讨论的
findall()
方法找到所有的匹配事件。
作为正则表达式的一部分,您还可以使用管道来匹配几种模式中的一种。例如,假设您想要匹配任意字符串'Batman'
、'Batmobile'
、'Batcopter'
和'Batbat'
。由于所有这些字符串都以Bat
开头,如果您可以只指定一次前缀就好了。这可以用括号来完成。在交互式 Shell 中输入以下内容:
>>> batRegex = re.compile(r'Bat(man|mobile|copter|bat)')
>>> mo = batRegex.search('Batmobile lost a wheel')
>>> mo.group()
'Batmobile'
>>> mo.group(1)
'mobile'
方法调用mo.group()
返回完全匹配的文本'Batmobile'
,而mo.group(1)
只返回第一个括号组'mobile'
内的部分匹配文本。通过使用管道字符和分组括号,您可以指定希望正则表达式匹配的几种替代模式。
如果需要匹配一个实际的管道字符,用反斜杠对其进行转义,比如\\|
。
问号与可选匹配
有时,有一种模式,您只想随意匹配。也就是说,无论该文本是否存在,正则表达式都应该找到一个匹配。?
字符将它前面的组标记为模式的可选部分。例如,在交互式 Shell 中输入以下内容:
>>> batRegex = re.compile(r'Bat(wo)?man')
>>> mo1 = batRegex.search('The Adventures of Batman')
>>> mo1.group()
'Batman'
>>> mo2 = batRegex.search('The Adventures of Batwoman')
>>> mo2.group()
'Batwoman'
正则表达式的(wo)?
部分意味着模式wo
是一个可选组。正则表达式将匹配没有实例或只有一个实例的文本。这就是正则表达式同时匹配'Batwoman'
和'Batman'
的原因。
使用前面的电话号码示例,您可以让正则表达式查找有或没有区号的电话号码。在交互式 Shell 中输入以下内容:
>>> phoneRegex = re.compile(r'(\\d\\d\\d-)?\\d\\d\\d-\\d\\d\\d\\d')
>>> mo1 = phoneRegex.search('My number is 415-555-4242')
>>> mo1.group()
'415-555-4242'
>>> mo2 = phoneRegex.search('My number is 555-4242')
>>> mo2.group()
'555-4242'
你可以认为?
是在说,“匹配这个问号前面的零个或一个组”。
如果需要匹配一个实际的问号字符,用\\?
转义。
使用星号匹配零个或多个
*
(称为星号或乘号)表示“匹配零个或更多”——星号前面的组可以在文本中出现任意次。可以完全没有,也可以一遍遍重复。让我们再来看看蝙蝠侠的例子。
>>> batRegex = re.compile(r'Bat(wo)*man')
>>> mo1 = batRegex.search('The Adventures of Batman')
>>> mo1.group()
'Batman'
>>> mo2 = batRegex.search('The Adventures of Batwoman')
>>> mo2.group()
'Batwoman'
>>> mo3 = batRegex.search('The Adventures of Batwowowowoman')
>>> mo3.group()
'Batwowowowoman'
对于'Batman'
,正则表达式的(wo)*
部分匹配字符串中wo
的零个实例;对于'Batwoman'
,(wo)*
匹配wo
的一个实例;而对于'Batwowowowoman'
,(wo)*
匹配wo
的四个实例。
如果需要匹配一个实际的星号字符,可以在正则表达式中的星号前面加上反斜杠\\*
。
使用加号匹配一个或多个
*
表示“匹配零个或多个”,而+
(加号)表示“匹配一个或多个”与星号不同,星号不要求其组出现在匹配的字符串中,加号前面的组必须至少出现一次。它不是可选的。在交互式 Shell 中输入以下内容,并与上一节中的星形正则表达式进行比较:
>>> batRegex = re.compile(r'Bat(wo)+man')
>>> mo1 = batRegex.search('The Adventures of Batwoman')
>>> mo1.group()
'Batwoman'
>>> mo2 = batRegex.search('The Adventures of Batwowowowoman')
>>> mo2.group()
'Batwowowowoman'
>>> mo3 = batRegex.search('The Adventures of Batman')
>>> mo3 == None
True
正则表达式Bat(wo)+man
将不匹配字符串'The Adventures of Batman'
,因为加号需要至少一个wo
。
如果你需要匹配一个实际的加号字符,在加号前加一个反斜杠来转义它:\\+
。
使用花括号匹配特定的重复
如果您有一个要重复特定次数的组,请在正则表达式中的该组后面加上一个大括号中的数字。例如,正则表达式(Ha)3
将匹配字符串'HaHaHa'
,但它不会匹配'HaHa'
,因为后者只有两个重复的(Ha)
组。
除了一个数字,您还可以通过在大括号之间写入最小值、逗号和最大值来指定一个范围。例如,正则表达式(Ha)3,5
将匹配'HaHaHa'
、'HaHaHaHa'
和'HaHaHaHaHa'
。
您也可以省略大括号中的第一个或第二个数字,使最小值或最大值不受限制。例如,(Ha)3,
将匹配(Ha)
组的三个或更多实例,而(Ha),5
将匹配零到五个实例。大括号有助于缩短正则表达式。这两个正则表达式匹配相同的模式:
(Ha)3
(Ha)(Ha)(Ha)
这两个正则表达式也匹配相同的模式:
(Ha)3,5
((Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha)(Ha))
在交互式 Shell 中输入以下内容:
>>> haRegex = re.compile(r'(Ha)3')
>>> mo1 = haRegex.search('HaHaHa')
>>> mo1.group()
'HaHaHa'
>>> mo2 = haRegex.search('Ha')
>>> mo2 == None
True
这里,(Ha)3
与'HaHaHa'
匹配,但与'Ha'
不匹配。由于与'Ha'
不匹配,search()
返回None
。
贪婪和非贪婪匹配
既然(Ha)3,5
可以匹配字符串'HaHaHaHaHa'
中的三个、四个或五个Ha
实例,您可能想知道为什么在前面的大括号示例中Match
对象对group()
的调用返回'HaHaHaHaHa'
而不是更短的可能性。毕竟'HaHaHa'
和'HaHaHaHa'
也是正则表达式(Ha)3,5
的有效匹配。
默认情况下,Python 的正则表达式是贪婪的,这意味着在不明确的情况下,它们将匹配最长的字符串。大括号的非贪婪(也称为惰性)版本匹配尽可能最短的字符串,右大括号后面跟一个问号。
在交互式 Shell 中输入以下内容,注意搜索相同字符串的大括号的贪婪形式和非贪婪形式之间的区别:
>>> greedyHaRegex = re.compile(r'(Ha)3,5')
>>> mo1 = greedyHaRegex.search('HaHaHaHaHa')
>>> mo1.group()
'HaHaHaHaHa'
>>> nongreedyHaRegex = re.compile(r'(Ha)3,5?')
>>> mo2 = nongreedyHaRegex.search('HaHaHaHaHa')
>>> mo2.group()
'HaHaHa'
请注意,问号在正则表达式中可能有两种含义:语句非贪婪匹配或标记可选组。这些意义完全不相关。
findall()
方法
除了search()
方法,Regex
对象也有一个findall()
方法。search()
将返回被搜索字符串中第一匹配文本的Match
对象,而findall()
方法将返回被搜索字符串中每个匹配的字符串。要查看search()
如何仅在匹配文本的第一个实例上返回一个Match
对象,请在交互式 Shell 中输入以下内容:*
>>> phoneNumRegex = re.compile(r'\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d')
>>> mo = phoneNumRegex.search('Cell: 415-555-9999 Work: 212-555-0000')
>>> mo.group()
'415-555-9999'
另一方面,只要正则表达式中没有组,findall()
就不会返回一个Match
对象,而是返回一个字符串列表。列表中的每个字符串都是匹配正则表达式的一段搜索文本。在交互式 Shell 中输入以下内容:
>>> phoneNumRegex = re.compile(r'\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d') # has no groups
>>> phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000')
['415-555-9999', '212-555-0000']
如果正则表达式中有分组,那么findall()
将返回元组列表。每个元组代表一个找到的匹配,它的项是正则表达式中每个组的匹配字符串。要查看findall()
的运行情况,请在交互式 Shell 中输入以下内容(注意,现在正在编译的正则表达式在括号中有组):
>>> phoneNumRegex = re.compile(r'(\\d\\d\\d)-(\\d\\d\\d)-(\\d\\d\\d\\d)') # has groups
>>> phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000')
[('415', '555', '9999'), ('212', '555', '0000')]
为了总结findall()
方法返回的内容,请记住以下几点:
- 当在不带组的正则表达式上调用时,比如
\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d
,方法findall()
返回一个字符串匹配列表,比如['415-555-9999', '212-555-0000']
。 - 当在有组的正则表达式上调用时,比如
(\\d\\d\\d)-(\\d\\d\\d)-(\\d\\d\\d\\d)
,方法findall()
返回一个字符串元组列表(每个组一个字符串),比如[('415', '555', '9999'), ('212', '555', '0000')]
。
字符类
在前面的电话号码正则表达式示例中,您了解到\\d
可以代表任何数字。也就是说,\\d
是正则表达式(0|1|2|3|4|5|6|7|8|9)
的简写。这样的速记字符类还有很多,如表 7-1 所示。
表 7-1: 常用字符类的速记代码
速记字符类 | 代表 |
---|---|
\\d | 从 0 到 9 的任何数字。 |
\\D | 任何不是从 0 到 9 的数字的字符。 |
\\w | 任何字母、数字或下划线字符。(把这个想象成匹配“单词”字符。) |
\\W | 任何不是字母、数字或下划线字符的字符。 |
\\s | 任何空格、制表符或换行符。(把这个想象成匹配“空白”字符。) |
\\S | 任何不是空格、制表符或换行符的字符。 |
字符类有利于缩短正则表达式。字符类[0-5]
将只匹配数字0
到5
;这比敲(0|1|2|3|4|5)
短多了。注意,虽然\\d
匹配数字,而\\w
匹配数字、字母和下划线,但是没有只匹配字母的速记字符类。(尽管您可以使用[a-zA-Z]
字符类,如下所述。)
例如,在交互式 Shell 中输入以下内容:
>>> xmasRegex = re.compile(r'\\d+\\s\\w+')
>>> xmasRegex.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7
swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge')
['12 drummers', '11 pipers', '10 lords', '9 ladies', '8 maids', '7 swans', '6
geese', '5 rings', '4 birds', '3 hens', '2 doves', '1 partridge']
正则表达式\\d+\\s\\w+
将匹配包含一个或多个数字(\\d+
)、一个空白字符(\\s
)、一个或多个字母/数字/下划线字符(\\w+
)的文本。findall()
方法在一个列表中返回正则表达式模式的所有匹配字符串。
创建自己的字符类
有时候,您想要匹配一组字符,但是速记字符类(\\d
、\\w
、\\s
等)太宽泛。您可以使用方括号定义自己的字符类。例如,字符类[aeiouAEIOU]
将匹配任何元音字母,包括小写和大写。在交互式 Shell 中输入以下内容:
>>> vowelRegex = re.compile(r'[aeiouAEIOU]')
>>> vowelRegex.findall('RoboCop eats baby food. BABY FOOD.')
['o', 'o', 'o', 'e', 'a', 'a', 'o', 'o', 'A'Python 自动化指南(繁琐工作自动化)第二版:十八发送电子邮件和短信
检查和回复电子邮件是一项巨大的时间消耗。当然,你不能只写一个程序来帮你处理所有的邮件,因为每封邮件都需要它自己的回应。但是,一旦你知道如何编写可以发送和接收电子邮件的程序,你仍然可以自动完成大量与电子邮件相关的任务。
例如,您可能有一个充满客户记录的电子表格,并希望根据每个客户的年龄和位置信息向他们发送不同的套用信函。商业软件可能无法为你做到这一点;幸运的是,您可以编写自己的程序来发送这些电子邮件,从而节省大量复制和粘贴表单电子邮件的时间。
你也可以编写程序,发送电子邮件和短信通知你,即使你不在电脑旁。如果你正在自动化一个需要几个小时才能完成的任务,你不会想每隔几分钟就回到你的电脑前检查程序的状态。相反,该程序可以在完成后给你的手机发短信——让你在离开电脑时专注于更重要的事情。
本章介绍了 EZGmail 模块,这是一种从 Gmail 帐户发送和阅读电子邮件的简单方法,以及一个使用标准 SMTP 和 IMAP 电子邮件协议的 Python 模块。
警告
我强烈建议你为任何发送或接收电子邮件的脚本设置一个单独的电子邮件帐户。这将防止程序中的错误影响您的个人电子邮件帐户(例如,通过删除电子邮件或意外发送垃圾邮件给您的联系人)。最好先做一次预演,注释掉实际发送或删除电子邮件的代码,并用一个临时的print()
调用来替换它。这样你可以在真正运行程序之前测试它。
使用 Gmail API 发送和接收电子邮件
Gmail 拥有将近三分之一的电子邮件客户端市场份额,你很可能至少有一个 Gmail 电子邮件地址。由于额外的安全和反垃圾邮件措施,通过 EZGmail 模块比通过smtplib
和imapclient
更容易控制 Gmail 帐户,这将在本章稍后讨论。EZGmail 是我编写的一个模块,它工作在官方 Gmail API 之上,并提供了一些功能,使从 Python 使用 Gmail 变得很容易。你可以在asweigart/ezgmail
找到 EZGmail 的全部细节。EZGmail 不是由谷歌制作的,也不隶属于谷歌;在developers.google.com/gmail/api/v1/reference
找到 Gmail API 官方文档。
要安装 EZGmail,在 Windows 上运行pip install --user --upgrade ezgmail
(或者在 MacOS 和 Linux 上使用pip3
)。--upgrade
选项将确保您安装最新版本的软件包,这是与不断变化的在线服务(如 Gmail API)交互所必需的。
启用 Gmail API
在你写代码之前,你必须先在 Gmail 注册一个 Gmail 邮箱账户。然后,转到developers.google.com/gmail/api/quickstart/python
,点击页面上的启用 Gmail API 按钮,并填写出现的表单。
填写完表单后,页面会显示一个指向credentials.json
文件的链接,您需要下载该文件并将其放在与您的py
文件相同的文件夹中。credentials.json
文件包含客户端 ID 和客户端机密信息,您应该将其视为您的 Gmail 密码,不要与任何人共享。
然后,在交互式 Shell 中,输入以下代码:
>>> import ezgmail, os
>>> os.chdir(r'C:\\path\\to\\credentials_json_file')
>>> ezgmail.init()
确保你将当前的工作目录设置在与credentials.json
相同的文件夹中,并且你已经连接到互联网。ezgmail.init()
函数将打开您的浏览器,进入谷歌登录页面。输入您的 Gmail 地址和密码。该页面可能会警告你“此应用未经验证”,但这没关系;点击高级,然后进入快速启动 ( 不安全)。(如果你为其他人编写 Python 脚本,并且不希望这个警告对他们出现,你需要了解 Google 的应用验证过程,这超出了本书的范围。)当下一页提示您“Quickstart 想要访问您的 Google 帐户”时,单击允许,然后关闭浏览器。
将生成一个token.json
文件,让您的 Python 脚本可以访问您输入的 Gmail 帐户。浏览器只有在找不到现有的token.json
文件时才会打开登录页面。有了credentials.json
和token.json
,你的 Python 脚本可以从你的 Gmail 账户发送和阅读电子邮件,而不需要你在源代码中包含 Gmail 密码。
从 Gmail 账户发送邮件
一旦有了一个token.json
文件,EZGmail 模块应该能够通过一个函数调用发送电子邮件:
>>> import ezgmail
>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email')
如果您想将文件附加到您的电子邮件中,您可以为send()
函数提供一个额外的列表参数:
>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email',
['attachment1.jpg', 'attachment2.mp3'])
请注意,作为其安全和反垃圾邮件功能的一部分,Gmail 可能不会重复发送文本完全相同的电子邮件(因为这些很可能是垃圾邮件),或包含exe
的电子邮件,或者zip
文件附件(因为它们可能是病毒)。
您还可以提供可选的关键字参数cc
和bcc
来发送副本和密件副本:
>>> import ezgmail
>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email',
cc='friend@example.com', bcc='otherfriend@example.com,someoneelse@example.com')
如果你需要记住token.json
文件是为哪个 Gmail 地址配置的,可以查看ezgmail.EMAIL_ADDRESS
。请注意,只有在调用了ezgmail.init()
或任何其他 EZGmail 函数之后,才会填充该变量:
>>> import ezgmail
>>> ezgmail.init()
>>> ezgmail.EMAIL_ADDRESS
'example@gmail.com'
确保将token.json
文件视为与您的密码相同。如果其他人获得了此文件,他们可以访问您的 Gmail 帐户(尽管他们无法更改您的 Gmail 密码)。要撤销之前发布的token.json
文件,请前往security.google.com/settings/security/permissions?pli=1
和撤销对快速入门应用的访问。您需要运行ezgmail.init()
并再次通过登录过程来获得一个新的token.json
文件。
从 Gmail 账户中读取邮件
Gmail 将相互回复的电子邮件组织成对话线索。当你通过网络浏览器或应用登录 Gmail 时,你看到的是邮件群,而不是单封邮件(即使邮件群中只有一封邮件)。
EZGmail 有GmailThread
和GmailMessage
对象分别代表对话线程和个人邮件。一个GmailThread
对象有一个messages
属性,它保存了一个GmailMessage
对象的列表。unread()
函数返回所有未读邮件的GmailThread
对象列表,然后可以将该列表传递给ezgmail.summary()
以打印该列表中对话线程的摘要:
>>> import ezgmail
>>> unreadThreads = ezgmail.unread() # List of GmailThread objects.
>>> ezgmail.summary(unreadThreads)
Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09
Jon - Thanks for stopping me from buying Bitcoin. - Dec 09
summary()
函数可以方便地显示电子邮件线程的快速摘要,但是要访问特定的消息(和部分消息),您需要检查GmailThread
对象的messages
属性。messages
属性包含组成线程的GmailMessage
对象的列表,这些对象具有描述电子邮件的subject
、body
、timestamp
、sender
和recipient
属性:
>>> len(unreadThreads)
2
>>> str(unreadThreads[0])
"<GmailThread len=2 snippet= Do you want to watch RoboCop this weekend?'>"
>>> len(unreadThreads[0].messages)
2
>>> str(unreadThreads[0].messages[0])
"<GmailMessage from='Al Sweigart <al@inventwithpython.com>' to='Jon Doe
<example@gmail.com>' timestamp=datetime.datetime(2018, 12, 9, 13, 28, 48)
subject='RoboCop' snippet='Do you want to watch RoboCop this weekend?'>"
>>> unreadThreads[0].messages[0].subject
'RoboCop'
>>> unreadThreads[0].messages[0].body
'Do you want to watch RoboCop this weekend?\\r\\n'
>>> unreadThreads[0].messages[0].timestamp
datetime.datetime(2018, 12, 9, 13, 28, 48)
>>> unreadThreads[0].messages[0].sender
'Al Sweigart <al@inventwithpython.com>'
>>> unreadThreads[0].messages[0].recipient
'Jon Doe <example@gmail.com>'
与ezgmail.unread()
函数类似,ezgmail.recent()
函数将返回您 Gmail 帐户中最近的 25 个主题。你可以通过一个可选的maxResults
关键字参数来改变这个限制:
>>> recentThreads = ezgmail.recent()
>>> len(recentThreads)
25
>>> recentThreads = ezgmail.recent(maxResults=100)
>>> len(recentThreads)
46
从 Gmail 账户中搜索邮件
除了使用ezgmail.unread()
和ezgmail.recent()
之外,你还可以搜索特定的电子邮件,就像你在搜索框中输入查询一样,通过调用ezgmail.search()
:
>>> resultThreads = ezgmail.search('RoboCop')
>>> len(resultThreads)
1
>>> ezgmail.summary(resultThreads)
Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09
前面的search()
调用应该产生相同的结果,就好像你在搜索框中输入“机械战警”,如图 18-1 中的所示。
图 18-1:在 Gmail 网站搜索“机械战警”邮件
像unread()
和recent()
一样,search()
函数返回一个GmailThread
对象的列表。您还可以将您可以在搜索框中输入的任何特殊搜索操作符传递给search()
函数,如下所示:
'label:UNREAD'
表示未读邮件
'from:al@inventwithpython.com'
表示来自al@inventwithpython.com
的邮件
'subject:hello'
表示主题中带有“你好”的电子邮件
'has:attachment'
表示带文件附件的邮件
您可以在support.google.com/mail/answer/7190?hl=en
查看搜索运营商的完整列表。
从 Gmail 帐户下载附件
GmailMessage
对象有一个 attachments 属性,它是消息附件的文件名列表。你可以将这些名字中的任何一个传递给对象的downloadAttachment()
方法来下载文件。也可以用downloadAllAttachments()
一次性下载全部。默认情况下,EZGmail 会将附件保存到当前工作目录,但是您也可以将一个额外的downloadFolder
关键字参数传递给downloadAttachment()
和downloadAllAttachments()
。例如:
>>> import ezgmail
>>> threads = ezgmail.search('vacation photos')
>>> threads[0].messages[0].attachments
['tulips.jpg', 'canal.jpg', 'bicycles.jpg']
>>> threads[0].messages[0].downloadAttachment('tulips.jpg')
>>> threads[0].messages[0].downloadAllAttachments(downloadFolder='vacat
ion2019')
['tulips.jpg', 'canal.jpg', 'bicycles.jpg']
如果以附件的文件名命名的文件已经存在,下载的附件将自动覆盖它。
EZGmail 包含额外的功能,你可以在asweigart/ezgmail
的找到完整的文档。
SMTP
就像 HTTP 是计算机通过互联网发送网页的协议一样,简单邮件传输协议(SMTP) 是用于发送电子邮件的协议。SMTP 规定了在您点按“发送”后,电子邮件应该如何格式化、加密以及在邮件服务器之间中继,以及您的电脑处理的所有其他详细信息。不过,你不需要知道这些技术细节,因为 Python 的smtplib
模块将它们简化成了几个函数。
SMTP 只是处理发送电子邮件给他人。另一种不同的协议叫做 IMAP,处理检索发送给你的电子邮件,在第 424 页的 IMAP 中有描述。
除了 SMTP 和 IMAP 之外,当今大多数基于 Web 的电子邮件运营商还采取了其他安全措施来防止垃圾邮件、网络钓鱼和其他恶意电子邮件的使用。这些措施防止 Python 脚本使用smtplib
和imapclient
模块登录电子邮件帐户。然而,其中许多服务都有 API 和特定的 Python 模块,允许脚本访问它们。本章介绍了 Gmail 的模块。对于其他人,您需要查阅他们的在线文档。
发送电子邮件
您可能熟悉从 Outlook 或 Thunderbird 或通过 Gmail 或 Yahoo Mail 等网站发送电子邮件。不幸的是,Python 并没有像那些服务一样提供给你一个漂亮的图形用户界面。相反,您可以调用函数来执行 SMTP 的每个主要步骤,如下面的交互式 Shell 示例所示。
注
不要在交互 Shell 中输入这个例子;这是行不通的,因为smtp.example.com
、bob@example.com
、MY_SECRET_PASSWORD
、alice@example.com
都只是占位符。这段代码只是用 Python 发送电子邮件过程的概述。
>>> import smtplib
>>> smtpObj = smtplib.SMTP('smtp.example.com', 587)
>>> smtpObj.ehlo()
(250, b'mx.example.com at your service, [216.172.148.131]\\nSIZE 35882577\\
n8BITMIME\\nSTARTTLS\\nENHANCEDSTATUSCODES\\nCHUNKING')
>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')
>>> smtpObj.login('bob@example.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')
>>> smtpObj.sendmail('bob@example.com', 'alice@example.com', 'Subject: So
long.\\nDear Alice, so long and thanks for all the fish. Sincerely, Bob')
>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')
在接下来的几节中,我们将介绍每个步骤,用您的信息替换占位符,以连接并登录到 SMTP 服务器,发送电子邮件,以及断开与服务器的连接。
连接 SMTP 服务器
如果您曾经设置过 Thunderbird、Outlook 或其他程序来连接到您的电子邮件帐户,您可能会熟悉 SMTP 服务器和端口的配置。这些设置因电子邮件运营商而异,但是在网上搜索你的运营商的 SMTP 设置应该会找到要使用的服务器和端口。
SMTP 服务器的域名通常是您的电子邮件运营商的域名,前面带有smtp.
。例如,威瑞森的 SMTP 服务器在smtp.verizon.net
。表 18-1 列出了一些常见的电子邮件运营商及其 SMTP 服务器。(端口是一个整数值,几乎总是 587。它由命令加密标准 TLS 使用。)
*表 18-1: 电子邮件运营商及其 SMTP 服务器
运营商 SMTP 服务器域名 Gmail⭐ smtp.gmail.com
Outlook/Hotmail smtp-mail.outlook.com
YahooMail⭐ smtp.mail.yahoo.com
ATT http://smpt.mail.att.net
(端口 465)Comcast smtp.comcast.net
Verizon incoming.verizon.net
(465 端口)⭐额外的安全措施阻止 Python 使用smtplib
模块登录这些服务器。EZGmail 模块可以为 Gmail 帐户绕过这个困难。
一旦您有了电子邮件运营商的域名和端口信息,通过调用smptlib.SMTP()
创建一个SMTP
对象,将域名作为字符串参数传递,将端口作为整数参数传递。SMTP
对象表示一个到 SMTP 邮件服务器的连接,并有发送电子邮件的方法。例如,下面的调用创建了一个SMTP
对象,用于连接到一个假想的电子邮件服务器:
>>> smtpObj = smtplib.SMTP('smtp.example.com', 587)
>>> type(smtpObj)
<class 'smtplib.SMTP'>
输入type(smtpObj)
会显示在smtpObj
中存储了一个SMTP
对象。您将需要这个SMTP
对象来调用让您登录和发送电子邮件的方法。如果smptlib.SMTP()
调用不成功,您的 SMTP 服务器可能不支持端口 587 上的 TLS。在这种情况下,您需要使用smtplib.SMTP_SSL()
和端口 465 创建一个SMTP
对象。
>>> smtpObj = smtplib.SMTP_SSL('smtp.example.com', 465)
注
如果你没有连接到互联网,Python 会抛出一个socket.gaierror: [Errno 11004] getaddrinfo failed
或者类似的异常。
对于您的程序来说,TLS 和 SSL 之间的区别并不重要。您只需要知道您的 SMTP 服务器使用哪种加密标准,这样您就知道如何连接到它。在接下来的所有交互式 Shell 示例中,smtpObj
变量将包含一个由smtplib.SMTP()
或smtplib.SMTP_SSL()
函数返回的SMTP
对象。
发送 SMTP“你好”消息
一旦有了SMTP
对象,调用其奇怪命名的ehlo()
方法向 SMTP 电子邮件服务器“问好”。这个问候语是 SMTP 中的第一步,对于建立与服务器的连接非常重要。你不需要知道这些协议的细节。只要确保在得到SMTP
对象后首先调用ehlo()
方法,否则后面的方法调用将导致错误。下面是一个ehlo()
调用及其返回值的例子:
>>> smtpObj.ehlo()
(250, b'mx.example.com at your service, [216.172.148.131]\\nSIZE 35882577\\
n8BITMIME\\nSTARTTLS\\nENHANCEDSTATUSCODES\\nCHUNKING')
如果返回的元组中的第一项是整数250
(SMTP 中“成功”的代码),那么问候成功。
启用 TLS 加密
如果您正在连接到 SMTP 服务器上的端口 587(也就是说,您正在使用 TLS 加密),那么接下来您需要调用starttls()
方法。这个必需的步骤为您的连接启用加密。如果您连接到端口 465(使用 SSL),那么加密已经设置好了,您应该跳过这一步。
下面是一个starttls()
方法调用的例子:
>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')
starttls()
方法将您的 SMTP 连接置于 TLS 模式。返回值中的220
告诉你服务器已经准备好了。
登录 SMTP 服务器
一旦建立了到 SMTP 服务器的加密连接,您就可以通过调用login()
方法使用您的用户名(通常是您的电子邮件地址)和电子邮件密码登录。
>>> smtpObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')
将您的电子邮件地址字符串作为第一个参数,将您的密码字符串作为第二个参数。返回值中的235
表示认证成功。Python 针对不正确的密码引发了一个smtplib.SMTPAuthenticationError
异常。
警告
在源代码中输入密码时要小心。如果任何人复制了你的程序,他们将可以访问你的电子邮件帐户!调用input()
并让用户输入密码是个好主意。每次运行程序时都必须输入密码可能不太方便,但这种方法可以防止您将密码保存在计算机上的未加密文件中,这样黑客或笔记本电脑窃贼就可以很容易地获得密码。
发送邮件
一旦登录到您的电子邮件运营商的 SMTP 服务器,您就可以调用sendmail()
方法来实际发送电子邮件。sendmail()
方法调用如下所示:
>>> smtpObj.sendmail('my_email_address@example.com
', 'recipient@example.com', 'Subject: So long.\\nDear Alice, so long and thanks for all the fish.
Sincerely, Bob')
sendmail()
方法需要三个参数:
- 字符串形式的电子邮件地址(代表电子邮件的“发件人”地址)
- 字符串形式的收件人电子邮件地址,或者多个收件人的字符串列表(对于“收件人”地址)
- 字符串形式的电子邮件正文
电子邮件正文字符串的开头必须是以'Subject: \\n'
开头的电子邮件主题行。'\\n'
换行符将电子邮件的主题行与正文分开。
从sendmail()
返回的值是一个字典。对于电子邮件传递失败的每个收件人,字典中都会有一个键值对。一个空字典意味着所有的收件人都成功发送了邮件。
断开与 SMTP 服务器的连接
发送完电子邮件后,一定要调用quit()
方法。这将断开您的程序与 SMTP 服务器的连接。
>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')
返回值中的221
表示会话结束。
要查看连接和登录服务器、发送电子邮件和断开连接的所有步骤,请参见第 420 页的发送电子邮件。
IMAP
正如 SMTP 是发送电子邮件的协议一样,互联网消息访问协议(IMAP) 规定了如何与电子邮件运营商的服务器通信,以检索发送到您的电子邮件地址的电子邮件。Python 自带了一个imaplib
模块,但实际上第三方的imapclient
模块更容易使用。本章介绍如何使用 IMAPClient 完整文档在imapclient.readthedocs.io
。
模块从 IMAP 服务器下载格式相当复杂的电子邮件。最有可能的是,您希望将它们从这种格式转换成简单的字符串值。pyzmail
模块为您完成解析这些电子邮件消息的艰巨工作。你可以在www.magiksys.net/pyzmail
找到 PyzMail 的完整文档。
在 Windows 上使用pip install --user -U imapclient==2.1.0
和pip install --user -U pyzmail36==
1.0.4
从终端窗口安装imapclient
和pyzmail
(或者在 MacOS 和 Linux 上使用pip3
)。附录 A 有如何安装第三方模块的步骤。
使用 IMAP 检索和删除电子邮件
在 Python 中查找和检索电子邮件是一个多步骤的过程,既需要imapclient
又需要pyzmail
第三方模块。为了给你一个概述,这里有一个完整的例子,登录到 IMAP 服务器,搜索电子邮件,获取它们,然后从中提取电子邮件的文本。
>>> import imapclient
>>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True)
>>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
'my_email_address@example.com Jane Doe authenticated (Success)'
>>> imapObj.select_folder('INBOX', readonly=True)
>>> UIDs = imapObj.search(['SINCE 05-Jul-2019'])
>>> UIDs
[40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041]
>>> rawMessages = imapObj.fetch([40041], ['BODY[]', 'FLAGS'])
>>> import pyzmail
>>> message = pyzmail.PyzMessage.factory(rawMessages[40041][b'BODY[]'])
>>> message.get_subject()
'Hello!'
>>> message.get_addresses('from')
[('Edward Snowden', 'esnowden@nsa.gov')]
>>> message.get_addresses('to')
[('Jane Doe', 'jdoe@example.com')]
>>> message.get_addresses('cc')
[]
>>> message.get_addresses('bcc')
[]
>>> message.text_part != None
True
>>> message.text_part.get_payload().decode(message.text_part.charset)
'Follow the money.\\r\\n\\r\\n-Ed\\r\\n'
>>> message.html_part != None
True
>>> message.html_part.get_payload().decode(message.html_part.charset)
'<div dir="ltr"><div>So long, and thanks for all the fish!<br><br></div>-
Al<br></div>\\r\\n'
>>> imapObj.logout()
你不必记住这些步骤。在我们详细介绍了每个步骤之后,您可以回到这个概述来刷新您的记忆。
连接到 IMAP 服务器
就像您需要一个SMTP
对象来连接到 SMTP 服务器并发送电子邮件一样,您需要一个IMAPClient
对象来连接到 IMAP 服务器并接收电子邮件。首先,你需要你的电子邮件运营商的 IMAP 服务器的域名。这将不同于 SMTP 服务器的域名。表 18-2 列出了几家流行的电子邮件运营商的 IMAP 服务器。
表 18-2: 电子邮件运营商及其 IMAP 服务器
运营商 IMAP 服务器域名 Gmail⭐ http://imap.gmail.com
Outlook/Hotmail⭐ imap-mail.outlook.com
YahooMail⭐ imap.mail.yahoo.com
ATT imap.mail.att.net
Comcast imap.comcast.net
Verizon incoming.verizon.net
⭐额外的安全措施阻止 Python 使用imapclient
模块登录这些服务器。
一旦有了 IMAP 服务器的域名,调用imapclient.IMAPClient()
函数创建一个IMAPClient
对象。大多数电子邮件运营商要求 SSL 加密,所以传递ssl=True
关键字参数。在交互式 Shell 中输入以下内容(使用您的运营商的域名):
>>> import imapclient
>>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True)
在接下来的所有交互式 Shell 示例中,imapObj
变量包含一个从imapclient.IMAPClient()
函数返回的IMAPClient
对象。在这个上下文中,客户端是连接到服务器的对象。
登录 IMAP 服务器
一旦有了一个IMAPClient
对象,调用它的login()
方法,以字符串的形式传入用户名(这通常是你的电子邮件地址)和密码。
>>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
'my_email_address@example.com Jane Doe authenticated (Success)'
警告
切记不要将密码直接写入您的代码中!相反,设计您的程序来接受从input()
返回的密码。
如果 IMAP 服务器拒绝这个用户名/密码组合,Python 就会引发一个imaplib.error
异常。
搜索邮件
一旦你登录,实际上检索你感兴趣的电子邮件是一个两步的过程。首先,您必须选择一个要搜索的文件夹。然后,您必须调用IMAPClient
对象的search()
方法,传入一串 IMAP 搜索关键字。
选择文件夹
几乎每个账户默认都有一个INBOX
文件夹,但是你也可以通过调用IMAPClient
对象的list_folders()
方法来获得文件夹列表。这将返回一个元组列表。每个元组包含关于单个文件夹的信息。通过输入以下内容继续交互式 Shell 示例:
>>> import pprint
>>> pprint.pprint(imapObj.list_folders())
[(('\\\\HasNoChildren',), '/', 'Drafts'),
(('\\\\HasNoChildren',), '/', 'Filler'),
(('\\\\HasNoChildren',), '/', 'INBOX'),
(('\\\\HasNoChildren',), '/', 'Sent'),
--snip--
(('\\\\HasNoChildren', '\\\\Flagged'), '/', 'Starred'),
(('\\\\HasNoChildren', '\\\\Trash'), '/', 'Trash')]
每个元组中的三个值(例如,(('\\\\HasNoChildren',), '/', 'INBOX')
)如下所示:
- 文件夹标志的元组。(这些标志具体代表什么超出了本书的范围,您可以放心地忽略这个字段。)
- 名称字符串中用于分隔父文件夹和子文件夹的分隔符。
- 文件夹的全名。
要选择要搜索的文件夹,将文件夹的名称作为字符串传递给IMAPClient
对象的select_folder()
方法。
>>> imapObj.select_folder('INBOX', readonly=TruePython 自动化指南(繁琐工作自动化)第二版:二流程控制
Python 自动化指南(繁琐工作自动化)第二版:五字典和结构化数据
Python 自动化指南(繁琐工作自动化)第二版:十八发送电子邮件和短信
Python 自动化指南(繁琐工作自动化)第二版:七使用正则表达式的模式匹配