Python-网络编程学习手册(二)

Python 网络编程学习手册(二)

原文:zh.annas-archive.org/md5/b9ea58a6220e445a9f19c9c78aff8a58

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:API 的实际应用

当我们谈论 Python 中的 API 时,通常指的是模块向我们呈现的类和函数,以便与之交互。在本章中,我们将谈论一些不同的东西,即 Web API。

Web API 是一种通过 HTTP 协议与之交互的 API 类型。如今,许多 Web 服务提供一组 HTTP 调用,旨在由客户端以编程方式使用,也就是说,它们是为机器而不是人类设计的。通过这些接口,可以自动化与服务的交互,并执行诸如提取数据、以某种方式配置服务以及将自己的内容上传到服务中等任务。

在本章中,我们将看到:

  • Web API 使用的两种流行数据交换格式:XML 和 JSON

  • 如何与两个主要 Web API 进行交互:Amazon S3 和 Twitter

  • 在 API 不可用时如何从 HTML 页面中提取数据

  • 如何为提供这些 API 和网站的网络管理员简化工作

有数百种提供 Web API 的服务。这些服务的相当全面且不断增长的列表可以在www.programmableweb.com找到。

我们将首先介绍 Python 中如何使用 XML,然后解释一种基于 XML 的 API,称为 Amazon S3 API。

开始使用 XML

可扩展标记语言XML)是一种以标准文本格式表示分层数据的方式。在使用基于 XML 的 Web API 时,我们将创建 XML 文档,并将其作为 HTTP 请求的主体发送,并接收 XML 文档作为响应的主体。

以下是 XML 文档的文本表示,也许代表奶酪店的库存:

<?xml version='1.0'?>
<inventory>
    <cheese id="c01">
        <name>Caerphilly</name>
        <stock>0</stock>
    </cheese>
    <cheese id="c02">
        <name>Illchester</name>
        <stock>0</stock>
    </cheese>
</inventory>

如果您以前使用过 HTML 编码,那么这可能看起来很熟悉。 XML 是一种基于标记的格式。它来自与 HTML 相同语言系列。数据以元素形式的层次结构进行组织。每个元素由两个标签表示,例如开始标签<name>和匹配的结束标签,例如</name>。在这两个标签之间,我们可以放置数据,例如Caerphilly,或者添加更多标签,代表子元素。

与 HTML 不同,XML 被设计成我们可以定义自己的标签并创建自己的数据格式。此外,与 HTML 不同,XML 语法始终严格执行。在 HTML 中,小错误(例如标签以错误顺序关闭,完全缺少关闭标签或属性值缺少引号)是可以容忍的,但在 XML 中,这些错误将导致完全无法阅读的 XML 文档。格式正确的 XML 文档称为格式良好的。

XML API

处理 XML 数据有两种主要方法:

  • 读取整个文档并创建基于对象的表示,然后使用面向对象的 API 进行操作。

  • 从头到尾处理文档,并在遇到特定标签时执行操作

现在,我们将专注于使用名为ElementTree的 Python XML API 的基于对象的方法。第二种所谓的拉或事件驱动方法(也经常称为SAX,因为 SAX 是这一类别中最流行的 API 之一)设置更加复杂,并且仅在处理大型 XML 文件时才需要。我们不需要这个来处理 Amazon S3。

ElementTree 的基础知识

我们将使用 Python 标准库中的ElementTree API 实现,该 API 位于xml.etree.ElementTree模块中。

让我们看看如何使用ElementTree创建上述示例 XML 文档。打开 Python 解释器并运行以下命令:

**>>> import xml.etree.ElementTree as ET**
**>>> root = ET.Element('inventory')**
**>>> ET.dump(root)**
**<inventory />**

我们首先创建根元素,也就是文档的最外层元素。我们在这里创建了一个根元素<inventory>,然后将其字符串表示打印到屏幕上。<inventory />表示是<inventory></inventory>的 XML 快捷方式。它用于显示一个空元素,即没有数据和子标签的元素。

我们通过创建一个新的ElementTree.Element对象来创建<inventory>元素。您会注意到我们给“Element()”的参数是创建的标签的名称。

我们的<inventory>元素目前是空的,所以让我们往里面放点东西。这样做:

**>>> cheese = ET.Element('cheese')**
**>>> root.append(cheese)**
**>>> ET.dump(root)**
**<inventory><cheese /></inventory>**

现在,在我们的<inventory>元素中有一个<cheese>元素。当一个元素直接嵌套在另一个元素内时,那么嵌套的元素称为外部元素的子元素,外部元素称为父元素。同样,处于同一级别的元素称为兄弟元素

让我们再添加另一个元素,这次给它一些内容。添加以下命令:

**>>> name = ET.SubElement(cheese, 'name')**
**>>> name.text = 'Caerphilly'**
**>>> ET.dump(root)**
**<inventory><cheese><name>Caerphilly</name></cheese></inventory>**

现在,我们的文档开始成形了。我们在这里做了两件新事情:首先,我们使用了快捷类方法“ElementTree.SubElement()”来创建新的<name>元素,并将其作为<cheese>的子元素一次性插入树中。其次,我们通过将一些文本赋给元素的text属性来为其赋予一些内容。

我们可以使用父元素上的“remove()”方法来删除元素,如下面的命令所示:

**>>> temp = ET.SubElement(root, 'temp')**
**>>> ET.dump(root)**
**<inventory><cheese><name>Caerphilly</name></cheese><temp /></inventory>**
**>>> root.remove(temp)**
**>>> ET.dump(root)**
**<inventory><cheese><name>Caerphilly</name></cheese></inventory>**

漂亮打印

我们能够以更易读的格式生成输出将会很有用,比如在本节开头展示的例子。ElementTree API 没有用于执行此操作的函数,但标准库提供的另一个 XML APIminidom有,并且使用起来很简单。首先,导入minidom

**>>> import xml.dom.minidom as minidom**

其次,使用以下命令打印一些格式良好的 XML:

**>>> print(minidom.parseString(ET.tostring(root)).toprettyxml())**
**<?xml version="1.0" ?>**
**<inventory>**
 **<cheese>**
 **<name>Caerphilly</name>**
 **</cheese>**
**</inventory>**

这些乍一看不是最容易的代码行,所以让我们来分解一下。 minidom库不能直接处理 ElementTree 元素,因此我们使用 ElementTree 的“tostring()”函数来创建我们的 XML 的字符串表示。我们通过使用“minidom.parseString()”将字符串加载到minidom API 中,然后使用“toprettyxml()”方法输出我们格式化的 XML。

这可以封装成一个函数,使其更加方便。在 Python shell 中输入以下命令块:

**>>> def xml_pprint(element):**
**...     s = ET.tostring(element)**
**...     print(minidom.parseString(s).toprettyxml())**

现在,只需执行以下操作进行漂亮的打印:

**>>> xml_pprint(root)**
**<?xml version="1.0" ?>**
**<inventory>**
 **<cheese>**
**...**

元素属性

在本节开头展示的例子中,您可能已经注意到了<cheese>元素的开标签中的内容,“id =c01”。这被称为属性。我们可以使用属性来附加额外的信息到元素上,元素可以拥有的属性数量没有限制。属性始终由属性名称组成,在本例中是id,以及一个值,在本例中是c01。值可以是任何文本,但必须用引号括起来。

现在,按照以下方式为<cheese>元素添加id属性:

**>>> cheese.attrib['id'] = 'c01'**
**>>> xml_pprint(cheese)**
**<?xml version="1.0" ?>**
**<cheese id="c01">**
 **<name>Caerphilly</name>**
**</cheese>**

元素的attrib属性是一个类似字典的对象,保存着元素的属性名称和值。我们可以像操作常规dict一样操作 XML 属性。

到目前为止,您应该能够完全重新创建本节开头展示的示例文档。继续尝试吧。

转换为文本

一旦我们有了满意的 XML 树,通常我们会希望将其转换为字符串以便通过网络发送。我们一直在使用的“ET.dump()”函数不适用于此。 “dump()”函数所做的只是将标签打印到屏幕上。它不会返回我们可以使用的字符串。我们需要使用“ET.tostring()”函数,如下面的命令所示:

**>>> text = ET.tostring(name)**
**>>> print(text)**
**b'<name>Caerphilly</name>'**

请注意它返回一个字节对象。它为我们编码字符串。默认字符集是us-ascii,但最好使用 UTF-8 进行 HTTP 传输,因为它可以编码完整的 Unicode 字符范围,并且得到了 Web 应用的广泛支持。

**>>> text = ET.tostring(name, encoding='utf-8')**

目前,这就是我们需要了解有关创建 XML 文档的所有内容,让我们看看如何将其应用到 Web API。

亚马逊 S3 API

亚马逊 S3 是一个数据存储服务。它支撑了今天许多知名的网络服务。尽管提供了企业级的弹性、性能和功能,但它非常容易上手。它价格合理,并且提供了一个简单的 API 用于自动访问。它是不断增长的亚马逊网络服务AWS)组合中的众多云服务之一。

API 不断变化,通常会被赋予一个版本号,以便我们可以跟踪它们。我们将使用当前版本的 S3 REST API,“2006-03-01”。

您会注意到在 S3 文档和其他地方,S3 Web API 被称为REST APIREST代表表述性状态转移,这是 Roy Fielding 在他的博士论文中最初提出的关于如何使用 HTTP 进行 API 的相当学术的概念。尽管一个 API 应该具有被认为是 RESTful 的属性是非常具体的,但实际上几乎任何基于 HTTP 的 API 现在都被贴上了 RESTful 的标签。S3 API 实际上是最具有 RESTful 特性的高调 API 之一,因为它适当地使用了 HTTP 方法的广泛范围。

注意

如果您想了解更多关于这个主题的信息,Roy Fielding 的博士论文可以在这里找到ics.uci.edu/~fielding/pubs/dissertation,而最初提出这个概念并且是一本很好的读物的书籍之一,RESTful Web ServicesLeonard RichardsonSam Ruby,现在可以从这个页面免费下载restfulwebapis.org/rws.html

注册 AWS

在我们可以访问 S3 之前,我们需要在 AWS 上注册。API 通常要求在允许访问其功能之前进行注册。您可以使用现有的亚马逊账户或在www.amazonaws.com上创建一个新账户。虽然 S3 最终是一个付费服务,但如果您是第一次使用 AWS,那么您将获得一年的免费试用,用于低容量使用。一年的时间足够完成本章的学习!试用提供 5GB 的免费 S3 存储空间。

认证

接下来,我们需要讨论认证,这是在使用许多 Web API 时的一个重要讨论话题。我们使用的大多数 Web API 都会指定一种提供认证凭据的方式,允许向它们发出请求,通常我们发出的每个 HTTP 请求都必须包含认证信息。

API 需要这些信息有以下原因:

  • 确保其他人无法滥用应用程序的访问权限

  • 应用每个应用程序的速率限制

  • 管理访问权限的委托,以便应用程序可以代表服务的其他用户或其他服务进行操作

  • 收集使用统计数据

所有的 AWS 服务都使用 HTTP 请求签名机制进行认证。为了签署一个请求,我们使用加密密钥对 HTTP 请求中的唯一数据进行哈希和签名,然后将签名作为标头添加到请求中。通过在服务器上重新创建签名,AWS 可以确保请求是由我们发送的,并且在传输过程中没有被更改。

AWS 签名生成过程目前处于第 4 版,需要进行详细讨论,因此我们将使用第三方库,即requests-aws4auth。这是一个Requests模块的伴侣库,可以自动处理签名生成。它可以在 PyPi 上获得。因此,请在命令行上使用pip安装它:

**$ pip install requests-aws4auth**
**Downloading/unpacking requests-aws4auth**
**...**

设置 AWS 用户

要使用身份验证,我们需要获取一些凭据。

我们将通过 AWS 控制台进行设置。注册 AWS 后,登录到console.aws.amazon.com控制台。

一旦您登录,您需要执行这里显示的步骤:

  1. 点击右上角的您的名称,然后选择安全凭据

  2. 点击屏幕左侧列表中的用户,然后点击顶部的创建新用户按钮。

  3. 输入用户名,确保已选中为每个用户生成访问密钥,然后点击右下角的创建按钮。

您将看到一个新页面,显示用户已成功创建。点击右下角的下载凭据按钮下载一个 CSV 文件,其中包含此用户的访问 ID访问密钥。这些很重要,因为它们将帮助我们对 S3 API 进行身份验证。请确保将它们安全地存储,因为它们将允许完全访问您的 S3 文件。

然后,点击屏幕底部的关闭,点击将出现的列表中的新用户,然后点击附加策略按钮。将显示一系列策略模板。滚动此列表并选择AmazonS3FullAccess策略,如下图所示:

设置 AWS 用户

最后,当它出现时,点击右下角的附加策略按钮。现在,我们的用户已完全访问 S3 服务。

区域

AWS 在世界各地都有数据中心,因此当我们在 AWS 中激活服务时,我们选择希望其存在的区域。S3 的区域列表在docs.aws.amazon.com/general/latest/gr/rande.html#s3_region上。

最好选择离将使用该服务的用户最近的区域。目前,您将是唯一的用户,所以只需为我们的第一个 S3 测试选择离您最近的区域。

S3 存储桶和对象

S3 使用两个概念来组织我们存储在其中的数据:存储桶和对象。对象相当于文件,即具有名称的数据块,而存储桶相当于目录。存储桶和目录之间唯一的区别是存储桶不能包含其他存储桶。

每个存储桶都有自己的 URL 形式:

