Python-Requests-精要-全-

Python Requests 精要(全)

原文:zh.annas-archive.org/md5/205b0f2fd27d62d56643a81eae2f92e8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Python 是我们这个时代不断发展的语言之一,并且近年来受到了越来越多的关注。它是一种功能强大且灵活的开源语言,内置了强大的库。对于每一位 Python 开发者来说,当需要与网络交互时,首先想到的库就是 Requests。随着内置的功能,Requests 使得与网络交互的过程变得轻而易举,并且作为世界上下载量超过 4200 万次的最佳客户端之一,它当之无愧。

随着社交媒体的兴起,API(应用程序编程接口)已成为每个应用的必备部分,以最佳方式与之交互将是一项挑战。了解如何与 API 交互、构建 API、抓取网页等,将有助于每位初出茅庐的网页开发者达到新的高度。

本书涵盖的内容

第一章, 使用 Requests 与网络交互,涵盖了诸如为什么 Requests 比 urllib2 更优秀、如何发起一个简单的请求、不同类型的响应内容、向我们的 Requests 添加自定义头部、处理表单编码数据、使用状态码查找、定位请求的重定向、位置和超时等问题。

第二章,深入挖掘请求,讨论了使用会话对象。它涉及请求和响应的结构、准备好的请求、使用请求进行 SSL 验证、流式上传、生成器和事件钩子。本章还演示了使用代理、链接头和传输头。

第三章, 使用请求进行认证,介绍了实际中用于认证的不同类型流程。你将了解使用 OAuth1、摘要认证和基本认证进行认证的知识。

第四章, 使用 HTTPretty 模拟 HTTP 请求,介绍了 HTTPretty 及其安装和用法。然后,我们处理实时示例,并学习如何使用 Python Requests 和 HTTPretty 来模拟服务器的行为。

第五章, 使用 Requests 与社交媒体互动,涵盖了重要内容。从介绍 Twitter API、Facebook API 和 reddit API 开始,我们将继续探讨如何获取密钥、创建认证请求,并通过各种示例与社交媒体进行交互。

第六章, 使用 Python Requests 和 BeautifulSoup 进行网页抓取,使您能够更好地理解在网页抓取中使用的库。您还将了解到如何使用 BeautifulSoup 库,其安装以及使用 Python Requests 和 BeautifulSoup 进行网页抓取的流程。

注意事项

我们想感谢www.majortests.com允许我们以他们的网站为基础,在本章中构建示例。

第七章, 使用 Python 和 Flask 实现 Web 应用,介绍了 Flask 框架,并继续讨论如何开发一个简单的调查应用,该应用涉及创建、列出和投票各种问题。在本章中,你将获得使用 Flask 构建 Web 应用所需的所有知识。

你需要这本书的内容

您需要以下软件来使用这本书:

  • Python 2.7 或以上

  • Python Requests

  • BeautifulSoup

  • HTTPretty

  • Flask

这本书面向的对象是谁

这本书面向所有 Python 开发者、Web 开发者,甚至想要使用 Requests 库向 Web 服务器发送 HTTP 请求和执行 HTML 抓取的管理员。

习惯用法

在这本书中,您将发现许多用于区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"该过程包括导入Requests模块,然后使用get方法获取网页。"

代码块设置如下:

parameters = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('url', params=parameters)

任何命令行输入或输出都应按照以下格式编写:

>>> r =  requests.get('http://google.com')

新术语重要词汇将以粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中,将以如下方式显示:“点击创建新应用按钮。”

注意事项

警告或重要提示会出现在这样的框中。

小贴士

小贴士和技巧看起来是这样的。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大收益的书籍。

要向我们发送一般性反馈,请直接发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果你在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,链接为www.packtpub.com/authors

客户支持

现在你已经是 Packt 图书的骄傲拥有者了,我们有许多事情可以帮助你从你的购买中获得最大收益。

下载示例代码

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

错误清单

尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下现有的任何勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support并在搜索字段中输入书籍名称。所需信息将显示在勘误部分下。

海盗行为

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接发送至 <copyright@packtpub.com> 与我们联系,以提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面所提供的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章:使用 Requests 与网络交互

在这些现代日子里,从网络服务中读取数据和获取信息往往是一项至关重要的任务。每个人都知道应用程序编程接口API)是如何让 Facebook 将“赞”按钮的使用推广到整个网络,并在社交通信领域占据主导地位的。它具有自己的特色,能够影响商业发展、产品开发和供应链管理。在这个阶段,学习一种有效处理 API 和打开网络 URL 的方法是当务之急。这将极大地影响许多网络开发过程。

HTTP 请求简介

每当我们的网页浏览器试图与网页服务器进行通信时,它都是通过使用超文本传输协议HTTP)来完成的,该协议作为一个请求-响应协议。在这个过程中,我们向网页服务器发送一个请求,并期待得到一个回应。以从网站下载 PDF 文件为例。我们发送一个请求说“给我这个特定的文件”,然后我们从网页服务器得到一个响应,其中包含“这里是文件,接着是文件本身”。我们发送的 HTTP 请求可能包含许多有趣的信息。让我们深入挖掘它。

这里是经过我的设备发送的 HTTP 请求的原始信息。通过查看以下示例,我们可以掌握请求的重要部分:

* Connected to google.com (74.125.236.35) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: google.com
> Accept: */*
>
< HTTP/1.1 302 Found
< Cache-Control: private
< Content-Type: text/html; charset=UTF-8
< Location: http://www.google.co.in/?gfe_rd=cr&ei=_qMUVKLCIa3M8gewuoCYBQ
< Content-Length: 261
< Date: Sat, 13 Sep 2014 20:07:26 GMT
* Server GFE/2.0 is not blacklisted
< Server: GFE/2.0
< Alternate-Protocol: 80:quic,p=0.002

现在,我们将向服务器发送一个请求。让我们利用 HTTP 请求的这些部分:

  • 方法:前一个示例中的 GET / http /1.1 是一个大小写敏感的 HTTP 方法。以下是一些 HTTP 请求方法:

    • GET:此操作通过给定的 URI 从指定的服务器获取信息。

    • HEAD: 这个功能与 GET 类似,但不同之处在于,它只返回状态行和头部部分。

    • POST:这可以将我们希望处理的数据提交到服务器。

    • PUT:当我们打算创建一个新的 URL 时,这个操作会创建或覆盖目标资源的所有当前表示。

    • DELETE:此操作将删除由给定Request-URI描述的所有资源。

    • OPTIONS: 这指定了请求/响应周期的通信选项。它允许客户端提及与资源相关联的不同选项。

  • 请求 URI:统一资源标识符(URI)具有识别资源名称的能力。在先前的例子中,主机名是请求 URI

  • 请求头字段:如果我们想添加更多关于请求的信息,我们可以使用请求头字段。它们是冒号分隔的键值对。一些request-headers的值包括:

    • Accept-Charset: 这用于指示响应可接受的字符集。

    • Authorization: 这包含用户代理的认证信息的凭证值。

    • 主机: 这标识了用户请求的资源所对应的互联网主机和端口号,使用用户提供的原始 URI。

    • User-agent: 它容纳了关于发起请求的用户代理的信息。这可以用于统计目的,例如追踪协议违规行为。

Python 模块

有些广泛使用的 Python 模块可以帮助打开 URL。让我们来看看它们:

  • httplib2: 这是一个功能全面的 HTTP 客户端库。它支持许多其他 HTTP 库中未提供的特性。它支持诸如缓存、持久连接、压缩、重定向以及多种认证等功能。

  • urllib2:这是一个在复杂世界中广泛使用的模块,用于获取 HTTP URL。它定义了帮助进行 URL 操作的功能和类,例如基本和摘要认证、重定向、cookies 等。

  • Requests:这是一个 Apache2 许可的 Python 编写的 HTTP 库,拥有许多功能,从而提高了生产力。

请求与 urllib2

让我们比较urllib2Requestsurllib2.urlopen()可以用来打开一个 URL(可以是字符串或请求对象),但在与网络交互时,还有很多其他事情可能会成为负担。在这个时候,一个简单的 HTTP 库,它具有使网络交互变得顺畅的能力,正是当务之急,而 Requests 就是其中之一。

以下是一个使用 urllib2Requests 获取网络服务数据的示例,它清晰地展示了使用 Requests 的工作是多么简单:

以下代码给出了urllib2的一个示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import urllib2

gh_url = 'https://api.github.com'

req = urllib2.Request(gh_url)

password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, 'user', 'pass')

auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)

urllib2.install_opener(opener)

handler = urllib2.urlopen(req)

print handler.getcode()
print handler.headers.getheader('content-type')

# ------

# 'application/json'

使用 Requests 实现的相同示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests

r = requests.get('https://api.github.com', auth=('user', 'pass'))

print r.status_code
print r.headers['content-type']

# ------

# 'application/json'

这些示例可以在gist.github.com/kennethreitz/973705找到。

在这个初始阶段,示例可能看起来相当复杂。不要深入到示例的细节中。只需看看requests库带来的美丽之处,它使我们能够用非常少的代码登录 GitHub。使用requests的代码似乎比urllib2示例简单且高效得多。这将有助于我们在各种事情上提高生产力。

请求的本质

HTTP/1.0 类似,HTTP/1.1 具有很多优点和新增功能,例如多次重用连接以减少相当大的开销、持久连接机制等。幸运的是,requests 库就是基于它构建的,这让我们能够与网络进行平滑和无缝的交互。我们无需手动将查询字符串添加到我们的 URL 中,也不必对 POST 数据进行编码。持久连接和 HTTP 连接池是完全自动的,由嵌入在 requests 中的 urllib3 提供。使用 requests,我们得到了一种无需再次考虑编码参数的方法,无论它是 GET 还是 POST。

在 URL 中无需手动添加查询字符串,同样也不需要添加诸如连接池保持活动状态、带有 cookie 持久性的会话、基本/摘要认证、浏览器风格的 SSL 验证、连接超时、多部分文件上传等功能。

提出一个简单的请求

现在我们来创建第一个获取网页的请求,这个过程非常简单。它包括导入requests模块,然后使用get方法获取网页。让我们来看一个例子:

>>> import requests
>>> r =  requests.get('http://google.com')

哇哦!我们完成了。

在前面的例子中,我们使用 requests.get 获取了 google 网页,并将其保存在变量 r 中,该变量最终变成了 response 对象。response 对象 r 包含了关于响应的大量信息,例如头部信息、内容、编码类型、状态码、URL 信息以及许多更复杂的细节。

同样地,我们可以使用所有 HTTP 请求方法,如 GET、POST、PUT、DELETE、HEAD,与 requests 一起使用。

现在我们来学习如何在 URL 中传递参数。我们可以使用params关键字将参数添加到请求中。

以下为传递参数所使用的语法:

parameters = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('url', params=parameters)

为了对此有一个清晰的了解,让我们通过登录 GitHub,使用以下代码中的requests来获取 GitHub 用户详细信息:

>>> r = requests.get('https://api.github.com/user', auth=('myemailid.mail.com', 'password'))
>>> r.status_code
200
>>> r.url
u'https://api.github.com/user'
>>> r.request
<PreparedRequest [GET]>

我们使用了auth元组,它支持基本/摘要/自定义认证,用于登录 GitHub 并获取用户详情。r.status_code的结果表明我们已成功获取用户详情,并且我们已经访问了 URL,以及请求的类型。

响应内容

响应内容是在我们发送请求时,服务器返回到我们控制台的信息。

在与网络交互时,解码服务器的响应是必要的。在开发应用程序时,我们可能会遇到许多需要处理原始数据、JSON 格式或甚至二进制响应的情况。为此,requests库具有自动解码服务器内容的能力。Requests 可以顺畅地解码许多 Unicode 字符集。此外,Requests 还会根据响应的编码进行有根据的猜测。这基本上是通过考虑头部信息来实现的。

如果我们访问 r.content 的值,它将返回一个原始字符串格式的响应内容。如果我们访问 r.text,Requests 库将使用 r.encoding 对响应(r.content 的值)进行编码,并返回一个新的编码字符串。在这种情况下,如果 r.encoding 的值为 None,Requests 将使用 r.apparent_encoding 来假设编码类型,r.apparent_encoding 是由 chardet 库提供的。

我们可以通过以下方式访问服务器的响应内容:

>>> import requests
>>> r = requests.get('https://google.com')
>>> r.content
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" …..'
>>> type(r.content)
<type 'str'>
>>> r.text
u'<!doctype html><html itemscope=""\ itemtype="http://schema.org/WebPage" lang="en-IN"><head><meta content="........
>>> type(r.text)
<type 'unicode'>

在前面的行中,我们尝试使用 requests.get() 获取 google 首页,并将其赋值给变量 r。这里的 r 变量实际上是一个请求对象,我们可以使用 r.content 访问原始内容,以及使用 r.text 获取编码后的响应内容。

如果我们想找出 Requests 正在使用的编码,或者如果我们想更改编码,我们可以使用属性r.encoding,如下面的示例所示:

>>> r.encoding
'ISO-8859-1'
>>> r.encoding = 'utf-8'

在代码的第一行,我们正在尝试访问 Requests 所使用的编码类型。结果是'ISO-8859-1'。在下一行,我希望将编码更改为'utf-8'。因此,我将编码类型赋值给了r.encoding。如果我们像第二行那样更改编码,Requests 通常会使用已分配的r.encoding的最新值。所以从那时起,每次我们调用r.text时,它都会使用相同的编码。

例如,如果r.encoding的值为None,Requests 通常会使用r.apparent_encoding的值。以下示例解释了这种情况:

>>> r.encoding = None
>>> r.apparent_encoding
'ascii'

通常,显式编码的值由chardet库指定。如果我们充满热情地尝试将新的编码类型设置为r.apparent_encoding,Requests 将引发一个AttributeError,因为其值不能被更改。

>>> r.apparent_encoding = 'ISO-8859-1'
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

请求足够高效,可以自定义编码。以我们创建了自己的编码并已在codecs模块中注册为例,我们可以轻松地使用我们的自定义编解码器;这是因为r.encoding的值和 Requests 会处理解码工作。

不同类型的请求内容

Requests 具有处理不同类型的请求内容的功能,例如二进制响应内容、JSON 响应内容和原始响应内容。为了清晰地展示不同类型的响应内容,我们列出了详细信息。这里使用的示例是用 Python 2.7.x 开发的。

自定义标题

我们可以在请求中发送自定义的头部信息。为此,我们只需创建一个包含我们头部信息的字典,并在getpost方法中传递头部参数。在这个字典中,键是头部信息的名称,而值则是,嗯,这对的值。让我们将一个 HTTP 头部信息传递给一个请求:

>>> import json
>>> url = 'https://api.github.com/some/endpoint'
>>>  payload = {'some': 'data'}
>>> headers = {'Content-Type': 'application/json'}
>>> r = requests.post(url, data=json.dumps(payload), headers=headers)

此示例取自docs.python-requests.org/en/latest/user/quickstart/#custom-headers中的请求文档。

在这个例子中,我们向请求发送了一个带有值application/json的头部content-type作为参数。

同样地,我们可以发送带有自定义头部的请求。比如说,我们有必要发送一个带有授权头部且值为某个令牌的请求。我们可以创建一个字典,其键为'Authorization',值为一个看起来如下所示的令牌:

>>> url = 'some url'
>>>  header = {'Authorization' : 'some token'}
>>> r.request.post(url, headers=headers)

发送表单编码的数据

我们可以使用 Requests 发送类似 HTML 表单的表单编码数据。将一个简单的字典传递给 data 参数即可完成此操作。当发起请求时,数据字典将自动转换为表单编码。

>>> payload = {'key1': 'value1', 'key2': 'value2'}
>>> r = request.post("some_url/post", data=payload)
>>> print(r.text)
{
 …
 "form": {
 "key2": "value2",
 "key1": "value1"
 },
 …
}

在前面的例子中,我们尝试发送了表单编码的数据。而在处理非表单编码的数据时,我们应该在字典的位置发送一个字符串。

发布多部分编码的文件

我们倾向于通过 POST 方法上传多部分数据,如图片或文件。在 requests 库中,我们可以使用 files 参数来实现,它是一个包含 'name'file-like-objects 值的字典。同时,我们也可以将其指定为 'name',值可以是 'filename'fileobj,就像以下这种方式:

{'name' : file-like-objects} or
{'name': ('filename',  fileobj)}

示例如下:

>>> url = 'some api endpoint'
>>> files = {'file': open('plan.csv', 'rb')}
>>> r = requests.post(url, files=files)

We can access the response using 'r.text'.
>>>  r.text
{
 …
 "files": {
 "file": "< some data … >"
 },
 ….
}

在前一个例子中,我们没有指定内容类型或头部信息。除此之外,我们还有能力为上传的文件设置名称:

>>> url = 'some url'
>>> files = {'file': ('plan.csv', open('plan.csv', 'rb'), 'application/csv', {'Expires': '0'})}
>>> r = requests.post(url, files)
>>> r.text
{
 …
 "files"
 "file": "< data...>"
 },
 …
}

我们也可以以下这种方式发送字符串作为文件接收:

>>> url = 'some url'
>>> files = {'file' : ('plan.csv', 'some, strings, to, send')}
>>> r.text
{
 …
 "files": {
 "file": "some, strings, to, send"
 },
 …
}

查看内置响应状态码

状态码有助于让我们知道请求发送后的结果。为了了解这一点,我们可以使用status_code

>>> r = requests.get('http://google.com')
>>> r.status_code
200

为了使处理状态码变得更加容易,Requests 模块内置了一个状态码查找对象,它作为一个便捷的参考。我们必须将requests.codes.okr.status_code进行比较以实现这一点。如果结果为True,则表示是200状态码,如果为False,则不是。我们还可以将r.status.coderequests.codes.okrequests.code.all_good进行比较,以使查找工作得以进行。

>>> r = requests.get('http://google.com')
>>> r.status_code == requests.codes.ok
True

现在,让我们尝试检查一个不存在的 URL。

>>> r = requests.get('http://google.com/404')
>>> r.status_code == requests.codes.ok
False

我们有处理不良请求的能力,例如 4XX 和 5XX 类型的错误,通过通知错误代码来实现。这可以通过使用Response.raise_for_status()来完成。

让我们先通过发送一个错误请求来尝试一下:

>>> bad_request = requests.get('http://google.com/404')
>>> bad_request.status_code
404
>>>bad_request.raise_for_status()
---------------------------------------------------------------------------
HTTPError                              Traceback (most recent call last)
----> bad_request..raise_for_status()

File "requests/models.py",  in raise_for_status(self)
 771
 772         if http_error_msg:
--> 773             raise HTTPError(http_error_msg, response=self)
 774
 775     def close(self):

HTTPError: 404 Client Error: Not Found

现在如果我们尝试一个有效的 URL,我们不会得到任何响应,这是成功的标志:

>>> bad_request = requests.get('http://google.com')
>>> bad_request.status_code
200
>>> bad_request.raise_for_status()
>>>

小贴士

下载示例代码

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

查看响应头

服务器响应头帮助我们了解原始服务器处理请求所使用的软件。我们可以通过r.headers访问服务器响应头:

>>> r = requests.get('http://google.com')
>>> r.headers
CaseInsensitiveDict({'alternate-protocol': '80:quic', 'x-xss-protection': '1; mode=block', 'transfer-encoding': 'chunked', 'set-cookie': 'PREF=ID=3c5de2786273fce1:FF=0:TM=1410378309:LM=1410378309:S=DirRRD4dRAxp2Q_3; …..

请求评论RFC)7230 表示 HTTP 头部名称不区分大小写。这使我们能够使用大写和小写字母访问头部。

>>> r.headers['Content-Type']
'text/html; charset=ISO-8859-1'

>>>  r.headers.get('content-type')
'text/html; charset=ISO-8859-1'

使用 Requests 访问 Cookies

我们可以访问响应中的 cookie,如果存在的话:

>>> url = 'http://somewebsite/some/cookie/setting/url'
>>> r = requests.get(url)

>>> r.cookies['some_cookie_name']
'some_cookie_value'

我们可以发送自己的 cookie,如下例所示:

>>> url = 'http://httpbin.org/cookies'
>>> cookies = dict(cookies_are='working')

>>> r = requests.get(url, cookies=cookies)
>>> r.text
'{"cookies": {"cookies_are": "working"}}'

使用请求历史跟踪请求的重定向

有时我们访问的 URL 可能已经被移动,或者可能会被重定向到其他位置。我们可以使用 Requests 来追踪它们。响应对象的 history 属性可以用来追踪重定向。Requests 除了 HEAD 之外,可以用每个动词完成位置重定向。Response.history列表包含了为了完成请求而生成的 Requests 对象。

>>> r = requests.get('http:google.com')
>>> r.url
u'http://www.google.co.in/?gfe_rd=cr&ei=rgMSVOjiFKnV8ge37YGgCA'
>>> r.status_code
200
>>> r.history
(<Response [302]>,)

在前面的例子中,当我们尝试向'www.google.com'发送请求时,我们得到了r.history的值为302,这意味着 URL 已经被重定向到了其他位置。r.url在这里显示了这一重定向的证明,包括重定向的 URL。

如果我们不想让 Requests 处理重定向,或者我们在使用 POST、GET、PUT、PATCH、OPTIONS 或 DELETE 时,我们可以将allow_redirects=False,的值设置为 False,这样重定向处理就会被禁用。

>>> r = requests.get('http://google.com', allow_redirects=False)
>>> r.url
u'http://google.com/'
>> r.status_code
302
>>> r.history
[ ]

在前面的例子中,我们使用了参数 allow_redirects=False,,这导致在 URL 中没有任何重定向,r.url 的值保持不变,而 r.history 被设置为空。

如果我们使用头部来访问 URL,我们可以简化重定向过程。

>>> r = requests.head('http://google.com', allow_redirects=True)
>>> r.url
u'http://www.google.co.in/?gfe_rd=cr&ei=RggSVMbIKajV8gfxzID4Ag'
>>> r.history
(<Response [302]>,)

在这个例子中,我们尝试使用带有启用参数allow_redirects的 head 方法访问 URL,结果导致 URL 被重定向。

使用超时来控制高效使用

考虑这样一个案例,我们正在尝试访问一个耗时过多的响应。如果我们不想让进程继续进行,并在超过特定时间后抛出异常,我们可以使用参数timeout

当我们使用timeout参数时,我们是在告诉 Requests 在经过特定的时间段后不要等待响应。如果我们使用timeout,这并不等同于在整个响应下载上定义一个时间限制。如果在指定的timeout秒内底层套接字上没有确认任何字节,抛出一个异常是一个好的实践。

>>> requests.get('http://google.com', timeout=0.03)
---------------------------------------------------------------------------
Timeout                                   Traceback (most recent call last)
…….
……..
Timeout: HTTPConnectionPool(host='google.com', port=80): Read timed\ out. (read timeout=0.03)

在本例中,我们已将timeout值指定为0.03,超时时间已超过,从而带来了响应,因此导致了timeout异常。超时可能发生在两种不同的情况下:

  • 在尝试连接到位于远程位置的服务器时,请求超时了。

  • 如果服务器在分配的时间内没有发送完整的响应,请求将超时。

错误和异常

在发送请求和获取响应的过程中,如果出现问题,将会引发不同类型的错误和异常。其中一些如下:

  • HTTPError: 当存在无效的 HTTP 响应时,Requests 将引发一个HTTPError异常

  • ConnectionError:如果存在网络问题,例如连接被拒绝和 DNS 故障,Requests 将引发ConnectionError异常

  • Timeout: 如果请求超时,将抛出此异常

  • TooManyRedirects: 如果请求超过了配置的最大重定向次数,则会引发此类异常

其他一些出现的异常类型包括Missing schema ExceptionInvalidURLChunkedEncodingErrorContentDecodingError等等。

此示例取自docs.python-requests.org/en/latest/user/quickstart/#errors-and-exceptions提供的请求文档。

摘要

在本章中,我们介绍了一些基本主题。我们学习了为什么 Requests 比urllib2更好,如何发起一个简单的请求,不同类型的响应内容,如何为我们的 Requests 添加自定义头信息,处理表单编码数据,使用状态码查找,定位请求的重定向位置以及关于超时的问题。

在下一章中,我们将深入学习 Requests 的高级概念,这将帮助我们根据需求灵活地使用 Requests 库。

第二章:深入挖掘请求

在本章中,我们将探讨 Requests 模块的高级主题。Requests 模块中还有许多更多功能,使得与网络的交互变得轻而易举。让我们更深入地了解使用 Requests 模块的不同方法,这有助于我们理解其使用的便捷性。

简而言之,我们将涵盖以下主题:

  • 使用 Session 对象在请求间持久化参数

  • 揭示请求和响应的结构

  • 使用预定义请求

  • 使用 Requests 验证 SSL 证书

  • 主体内容工作流程

  • 使用生成器发送分块编码的请求

  • 使用事件钩子获取请求方法参数

  • 遍历流式 API

  • 使用链接头描述 API

  • 传输适配器

使用 Session 对象在请求间持久化参数

Requests 模块包含一个session对象,该对象具有在请求之间持久化设置的能力。使用这个session对象,我们可以持久化 cookies,我们可以创建准备好的请求,我们可以使用 keep-alive 功能,并且可以做更多的事情。Session 对象包含了 Requests API 的所有方法,如GETPOSTPUTDELETE等等。在使用 Session 对象的所有功能之前,让我们了解一下如何使用会话并在请求之间持久化 cookies。

让我们使用会话方法来获取资源。

>>> import requests
>>> session = requests.Session()
>>> response = requests.get("https://google.co.in", cookies={"new-cookie-identifier": "1234abcd"})

在前面的例子中,我们使用requests创建了一个session对象,并使用其get方法来访问网络资源。

在前一个示例中我们设置的cookie值可以通过response.request.headers来访问。

>>> response.request.headers
CaseInsensitiveDict({'Cookie': 'new-cookie-identifier=1234abcd', 'Accept-Encoding': 'gzip, deflate, compress', 'Accept': '*/*', 'User-Agent': 'python-requests/2.2.1 CPython/2.7.5+ Linux/3.13.0-43-generic'})
>>> response.request.headers['Cookie']
'new-cookie-identifier=1234abcd'

