本章将重点介绍并发在 web 请求中的应用。直观地说,向某个网页发出请求以收集有关该网页的信息独立于将同一任务应用于另一个网页。因此,并发性,特别是在本例中的线程化,可以成为一个强大的工具,在这个过程中提供显著的加速。在本章中,我们将学习 web 请求的基础知识以及如何使用 Python 与网站交互。我们还将看到并发如何帮助我们高效地发出多个请求。最后,我们将介绍 web 请求中的一些良好实践。
在本章中,我们将介绍以下概念:
- web 请求的基础知识
- 请求模块
- 并发 web 请求
- 超时问题
- 提出 web 请求的良好实践
以下是本章的先决条件列表:
- 您的计算机上必须安装 Python 3
- 在下载 GitHub 存储库 https://github.com/PacktPublishing/Mastering-Concurrency-in-Python
- 在本章中,我们将使用名为
Chapter05
的子文件夹 - 查看以下视频以查看代码的运行:http://bit.ly/2Fy1ZcS
据估计,全球生成数据的能力每两年翻一番。尽管有一个被称为数据科学的跨学科领域完全致力于数据的研究,但软件开发中几乎每一项编程任务都与收集和分析数据有关。当然,其中一个重要部分是数据收集。然而,我们的应用所需的数据有时不能很好、干净地存储在数据库中,有时我们需要从网页收集所需的数据。
例如,web scraping 是一种数据提取方法,可自动向网页发出请求并下载特定信息。Web scraping 允许我们对众多网站进行梳理,并以系统和一致的方式收集我们需要的任何数据。收集的数据可以稍后由我们的应用进行分析,也可以简单地以各种格式保存在计算机上。这方面的一个例子是谷歌,它自己编写并运行大量的网络爬虫程序,为搜索引擎查找和索引网页。
Python 语言本身为这类应用提供了许多很好的选择。在本章中,我们将主要使用requests
模块从 Python 程序发出客户端 web 请求。然而,在我们更详细地研究本模块之前,我们需要了解一些 web 术语,以便能够有效地设计我们的应用。
超文本标记语言(HTML)是开发网页和 web 应用的标准和最常用的标记语言。HTML 文件只是一个具有.html
文件扩展名的纯文本文件。在 HTML 文档中,文本由尖括号中的标记包围和分隔:<p>
、<img>
、<i>
等等。这些标签通常由一对开始标签和一对结束标签组成,指示样式或包含在其中的数据的
性质。
也可以在 HTML 代码中包含其他形式的媒体,如图像或视频。在普通 HTML 文档中还使用了许多其他标记。有些元素指定了一组具有某些共同特征的元素,例如<id></id>
和<class></class>
。
以下是 HTML 代码的示例:
Sample HTML code
幸运的是,我们不需要详细了解每个 HTML 标记完成了什么,就可以做出有效的 web 请求。正如我们将在本章后面看到的,进行 web 请求的更重要的部分是有效地与 web 页面交互的能力。
在 web 上的典型通信过程中,HTML 文本是要保存和/或进一步处理的数据。这些数据需要首先从网页上收集,但我们如何才能做到这一点呢?大多数通信是通过互联网完成的,更具体地说,是万维网,这利用了超文本传输协议(HTTP)。在 HTTP 中,请求方法用于传递所请求的数据以及应该从服务器发回的数据的信息。
例如,当您在浏览器中键入packtpub.com
时,浏览器通过 HTTP 向 Packt 网站的主服务器发送请求方法,请求从网站获取数据。现在,如果您的 internet 连接和 Packt 的服务器都工作正常,那么您的浏览器将收到来自服务器的响应,如下图所示。此响应将以 HTML 文档的形式出现,您的浏览器将对其进行解释,并且您的浏览器将在屏幕上显示相应的 HTML 输出。
Diagram of HTTP communication
通常,请求方法被定义为指示 HTTP 客户端(web 浏览器)和服务器彼此通信时要执行的所需操作的动词:GET
、HEAD
、POST
、PUT
、DELETE
等。在这些方法中,GET
和POST
是 web 抓取应用中最常用的两种请求方法;其功能如下表所示:
GET
方法从服务器请求特定数据。此方法仅检索数据,对服务器及其数据库没有其他影响。POST
方法以服务器接受的特定形式发送数据。例如,这些数据可以是发送给公告板、邮件列表或新闻组的消息;提交到 web 表单的信息;或要添加到数据库的项。
我们在互联网上常见的所有通用 HTTP 服务器实际上至少需要实现GET
(和HEAD
)方法,而POST
方法被认为是可选的。
当发出 web 请求并将其发送到 web 服务器时,服务器将处理该请求并返回所请求的数据,但这种情况并不总是如此。有时,服务器可能完全关闭或正忙于与其他客户端交互,因此对新请求没有响应;有时,客户端本身向服务器发出错误请求(例如,格式错误或恶意请求)。
为了对这些问题进行分类,并在 web 请求产生的通信过程中提供尽可能多的信息,HTTP 要求服务器以HTTP 响应****状态码响应其客户端的每个请求。状态代码通常是一个三位数的数字,表示服务器发送回客户端的响应的特定特征。
总共有五大类 HTTP 响应状态代码,由代码的第一位数字表示。详情如下:
-
1xx(信息状态码):已收到请求,服务器正在处理。例如,100 表示已经接收到请求头,服务器正在等待请求体;102 表示当前正在处理请求(这用于大型请求并防止客户端超时)。
-
2xx(成功状态码):请求被服务器成功接收、理解和处理。例如,200 表示请求已成功完成;202 表示已接受请求进行处理,但处理本身未完成。
-
3xx(重定向状态码):需要采取额外的操作才能成功处理请求。例如,300 表示存在关于如何处理来自服务器的响应的多个选项(例如,当要下载视频文件时,向客户端提供多个视频格式选项);301 表示服务器已永久移动,所有请求都应定向到另一个地址(在服务器的响应中提供)。
-
4xx(客户端错误状态代码):客户端错误格式化请求,无法处理。例如,400 表示客户端发送了错误的请求(例如,语法错误或请求的大小太大);404(可以说是最有名的状态代码)表示服务器不支持请求方法。
-
5xx(服务器错误状态码):请求虽然有效,但服务器无法处理。例如,500 表示存在内部服务器错误,其中遇到意外情况;504(网关超时)表示充当网关或代理的服务器没有及时收到来自最终服务器的响应。
关于这些状态代码,我们可以说得更多,但当我们从 Python 发出 web 请求时,记住前面提到的五大类别已经足够了。如果您想查找有关上述或其他状态代码的更多具体信息,互联网分配号码管理局(IANA维护 HTTP 状态代码的官方注册。
requests
模块允许用户创建和发送 HTTP 请求方法。在我们将要考虑的应用中,它主要用于与我们要从中提取数据的网页的服务器联系,并获取服务器的响应。
According to the official documentation of the module, the use of Python 3 is highly recommended over Python 2 for requests
.
要在计算机上安装模块,请运行以下操作:
pip install requests
如果您使用pip
作为包管理器,则应使用此代码。但是,如果您使用的是蟒蛇,只需使用以下命令:
conda install requests
如果您的系统尚未安装requests
和任何其他必需的依赖项(idna
、certifi
、urllib3
等),则这些命令应为您安装。之后,在 Python 解释器中运行import requests
以确认模块已成功安装。
让我们看一下该模块的一个使用示例。如果您已经从 GitHub 页面下载了本书的代码,请继续导航到Chapter05
文件夹。让我们看一看 Oracle T1A.文件,如下面的代码所示:
# Chapter05/example1.py
import requests
url = 'http://www.google.com'
res = requests.get(url)
print(res.status_code)
print(res.headers)
with open('google.html', 'w') as f:
f.write(res.text)
print('Done.')
在本例中,我们使用requests
模块下载网页www.google.com
的 HTML 代码。requests.get()
方法向url
发送GET
请求方法,我们存储对res
变量的响应。在通过打印检查响应的状态和标题之后,我们创建了一个名为google.html
的文件,并将存储在响应文本中的 HTML 代码写入该文件。
运行编程后(假设您的 internet 正常工作且 Google 服务器未关闭),您应获得以下输出:
200
{'Date': 'Sat, 17 Nov 2018 23:08:58 GMT', 'Expires': '-1', 'Cache-Control': 'private, max-age=0', 'Content-Type': 'text/html; charset=ISO-8859-1', 'P3P': 'CP="This is not a P3P policy! See g.co/p3phelp for more info."', 'X-XSS-Protection': '1; mode=block', 'X-Frame-Options': 'SAMEORIGIN', 'Content-Encoding': 'gzip', 'Server': 'gws', 'Content-Length': '4958', 'Set-Cookie': '1P_JAR=2018-11-17-23; expires=Mon, 17-Dec-2018 23:08:58 GMT; path=/; domain=.google.com, NID=146=NHT7fic3mjBO_vdiFB3-gqnFPyGN1EGxyMkkNPnFMEVsqjGJ8S0EwrivDBWBgUS7hCPZGHbosLE4uxz31shnr3X4adRpe7uICEiK8qh3Asu6LH_bIKSLWStAp8gMK1f9_GnQ0_JKQoMvG-OLrT_fwV0hwTR5r2UVYsUJ6xHtX2s; expires=Sun, 19-May-2019 23:08:58 GMT; path=/; domain=.google.com; HttpOnly'}
Done.
响应有一个200
状态代码,我们知道这意味着请求已成功完成。存储在res.headers
中的响应头还包含关于响应的更多特定信息。例如,我们可以看到请求的日期和时间,或者响应的内容是文本和 HTML,内容的总长度是4958
。
服务器发送的完整数据也写入了google.html
文件。当您在文本编辑器中打开该文件时,您将能够看到我们使用请求下载的网页的 HTML 代码。另一方面,如果您使用 web 浏览器打开文件,您将看到原始网页中的大部分信息现在是如何通过下载的脱机文件显示的。
例如,以下是我的系统上的 Google Chrome 如何解释 HTML 文件:
Downloaded HTML opened offline
服务器上还存储有其他信息,该服务器的网页可以引用这些信息。这意味着并非在线网页提供的所有信息都可以通过GET
请求下载,这就是为什么离线 HTML 代码有时无法包含在线网页上下载的所有信息。(例如,前面截图中下载的 HTML 代码没有正确显示 Google 图标。)
考虑到 HTTP 请求和 Python 中的requests
模块的基本知识,我们将带着一个中心问题来完成本章的其余部分:运行 ping 测试。ping 测试是一个测试系统和特定 web 服务器之间通信的过程,只需向每个相关服务器发出请求即可。通过考虑服务器返回的 HTTP 响应状态代码(可能),测试用于评估您自己系统的 internet 连接或服务器的可用性。
Ping 测试在 web 管理员中非常常见,他们通常需要同时管理大量网站。Ping 测试是一个很好的工具,可以快速识别意外无响应或关闭的页面。在 ping 测试中,有许多工具为您提供了强大的选项,在本章中,我们将设计一个 ping 测试应用,它可以同时发送多个 web 请求。
为了模拟发送回我们程序的不同 HTTP 响应状态代码,我们将使用httpstat.us,这是一个可以生成各种状态代码的网站,通常用于测试发出 web 请求的应用如何处理各种响应。具体来说,要在程序中使用将返回 200 状态代码的请求,我们只需向httpstat.us/200发出请求,其他状态代码也是如此。在我们的 ping 测试程序中,我们将有一个具有不同状态代码的httpstat.usURL 列表。
现在我们来看一下Chapter05/example2.py
文件,如下代码所示:
# Chapter05/example2.py
import requests
def ping(url):
res = requests.get(url)
print(f'{url}: {res.text}')
urls = [
'http://httpstat.us/200',
'http://httpstat.us/400',
'http://httpstat.us/404',
'http://httpstat.us/408',
'http://httpstat.us/500',
'http://httpstat.us/524'
]
for url in urls:
ping(url)
print('Done.')
在此程序中,ping()
函数接收 URL 并尝试向站点发出GET
请求。然后,它将打印出服务器返回的响应内容。在我们的主程序中,我们有一个我们前面提到的不同状态代码的列表,我们将遍历其中的每一个代码并调用ping()
函数。
运行上述示例后的最终输出应如下所示:
http://httpstat.us/200: 200 OK
http://httpstat.us/400: 400 Bad Request
http://httpstat.us/404: 404 Not Found
http://httpstat.us/408: 408 Request Timeout
http://httpstat.us/500: 500 Internal Server Error
http://httpstat.us/524: 524 A timeout occurred
Done.
我们看到我们的 ping 测试程序能够从服务器获得相应的响应。
在并发编程的上下文中,我们可以看到向 web 服务器发出请求并获取返回响应的过程独立于不同 web 服务器的相同过程。也就是说,我们可以将并发性和并行性应用于 ping 测试应用,以加快执行速度。
在我们正在设计的并发 ping 测试应用中,将同时向服务器发出多个 HTTP 请求,并将相应的响应发送回我们的程序,如下图所示。如前所述,并发和并行在 web 开发中有着重要的应用,如今,大多数服务器都能够同时处理大量请求:
Parallel HTTP requests
为了应用并发性,我们只需使用我们讨论过的 AutoT0AE 模块来创建单独的线程来处理不同的 Web 请求。
# Chapter05/example3.py
import threading
import requests
import time
def ping(url):
res = requests.get(url)
print(f'{url}: {res.text}')
urls = [
'http://httpstat.us/200',
'http://httpstat.us/400',
'http://httpstat.us/404',
'http://httpstat.us/408',
'http://httpstat.us/500',
'http://httpstat.us/524'
]
start = time.time()
for url in urls:
ping(url)
print(f'Sequential: {time.time() - start : .2f} seconds')
print()
start = time.time()
threads = []
for url in urls:
thread = threading.Thread(target=ping, args=(url,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f'Threading: {time.time() - start : .2f} seconds')
在本例中,我们使用上一个示例中的顺序逻辑来处理 URL 列表,以便在将线程应用于 ping 测试程序时比较速度的提高。我们还创建了一个线程,使用threading
模块 ping URL 列表中的每个 URL;这些线程将彼此独立执行。还使用time
模块中的方法跟踪顺序和并发处理 URL 所需的时间。
运行该程序,您的输出应类似于以下内容:
http://httpstat.us/200: 200 OK
http://httpstat.us/400: 400 Bad Request
http://httpstat.us/404: 404 Not Found
http://httpstat.us/408: 408 Request Timeout
http://httpstat.us/500: 500 Internal Server Error
http://httpstat.us/524: 524 A timeout occurred
Sequential: 0.82 seconds
http://httpstat.us/404: 404 Not Found
http://httpstat.us/200: 200 OK
http://httpstat.us/400: 400 Bad Request
http://httpstat.us/500: 500 Internal Server Error
http://httpstat.us/524: 524 A timeout occurred
http://httpstat.us/408: 408 Request Timeout
Threading: 0.14 seconds
虽然顺序逻辑和线程逻辑处理所有 URL 所需的具体时间可能因系统而异,但两者之间仍应有明确的区别。具体来说,这里我们可以看到线程逻辑的速度几乎是顺序逻辑的六倍(这与我们有六个线程并行处理六个 URL 的事实相对应)。因此,毫无疑问,并发性可以为我们的 ping 测试应用提供显著的加速,尤其是对于一般的 web 请求处理。
ping 测试应用的当前版本按预期工作,但我们可以通过将发出 web 请求的逻辑重构为线程类来提高其可读性。考虑{ To.t0}文件,特别是 AuthT1 类:
# Chapter05/example4.py
import threading
import requests
class MyThread(threading.Thread):
def __init__(self, url):
threading.Thread.__init__(self)
self.url = url
self.result = None
def run(self):
res = requests.get(self.url)
self.result = f'{self.url}: {res.text}'
在本例中,MyThread
继承自threading.Thread
类,并包含两个附加属性:url
和result
。url
属性保存线程实例应该处理的 URL,web 服务器返回给该线程的响应将写入result
属性(在run()
函数中)。
在这个类之外,我们现在可以简单地循环 URL 列表,并相应地创建和管理线程,而不必担心主程序中的请求逻辑:
urls = [
'http://httpstat.us/200',
'http://httpstat.us/400',
'http://httpstat.us/404',
'http://httpstat.us/408',
'http://httpstat.us/500',
'http://httpstat.us/524'
]
start = time.time()
threads = [MyThread(url) for url in urls]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
for thread in threads:
print(thread.result)
print(f'Took {time.time() - start : .2f} seconds')
print('Done.')
请注意,我们现在将响应存储在MyThread
类的result
属性中,而不是像前面示例中的旧ping()
函数那样直接打印出来。这意味着,在确保所有线程都已完成之后,我们将需要再次循环线程并打印这些响应。
重构请求逻辑不应该对我们当前程序的性能产生太大影响;我们正在跟踪执行速度,看看是否真的是这样。执行该程序,您将获得与以下类似的输出:
http://httpstat.us/200: 200 OK
http://httpstat.us/400: 400 Bad Request
http://httpstat.us/404: 404 Not Found
http://httpstat.us/408: 408 Request Timeout
http://httpstat.us/500: 500 Internal Server Error
http://httpstat.us/524: 524 A timeout occurred
Took 0.14 seconds
Done.
正如我们所预期的,我们仍然在使用这种重构的请求逻辑实现程序顺序版本的显著加速。同样,我们的主程序现在更具可读性,请求逻辑的进一步调整(我们将在下一节中看到)可以直接指向MyThread
类,而不会影响程序的其余部分。
在本节中,我们将探讨 ping 测试应用的一个潜在改进:超时处理。当服务器处理特定请求花费异常长的时间,并且服务器与其客户端之间的连接终止时,通常会发生超时。
在 ping 测试应用的上下文中,我们将实现一个定制的超时阈值。回想一下,ping 测试用于确定特定服务器是否仍有响应,因此我们可以在程序中指定,如果请求的响应时间超过服务器的超时阈值,我们将使用超时对特定服务器进行分类。
除了状态代码的不同选项外,httpstat.us网站还提供了一种方法,在我们发送请求时模拟其响应延迟。具体来说,我们可以在GET
请求中使用查询参数自定义延迟时间(以毫秒为单位)。例如,httpstat.us/200?sleep=5000将在延迟 5 秒后返回响应。
现在,让我们看看这样的延迟会如何影响程序的执行。考虑{ OutT0}文件,它包含我们的 ping 测试应用的当前请求逻辑,但具有不同的 URL 列表:
# Chapter05/example5.py
import threading
import requests
class MyThread(threading.Thread):
def __init__(self, url):
threading.Thread.__init__(self)
self.url = url
self.result = None
def run(self):
res = requests.get(self.url)
self.result = f'{self.url}: {res.text}'
urls = [
'http://httpstat.us/200',
'http://httpstat.us/200?sleep=20000',
'http://httpstat.us/400'
]
threads = [MyThread(url) for url in urls]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
for thread in threads:
print(thread.result)
print('Done.')
这里我们有一个 URL,返回响应大约需要 20 秒。考虑到我们将阻塞主程序,直到所有线程完成它们的执行(使用join()
方法),我们的程序很可能会在打印任何响应之前挂起 20 秒。
运行程序,亲自体验这一点。将发生 20 秒的延迟(这将使执行需要更长的时间才能完成),我们将获得以下输出:
http://httpstat.us/200: 200 OK
http://httpstat.us/200?sleep=20000: 200 OK
http://httpstat.us/400: 400 Bad Request
Took 22.60 seconds
Done.
一个高效的 ping 测试应用不应该长时间等待来自其网站的响应;它应该为超时设置一个阈值,如果服务器未能返回低于该阈值的响应,应用将认为该服务器没有响应。因此,我们需要实现一种方法来跟踪自向服务器发送请求以来经过的时间。我们将从超时阈值开始倒数,一旦超过该阈值,所有响应(无论是否返回)都将打印出来。
此外,我们还将跟踪有多少请求仍处于挂起状态,尚未返回响应。我们将使用threading.Thread
类中的isAlive()
方法来间接确定是否已针对特定请求返回响应:如果在某一点上,处理特定请求的线程处于活动状态,我们可以断定该特定请求仍处于挂起状态。
导航到{ To.t0}文件,并首先考虑 ART1 函数:
# Chapter05/example6.py
import time
UPDATE_INTERVAL = 0.01
def process_requests(threads, timeout=5):
def alive_count():
alive = [1 if thread.isAlive() else 0 for thread in threads]
return sum(alive)
while alive_count() > 0 and timeout > 0:
timeout -= UPDATE_INTERVAL
time.sleep(UPDATE_INTERVAL)
for thread in threads:
print(thread.result)
在前面的示例中,该函数接收用于发出 web 请求的线程列表,以及指定超时阈值的可选参数。在这个函数中,我们有一个内部函数alive_count()
,它返回在函数调用时仍处于活动状态的线程的计数。
在process_requests()
函数中,只要存在当前处于活动状态的线程并处理请求,我们将允许线程继续执行(这在具有双条件的while
循环中完成)。如您所见,UPDATE_INTERVAL
变量指定我们检查此条件的频率。如果任一条件失败(如果没有活动线程剩余或如果超过阈值超时),则我们将继续打印响应(即使某些响应可能尚未返回)。
让我们把注意力转向新的MyThread
类:
# Chapter05/example6.py
import threading
import requests
class MyThread(threading.Thread):
def __init__(self, url):
threading.Thread.__init__(self)
self.url = url
self.result = f'{self.url}: Custom timeout'
def run(self):
res = requests.get(self.url)
self.result = f'{self.url}: {res.text}'
这个类与我们在上一个示例中考虑的类几乎相同,只是result
属性的初始值是一条指示超时的消息。在我们前面讨论过的情况下,传递了process_requests()
函数中指定的超时阈值,在打印响应时将使用此初始值。
最后,让我们考虑一下我们的主要计划:
# Chapter05/example6.py
urls = [
'http://httpstat.us/200',
'http://httpstat.us/200?sleep=4000',
'http://httpstat.us/200?sleep=20000',
'http://httpstat.us/400'
]
start = time.time()
threads = [MyThread(url) for url in urls]
for thread in threads:
thread.setDaemon(True)
thread.start()
process_requests(threads)
print(f'Took {time.time() - start : .2f} seconds')
print('Done.')
这里,在我们的 URL 列表中,我们有一个需要 4 秒的请求,另一个需要 20 秒,除了那些会立即响应的请求。由于我们使用的超时阈值是 5 秒,理论上我们应该能够看到延迟 4 秒的请求将成功获得响应,而延迟 20 秒的请求则不会。
关于这个程序还有另外一点需要说明:守护进程线程。在process_requests()
函数中,如果超过超时阈值,但仍有至少一个线程处理,则该函数将继续打印出每个线程的result
属性:
while alive_count() > 0 and timeout > 0:
timeout -= UPDATE_INTERVAL
time.sleep(UPDATE_INTERVAL)
for thread in threads:
print(thread.result)
这意味着,在所有线程使用join()
函数完成执行之前,我们不会阻塞程序,因此,如果达到超时阈值,程序可以简单地向前移动。但是,这意味着线程本身不会在此点终止。具体来说,20 秒延迟请求很可能在程序退出process_requests()
功能后仍在运行。
如果处理该请求的线程不是守护进程线程(正如我们所知,守护进程线程在后台执行并且从不终止),它将阻止主程序完成,直到线程本身完成。通过使这个线程和任何其他线程成为守护进程线程,我们允许主程序在执行其指令的最后一行时立即完成,即使有线程仍在运行。
让我们看看这个计划的实施情况。执行代码,您的输出应与以下内容类似:
http://httpstat.us/200: 200 OK
http://httpstat.us/200?sleep=4000: 200 OK
http://httpstat.us/200?sleep=20000: Custom timeout
http://httpstat.us/400: 400 Bad Request
Took 5.70 seconds
Done.
正如你所看到的,这次我们的程序花了大约 5 秒钟才完成。这是因为它花了 5 秒钟等待仍在运行的线程,一旦超过 5 秒钟的阈值,程序就会打印出结果。这里我们看到,20 秒延迟请求的结果只是MyThread
类的result
属性的默认值,而其余的请求能够从服务器获得正确的响应(包括 4 秒延迟请求,因为它有足够的时间获得响应)。
如果您希望看到我们前面讨论的非守护进程线程的效果,只需注释掉主程序中相应的代码行,如下所示:
threads = [MyThread(url) for url in urls]
for thread in threads:
#thread.setDaemon(True)
thread.start()
process_requests(threads)
您将看到主程序将挂起约 20 秒,因为处理 20 秒延迟请求的非守护进程线程仍在运行,然后才能完成其执行(即使生成的输出相同)。
进行并发 web 请求有几个方面需要仔细考虑和实现。在本节中,我们将介绍这些方面以及开发应用时应该使用的一些最佳实践。
在过去的几年里,未经授权的数据收集一直是技术界讨论的话题,而且在很长一段时间内,这一话题也将继续下去。因此,对于在应用中进行自动 web 请求的开发人员来说,查找网站的数据收集策略是极其重要的。您可以在其服务条款或类似文档中找到这些政策。当有疑问时,通常最好直接联系网站,询问更多细节。
在编程领域,错误是任何人都无法轻易避免的,尤其是在发出 web 请求时。这些程序中的错误可能包括发出错误的请求(无效请求或甚至错误的 internet 连接)、错误处理下载的 HTML 代码或解析 HTML 代码失败。因此,在 Python 中使用try...except
块和其他错误处理工具以避免应用崩溃非常重要。如果您的代码/应用用于生产和更大的应用中,避免崩溃尤其重要。
特别是在并发 web 抓取中,一些线程可能成功收集数据,而另一些线程可能失败。通过在程序的多线程部分实现错误处理功能,可以确保失败的线程不会使整个程序崩溃,并确保成功的线程仍然可以返回其结果。
然而,需要注意的是,盲错误捕获仍然是不可取的。该术语表示程序中有一个大的try...expect
块,它将捕获程序执行过程中发生的任何和所有错误,并且无法获得有关错误的进一步信息;这种做法也可能被称为错误吞咽。强烈建议在程序中使用特定的错误处理代码,这样不仅可以对该特定错误采取适当的操作,而且其他未被考虑的错误也可能暴露出来。
网站经常更改其请求处理逻辑以及显示的数据。如果向网站发出请求的程序与网站服务器交互的逻辑相当不灵活(例如,以特定格式构造其请求,仅处理一种响应),那么如果网站改变其处理客户端请求的方式,程序很可能会停止正常运行。这种情况经常发生在 web 抓取程序中,这些程序在特定的 HTML 标记中查找数据;更改 HTML 标记时,这些程序将无法找到其数据。
实施此做法是为了防止自动数据收集程序正常运行。要继续使用最近更改了请求处理逻辑的网站,唯一的方法就是分析更新的协议并相应地修改我们的程序。
每次我们讨论的一个程序运行时,它都会向管理您要从中提取数据的站点的服务器发出 HTTP 请求。这个过程在并发程序中发生得更频繁,时间更短,多个请求被提交到该服务器。
如前所述,现在的服务器能够轻松地同时处理多个请求。然而,为了避免过度工作和过度消耗资源,服务器也被设计为停止响应过于频繁的请求。大型科技公司的网站,如亚马逊或 Twitter,寻找大量来自同一 IP 地址的自动请求,并实施不同的响应协议;有些请求可能被延迟,有些请求可能被拒绝响应,或者 IP 地址甚至可能被禁止在特定的时间内发出进一步的请求。
有趣的是,重复向服务器发出繁重的请求实际上是一种对网站的黑客攻击。在拒绝服务(DoS)和分布式拒绝服务(DDoS)攻击中,同时向服务器发出大量请求,使目标服务器的带宽充满流量,导致正常,来自其他客户端的非恶意请求被拒绝,因为服务器正忙于处理并发请求,如下图所示:
A of a DDoS attack
因此,隔离应用向服务器发出的并发请求非常重要,这样应用就不会被视为攻击者,并可能被禁止或视为恶意客户端。这可能很简单,比如限制程序中一次可执行的线程/请求的最大数量,或者在向服务器发出请求之前暂停线程一段特定时间(例如,使用time.sleep()
函数)。
在本章中,我们学习了 HTML 和 web 请求的基础知识。两个最常见的 web 请求是GET
和POST
请求。HTTP 响应状态代码有五个主要类别,每个类别都表示服务器与其客户机之间通信的不同概念。通过考虑从不同网站收到的状态代码,我们可以编写一个 ping 测试应用,有效地检查这些网站的响应性。
并发可以应用于通过线程同时发出多个 web 请求的问题,从而显著提高应用速度。但是,在进行并发 web 请求时,一定要记住一些注意事项。
在下一章中,我们将开始讨论并发编程中的另一个主要角色:进程。我们将考虑流程背后的概念和基本思想,以及 Python 为我们提供的处理流程的选项。
- 什么是 HTML?
- 什么是 HTTP 请求?
- 什么是 HTTP 响应状态代码?
requests
模块如何帮助进行 web 请求?- 什么是 ping 测试?ping 测试通常是如何设计的?
- 为什么并发适用于 web 请求?
- 在开发并发 web 请求的应用时,需要考虑哪些因素?
有关更多信息,请参阅以下链接:
- 用 Python 自动化枯燥的东西:面向初学者的实用编程,艾尔·斯维加特,无淀粉出版社,2015 年
- 用 Python 进行网页抓取,Richard Lawson,Packt 出版有限公司,2015
- Java 即时抓取网页,Ryan Mitchell,Packt 出版有限公司,2013