http://<bucketname>.s3-<region>.amazonaws.com

在 URL 中,<bucketname>是存储桶的名称,<region>是存储桶所在的 AWS 区域,例如eu-west-1。存储桶名称和区域在创建存储桶时设置。

存储桶名称在所有 S3 用户之间是全局共享的,因此它们必须是唯一的。如果您拥有域名,则该域名的子域名将成为适当的存储桶名称。您还可以使用您的电子邮件地址,将@符号替换为连字符或下划线。

对象在我们首次上传时命名。我们通过将对象名称作为路径添加到存储桶的 URL 末尾来访问对象。例如,如果我们在eu-west-1区域有一个名为mybucket.example.com的存储桶,其中包含名为cheeseshop.txt的对象,那么我们可以通过 URLmybucket.example.com.s3-eu-west-1.amazonaws.com/cheeseshop.txt来访问它。

让我们通过 AWS 控制台创建我们的第一个存储桶。我们可以通过这个网页界面手动执行 API 公开的大多数操作,并且这是检查我们的 API 客户端是否执行所需任务的好方法:

  1. 登录到console.aws.amazon.com控制台。

  2. 转到 S3 服务。您将看到一个页面,提示您创建一个存储桶。

  3. 点击创建存储桶按钮。

  4. 输入存储桶名称,选择一个区域,然后点击创建

  5. 您将被带到存储桶列表,并且您将能够看到您的存储桶。

一个 S3 命令行客户端

好了,准备工作足够了,让我们开始编码。在接下来的 S3 部分中,我们将编写一个小的命令行客户端,这将使我们能够与服务进行交互。我们将创建存储桶,然后上传和下载文件。

首先,我们将设置我们的命令行解释器并初始化身份验证。创建一个名为s3_client.py的文件,并将以下代码块保存在其中:

import sys
import requests
import requests_aws4auth as aws4auth
import xml.etree.ElementTree as ET
import xml.dom.minidom as minidom

access_id = '<ACCESS ID>'
access_key = '<ACCESS KEY>'
region = '<REGION>'
endpoint = 's3-{}.amazonaws.com'.format(region)
auth = aws4auth.AWS4Auth(access_id, access_key, region, 's3')
ns = 'http://s3.amazonaws.com/doc/2006-03-01/'

def xml_pprint(xml_string):
    print(minidom.parseString(xml_string).toprettyxml())

def create_bucket(bucket):
    print('Bucket name: {}'.format(bucket))

if __name__ == '__main__':
    cmd, *args = sys.argv[1:]
    globals()cmd

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您需要用之前下载的凭据 CSV 中的值替换<ACCESS ID><ACCESS KEY>,并用您选择的 AWS 区域替换<REGION>

那么,我们在这里做什么呢?首先,我们设置了我们的端点。端点是一个通用术语,用于访问 API 的 URL。一些 Web API 只有一个端点,一些有多个端点,这取决于 API 的设计方式。我们在这里生成的端点实际上只是我们在使用存储桶时将使用的完整端点的一部分。我们的实际端点是由存储桶名称前缀的端点。

接下来,我们创建我们的auth对象。我们将与Requests一起使用它来为我们的 API 请求添加 AWS 身份验证。

ns变量是一个字符串,我们需要用它来处理来自 S3 API 的 XML。我们将在使用它时讨论这个。

我们已经包含了我们的xml_pprint()函数的修改版本,以帮助调试。目前,create_bucket()函数只是一个占位符。我们将在下一节中了解更多。

最后,我们有命令解释器本身 - 它只是获取脚本在命令行上给出的第一个参数,并尝试运行一个同名的函数,将任何剩余的命令行参数传递给函数。让我们进行一次测试。在命令提示符中输入以下内容:

**$ python3.4 s3_client.py create_bucket mybucket**
**Bucket name: mybucket**

您可以看到脚本从命令行参数中提取create_bucket,因此调用create_bucket()函数,将myBucket作为参数传递。

这个框架使得添加功能来扩展我们客户的能力成为一个简单的过程。让我们从使create_bucket()做一些有用的事情开始。

使用 API 创建一个存储桶

每当我们为 API 编写客户端时,我们的主要参考点是 API 文档。文档告诉我们如何构造执行操作的 HTTP 请求。S3 文档可以在docs.aws.amazon.com/AmazonS3/latest/API/APIRest.html找到。docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html URL 将提供存储桶创建的详细信息。

这份文档告诉我们,要创建一个存储桶,我们需要通过使用 HTTP PUT方法向我们新存储桶的端点发出 HTTP 请求。它还告诉我们,请求正文必须包含一些 XML,其中指定了我们希望创建存储桶的 AWS 区域。

所以,现在我们知道我们的目标是什么,让我们讨论我们的功能。首先,让我们创建 XML。用以下代码块替换create_bucket()的内容:

def create_bucket(bucket):
    XML = ET.Element('CreateBucketConfiguration')
    XML.attrib['xmlns'] = ns
    location = ET.SubElement(XML, 'LocationConstraint')
    location.text = auth.region
    data = ET.tostring(XML, encoding='utf-8')
    xml_pprint(data)

在这里,我们创建一个遵循 S3 文档中给出的格式的 XML 树。如果我们现在运行我们的客户端,那么我们将看到这里显示的 XML:

**$ python3.4 s3_client.py create_bucket mybucket.example.com**
**<?xml version="1.0" ?>**
**<CreateBucketConfiguration >**
 **<LocationConstraint>eu-west-1</LocationConstraint>**
**</CreateBucketConfiguration>**

这与文档中指定的格式相匹配。您可以看到我们使用ns变量来填充xmlns属性。这个属性在整个 S3 XML 中都会出现,预定义ns变量使得更快地处理它。

现在,让我们添加代码来发出请求。将create_bucket()末尾的xml_pprint(data)替换为以下内容:

    url = 'http://{}.{}'.format(bucket, endpoint)
    r = requests.put(url, data=data, auth=auth)
    if r.ok:
        print('Created bucket {} OK'.format(bucket))
    else:
        xml_pprint(r.text)

这里显示的第一行将从我们的存储桶名称和端点生成完整的 URL。第二行将向 S3 API 发出请求。请注意,我们使用requests.put()函数使用 HTTP PUT方法进行此请求,而不是使用requests.get()方法或requests.post()方法。还要注意,我们已经提供了我们的auth对象给调用。这将允许Requests为我们处理所有 S3 身份验证!

如果一切顺利,我们将打印出一条消息。如果一切不如预期,我们将打印出响应正文。S3 将错误消息作为 XML 返回到响应正文中。因此,我们使用我们的xml_pprint()函数来显示它。稍后我们将在处理错误部分讨论处理这些错误。

现在运行客户端,如果一切正常,那么我们将收到确认消息。确保您选择的存储桶尚未创建:

**$ python3.4 s3_client.py create_bucket mybucket.example.com**
**Created bucket mybucket.example.com OK**

当我们在浏览器中刷新 S3 控制台时,我们将看到我们的存储桶已创建。

上传文件

现在我们已经创建了一个存储桶,我们可以上传一些文件。编写一个上传文件的函数类似于创建一个存储桶。我们查看文档以了解如何构建我们的 HTTP 请求,找出应该在命令行收集哪些信息,然后编写函数。

我们需要再次使用 HTTP PUT。我们需要存储文件的存储桶名称以及我们希望文件在 S3 中存储的名称。请求的正文将包含文件数据。在命令行中,我们将收集存储桶名称,我们希望文件在 S3 服务中存储的名称以及要上传的本地文件的名称。

create_bucket()函数之后将以下函数添加到您的s3_client.py文件中:

def upload_file(bucket, s3_name, local_path):
    data = open(local_path, 'rb').read()
    url = 'http://{}.{}/{}'.format(bucket, endpoint, s3_name)
    r = requests.put(url, data=data, auth=auth)
if r.ok:
        print('Uploaded {} OK'.format(local_path))
    else:
        xml_pprint(r.text)

在创建此函数时,我们遵循了与创建存储桶类似的模式:

  1. 准备要放入请求正文中的数据。

  2. 构建我们的 URL。

  3. 发出请求。

  4. 检查结果。

请注意,我们以二进制模式打开本地文件。文件可以包含任何类型的数据,因此我们不希望应用文本转换。我们可以从任何地方获取这些数据,例如数据库或另一个 Web API。在这里,我们只是简单地使用本地文件。

URL 与我们在create_bucket()中构建的端点相同,并且 S3 对象名称附加到 URL 路径。稍后,我们可以使用此 URL 检索对象。

现在,运行这里显示的命令来上传一个文件:

**$ python3.4 s3_client.py mybucket.example.com test.jpg ~/test.jpg**
**Uploaded ~/test.jpg OK**

您需要将mybucket.example.com替换为您自己的存储桶名称。一旦文件上传完成,您将在 S3 控制台中看到它。

我使用了一个存储在我的主目录中的 JPEG 图像作为源文件。您可以使用任何文件,只需将最后一个参数更改为适当的路径。但是,使用 JPEG 图像将使您更容易重现以下部分。

通过 Web 浏览器检索已上传的文件

默认情况下,S3 对存储桶和对象应用限制权限。创建它们的帐户具有完全的读写权限,但对于其他人完全拒绝访问。这意味着我们刚刚上传的文件只有在下载请求包括我们帐户的身份验证时才能下载。如果我们在浏览器中尝试结果 URL,那么我们将收到访问被拒绝的错误。如果我们试图使用 S3 与其他人共享文件,这并不是很有用。

解决此问题的方法是使用 S3 的一种机制来更改权限。让我们看看使我们上传的文件公开的简单任务。将upload_file()更改为以下内容:

def upload_file(bucket, s3_name, local_path, acl='private'):
    data = open(local_path, 'rb').read()
    url = 'http://{}.{}/{}'.format(bucket, endpoint, s3_name)
    headers = {'x-amz-acl': acl}
    r = requests.put(url, data=data, headers=headers, auth=auth)
if r.ok:
        print('Uploaded {} OK'.format(local_path))
    else:
        xml_pprint(r.text)

我们现在在我们的 HTTP 请求中包含了一个头部,x-amz-acl,它指定了要应用于对象的权限集。我们还在函数签名中添加了一个新的参数,这样我们就可以在命令行上指定权限集。我们使用了 S3 提供的所谓的预设 ACLs预设 访问控制列表),并在docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl中进行了记录。

我们感兴趣的 ACL 称为public-read。这将允许任何人下载文件而无需任何形式的身份验证。现在,我们可以重新运行我们的上传,但这次会将这个 ACL 应用到它上面:

**$ python3.4 s3_client.py mybucket.example.com test.jpg ~/test.jpg public-read**
**Uploaded test.jpg OK**

现在,在浏览器中访问文件的 S3 URL 将给我们下载文件的选项。

在 Web 浏览器中显示上传的文件

如果你上传了一张图片,那么你可能会想知道为什么浏览器要求我们保存它而不是直接显示它。原因是我们没有设置文件的Content-Type

如果你还记得上一章,HTTP 响应中的Content-Type头部告诉客户端,这里是我们的浏览器,正文中的文件类型。默认情况下,S3 应用binary/octet-stream的内容类型。由于这个Content-Type,浏览器无法知道它正在下载一个图像,所以它只是将它呈现为一个可以保存的文件。我们可以通过在上传请求中提供Content-Type头部来解决这个问题。S3 将存储我们指定的类型,并在随后的下载响应中使用它作为Content-Type

s3_client.py的开头添加以下代码块到导入中:

import mimetypes

然后将upload_file()更改为以下内容:

def upload_file(bucket, s3_name, local_path, acl='private'):
    data = open(local_path, 'rb').read()
    url = 'http://{}.{}/{}'.format(bucket, endpoint, s3_name)
    headers = {'x-amz-acl': acl}
    mimetype = mimetypes.guess_type(local_path)[0]
    if mimetype:
        headers['Content-Type'] = mimetype
    r = requests.put(url, data=data, headers=headers, auth=auth)
if r.ok:
        print('Uploaded {} OK'.format(local_path))
    else:
        xml_pprint(r.text)

在这里,我们使用了mimetypes模块来猜测一个适合的Content-Type,通过查看local_path的文件扩展名。如果mimetypes无法从local_path确定Content-Type,那么我们就不包括Content-Type头部,让 S3 应用默认的binary/octet-stream类型。

不幸的是,在 S3 中,我们无法通过简单的PUT请求覆盖现有对象的元数据。可以通过使用PUT复制请求来实现,但这超出了本章的范围。现在,最好的方法是在上传文件之前使用 AWS 控制台从 S3 中删除文件。我们只需要做一次。现在,我们的代码将自动为我们上传的任何新文件添加Content-Type

一旦你删除了文件,就像上一节所示重新运行客户端,也就是说,用新的Content-Type上传文件并尝试在浏览器中再次下载文件。如果一切顺利,那么图像将被显示。

使用 API 下载文件

通过 S3 API 下载文件与上传文件类似。我们只需要再次提供存储桶名称、S3 对象名称和本地文件名,但是发出一个GET请求而不是PUT请求,然后将接收到的数据写入磁盘。

在你的程序中添加以下函数,放在upload_file()函数下面:

def download_file(bucket, s3_name, local_path):
    url = 'http://{}.{}/{}'.format(bucket, endpoint, s3_name)
    r = requests.get(url, auth=auth)
    if r.ok:
        open(local_path, 'wb').write(r.content)
        print('Downloaded {} OK'.format(s3_name))
    else:
        xml_pprint(r.text)

现在,运行客户端并下载一个文件,你之前上传的文件,使用以下命令:

**$ python3.4 s3_client.py download_file mybucket.example.com test.jpg ~/test_downloaded.jpg**
**Downloaded test.jpg OK**

解析 XML 和处理错误

如果在运行上述代码时遇到任何错误,那么你会注意到清晰的错误消息不会被显示。S3 将错误消息嵌入到响应体中返回的 XML 中,直到现在我们只是将原始 XML 转储到屏幕上。我们可以改进这一点,并从 XML 中提取文本。首先,让我们生成一个错误消息,这样我们就可以看到 XML 的样子。在s3_client.py中,将你的访问密钥替换为空字符串,如下所示:

access_secret = ''

现在,尝试在服务上执行以下操作:

**$ python3.4 s3_client.py create_bucket failbucket.example.com**
**<?xml version="1.0" ?>**
**<Error>**
 **<Code>SignatureDoesNotMatch</Code>**
 **<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>**
 **<AWSAccessKeyId>AKIAJY5II3SZNHZ25SUA</AWSAccessKeyId>**
 **<StringToSign>AWS4-HMAC-SHA256...</StringToSign>**
 **<SignatureProvided>e43e2130...</SignatureProvided>**
 **<StringToSignBytes>41 57 53 34...</StringToSignBytes>**
 **<CanonicalRequest>PUT...</CanonicalRequest>**
 **<CanonicalRequestBytes>50 55 54...</CanonicalRequestBytes>**
 **<RequestId>86F25A39912FC628</RequestId>**
 **<HostId>kYIZnLclzIW6CmsGA....</HostId>**
**</Error>**

前面的 XML 是 S3 错误信息。我已经截断了几个字段以便在这里显示。你的代码块会比这个稍微长一点。在这种情况下,它告诉我们它无法验证我们的请求,这是因为我们设置了一个空的访问密钥。

解析 XML

打印所有的 XML 对于错误消息来说太多了。有很多无用的额外信息对我们来说没有用。最好的办法是只提取错误消息的有用部分并显示出来。

嗯,ElementTree为我们从 XML 中提取这样的信息提供了一些强大的工具。我们将回到 XML 一段时间,来探索这些工具。

首先,我们需要打开一个交互式的 Python shell,然后使用以下命令再次生成上述错误消息:

**>>> import requests**
**>>> import requests_aws4auth**
**>>> auth = requests_aws4auth.AWS4Auth('<ID>', '', 'eu-west-1', '')**
**>>> r = requests.get('http://s3.eu-west-1.amazonaws.com', auth=auth)**

你需要用你的 AWS 访问 ID 替换<ID>。打印出r.text以确保你得到一个错误消息,类似于我们之前生成的那个。

现在,我们可以探索我们的 XML。将 XML 文本转换为ElementTree树。一个方便的函数是:

**>>> import xml.etree.ElementTree as ET**
**>>> root = ET.fromstring(r.text)**

现在我们有了一个 ElementTree 实例,root作为根元素。

查找元素

通过使用元素作为迭代器来浏览树的最简单方法。尝试做以下事情:

**>>> for element in root:**
**...     print('Tag: ' + element.tag)**
**Tag: Code**
**Tag: Message**
**Tag: AWSAccessKeyId**
**Tag: StringToSign**
**Tag: SignatureProvided**
**...**

迭代root会返回它的每个子元素,然后我们通过使用tag属性打印出元素的标签。

我们可以使用以下命令对我们迭代的标签应用过滤器:

**>>> for element in root.findall('Message'):**
**...     print(element.tag + ': ' + element.text)**
**Message: The request signature we calculated does not match the signature you provided. Check your key and signing method.**

在这里,我们使用了root元素的findall()方法。这个方法将为我们提供与指定标签匹配的root元素的所有直接子元素的列表,在这种情况下是<Message>

这将解决我们只提取错误消息文本的问题。现在,让我们更新我们的错误处理。

处理错误

我们可以回去并将这添加到我们的s3_client.py文件中,但让我们在输出中包含更多信息,并结构化代码以允许重用。将以下函数添加到download_file()函数下面的文件中:

def handle_error(response):
    output = 'Status code: {}\n'.format(response.status_code)
    root = ET.fromstring(response.text)
    code =  root.find('Code').text
    output += 'Error code: {}\n'.format(code)
    message = root.find('Message').text
    output += 'Message: {}\n'.format(message)
    print(output)

你会注意到我们在这里使用了一个新的函数,即root.find()。这与findall()的工作方式相同,只是它只返回第一个匹配的元素,而不是所有匹配的元素列表。

然后,用handle_error(r)替换文件中每个xml_pprint(r.text)的实例,然后再次使用错误的访问密钥运行客户端。现在,你会看到一个更详细的错误消息:

**$ python3.4 s3_client.py create_bucket failbucket.example.com**
**Status code: 403**
**Error code: SignatureDoesNotMatch**
**Message: The request signature we calculated does not match the signature you provided. Check your key and signing method.**

进一步的增强

这就是我们要为客户提供的服务。我们编写了一个命令行程序,可以在 Amazon S3 服务上执行创建存储桶、上传和下载对象等基本操作。还有很多操作可以实现,这些可以在 S3 文档中找到;例如列出存储桶内容、删除对象和复制对象等操作。

我们可以改进一些其他东西,特别是如果我们要将其制作成一个生产应用程序。命令行解析机制虽然紧凑,但从安全角度来看并不令人满意,因为任何有权访问命令行的人都可以运行任何内置的 python 命令。最好是有一个函数白名单,并使用标准库模块之一,如argparse来实现一个适当的命令行解析器。

将访问 ID 和访问密钥存储在源代码中也是安全问题。由于密码存储在源代码中,然后上传到云代码仓库,发生了几起严重的安全事件。最好在运行时从外部来源加载密钥,比如文件或数据库。

Boto 包

我们已经讨论了直接使用 S3 REST API,并且这给了我们一些有用的技术,让我们能够在将来编写类似 API 时进行编程。在许多情况下,这将是我们与 Web API 交互的唯一方式。

然而,一些 API,包括 AWS,有现成的包可以暴露服务的功能,而无需处理 HTTP API 的复杂性。这些包通常使代码更清晰、更简单,如果可用的话,应该优先用于生产工作。

AWS 包被称为Boto。我们将快速浏览一下Boto包,看看它如何提供我们之前编写的一些功能。

boto包在 PyPi 中可用,所以我们可以用pip安装它:

**$ pip install boto**
**Downloading/unpacking boto**
**...**

现在,启动一个 Python shell,让我们试一试。我们需要先连接到服务:

**>>> import boto**
**>>> conn = boto.connect_s3('<ACCESS ID>', '<ACCESS SECRET>')**

您需要用您的访问 ID 和访问密钥替换<ACCESS ID><ACCESS SECRET>。现在,让我们创建一个存储桶:

**>>> conn.create_bucket('mybucket.example.com')**

这将在默认的标准美国地区创建存储桶。我们可以提供不同的地区,如下所示:

**>>> from boto.s3.connection import Location**
**>>> conn.create_bucket('mybucket.example.com', location=Location.EU)**

我们需要使用不同的区域名称来执行此功能,这些名称与我们之前创建存储桶时使用的名称不同。要查看可接受的区域名称列表,请执行以下操作:

**>>> [x for x in dir(Location) if x.isalnum()]**
**['APNortheast', 'APSoutheast', 'APSoutheast2', 'CNNorth1', 'DEFAULT', 'EU', 'SAEast', 'USWest', 'USWest2']**

执行以下操作以显示我们拥有的存储桶列表:

**>>> buckets = conn.get_all_buckets()**
**>>> [b.name for b in buckets]**
**['mybucket.example.com', 'mybucket2.example.com']**

我们还可以列出存储桶的内容。为此,首先我们需要获取对它的引用:

**>>> bucket = conn.get_bucket('mybucket.example.com')**

然后列出内容:

**>>> [k.name for k in bucket.list()]**
**['cheesehop.txt', 'parrot.txt']**

上传文件是一个简单的过程。首先,我们需要获取要放入的存储桶的引用,然后我们需要创建一个Key对象,它将代表我们在存储桶中的对象:

**>>> bucket = conn.get_bucket('mybucket.example.com')**
**>>> from boto.s3.key import Key**
**>>> key = Key(bucket)**

接下来,我们需要设置Key名称,然后上传我们的文件数据:

**>>> key.key = 'lumberjack_song.txt'**
**>>> key.set_contents_from_filename('~/lumberjack_song.txt')**

boto包在上传文件时会自动设置Content-Type,它使用了我们之前用于确定类型的mimetypes模块。

下载也遵循类似的模式。尝试以下命令:

**>>> bucket = conn.get_bucket('mybucket.example.com')**
**>>> key = bucket.get_key('parrot.txt')**
**>>> key.get_contents_to_filename('~/parrot.txt')**

这将下载mybucket.example.com存储桶中的parrot.txt S3 对象,然后将其存储在~/parrot.txt本地文件中。

一旦我们有了对Key的引用,只需使用以下内容来设置 ACL:

**>>> key.set_acl('public-read')**

我将让您通过教程进一步探索boto包的功能,该教程可以在boto.readthedocs.org/en/latest/s3_tut.html找到。

显然,对于 Python 中的日常 S3 工作,boto应该是您的首选包。

结束 S3

因此,我们已经讨论了 Amazon S3 API 的一些用途,并学到了一些关于在 Python 中使用 XML 的知识。这些技能应该让您在使用任何基于 XML 的 REST API 时有一个良好的开端,无论它是否有像boto这样的预构建库。

然而,XML 并不是 Web API 使用的唯一数据格式,S3 处理 HTTP 的方式也不是 Web API 使用的唯一模型。因此,我们将继续并看一看今天使用的另一种主要数据格式,JSON 和另一个 API:Twitter。

JSON

JavaScript 对象表示法(JSON)是一种用文本字符串表示简单对象(如列表字典)的标准方式。尽管最初是为 JavaScript 开发的,但 JSON 是与语言无关的,大多数语言都可以使用它。它轻巧,但足够灵活,可以处理广泛的数据范围。这使得它非常适合在 HTTP 上传数据,许多 Web API 使用它作为其主要数据格式。

编码和解码

我们使用json模块来处理 Python 中的 JSON。通过以下命令,让我们创建一个 Python 列表的 JSON 表示:

**>>> import json**
**>>> l = ['a', 'b', 'c']**
**>>> json.dumps(l)**
**'["a", "b", "c"]'**

我们使用json.dumps()函数将对象转换为 JSON 字符串。在这种情况下,我们可以看到 JSON 字符串似乎与 Python 对列表的表示相同,但请注意这是一个字符串。通过以下操作确认:

**>>> s = json.dumps(['a', 'b', 'c'])**
**>>> type(s)**
**<class 'str'>**
**>>> s[0]**
**'['**

将 JSON 转换为 Python 对象也很简单,如下所示:

**>>> s = '["a", "b", "c"]'**
**>>> l = json.loads(s)**
**>>> l**
**['a', 'b', 'c']**
**>>> l[0]**
**'a'**

我们使用json.loads()函数,只需传递一个 JSON 字符串。正如我们将看到的,这在与 Web API 交互时非常强大。通常,我们将收到一个 JSON 字符串作为 HTTP 响应的主体,只需使用json.loads()进行解码,即可提供可立即使用的 Python 对象。

使用 JSON 的字典

JSON 本身支持映射类型对象,相当于 Python 的dict。这意味着我们可以直接通过 JSON 使用dicts

**>>> json.dumps({'A':'Arthur', 'B':'Brian', 'C':'Colonel'})**
**'{"A": "Arthur", "C": "Colonel", "B": "Brian"}'**

此外,了解 JSON 如何处理嵌套对象也是有用的。

**>>> d = {**
**...     'Chapman': ['King Arthur', 'Brian'],**
**...     'Cleese': ['Sir Lancelot', 'The Black Knight'],**
**...     'Idle': ['Sir Robin', 'Loretta'],**
**... }**
**>>> json.dumps(d)**
**'{"Chapman": ["King Arthur", "Brian"], "Idle": ["Sir Robin", "Loretta"], "Cleese": ["Sir Lancelot", "The Black Knight"]}'**

不过有一个需要注意的地方:JSON 字典键只能是字符串形式。

**>>> json.dumps({1:10, 2:20, 3:30})**
**'{"1": 10, "2": 20, "3": 30}'**

注意,JSON 字典中的键如何成为整数的字符串表示?要解码使用数字键的 JSON 字典,如果我们想将它们作为数字处理,我们需要手动进行类型转换。执行以下操作来实现这一点:

**>>> j = json.dumps({1:10, 2:20, 3:30})**
**>>> d_raw = json.loads(j)**
**>>> d_raw**
**{'1': 10, '2': 20, '3': 30}**
**>>> {int(key):val for key,val in d_raw.items()}**
**{1: 10, 2: 20, 3: 30}**

我们只需使用字典推导将int()应用于字典的键。

其他对象类型

JSON 只能干净地处理 Python 的listsdicts,对于其他对象类型,json可能会尝试将对象类型转换为其中一个,或者完全失败。尝试一个元组,如下所示:

**>>> json.dumps(('a', 'b', 'c'))**
**'["a", "b", "c"]'**

JSON 没有元组数据类型,因此json模块将其转换为list。如果我们将其转换回:

**>>> j = json.dumps(('a', 'b', 'c'))**
**>>> json.loads(j)**
**['a', 'b', 'c']**

它仍然是一个listjson模块不支持sets,因此它们也需要重新转换为lists。尝试以下命令:

**>>> s = set(['a', 'b', 'c'])**
**>>> json.dumps(s)**
**...**
**TypeError: {'a', 'c', 'b'} is not JSON serializable**
**>>> json.dumps(list(s))**
**'["a", "b", "c"]'**

这将导致类似于元组引起的问题。如果我们将 JSON 转换回 Python 对象,那么它将是一个list而不是set

我们几乎从不遇到需要这些专门的 Python 对象的 Web API,如果我们确实遇到,那么 API 应该提供一些处理它的约定。但是,如果我们将数据存储在除listsdicts之外的任何格式中,我们需要跟踪我们需要应用于传出或传入对象的任何转换。

现在我们对 JSON 有了一定的了解,让我们看看它在 Web API 中是如何工作的。

Twitter API

Twitter API 提供了访问我们可能希望 Twitter 客户端执行的所有功能。使用 Twitter API,我们可以创建搜索最新推文、查找趋势、查找用户详细信息、关注用户时间线,甚至代表用户发布推文和直接消息的客户端。

我们将查看 Twitter API 版本 1.1,这是撰写本章时的当前版本。

注意

Twitter 为其 API 提供了全面的文档,可以在dev.twitter.com/overview/documentation找到。

一个 Twitter 世界时钟

为了说明 Twitter API 的一些功能,我们将编写一个简单的 Twitter 世界时钟的代码。我们的应用程序将定期轮询其 Twitter 账户,寻找包含可识别城市名称的提及,如果找到,则会回复推文并显示该城市的当前当地时间。在 Twitter 中,提及是指包含我们账户名前缀@的任何推文,例如@myaccount

Twitter 的身份验证

与 S3 类似,我们需要确定在开始之前如何管理身份验证。我们需要注册,然后了解 Twitter 希望我们如何对请求进行身份验证。

为 Twitter API 注册您的应用程序

我们需要创建一个 Twitter 账户,注册我们的应用程序,并且我们将收到我们应用程序的身份验证凭据。另外,建立一个第二个账户也是一个好主意,我们可以用它来向应用程序账户发送测试推文。这提供了一种更干净的方式来检查应用程序是否正常工作,而不是让应用程序账户向自己发送推文。您可以创建的 Twitter 账户数量没有限制。

要创建帐户,请转到www.twitter.com并完成注册过程。一旦您拥有 Twitter 帐户,执行以下操作注册您的应用程序:

  1. 使用您的主要 Twitter 帐户登录apps.twitter.com,然后创建一个新应用程序。

  2. 填写新应用程序表格,注意 Twitter 应用程序名称需要在全球范围内是唯一的。

  3. 转到应用程序设置,然后更改应用程序权限以具有读写访问权限。您可能需要注册您的手机号码以启用此功能。即使您不愿意提供这个信息,我们也可以创建完整的应用程序;但是,最终发送回复推文的最终功能将不会激活。

现在我们需要获取我们的访问凭证,如下所示:

  1. 转到Keys and Access Tokens部分,然后记下Consumer KeyAccess Secret

  2. 生成一个访问令牌

  3. 记下访问令牌访问密钥

认证请求

我们现在有足够的信息来进行请求认证。Twitter 使用一个称为oAuth的认证标准,版本 1.0a。详细描述在oauth.net/core/1.0a/

oAuth 认证标准有点棘手,但幸运的是,Requests模块有一个名为requests-oauthlib的伴侣库,它可以为我们处理大部分复杂性。这在 PyPi 上可用,因此我们可以使用pip下载和安装它。

**$ pip install requests-oauthlib**
**Downloading/unpacking requests-oauthlib**
**...**

现在,我们可以为我们的请求添加认证,然后编写我们的应用程序。

一个 Twitter 客户端

将此处提到的代码保存到文件中,并将其保存为twitter_worldclock.py。您需要用从上述 Twitter 应用程序配置中获取的值替换<CONSUMER_KEY><CONSUMER_SECRET><ACCESS_TOKEN><ACCESS_SECRET>

import requests, requests_oauthlib, sys

consumer_key = '<CONSUMER_KEY>'
consumer_secret = '<CONSUMER_SECRET>'
access_token = '<ACCESS_TOKEN>'
access_secret = '<ACCESS_KEY>'

def init_auth():
    auth_obj = requests_oauthlib.OAuth1(
                    consumer_key, consumer_secret,
                    access_token, access_secret)

    if verify_credentials(auth_obj):
        print('Validated credentials OK')
        return auth_obj
    else:
        print('Credentials validation failed')
        sys.exit(1)	

def verify_credentials(auth_obj):
    url = 'https://api.twitter.com/1.1/' \
          'account/verify_credentials.json'
    response = requests.get(url, auth=auth_obj)
    return response.status_code == 200

if __name__ == '__main__':
    auth_obj = init_auth()

请记住,consumer_secretaccess_secret充当您的 Twitter 帐户的密码,因此在生产应用程序中,它们应该从安全的外部位置加载,而不是硬编码到源代码中。

在上述代码中,我们通过使用我们的访问凭证在init_auth()函数中创建OAuth1认证实例auth_obj。每当我们需要发出 HTTP 请求时,我们将其传递给Requests,通过它Requests处理认证。您可以在verify_credentials()函数中看到这个例子。

verify_credentials()函数中,我们测试 Twitter 是否识别我们的凭据。我们在这里使用的 URL 是 Twitter 专门用于测试我们的凭据是否有效的终点。如果它们有效,则返回 HTTP 200 状态代码,否则返回 401 状态代码。

现在,让我们运行twitter_worldclock.py,如果我们已经注册了我们的应用程序并正确填写了令牌和密钥,那么我们应该会看到验证凭据 OK。现在认证已经工作,我们程序的基本流程将如下图所示:

Twitter 客户端

我们的程序将作为守护程序运行,定期轮询 Twitter,查看是否有任何新的推文需要我们处理和回复。当我们轮询提及时间线时,我们将下载自上次轮询以来接收到的任何新推文,以便我们可以处理所有这些推文而无需再次轮询。

轮询推文

让我们添加一个函数来检查并从我们的提及时间线中检索新推文。在我们添加循环之前,我们将使其工作。在verify_credentials()下面添加新函数,然后在主部分中添加对此函数的调用;同时,在文件开头的导入列表中添加json

def get_mentions(since_id, auth_obj):
    params = {'count': 200, 'since_id': since_id,
              'include_rts':  0, 'include_entities': 'false'}
    url = 'https://api.twitter.com/1.1/' \
          'statuses/mentions_timeline.json'
    response = requests.get(url, params=params, auth=auth_obj)
    response.raise_for_status()
    return json.loads(response.text)

if __name__ == '__main__':
    auth_obj = init_auth()
    since_id = 1
    for tweet in get_mentions(since_id, auth_obj):
        print(tweet['text'])

使用get_mentions(),我们通过连接到statuses/mentions_timeline.json端点来检查并下载提及我们应用账户的任何推文。我们提供了一些参数,Requests将其作为查询字符串传递。这些参数由 Twitter 指定,它们控制推文将如何返回给我们。它们如下:

  • 'count':这指定将返回的最大推文数。Twitter 将允许通过单个请求接收 200 条推文。

  • 'include_entities':这用于从检索到的推文中删除一些多余的信息。

  • 'include_rts':这告诉 Twitter 不要包括任何转发。如果有人转发我们的回复,我们不希望用户收到另一个时间更新。

  • 'since_id':这告诉 Twitter 只返回 ID 大于此值的推文。每条推文都有一个唯一的 64 位整数 ID,后来的推文比先前的推文具有更高的值 ID。通过记住我们处理的最后一条推文的 ID,然后将其作为此参数传递,Twitter 将过滤掉我们已经看过的推文。

在运行上述操作之前,我们希望为我们的账户生成一些提及,这样我们就有东西可以下载。登录您的 Twitter 测试账户,然后创建一些包含@username的推文,其中您将username替换为您的应用账户用户名。之后,当您进入应用账户的通知选项卡的提及部分时,您将看到这些推文。

现在,如果我们运行上述代码,我们将在屏幕上打印出我们提及的文本。

处理推文

下一步是解析我们的提及,然后生成我们想要包含在回复中的时间。解析是一个简单的过程。在这里,我们只需检查推文的“text”值,但生成时间需要更多的工作。实际上,为此,我们需要一个城市及其时区的数据库。这在pytz包中可用,在 PyPi 上可以找到。为此,请安装以下包:

**$ pip install pytz**
**Downloading/unpacking pytz**
**...**

然后,我们可以编写我们的推文处理函数。将此函数添加到get_mentions()下方,然后在文件开头的导入列表中添加datetimepytz

def process_tweet(tweet):
    username = tweet['user']['screen_name']
    text = tweet['text']
    words = [x for x in text.split() if
                        x[0] not in ['@', '#']]
    place = ' '.join(words)
    check = place.replace(' ', '_').lower()
    found = False
    for tz in pytz.common_timezones:
        tz_low = tz.lower()
        if check in tz_low.split('/'):
            found = True
            break
    if found:
        timezone = pytz.timezone(tz)
        time = datetime.datetime.now(timezone).strftime('%H:%M')
        reply = '@{} The time in {} is currently {}'.format(username, place, time)
    else:
        reply = "@{} Sorry, I didn't recognize " \
                        "'{}' as a city".format(username, place)
    print(reply)

if __name__ == '__main__':
    auth_obj = init_auth()
    since_id = 1
    for tweet in get_mentions(since_id, auth_obj):
        process_tweet(tweet)

process_tweet()的大部分内容用于格式化推文文本和处理时区数据。首先,我们将从推文中删除任何@username提及和#hashtags。然后,我们准备剩下的推文文本与时区名称数据库进行比较。时区名称数据库存储在pytz.common_timezones中,但名称中还包含地区,用斜杠(/)与名称分隔。此外,在这些名称中,下划线用于代替空格。

我们通过扫描数据库来检查格式化的推文文本。如果找到匹配项,我们将构建一个包含匹配时区的当地时间的回复。为此,我们使用datetime模块以及由pytz生成的时区对象。如果在时区数据库中找不到匹配项,我们将组成一个回复,让用户知道这一点。然后,我们将我们的回复打印到屏幕上,以检查它是否按预期工作。

同样,在运行此操作之前,我们可能希望创建一些只包含城市名称并提及我们的世界时钟应用账户的推文,以便函数有东西可以处理。在时区数据库中出现的一些城市包括都柏林、纽约和东京。

试一试!当您运行它时,您将在屏幕上得到一些包含这些城市和这些城市当前当地时间的推文回复文本。

速率限制

如果我们多次运行上述操作,然后我们会发现它在一段时间后会停止工作。要么凭据暂时无法验证,要么get_mentions()中的 HTTP 请求将失败。

这是因为 Twitter 对其 API 应用速率限制,这意味着我们的应用程序只允许在一定时间内对端点进行一定数量的请求。限制在 Twitter 文档中列出,根据认证路线(稍后讨论)和端点的不同而有所不同。我们使用statuses/mentions_timeline.json,因此我们的限制是每 15 分钟 15 次请求。如果我们超过这个限制,那么 Twitter 将以429 Too many requests状态代码做出响应。这将迫使我们等待下一个 15 分钟窗口开始之前,才能让我们获得任何有用的数据。

速率限制是 Web API 的常见特征,因此在使用它们时,有一些有效的测试方法是很有用的。使用速率限制的 API 数据进行测试的一种方法是下载一些数据,然后将其存储在本地。之后,从文件中加载它,而不是从 API 中拉取它。通过使用 Python 解释器下载一些测试数据,如下所示:

**>>> from twitter_worldclock import ***
**>>> auth_obj = init_auth()**
**Credentials validated OK**
**>>> mentions = get_mentions(1, auth_obj)**
**>>> json.dump(mentions, open('test_mentions.json', 'w'))**

当您运行此时,您需要在与twitter_worldclock.py相同的文件夹中。这将创建一个名为test_mentions.json的文件,其中包含我们的 JSON 化提及。在这里,json.dump()函数将提供的数据写入文件,而不是将其作为字符串返回。

我们可以通过修改程序的主要部分来使用这些数据,看起来像下面这样:

if __name__ == '__main__':
    mentions = json.load(open('test_mentions.json'))
    for tweet in mentions:
        process_tweet(tweet)

发送回复

我们需要执行的最后一个函数是对提及进行回复。为此,我们使用statuses/update.json端点。如果您尚未在应用帐户中注册您的手机号码,则这将无法工作。因此,只需将程序保持原样。如果您已经注册了手机号码,则在process_tweets()下添加此功能:

def post_reply(reply_to_id, text, auth_obj):
    params = {
        'status': text,
        'in_reply_to_status_id': reply_to_id}
    url = 'https://api.twitter.com/1.1./statuses/update.json'
    response = requests.post(url, params=params, auth=auth_obj)
    response.raise_for_status()

并在process_tweet()末尾的print()调用下面,与相同的缩进级别:

post_reply(tweet['id'], reply, auth_obj)

现在,如果您运行此程序,然后检查您的测试帐户的 Twitter 通知,您将看到一些回复。

post_reply()函数只是使用以下参数调用端点,通知 Twitter 要发布什么:

  • status:这是我们回复推文的文本。

  • in_reply_to_status_id:这是我们要回复的推文的 ID。我们提供这个信息,以便 Twitter 可以将推文链接为对话。

在测试时,我们可能会收到一些403状态代码响应。这没关系,只是 Twitter 拒绝让我们连续发布两条相同文本的推文,这可能会发生在这个设置中,具体取决于我们发送了什么测试推文。