使用 session 对象,我们可以指定一些属性的默认值,这些值需要通过 GET、POST、PUT 等方式发送到服务器。我们可以通过在 Session 对象上指定 headersauth 等属性的值来实现这一点。

>>> session.params = {"key1": "value", "key2": "value2"}
>>> session.auth = ('username', 'password')
>>> session.headers.update({'foo': 'bar'})

在前面的示例中,我们使用session对象为属性设置了一些默认值——paramsauthheaders。如果我们想在后续请求中覆盖它们,可以像以下示例中那样操作:

>>> session.get('http://mysite.com/new/url', headers={'foo': 'new-bar'})

揭示请求和响应的结构

请求对象是用户在尝试与网络资源交互时创建的。它将以准备好的请求形式发送到服务器,并且包含一些可选的参数。让我们来仔细看看这些参数:

  • 方法: 这是与网络服务交互所使用的 HTTP 方法。例如:GET、POST、PUT。

  • URL: 需要发送请求的网页地址。

  • headers: 请求中要发送的头部信息字典。

  • files: 在处理分片上传时可以使用。这是一个文件字典,键为文件名,值为文件对象。

  • data:这是要附加到request.json的正文。这里有两种情况会出现:

    • 如果提供了json,则头部中的content-type会被更改为application/json,在此点,json作为请求的主体。

    • 在第二种情况下,如果同时提供了jsondata,则data会被静默忽略。

  • params:一个字典,包含要附加到 URL 的 URL 参数。

  • auth: 这用于我们在需要指定请求的认证时。它是一个包含用户名和密码的元组。

  • cookies:一个字典或饼干罐,可以添加到请求中。

  • hooks: 回调钩子的字典。

响应对象包含服务器对 HTTP 请求的响应。它是在 Requests 从服务器收到响应后生成的。它包含了服务器返回的所有信息,同时也存储了我们最初创建的请求对象。

每当我们使用 requests 向服务器发起调用时,在此背景下会发生两个主要的事务,具体如下:

  • 我们正在构建一个请求对象,该对象将被发送到服务器以请求资源

  • requests模块生成一个响应对象

现在,让我们来看一个从 Python 官方网站获取资源的例子。

>>> response = requests.get('https://python.org')

在上一行代码中,创建了一个requests对象,并将发送到'https://python.org'。因此获得的 Requests 对象将被存储在response.request变量中。我们可以以下这种方式访问发送到服务器的请求对象的头部信息:

>>> response.request.headers
CaseInsensitiveDict({'Accept-Encoding': 'gzip, deflate, compress', 'Accept': '*/*', 'User-Agent': 'python-requests/2.2.1 CPython/2.7.5+ Linux/3.13.0-43-generic'})

服务器返回的标题可以通过其 'headers' 属性进行访问,如下例所示:

>>> response.headers
CaseInsensitiveDict({'content-length': '45950', 'via': '1.1 varnish', 'x-cache': 'HIT', 'accept-ranges': 'bytes', 'strict-transport-security': 'max-age=63072000; includeSubDomains', 'vary': 'Cookie', 'server': 'nginx', 'age': '557','content-type': 'text/html; charset=utf-8', 'public-key-pins': 'max-age=600; includeSubDomains; ..)

response 对象包含不同的属性,如 _contentstatus_codeheadersurlhistoryencodingreasoncookieselapsedrequest

>>> response.status_code
200
>>> response.url
u'https://www.python.org/'
>>> response.elapsed
datetime.timedelta(0, 1, 904954)
>>> response.reason
'OK'

使用准备好的 Requests

我们发送给服务器的每个请求默认都会转换为PreparedRequest。从 API 调用或会话调用中接收到的Response对象的request属性实际上就是使用的PreparedRequest

可能存在需要发送请求并额外添加不同参数的情况。参数可以是cookiesfilesauthtimeout等等。我们可以通过使用会话和预请求的组合来高效地处理这个额外步骤。让我们来看一个例子:

>>> from requests import Request, Session
>>> header = {}
>>> request = Request('get', 'some_url', headers=header)

我们在之前的例子中尝试发送一个带有头部的get请求。现在,假设我们打算使用相同的方法、URL 和头部信息发送请求,但想要添加一些额外的参数。在这种情况下,我们可以使用会话方法来接收完整的会话级别状态,以便访问最初发送请求的参数。这可以通过使用session对象来实现。

>>> from requests import Request, Session
>>> session = Session()
>>> request1 = Request('GET', 'some_url', headers=header)

现在,让我们使用session对象来准备一个请求,以获取session级别的状态值:

>>> prepare = session.prepare_request(request1)

我们现在可以发送带有更多参数的请求对象 request,如下所示:

>>> response = session.send(prepare, stream=True, verify=True)
200

哇!节省了大量时间!

prepare 方法使用提供的参数准备完整的请求。在之前的示例中使用了 prepare_request 方法。还有一些其他方法,如 prepare_authprepare_bodyprepare_cookiesprepare_headersprepare_hooksprepare_methodprepare_url,这些方法用于创建单个属性。

使用 Requests 验证 SSL 证书

Requests 提供了验证 HTTPS 请求的 SSL 证书的功能。我们可以使用 verify 参数来检查主机的 SSL 证书是否已验证。

让我们考虑一个没有 SSL 证书的网站。我们将向其发送带有参数verify的 GET 请求。

发送请求的语法如下:

requests.get('no ssl certificate site', verify=True)

由于该网站没有 SSL 证书,将会导致类似以下错误:

requests.exceptions.ConnectionError: ('Connection aborted.', error(111, 'Connection refused'))

让我们验证一个已认证网站的 SSL 证书。考虑以下示例:

>>> requests.get('https://python.org', verify=True)
<Response [200]>

在前面的例子中,结果是200,因为提到的网站是 SSL 认证的。

如果我们不想通过请求验证 SSL 证书,那么我们可以将参数设置为verify=False。默认情况下,verify的值将变为True

主体内容工作流程

以一个在我们发起请求时正在下载连续数据流的例子来说明。在这种情况下,客户端必须持续监听服务器,直到接收到完整的数据。考虑先访问响应内容,然后再担心体(body)的情况。在上面的两种情况下,我们可以使用参数stream。让我们来看一个例子:

>>> requests.get("https://pypi.python.org/packages/source/F/Flask/Flask-0.10.1.tar.gz", stream=True)

如果我们使用参数 stream=True, 发起请求,连接将保持开启状态,并且只会下载响应的头信息。这使我们能够通过指定条件,如数据字节数,在需要时随时获取内容。

语法如下:

if int(request.headers['content_length']) < TOO_LONG:
content = r.content

通过设置参数 stream=True 并将响应作为类似文件的 response.raw 对象访问,如果我们使用 iter_content 方法,我们可以遍历 response.data。这将避免一次性读取较大的响应。

语法如下:

iter_content(chunk_size=size in bytes, decode_unicode=False)

同样地,我们可以使用iter_lines方法遍历内容,该方法将逐行遍历响应数据。

语法如下:

iter_lines(chunk_size = size in bytes, decode_unicode=None, delimitter=None)

注意

在使用stream参数时需要注意的重要事项是,当它被设置为True时,并不会释放连接,除非所有数据都被消耗或者执行了response.close

Keep-alive 功能

由于urllib3支持对同一套接字连接进行多次请求的重用,我们可以使用一个套接字发送多个请求,并通过Requests库中的 keep-alive 功能接收响应。

在一个会话中,它变成了自动的。会话中提出的每个请求默认都会自动使用适当的连接。在读取完所有来自主体的数据后,所使用的连接将被释放。

流式上传

一个类似文件的大尺寸对象可以使用Requests库进行流式传输和上传。我们所需做的只是将流的内容作为值提供给request调用中的data属性,如下面的几行所示。

语法如下:

with open('massive-body', 'rb') as file:
    requests.post('http://example.com/some/stream/url',
                  data=file)

使用生成器发送分块编码的请求

分块传输编码是 HTTP 请求中传输数据的一种机制。通过这个机制,数据被分成一系列的数据块进行发送。请求支持分块传输编码,适用于出站和入站请求。为了发送一个分块编码的请求,我们需要提供一个用于你主体的生成器。

使用方法如下所示:

>>> def generator():
...     yield "Hello "
...     yield "World!"
...
>>> requests.post('http://example.com/some/chunked/url/path',
 data=generator())

使用事件钩子获取请求方法参数

我们可以使用钩子来修改请求过程信号事件的处理部分。例如,有一个名为response的钩子,它包含了从请求生成的响应。它是一个可以作为请求参数传递的字典。其语法如下:

hooks = {hook_name: callback_function, … }

callback_function 参数可能返回也可能不返回值。当它返回值时,假设它是用来替换传入的数据。如果回调函数不返回任何值,则对数据没有任何影响。

这里是一个回调函数的示例:

>>> def print_attributes(request, *args, **kwargs):
...     print(request.url)
...     print(request .status_code)
...     print(request .headers)

如果在执行callback_function函数时出现错误,你将在标准输出中收到一个警告信息。

现在我们使用前面的callback_function来打印一些请求的属性:

>>> requests.get('https://www.python.org/',
 hooks=dict(response=print_attributes))
https://www.python.org/
200
CaseInsensitiveDict({'content-type': 'text/html; ...})
<Response [200]>

遍历流式 API

流式 API 通常会保持请求开启,使我们能够实时收集流数据。在处理连续数据流时,为确保不遗漏任何消息,我们可以借助 Requests 库中的iter_lines()方法。iter_lines()会逐行遍历响应数据。这可以通过在发送请求时将参数 stream 设置为True来实现。

注意事项

最好记住,调用 iter_lines() 函数并不总是安全的,因为它可能会导致接收到的数据丢失。

考虑以下例子,它来自docs.python-requests.org/en/latest/user/advanced/#streaming-requests:

>>> import json
>>> import requests
>>> r = requests.get('http://httpbin.org/stream/4', stream=True)
>>> for line in r.iter_lines():
...     if line:
...         print(json.loads(line) )

在前面的例子中,响应包含了一系列数据。借助iter_lines()函数,我们尝试通过遍历每一行来打印这些数据。

编码

如 HTTP 协议(RFC 7230)中所述,应用程序可以请求服务器以编码格式返回 HTTP 响应。编码过程将响应内容转换为可理解格式,使其易于访问。当 HTTP 头无法返回编码类型时,Requests 将尝试借助chardet来假设编码。

如果我们访问请求的响应头,它确实包含content-type键。让我们看看响应头的content-type

>>> re = requests.get('http://google.com')
>>> re.headers['content-type']
 'text/html; charset=ISO-8859-1'

在前面的例子中,内容类型包含 'text/html; charset=ISO-8859-1'。这种情况发生在 Requests 库发现charset值为None'content-type'值为'Text'时。

它遵循 RFC 7230 协议,在这种情况下将charset的值更改为ISO-8859-1。如果我们处理的是像'utf-8'这样的不同编码类型,我们可以通过将属性设置为Response.encoding来显式指定编码。

HTTP 动词

请求支持使用以下表中定义的完整范围的 HTTP 动词。在使用这些动词的大多数情况下,'url' 是必须传递的唯一参数。

方法 描述
GET GET 方法请求获取指定资源的表示形式。除了检索数据外,使用此方法不会产生其他效果。定义如下 requests.get(url, **kwargs)
POST POST 方法用于创建新的资源。提交的 data 将由服务器处理到指定的资源。定义如下 requests.post(url, data=None, json=None, **kwargs)
PUT 此方法上传指定 URI 的表示。如果 URI 不指向任何资源,服务器可以使用给定的data创建一个新的对象,或者修改现有的资源。定义如下requests.put(url, data=None, **kwargs)
删除 这很容易理解。它用于删除指定的资源。定义如下 requests.delete(url, **kwargs)
头部 此动词用于检索响应头中写入的元信息,而无需获取响应体。定义如下 requests.head(url, **kwargs)
选项 选项是一个 HTTP 方法,它返回服务器为指定 URL 支持的 HTTP 方法。定义如下 requests.options(url, **kwargs)
PATCH 此方法用于对资源应用部分修改。定义如下 requests.patch(url, data=None, **kwargs)

使用链接头描述 API

以访问一个信息分布在不同页面的资源为例。如果我们需要访问资源的下一页,我们可以利用链接头。链接头包含了请求资源的元数据,即在我们这个例子中的下一页信息。

>>> url = "https://api.github.com/search/code?q=addClass+user:mozilla&page=1&per_page=4"
>>> response = requests.head(url=url)
>>> response.headers['link']
'<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2&per_page=4>; rel="next", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=250&per_page=4>; rel="last"

在前面的例子中,我们在 URL 中指定了想要访问第一页,并且该页应包含四条记录。Requests 会自动解析链接头并更新关于下一页的信息。当我们尝试访问链接头时,它显示了包含页码和每页记录数的输出值。

传输适配器

它用于为 Requests 会话提供一个接口,以便连接到 HTTP 和 HTTPS。这将帮助我们模拟网络服务以满足我们的需求。借助传输适配器,我们可以根据我们选择的 HTTP 服务来配置请求。Requests 包含一个名为HTTPAdapter的传输适配器。

考虑以下示例:

>>> session = requests.Session()
>>> adapter = requests.adapters.HTTPAdapter(max_retries=6)
>>> session.mount("http://google.co.in", adapter)

在这个例子中,我们创建了一个请求会话,在这个会话中,每次请求在连接失败时只会重试六次。

摘要

在本章中,我们学习了创建会话以及根据不同标准使用会话。我们还深入探讨了 HTTP 动词和代理的使用。我们了解了流式请求、处理 SSL 证书验证和流式响应。此外,我们还了解了如何使用预定义请求、链接头和分块编码请求。

在下一章中,我们将学习各种认证类型以及如何使用 Requests 库来应用它们。

第三章:使用请求进行身份验证

Requests 支持多种认证流程,并且其构建方式使得认证方法感觉像轻松的散步。在本章中,我们选择详细探讨各种技术巨头用于访问网络资源的不同类型的认证流程。

我们将涵盖以下主题:

  • 基本认证

  • 摘要认证

  • Kerberos 认证

  • OAuth 认证

  • 自定义认证

基本认证

基本认证是一种流行的、行业标准的认证方案,它在HTTP 1.0中被指定。这种方法利用用户提交的用户 ID密码来进行认证。提交的用户 ID密码使用Base64编码标准进行编码,并通过 HTTP 传输。只有当用户 ID密码有效时,服务器才会向用户提供访问权限。以下使用基本认证的优点:

  • 使用此方案的主要优势是它被大多数网络浏览器和服务器支持。尽管它简单直接,但它确实存在一些缺点。尽管所有凭证都在请求中编码和传输,但它们并未加密,这使得整个过程不安全。克服这个问题的方法之一是在启动安全会话时使用 SSL 支持。

  • 其次,凭证在服务器上持续存在,直到浏览器会话结束,这可能会导致资源被占用。此外,这个认证过程很容易受到跨站请求伪造CSRF)攻击的影响,因为浏览器会自动在后续请求中发送用户的凭证。

基本认证流程包含两个步骤:

  1. 如果请求的资源需要身份验证,服务器将返回包含WWW-Authenticate头部的http 401响应。

  2. 如果用户在Authorization头中发送带有用户 ID 和密码的另一个请求,服务器将处理提交的凭据并授予访问权限。

你可以在以下图表中看到这一点:

基本认证

使用 Requests 进行基本身份验证

我们可以使用requests模块非常容易地发送一个请求进行基本认证。这个过程可以看作如下:

>>> from requests.auth import HTTPBasicAuth
>>> requests.get('https://demo.example.com/resource/path', auth=HTTPBasicAuth('user-ID', 'password'))

在前面的代码行中,我们通过创建一个HTTPBasicAuth对象来执行基本认证;然后将其传递给auth参数,该参数将被提交到服务器。如果提交的凭据认证成功,服务器将返回一个200(成功)响应,否则,它将返回一个401(未授权)响应。

摘要认证

摘要认证是众所周知的一种 HTTP 认证方案,它被引入以克服基本认证的大部分缺点。这种认证方式与基本认证类似,都使用用户 ID密码,但主要区别在于凭证传输到服务器时的过程。

摘要认证通过在加密加密概念上更进一步,增加了凭证的安全性。当用户提交密码以进行身份验证时,浏览器将对其应用 MD5 散列方案。这个过程的核心在于在加密密码时使用 nonce 值(伪随机数),这减少了重放攻击。

摘要认证

这种认证方式变得更加强大,因为在这个加密过程中,密码不是以纯文本形式使用。在摘要认证中使用 nonce(一次性随机数),使得密码散列的破解变得困难,从而抵消了选择纯文本攻击。

即使摘要认证克服了基本认证的大部分缺点,但它确实存在一些不足。这种认证方案容易受到中间人攻击。它降低了将密码存储在密码数据库中的灵活性,因为所有设计良好的密码数据库都使用其他加密方法来存储密码。

使用 Requests 库进行摘要认证

使用requests库进行摘要认证非常简单。让我们看看它是如何实现的:

>>> from requests.auth import HTTPDigestAuth
>>> requests.get('https://demo.example.com/resource/path', auth=HTTPDigestAuth('user-ID', 'password'))

在前面的代码行中,我们通过创建一个HTTPDigestAuth对象并将其设置为'auth'参数,该参数将被提交到服务器,从而执行了摘要认证。如果提交的凭据认证成功,服务器将返回一个200响应,否则,它将返回一个401响应。

Kerberos 认证

Kerberos 是一种网络身份验证协议,它使用密钥加密技术来在客户端和服务器之间进行通信。它是在麻省理工学院开发的,旨在缓解许多安全问题,如重放攻击和间谍活动。它利用票据为服务器端资源提供身份验证。它遵循避免额外登录(单点登录)和将密码存储在集中位置的理念。

简而言之,认证服务器、票据授权服务器和主机机作为认证过程中的主要角色。

  • 认证服务器:一种服务器端应用程序,通过使用用户提交的凭据来辅助认证过程

  • 票据授权服务器:一个验证票据的逻辑密钥分发中心KDC

  • 主机: 接受请求并提供资源的服务器

你可以在以下图表中看到这一点:

Kerberos 认证

Kerberos 认证过程如下:

  1. 当一个人使用凭证登录他的机器时,将发送一个请求到票据授权票据TGT)。

  2. 如果用户验证结果为真,从用户数据库中进行检查后,认证服务器(AS)将创建一个会话密钥和一个 TGT。

  3. 因此,获得的 TGT 和会话密钥将以两条消息的形式发送回用户,其中 TGT 将使用授予服务器的密钥的票据进行加密。会话密钥将使用客户端的密钥进行加密,并包含时间戳、有效期、TGS 名称和 TGS 会话密钥。

  4. 对端的用户,在收到两条消息后,使用客户端密钥,即用户的密码来解密会话密钥的消息。没有 TGS 密钥,无法解密 TGT。

  5. 使用可用的会话密钥和 TGT,用户可以发送请求以访问服务。请求包含两条消息和一些信息。在这两条消息中,一条是加密消息,包含用户 ID 和时间戳。另一条是解密消息,包含 HTTP 服务名称和票据的有效期。通过上述两条消息,认证器和 TGT 将被发送到票据授权服务器。

  6. 消息和信息(身份验证器和 TGT)将由 TGS 接收,并且它将检查来自 KDC 数据库的 HTTP 服务的可信度,并解密身份验证器和 TGT。一旦一切顺利,TGS 将尝试验证一些重要部分,如客户端 ID、时间戳、TGT 和身份验证器的有效期。如果验证成功,则 TGS 生成加密的 HTTP 服务票据、HTTP 服务名称、时间戳、票据有效信息以及 HTTP 服务的会话密钥。所有这些都将由 HTTP 服务会话密钥加密,并返回给用户。

  7. 现在,用户接收信息并使用在早期步骤中收到的 TGS 会话密钥对其进行解密。

  8. 在下一步中,为了访问 HTTP 服务,用户将加密的 HTTP 服务票据和一个用 HTTP 服务会话密钥加密的验证器发送到 HTTP 服务。HTTP 服务使用其密钥解密票据并掌握 HTTP 服务会话密钥。通过获取的 HTTP 服务会话密钥,它解密验证器并验证客户端 ID 时间戳、票据有效期等。

  9. 如果验证结果成功,HTTP 服务会发送一个包含其 ID 和时间的认证器消息,以确认其身份给用户。用户的机器通过使用 HTTP 服务会话密钥来验证认证器,并识别用户为访问 HTTP 服务的已认证用户。从那时起,用户可以无障碍地访问 HTTP 服务,直到会话密钥过期。

Kerberos 是一种安全的协议,因为用户的密码永远不会以明文形式发送。由于认证过程是在客户端和服务器通过加密和解密达成一致的情况下进行的,因此在某种程度上它变得难以破解。另一个优点来自于它能够允许用户在会话密钥过期之前无需重新输入密码即可访问服务器。

Kerberos 确实存在一些缺点:

  • 服务器必须持续可用以验证门票,如果服务器宕机,可能会导致阻塞。

  • 用户密钥保存在中央服务器上。该服务器的安全漏洞可能会危及整个基础设施的安全性。

  • Kerberos 需要大量的基础设施,这意味着一个简单的 Web 服务器是不够的。

  • Kerberos 的设置和管理需要专业的技能。

使用 Requests 与 Kerberos 认证

Requests 库为了实现认证功能,支持requests-kerberos库。因此,我们首先应该安装requests-kerberos模块。

>>> pip install 'requests-kerberos'

让我们来看看语法:

>>> import requests
>>> from requests.kerberos import HTTPKerberosAuth
>>> requests.get('https://demo.example.com/resource/path', auth=HTTTPKerberosAuth())

在前面的代码行中,我们通过创建一个HTTPKerberosAuth对象并将其设置为auth参数,该参数将被提交到服务器,来执行 Kerberos 身份验证。

OAuth 身份验证

OAuth 是一种开放标准的授权协议,它允许客户端应用程序对第三方服务(如 Google、Twitter、GitHub 等)上的用户账户进行安全的委托访问。在本主题中,我们将介绍两个版本:- OAuth 1.0 和 OAuth 2.0。

OAuth 1.0

OAuth 身份验证协议提出了一种减轻密码使用的方法,通过应用程序之间的 API 调用实现安全的握手来替代密码。这一想法是由一群受 OpenID 启发的网络开发者所开发的。

这里列出了 OAuth 认证过程中使用的关键术语。

  • 消费者:能够进行认证请求的 HTTP 客户端

  • 服务提供商: 处理 OAuth 请求的 HTTP 服务器

  • 用户:拥有对 HTTP 服务器上受保护资源控制权的人

  • 消费者密钥和密钥: 具有认证和授权请求能力的标识符

  • 请求令牌和密钥:用于从用户那里获取授权的凭证

  • 访问令牌和密钥:获取用户受保护资源所需的凭证

你可以在以下图表中看到这一点:

OAuth 1.0

初始时,客户端应用程序请求服务提供者授予请求令牌。可以通过验证请求令牌的信誉来识别用户为已批准用户。这也有助于获取访问令牌,客户端应用程序可以使用该令牌访问服务提供者的资源。

在第二步,服务提供商接收请求并发放请求令牌,该令牌将被发送回客户端应用程序。随后,用户在之前接收到的请求令牌作为参数的情况下,被重定向到服务提供商的授权页面。

在下一步中,用户授权使用消费者应用程序。现在,服务提供商将用户返回到客户端应用程序,应用程序接受一个授权请求令牌并返回一个访问令牌。使用访问令牌,用户将获得对应用程序的访问权限。

使用 Requests 进行 OAuth 1.0 身份验证

requests_oauthlib 是一个可选的 oauth 库,它不包括在 Requests 模块中。因此,我们应该单独安装 requests_oauthlib

让我们来看看语法:

>>> import requests
>>> from requests_oauthlib import OAuth1
>>> auth = OAuth1('<consumer key>', '<consumer secret>',
...               '<user oauth token>', '<user oauth token secret>')
>>> requests.get('https://demo.example.com/resource/path', auth=auth)

OAuth 2.0

OAuth 2.0 是继 OAuth 1.0 之后的下一个版本,它被开发出来以克服其前辈的缺点。在现代,OAuth 2.0 几乎被所有领先的互联网服务广泛使用。由于其使用简便且安全性更高,它吸引了很多人。OAuth 2.0 的美在于其简洁性以及为不同类型的应用程序(如网页、移动和桌面)提供特定授权方法的能力。

基本上,在使用 OAuth 2.0 时,有四种可用的工作流程,这些工作流程也被称为授权类型。它们是:

  1. 授权码授权:这基本上用于 Web 应用程序,以简化授权和安全的资源委派。

  2. 隐式授权:此流程用于在移动应用程序中提供 OAuth 授权。

  3. 资源所有者密码凭证授权:此类授权用于使用受信任客户端的应用程序。

  4. 客户端凭证授权:此类授权用于机器之间的身份验证。关于授权类型的深入解释超出了本书的范围。

OAuth 2.0 提出了能够克服 OAuth 1.0 关注的问题的功能。使用签名来验证 API 请求可信度的过程在 OAuth 2.0 中已被使用 SSL 所取代。它提出了支持不同类型流程以适应不同环境的思想,从网页到移动应用程序等。此外,还引入了刷新令牌的概念,以增加安全性。

让我们来看看用法:

>>> from requests_oauthlib import OAuth2Session
>>> client = OAuth2Session('<client id>', token='token')
>>> resp = client.get('https://demo.example.com/resource/path')

自定义认证

Requests 还提供了根据用户需求和灵活性编写新或自定义认证的能力。它配备了 requests.auth.AuthBase 类,这是所有认证类型的基类。这可以通过在 requests.auth.AuthBase__call__() 方法中实现自定义认证来实现。

让我们来看看它的语法:

>>> import requests
>>> class CustomAuth(requests.auth.AuthBase):
...     def __call__(self, r):
...         # Custom Authentication Implemention
...         return r
...
>>> requests.get('https://demo.example.com/resource/path',
... auth=CustomAuth())

摘要

在本章中,我们了解了 Requests 支持的多种认证类型,如基本认证、摘要认证、Kerberos 认证、OAuth 1.0 认证和 OAuth 2.0 认证。随后,我们获得了如何使用各种类型认证以及认证流程的概览。我们还学习了如何使用自定义认证,并掌握了不同认证与 Requests 结合使用的方法以及使用技巧。

在下一章中,我们将了解一个实用的模块,HTTPretty

第四章:使用 HTTPretty 模拟 HTTP 请求

使用 Requests 模块,我们获得了打开 URL、发送数据以及从网络服务获取数据的手段。让我们以构建一个应用程序的实例来举例,该应用程序使用 RESTful API,不幸的是,服务器运行的 API 出现了故障。尽管我们通过 Requests 实现了与网络的交互,但这次我们失败了,因为我们从服务器端没有得到任何响应。这种状况可能会让我们感到恼火并阻碍我们的进展,因为我们找不到进一步测试我们代码的方法。

因此,产生了创建一个 HTTP 请求模拟工具的想法,它可以模拟客户端的 Web 服务器来为我们提供服务。即使 HTTPretty 与 Requests 没有直接连接,我们仍然希望介绍一个模拟工具,以帮助我们在之前提到的情况下。

注意事项

HTTP 模拟工具通过伪造请求来模拟网络服务。

我们在本章中将探讨以下主题:

  • 理解 HTTPretty

  • 安装 HTTPretty

  • 详细用法

  • 设置标题

  • 与响应一起工作

理解 HTTPretty

HTTPretty 是一个用于 Python 的 HTTP 客户端模拟库。HTTPretty 的基本理念受到了 Ruby 社区中广为人知的 FakeWeb 的启发,它通过模拟请求和响应来重新实现 HTTP 协议。

本质上,HTTPretty 在套接字级别上工作,这使得它具有与大多数 HTTP 客户端库协同工作的内在优势,并且它特别针对像 Requestshttplib2urlib2 这样的 HTTP 客户端库进行了实战测试。因此,我们可以毫无困难地模拟我们的请求库中的交互。

这里是 HTTPretty 提供帮助的两个案例:

  • API 服务器宕机的情况

  • API 内容发生变化的条件

安装 HTTPretty

我们可以轻松地从Python 软件包索引PyPi)安装 HTTPretty。

pip install HTTPretty

在熟悉 HTTPretty 的过程中,我们将通过实例学习到更多内容;在这个过程中,我们将使用 mock、sure 以及显然的 Requests 等库。现在,让我们开始这些安装:

>>> pip install requests sure mock

让我们一探究竟,看看那些包具体处理了什么:

  • mock: 它是一个测试库,允许我们用模拟对象替换被测试系统中的部分组件

  • sure: 它是一个用于进行断言的 Python 库

使用 HTTPretty

处理 HTTPretty 时需要遵循三个主要步骤:

  1. 启用 HTTPretty

  2. 将统一资源定位符注册到 HTTPretty

  3. 禁用 HTTPretty

我们应该最初启用 HTTPretty,这样它就会应用猴子补丁;也就是说,动态替换套接字模块的属性。我们将使用register_uri函数来注册统一资源定位符。register_uri函数接受classuribody作为参数:

 method: register_uri(class, uri, body)

在我们的测试过程结束时,我们应该禁用 HTTPretty,以免它改变其他组件的行为。让我们通过一个示例来看看如何使用 HTTPretty:

import httpretty
import requests
from sure import expect

def example():
 httpretty.enable()
 httpretty.register_uri(httpretty.GET, "http://google.com/",
 body="This is the mocked body",
 status=201)
 response = requests.get("http://google.com/")
 expect(response.status_code).to.equal(201)
 httpretty.disable()

在这个例子中,我们使用了httpretty.GET类在register_uri函数中注册了uri值为"http://google.com/"。在下一行,我们使用 Request 从 URI 获取信息,然后使用 expect 函数断言预期的状态码。总的来说,前面的代码试图模拟 URI 并测试我们是否得到了预期的相同状态码。

我们可以使用装饰器来简化前面的代码。正如第一步和第三步,即启用和禁用 HTTPretty 始终相同,我们可以使用装饰器,这样那些函数就可以在我们需要它们出现的时候被包装起来。装饰器看起来是这样的:@httpretty.activate。之前的代码示例可以用以下方式使用装饰器重写:

import httpretty
import requests

from sure import expect

@httpretty.activate
def example():
 httpretty.register_uri(httpretty.GET, "http://google.com/",
 body="This is the mocked body",
 status=201)
 response = requests.get("http://google.com/")
 expect(response.status_code).to.equal(201)

设置标题

HTTP 头部字段提供了关于请求或响应的必要信息。我们可以通过使用 HTTPretty 来模拟任何 HTTP 响应头部。为了实现这一点,我们将它们作为关键字参数添加。我们应该记住,关键字参数的键始终是小写字母,并且使用下划线(_)而不是破折号(-)。

例如,如果我们想模拟返回 Content-Type 的服务器,我们可以使用content_type参数。请注意,在下面的部分,我们使用一个不存在的 URL 来展示语法:

import httpretty
import requests

from sure import expect

@httpretty.activate
def setting_header_example():
 httpretty.register_uri(httpretty.GET,
 "http://api.example.com/some/path",
 body='{"success": true}',
 status=200,
 content_type='text/json')

 response = requests.get("http://api.example.com/some/path")

 expect(response.json()).to.equal({'success': True})
 expect(response.status_code).to.equal(200)

同样,HTTPretty 会接收所有关键字参数并将其转换为 RFC2616 的等效名称。

与响应一起工作

当我们使用 HTTPretty 模拟 HTTP 请求时,它会返回一个httpretty.Response对象。我们可以通过回调函数生成以下响应:

  • 旋转响应

  • 流式响应

  • 动态响应

旋转响应

旋转响应是我们向服务器发送请求时,按照给定顺序收到的响应。我们可以使用响应参数定义我们想要的任意数量的响应。

以下片段解释了旋转响应的模拟过程:

import httpretty
import requests

from sure import expect

@httpretty.activate
def rotating_responses_example():
 URL = "http://example.com/some/path"
 RESPONSE_1 = "This is Response 1."
 RESPONSE_2 = "This is Response 2."
 RESPONSE_3 = "This is Last Response."

 httpretty.register_uri(httpretty.GET,
 URL,
 responses=[
 httpretty.Response(body=RESPONSE_1,
 status=201),
 httpretty.Response(body=RESPONSE_2,
 status=202),
 httpretty.Response(body=RESPONSE_3,
 status=201)])

 response_1 = requests.get(URL)
 expect(response_1.status_code).to.equal(201)
 expect(response_1.text).to.equal(RESPONSE_1)

 response_2 = requests.get(URL)
 expect(response_2.status_code).to.equal(202)
 expect(response_2.text).to.equal(RESPONSE_2)

 response_3 = requests.get(URL)
 expect(response_3.status_code).to.equal(201)
 expect(response_3.text).to.equal(RESPONSE_3)

 response_4 = requests.get(URL)
 expect(response_4.status_code).to.equal(201)
 expect(response_4.text).to.equal(RESPONSE_3)

在这个例子中,我们使用httpretty.register_uri方法通过responses参数注册了三种不同的响应。然后,我们向服务器发送了四个不同的请求,这些请求具有相同的 URI 和相同的方法。结果,我们按照注册的顺序收到了前三个响应。从第四个请求开始,我们将获得responses对象中定义的最后一个响应。

流式响应

流式响应将不会包含Content-Length头部。相反,它们有一个值为chunkedTransfer-Encoding头部,以及由一系列数据块组成的内容体,这些数据块由它们各自的大小值 precede。这类响应也被称为分块响应

我们可以通过注册一个生成器响应体来模拟一个 Streaming 响应:

import httpretty
import requests
from time import sleep
from sure import expect

def mock_streaming_repos(repos):
 for repo in repos:
 sleep(.5)
 yield repo

@httpretty.activate
def streaming_responses_example():
 URL = "https://api.github.com/orgs/python/repos"
 REPOS = ['{"name": "repo-1", "id": 1}\r\n',
 '\r\n',
 '{"name": "repo-2", "id": 2}\r\n']

 httpretty.register_uri(httpretty.GET,
 URL,
 body=mock_streaming_repos(REPOS),
 streaming=True)

 response = requests.get(URL,
 data={"track": "requests"})

 line_iter = response.iter_lines()
 for i in xrange(len(REPOS)):
 expect(line_iter.next().strip()).to.equal(REPOS[i].strip())

为了模拟流式响应,我们需要在注册uri时将流式参数设置为True。在之前的示例中,我们使用生成器mock_streaming_repos来模拟流式响应,该生成器将列表作为参数,并且每半秒产生列表中的一个项目。

通过回调函数实现动态响应

如果 API 服务器的响应是根据请求的值生成的,那么我们称之为动态响应。为了根据请求模拟动态响应,我们将使用以下示例中定义的回调方法:

import httpretty
import requests

from sure import expect

@httpretty.activate
def dynamic_responses_example():
 def request_callback(method, uri, headers):
 return (200, headers, "The {} response from {}".format(method, uri)
 httpretty.register_uri(
 httpretty.GET, "http://example.com/sample/path",
 body=request_callback)

 response = requests.get("http://example.com/sample/path")

 expect(response.text).to.equal(' http://example.com/sample/path')

在此示例中,在模拟响应时注册了request_callback方法,以便生成动态响应内容。

摘要

在本章中,我们学习了与 HTTPretty 相关的基本概念。我们了解了 HTTPretty 是什么,以及为什么我们需要 HTTPretty。我们还详细介绍了模拟库的使用,设置头部信息和模拟不同类型的响应。这些主题足以让我们开始并保持进展。

在下一章中,我们将学习如何使用 requests 库与社交网络如 Facebook、Twitter 和 Reddit 进行交互。

第五章. 使用 Requests 与社交媒体交互

在这个当代世界中,我们的生活与社交媒体的互动和协作紧密相连。网络上的信息非常宝贵,并且被大量资源所利用。例如,世界上的热门新闻可以通过 Twitter 标签轻松找到,这可以通过与 Twitter API 的交互来实现。

通过使用自然语言处理,我们可以通过抓取账户的 Facebook 状态来分类一个人的情绪。所有这些都可以通过使用 Requests 和相关的 API 轻松完成。如果我们要频繁地调用 API,Requests 是一个完美的模块,因为它几乎支持所有功能,如缓存、重定向、代理等等。

我们在本章中将涵盖以下主题:

  • 与 Twitter 互动

  • 与 Facebook 互动

  • 与 Reddit 互动

API 简介

在深入细节之前,让我们快速了解一下应用程序编程接口API)究竟是什么。

网络 API 是一套规则和规范。它帮助我们与不同的软件进行通信。API 有不同类型,而本例中讨论的是 REST API。表征状态转移REST)是一种包含构建可扩展网络服务指南的架构。遵循这些指南并符合 REST 约束的 API 被称为RESTful API。简而言之,约束包括:

  • 客户端-服务器

  • 无状态

  • 可缓存

  • 分层系统

  • 统一接口

  • 按需编码

Google Maps API、Twitter API 和 GitHub API 是各种 RESTful API 的示例。

让我们更深入地了解 API。以获取带有“worldtoday”标签的所有 Twitter 推文为例,这包括认证过程、向不同 URL 发送请求并接收响应,以及处理不同的方法。所有这些过程和步骤都将由 Twitter 的 API 指定。通过遵循这些步骤,我们可以与网络顺利协作。

Twitter API 入门

要开始使用 Twitter API,我们首先需要获取一个 API 密钥。这是一个在调用 API 时由计算机程序传递的代码。API 密钥的基本目的是它能够唯一地识别它试图与之交互的程序。它还通过其令牌在我们进行身份验证的过程中为我们提供服务。

下一步涉及创建一个认证请求的过程,这将使我们能够访问 Twitter 账户。一旦我们成功认证,我们将可以自由地处理推文、关注者、趋势、搜索等内容。让我们来了解一下需要遵循的步骤。

注意

请注意,在所有示例中,我们将使用 Twitter API 1.1 版本。

获取 API 密钥

获取 API 密钥非常简单。您需要遵循以下章节中规定的步骤:

  1. 首先,您需要使用您的 Twitter 凭证登录到页面apps.twitter.com/

  2. 点击创建新应用按钮。

  3. 现在,您需要填写以下字段以设置新的应用程序:

    • 名称:指定您的应用程序名称。这用于归因于推文的来源以及在面向用户的授权屏幕中。

    • 描述:输入您应用的简短描述。当用户面对授权界面时,将显示此描述。

    • 网站: 指定您的完整网站 URL。一个完整的 URL 包括 http://或 https://,并且末尾不会带有斜杠(例如:http://example.comhttp://www.example.com)。

    • 回调 URL:此字段回答了问题——在成功认证后我们应该返回哪里。

    • 开发者协议:仔细阅读开发者协议,然后勾选是,我同意

  4. 现在,通过点击创建您的 Twitter 应用,将为我们创建一个包含之前指定详情的新应用。

  5. 成功创建后,我们将被重定向到一个页面,其中默认选中了详情标签页。现在,请选择密钥和访问令牌标签页。我们应该点击创建我的访问令牌按钮来生成我们的访问令牌。

  6. 最后,记下消费者密钥(API 密钥)消费者密钥(API 密钥)访问令牌访问令牌密钥

创建一个认证请求

如果我们还记得第三章的主题,我们学习了使用 requests 进行不同类型的身份验证,例如基本身份验证、摘要身份验证和 OAuth 身份验证。现在是时候将这些知识应用到实际中了!

现在,我们将使用 OAuth1 认证来获取访问 Twitter API 的权限。在获取密钥的第一步中,我们获得了消费者密钥、消费者密钥密钥、访问令牌和访问令牌密钥,现在我们应该使用它们来验证我们的应用程序。以下命令展示了我们如何完成这个过程:

>>> import requests
>>> from requests_oauthlib import OAuth1
>>> CONSUMER_KEY = 'YOUR_APP_CONSUMER_KEY'
>>> CONSUMER_SECRET = 'YOUR_APP_CONSUMER_SECRET'
>>> ACCESS_TOKEN = 'YOUR_APP_ACCESS_TOKEN'
>>> ACCESS_TOKEN_SECRET = 'YOUR_APP_ACCESS_TOKEN_SECRET'

>>> auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET,
...               ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

在前面的行中,我们已经将我们的密钥和令牌发送到 API,并完成了身份验证,并将它们存储在变量auth中。现在,我们可以使用这个变量进行各种与 API 的交互。让我们开始与 Twitter API 进行交互。

注意事项

请记住,在此之后展示的所有推特互动示例都将使用上一节中获得的“auth”值。

获取你喜欢的推文

首先,让我们获取认证用户的几个喜欢的推文。为此,我们应该向 Twitter API 发送请求以访问喜欢的推文。可以通过指定参数通过资源 URL发送请求。获取喜欢的列表的资源 URL看起来像这样:

https://api.twitter.com/1.1/favorites/list.json

我们还可以向 URL 发送一些可选参数,如user_idscreen_namecountsince_idmax_idinclude_identities,以满足我们的需求。现在让我们获取一条喜欢的推文。

>>> favorite_tweet = requests.get('https://api.twitter.com/1.1/favorites/list.json?count=1', auth=auth)
>>> favorite_tweet.json()
[{u'contributors': None, u'truncated': False, u'text': u'India has spent $74 mil to reach Mars. Less than the budget of the film \u201cGravity,\u201d $100 million.\n\n#respect\n#ISRO\n#Mangalyaan', u'in_reply_to_status_id': None, …}]

在第一步中,我们向资源 URL 发送了一个带有参数count和认证authget请求。在下一步中,我们访问了以 JSON 格式返回的响应,其中包含了我最喜欢的推文,就这么简单。