最后的修饰

建筑模块已经就位,我们可以添加主循环使程序成为守护进程。在顶部导入time模块,然后将主要部分更改为以下内容:

if __name__ == '__main__':
    auth_obj = init_auth()
    since_id = 1
    error_count = 0
    while error_count < 15:
        try:
            for tweet in get_mentions(since_id, auth_obj):
                process_tweet(tweet)
                since_id = max(since_id, tweet['id'])
            error_count =  0
        except requests.exceptions.HTTPError as e:
            print('Error: {}'.format(str(e)))
            error_count += 1
        time.sleep(60)

这将每 60 秒调用get_mentions(),然后处理已下载的任何新推文。如果出现任何 HTTP 错误,它将在退出程序之前重试 15 次。

现在,如果我们运行程序,它将持续运行,回复提及世界时钟应用帐户的推文。试一试,运行程序,然后从您的测试帐户发送一些推文。一分钟后,您将看到一些回复您的通知。

进一步进行

现在我们已经编写了一个基本的功能 Twitter API 客户端,肯定有一些可以改进的地方。虽然本章没有空间详细探讨增强功能,但值得提到一些,以便通知您可能想要承担的未来项目。

轮询和 Twitter 流 API

您可能已经注意到一个问题,即我们的客户端每次轮询最多只能拉取 200 条推文。在每次轮询中,Twitter 首先提供最近的推文。这意味着如果我们在 60 秒内收到超过 200 条推文,那么我们将永久丢失最先收到的推文。实际上,使用statuses/mentions_timeline.json端点没有完整的解决方案。

Twitter 针对这个问题的解决方案是提供一种另类的 API,称为流式 API。连接到这些 API 时,HTTP 响应连接实际上是保持打开状态的,并且传入的推文会不断通过它进行流式传输。Requests包提供了处理这种情况的便捷功能。Requests响应对象具有iter_lines()方法,可以无限运行。它能够在服务器发送数据时输出一行数据,然后我们可以对其进行处理。如果您发现您需要这个功能,那么在 Requests 文档中有一个示例可以帮助您入门,可以在docs.python-requests.org/en/latest/user/advanced/#streaming-requests找到。

替代 oAuth 流程

我们的设置是让我们的应用程序针对我们的主账户进行操作,并为发送测试推文使用第二个账户,这有点笨拙,特别是如果您将您的应用账户用于常规推文。有没有更好的办法,专门有一个账户来处理世界时钟的推文?

嗯,是的。理想的设置是在一个主账户上注册应用程序,并且您也可以将其用作常规 Twitter 账户,并且让应用程序处理第二个专用世界时钟账户的推文。

oAuth 使这成为可能,但需要一些额外的步骤才能使其正常工作。我们需要世界时钟账户来授权我们的应用代表其行事。您会注意到之前提到的 oAuth 凭据由两个主要元素组成,消费者访问。消费者元素标识我们的应用程序,访问元素证明了访问凭据来自授权我们的应用代表其行事的账户。在我们的应用程序中,我们通过让应用程序代表注册时的账户,也就是我们的应用账户,来简化完整的账户授权过程。当我们这样做时,Twitter 允许我们直接从dev.twitter.com界面获取访问凭据。要使用不同的用户账户,我们需要插入一个步骤,让用户转到 Twitter,这将在 Web 浏览器中打开,用户需要登录,然后明确授权我们的应用程序。

注意

这个过程在requests-oauthlib文档中有演示,可以在requests-oauthlib.readthedocs.org/en/latest/oauth1_workflow.html找到。

HTML 和屏幕抓取

尽管越来越多的服务通过 API 提供其数据,但当一个服务没有这样做时,以编程方式获取数据的唯一方法是下载其网页,然后解析 HTML 源代码。这种技术称为屏幕抓取

虽然原则上听起来很简单,但屏幕抓取应该被视为最后的手段。与 XML 不同,XML 的语法严格执行,数据结构通常是相对稳定的,有时甚至有文档记录,而网页源代码的世界却是一个混乱的世界。这是一个不断变化的地方,代码可能会意外改变,以一种完全破坏你的脚本并迫使你从头开始重新设计解析逻辑的方式。

尽管如此,有时这是获取基本数据的唯一方法,因此我们将简要讨论开发一种抓取方法。我们将讨论在 HTML 代码发生变化时减少影响的方法。

在抓取之前,您应该始终检查网站的条款和条件。一些网站明确禁止自动解析和检索。违反条款可能导致您的 IP 地址被禁止。然而,在大多数情况下,只要您不重新发布数据并且不进行过于频繁的请求,您应该没问题。

HTML 解析器

我们将解析 HTML 就像我们解析 XML 一样。我们再次可以选择拉取式 API 和面向对象的 API。我们将使用ElementTree,原因与之前提到的相同。

有几个可用的 HTML 解析库。它们的区别在于它们的速度、在 HTML 文档中导航的接口,以及它们处理糟糕构建的 HTML 的能力。Python 标准库不包括面向对象的 HTML 解析器。这方面普遍推荐的第三方包是lxml,它主要是一个 XML 解析器。但是,它确实包含一个非常好的 HTML 解析器。它快速,提供了几种浏览文档的方式,并且对破碎的 HTML 宽容。

lxml库可以通过python-lxml包在 Debian 和 Ubuntu 上安装。如果您需要一个最新版本,或者无法安装系统包,那么可以通过pip安装lxml。请注意,您需要一个构建环境。Debian 通常带有一个已经设置好的环境,但如果缺少,那么以下内容将为 Debian 和 Ubuntu 都安装一个:

**$ sudo apt-get install build-essential**

然后你应该能够像这样安装lxml

**$ sudo STATIC_DEPS=true pip install lxml**

如果您在 64 位系统上遇到编译问题,那么您也可以尝试:

**$ CFLAGS="$CFLAGS -fPIC" STATIC_DEPS=true pip install lxml**

在 Windows 上,可以从lxml网站lxml.de/installation.html获取安装程序包。如果您的 Python 版本没有安装程序,可以在页面上查找第三方安装程序的链接。

如果lxml对您不起作用,下一个最好的库是 BeautifulSoup。BeautifulSoup 是纯 Python,因此可以使用pip安装,并且应该可以在任何地方运行。尽管它有自己的 API,但它是一个备受尊重和有能力的库,实际上它可以使用lxml作为后端库。

给我看数据

在开始解析 HTML 之前,我们需要解析的东西!让我们从 Debian 网站上获取最新稳定版 Debian 发行版的版本和代号。有关当前稳定版发行版的信息可以在www.debian.org/releases/stable/找到。

我们想要的信息显示在页面标题和第一句中:

给我看数据

因此,我们应该提取"jessie"代号和 8.0 版本号。

使用 lxml 解析 HTML

让我们打开一个 Python shell 并开始解析。首先,我们将使用Requests下载页面。

**>>> import requests**
**>>> response = requests.get('https://www.debian.org/releases/stable')**

接下来,我们将源代码解析成ElementTree树。这与使用标准库的ElementTree解析 XML 相同,只是这里我们将使用lxml专家HTMLParser

**>>> from lxml.etree import HTML**
**>>> root = HTML(response.content)**

HTML()函数是一个快捷方式,它读取传递给它的 HTML,然后生成一个 XML 树。请注意,我们传递的是response.content而不是response.textlxml库在使用原始响应而不是解码的 Unicode 文本时会产生更好的结果。

lxml库的ElementTree实现已经被设计为与标准库的 100%兼容,因此我们可以像处理 XML 一样开始探索文档:

**>>> [e.tag for e in root]**
**['head', 'body']**
**>>> root.find('head').find('title').text**
**'Debian –- Debian \u201cjessie\u201d Release Information'**

在上面的代码中,我们已经打印出了文档的<title>元素的文本内容,这是在上面截图的标签中显示的文本。我们已经看到它包含了我们想要的代号。

聚焦

屏幕抓取是一种寻找明确地址 HTML 元素的艺术,这些元素包含我们想要的信息,并且只从这些元素中提取信息。

然而,我们也希望选择标准尽可能简单。我们依赖文档的内容越少,页面的 HTML 发生变化时就越不容易破坏。

让我们检查页面的 HTML 源代码,看看我们正在处理什么。为此,可以在 Web 浏览器中使用查看源代码,或者将 HTML 保存到文件中并在文本编辑器中打开。本书的源代码下载中也包含了页面的源代码。搜索文本Debian 8.0,这样我们就可以直接找到我们想要的信息。对我来说,它看起来像以下代码块:

<body>
...
<div id="content">
<h1>Debian &ldquo;jessie&rdquo; Release Information</h1>
<p>**Debian 8.0** was
released October 18th, 2014.
The release included many major
changes, described in
...

我跳过了<body><div>之间的 HTML,以显示<div><body>元素的直接子元素。从上面可以看出,我们想要<div>元素的<p>标签子元素的内容。

如果我们使用之前使用过的ElementTree函数导航到此元素,那么我们最终会得到类似以下的内容:

**>>> root.find('body').findall('div')[1].find('p').text**
**Debian 8.0 was.**
**...**

但这并不是最佳方法,因为它相当大程度上依赖于 HTML 结构。例如,插入一个我们需要的<div>标签之前的变化会破坏它。此外,在更复杂的文档中,这可能导致可怕的方法调用链,难以维护。我们在上一节中使用<title>标签来获取代号的方法是一个很好的技巧的例子,因为文档中始终只有一个<head>和一个<title>标签。找到我们的<div>的更好方法是利用它包含的id="content"属性。将页面分成几个顶级<div>,如页眉、页脚和内容,并为<div>赋予标识它们的id属性,是一种常见的网页设计模式。

因此,如果我们可以搜索具有id属性为"content"<div>,那么我们将有一种干净的方法来选择正确的<div>。文档中只有一个匹配的<div>,并且不太可能会添加另一个类似的<div>到文档中。这种方法不依赖于文档结构,因此不会受到对结构所做的任何更改的影响。我们仍然需要依赖于<div>中的<p>标签是出现的第一个<p>标签,但鉴于没有其他方法来识别它,这是我们能做的最好的。

那么,我们如何运行这样的搜索来找到我们的内容<div>呢?

使用 XPath 搜索

为了避免穷举迭代和检查每个元素,我们需要使用XPath,它比我们迄今为止使用的更强大。它是一种专门为 XML 开发的查询语言,并且得到了lxml的支持。此外,标准库实现对其提供了有限的支持。

我们将快速了解 XPath,并在此过程中找到之前提出的问题的答案。

要开始使用 Python shell,可以执行以下操作:

**>>> root.xpath('body')**
**[<Element body at 0x39e0908>]**

这是 XPath 表达式的最简单形式:它搜索当前元素的子元素,其标签名称与指定的标签名称匹配。当前元素是我们在其上调用xpath()的元素,在本例中是rootroot元素是 HTML 文档中的顶级<html>元素,因此返回的元素是<body>元素。

XPath 表达式可以包含多个级别的元素。搜索从进行xpath()调用的节点开始,并随着它们在表达式中匹配连续元素而向下工作。我们可以利用这一点来仅查找<body><div>子元素。

**>>> root.xpath('body/div')**
**[<Element div at 0x39e06c8>, <Element div at 0x39e05c8>, <Element div at 0x39e0608>]**

body/div表达式意味着匹配当前元素的<body>子元素的<div>子元素。在 XML 文档中,具有相同标签的元素可以在同一级别出现多次,因此 XPath 表达式可以匹配多个元素,因此xpath()函数始终返回一个列表。

前面的查询是相对于我们称之为xpath()的元素的,但我们可以通过在表达式开头添加斜杠来强制从树的根部进行搜索。我们还可以通过双斜杠来对元素的所有后代进行搜索。要做到这一点,请尝试以下操作:

**>>> root.xpath('//h1')**
**[<Element h1 at 0x2ac3b08>]**

在这里,我们只通过指定单个标记就直接找到了我们的<h1>元素,即使它在root下面几个级别。表达式开头的双斜杠将始终从根目录搜索,但如果我们希望从上下文元素开始搜索,可以在前面加上一个点。

**>>> root.find('head').xpath('.//h1')**
**[]**

这将找不到任何内容,因为<head>没有<h1>的后代。

XPath 条件

因此,通过提供路径,我们可以非常具体,但 XPath 的真正力量在于对路径中的元素应用附加条件。特别是,我们前面提到的问题,即测试元素属性。

**>>> root.xpath('//div[@id="content"]')**
**[<Element div at 0x39e05c8>]**

div后面的方括号[@id="content"]形成了我们放在匹配的<div>元素上的条件。id之前的@符号表示id是一个属性,因此条件的含义是:只有id属性等于"content"的元素。这就是我们如何找到我们的内容<div>

在我们使用它来提取信息之前,让我们简单介绍一下我们可以使用条件做的一些有用的事情。我们可以只指定一个标记名称,如下所示:

**>>> root.xpath('//div[h1]')**
**[<Element div at 0x39e05c8>]**

这将返回所有具有<h1>子元素的<div>元素。也可以尝试:

**>>> root.xpath('body/div[2]'):**
**[<Element div at 0x39e05c8>]**

将数字作为条件将返回匹配列表中的该位置的元素。在这种情况下,这是<body>的第二个<div>子元素。请注意,这些索引从1开始,而不像 Python 索引从0开始。

XPath 还有很多功能,完整的规范是万维网联盟W3C)的标准。最新版本可以在www.w3.org/TR/xpath-3/上找到。

汇总