由于我们在请求中指定了计数参数为1,我们偶然看到了一条喜欢的推文的结果。默认情况下,如果我们没有指定可选参数count,请求将返回20条最近的喜欢的推文。

执行简单搜索

我们现在将使用 Twitter 的 API 进行搜索。为此,我们将利用 Twitter 的Search API。搜索的基本 URL 结构具有以下语法:

https://api.twitter.com/1.1/search/tweets.json?q=%40twitterapi

它还增加了额外的参数,如结果类型地理位置语言在结果集中迭代

>>> search_results = requests.get('https://api.twitter.com/1.1/search/tweets.json?q=%40python', auth=auth)
>>> search_results.json().keys()
[u'search_metadata', u'statuses']
>>> search_results.json()["search_metadata"]
{u'count': 15, u'completed_in': 0.022, u'max_id_str': u'529975076746043392', u'since_id_str': u'0', u'next_results': u'?max_id=527378999857532927&q=%40python&include_entities=1', u'refresh_url': u'?since_id=529975076746043392&q=%40python&include_entities=1', u'since_id': 0, u'query': u'%40python', u'max_id': 529975076746043392}

在前面的例子中,我们尝试搜索包含单词python的推文。

访问关注者列表

让我们访问指定用户的关注者。默认情况下,当我们查询关注者列表时,它返回最近的20位关注用户。资源 URL 看起来像这样:

https://api.twitter.com/1.1/followers/list.json

它返回指定用户关注的用户对象的带光标集合:

>>> followers = requests.get('https://api.twitter.com/1.1/followers/list.json', auth=auth)
>>> followers.json().keys()
[u'previous_cursor', u'previous_cursor_str', u'next_cursor', u'users', u'next_cursor_str']
>>> followers.json()["users"]
[{u'follow_request_sent': False, u'profile_use_background_image': True, u'profile_text_color': u'333333'... }]

转发

被转发过的推文称为转发推文。要访问由认证用户创建的最新转发推文,我们将使用以下网址:

https://api.twitter.com/1.1/statuses/retweets_of_me.json

可以与其一起发送的可选参数有 countsince_idmax_idtrim_userinclude_entitiesinclude_user_entities

>>> retweets = requests.get('https://api.twitter.com/1.1/statuses/retweets_of_me.json', auth=auth)
>>> len(retweets.json())
16
>>> retweets.json()[0]
{u'contributors': None, u'text': u'I\u2019m now available to take on new #python #django #freelance projects. Reply for more details!', {u'screen_name': u'vabasu', ...}}

访问可用趋势

Twitter 的热门话题是由标签驱动的特定时间内的主题。以获取 Twitter 中可用趋势的位置为例。为此,我们将使用以下网址:

https://api.twitter.com/1.1/trends/available.json

资源 URL 的响应是一个以编码形式表示的位置数组:

>>> available_trends = requests.get('https://api.twitter.com/1.1/trends/available.json', auth=auth)
>>> len(available_trends.json())
467
>>> available_trends.json()[10]
{u'name': u'Blackpool', u'countryCode': u'GB', u'url': u'http://where.yahooapis.com/v1/place/12903', u'country': u'United Kingdom', u'parentid': 23424975, u'placeType': {u'code': 7, u'name': u'Town'}, u'woeid': 12903}

在前面的代码行中,我们搜索了available_trends的位置。然后,我们了解到拥有available_trends的位置数量是467。后来,我们尝试访问第十个位置的数据,结果返回了一个包含位置信息的响应,该信息是用woeid编码的。这是一个称为Where on Earth ID的唯一标识符。

更新用户状态

为了更新认证用户的当前状态,这通常被称为发推文,我们遵循以下程序。

对于每次更新尝试,更新文本将与认证用户的最近推文进行比较。任何可能导致重复的尝试都将被阻止,从而导致403 错误。因此,用户不能连续两次提交相同的状态。

>>> requests.post('https://api.twitter.com/1.1/statuses/update.json?status=This%20is%20a%20Tweet', auth=auth)

与 Facebook 互动

Facebook API 平台帮助我们这样的第三方开发者创建自己的应用程序和服务,以便访问 Facebook 上的数据。

让我们使用 Facebook API 来绘制 Facebook 数据。Facebook 提供了两种类型的 API;即 Graph API 和 Ads API。Graph API 是一个 RESTful JSON API,通过它可以访问 Facebook 的不同资源,如状态、点赞、页面、照片等。Ads API 主要处理管理对广告活动、受众等访问的权限。

在本章中,我们将使用 Facebook Graph API 与 Facebook 进行交互。它以节点和边的方式命名,表示其表示方式。节点代表事物,这意味着一个用户、一张照片、一个页面;而边则代表事物之间的连接;即页面的照片、照片的评论。

注意事项

本节中的所有示例都将使用 Graph API 版本 2.2

开始使用 Facebook API

要开始使用 Facebook API,我们需要一个被称为访问令牌的不透明字符串,该字符串由 Facebook 用于识别用户、应用或页面。其后是获取密钥的步骤。我们将几乎向 API 发送所有请求到 graph.facebook.com,除了视频上传相关的内容。发送请求的流程是通过使用以下方式中的节点唯一标识符来进行的:

GET graph.facebook.com/{node-id}

同样地,我们可以这样进行 POST 操作:

POST graph.facebook.com/{node-id}

获取一个密钥

Facebook API 的令牌是可移植的,可以从移动客户端、网页浏览器或服务器进行调用。

有四种不同类型的访问令牌:

  • 用户访问令牌:这是最常用的一种访问令牌,需要用户的授权。此令牌可用于访问用户信息和在用户的动态时间轴上发布数据。

  • 应用访问令牌:当在应用级别处理时,这个令牌就会出现。这个令牌并不能帮助获取用户的访问权限,但它可以用来读取流。

  • 页面访问令牌:此令牌可用于访问和管理 Facebook 页面。

  • 客户端令牌:此令牌可以嵌入到应用程序中以获取对应用级 API 的访问权限。

在本教程中,我们将使用应用访问令牌,该令牌由应用 ID 和应用密钥组成,以获取对资源的访问权限。

按照以下步骤获取应用访问令牌:

  1. 使用位于developers.facebook.com/developer-console/的 Facebook 开发者控制台创建一个应用程序。请注意,我们应该登录到developers.facebook.com,以便我们能够获得创建应用程序的权限。

  2. 一旦我们完成了应用程序的创建,我们就可以在我们的developers.facebook.com账户的应用程序页面上获取 App Id 和 App Secret 的访问权限。

就这些;获取密钥就这么简单。我们不需要创建任何认证请求来发送消息,与 Twitter 上的情况不同。App ID 和 App Secret 就足以赋予我们访问资源的权限。

获取用户资料

我们可以使用 API URL https://graph.facebook.com/me 通过 GET 请求访问已登录网站的人的当前用户资料。在通过 requests 使用任何 Graph API 调用时,我们需要传递之前获得的访问令牌作为参数。

首先,我们需要导入 requests 模块,然后我们必须将访问令牌存储到一个变量中。这个过程按照以下方式进行:

>>> import requests
>>> ACCESS_TOKEN = '231288990034554xxxxxxxxxxxxxxx'

在下一步,我们应该以以下方式发送所需的图形 API 调用:

>>> me = requests.get("https://graph.facebook.com/me", params={'access_token': ACCESS_TOKEN})

现在,我们有一个名为 merequests.Response 对象。me.text 返回一个 JSON 响应字符串。要访问检索到的用户配置文件中的各种元素(例如,idnamelast_namehometownwork),我们需要将 json response 字符串转换为 json object 字符串。我们可以通过调用方法 me.json() 来实现这一点。me.json.keys() 返回字典中的所有键:

>>> me.json().keys()
[u'website', u'last_name', u'relationship_status', u'locale', u'hometown', u'quotes', u'favorite_teams', u'favorite_athletes', u'timezone', u'education', u'id', u'first_name', u'verified', u'political', u'languages', u'religion', u'location', u'username', u'link', u'name', u'gender', u'work', u'updated_time', u'interested_in']

用户的id是一个唯一的数字,用于在 Facebook 上识别用户。我们可以通过以下方式从用户资料中获取当前资料 ID。在后续的示例中,我们将使用此 ID 来检索当前用户的友人、动态和相册。

>>> me.json()['id']
u'10203783798823031'
>>> me.json()['name']
u'Bala Subrahmanyam Varanasi'

获取朋友列表

让我们收集特定用户的好友列表。为了实现这一点,我们应该向https://graph.facebook.com/<user-id>/friends发起 API 调用,并将user-id替换为用户的 ID 值。

现在,让我们获取在前一个示例中检索到的用户 ID 的朋友列表:

>>> friends = requests.get("https://graph.facebook.com/10203783798823031/friends", params={'access_token': ACCESS_TOKEN})
>>> friends.json().keys()
[u'paging', u'data']

API 调用的响应包含一个 JSON 对象字符串。朋友的信息存储在response json对象的data属性中,这是一个包含朋友 ID 和名称作为键的朋友对象列表。

>>> len(friends.json()['data'])
32
>>> friends.json().keys()
[u'paging', u'data']
>>> friends.json()['data'][0].keys()
[u'name', u'id']

获取推送内容

为了检索包括当前用户或他人发布在当前用户个人资料中的状态更新和链接的帖子流,我们应该在请求中使用 feed 参数。

>>> feed = requests.get("https://graph.facebook.com/10203783798823031/feed", params={'access_token': ACCESS_TOKEN})
>>> feed.json().keys()
[u'paging', u'data']
>>> len(feed.json()["data"])
24
>>> feed.json()["data"][0].keys()
[u'from', u'privacy', u'actions', u'updated_time', u'likes', u'created_time', u'type', u'id', u'status_type']

在前面的例子中,我们发送了一个请求以获取具有用户 ID 10203783798823031的特定用户的动态。

检索专辑

让我们访问当前登录用户创建的相册。这可以通过以下方式实现:

>>> albums = requests.get("https://graph.facebook.com/10203783798823031/albums", params={'access_token': ACCESS_TOKEN})
>>> albums.json().keys()
[u'paging', u'data']
>>> len(albums.json()["data"])
13
>>> albums.json()["data"][0].keys()
[u'count', u'from', u'name', u'privacy', u'cover_photo', u'updated_time', u'link', u'created_time', u'can_upload', u'type', u'id']
>>> albums.json()["data"][0]["name"]
u'Timeline Photos'

在前面的例子中,我们向图 API 发送了一个请求,以获取具有user-id 10203783798823031的用户的专辑。然后我们尝试通过 JSON 访问响应数据。

与 Reddit 互动

Reddit 是一个流行的社交网络、娱乐和新闻网站,注册会员可以提交内容,例如文本帖子或直接链接。它允许注册用户对提交的内容进行“赞同”或“反对”的投票,以在网站页面上对帖子进行排名。每个内容条目都按兴趣领域分类,称为 SUBREDDITS

在本节中,我们将直接访问 reddit API,使用 Python 的 requests 库。我们将涵盖以下主题:对 reddit API 的基本概述、获取与我们自己的 reddit 账户相关的数据,以及使用搜索 API 检索链接。

开始使用 reddit API

Reddit API 由四个重要的部分组成,在开始与之交互之前,我们需要熟悉这四个部分。这四个部分是:

  1. 列表: Reddit 中的端点被称为列表。它们包含诸如 after/beforelimitcountshow 等参数。

  2. modhashes:这是一个用于防止跨站请求伪造CSRF)攻击的令牌。我们可以通过使用GET /api/me.json来获取我们的 modhash。

  3. fullnames: 全名是一个事物的类型和其唯一 ID 的组合,它构成了 Reddit 上全局唯一 ID 的紧凑编码。

  4. 账户: 这涉及到用户的账户。使用它我们可以注册、登录、设置强制 HTTPS、更新账户、更新电子邮件等等。

注册新账户

在 reddit 上注册新账户很简单。首先,我们需要访问 reddit 网站——www.reddit.com/,然后点击右上角的登录或创建账户链接,就会出现注册表单。注册表单包括:

  • 用户名:用于唯一标识 Reddit 社区成员

  • 电子邮件:用于直接与用户沟通的可选字段

  • 密码:登录 Reddit 平台的加密密码

  • 验证密码:此字段应与密码字段相同

  • 验证码: 此字段用于检查尝试登录的用户是真人还是可编程的机器人

让我们创建一个新账户,使用我们选择的用户名和密码。目前,请将电子邮件字段留空。我们将在下一节中添加它。

在以下示例中,我假设我们之前创建的用户名和密码分别是OUR_USERNAMEOUR_PASSWORD

修改账户信息

现在,让我们在我们的账户资料中添加一封电子邮件,这是我们在上一个部分创建账户时故意未完成的。

  1. 让我们从创建一个会话对象开始这个过程,它允许我们在所有请求中维护某些参数和 cookie。

    >>> import requests
    >>> client = requests.session()
    >>> client.headers = {'User-Agent': 'Reddit API - update profile'}
    
    
  2. 让我们创建一个具有'user''passwd''api type'属性的DATA属性。

    >>> DATA = {'user': 'OUR_USERNAME', 'passwd': 'OUR_PASSWORD', 'api type': 'json'}
    
    
  3. 我们可以通过向 URL 发起一个post请求调用来访问我们的 Reddit 账户——ssl.reddit.com/api/login,其中登录凭证存储在DATA属性中。

    >>> response = client.post('https://ssl.reddit.com/api/login', data=DATA)
    
    
  4. Reddit API 对上述帖子请求的响应将被存储在 response 变量中。response 对象包含 dataerrors 信息,如下例所示:

    >>> print response.json()
    {u'json': {u'errors': [], u'data': {u'need_https': False, u'modhash': u'v4k68gabo0aba80a7fda463b5a5548120a04ffb43490f54072', u'cookie': u'32381424,2014-11-09T13:53:30,998c473d93cfeb7abcd31ac457c33935a54caaa7'}}}
    
    
  5. 我们需要将前一个响应中获得的modhash值发送,以执行更新调用以更改我们的email。现在,让我们调用以下示例中的 reddit 更新 API:

    >>> modhash = response.json()['json']['data']['modhash']
    >>> update_params = {"api_type": "json", "curpass": "OUR_PASSWORD",
    ...                  "dest": "www.reddit.com", "email": "user@example.com",
    ...                  "verpass": "OUR_PASSWORD", "verify": True, 'uh': modhash}
    >>> r = client.post('http://www.reddit.com/api/update', data=update_params)
    
    
  6. 更新调用响应存储在 r 中。如果没有错误,则 status_code 将为 200errors 属性的值将是一个空列表,如下例所示:

    >>> print r.status_code
    200
    >>> r.text
    u'{"json": {"errors": []}}'
    
    
  7. 现在,让我们通过获取当前认证用户的详细信息来检查email字段是否已设置。如果has_mail属性为True,那么我们可以假设电子邮件已成功更新。

    >>> me = client.get('http://www.reddit.com/api/me.json')
    >>> me.json()['data']['has_mail']
    True
    
    

执行简单搜索

我们可以使用 Reddit 的搜索 API 来搜索整个网站或特定子版块。在本节中,我们将探讨如何发起一个搜索 API 请求。按照以下步骤进行,以发起一个搜索请求。

要进行搜索 API 调用,我们需要向http://www.reddit.com/search.json URL 发送一个带有搜索查询参数q的 GET 请求。

>>> search = requests.get('http://www.reddit.com/search.json', params={'q': 'python'})
>>> search.json().keys()
[u'kind', u'data']
>>> search.json()['data']['children'][0]['data'].keys()
[u'domain', u'author', u'media', u'score', u'approved_by', u'name', u'created', u'url', u'author_flair_text', u'title' ... ]

搜索响应存储在search变量中,它是一个requests.Response对象。搜索结果存储在data属性的children属性中。我们可以像以下示例中那样访问搜索结果中的titleauthorscore或其他项目:

>>> search.json()['data']['children'][0]['data']['title']
u'If you could change something in Python what would it be?'
>>> search.json()['data']['children'][0]['data']['author']
u'yasoob_python'
>>> search.json()['data']['children'][0]['data']['score']
146

搜索 subreddits

在 reddit 的子版块中通过标题和描述进行搜索与在 reddit 中进行搜索相同。为此,我们需要向http://www.reddit.com/search.json URL 发送一个带有搜索查询参数q的 GET 请求。

>>> subreddit_search = requests.get('http://www.reddit.com/subreddits/search.json', params={'q': 'python'})

搜索响应存储在search变量中,它是一个requests.Response对象。搜索结果存储在data属性中。

>>> subreddit_search.json()['data']['children'][0]['data']['title']
u'Python'

摘要

本章旨在指导您使用 Python 和 requests 库与一些最受欢迎的社交媒体平台进行交互。我们首先学习了在现实世界中 API 的定义和重要性。然后,我们与一些最受欢迎的社会化媒体网站,如 Twitter、Facebook 和 Reddit 进行了交互。每个关于社交网络的章节都将通过一组有限的示例提供实际操作经验。

在下一章,我们将逐步学习使用 requests 和 BeautifulSoup 库进行网络爬取。

第六章. 使用 Python Requests 和 BeautifulSoup 进行网页抓取

我们已经成为了如何通过 Requests 与网络进行通信的专家。在与 API 一起工作时,一切进展得非常热烈。然而,有些情况下我们需要注意 API 传说。

我们首先关注的问题是并非所有网络服务都为第三方客户构建了 API。此外,也没有法律规定 API 必须得到完美的维护。即使是像谷歌、Facebook 和 Twitter 这样的技术巨头,也倾向于在没有事先通知的情况下突然更改他们的 API。因此,我们最好理解,当我们从网络资源中寻找一些关键信息时,并不总是 API 会及时伸出援手。

网络爬虫这一概念在我们迫切需要从没有提供 API 的网页资源中获取信息时,就像一位救星。在本章中,我们将讨论如何遵循网络爬虫的所有原则,从网页资源中提取信息的技巧。

在我们开始之前,让我们了解一些重要的概念,这些概念将帮助我们实现目标。看看请求的响应内容格式,这将向我们介绍一种特定的数据类型:

>>> import requests
>>> r = requests.get("http://en.wikipedia.org/wiki/List_of_algorithms")
>>> r
<Response [200]>
>>> r.text
u'<!DOCTYPE html>\n<html lang="en" dir="ltr" class="client-nojs">\n<head>\n<meta charset="UTF-8" />\n<title>List of algorithms - Wikipedia, the free encyclopedia</title>\n...

在前面的例子中,响应内容以半结构化数据的形式呈现,使用 HTML 标签进行表示;这反过来又帮助我们分别访问网页不同部分的信息。

现在,让我们了解网络通常处理的不同类型的数据。

数据类型

在处理网络资源时,我们通常会遇到三种类型的数据。具体如下:

  • 结构化数据

  • 非结构化数据

  • 半结构化数据

结构化数据

结构化数据是一种以组织形式存在的数据类型。通常,结构化数据具有预定义的格式,并且是机器可读的。结构化数据中的每一份数据都与其它数据以特定格式相关联。这使得访问数据的不同部分更加容易和快捷。处理大量数据时,结构化数据类型有助于减少冗余数据。

数据库总是包含结构化数据,可以使用 SQL 技术来访问这些数据。我们可以将人口普查记录视为结构化数据的例子。它们包含关于一个国家人民出生日期、性别、地点、收入等信息。

非结构化数据

与结构化数据相比,非结构化数据要么缺少标准格式,要么即使施加了特定格式也保持无序。由于这个原因,处理数据的各个部分变得困难。此外,它变成了一项繁琐的任务。为了处理非结构化数据,使用了不同的技术,如文本分析、自然语言处理(NLP)和数据挖掘。图像、科学数据、内容繁多的文本(如报纸、健康记录等)都属于非结构化数据类型。

半结构化数据

半结构化数据是一种遵循不规则趋势或具有快速变化结构的数据类型。这种数据可以是自我描述的,它使用标签和其他标记来建立数据元素之间的语义关系。半结构化数据可能包含来自不同来源的信息。"抓取"是用于从这类数据中提取信息的技巧。网络上的信息是半结构化数据的完美例子。

什么是网络爬取?

简而言之,网络爬虫是从网络资源中提取所需数据的过程。这种方法涉及不同的步骤,如与网络资源交互、选择合适的数据、从数据中获取信息,以及将数据转换为所需格式。在考虑了所有之前的方法之后,主要关注点将集中在从半结构化数据中提取所需数据的过程。

网络爬取的注意事项与禁忌

爬取网络资源并不总是受到所有者的欢迎。一些公司会对使用针对他们的机器人进行限制。在爬取时遵循某些规则是一种礼仪。以下是一些关于网络爬取的应该做和不应该做的事情:

  • 请务必查阅条款和条件:在我们开始抓取数据之前,应该首先想到的是条款和条件。请访问网站的条款和条件页面,了解他们是否禁止从其网站抓取数据。如果是这样,最好是退而求其次。

  • 不要向服务器发送大量请求:每个网站都运行在只能处理特定工作量负载的服务器上。如果在特定时间段内向服务器发送大量请求,这相当于是一种无礼行为,可能会导致服务器崩溃。请等待一段时间后再发送请求,而不是一次性向服务器发送过多请求。

    注意事项

    一些网站对每分钟处理的最大请求数量有限制,如果不遵守这一规定,将会禁止请求发送者的 IP 地址。

  • 定期跟踪网络资源:一个网站并不总是保持不变。根据其可用性和用户需求,它们往往会不时地进行更改。如果网站有任何变动,我们用于抓取的代码可能会失效。请务必跟踪网站所做的更改,修改抓取脚本,并相应地进行抓取。

执行网络爬取的主要步骤

通常,网络爬取的过程需要使用以下不同的工具和库:

  • Chrome DevTools 或 FireBug 插件:这可以用来定位 HTML/XML 页面中的信息片段。

  • HTTP 库:这些库可以用来与服务器交互并获取响应文档。一个例子是python-requests

  • 网页抓取工具:这些工具用于从半结构化文档中提取数据。例如包括 BeautifulSoupScrappy

网络爬取的整体流程可以观察以下步骤:

  1. 确定执行网络爬取任务的网页资源的 URL(s)。

  2. 使用您喜欢的 HTTP 客户端/库来提取半结构化文档。

  3. 在提取所需数据之前,发现那些处于半结构化格式的数据片段。

  4. 使用网络爬虫工具将获取的半结构化文档解析成更结构化的形式。

  5. 绘制我们希望使用的所需数据。这就完成了!

关键网络爬取任务

在从半结构化文档中提取所需数据时,我们执行各种任务。以下是我们采用的抓取的基本任务:

  • 搜索半结构化文档: 在文档中访问特定元素或特定类型的元素可以通过使用其标签名称和标签属性,例如idclass等来实现。

  • 在半结构化文档中导航:我们可以通过四种方式在网页文档中导航以获取不同类型的数据,这包括向下导航、横向导航、向上导航以及来回导航。我们将在本章后面详细了解这些内容。

  • 修改半结构化文档: 通过修改文档的标签名称或标签属性,我们可以使文档更加简洁并提取所需的数据。

什么是 BeautifulSoup?

BeautifulSoup 库是一个简单而强大的网页抓取库。它能够在提供 HTML 或 XML 文档时提取所需数据。它配备了一些出色的方法,这些方法帮助我们轻松地执行网页抓取任务。

文档解析器

文档解析器帮助我们解析和序列化使用 HTML5、lxml 或其他任何标记语言编写的半结构化文档。默认情况下,BeautifulSoup拥有 Python 的标准的HTMLParser对象。如果我们处理的是不同类型的文档,例如 HTML5 和 lxml,我们需要明确地安装它们。

在本章中,我们的主要关注点将仅放在图书馆的特定部分,这些部分帮助我们理解开发将在本章末尾构建的实用爬虫的技术。

安装

安装 BeautifulSoup 非常简单。我们可以使用 pip 轻松地安装它:

$ pip install beautifulsoup4

每当我们打算使用 BeautifulSoup 抓取网页资源时,我们需要为它创建一个 BeautifulSoup 对象。以下是为此执行的命令:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(<HTML_DOCUMENT_STRING>)

BeautifulSoup 中的对象

BeautifulSoup 对象解析给定的 HTML/XML 文档,并将其转换为以下章节中讨论的 Python 对象树。

标签

单词tag代表在提供的文档中的 HTML/XML 标签。每个tag对象都有一个名称以及许多属性和方法。以下示例展示了处理tag对象的方式:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup("<h1 id='message'>Hello, Requests!</h1>")