现在我们已经将 XPath 添加到我们的超能力中,让我们通过编写一个脚本来获取我们的 Debian 版本信息来完成。创建一个新文件get_debian_version.py,并将以下内容保存到其中:

import re
import requests
from lxml.etree import HTML

response = requests.get('http://www.debian.org/releases/stable/')
root = HTML(response.content)
title_text = root.find('head').find('title').text
release = re.search('\u201c(.*)\u201d', title_text).group(1)
p_text = root.xpath('//div[@id="content"]/p[1]')[0].text
version = p_text.split()[1]

print('Codename: {}\nVersion: {}'.format(release, version))

在这里,我们通过 XPath 下载和解析了网页,通过 XPath 提取我们想要的文本。我们使用了正则表达式来提取jessie,并使用split来提取版本 8.0。最后我们将其打印出来。

因此,像这里显示的那样运行它:

**$ python3.4 get_debian_version.py**
**Codename: jessie**
**Version: 8.0**

了不起。至少非常巧妙。有一些第三方包可用于加快抓取和表单提交的速度,其中两个流行的包是 Mechanize 和 Scrapy。请在wwwsearch.sourceforge.net/mechanize/scrapy.org上查看它们。

伟大的力量……

作为 HTTP 客户端开发人员,您可能有不同的优先级,与运行网站的网络管理员不同。网络管理员通常会为人类用户提供网站;可能提供旨在产生收入的服务,并且很可能所有这些都需要在非常有限的资源的帮助下完成。他们将对分析人类如何使用他们的网站感兴趣,并且可能有他们希望自动客户端不要探索的网站区域。

自动解析和下载网站页面的 HTTP 客户端被称为各种各样的东西,比如机器人网络爬虫蜘蛛。机器人有许多合法的用途。所有的搜索引擎提供商都大量使用机器人来爬取网页并构建他们庞大的页面索引。机器人可以用来检查死链接,并为存储库存档网站,比如 Wayback Machine。但是,也有许多可能被认为是非法的用途。自动遍历信息服务以提取其页面上的数据,然后在未经网站所有者许可的情况下重新打包这些数据以在其他地方展示,一次性下载大批量的媒体文件,而服务的精神是在线查看等等,这些都可能被认为是非法的。一些网站有明确禁止自动下载的服务条款。尽管一些行为,比如复制和重新发布受版权保护的材料,显然是非法的,但其他一些行为则需要解释。这个灰色地带是一个持续辩论的话题,而且不太可能会得到所有人的满意解决。

然而,即使它们确实有合法的目的,总的来说,机器人确实使网站所有者的生活变得更加困难。它们污染了 Web 服务器日志,而网站所有者用这些日志来计算他们的人类受众如何使用他们的网站的统计数据。机器人还会消耗带宽和其他服务器资源。

使用本章中我们正在研究的方法,编写一个执行许多前述功能的机器人是非常简单的。网站所有者为我们提供了我们将要使用的服务,因此,作为回报,我们应该尊重上述领域,并设计我们的机器人,使它们对他们的影响尽可能小。

选择用户代理

我们可以做一些事情来帮助我们的网站所有者。我们应该为我们的客户端选择一个合适的用户代理。网站所有者从日志文件中过滤出机器人流量的主要方法是通过用户代理分析。

有已知机器人的用户代理列表,例如,可以在www.useragentstring.com/pages/Crawlerlist/找到这样的列表。

网站所有者可以在他们的过滤器中使用这些。许多网站所有者也会简单地过滤掉包含botspidercrawler等词的用户代理。因此,如果我们编写的是一个自动化机器人而不是一个浏览器,那么如果我们使用包含这些词中的一个的用户代理,那么这将使网站所有者的生活变得更加轻松。搜索引擎提供商使用的许多机器人都遵循这个惯例,这里列举了一些例子:

  • Mozilla/5.0 compatible; bingbot/2.0; http://www.bing.com/bingbot.htm

  • Baiduspider: http://www.baidu.com/search/spider.htm

  • Mozilla/5.0 compatible; Googlebot/2.1; http://www.google.com/bot.html

在 HTTP RFC 7231 的第 5.5.3 节中也有一些指南。

Robots.txt 文件

有一个非官方但标准的机制,可以告诉机器人网站的哪些部分不应该被爬取。这个机制称为robots.txt,它采用一个名为robots.txt的文本文件的形式。这个文件总是位于网站的根目录,以便机器人总是可以找到它。它包含描述网站可访问部分的规则。文件格式在www.robotstxt.org中有描述。

Python 标准库提供了urllib.robotparser模块,用于解析和处理robots.txt文件。您可以创建一个解析器对象,将robots.txt文件传递给它,然后可以简单地查询它,以查看给定用户代理是否允许给定 URL。在标准库的文档中可以找到一个很好的例子。如果您在访问之前检查客户端可能想要访问的每个 URL,并遵守网站所有者的意愿,那么您将会帮助他们。

最后,由于我们可能会频繁地进行请求来测试我们新建的客户端,最好是在本地复制你想让客户端解析和测试的网页或文件。这样,我们既可以为自己节省带宽,也可以为网站节省带宽。

总结

在本章中,我们涵盖了很多内容,但现在你应该能够开始真正利用你遇到的 Web API 了。

我们研究了 XML,如何构建文档,解析它们并通过使用ElementTree API 从中提取数据。我们研究了 Python 的ElementTree实现和lxml。我们还研究了 XPath 查询语言如何有效地从文档中提取信息。

我们研究了 Amazon S3 服务,并编写了一个客户端,让我们可以执行基本操作,比如创建存储桶,通过 S3 REST API 上传和下载文件。我们学习了如何设置访问权限和内容类型,使文件在 Web 浏览器中正常工作。

我们讨论了 JSON 数据格式,如何将 Python 对象转换为 JSON 数据格式,以及如何将它们转换回 Python 对象。

然后,我们探索了 Twitter API,并编写了一个按需的世界时钟服务,通过这个服务,我们学会了如何阅读和处理账户的推文,以及如何发送推文作为回复。

我们看到了如何从网页的 HTML 源代码中提取信息。我们学习了在使用ElementTreelxml HTML 解析器时如何处理 HTML。我们还学习了如何使用 XPath 来帮助使这个过程更加高效。

最后,我们研究了如何回报给为我们提供所有数据的网站管理员。我们讨论了一些编写客户端的方式,使网站管理员的生活变得更轻松,并尊重他们希望我们如何使用他们的网站。

所以,暂时就介绍这么多关于 HTTP 了。我们将在第九章中重新讨论 HTTP,Web 应用程序,届时我们将学习如何使用 Python 构建 Web 应用程序的服务器端。在下一章中,我们将讨论互联网的另一个重要工具:电子邮件。

第四章: 与电子邮件互动

电子邮件是数字通信最流行的方式之一。Python 有丰富的内置库用于处理电子邮件。在本章中,我们将学习如何使用 Python 来撰写、发送和检索电子邮件。本章将涵盖以下主题:

  • 通过smtplib库使用 SMTP 发送电子邮件

  • 使用 TLS 保护电子邮件传输

  • 使用poplib通过 POP3 检索电子邮件

  • 使用imapclient通过 IMAP 检索电子邮件

  • 使用 IMAP 在服务器上操作电子邮件

  • 使用logging模块发送电子邮件

电子邮件术语

在我们开始使用 Python 撰写第一封电子邮件之前,让我们重新审视一些电子邮件的基本概念。通常,最终用户使用软件或图形用户界面(GUI)来撰写、发送和接收电子邮件。这种软件称为电子邮件客户端,例如 Mozilla Thunderbird、Microsoft Outlook 等都是电子邮件客户端。同样的任务也可以通过 Web 界面完成,即 Web 邮件客户端界面。一些常见的例子包括:Gmail、Yahoo 邮件、Hotmail 等。

您从客户端界面发送的邮件不会直接到达接收者的计算机。您的邮件会经过多个专用电子邮件服务器。这些服务器运行一个名为邮件传输代理MTA)的软件,其主要工作是通过分析邮件头等内容将电子邮件路由到适当的目的地。

还有许多其他事情发生在路上,然后邮件到达收件人的本地电子邮件网关。然后,收件人可以使用他或她的电子邮件客户端检索电子邮件。

上述过程涉及一些协议。其中最常见的已列在这里:

  • 简单邮件传输协议SMTP):MTA 使用 SMTP 协议将您的电子邮件传递到收件人的电子邮件服务器。SMTP 协议只能用于从一个主机发送电子邮件到另一个主机。

  • 邮局协议 3POP3):POP3 协议为用户提供了一种简单和标准化的方式,以便访问邮箱,然后将邮件下载到他们的计算机上。使用 POP3 协议时,您的电子邮件消息将从互联网服务提供商(ISP)的邮件服务器下载到本地计算机。您还可以将电子邮件的副本留在 ISP 服务器上。

  • 互联网消息访问协议IMAP):IMAP 协议还提供了一种简单和标准化的方式,用于从 ISP 的本地服务器访问您的电子邮件。IMAP 是一种客户端/服务器协议,其中电子邮件由 ISP 接收并保存。由于这只需要进行少量数据传输,即使在较慢的连接(如手机网络)上,这种方案也能很好地工作。只有当您发送请求读取特定的电子邮件时,该电子邮件消息才会从 ISP 下载。您还可以做一些其他有趣的事情,比如在服务器上创建和操作文件夹或邮箱、删除消息等。

Python 有三个模块,smtplibpoplibimaplib,分别支持 SMTP、POP3 和 IMAP 协议。每个模块都有选项,可以使用传输层安全TLS)协议安全地传输信息。每个协议还使用某种形式的身份验证来确保数据的保密性。

使用 SMTP 发送电子邮件

我们可以使用smtplibe-mail包从 Python 脚本发送电子邮件。smtplib模块提供了一个 SMTP 对象,用于使用 SMTP 或扩展 SMTPESMTP)协议发送邮件。e-mail模块帮助我们构造电子邮件消息,并使用各种标题信息和附件。该模块符合tools.ietf.org/html/rfc2822.html中描述的Internet Message FormatIMF)。

撰写电子邮件消息

让我们使用email模块中的类构造电子邮件消息。email.mime模块提供了从头开始创建电子邮件和 MIME 对象的类。MIME多用途互联网邮件扩展的缩写。这是原始互联网电子邮件协议的扩展。这被广泛用于交换不同类型的数据文件,如音频、视频、图像、应用程序等。

许多类都是从 MIME 基类派生的。我们将使用一个 SMTP 客户端脚本,使用email.mime.multipart.MIMEMultipart()类作为示例。它接受通过关键字字典传递电子邮件头信息。让我们看看如何使用MIMEMultipart()对象指定电子邮件头。多部分 mime 指的是在单个电子邮件中发送 HTML 和 TEXT 部分。当电子邮件客户端接收多部分消息时,如果可以呈现 HTML,它将接受 HTML 版本,否则它将呈现纯文本版本,如下面的代码块所示:

    from email.mime.multipart import MIMEMultipart()
    msg = MIMEMultipart()
    msg['To'] = recipient
    msg['From'] = sender
    msg['Subject'] = 'Email subject..'

现在,将纯文本消息附加到此多部分消息对象。我们可以使用MIMEText()对象来包装纯文本消息。这个类的构造函数接受额外的参数。例如,我们可以将textplain作为它的参数。可以使用set_payload()方法设置此消息的数据,如下所示:

    part = MIMEText('text', 'plain')
    message = 'Email message ….'
    part.set_payload(message)

现在,我们将将纯文本消息附加到多部分消息中,如下所示:

    msg.attach(part)

该消息已准备好通过一个或多个 SMTP MTA 服务器路由到目标邮件服务器。但是,显然,脚本只与特定的 MTA 通信,而该 MTA 处理消息的路由。

发送电子邮件消息

smtplib模块为我们提供了一个 SMTP 类,可以通过 SMTP 服务器套接字进行初始化。成功初始化后,这将为我们提供一个 SMTP 会话对象。SMTP 客户端将与服务器建立适当的 SMTP 会话。这可以通过为 SMTPsession对象使用ehlo()方法来完成。实际的消息发送将通过将sendmail()方法应用于 SMTP 会话来完成。因此,典型的 SMTP 会话将如下所示:

    session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    session.ehlo()
    session.sendmail(sender, recipient, msg.as_string())
    session.quit()

在我们的示例 SMTP 客户端脚本中,我们使用了谷歌的免费 Gmail 服务。如果您有 Gmail 帐户,那么您可以通过 SMTP 从 Python 脚本发送电子邮件到该帐户。您的电子邮件可能会被最初阻止,因为 Gmail 可能会检测到它是从不太安全的电子邮件客户端发送的。您可以更改 Gmail 帐户设置,并启用您的帐户以从不太安全的电子邮件客户端发送/接收电子邮件。您可以在 Google 网站上了解有关从应用程序发送电子邮件的更多信息,网址为support.google.com/a/answer/176600?hl=en

如果您没有 Gmail 帐户,则可以在典型的 Linux 框中使用本地 SMTP 服务器设置并运行此脚本。以下代码显示了如何通过公共 SMTP 服务器发送电子邮件:

#!/usr/bin/env python3
# Listing 1 – First email client
import smtplib

from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

SMTP_SERVER = 'aspmx.l.google.com'
SMTP_PORT = 25

def send_email(sender, recipient):
    """ Send email message """
    msg = MIMEMultipart()
    msg['To'] = recipient
    msg['From'] = sender
    subject = input('Enter your email subject: ')
    msg['Subject'] = subject
    message = input('Enter your email message. Press Enter when finished. ')
    part = MIMEText('text', "plain")
    part.set_payload(message)
    msg.attach(part)
    # create smtp session
    session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    session.ehlo()
    #session.set_debuglevel(1)
    # send mail
    session.sendmail(sender, recipient, msg.as_string())
    print("You email is sent to {0}.".format(recipient))
    session.quit()