为了访问我们在前一个示例中创建的BeautifulSoup对象soup的类型、名称和属性,请使用以下命令:

  • 对于访问标签类型

    >>> tag = soup.h1
    >>> type(tag)
    <class 'bs4.element.Tag'>
    
    
  • 要访问标签名

    >>> tag.name
    'h1'
    
    
  • 为了访问tag属性(在给定的 HTML 字符串中为'id'

    >>> tag['id']
    'message'
    
    

BeautifulSoup

当我们打算抓取网络资源时创建的对象被称为BeautifulSoup对象。简单来说,这是我们计划抓取的完整文档。这可以通过以下命令完成:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup("<h1 id='message'>Hello, Requests!</h1>") >>> type(soup)
<class 'bs4.BeautifulSoup'>

可导航字符串

一个 NavigableString 对象表示 tag 的内容。我们使用 tag 对象的 .string 属性来访问它:

>>> tag.string
u'Hello, Requests!'

评论

comment 对象说明了网页文档的注释部分。以下代码行展示了 comment 对象:

>>> soup = BeautifulSoup("<p><!-- This is comment --></p>")
>>> comment = soup.p.string
>>> type(comment)
<class 'bs4.element.Comment'>

与 BeautifulSoup 相关的网页抓取任务

如前文 关键网络爬取任务 部分所述,BeautifulSoup 在网络爬取过程中始终遵循那些基本任务。我们可以通过一个实际例子,使用 HTML 文档来详细了解这些任务。在本章中,我们将使用以下 HTML 文档 scraping_example.html 作为示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>
      Chapter 6 - Web Scrapping with Python Requests and BeatuifulSoup
    </title>
  </head>
  <body>
    <div class="surveys">
      <div class="survey" id="1">
        <p class="question">
          <a href="/surveys/1">Are you from India?</a>
        </p>
        <ul class="responses">
          <li class="response">Yes - <span class="score">21</span>
          </li>
          <li class="response">No - <span class="score">19</span>
          </li>
        </ul>
      </div>
      <div class="survey" id="2">
        <p class="question">
          <a href="/surveys/2">Have you ever seen the rain?</a>
        </p>
        <ul class="responses">
          <li class="response">Yes - <span class="score">40</span>
          </li>
          <li class="response">No - <span class="score">0</span>
          </li>
        </ul>
      </div>
      <div class="survey" id="3">
        <p class="question">
          <a href="/surveys/1">Do you like grapes?</a>
        </p>
        <ul class="responses">
          <li class="response">Yes - <span class="score">34</span>
          </li>
          <li class="response">No - <span class="score">6</span>
          </li>
        </ul>
      </div>
    </div>
  </body>
</html>

为了对前面的网页文档有一个清晰明确的理解,我们将其展示为一个文档树。以下图表代表了前面的 HTML 文档:

与 BeautifulSoup 相关的网页抓取任务

当我们为之前展示的网页文档创建BeautifulSoup对象时,它将生成一个 Python 对象的树形结构。

要使用之前的文档scraping_example.html执行不同的任务,我们需要创建一个BeautifulSoup对象。为了创建它,打开 Python 交互式命令行并运行以下命令:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(open("scraping_example.html"))

从现在开始,我们将使用前面的 BeautifulSoup 对象来执行不同的任务。让我们对 scraping_example.html 文档进行网络爬取操作,并对所有任务有一个整体的了解。

搜索树

要识别 HTML/XML 文档中的不同标签,我们需要搜索整个文档。在类似情况下,我们可以使用 BeautifulSoup 的方法,如 findfind_all 等等。

这里是搜索整个文档以识别标签的语法:

  • find(name, attributes, recursive, text, **kwargs)

    • name: 这是发现过程中首次出现的标签名称。它可以是字符串、正则表达式、列表、函数或值 True
  • find_all(name, attributes, recursive, text, limit, **kwargs)

    • name: 这用于通过名称访问特定类型的标签。它可以是字符串、正则表达式、列表、函数或值 True

    • limit: 这是输出结果中的最大结果数。

前两种方法的共同属性如下:

  • attributes: 这些是 HTML/XML 标签的属性。

  • recursive: 这接受一个布尔值。如果设置为 TrueBeautifulSoup 库会检查特定标签的所有子标签。反之,如果设置为 falseBeautifulSoup 库只会检查下一级的子标签。

  • text: 此参数用于标识由字符串内容组成的标签。

在树结构中导航

使用Beautifulsoup4模块在文档树中进行导航涉及不同的任务;它们将在下一节中进行讨论。

导航向下

我们可以通过在文档中向下移动来访问特定元素的数据。如果我们考虑前一个图中的文档树,我们可以通过从顶级元素——html向下移动来访问不同的元素。

每个元素都可以通过其标签名称进行访问。以下是通过html属性访问内容的方法:

>>> soup.html
<html lang="en">
...
...
</html>

这里介绍了我们通过向下导航访问前一个文档树元素的方法。为了访问title元素,我们应该从上到下进行,即从htmlhead,再从headtitle,如下面的命令所示:

>>> soup.html.head.title
<title>Chapter 6 - Web Scraping with Python Requests and BeatuifulSoup</title>

同样,你可以访问meta元素,如下命令所示:

>>> soup.html.head.meta
<meta charset="utf-8"/>

横向导航

要访问文档树中的兄弟节点,我们应该横向导航。BeautifulSoup库提供了各种tag对象属性,例如.next_sibling.previous_sibling.next_siblings.previous_siblings

如果你查看包含文档树的上一张图,树中不同层级的不同兄弟节点,在横向导航时如下所示:

  • 头部主体

  • div1div2div3

在文档树中,head 标签是 html 的第一个子元素,而 bodyhtml 的下一个子元素。为了访问 html 标签的子元素,我们可以使用它的 children 属性:

>>> for child in soup.html.children:
...     print child.name
...
head
body

要访问head元素的下一个兄弟元素,我们可以使用.find_next_sibling

>>> soup.head.find_next_sibling()
<body>
 <div class="surveys">
 .
 .
 .
 </div>
</body>

要访问body的上一级兄弟元素,我们可以使用.find_previous_sibling方法:

>>> soup.body.find_previous_sibling
<head><meta charset="utf-8"/><title>... </title></head>

向上导航

我们可以通过向文档树顶部移动来访问特定元素的父亲元素。BeautifulSoup 库提供了两个属性——.parent.parents——分别用于访问 tag 元素的第一级父元素及其所有祖先元素。

这里有一个例子:

>>> soup.div.parent.name
'body'

>>> for parent in soup.div.parents:
...     print parent.name
...
body
html
[document]

在导航中来回切换

要访问之前解析的元素,我们需要在树的节点中回溯,而要访问下一个即将解析的元素,我们需要在树的节点中前进。为了处理这种情况,tag对象提供了.find_previous_element.find_next_element属性,如下例所示:

>>> soup.head.find_previous().name
'html'
>>> soup.head.find_next().name
'meta'

修改树结构

BeautifulSoup 库还使我们能够根据我们的需求对网页文档进行修改。我们可以通过其属性来更改标签的属性,例如 .name.string.append() 方法。我们还可以借助 .new_string().new_tag() 方法向现有标签添加新的标签和字符串。此外,还有一些其他方法,如 .insert().insert_before().insert_after() 等,用于对文档树进行各种修改。

这里是一个更改title标签的.string属性的示例:

  • 在修改title标签之前,标题内容如下:

    >>> soup.title.string
    u'Chapter 6 - Web Scrapping with Python Requests and BeatuifulSoup'
    
    
  • 这是修改标题标签内容的方法:

    >>> soup.title.string = 'Web Scrapping with Python Requests and BeatuifulSoup by Balu and Rakhi'
    
    
  • 修改后,tilte 标签的内容看起来是这样的:

    >>> soup.title.string
    u'Web Scrapping with Python Requests and BeatuifulSoup by Balu and Rakhi'
    
    

构建一个网络爬虫机器人——一个实用示例

在这个时间点,我们的思维被各种线索启发,以刮取网络信息。收集了所有信息后,让我们来看一个实际例子。现在,我们将创建一个网络爬虫,它将从网络资源中提取单词列表并将它们存储在一个 JSON 文件中。

让我们开启抓取模式!

网络爬虫机器人

在这里,网络爬虫是一个自动脚本来提取名为 majortests.com 的网站上的单词。这个网站包含各种测试和研究生入学考试(GRE)单词列表。使用这个网络爬虫,我们将爬取之前提到的网站,并在 JSON 文件中创建一个 GRE 单词及其含义的列表。

以下图片是我们将要抓取的网站样本页面:

网络爬虫机器人

在我们开始爬取过程之前,让我们回顾一下章节开头提到的网络爬取的注意事项和禁忌。信不信由你,它们肯定会让我们安心的:

  • 请务必查阅条款和条件:是的,在抓取 majortests.com 之前,请查阅该网站的条款和条件,并获取抓取所需的必要法律许可。

  • 不要向服务器发送大量请求:牢记这一点,对于我们将要发送到网站的每一个请求,我们都使用了 Python 的 time.sleep 函数来引入延迟。

  • 定期跟踪网络资源:我们确保代码与服务器上运行的网站完美兼容。在开始抓取之前,请检查网站一次,以免破坏代码。这可以通过运行一些符合我们预期结构的单元测试来实现。

现在,让我们按照之前讨论的步骤开始实施抓取操作。

识别 URL 或 URLs

网络爬取的第一步是确定要获取所需资源的 URL 或 URL 列表。在这种情况下,我们的目的是找到所有导致预期 GRE 单词列表的 URL。以下是我们将要爬取的网站的 URL 列表:

www.majortests.com/gre/wordlist_01

www.majortests.com/gre/wordlist_02

www.majortests.com/gre/wordlist_03,等等

我们的目标是从九个这样的 URL 中抓取单词,我们发现了一个共同的模式。这将帮助我们爬取所有这些 URL。所有这些 URL 的共同 URL 模式是用 Python 的string对象编写的,如下所示:

http://www.majortests.com/gre/wordlist_0%d

在我们的实现中,我们定义了一个名为 generate_urls 的方法,该方法将使用前面的 URL 字符串生成所需的 URL 列表。以下代码片段展示了在 Python 命令行中执行此过程的方法:

>>> START_PAGE, END_PAGE = 1, 10
>>> URL = "http://www.majortests.com/gre/wordlist_0%d"
>>> def generate_urls(url, start_page, end_page):
...     urls = []
...     for page in range(start_page, end_page):
...         urls.append(url % page)
...     return urls
...
>>> generate_urls(URL, START_PAGE, END_PAGE)
['http://www.majortests.com/gre/wordlist_01', 'http://www.majortests.com/gre/wordlist_02', 'http://www.majortests.com/gre/wordlist_03', 'http://www.majortests.com/gre/wordlist_04', 'http://www.majortests.com/gre/wordlist_05', 'http://www.majortests.com/gre/wordlist_06', 'http://www.majortests.com/gre/wordlist_07', 'http://www.majortests.com/gre/wordlist_08', 'http://www.majortests.com/gre/wordlist_09']

使用 HTTP 客户端

我们将使用requests模块作为 HTTP 客户端来获取网络资源:

>>> import requests
>>> def get_resource(url):
...     return requests.get(url)
...
>>> get_resource("http://www.majortests.com/gre/wordlist_01")
<Response [200]>

在前面的代码中,get_resource 函数接受 url 作为参数,并使用 requests 模块获取资源。

发现需要抓取的数据片段

现在,是时候分析和分类网页的内容了。在这个上下文中,内容是一系列带有定义的单词列表。为了识别单词及其定义的元素,我们使用了 Chrome DevTools。元素的感知信息(HTML 元素)可以帮助我们识别单词及其定义,这些信息可以在抓取过程中使用。

要执行此操作,请在 Chrome 浏览器中打开 URL (www.majortests.com/gre/wordlist_01),并通过右键点击网页来访问检查元素选项:

发现可抓取的数据片段

从前面的图像中,我们可以识别出单词列表的结构,它以以下方式呈现:

<div class="grid_9 alpha">
  <h3>Group 1</h3>
  <a name="1"></a>
  <table class="wordlist">
    <tbody>
      <tr>
        <th>Abhor</th>
        <td>hate</td>
      </tr>
      <tr>
        <th>Bigot</th>
        <td>narrow-minded, prejudiced person</td>
      </tr>
      ...
      ...
    </tbody>
  </table>
</div>

通过查看之前提到的网页部分,我们可以解读如下:

  • 每个网页都包含一个单词列表

  • 每个单词列表都包含许多在相同div标签中定义的单词组

  • 一个词组中的所有单词都描述在一个具有类属性—wordlist的表中

  • 表格中的每一行(tr)都分别使用thtd标签代表一个单词及其定义

使用网络爬虫工具

让我们使用BeautifulSoup4作为网络爬虫工具来解析我们在前一步骤中使用requests模块获取到的网页内容。通过遵循前面的解释,我们可以指导BeautifulSoup访问网页所需的内容,并将其作为对象提供:

def make_soup(html_string):
    return BeautifulSoup(html_string)

在前面的代码行中,make_soup 方法接收以字符串形式的 html 内容,并返回一个 BeautifulSoup 对象。

绘制所需数据

在上一步中我们获得的 BeautifulSoup 对象用于从中提取所需的单词及其定义。现在,利用 BeautifulSoup 对象中可用的方法,我们可以遍历获取到的 HTML 响应,然后我们可以提取单词列表及其定义:

def get_words_from_soup(soup):
    words = {}

    for count, wordlist_table in enumerate(
    soup.find_all(class_='wordlist')):

        title = "Group %d" % (count + 1)

        new_words = {}
        for word_entry in wordlist_table.find_all('tr'):
            new_words[word_entry.th.text] = word_entry.td.text

        words[title] = new_words

    return words

在前面的代码行中,get_words_from_soup 函数接收一个 BeautifulSoup 对象,然后使用实例的 find_all() 方法查找 wordlists 类中包含的所有单词,并返回一个单词的字典。

之前获得的单词字典将使用以下helper方法保存到 JSON 文件中:

def save_as_json(data, output_file):
    """ Writes the given data into the specified output file"""
    with open(output_file, 'w') as outfile:
        json.dump(data, outfile)

总体来说,这个过程可以用以下程序来描述:

import json
import time

import requests

from bs4 import BeautifulSoup

START_PAGE, END_PAGE, OUTPUT_FILE = 1, 10, 'words.json'

# Identify the URL
URL = "http://www.majortests.com/gre/wordlist_0%d"

def generate_urls(url, start_page, end_page):
    """
    This method takes a 'url' and returns a generated list of url strings

        params: a 'url', 'start_page' number and 'end_page' number
        return value: a list of generated url strings
    """
    urls = []
    for page in range(start_page, end_page):
        urls.append(url % page)
    return urls

def get_resource(url):
    """
    This method takes a 'url' and returns a 'requests.Response' object

        params: a 'url'
        return value: a 'requests.Response' object
    """
    return requests.get(url)

def make_soup(html_string):
    """
    This method takes a 'html string' and returns a 'BeautifulSoup' object

        params: html page contents as a string
        return value: a 'BeautifulSoup' object
    """
    return BeautifulSoup(html_string)

def get_words_from_soup(soup):

    """
    This method extracts word groups from a given 'BeautifulSoup' object

        params: a BeautifulSoup object to extract data
        return value: a dictionary of extracted word groups
    """

    words = {}
    count = 0

    for wordlist_table in soup.find_all(class_='wordlist'):

        count += 1
        title = "Group %d" % count

        new_words = {}
        for word_entry in wordlist_table.find_all('tr'):
            new_words[word_entry.th.text] = word_entry.td.text

        words[title] = new_words
        print " - - Extracted words from %s" % title

    return words

def save_as_json(data, output_file):
    """ Writes the given data into the specified output file"""
            json.dump(data, open(output_file, 'w'))

def scrapper_bot(urls):
    """
    Scrapper bot:
        params: takes a list of urls

        return value: a dictionary of word lists containing
                      different word groups
    """

    gre_words = {}
    for url in urls:

        print "Scrapping %s" % url.split('/')[-1]

        # step 1

        # get a 'url'

        # step 2
        html = requets.get(url)

        # step 3
        # identify the desired pieces of data in the url using Browser tools

        #step 4
        soup = make_soup(html.text)

        # step 5
        words = get_words_from_soup(soup)

        gre_words[url.split('/')[-1]] = words

        print "sleeping for 5 seconds now"
        time.sleep(5)

    return gre_words

if __name__ == '__main__':

    urls = generate_urls(URL, START_PAGE, END_PAGE+1)

    gre_words = scrapper_bot(urls)

    save_as_json(gre_words, OUTPUT_FILE)

这里是words.json文件的内容:

{"wordlist_04":
    {"Group 10":
        {"Devoured": "greedily eaten/consumed",
         "Magnate": "powerful businessman",
         "Cavalcade": "procession of vehicles",
         "Extradite": "deport from one country back to the home...
    .
    .
    .
}

摘要

在本章中,你了解了我们在网络资源中遇到的不同类型的数据,并对一些想法进行了调整。我们认识到了网络爬取的必要性、法律问题以及它所提供的便利。然后,我们深入探讨了网络爬取任务及其潜力。你学习了一个名为 BeautifulSoup 的新库,以及它的功能和细节,并伴随着示例。

我们深入了解了BeautifulSoup的功能,并通过对一些示例进行操作,对其有了清晰的认识。最后,我们通过应用从前面章节中获得的知识,创建了一个实用的抓取机器人,这个经验让我们明白了如何实时抓取网站。

在下一章中,你将学习关于 Flask 微型框架的内容,我们将遵循最佳实践来构建一个使用该框架的应用程序。

第七章. 使用 Flask 用 Python 实现 Web 应用程序

为了在学习 Requests 模块的过程中确保繁荣,似乎没有什么比应用你至今所获得的所有技能和知识更重要。因此,我们在这里铺平道路,通过使用 Flask 框架创建一个网络应用程序来应用你至今所获得的专长。这将使你对开发实际网络应用程序及其测试用例的编写有深入的了解。我们在这一过程中倾向于遵循最佳实践和动手操作的方法。让我们深入其中,学习这些知识。

什么是 Flask?

Flask 是一个轻量级但功能强大的 Python 用于创建 Web 应用的框架。它可以被称作一个 微框架。它如此小巧,以至于如果你能与之建立良好的关系,你就能理解它的所有源代码。它之所以强大,是因为它拥有被称为 扩展 的优点,以及它作为一个整体提供所有基本服务的能力。扩展可以根据应用的需求进行添加。Flask 框架背后的开发者是 Armin Ronacher,他于 2010 年 4 月 1 日发布了它。

Flask 的优点如下:

  • Flask 自带了一个内置的开发服务器,它可以帮助你在开发过程中以及测试程序时。

  • 在 Flask 中,错误日志记录变得简单,因为它拥有交互式的基于网页的调试器。当执行你的代码时,如果以某种方式出现了错误,错误堆栈跟踪将会显示在网页上,这使得处理错误变得容易。这可以通过将 app.debug 的标志设置为 True 来实现。

  • 由于其轻量级特性,Flask 是一个构建 RESTful 网络服务的完美框架。该路由装饰器可以帮助将一个函数绑定到一个 URL,它可以接受 HTTP 方法作为参数,从而为以理想方式构建 API 铺平道路。此外,使用 Flask 处理 JSON 数据非常简单。

  • Flask 的模板支持由一个名为Jinja2的灵活模板引擎提供。这使得渲染模板的过程变得更加顺畅。

  • Session 对象是另一个好东西,它保存了用户的会话。它存储用户的请求,以便应用程序能够记住用户的不同请求。

  • Flask 在处理来自客户端的请求时使用Web Server Gateway InterfaceWSGI)协议,并且完全符合 100 % WSGI 规范。

Flask 入门

我们可以通过一个简单的示例来启动我们的应用程序开发,这个示例能让你了解我们如何使用 Flask 框架在 Python 中编程。为了编写这个程序,我们需要执行以下步骤:

  1. 创建一个 WSGI 应用程序实例,因为 Flask 中的每个应用程序都需要一个来处理来自客户端的请求。

  2. 定义一个route方法,它将一个 URL 与其处理的函数关联起来。

  3. 激活应用程序的服务器。

这里是一个按照前面步骤制作简单应用的示例:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def home():
 return "Hello Guest!"

if __name__ == "__main__":
 app.run()

在前面的代码行中,我们使用 Flask 的 Flask 类创建了一个 WSGI 应用实例,然后我们定义了一个路由,将路径 "/" 和视图函数 home 映射到使用 Flask 的装饰器函数 Flask.route() 处理请求。接下来,我们使用了 app.run(),这告诉服务器运行代码。在那一端,当代码执行时,它将显示一个包含 "Hello Guest!" 的网页。

安装 Flask

在开始编程过程之前,您需要安装所需的依赖项。让我们通过使用虚拟环境包装器来创建虚拟环境来启动安装过程。在创建应用程序时使用虚拟环境是一种最佳实践。虚拟环境包装器是一个工具,它将项目的所有依赖项放在一个地方。

这种实践将减轻你在系统中处理不同项目时遇到的大量复杂性。在我们的教程中,安装和应用开发使用的是 Python 2.7 版本。

以下是为设置环境所需的步骤:

  1. 使用 pip 安装虚拟环境包装器。你可能需要使用 sudo 以获得管理员权限:

    $ pip install virtualenvwrapper
    
    
  2. 为了方便起见,所有与虚拟环境相关的安装包都放置在一个文件夹中。Virtualenvwrapper通过环境变量WORKON_HOME来识别该目录。因此,请将环境变量设置为~/Envs或您选择的任何名称。

    $ export WORKON_HOME=~/Envs
    
    
  3. 如果在您的本地机器上不存在WORKON_HOME目录,请使用以下命令创建:

    $ mkdir -p $WORKON_HOME
    
    
  4. 为了使用virtualenvwrapper提供的工具,我们需要激活如下所示的virtualenvwrapper.sh壳脚本。在 Ubuntu 机器上,我们可以在/usr/local/bin位置找到此脚本:

    $ source /usr/local/bin/virtualenvwrapper.sh
    
    
  5. 为了方便起见,将步骤 2 和 4 中的命令添加到您的 shell 启动文件中,以便在终端启动时初始化和激活virtualenvwrapper工具。

  6. 现在,使用 mkvirtualenv 命令为您的项目创建一个名为 survey 的新虚拟环境。一旦 survey 环境被激活,它就会在 shell 提示符前的闭合括号中显示环境名称。

    $ mkvirtualenv survey
    New python executable in survey/bin/python
    Installing setuptools, pip...done.
    (survey) $
    
    

使用 pip 安装所需软件包

我们将在本项目中使用 Flask-SQLAlchemy,这是一个作为 对象关系映射器ORM)的 Flask 扩展模块,用于与数据库交互。我们还将使用 requestshttprettybeautifulsoup 等模块来开发我们的 survey 应用程序,该应用程序将在本教程中构建。

现在请确保你的虚拟环境已激活,然后安装以下包:

(survey)~ $ pip install flask flask-sqlalchemy requests httpretty beautifulsoup4

调查 - 使用 Flask 的简单投票应用程序

要创建调查应用程序,我们将采用一种方法,这种方法将使你轻松理解应用程序的方方面面,同时也会让开发过程变得愉快。

我们的开发流程将引导您逐步了解项目所涉及的所有功能。然后,我们将逐步实现每一个功能。在开发过程中,我们将遵循模型-视图-控制器MVC)设计模式,这种模式在开发 Web 应用中非常流行。

调查应用的主要目的是记录创建的调查问题的回答数量——“是”、“否”和“可能”。

基本文件结构

为了开发 Flask 应用程序,我们遵循特定的结构来组织应用程序的内容。以下是我们要开发的应用程序的文件结构:

基本文件结构

这里是关于我们应用程序文件结构中所有文件和文件夹的描述:

文件/文件夹名称 描述
__init__.py 初始化我们的项目并将其添加到 PYTHONPATH
server.py 调用应用程序开发服务器以启动。
survey/__init__.py 初始化我们的应用程序并将各种组件集中在一起。
survey/app.db 一个用于存储数据的 sqlite3 文件
survey/models.py 定义了我们应用程序的模型。
survey/templates 一个存放所有 Jinja2 模板的地方。
survey/tests.py 一个包含与该应用相关的各种测试用例的文件。
survey/views.py 定义了您应用程序的路由。

在我们的调查应用中,survey_project 是项目根目录。现在,让我们根据上述文件结构创建所有文件和文件夹,并将以下内容放置在 survey_project/__init__.py 文件中。

import os
import sys
current_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
sys.path.insert(0, parent_dir)

构建应用程序

现在,我们将向您介绍调查应用的所有功能。以下是我们应用必须执行的一系列详细任务:

  • 创建调查问题

  • 查看所有问题列表

  • 查看特定问题

  • 修改问题

  • 删除一个问题

  • 投票支持一个问题

每个问题都存储与特定调查相关的信息。问题模型(关于数据的单一确定信息来源)包含的字段如下:

  • id: 用于唯一标识每个问题的主键

  • question_text: 描述调查

  • number_of_yes_votes: 存储投票中 'yes' 的数量

  • number_of_no_votes: 存储被投票的 'no' 票数

  • number_of_maybe_votes: 存储了被调查的 'maybe' 票数

现在,让我们开始设计资源持有者,也就是我们所说的 URL,用于之前提到的任务。这些 URL 需要特定的 HTTP 方法来与服务器进行通信。

以下表格突出了我们将如何设计 URL:

任务 HTTP 方法 URL
所有问题的列表 GET http://[主机名:端口]/
创建调查问题 POST http://[hostname:port]/questions
查看特定问题 GET http://[hostname:port]/questions/[question_id]
修改一个问题 PUT http://[hostname:port]/questions/[question_id]
删除一个问题 DELETE http://[hostname:port]/questions/[question_id]
投票支持一个问题 POST http://[hostname:port]/questions/[question_id]/vote
投票支持一个问题 GET http://[hostname:port]/questions/[question_id]/vote
新问题表单 GET http://[hostname:port]/questions/new

使用 Flask-SQLAlchemy 编写模型

SQLAlchemy 是一个 Python 对象关系映射器 (ORM) 以及一个用于与各种数据库交互的查询工具包。它提供了一套实用工具,包括一个基类来表示模型,以及一组辅助类和函数来表示数据库。

注意

模型是关系数据库中表格的逻辑表示,其中包含有关数据的信息。

Flask-SQLAlchemy 是 Flask 框架的一个扩展,它增加了对 SQLAlchemy 的支持。

定义一个模型

在使用 Flask-SQLAlchemy 定义模型时,我们需要牢记以下三个步骤:

  1. 创建数据库实例。

  2. 使用之前创建的数据库实例定义一个模型。

  3. 在数据库实例中调用一个方法来创建数据库中的表。

创建数据库实例

在我们的应用程序中,我们确实需要创建一个数据库实例来存储数据。为此,我们需要在 WSGI 应用程序实例中配置'SQLALCHEMY_DATABASE_URI'属性,如下面的代码所示。此代码应保存在survey/__init__.py文件中。

init.py

import os

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

BASE_DIR = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = \
    'sqlite:///' + os.path.join(BASE_DIR, 'app.db')
db = SQLAlchemy(app)

在前面的代码行中,我们使用 Flask 的 Flask 类创建了一个 WSGI 应用程序实例,并配置了 'SQLALCHEMY_DATABASE_URI' 变量。接下来,我们创建了一个名为 db 的数据库实例,该实例用于定义模型和执行各种查询。

创建调查模型

为了在数据库中存储与调查应用相关的数据,我们应该定义一个名为Question的模型。这段代码位于survey/models.py文件中。

models.py

class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    question_text = db.Column(db.String(200))
    number_of_yes_votes = db.Column(db.Integer, default=0)
    number_of_no_votes = db.Column(db.Integer, default=0)
    number_of_maybe_votes = db.Column(db.Integer, default=0)

在前面的代码中,我们定义了Question模型,它继承自db.Model。它包含五个字段来存储与特定调查相关的数据:

  • id

  • question_text

  • number_of_yes_votes

  • number_of_no_votes

  • number_of_maybe_votes

现在,让我们继续添加一个构造方法,这样我们就可以为之前代码行中创建的Question对象设置实例变量:

class Question(db.Model):
    ...
    ...

    def __init__(self,
                 question_text,
                 number_of_yes_votes=0,
                 number_of_no_votes=0,
                 number_of_maybe_votes=0):

        self.question_text = question_text

        self.number_of_yes_votes = number_of_yes_votes
        self.number_of_maybe_votes = number_of_maybe_votes
        self.number_of_no_votes = number_of_no_votes

之前的 __init__() 方法接收 Question 对象及其值作为参数。然后,它将设置我们传递的对象的实例变量。

现在,我们将创建一个名为 vote() 的方法,该方法将增加 'yes''no''maybe' 投票的计数器变量。

class Question(db.Model):
    ...
    ...

    def vote(self, vote_type):
        if vote_type == 'yes':
            self.number_of_yes_votes += 1
        elif vote_type == 'no':
            self.number_of_no_votes += 1
        elif vote_type == 'maybe':
            self.number_of_maybe_votes += 1
        else:
            raise Exception("Invalid vote type")

在前面的代码行中,我们定义了一个vote()方法,它接受Question对象作为其第一个参数,并将vote_type作为其第二个参数。根据vote_type'yes''no''maybe'),我们传递的Question对象的相应number_of_<vote_type>_votes值会增加。

在数据库中创建表格

现在我们已经使用名为 db 的数据库实例对象定义了与我们的应用程序相关的模型,接下来我们需要在数据库中创建相应的表。为此,我们需要调用存在于数据库实例——db 中的 create_all() 方法。

在我们的应用中,我们通常在调用runserver.py文件中定义的服务器之前调用这个函数。

查询数据库模型

现在,我们已经准备好了数据库模型。让我们使用 SQLAlchemy 的 ORM 从数据库中查询数据。我们将在我们的数据库实例 db 上执行基本的创建、检索、更新和删除(CRUD)操作。

在进行查询之前,让我们先切换到我们的项目根目录,并启动 Python 控制台以执行以下命令:

>>> from survey import app, db
>>> from survey.models import Question

现在,让我们在数据库中创建一个问题对象。使用 SQLAlchemy 的 ORM 创建对象涉及三个基本步骤,如下面的代码所示:

>>> question = Question("Are you an American?")
>>> db.session.add(question)
>>> db.session.commit()

我们可以看到:

  • 第一步为模型创建一个 Python 对象。

  • 下一步是将创建的 Python 对象添加到数据库的会话中。

  • 最后一步是将对象提交到数据库中。

使用 ORM 从数据库中检索对象非常简单。以下查询从数据库中检索所有对象:

>>> Question.query.all()
[<Question 1 - u'Are you an American?'>]

我们也可以使用主键从数据库中检索模型对象。如果我们查看Question模型,我们有一个名为id的列作为主键。现在,让我们继续访问它。

>>> Question.query.get(1)
<Question 1 - u'Are you an American?'>

是时候投票进行调查了。获取id值为1的对象,并使用其vote()方法来增加该选项的投票数。

>>> question = Question.query.get(1)
>>> question.number_of_yes_votes
0
>>> question.vote('yes')
>>> db.session.add(question)
>>> db.session.commit()

让我们学习如何使用db.session.delete()方法从数据库中删除记录,如下面的代码所示:

>>> question = Question.query.get(1)
>>> db.session.delete(question)
>>> db.session.commit()

如果你尝试访问相同的对象,它将导致None值。

>>> print Question.query.get(1)
None

视图

视图是一个 Python 函数,它接收一个网络请求并返回一个网络响应。视图的响应可以是一个简单的字符串、网页、文件内容,或者任何东西。每当 Flask 应用程序从客户端接收到请求时,它都会寻找一个视图函数来处理它。视图包含处理请求所必需的业务逻辑。

在前面的章节中,我们已经创建了必要的数据库模型。现在,在本节中,我们将编写视图函数。让我们为前面表格中提到的每个资源创建视图,这将突出我们如何设计 URL。所有视图都应该在文件survey/views.py中创建。

所有问题的列表

此视图显示了数据库中我们创建的所有调查。每当客户端请求应用程序的根目录时,Flask 应用程序将调用此视图。请将以下代码添加到survey/views.py文件中:

from flask import render_template
from survey import app
from survey.models import Question

@app.route('/', methods=['GET'])
def home():
    questions = Question.query.all()
    context = {'questions': questions, 'number_of_questions': len(questions)}
    return render_template('index.html',**context)

@app.route() 装饰器将路径 '/' 和视图函数 home() 进行映射。home 视图使用 SQLAlchemy ORM 从数据库中检索所有问题,并通过 render_template 方法渲染名为 'index.html' 的模板。render_template 方法接受模板名称和一系列参数,以返回一个网页。

新调查

此视图返回一个 HTML 网页表单以创建新的调查问题。当用户访问路径/questions/new时调用此视图。将以下代码添加到survey/views.py文件中:

. . .
. . .
@app.route('/questions/new', methods=['GET'])
def new_questions():
    return render_template('new.html')

创建一个新的调查

此视图在数据库中创建一个新的调查,并显示可用的问题列表作为响应。这是由 Flask 应用程序调用的,当用户提交一个包含 /questions 的 URL 的请求,并使用 POST 方法时。可以通过 request.form 字典在视图中访问创建新问题的数据。

@app.route('/questions', methods=['POST'])
def create_questions():
    if request.form["question_text"].strip() != "":
        new_question = Question(question_text=request.form["question_text"])
        db.session.add(new_question)
        db.session.commit()
        message = "Succefully added a new poll!"
    else:
        message = "Poll question should not be an empty string!"

    context = {'questions': Question.query.all(),'message': message}
    return render_template('index.html',**context)

展示调查

此视图通过在 URL 中传递question_id参数来显示请求的调查。当用户使用 HTTP 的'GET'动词请求路径'/questions/<question_id>'时,此视图会被触发:

@app.route('/questions/<int:question_id>', methods=['GET'])
def show_questions(question_id):
    context = {'question': Question.query.get(question_id)}
    return render_template('show.html', **context)

更新调查

此视图用于用户想要修改现有问题时。当用户提交数据以修改Question时,会调用此视图。我们可以通过 HTTP 的'PUT'方法在'/questions/<question_id>'上连接到这个资源:

@app.route('/questions/<int:question_id>', methods=['PUT'])
def update_questions(question_id):
    question = Question.query.get(question_id)
    if request.form["question_text"].strip() != "":
        question.question_text = request.form["question_text"]
        db.session.add(question)
        db.session.commit()
        message = "Successfully updated a poll!"
    else:

        message = "Question cannot be empty!"

    context = {'question': question,
               'message': message}

    return render_template('show.html', **context)

删除一个调查

此视图用于从数据库中删除特定的调查。特定的调查是根据 URL 中传递的question_id值来识别的。用户可以使用'DELETE' HTTP 动词在'/questions/<question_id>'访问此网页。一旦问题从数据库中删除,用户将收到一条消息和可用问题的列表。

@app.route('/questions/<int:question_id>', methods=['DELETE'])
def delete_questions(question_id):
    question = Question.query.get(question_id)
    db.session.delete(question)
    db.session.commit()
    context = {'questions': Question.query.all(),
               'message': 'Successfully deleted'}
    return render_template('index.html', **context)

新投票表用于在调查中投票

此视图返回一个包含 HTML 表单的网页,用于在调查中对特定选项进行投票。它可以通过 '/questions/<question_id>/vote' 访问。

@app.route('/questions/<int:question_id>/vote', methods=['GET'])
def new_vote_questions(question_id):
    question = Question.query.get(question_id)
    context = {'question': question}
    return render_template('vote.html', **context)

在调查中对某个特定选项进行投票

此视图用于在调查中对特定选项投新票。用户必须使用 'POST' 方法将特定选项提交到资源 '/questions/<question_id>/vote'。在成功投票后,用户将被重定向到调查详情页面。

@app.route('/questions/<int:question_id>/vote', methods=['POST'])
def create_vote_questions(question_id):
    question = Question.query.get(question_id)

    if request.form["vote"] in ["yes", "no", "maybe"]:
        question.vote(request.form["vote"])

    db.session.add(question)
    db.session.commit()
    return redirect("/questions/%d" % question.id)

模板

模板是一个包含块标签或变量的简单文本文档。Flask 微框架使用Jinja2模板引擎来渲染 HTML 页面。

在我们的应用中,我们使用了五种不同的模板,其中包括一个base模板——base.html。这个base模板是一个由所有模板共同元素组成的布局。其他四个模板(index.htmlshow.htmlvote.htmlnew.html)利用了Jinja2模板引擎提供的一个称为模板继承的概念。它用于使这些共同功能能够在每个模板中显示出来,而无需在每个模板中编写冗余的代码。

基础模板

此模板是所有其他模板的骨架。它包含一个通用的导航菜单部分,以及一个占位符,用于存放本应用中每个页面的主要内容块。survey/templates/base.html模板将包含以下代码:

<html>
  <head>
    <title>Welcome to Survey Application</title>
  </head>
  <body>
    {% if message %}
        <p style="text-align: center;">{{ message }}</p>
    {% endif %}
    <div>
      <a href="/">Home</a> |
      <a href="/questions">All Questions</a> |
      <a href="/questions/new">Create a new Question</a>
    </div>
    <hr>
    {% block content %}{% endblock %}
  </body>
</html>

问题列表模板

由于我们需要在网页上展示问题列表,我们使用for循环标签遍历questions变量,并显示特定调查的所有投票数。请将以下内容添加到survey/templates/index.html文件中:

{% extends "base.html" %}

{% block content %}
    <p>Number of Questions - <span id="number_of_questions">{{ number_of_questions }}</span></p>
    {% for question in questions %}
    <div>
        <p>
            <p><a href="/questions/{{ question.id }}">{{ question.question_text }}</a></p>
            <ul>
                <li>Yes - {{ question.number_of_yes_votes }} </li>
                <li>No - {{ question.number_of_no_votes }} </li>
                <li>Maybe - {{ question.number_of_maybe_votes }}
</li>
            </ul>
        </p>
    </div>
    {% endfor %}
    <hr />
{% endblock %}

创建一个新的调查模板

为了展示包含一个新调查问题的 HTML 表单,我们定义了一个名为 survey/templates/new.html 的模板:

new.html

{% extends "base.html" %}

{% block content %}
    <h1>Create a new Survey</h1>
    <form method="POST" action="/questions">
        <p>Question: <input type="text" name="question_text"></p>
        <p><input type="submit" value="Create a new Survey"></p>
    </form>
{% endblock %}

展示调查模板的详细信息

要显示调查的所有详细信息,请按照以下方式创建模板。此模板还包括一个指向投票页面的链接。将以下代码添加到survey/templates/show.html文件中:

{% extends "base.html" %}

{% block content %}
    <div>
        <p>
        {% if question %}
            <p>{{ question.question_text }}</p>
            <ul>
                <li>Yes - {{ question.number_of_yes_votes }}</li>
                <li>No - {{ question.number_of_no_votes }}
</li>
                <li>Maybe - {{ question.number_of_maybe_votes}}</li>
            </ul>
            <p><a href="/questions/{{ question.id }}/vote">Cast your vote now</a></p>
        {% else %}
            Not match found!
        {% endif %}
        </p>
    </div>
    <hr />
{% endblock %}

投票模板

要投票,我们需要显示一个包含带有调查及其选项的 HTML 表单的网页。将以下代码添加到survey/templates/vote.html文件中:

{% extends "base.html" %}

{% block content %}
    <div>
        <p>
        {% if question %}
            <p>{{ question.question_text }}</p>

            <form action="/questions/{{ question.id }}/vote" method="POST">
                <input type="radio" name="vote" value="yes">Yes<br>
                <input type="radio" name="vote" value="no">No<br>
                <input type="radio" name="vote" value="maybe">Maybe<br>

                <input type="submit" value="Submit" /><br>
            </form>
            <p><a href="/questions/{{ question.id }}">Back to Question</a></p>
        {% else %}
            Not match found!
        {% endif %}
        </p>
    </div>
    <hr />
{% endblock %}

运行调查应用程序

欢呼!我们成功创建了一个应用程序,该应用程序将允许用户创建调查、检索调查、更新调查、删除调查,并为调查投出选择票。运行服务器请执行以下步骤:

  1. 在运行服务器之前,让我们先给server.py文件填充以下代码:

    import sys
    
    from survey import app, db
    from survey import views
    
    def main():
        db.create_all()
        app.run(debug=True)
        return 0
    
    if __name__ == '__main__':
        sys.exit(main())
    
  2. 现在,让我们使用runserver.py脚本运行应用程序,如下所示:

    $ python runserver.py
    * Running on http://127.0.0.1:5000/
    * Restarting with reloader
    
    
  3. 现在,服务器已启动并运行。要在网页浏览器中访问应用程序,请访问以下网址—http://127.0.0.1:5000/.

我们完成了!

编写单元测试以调查应用程序

创建一个没有测试用例的应用程序就相当于完成了一半。即使你在开发应用程序时非常小心,也可能会在某些时候遇到错误。编写测试用例总能让我们处于一个安全的位置。

在本节中,我们将为我们的调查应用中的某些任务编写单元测试用例。将以下测试用例代码添加到survey/tests.py文件中:

import unittest
import requests

from bs4 import BeautifulSoup
from survey import db
from survey.models import Question

class TestSurveyApp(unittest.TestCase):

    def setUp(self):
        db.drop_all()
        db.create_all()

    def test_defaults(self):
        question = Question('Are you from India?')
        db.session.add(question)
        db.session.commit()

        self.assertEqual(question.number_of_yes_votes, 0)
        self.assertEqual(question.number_of_no_votes, 0)
        self.assertEqual(question.number_of_maybe_votes, 0)

    def test_votes(self):
        question = Question('Are you from India?')
        question.vote('yes')
        db.session.add(question)
        db.session.commit()

        self.assertEqual(question.number_of_yes_votes, 1)
        self.assertEqual(question.number_of_no_votes, 0)
        self.assertEqual(question.number_of_maybe_votes, 0)

    def test_title(self):
        title = "Welcome to Survey Application"
        response = requests.get("http://127.0.0.1:5000/")
        soup = BeautifulSoup(response.text)
        self.assertEqual(soup.title.get_text(), title)

我们可以从前面的代码块中看到以下内容:

  • 代码的初始行将所有必要的模块导入到内存中。

  • TestSurveyApp 中的 setUp() 方法会删除所有现有的表,并为每个测试用例创建它们。

  • test_defaults 测试用例将测试创建的 Question 对象的默认值。如果默认值与预期输入不匹配,则测试用例失败。

  • test_votes() 函数会对调查中的某个特定选项进行点赞,并测试被点赞的选项是否增加,其他选项是否保持不变。

  • test_title() 函数将测试响应标题是否与预期标题匹配。它使用 BeautifulSoup 库从响应内容中访问标题。

摘要

在本章中,我们学习了 Flask 微型框架并了解了 Flask 的不同特性。我们还使用 virtualenvwrapper 设置了虚拟环境,并使用 Flask、Flask-SQLAlchemy 和 Jinja2 创建了一个网络应用程序。最后,我们为开发的应用程序编写了单元测试。

posted @ 2025-09-19 10:35  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报