if __name__ == '__main__':
    sender = input("Enter sender email address: ")
    recipient = input("Enter recipient email address: ")
    send_email(sender, recipient)

如果您运行此脚本,则可以看到输出与此处提到的类似。出于匿名性考虑,在以下示例中未显示真实的电子邮件地址:

**$ python3 smtp_mail_sender.py** 
**Enter sender email address: <SENDER>@gmail.com** 
**Enter recipeint email address: <RECEIVER>@gmail.com**
**Enter your email subject: Test mail**
**Enter your email message. Press Enter when finished. This message can be ignored**
**You email is sent to <RECEIVER>@gmail.com.**

这个脚本将使用 Python 的标准库模块smtplib发送一个非常简单的电子邮件消息。为了构成消息,从email.mime子模块导入了MIMEMultipartMIMEText类。这个子模块有各种类型的类,用于以不同类型的附件组成电子邮件消息,例如MIMEApplication()MIMEAudio()MIMEImage()等。

在这个例子中,send_mail()函数被调用了两个参数:发件人和收件人。这两个参数都是电子邮件地址。电子邮件消息是由MIMEMultipart()消息类构造的。这个类命名空间中添加了ToFromSubject等基本标头。消息的正文是由MIMEText()类的实例组成的。这是通过set_payload()方法完成的。然后,这个有效载荷通过attach()方法附加到主消息上。

为了与 SMTP 服务器通信,将通过实例化smtplib模块的SMTP()类创建与服务器的会话。服务器名称和端口参数将传递给构造函数。根据 SMTP 协议,客户端将通过ehlo()方法向服务器发送扩展的问候消息。消息将通过sendmail()方法发送。

请注意,如果在 SMTP 会话对象上调用set_debuglevel()方法,它将产生额外的调试消息。在前面的例子中,这行被注释掉了。取消注释该行将产生类似以下的调试消息:

**$ python3 smtp_mail_sender.py** 
**Enter sender email address: <SENDER>@gmail.com**
**Enter recipeint email address: <RECEIVER>@gmail.com**
**Enter your** 
**email subject: Test email**
**Enter your email message. Press Enter when finished. This is a test email**
**send: 'mail FROM:<SENDER@gmail.com> size=339\r\n'**
**reply: b'250 2.1.0 OK hg2si4622244wib.38 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.1.0 OK hg2si4622244wib.38 - gsmtp'**
**send: 'rcpt TO:<RECEIVER@gmail.com>\r\n'**
**reply: b'250 2.1.5 OK hg2si4622244wib.38 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.1.5 OK hg2si4622244wib.38 - gsmtp'**
**send: 'data\r\n'**
**reply: b'354  Go ahead hg2si4622244wib.38 - gsmtp\r\n'**
**reply: retcode (354); Msg: b'Go ahead hg2si4622244wib.38 - gsmtp'**
**data: (354, b'Go ahead hg2si4622244wib.38 - gsmtp')**
**send: 'Content-Type: multipart/mixed; 
boundary="===============1431208306=="\r\nMIME-Version: 1.0\r\nTo: RECEIVER@gmail.com\r\nFrom: SENDER@gmail.com\r\nSubject: Test  email\r\n\r\n--===============1431208306==\r\nContent-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent- Transfer-Encoding: 7bit\r\n\r\nThis is a test email\r\n-- ===============1431208306==--\r\n.\r\n'**
**reply: b'250 2.0.0 OK 1414233177 hg2si4622244wib.38 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.0.0 OK 1414233177 hg2si4622244wib.38 - gsmtp'**
**data: (250, b'2.0.0 OK 1414233177 hg2si4622244wib.38 - gsmtp')**
**You email is sent to RECEIVER@gmail.com.**
**send: 'quit\r\n'**
**reply: b'221 2.0.0 closing connection hg2si4622244wib.38 - gsmtp\r\n'**
**reply: retcode (221); Msg: b'2.0.0 closing connection hg2si4622244wib.38 - gsmtp'**

这很有趣,因为消息是通过逐步方式通过公共 SMTP 服务器发送的。

使用 TLS 安全地发送电子邮件

TLS 协议是 SSL 或安全套接字层的后继者。这确保了客户端和服务器之间的通信是安全的。这是通过以加密格式发送消息来实现的,以便未经授权的人无法看到消息。使用smtplib使用 TLS 并不困难。创建 SMTP 会话对象后,需要调用starttls()方法。在发送电子邮件之前,需要使用 SMTP 服务器凭据登录到服务器。

这是第二个电子邮件客户端的示例:

#!/usr/bin/env python3
# Listing 2
import getpass
import smtplib

from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587 # ssl port 465, tls port 587

def send_email(sender, recipient):
    """ Send email message """
    msg = MIMEMultipart()
    msg['To'] = recipient
    msg['From'] = sender
    msg['Subject'] = input('Enter your email subject: ')
    message = input('Enter your email message. Press Enter when finished. ')
    part = MIMEText('text', "plain")
    part.set_payload(message)
    msg.attach(part)
    # create smtp session
    session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    session.set_debuglevel(1)
    session.ehlo()
    session.starttls()
    session.ehlo
    password = getpass.getpass(prompt="Enter you email password: ") 
    # login to server
    session.login(sender, password)
    # send mail
    session.sendmail(sender, recipient, msg.as_string())
    print("You email is sent to {0}.".format(recipient))
    session.quit()

if __name__ == '__main__':
    sender = input("Enter sender email address: ")
    recipient = input("Enter recipeint email address: ")
    send_email(sender, recipient)

前面的代码与我们的第一个例子类似,只是对服务器进行了身份验证。在这种情况下,SMTP 用户会被服务器验证。如果我们在打开 SMTP 调试后运行脚本,那么我们将看到类似以下的输出:

**$ python3 smtp_mail_sender_tls.py** 
**Enter sender email address: SENDER@gmail.com**
**Enter recipeint email address: RECEPIENT@gmail.com**
**Enter your email subject: Test email**
**Enter your email message. Press Enter when finished. This is a test email that can be ignored.**

用户输入后,将开始与服务器的通信。它将通过ehlo()方法开始。作为对这个命令的响应,SMTP 服务器将发送几行带有返回代码250的响应。这个响应将包括服务器支持的特性。

这些响应的摘要将表明服务器准备好与客户端继续,如下所示:

**send: 'ehlo debian6box.localdomain.loc\r\n'**
**reply: b'250-mx.google.com at your service, [77.233.155.107]\r\n'**
**reply: b'250-SIZE 35882577\r\n'**
**reply: b'250-8BITMIME\r\n'**
**reply: b'250-STARTTLS\r\n'**
**reply: b'250-ENHANCEDSTATUSCODES\r\n'**
**reply: b'250-PIPELINING\r\n'**
**reply: b'250-CHUNKING\r\n'**
**reply: b'250 SMTPUTF8\r\n'**
**reply: retcode (250); Msg: b'mx.google.com at your service, [77.233.155.107]\nSIZE 35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\ nCHUNKING\nSMTPUTF8'**

在初始命令之后,客户端将使用starttls()方法将连接升级到 TLS,如下所示:

**send: 'STARTTLS\r\n'**
**reply: b'220 2.0.0 Ready to start TLS\r\n'**
**reply: retcode (220); Msg: b'2.0.0 Ready to start TLS'**
**Enter you email password:** 
**send: 'ehlo debian6box.localdomain.loc\r\n'**
**reply: b'250-mx.google.com at your service, [77.233.155.107]\r\n'**
**reply: b'250-SIZE 35882577\r\n'**
**reply: b'250-8BITMIME\r\n'**
**reply: b'250-AUTH LOGIN PLAIN XOAUTH XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER\r\n'**
**reply: b'250-ENHANCEDSTATUSCODES\r\n'**
**reply: b'250-PIPELINING\r\n'**
**reply: b'250-CHUNKING\r\n'**
**reply: b'250 SMTPUTF8\r\n'**
**reply: retcode (250); Msg: b'mx.google.com at your service, [77.233.155.107]\nSIZE 35882577\n8BITMIME\nAUTH LOGIN PLAIN XOAUTH XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'**

在认证阶段,客户端脚本通过login()方法发送认证数据。请注意,认证令牌是一个 base-64 编码的字符串,用户名和密码之间用空字节分隔。还有其他支持的身份验证协议适用于复杂的客户端。以下是认证令牌的示例:

**send: 'AUTH PLAIN A...dvXXDDCCD.......sscdsvsdvsfd...12344555\r\n'**
**reply: b'235 2.7.0 Accepted\r\n'**
**reply: retcode (235); Msg: b'2.7.0 Accepted'**

客户端经过认证后,可以使用sendmail()方法发送电子邮件消息。这个方法传递了三个参数,发件人、收件人和消息。示例输出如下:

**send: 'mail FROM:<SENDER@gmail.com> size=360\r\n'**
**reply: b'250 2.1.0 OK xw9sm8487512wjc.24 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.1.0 OK xw9sm8487512wjc.24 - gsmtp'**
**send: 'rcpt TO:<RECEPIENT@gmail.com>\r\n'**
**reply: b'250 2.1.5 OK xw9sm8487512wjc.24 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.1.5 OK xw9sm8487512wjc.24 - gsmtp'**
**send: 'data\r\n'**
**reply: b'354  Go ahead xw9sm8487512wjc.24 - gsmtp\r\n'**
**reply: retcode (354); Msg: b'Go ahead xw9sm8487512wjc.24 - gsmtp'**
**data: (354, b'Go ahead xw9sm8487512wjc.24 - gsmtp')**
**send: 'Content-Type: multipart/mixed; boundary="===============1501937935=="\r\nMIME-Version: 1.0\r\n**
**To: <Output omitted>-===============1501937935==--\r\n.\r\n'**
**reply: b'250 2.0.0 OK 1414235750 xw9sm8487512wjc.24 - gsmtp\r\n'**
**reply: retcode (250); Msg: b'2.0.0 OK 1414235750 xw9sm8487512wjc.24 - gsmtp'**
**data: (250, b'2.0.0 OK 1414235750 xw9sm8487512wjc.24 - gsmtp')**
**You email is sent to RECEPIENT@gmail.com.**
**send: 'quit\r\n'**
**reply: b'221 2.0.0 closing connection xw9sm8487512wjc.24 - gsmtp\r\n'**
**reply: retcode (221); Msg: b'2.0.0 closing connection xw9sm8487512wjc.24 - gsmtp'**

使用 poplib 通过 POP3 检索电子邮件

存储的电子邮件消息可以通过本地计算机下载和阅读。 POP3 协议可用于从电子邮件服务器下载消息。 Python 有一个名为poplib的模块,可以用于此目的。 此模块提供了两个高级类,POP()POP3_SSL(),它们分别实现了与 POP3/POP3S 服务器通信的 POP3 和 POP3S 协议。 它接受三个参数,主机、端口和超时。 如果省略端口,则可以使用默认端口(110)。 可选的超时参数确定服务器上的连接超时长度(以秒为单位)。

POP3()的安全版本是其子类POP3_SSL()。 它接受附加参数,例如 keyfile 和 certfile,用于提供 SSL 证书文件,即私钥和证书链文件。

编写 POP3 客户端也非常简单。 要做到这一点,通过初始化POP3()POP3_SSL()类来实例化一个邮箱对象。 然后,通过以下命令调用user()pass_()方法登录到服务器:

  mailbox = poplib.POP3_SSL(<POP3_SERVER>, <SERVER_PORT>) 
  mailbox.user('username')
       mailbox.pass_('password')

现在,您可以调用各种方法来操作您的帐户和消息。 这里列出了一些有趣的方法:

  • stat(): 此方法根据两个整数的元组返回邮箱状态,即消息计数和邮箱大小。

  • list(): 此方法发送一个请求以获取消息列表,这在本节后面的示例中已经演示过。

  • retr(): 此方法给出一个参数消息编号,表示要检索的消息。 它还标记消息为已读。

  • dele(): 此方法提供了要删除的消息的参数。 在许多 POP3 服务器上,直到 QUIT 才执行删除操作。 您可以使用rset()方法重置删除标志。

  • quit(): 此方法通过提交一些更改并将您从服务器断开连接来使您脱离连接。

让我们看看如何通过访问谷歌的安全 POP3 电子邮件服务器来读取电子邮件消息。 默认情况下,POP3 服务器在端口995上安全监听。 以下是使用 POP3 获取电子邮件的示例:

#!/usr/bin/env python3
import getpass
import poplib

GOOGLE_POP3_SERVER = 'pop.googlemail.com'
POP3_SERVER_PORT = '995'

def fetch_email(username, password): 
    mailbox = poplib.POP3_SSL(GOOGLE_POP3_SERVER, POP3_SERVER_PORT) 
    mailbox.user(username)
    mailbox.pass_(password) 
    num_messages = len(mailbox.list()[1])
    print("Total emails: {0}".format(num_messages))
    print("Getting last message") 
    for msg in mailbox.retr(num_messages)[1]:
        print(msg)
    mailbox.quit()

if __name__ == '__main__':
    username = input("Enter your email user ID: ")
    password = getpass.getpass(prompt="Enter your email password:    ") 
    fetch_email(username, password)

正如您在前面的代码中所看到的,fetch_email()函数通过调用POP3_SSL()以及服务器套接字创建了一个邮箱对象。 通过调用user()pass_()方法在此对象上设置了用户名和密码。 成功验证后,我们可以通过使用list()方法调用 POP3 命令。 在此示例中,消息的总数已显示在屏幕上。 然后,使用retr()方法检索了单个消息的内容。

这里显示了一个示例输出:

**$ python3 fetch_email_pop3.py** 
**Enter your email user ID: <PERSON1>@gmail.com**
**Enter your email password:** 
**Total emails: 330**
**Getting last message**
**b'Received: by 10.150.139.7 with HTTP; Tue, 7 Oct 2008 13:20:42 -0700** 
**(PDT)'**
**b'Message-ID: <fc9dd8650810...@mail.gmail.com>'**
**b'Date: Tue, 7 Oct 2008 21:20:42 +0100'**
**b'From: "Mr Person1" <PERSON1@gmail.com>'**
**b'To: "Mr Person2" <PERSON2@gmail.com>'**
**b'Subject: Re: Some subject'**
**b'In-Reply-To: <1bec119d...@mail.gmail.com>'**
**b'MIME-Version: 1.0'**
**b'Content-Type: multipart/alternative; '**
**b'\tboundary="----=_Part_63057_22732713.1223410842697"'**
**b'References: <fc9dd8650809270....@mail.gmail.com>'**
**b'\t <1bec119d0810060337p557bc....@mail.gmail.com>'**
**b'Delivered-To: PERSON1@gmail.com'**
**b''**
**b'------=_Part_63057_22732713.1223410842697'**
**b'Content-Type: text/plain; charset=ISO-8859-1'**
**b'Content-Transfer-Encoding: quoted-printable'**
**b'Content-Disposition: inline'**
**b''**
**b'Dear Person2,'**

使用 imaplib 通过 IMAP 检索电子邮件

正如我们之前提到的,通过 IMAP 协议访问电子邮件不一定会将消息下载到本地计算机或手机。 因此,即使在任何低带宽互联网连接上使用,这也可以非常高效。

Python 提供了一个名为imaplib的客户端库,可用于通过 IMAP 协议访问电子邮件。 这提供了实现 IMAP 协议的IMAP4()类。 它接受两个参数,即用于实现此协议的主机和端口。 默认情况下,143已被用作端口号。

派生类IMAP4_SSL()提供了 IMAP4 协议的安全版本。 它通过 SSL 加密套接字连接。 因此,您将需要一个 SSL 友好的套接字模块。 默认端口是993。 与POP3_SSL()类似,您可以提供私钥和证书文件路径。

可以在这里看到 IMAP 客户端的典型示例:

  mailbox = imaplib.IMAP4_SSL(<IMAP_SERVER>, <SERVER_PORT>) 
      mailbox.login('username', 'password')
      mailbox.select('Inbox')

上述代码将尝试启动一个 IMAP4 加密客户端会话。在login()方法成功之后,您可以在创建的对象上应用各种方法。在上述代码片段中,使用了select()方法。这将选择用户的邮箱。默认邮箱称为Inbox。此邮箱对象支持的方法的完整列表可在 Python 标准库文档页面上找到,网址为docs.python.org/3/library/imaplib.html

在这里,我们想演示如何使用search()方法搜索邮箱。它接受字符集和搜索条件参数。字符集参数可以是None,其中将向服务器发送不带特定字符的请求。但是,至少需要指定一个条件。为了执行高级搜索以对消息进行排序,可以使用sort()方法。

与 POP3 类似,我们将使用安全的 IMAP 连接来连接到服务器,使用IMAP4_SSL()类。以下是一个 Python IMAP 客户端的简单示例:

#!/usr/bin/env python3
import getpass
import imaplib
import pprint

GOOGLE_IMAP_SERVER = 'imap.googlemail.com'
IMAP_SERVER_PORT = '993'

def check_email(username, password): 
    mailbox = imaplib.IMAP4_SSL(GOOGLE_IMAP_SERVER, IMAP_SERVER_PORT) 
    mailbox.login(username, password)
    mailbox.select('Inbox')
    tmp, data = mailbox.search(None, 'ALL')
    for num in data[0].split():
        tmp, data = mailbox.fetch(num, '(RFC822)')
        print('Message: {0}\n'.format(num))
        pprint.pprint(data[0][1])
        break
    mailbox.close()
    mailbox.logout()

if __name__ == '__main__':
    username = input("Enter your email username: ")
    password = getpass.getpass(prompt="Enter you account password: ")
    check_email(username, password)

在此示例中,创建了IMPA4_SSL()的实例,即邮箱对象。在其中,我们将服务器地址和端口作为参数。成功使用login()方法登录后,您可以使用select()方法选择要访问的邮箱文件夹。在此示例中,选择了Inbox文件夹。为了阅读消息,我们需要从收件箱请求数据。其中一种方法是使用search()方法。在成功接收一些邮件元数据后,我们可以使用fetch()方法检索电子邮件消息信封部分和数据。在此示例中,使用fetch()方法寻找了 RFC 822 类型的标准文本消息。我们可以使用 Python 的 pretty print 或 print 模块在屏幕上显示输出。最后,将close()logout()方法应用于邮箱对象。

上述代码将显示类似以下内容的输出:

$ python3 fetch_email_imap.py 
Enter your email username: RECIPIENT@gmail.comn
Enter you Google password: 
Message b'1'
b'X-Gmail-Received: 3ec65fa310559efe27307d4e37fdc95406deeb5a\r\nDelivered-To: RECIPIENT@gmail.com\r\nReceived: by 10.54.40.10 with SMTP id n10cs1955wrn;\r\n    [Message omitted]

发送电子邮件附件

在前面的部分中,我们已经看到如何使用 SMTP 协议发送纯文本消息。在本节中,让我们探讨如何通过电子邮件消息发送附件。我们可以使用我们的第二个示例,其中我们使用了 TLS 发送电子邮件。在撰写电子邮件消息时,除了添加纯文本消息,还包括附加附件字段。

在此示例中,我们可以使用email.mime.image子模块的MIMEImage类型。一个 GIF 类型的图像将附加到电子邮件消息中。假设可以在文件系统路径的任何位置找到 GIF 图像。该文件路径通常基于用户输入。

以下示例显示了如何在电子邮件消息中发送附件:

#!/usr/bin/env python3

import os
import getpass
import re
import sys
import smtplib

from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

SMTP_SERVER = 'aspmx.l.google.com'
SMTP_PORT = 25

def send_email(sender, recipient):
    """ Sends email message """
    msg = MIMEMultipart()
    msg['To'] = recipient
    msg['From'] = sender
    subject = input('Enter your email subject: ')
    msg['Subject'] = subject
    message = input('Enter your email message. Press Enter when     finished. ')
    part = MIMEText('text', "plain")
    part.set_payload(message)
    msg.attach(part)
    # attach an image in the current directory
    filename = input('Enter the file name of a GIF image: ')
    path = os.path.join(os.getcwd(), filename)
    if os.path.exists(path):
        img = MIMEImage(open(path, 'rb').read(), _subtype="gif")
        img.add_header('Content-Disposition', 'attachment', filename=filename)
        msg.attach(img)
    # create smtp session
    session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    session.ehlo()
    session.starttls()
    session.ehlo
    # send mail
    session.sendmail(sender, recipient, msg.as_string())
    print("You email is sent to {0}.".format(recipient))
    session.quit()

if __name__ == '__main__':
    sender = input("Enter sender email address: ")
    recipient = input("Enter recipeint email address: ")
    send_email(sender, recipient)

如果运行上述脚本,它将询问通常的内容,即电子邮件发送者、收件人、用户凭据和图像文件的位置。

**$ python3 smtp_mail_sender_mime.py** 
**Enter sender email address: SENDER@gmail.com**
**Enter recipeint email address: RECIPIENT@gmail.com**
**Enter your email subject: Test email with attachment** 
**Enter your email message. Press Enter when finished. This is a test email with atachment.**
**Enter the file name of a GIF image: image.gif**
**You email is sent to RECIPIENT@gmail.com.**

通过日志模块发送电子邮件

在任何现代编程语言中,都提供了常见功能的日志记录设施。同样,Python 的日志模块在功能和灵活性上非常丰富。我们可以使用日志模块的不同类型的日志处理程序,例如控制台或文件日志处理程序。您可以最大化日志记录的好处的一种方法是在生成日志时将日志消息通过电子邮件发送给用户。Python 的日志模块提供了一种称为BufferingHandler的处理程序类型,它能够缓冲日志数据。

稍后显示了扩展BufferingHandler的示例。通过BufferingHandler定义了一个名为BufferingSMTPHandler的子类。在此示例中,使用日志模块创建了一个记录器对象的实例。然后,将BufferingSMTPHandler的实例绑定到此记录器对象。将日志级别设置为 DEBUG,以便记录任何消息。使用了一个包含四个单词的示例列表来创建四个日志条目。每个日志条目应类似于以下内容:

**<Timestamp> INFO  First line of log**
**This accumulated log message will be emailed to a local user as set on top of the script.**

现在,让我们来看一下完整的代码。以下是使用日志模块发送电子邮件的示例:

import logging.handlers
import getpass

MAILHOST = 'localhost'
FROM = 'you@yourdomain'
TO = ['%s@localhost' %getpass.getuser()] 
SUBJECT = 'Test Logging email from Python logging module (buffering)'

class BufferingSMTPHandler(logging.handlers.BufferingHandler):
    def __init__(self, mailhost, fromaddr, toaddrs, subject, capacity):
        logging.handlers.BufferingHandler.__init__(self, capacity)
        self.mailhost = mailhost
        self.mailport = None
        self.fromaddr = fromaddr
        self.toaddrs = toaddrs
        self.subject = subject
        self.setFormatter(logging.Formatter("%(asctime)s %(levelname)-5s %(message)s"))

    def flush(self):
        if len(self.buffer) > 0:
            try:
                import smtplib
                port = self.mailport
                if not port:
                    port = smtplib.SMTP_PORT
                    smtp = smtplib.SMTP(self.mailhost, port)
                    msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n" % (self.fromaddr, ",".join(self.toaddrs), self.subject)
                for record in self.buffer:
                    s = self.format(record)
                    print(s)
                    msg = msg + s + "\r\n"
                smtp.sendmail(self.fromaddr, self.toaddrs, msg)
                smtp.quit()
            except:
                self.handleError(None) # no particular record
            self.buffer = []

def test():
    logger = logging.getLogger("")
    logger.setLevel(logging.DEBUG)
    logger.addHandler(BufferingSMTPHandler(MAILHOST, FROM, TO, SUBJECT, 10))
    for data in ['First', 'Second', 'Third', 'Fourth']:
        logger.info("%s line of log", data)
    logging.shutdown()

if __name__ == "__main__":
    test()

如您所见,我们的BufferingSMTPHandler方法只覆盖了一个方法,即flush()。在构造函数__init__()中,设置了基本变量以及使用setFormatter()方法设置了日志格式。在flush()方法中,我们创建了一个SMTP()对象的实例。使用可用数据创建了 SMTP 消息头。将日志消息附加到电子邮件消息,并调用sendmail()方法发送电子邮件消息。flush()方法中的代码包裹在try-except块中。

所讨论的脚本的输出将类似于以下内容:

**$ python3 logger_mail_send.py** 
**2014-10-25 13:15:07,124 INFO  First line of log**
**2014-10-25 13:15:07,127 INFO  Second line of log**
**2014-10-25 13:15:07,127 INFO  Third line of log**
**2014-10-25 13:15:07,129 INFO  Fourth line of log**

现在,当您使用电子邮件命令(Linux/UNIX 机器上的本机命令)检查电子邮件消息时,您可以期望本地用户已收到电子邮件,如下所示:

**$ mail**
**Mail version 8.1.2 01/15/2001\.  Type ? for help.**
**"/var/mail/faruq": 1 message 1 new**
**>N  1 you@yourdomain     Sat Oct 25 13:15   20/786   Test Logging email from Python logging module (buffering)**

您可以通过在命令提示符上输入消息 ID 和&来查看消息的内容,如下输出所示:

**& 1**
**Message 1:**
**From you@yourdomain Sat Oct 25 13:15:08 2014**
**Envelope-to: faruq@localhost**
**Delivery-date: Sat, 25 Oct 2014 13:15:08 +0100**
**Date: Sat, 25 Oct 2014 13:15:07 +0100**
**From: you@yourdomain**
**To: faruq@localhost**
**Subject: Test Logging email from Python logging module (buffering)**

**2014-10-25 13:15:07,124 INFO  First line of log**
**2014-10-25 13:15:07,127 INFO  Second line of log**
**2014-10-25 13:15:07,127 INFO  Third line of log**
**2014-10-25 13:15:07,129 INFO  Fourth line of log**

最后,您可以通过在命令提示符上输入快捷键q来退出邮件程序,如下所示:

**& q**
**Saved 1 message in /home/faruq/mbox**

总结

本章演示了 Python 如何与三种主要的电子邮件处理协议交互:SMTP、POP3 和 IMAP。在每种情况下,都解释了客户端代码的工作方式。最后,展示了在 Python 的日志模块中使用 SMTP 的示例。

在下一章中,您将学习如何使用 Python 与远程系统一起执行各种任务,例如使用 SSH 进行管理任务,通过 FTP、Samba 等进行文件传输。还将简要讨论一些远程监控协议,如 SNMP,以及身份验证协议,如 LDAP。因此,请在下一章中享受编写更多的 Python 代码。

posted @ 2024-04-18 10:50  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报