Python-自动化指南-繁琐工作自动化-第三版-八-
Python 自动化指南(繁琐工作自动化)第三版(八)
原文:
automatetheboringstuff.com/译者:飞龙
13 网络爬虫

在那些罕见的、令人恐惧的时刻,当我没有 Wi-Fi 时,我意识到我在电脑上所做的许多事情实际上是在互联网上进行的。出于纯粹的习惯,我会发现自己试图检查电子邮件、阅读社交媒体,或者回答问题:“Kurtwood Smith 在 1987 年原始的 RoboCop 中之前有没有扮演过任何重要的角色?”^(1)
由于在计算机上进行的许多工作都涉及到上网,如果你的程序能够上网那就太好了。网络爬虫 (Web scraping) 是指使用程序从网络上下载和处理内容。例如,谷歌运行了许多网络爬虫程序来为搜索引擎索引网页。在本章中,你将学习以下模块,这些模块使得在 Python 中抓取网页变得容易:
webbrowser Python 内置的模块,可以打开浏览器到指定的页面
requests 下载互联网上的文件和网页
Beautiful Soup (bs4) 解析 HTML,这是网页所使用的格式,以提取你想要的信息
Selenium 启动并控制一个网络浏览器,例如通过填写表单和模拟鼠标点击
Playwright 启动并控制一个网络浏览器;比 Selenium 更新,并有一些额外的功能
HTTP 和 HTTPS
当你访问一个网站时,它的网址,例如 autbor.com/example3.html,被称为 统一资源定位符 (URL)。URL 中的 HTTPS 代表 超文本传输协议安全 (HyperText Transfer Protocol Secure),这是你的网络浏览器用来访问网站的协议。本章中的包允许你的脚本通过此协议访问网络服务器。
更确切地说,HTTPS 是 HTTP 的加密版本,因此在使用互联网时保护了你的隐私。如果你使用的是 HTTP,身份盗贼、国家情报机构和你的互联网服务提供商可以查看你访问的网页的内容,包括你提交的任何密码和信用卡信息。使用虚拟私人网络 (VPN) 可以防止你的互联网服务提供商查看你的互联网流量;然而,现在 VPN 提供商将能够查看你的流量。一个无良的 VPN 提供商可能会然后将你访问的网站信息卖给数据经纪人。(Tom Scott 在他的视频“这个视频由 VPN 赞助”中讨论了 VPN 能做什么和不能做什么。)
>>> import webbrowser
>>> webbrowser.open('https://inventwithpython.com/')
C:\Users\al> showmap 777 Valencia St, San Francisco, CA 94110
要这样做,您需要确定给定街道地址应使用哪个 URL。当您在浏览器中加载www.openstreetmap.org并搜索地址时,地址栏中的 URL 看起来可能如下所示:www.openstreetmap.org/search?query=777%20Valencia%20St%2C%20San%20Francisco%2C%20CA%2094110#map=19/37.75897/-122.42142。
我们可以通过从地址栏中移除#map部分并访问该网站来确认 URL 不需要该部分,以测试它是否仍然可以正确加载。因此,您的程序可以设置为打开网页浏览器到www.openstreetmap.org/search?query=<your_address_string>(其中<your_address_string>是您想要映射的地址)。请注意,您的浏览器会自动处理任何必要的 URL 编码,例如将 URL 中的空格字符转换为%20。
第 2 步:处理命令行参数
让您的代码看起来像这样:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
# TODO: Get address from clipboard.
# TODO: Open the web browser.
首先,您需要导入webbrowser模块以启动浏览器和sys模块以读取潜在的命令行参数。sys.argv变量存储程序的文件名和命令行参数作为一个列表。如果这个列表中除了文件名之外还有其他内容,那么len(sys.argv)评估为一个大于1的整数,这意味着确实提供了命令行参数。
命令行参数通常由空格分隔,但在这个情况下,您将想要将所有参数解释为一个单独的字符串。因为sys.argv是一个字符串列表,您可以将其传递给join()方法,该方法返回一个单个字符串值。您不希望在这个字符串中有程序名称,因此应该传递sys.argv[1:]而不是sys.argv来移除数组的第一元素。这个表达式评估出的最终字符串存储在address变量中。
如果您通过在命令行中输入此内容来运行程序
showmap 777 Valencia St, San Francisco, CA 94110
sys.argv变量将包含此列表值:
['showmap.py', '777', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110']
在您将sys.argv[1:]与空格字符连接后,address变量将包含字符串'777 Valencia St, San Francisco, CA 94110'。
第 3 步:检索剪贴板内容
要从剪贴板获取 URL,让您的代码看起来如下所示:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys, pyperclip
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
else:
# Get address from clipboard.
address = pyperclip.paste()
# Open the web browser.
webbrowser.open('https://www.openstreetmap.org/search?query=' + address)
如果没有命令行参数,程序将假定地址存储在剪贴板上。您可以使用pyperclip.paste()获取剪贴板内容并将其存储在名为address的变量中。最后,为了使用 OpenStreetMap URL 启动网页浏览器,调用webbrowser.open()。
虽然你编写的一些程序可以执行巨大的任务,节省你数小时的时间,但使用一个程序方便地每次执行常见任务(如获取地址的地图)节省几秒钟同样令人满意。表 13-1 比较了使用和未使用showmap.py显示地图所需的步骤。
表 13-1:使用和未使用 showmap.py 获取地图
| 手动获取地图 | 使用 showmap.py |
|---|---|
| 1. 高亮地址。 | 1. 高亮地址。 |
| 2. 复制地址。 | 2. 复制地址。 |
| 3. 打开网页浏览器。 | 3. 运行 showmap.py。 |
4. 前往www.openstreetmap.org |
|
| 5. 点击地址文本框。 | |
| 6. 粘贴地址。 | |
| 7. 按下 ENTER 键。 |
我们很幸运,OpenStreetMap 网站获取地图不需要任何交互;我们只需直接将地址信息放入 URL 中。showmap.py脚本使这项任务不那么繁琐,尤其是如果你经常这样做的话。
类似程序的创意
只要你有 URL,webbrowser模块就允许用户省略打开浏览器并自行导航到网站的一步。其他程序可以使用此功能执行以下操作:
-
在单独的浏览器标签页中打开页面上的所有链接。
-
将浏览器导航到你的本地天气网站的 URL。
-
打开几个你经常检查的社交网络网站或书签网站。
-
在你的硬盘上打开一个本地的.html文件。
最后的建议对于显示帮助文件很有用。虽然你的程序可以使用print()向用户显示帮助页面,但调用webbrowser.open()打开包含帮助信息的.html文件可以让页面有不同的字体、颜色、表格和图像。不要使用https://前缀,而使用file://前缀。例如,你的桌面文件夹应该在 Windows 上的file:///C:/Users/al/Desktop/help.html或 macOS 上的file:///Users/al/Desktop/ help.html中有一个本地的help.html文件。
使用 requests 模块从网络上下载文件
requests模块让你可以轻松地从网络上下载文件,无需担心复杂的网络错误、连接路由和数据压缩等问题。该模块不是 Python 的一部分,所以你需要在附录 A 中的说明指导下安装它才能使用。
下载网页
requests.get()函数接受一个表示要下载的 URL 的字符串。通过在函数的返回值上调用type(),你可以看到它返回一个Response对象,该对象包含网络服务器对你的请求给出的响应。我将在稍后更详细地解释Response对象,但现在,当你的电脑连接到互联网时,在交互式外壳中输入以下内容:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt') # ❶
>>> type(response)
<class 'requests.models.Response'>
>>> response.status_code == requests.codes.ok # ❷
True
>>> len(response.text)
178978
>>> print(response.text[:210])
The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
URL 会带您进入一个包含 罗密欧与朱丽叶 整个文本的网页 ❶。您可以通过检查 Response 对象的 status_code 属性来判断网页请求是否成功。如果它等于 requests.codes.ok 的值,那么一切顺利 ❷。顺便提一下,HTTP 中“OK”的状态码是 200。您可能已经熟悉了“未找到”的 404 状态码。
如果请求成功,下载的网页存储在 Response 对象的 text 变量中作为一个字符串。这个大字符串包含了整个剧本;调用 len(response.text) 会显示它超过 178,000 个字符长。最后,调用 print(response.text[:210]) 只会显示前 210 个字符。
如果请求失败并显示错误消息,如“无法建立新的连接”或“最大重试次数超出”,请检查您的互联网连接。连接到服务器可能相当复杂,我无法在此列出所有可能的问题。您可以通过在引号内搜索错误消息来找到错误的原因。另外,请注意,如果您使用 requests 下载网页,您将只获得网页的 HTML 内容。您必须单独下载图片和其他媒体。
检查错误
正如您所看到的,Response 对象有一个 status_code 属性,您可以将其与 requests.codes.ok 进行比较,以查看下载是否成功。检查成功的一个更简单的方法是在 Response 对象上调用 raise_for_status() 方法。如果下载文件时发生错误,此方法将引发异常;如果下载成功,则不会执行任何操作。在交互式外壳中输入以下内容:
>>> response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
>>> response.raise_for_status()
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
File "C:\Users\Al\AppData\Local\Programs\Python\Python`XX`\lib\site-packages\
requests\models.py", line 940, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
raise_for_status() 方法是确保在发生不良下载时程序停止的一种简单方法。通常,您希望程序在发生一些意外错误时立即停止。如果失败的下载不是决定性的,您可以使用 try 和 except 语句将 raise_for_status() 行包装起来,以处理此错误情况而不会崩溃:
import requests
response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
try:
response.raise_for_status()
except Exception as exc:
print(f'There was a problem: {exc}')
调用 raise_for_status() 方法会导致程序输出以下内容:
There was a problem: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
总是在调用 requests.get() 之后调用 raise_for_status()。在您的程序继续之前,您应该确保下载已经成功。
将下载的文件保存到硬盘
从这里,您可以使用标准的 open() 函数和 write() 方法将网页保存到您的硬盘上的文件中。但是,您必须通过将字符串 'wb' 作为 open() 的第二个参数来以 写入二进制 模式打开文件。即使页面是纯文本(例如您之前下载的 罗密欧与朱丽叶 文本),您也需要写入二进制数据而不是文本数据,以保持文本的 Unicode 编码。
要将网页写入文件,您可以使用 for 循环和 Response 对象的 iter_content() 方法:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt')
>>> response.raise_for_status()
>>> with open('RomeoAndJuliet.txt', 'wb') as play_file:
... for chunk in response.iter_content(100000):
... play_file.write(chunk)
...
100000
78978
iter_content()方法在循环的每次迭代中返回内容“块”。每个块都是字节数据类型,你可以指定每个块将包含多少字节。一百万字节通常是一个很好的大小,所以将100000作为iter_content()的参数传递。
文件RomeoAndJuliet.txt现在存在于当前工作目录中。请注意,虽然网站上的文件名是rj.txt,但硬盘上的文件有不同的文件名。
write()方法返回写入文件的字节数。在上一个例子中,第一块中有 100,000 字节,文件剩余部分只需要 78,978 字节。
这就是requests模块的全部内容!你可以在requests.readthedocs.io/en/latest/了解该模块的其他功能。
如果你想要从 YouTube、Facebook、X(前身为 Twitter)或其他网站下载视频文件,请使用第二十四章中介绍的yt-dlp模块。
访问天气 API
你使用的应用程序是为了与人类用户交互而设计的。然而,你可以编写程序通过它们的应用程序编程接口(API)与其他程序交互,这是定义如何一块软件(例如你的 Python 程序)可以与另一块软件(例如天气网站的 web 服务器)通信的规范。在线服务通常都有 API。例如,你可以编写一个 Python 脚本来发布到你的社交媒体账户或下载新照片。在本节中,我们将编写一个脚本,从免费的 OpenWeather 网站获取天气信息。
几乎所有在线服务都要求你注册一个电子邮件地址才能使用它们的 API。即使 API 是免费的,它们也可能对每小时或每天可以进行的 API 请求次数有限制。如果你担心收到垃圾邮件,可以使用临时、一次性电子邮件地址服务,例如10minutemail.com。请记住,你应该只使用此类服务注册你不在乎的在线账户,因为一个无良的电子邮件服务可能会通过以你的名义发起密码重置请求来控制你的在线账户。
要开始,请在openweathermap.org上注册一个免费账户。免费账户级别限制你每分钟只能进行 60 次 API 请求。这对于你的小型或中型编程项目来说已经足够了。如果你的程序需要超过这个限制(比如说,因为它正在处理来自你的网站数百个同时访问者的请求),你可以购买付费账户级别。在线服务将给你一个API 密钥,这相当于一个密码,用于在 API 请求中识别你的账户。请保密这个 API 密钥!任何拥有这个密钥的人都可以对你的账户进行 API 请求。如果你编写了一个使用 API 密钥的程序,考虑让程序读取一个包含密钥的文本文件,而不是直接在源代码中包含 API 密钥。这样,你就可以与他人分享你的程序(他们可以注册自己的 API 密钥),而不用担心超出你账户的 API 请求限制。
许多 HTTP API 将它们的响应作为一个大字符串提供。这个字符串通常格式化为 JSON 或 XML。第十八章更详细地介绍了 JSON 和 XML,但就目前而言,你只需要知道json.loads(response.text)返回一个包含response.text中 JSON 数据的 Python 数据结构,该结构由列表和字典组成。本章中的示例将此数据存储在一个名为response_data的变量中,但这只是一个任意选择,你可以使用任何你喜欢的变量名。
所有在线服务都记录了如何使用它们的 API。OpenWeather 在其openweathermap.org/api上提供了文档。登录到你的账户并从home.openweathermap.org/api_keys页面获取你的 API 密钥后,在以下交互式 shell 代码中使用它。在这个例子中,我将使用'30ee784a80d81480dab1749d33980112'作为伪造的 API 密钥。不要在你的代码中使用这个伪造的 API 密钥示例;它不会工作。
首先,你将使用 OpenWeather 来查找旧金山的经纬度:
>>> import requests
>>> city_name = 'San Francisco'
>>> state_code = 'CA'
>>> country_code = 'US'
>>> API_key = '30ee784a80d81480dab1749d33980112' # Not a real API key
>>> response = requests.get(f'https://api.openweathermap.org/geo/1.0/
direct?q={city_name},{state_code},{country_code}&appid={API_key}')
>>> response.text # This is a Python string.
'[{"name":"San Francisco","local_names":{"id":"San Francisco",
# --snip--
,"lat":37.7790262,"lon":-122.419906,"country":"US","state":"California"}]'
>>> import json
>>> response_data = json.loads(response.text)
>>> response_data # This is a Python data structure.
[{"name":"San Francisco","local_names":{"id":"San Francisco",
# --snip--
,"lat":37.7790262,"lon":-122.419906,"country":"US","state":"California"}]
要理解响应中的数据,你应该查看 OpenWeather 的在线 API 文档或检查交互式 shell 中的response_data字典。你会了解到响应是一个列表,其第一个项目(索引0)是一个包含'lat'和'lon'键的字典。这些键的值是经纬度的浮点值:
>>> response_data[0]['lat']
37.7790262
>>> response_data[0]['lon']
-122.419906
用于进行 API 请求的特定 URL 称为端点。这个例子中的 f-strings 用变量的值替换了花括号中的部分。上一个例子中的direct?q={city_name},{state_code},{country_code}&appid={API_key}'变成了direct?q=San Francisco,CA,US&appid=30ee784a80d81480dab1749d33980112'。
接下来,你可以使用这些经纬度信息来查找旧金山的当前温度:
>>> lat = json.loads(response.text)[0]['lat']
>>> lon = json.loads(response.text)[0]['lon']
>>> response = requests.get(f'https://api.openweathermap.org/data/2.5/
weather?lat={lat}&lon={lon}&appid={API_key}')
>>> response_data = json.loads(response.text)
>>> response_data
{'coord': {'lon': -122.4199, 'lat': 37.779}, 'weather': [{'id': 803,
# --snip--
'timezone': -25200, 'id': 5391959, 'name': 'San Francisco', 'cod': 200}
>>> response_data['main']['temp']
285.44
>>> round(285.44 - 273.15, 1) # Convert Kelvin to Celsius.
12.3
>>> round(285.44 * (9 / 5) - 459.67, 1) # Convert Kelvin to Fahrenheit.
54.1
注意到 OpenWeather 返回的温度是以开尔文为单位的,因此您需要进行一些数学运算才能得到摄氏度或华氏度的温度。
让我们分解上一个示例中地理位置端点的完整 URL:
https:// 用于访问服务器的 方案,即协议名称(对于在线 API 几乎总是 HTTPS)后跟一个冒号和两个正斜杠。
api.openweathermap.org 处理 API 请求的 Web 服务器的域名。
/geo/1.0/direct API 的路径。
?q={city_name},{state_code},{country_code}&appid={API_key} URL 的查询字符串。花括号内的部分需要替换为实际值;您可以将它们视为函数调用的参数。在 URL 编码中,参数名称和参数值由等号分隔,多个参数-参数对由与号分隔。
您可以将端点 URL(包含完整的查询字符串)粘贴到您的网页浏览器中,以直接查看响应文本。当您刚开始学习如何使用 API 时,这通常是一个很好的做法。基于 Web 的 API 的响应文本通常格式化为 JSON 或 XML。
为了在更新 API 时避免混淆,大多数在线服务都将版本号作为 URL 的一部分。随着时间的推移,一个服务可能会发布 API 的新版本并弃用旧版本。在这种情况下,您将不得不更新脚本中的代码,以便继续使用它们。
OpenWeather 的免费层还提供五天预报以及有关降水、风速和空气污染的信息。文档网页显示了获取这些数据的 URL,以及这些 API 调用的 JSON 响应结构。下一几节中的代码假设您已运行 response_data = json.loads(response.text) 将网站返回的文本转换为 Python 数据结构。
请求经纬度
获取城市经纬度坐标的端点是 api.openweathermap.org/geo/1.0/direct?q={city_name},{state_code},{country_code}&appid={API_key}。州代码是指州的缩写,仅适用于美国城市。国家代码是两到三位字母的 ISO 3166 代码,列在 en.wikipedia.org/wiki/List_of_ISO_3166_country_codes。例如,使用代码 'US' 表示美国或 'NZ' 表示新西兰。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data[0]['lat'] 以浮点值形式保存城市的纬度
response_data[0]['lon'] 保存城市经度,以浮点值表示。
如果城市名称匹配多个响应,response_data 中的列表将包含不同的字典,例如 response_data[0]、response_data[1] 等。如果 OpenWeather 无法定位城市,response_data 将为一个空列表。
获取当前天气
获取基于某些纬度和经度的当前天气信息端点是 api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['weather'][0]['main'] 保存字符串描述,例如 'Clear'、'Rain' 或 'Snow'。
response_data['weather'][0]['description'] 保存更详细的字符串,例如 'light rain'、'moderate rain' 或 'extreme rain'。
response_data['main']['temp'] 保存当前温度,单位为开尔文。
response_data['main']['feels_like'] 保存人类对温度的感知,单位为开尔文。
response_data['main']['humidity'] 保存湿度,以百分比表示。
如果您提供了错误的纬度或经度参数,response_data 将是一个字典,例如 {"cod":"400","message":"wrong latitude"}。
获取天气预报
获取基于某些纬度和经度的五天预报的端点是 api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['list'] 保存包含给定时间天气预报的字典列表。
response_data['list'][0]['dt'] 保存以 Unix 纪元浮点数形式的时间戳。将此值作为参数传递给 datetime.datetime.fromtimestamp() 以获得 datetime 对象。第十九章将更详细地讨论 Python 的 datetime 模块。
response_data['list'][0]['main'] 保存一个包含像 'temp'、'feels_like'、'humidity' 等键的字典。
response_data['list'][0]['weather'][0] 保存包含像 'main'、'description' 等键的描述字典。
response_data['list'] 中的列表包含 40 个字典,以三小时为增量预测未来五天的天气,尽管这可能在 API 的后续版本中有所变化。
探索 API
其他网站,如 weather.gov 和 www.weatherapi.com/,提供他们自己的免费天气 API。每个 API 工作方式不同,但它们通常通过 HTTPS 请求访问,在这种情况下,你可以使用 Requests 库,并将响应格式化为 JSON 或 XML 文本。然而,有人可能已经创建了一个第三方 Python 包,使使用这些 API 更容易,其中包含处理访问端点和为你解析响应的 Python 函数。你可以在 pypi.org 上找到这些包;阅读包文档以了解它们的使用。
理解 HTML
在你拆解网页之前,你必须学习一些 超文本标记语言(HTML) 的基础知识。HTML 是网页的编写格式,而 层叠样式表(CSS) 是一种对网页中 HTML 元素外观进行分类更改的方法。本章假设你已经有了一些基本的 HTML 经验,但如果你需要入门教程,我建议以下网站之一:
在本节中,你还将学习如何访问你网络浏览器的强大开发者工具,这些工具使从网络中抓取信息变得容易得多。
探索格式
HTML 文件是一个带有 .html 扩展名的纯文本文件。这些文件中的文本被 HTML 标签 所包围,标签是位于尖括号 (<>) 中的单词。标签告诉浏览器如何格式化网页。起始标签和结束标签可以包围一些文本,形成一个 HTML 元素。要显示的文本是起始和结束标签之间的内容。例如,以下 HTML 将在浏览器中显示 Hello, world!,其中 Hello 以粗体显示:
<b>Hello</b>, world!
在浏览器中,这段 HTML 将会显示如图 13-1 所示。

图 13-1:在浏览器中渲染的“Hello, world!”
开头 <b> 标签表示包含的文本将以粗体显示。结束 </b> 标签告诉浏览器粗体文本的结束位置。它们共同构成一个元素:<b>Hello</b>。
HTML 中有许多不同的标签。其中一些标签在尖括号内有额外的属性,称为属性。例如,<a>标签包围应作为链接的文本,而href属性确定要链接到的 URL。以下是一个示例:
<a href="https://inventwithpython.com”>This text is a link</a>
一些元素具有用于在页面中唯一标识元素的id属性。您通常会指示您的程序通过id属性查找元素,因此在使用浏览器开发者工具查找此属性是编写网络爬虫程序时的常见任务。
查看网页的源代码
您需要查看您的程序将与之工作的网页的 HTML,称为源代码。为此,在您的网络浏览器中右键单击任何网页(或在 macOS 上按 CTRL 点击),然后选择查看源代码或查看页面源代码(图 13-2)。源代码是浏览器实际接收的文本。浏览器知道如何从 HTML 中显示或渲染网页。

图 13-2:查看网页的源代码
好吧,查看您最喜欢的网站的一些源 HTML。如果您不完全理解您所看到的内容,那没关系。您不需要完全掌握 HTML 来编写简单的网络爬虫程序。您只需要足够的知识来从现有网站中挑选数据。
打开您的浏览器开发者工具
除了查看网页的源代码外,您还可以使用浏览器的开发者工具查看网页的 HTML。在 Firefox、Chrome 和 Microsoft Edge 中,您可以按 F12 键使工具出现(图 13-3)。再次按 F12 键将使它们消失。

图 13-3:Chrome 浏览器中的开发者工具窗口
右键单击网页的任何部分,并在上下文菜单中选择检查元素,以显示该部分页面的 HTML。这将有助于您为网络爬虫程序解析 HTML。
查找 HTML 元素
一旦您的程序使用requests模块下载了网页,您将拥有页面 HTML 内容的单个字符串值。现在您需要弄清楚 HTML 的哪一部分对应于您感兴趣的网页上的信息。
这正是浏览器开发者工具可以提供帮助的地方。比如说,您想编写一个程序从weather.gov获取天气预报数据。在编写任何代码之前,先做一些研究。如果您访问该网站并搜索 94105 邮政编码,它应该会带您到一个显示该地区预报的页面。
如果你对该 ZIP 代码的天气信息感兴趣,请右键单击页面上的该信息(或在 macOS 上 CTRL-单击),然后从出现的上下文菜单中选择检查元素。这会打开开发者工具窗口,显示生成该网页特定部分的 HTML。图 13-4 显示了开发者工具打开到最近的预报的 HTML。请注意,如果 weather.gov 网站更改其网页的设计,你需要重复此过程来检查新元素。

图 13-4:检查包含预报文本的元素
从开发者工具中,你可以看到负责网页预报部分的 HTML 是这样的:
<div class="col-sm-10 forecast-text">Sunny, with a high near 64.
West wind 11 to 16 mph, with gusts as high as 21 mph.</div>
这正是你正在寻找的!看起来预报信息包含在一个具有 forecast-text CSS 类的 <div> 元素中。
在浏览器开发者控制台中右键单击此元素,然后从出现的上下文菜单中选择复制CSS 选择器。此选项将一个如 'div.row-odd:nth-child(1) > div:nth-child(2)' 的字符串复制到剪贴板。你可以将它传递给 Beautiful Soup 的 select() 方法或 Selenium 的 find_element() 方法,正如本章后面所解释的,以在字符串中找到该元素。
此字符串中使用的 CSS 选择器 语法指定了要从网页中检索哪些 HTML 元素。完整的选择器语法超出了本书的范围,但你可以从浏览器开发者工具中获取选择器,就像我们在这里做的那样。XPath 是另一种选择 HTML 元素的语法,但这也超出了本书的范围。
请记住,当网站更改其布局时,你需要更新你的脚本检查的 HTML 标签。这可能会在没有或几乎没有警告的情况下发生,所以请确保密切关注你的程序,以防它突然显示无法找到元素的错误。一般来说,如果网站提供 API,最好使用 API,因为它比网站本身更不可能更改。
使用 Beautiful Soup 解析 HTML
Beautiful Soup 是一个用于从 HTML 页面提取信息的包。你将使用 beautifulsoup4 名称来安装该包,但使用较短的模块名称 bs4 来导入它。在本节中,我们将使用 Beautiful Soup 来 解析(即分析并提取)autbor.com/example3.html 中的 HTML 文件,该文件具有以下内容:
<!-- This is an HTML comment. -->
<html>
<head>
<title>Example Website Title</title>
<style>
.slogan {
color: gray;
font-size: 2em;
}
</style>
</head>
<body>
<h1>Example Website</h1>
<p>This <p> tag puts <b>content</b> into a <i>single</i> paragraph.</p>
<p><a href="https://inventwithpython.com”>This text is a link</a> to books by <span id=
"author">Al Sweigart</span>.</p>
<p><img src="wow_such_zophie_thumb.webp" alt="Close-up of my cat Zophie." /></p>
<p class="slogan">Learn to program in Python!</p>
<form>
<p><label>Username: <input id="login_user" placeholder="admin" /></label></p>
<p><label>Password: <input id="login_pass" type="password" placeholder="swordfish" />
</form>
</label></p>
<p><label>Agree to disagree: <input type="checkbox" /></label><input type="submit"
value="Fake Button" /></p>
</body>
</html>
注意,此页面上的登录表单是假的,仅用于美观。
即使是一个简单的 HTML 文件也涉及许多不同的标签和属性,当涉及到复杂的网站时,事情会很快变得混乱。幸运的是,Beautiful Soup 使处理 HTML 变得容易得多。
创建一个 Beautiful Soup 对象
bs4.BeautifulSoup() 函数接受一个包含它将解析的 HTML 的字符串,然后返回一个 BeautifulSoup 对象。例如,当您的计算机连接到互联网时,在交互式外壳中输入以下内容:
>>> import requests, bs4
>>> res = requests.get('https://autbor.com/example3.html')
>>> res.raise_for_status()
>>> example_soup = bs4.BeautifulSoup(res.text, 'html.parser')
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
此代码使用 requests.get() 下载 Automate the Boring Stuff 网站的主页,然后将响应的 text 属性传递给 bs4.BeautifulSoup()。Beautiful Soup 可以解析不同的格式,'html.parser' 参数告诉它我们正在解析 HTML。最后,代码将返回的 BeautifulSoup 对象存储在名为 example_soup 的变量中。
您还可以通过将 File 对象传递给 bs4.BeautifulSoup() 来从您的硬盘驱动器加载 HTML 文件。在确保 example3.html 文件位于工作目录后,在交互式外壳中输入以下内容:
>>> import bs4
>>> with open('example3.html') as example_file:
... example_soup = bs4.BeautifulSoup(example_file, 'html.parser')
...
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
一旦您有一个 BeautifulSoup 对象,您就可以使用其方法来定位 HTML 文档的特定部分。
查找元素
您可以通过调用 BeautifulSoup 对象的 select() 方法并传递您要查找的元素的 CSS 选择器字符串来从 BeautifulSoup 对象中检索网页元素。该方法返回一个 Tag 对象的列表,这些对象代表匹配的 HTML 元素。表 13-2 显示了使用 select() 的最常见的 CSS 选择器模式的示例。
表 13-2:CSS 选择器示例
| 传递给 select()方法的选择器 | 将匹配... |
|---|---|
soup.select('div') |
所有名为 <div> 的元素 |
soup.select('#author') |
具有名为 author 的 id 属性的元素 |
soup.select('.notice') |
使用 CSS class 属性名为 notice 的所有元素 |
soup.select('div span') |
所有位于名为 <div> 的元素内部的名为 <span> 的元素 |
soup.select('div > span') |
所有直接位于名为 <div> 的元素内部的名为 <span> 的元素,中间没有其他元素 |
soup.select('input[name]') |
所有具有任何值 name 属性的名为 <input> 的元素 |
soup.select('input[type="button"]') |
所有具有名为 type 的属性且其值为 button 的名为 <input> 的元素 |
您可以将各种选择器模式组合起来以进行复杂匹配。例如,soup.select('p #author') 匹配任何具有 id 属性为 author 的元素,只要它也位于 <p> 元素内部。
您可以将标签值传递给 str() 函数以显示它们所代表的 HTML 标签。标签值还具有一个包含所有 HTML 属性的字典的 attrs 属性。例如,下载autbor.com/example3.html页面为 example3.html,然后在交互式外壳中输入以下内容:
>>> import bs4
>>> example_file = open('example3.html')
>>> example_soup = bs4.BeautifulSoup(example_file.read(), 'html.parser')
>>> elems = example_soup.select('#author')
>>> type(elems) # elems is a list of Tag objects.
<class 'bs4.element.ResultSet'>
>>> len(elems)
1
>>> type(elems[0])
<class 'bs4.element.Tag'>
>>> str(elems[0]) # The Tag object as a string
'<span id="author">Al Sweigart</span>'
>>> elems[0].get_text() # The inner text of the element
'Al Sweigart'
>>> elems[0].attrs
{'id': 'author'}
此代码在我们的示例 HTML 中查找具有 id="author" 的元素。我们使用 select('#author') 返回所有具有 id="author" 的元素列表。然后我们将这个 Tag 对象列表存储在变量 elems 中。运行 len(elems) 告诉我们列表中有一个 Tag 对象,这意味着有一个匹配项。
将元素传递给 str() 返回一个包含起始和结束标签以及元素文本的字符串。在元素上调用 get_text() 返回元素的文本,即打开和关闭标签之间的内容:在这种情况下,'Al Sweigart'。最后,attrs 给我们一个包含元素属性 'id' 和 id 属性值的字典。
您也可以从 BeautifulSoup 对象中提取所有 <p> 元素。在交互式外壳中输入以下内容:
>>> p_elems = example_soup.select('p')
>>> str(p_elems[0])
'<p>This <p> tag puts <b>content</b> into a <i>single</i> paragraph.</p>'
>>> p_elems[0].get_text()
'This <p> tag puts content into a single paragraph.'
>>> str(p_elems[1])
'<p> <a href="https://inventwithpython.com/”>This text is a link</a> to books by
<span id="author">Al Sweigart</span>.</p>'
>>> p_elems[1].get_text()
'This text is a link to books by Al Sweigart.'
>>> str(p_elems[2])
'<p><img alt="Close-up of my cat Zophie." src="wow_such_zophie_thumb.webp"/></p>'
>>> p_elems[2].get_text()
''
这次,select() 给我们三个匹配项的列表,我们将其存储在 p_elems 中。使用 str() 对 p_elems[0]、p_elems[1] 和 p_elems[2] 进行操作,您可以看到每个元素作为字符串,使用 get_text() 对每个元素进行操作,您可以看到其文本。
从元素的属性中获取数据
Tag 对象的 get() 方法允许您从元素中访问 HTML 属性值。您需要将一个属性名称作为字符串传递给该方法,并接收该属性值。使用 example3.html 从 autbor.com/example3.html,在交互式外壳中输入以下内容:
>>> import bs4
>>> soup = bs4.BeautifulSoup(open('example3.html'), 'html.parser')
>>> span_elem = soup.select('span')[0]
>>> str(span_elem)
'<span id="author">Al Sweigart</span>'
>>> span_elem.get('id')
'author'
>>> span_elem.get('some_nonexistent_addr') == None
True
>>> span_elem.attrs
{'id': 'author'}
在这里,我们使用 select() 来查找任何 <span> 元素,并将第一个匹配的元素存储在 span_elem 中。将属性名称 'id' 传递给 get() 返回该属性的值,即 'author'。
项目 7:打开所有搜索结果
当我在搜索引擎上查找一个主题时,我不会一次只查看一个搜索结果。通过 中间点击 搜索结果链接(或按住 CTRL 键点击),我会在一堆新标签中打开前几个链接以供稍后阅读。我经常上网搜索,这个工作流程——打开我的浏览器、搜索一个主题,并逐个中间点击几个链接——是繁琐的。如果能简单地输入一个术语到命令行,让我的电脑自动在新浏览器标签中打开顶级搜索结果,那就太好了。
让我们编写一个脚本来完成 Python 包索引搜索结果页面的操作,网址为 pypi.org。您可以将这样的程序适应到许多其他网站上,尽管 Google、DuckDuckGo、Amazon 和其他大型网站通常采取一些措施,使得抓取它们的搜索结果页面变得困难。
这就是程序应该执行的操作:
-
从命令行参数获取搜索关键词
-
获取搜索结果页面
-
为每个结果打开一个浏览器标签
这意味着您的代码需要执行以下操作:
-
从
sys.argv中读取命令行参数。 -
使用
requests模块获取搜索结果页面。 -
找到每个搜索结果的链接。
-
调用
webbrowser.open()函数打开网页浏览器。
打开一个新的文件编辑标签,并将其保存为searchpypi.py。
第 1 步:获取搜索页面
在编写代码之前,你首先需要知道搜索结果页面的 URL。通过在搜索后查看浏览器的地址栏,你可以看到结果页面有一个看起来像这样的 URL:pypi.org/search/?q=<SEARCH_TERM_HERE>。requests模块可以下载这个页面;然后,你可以使用 Beautiful Soup 在 HTML 中找到搜索结果链接。最后,你将使用webbrowser模块在浏览器标签中打开这些链接。
让你的代码看起来像以下这样:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
print('Searching...') # Display text while downloading the search results page.
res = requests.get('https://pypi.org/search/?q=' + ' '.join(sys.argv[1:]))
res.raise_for_status()
# TODO: Retrieve top search result links.
# TODO: Open a browser tab for each result.
用户在启动程序时将指定搜索词作为命令行参数,代码将这些参数作为字符串存储在sys.argv列表中。
第 2 步:查找所有结果
现在你需要使用 Beautiful Soup 从下载的 HTML 中提取顶级搜索结果链接。但你怎么确定正确的选择器呢?例如,你不能仅仅搜索所有的<a>标签,因为在 HTML 中有很多你不需要的链接。相反,你必须使用浏览器的开发者工具检查搜索结果页面,尝试找到一个选择器,以仅选择你想要的链接。
在搜索pyautogui之后,你可以打开浏览器的开发者工具并检查页面上的某些链接元素。它们可能看起来很复杂,就像这样:<a class="package-snippet" href="/project/pyautogui" >。但元素看起来多么复杂并不重要。你只需要找到所有搜索结果链接的共同模式。
让你的代码看起来像以下这样:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
# --snip--
# Retrieve top search result links.
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
如果你查看<a>元素,你会看到搜索结果链接都具有class="package-snippet"。浏览 HTML 源代码的其余部分,看起来package-snippet类仅用于搜索结果链接。你不需要知道 CSS 类package-snippet是什么或它做什么。你只是用它作为你正在寻找的<a>元素的标记。
你可以从下载页面的 HTML 文本创建一个BeautifulSoup对象,然后使用选择器'.package-snippet'找到所有位于具有package-snippet CSS 类的元素内的<a>元素。请注意,如果 PyPI 网站更改了其布局,你可能需要更新此程序,以使用新的 CSS 选择器字符串传递给soup.select()。程序的其他部分应保持最新。
第 3 步:为每个结果打开网络浏览器
最后,你必须告诉程序打开浏览器标签以显示结果。将以下内容添加到你的程序末尾:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
--`snip`--
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
num_open = min(5, len(link_elems))
for i in range(num_open):
url_to_open = 'https://pypi.org' + link_elems[i].get('href')
print('Opening', url_to_open)
webbrowser.open(url_to_open)
默认情况下,程序使用 webbrowser 模块在新标签页中打开前五个搜索结果。然而,用户可能搜索的内容结果少于五个。soup.select() 调用返回了一个匹配您的 '.package-snippet' 选择器的所有元素列表,因此您想要打开的标签页数量是 5 或此列表的长度(取较小者)。
内置的 Python 函数 min() 返回它传递的整数或浮点数参数中的最小值。(还有一个内置的 max() 函数,返回它传递的最大参数。)您可以使用 min() 来找出列表中是否有少于五个链接,并将要打开的链接数量存储在名为 num_open 的变量中。然后,您可以通过调用 range(num_open) 来运行一个 for 循环。
在循环的每次迭代中,代码使用 webbrowser.open() 在网页浏览器中打开一个新标签页。请注意,返回的 <a> 元素中的 href 属性值没有初始的 pypi.org 部分,因此您必须将其连接到 href 属性的字符串值。
现在,您可以通过在命令行中运行 searchpypi boring stuff 立即打开关于“无聊的东西”的前五个 PyPI 搜索结果!请参阅第十二章了解如何在您的操作系统上轻松运行程序。
相似程序的思路
分页浏览的好处是您可以轻松地在新标签页中打开链接以供稍后查看。一个可以一次性打开多个链接的程序可以是一个很好的快捷方式来完成以下操作:
-
在搜索了像 Amazon 这样的购物网站后打开所有产品页面。
-
打开单个产品所有评论的链接。
-
在像 Flickr 或 Imgur 这样的照片网站上执行搜索后打开结果链接。
项目 8:下载 XKCD 漫画
博客、网络漫画和其他定期更新的网站通常都有一个包含最新帖子的首页,以及一个“上一页”按钮,该按钮将您带到上一页。那个帖子也将有一个“上一页”按钮,依此类推,从最新页面创建到网站第一个帖子的路径。如果您想在离线时阅读网站内容,您可以手动浏览每个页面并保存每个页面。但这是一项相当无聊的工作,所以让我们编写一个程序来代替它。
如图 13-5 所示的 XKCD 是一个流行的极客网络漫画,其网站符合这种结构。首页在 xkcd.com 有一个“上一页”按钮,可以引导用户浏览之前的漫画。手动下载每幅漫画将花费很长时间,但您可以在几分钟内编写一个脚本来完成这项工作。

图 13-5:XKCD,“一部关于浪漫、讽刺、数学和语言的网络漫画”
这是您的程序应该执行的操作:
-
加载 XKCD 主页。
-
保存该页面上的漫画图片。
-
点击上一页漫画链接。
-
重复直到达到第一页漫画或最大下载限制。
这意味着你的代码需要执行以下操作:
-
使用
requests模块下载页面。 -
使用 Beautiful Soup 找到页面的漫画图像 URL。
-
使用
iter_content()将漫画图像下载并保存到硬盘上。 -
找到上一页漫画链接的 URL,并重复。
打开一个新的文件编辑标签,并将其保存为 downloadXkcdComics.py。
第 1 步:设计程序
如果你打开浏览器的开发者工具并检查页面上的元素,你应该会发现以下情况是正确的:
-
<img>元素的src属性存储了漫画图像文件的 URL。 -
<img>元素位于一个<div id="comic">元素内部。 -
上一页按钮有一个值为
prev的relHTML 属性。 -
最古老的漫画的上一页按钮链接到
xkcd.com/#URL,这表明没有更多的上一页。
为了防止本书的读者消耗过多的 XKCD 网站带宽,让我们默认限制下载次数为 10 次。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# TODO: Download the page.
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
程序创建一个以 'https://xkcd.com' 开头的 url 变量,并在 while 循环中重复更新它(使用当前页面的上一页链接的 URL)。在循环的每一步中,你将下载 url 上的漫画。循环在 url 以 '#' 结尾或你已下载 MAX_DOWNLOADS 漫画时停止。
你将下载的图像文件将保存在当前工作目录中的一个名为 xkcd 的文件夹中。os.makedirs() 调用确保该文件夹存在,exist_ok=True 关键字参数防止函数在文件夹已创建时抛出异常。
第 2 步:下载网页
让我们实现下载页面的代码。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# Download the page.
print(f'Downloading page {url}...')
res = requests.get(url)
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
首先,打印 url 以让用户知道程序将要下载哪个 URL;然后,使用 requests 模块的 requests.get() 函数下载它。像往常一样,你应该立即调用 Response 对象的 raise_for_status() 方法,如果下载过程中出现问题,则抛出异常并结束程序。否则,从下载的页面文本中创建一个 BeautifulSoup 对象。
第 3 步:查找并下载漫画图像
为了下载每页的漫画,让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Find the URL of the comic image.
comic_elem = soup.select('#comic img')
if comic_elem == []:
print('Could not find comic image.')
else:
comic_URL = 'https:' + comic_elem[0].get('src')
# Download the image.
print(f'Downloading image {comic_URL}...')
res = requests.get(comic_URL)
res.raise_for_status()
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
由于你使用开发者工具检查了 XKCD 首页,你知道漫画图像的 <img> 元素位于一个设置了 id 属性为 comic 的另一个元素内部,因此选择器 '#comic img' 将从 BeautifulSoup 对象中获取正确的 <img> 元素。
一些 XKCD 页面有特殊内容,不是简单的图像文件。这没关系;你只需跳过那些页面。如果你的选择器找不到任何元素,soup.select('#comic img')将返回一个空列表的ResultSet对象。当这种情况发生时,程序可以只打印一条错误消息并继续,而无需下载图像。
否则,选择器将返回一个包含一个<img>元素的列表。你可以从这个<img>元素获取src属性,并将其传递给requests.get()以下载漫画的图像文件。
第 4 步:保存图像并查找上一则漫画
到目前为止,漫画的图像文件存储在res变量中。你需要将此图像数据写入硬盘上的文件。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Save the image to ./xkcd.
image_file = open(os.path.join('xkcd', os.path.basename(comic_URL)), 'wb')
for chunk in res.iter_content(100000):
image_file.write(chunk)
image_file.close()
# Get the Prev button's URL.
prev_link = soup.select('a[rel="prev"]')[0]
url = 'https://xkcd.com' + prev_link.get('href')
num_downloads += 1
time.sleep(1) # Pause so we don't hammer the web server.
print('Done.')
你还需要一个本地图像文件的文件名,以便传递给open()。comic_URL将具有类似于'https://imgs.xkcd.com/comics/heartbleed_explanation.png'的值,你可能已经注意到它看起来很像一个文件路径。实际上,你可以使用os.path.basename()和comic_URL来返回 URL 的最后一部分,即'heartbleed_explanation.png',并在将图像保存到硬盘时使用这个文件名。使用os.path.join()将此名称与你的xkcd文件夹名称连接起来,这样你的程序在 Windows 上使用反斜杠(\),在 macOS 和 Linux 上使用正斜杠(/)。现在你终于有了文件名,你可以调用open()以在'wb'模式下打开一个新文件。
记住,在本章前面提到,为了保存使用requests下载的文件,你需要遍历iter_content()方法的返回值。for循环中的代码将图像数据块写入文件。然后,代码关闭文件,将图像保存到你的硬盘上。
之后,选择器'a[rel="prev"]'识别具有rel属性设置为prev的<a>元素。你可以使用这个<a>元素的href属性来获取上一则漫画的 URL,并将其存储在url中。
循环代码的最后部分将num_downloads增加1,这样就不会默认下载所有漫画。它还通过time.sleep(1)引入了一秒的暂停,以防止脚本“猛击”网站(即不礼貌地尽可能快地下载漫画,这可能会对其他网站访客造成性能问题)。然后,while循环再次开始整个下载过程。
这个程序的输出将看起来像这样:
Downloading page https://xkcd.com...
Downloading image https://imgs.xkcd.com/comics/phone_alarm.png...
Downloading page https://xkcd.com/1358/...
Downloading image https://imgs.xkcd.com/comics/nro.png...
Downloading page https://xkcd.com/1357/...
Downloading image https://imgs.xkcd.com/comics/free_speech.png...
Downloading page https://xkcd.com/1356/...
Downloading image https://imgs.xkcd.com/comics/orbital_mechanics.png...
Downloading page https://xkcd.com/1355/...
Downloading image https://imgs.xkcd.com/comics/airplane_message.png...
Downloading page https://xkcd.com/1354/...
Downloading image https://imgs.xkcd.com/comics/heartbleed_explanation.png...
--`snip`--
这个项目是一个很好的例子,说明了一个可以自动跟随链接以从网络中抓取大量数据的程序。你可以从其文档中了解 Beautiful Soup 的其他功能,文档地址为www.crummy.com/software/BeautifulSoup/bs4/doc/。
相似程序的创意
许多网络爬虫程序涉及下载页面和跟随链接。类似的程序可以执行以下操作:
-
通过跟随所有链接来备份整个网站。
-
复制一个网络论坛上的所有消息。
-
复制在线商店上出售的商品目录。
requests 和 bs4 模块在你能找出需要传递给 requests.get() 的 URL 时非常出色。然而,这个 URL 并非总是那么容易找到。或者,也许你想要你的程序导航的网站要求你先登录。Selenium 将赋予你的程序执行此类复杂任务的能力。
使用 Selenium 控制浏览器
Selenium 允许 Python 通过编程方式点击链接和填写表单来直接控制浏览器,就像人类用户一样。使用 Selenium,你可以以比使用 requests 和 Beautiful Soup 更高级的方式与网页交互;但因为它启动了一个网络浏览器,所以它有点慢,如果你只是需要从网络上下载一些文件,那么在后台运行就有点困难了。
尽管如此,如果你需要以某种方式与网页交互,例如依赖于更新网页的 JavaScript 代码,那么你需要使用 Selenium 而不是 requests。这是因为像亚马逊这样的主要电子商务网站几乎肯定有软件系统来识别他们怀疑是脚本抓取他们的信息或注册多个免费账户的流量。这些网站可能会在一段时间后拒绝为你提供服务,从而破坏你编写的任何脚本。与 requests 相比,Selenium 在这些网站上长期运行的可能性要大得多。
使用脚本的网站的一个主要“迹象”是 user-agent 字符串,它标识了网络浏览器,并包含在所有 HTTP 请求中。例如,requests 模块的用户代理字符串可能类似于 'python-requests/X.XX.X'。你可以访问像 www.whatsmyua.info 这样的网站来查看你的用户代理字符串。使用 Selenium,你更有可能被当作人类,因为 Selenium 的用户代理与常规浏览器相同(例如,' Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0'),并且具有相同的流量模式:Selenium 控制的浏览器将下载图片、广告、cookies 和侵犯隐私的跟踪器,就像常规浏览器一样。然而,网站仍然可以找到方法来检测 Selenium,并且主要的票务和电子商务网站通常阻止它,以防止其页面被网络爬虫抓取。
启动 Selenium 控制的浏览器
以下示例将向您展示如何控制 Firefox 的网络浏览器。如果您还没有 Firefox,您可以从 getfirefox.com 免费下载。
导入 Selenium 模块稍微有些棘手。你不能使用import selenium,而必须运行from selenium import webdriver。 (Selenium 为何设置为这种方式的具体原因超出了本书的范围。)之后,你可以使用 Selenium 启动 Firefox 浏览器。在交互式 shell 中输入以下内容:
>>> from selenium import webdriver
>>> browser = webdriver.Firefox()
>>> type(browser)
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
>>> browser.get('https://inventwithpython.com')
你会注意到,当调用webdriver.Firefox()时,Firefox 网络浏览器会启动。在webdriver.Firefox()的值上调用type()会显示它属于WebDriver数据类型。并且调用browser.get('https://inventwithpython.com')会将浏览器导向inventwithpython.com。你的浏览器应该看起来像图 13-6 所示。

图 13-6:在 Mu 中调用 webdriver.Firefox()和 get()之后,Firefox 浏览器出现。
如果你遇到错误消息“geckodriver 可执行文件需要位于 PATH 中”,在使用 Selenium 控制它之前,你需要手动下载 Firefox 的 WebDriver。如果你为其他浏览器安装了 WebDriver,你也可以控制这些浏览器,并且你可以使用来自pypi.org/project/webdriver-manager/的 webdriver-manager 包来代替手动下载浏览器 WebDriver。
点击浏览器按钮
Selenium 可以通过以下方法模拟点击各种浏览器按钮:
browser.back() 点击后退按钮
browser.forward() 点击前进按钮
browser.refresh() 点击刷新/重新加载按钮
browser.quit() 点击关闭窗口按钮
在页面上查找元素
WebDriver对象有find_element()和find_elements()方法,用于在网页上查找元素。find_element()方法返回一个代表页面上与你的查询匹配的第一个元素的WebElement对象。find_elements()方法返回一个包含页面上所有匹配元素的WebElement对象列表。
你可以通过它们的类名、CSS 选择器、ID 或其他方式来查找元素。首先,运行from selenium.webdriver.common.by import By以获取By对象。By对象有几个常量可以传递给find_element()和find_elements()方法。表 13-3 列出了这些常量。
表 13-3:Selenium 用于查找元素的By常量
| 常量名称 | 返回的 WebElement 对象/列表 |
|---|---|
By.CLASS_NAME |
使用 CSS 类名的元素 |
By.CSS_SELECTOR |
匹配 CSS 选择器的元素 |
By.ID |
具有匹配的 id 属性值的元素 |
By.LINK_TEXT |
完全匹配提供文本的<a>元素 |
By.PARTIAL_LINK_TEXT |
包含提供文本的<a>元素 |
By.NAME |
具有匹配的 name 属性值的元素 |
By.TAG_NAME |
具有匹配标签名的元素(不区分大小写;<a> 元素由 'a' 和 'A' 匹配) |
如果页面上没有与该方法查找的内容匹配的元素,Selenium 会引发 NoSuchElement 异常。如果您不希望这个异常使您的程序崩溃,请向您的代码中添加 try 和 except 语句。
一旦您有了 WebElement 对象,您可以通过阅读表 13-4 中的属性或调用方法来了解更多关于它的信息。
表 13-4:WebElement 属性和方法
| 属性或方法 | 描述 |
|---|---|
tag_name |
标签名,例如 <a> 元素的 'a'。 |
get_attribute(name) |
元素的 name 属性的值,例如 <a> 元素中的 href。 |
get_property(name) |
元素的属性值,该属性值不出现在 HTML 代码中。一些 HTML 属性的示例包括 innerHTML 和 innerText。 |
text |
元素内的文本,例如以下示例中的 'hello':<span>hello</span> |
clear() |
对于文本字段或文本区域元素,清除其中输入的文本。 |
is_displayed() |
如果元素可见,则返回 True;否则返回 False。 |
is_enabled() |
对于输入元素,如果元素是启用的,则返回 True;否则返回 False。 |
is_selected() |
对于复选框或单选按钮元素,如果元素被选中,则返回 True;否则返回 False。 |
location |
一个字典,包含 'x' 和 'y' 键,用于元素在页面中的位置。 |
size |
一个字典,包含 'width' 和 'height' 键,用于元素在页面中的大小。 |
例如,打开一个新的文件编辑标签,并输入以下程序:
from selenium import webdriver
from selenium.webdriver.common.by import By
browser = webdriver.Firefox()
browser.get('https://autbor.com/example3.html')
elems = browser.find_elements(By.CSS_SELECTOR, 'p')
print(elems[0].text)
print(elems[0].get_property('innerHTML'))
在这里,我们打开 Firefox 并将其指向一个 URL。在这个页面上,我们获取一个 <p> 元素的列表,查看索引为 0 的第一个元素,然后获取该 <p> 元素内文本的字符串。接下来,我们获取其 innerHTML 属性的字符串。此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
元素的 text 属性显示的文本是我们会在网页浏览器中看到的文本:“这个 <p> 标签将内容放入一个单独的段落中。”我们还可以通过调用 get_property() 方法来检查元素的 innerHTML 属性,这是包含标签和 HTML 实体的 HTML 源代码。(< 和 > 是 HTML 转义字符,代表小于 [<] 和大于 [>] 字符。)
注意,text 属性只是调用 get_property('innerText') 的快捷方式。innerHTML 和 innerText 是 HTML 元素的属性的标准名称。简而言之,这些元素属性是通过 JavaScript 代码和 Web 驱动程序访问的,而元素属性是 HTML 源代码的一部分,例如 <a href="https://inventwithpython.com"> 中的 href。
在页面上点击元素
从 find_element() 和 find_elements() 方法返回的 WebElement 对象具有一个 click() 方法,该方法模拟在该元素上点击鼠标。此方法可用于跟随链接、在单选按钮上做出选择、点击提交按钮或触发鼠标点击元素时可能发生的任何其他操作。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> link_elem = browser.find_element(By.LINK_TEXT, 'This text is a link')
>>> type(link_elem)
<class 'selenium.webdriver.remote.webelement.WebElement'>
>>> link_elem.click() # Follows the "This text is a link" link
此代码打开 Firefox,访问 autbor.com/example3.html,获取文本为 This is a link 的 <a> 元素的 WebElement 对象,然后模拟点击该 <a> 元素,就像您亲自点击链接一样;浏览器随后会跟随该链接。
填写和提交表单
向网页上的文本字段发送按键是找到该文本字段的 <input> 或 <textarea> 元素,然后调用 send_keys() 方法的问题。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> user_elem = browser.find_element(By.ID, 'login_user')
>>> user_elem.send_keys('`your_real_username_here`')
>>> password_elem = browser.find_element(By.ID, 'login_pass')
>>> password_elem.send_keys('`your_real_password_here`')
>>> password_elem.submit()
只要登录页面没有更改用户名和密码 <input> 元素的 id,之前的代码就会用提供的文本填写这些文本字段。(您始终可以使用浏览器的检查器来验证 id。)在任意元素上调用 submit() 方法将产生与点击该元素所在的表单的提交按钮相同的结果。(您也可以调用 user_elem.submit(),代码会执行相同的事情。)
警告
尽可能避免在源代码中放置密码。当密码在硬盘上未加密时,很容易不小心将密码泄露给他人。
发送特殊按键
Selenium 有一个模块 selenium.webdriver.common.keys,用于表示键盘按键,并将其存储在属性中。由于该模块具有如此长的名称,在程序顶部运行 from selenium.webdriver.common.keys import Keys 会容易得多;如果您这样做,您可以在任何通常需要写入 selenium.webdriver.common.keys 的地方简单地写入 Keys。
您可以向 send_keys() 方法传递以下任何常量:
Keys.ENTER Keys.PAGE_UP Keys.DOWN
Keys.RETURN Keys.ESCAPE Keys.LEFT
Keys.HOME Keys.BACK_SPACE Keys.RIGHT
Keys.END Keys.DELETE Keys.TAB
Keys.PAGE_DOWN Keys.UP Keys.F1 to Keys.F12
您还可以向方法传递一个字符串,例如 'hello' 或 '?'。
例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别将浏览器滚动到页面顶部和底部。在交互式外壳中输入以下内容,并注意 send_keys() 调用如何滚动页面:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> from selenium.webdriver.common.keys import Keys
>>> browser = webdriver.Firefox()
>>> browser.get('https://nostarch.com')
>>> html_elem = browser.find_element(By.TAG_NAME, 'html')
>>> html_elem.send_keys(Keys.END) # Scrolls to bottom
>>> html_elem.send_keys(Keys.HOME) # Scrolls to top
<html> 标签是 HTML 文件中的基础标签:HTML 文件的全部内容都包含在 <html> 和 </html> 标签之间。调用 browser.find_element(By.TAG_NAME, 'html') 是通过主 <html> 标签向整个网页发送键值的好方法。例如,当你滚动到页面底部并加载新内容时,这会很有用。
Selenium 的功能远不止这里所描述的。它可以修改你的浏览器 cookies,截取网页截图,并运行自定义 JavaScript。要了解更多关于这些功能的信息,你可以访问 Selenium 文档,网址为selenium-python.readthedocs.io。你还可以通过搜索网站pyvideo.org*来找到关于 Selenium 的 Python 会议演讲。
使用 Playwright 控制浏览器
Playwright 是一个类似于 Selenium 的浏览器控制库,但它更新。虽然它可能目前没有 Selenium 那么广泛的受众,但它确实提供了一些值得学习的新特性。其中最重要的新特性是能够在无头模式下运行,这意味着你可以模拟浏览器而无需在屏幕上打开浏览器窗口。这使得它在后台运行自动化测试或网络爬虫作业时非常有用。Playwright 的完整文档在playwright.dev/python/docs/intro。
此外,与 Selenium 相比,使用 Playwright 安装单个浏览器的驱动程序更容易:只需在 Windows 上运行python -m playwright install,在 macOS 和 Linux 上从终端窗口运行python3 –m playwright install,即可安装 Firefox、Chrome 和 Safari 的驱动程序。由于 Playwright 在其他方面与 Selenium 相似,因此我不会在本节中介绍通用的网络爬虫和 CSS 选择器信息。
启动 Playwright 控制的浏览器
一旦安装了 Playwright,你可以使用以下程序进行测试:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch()
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
print(page.title())
browser.close()
当程序运行时,它会暂停以加载 Firefox 浏览器和autbor.com/example3.html网站,然后打印其标题,“示例网站。”你还可以使用playwright.chromium.launch()或playwright.webkit.launch()来分别使用 Chrome 和 Safari 浏览器。
Playwright 在执行进入和退出with语句块时,会自动调用start()和stop()方法。Playwright 有一个同步模式,其中其函数在操作完成之前不会返回。这样,你就不会不小心在页面加载完成之前告诉浏览器查找元素。Playwright 的异步特性超出了本书的范围。
你可能已经注意到,根本没有任何浏览器窗口出现,因为默认情况下,Playwright 以无头模式运行。这一点,加上 Playwright 如何将其代码放入with语句中,可能会使调试变得复杂。要逐步运行 Playwright,请在交互式 shell 中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> browser.close()
>>> playwright.stop()
playwright.firefox.launch() 函数的 headless=False 和 slow_mo=50 关键字参数使得浏览器窗口出现在你的屏幕上,并在其操作中添加 50 毫秒的延迟,这样你更容易看到正在发生的事情。你不必担心添加暂停以给网页加载时间:Playwright 在完成上一个操作之前不会继续进行新操作,这比 Selenium 要好得多。
new_page() 浏览器方法返回的 Page 对象代表一个新浏览器窗口中的新标签页。使用 Playwright 时,你可以同时打开多个浏览器窗口。
点击浏览器按钮
Playwright 可以通过在 browser.new_page() 返回的 Page 对象上调用以下方法来模拟点击浏览器按钮:
page.go_back() 点击后退按钮
page.go_forward() 点击前进按钮
page.reload() 点击刷新/重新加载按钮
page.close() 点击关闭窗口按钮
在页面上查找元素
Playwright 有一些被称为 定位器 的 Page 对象方法,这些方法返回 Locator 对象,代表网页上可能的 HTML 元素。我说 可能的 是因为,虽然 Selenium 如果找不到你请求的元素会立即抛出错误,但 Playwright 理解页面可能会稍后动态创建该元素。这很有用,但也有一些不太幸运的副作用:如果你指定的元素不存在,Playwright 会暂停 30 秒,等待元素出现。
但如果你只是犯了一个拼写错误,这个 30 秒的暂停会显得很繁琐。为了立即检查元素是否存在于页面上并且可见,请在定位器返回的 Locator 对象上调用 is_visible() 方法。你也可以调用 page.query_selector('selector'),其中 selector 是元素的 CSS 或 XPath 选择器字符串。page.query_selector() 方法会立即返回,如果它返回 None,则表示当前页面上不存在该元素。Locator 对象可能匹配网页上的一个或多个 HTML 元素。表 13-5 包含 Playwright 的定位器。
表 13-5:Playwright 的查找元素定位器
| 定位器 | 返回的定位器对象 |
|---|---|
page.get_by_role(role, name=label) |
通过其角色和可选的标签来定位元素 |
page.get_by_text(text) |
作为其内部文本一部分包含文本的元素 |
page.get_by_label(label) |
具有与 <label> 文本匹配的标签的元素 |
page.get_by_placeholder(text) |
与提供的文本匹配的 <input> 和 <textarea> 元素,具有匹配的 placeholder 属性值 |
page.get_by_alt_text(text) |
具有与提供的文本匹配的 alt 属性值的 <img> 元素 |
page.locator(selector) |
具有匹配 CSS 或 XPath 选择器的元素 |
get_by_role() 方法利用 Accessible Rich Internet Applications (ARIA) 角色,这是一套标准,它使软件能够识别网页内容以适应有视力或其他残疾的用户。例如,“标题”角色适用于 <h1> 到 <h6> 标签,其中 <h1> 和 </h1> 之间的文本是你可以使用 get_by_role() 方法的 name 关键字参数识别的文本。(ARIA 角色的内容远不止这些,但这个主题超出了本书的范围。)
你可以使用起始和结束标签之间的文本来定位元素。调用 page.get_by_text('is a link') 会定位到 <a href="https://inventwithpython.com">This text is a link</a> 中的 <a> 元素。通常,部分、不区分大小写的文本匹配就足够定位元素。
page.get_by_label() 方法通过 <label> 和 </label> 标签之间的文本来定位元素。例如,page.get_by_label('Agree') 会定位到 <label>Agree to disagree: <input type="checkbox" /></label> 中的 <input> 复选框元素。
<input> 和 <textarea> 标签可以有一个 placeholder 属性来显示占位文本,直到用户输入实际文本。例如,page.get_by_placeholder('admin') 会定位到 <input id="login_user" placeholder="admin" /> 的 <input> 元素。
网页上的图片可以在它们的 alt 属性中包含 alt 文本来描述图片内容,以便向视力受损的用户描述。一些浏览器在将鼠标光标悬停在图片上时显示 alt 文本作为工具提示。page.get_by_alt_text('Zophie') 调用会返回 <img src="wow_such_zophie_thumb.webp" alt="Close-up of my cat Zophie." /> 中的 <img> 元素。
如果你只需要通过 CSS 选择器获取一个 Locator 对象,请调用 locator() 定位器并传递选择器字符串。这与 Selenium 的 find_elements() 方法与 By.CSS_SELECTOR 常量类似。
表 13-6: Locator 方法
| 方法 | 描述 |
|---|---|
get_attribute(name) |
返回元素名称属性的值,例如 <a href="https://nostarch.com"> 元素中的 href 属性的 'https://nostarch.com'。 |
count() |
返回此 Locator 对象中匹配元素的数量。 |
nth(index) |
返回给定索引的匹配元素的 Locator 对象。例如,nth(3) 返回第四个匹配元素,因为索引 0 是第一个匹配元素。 |
first |
第一个匹配元素的 Locator 对象。这与 nth(0) 相同。 |
last |
最后匹配元素的 Locator 对象。如果有,比如说,五个匹配元素,这相当于 nth(4)。 |
all() |
返回每个单独匹配元素的 Locator 对象列表。 |
inner_text() |
返回元素内的文本,例如 <b>hello</b> 中的 'hello'。 |
inner_html() |
返回元素内的 HTML 源代码,例如 <b>hello</b> 在 <b>hello</b> 中。 |
click() |
模拟对元素的点击,这对于链接、复选框和按钮元素很有用。 |
is_visible() |
如果元素是可见的,则返回 True;否则返回 False。 |
is_enabled() |
对于输入元素,如果元素是启用的,则返回 True;否则返回 False。 |
is_checked() |
对于复选框或单选按钮元素,如果元素被选中,则返回 True;否则返回 False。 |
bounding_box() |
返回一个字典,包含 'x' 和 'y' 键,表示元素左上角在页面上的位置,以及 'width' 和 'height' 键,表示元素的大小。 |
由于 Locator 对象可以表示多个元素,你可以使用 nth() 方法通过传递零基索引来获取单个元素的 Locator 对象。例如,打开一个新的文件编辑标签并输入以下程序:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch(headless=False, slow_mo=50)
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
elems = page.locator('p')
print(elems.nth(0).inner_text())
print(elems.nth(0).inner_html())
与 Selenium 示例类似,此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
page.locator('p') 代码返回一个匹配网页中所有 <p> 元素的 Locator 对象,而 nth(0) 方法调用返回一个仅匹配第一个 <p> 元素的 Locator 对象。Locator 对象还有一个 count() 方法,用于返回定位器中匹配元素的数量(类似于 Python 列表的 len() 函数)。还有 first 和 last 属性,它们包含匹配第一个或最后一个元素的定位器。如果你想要每个单独匹配元素的 Locator 对象列表,请调用 all() 方法。
一旦你有了元素的 Locator 对象,你可以按照下一节所述在它们上执行鼠标点击和按键操作。
在页面上点击元素
Page 对象有 click()、check()、uncheck() 和 set_checked() 方法,用于模拟对链接、按钮和复选框元素的点击。你可以调用这些方法并传递元素的 CSS 或 XPath 选择器字符串,或者你可以使用 Playwright 的 Locator 函数(如表 13-6 所示)。在交互式外壳中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.click('input[type=checkbox]') # Checks the checkbox
>>> page.click('input[type=checkbox]') # Unchecks the checkbox
>>> page.click('a') # Clicks the link
>>> page.go_back()
>>> checkbox_elem = page.get_by_role('checkbox') # Calls a Locator method
>>> checkbox_elem.check() # Checks the checkbox
>>> checkbox_elem.uncheck() # Unchecks the checkbox
>>> checkbox_elem.set_checked(True) # Checks the checkbox
>>> checkbox_elem.set_checked(False) # Unchecks the checkbox
>>> page.get_by_text('is a link').click() # Uses a Locator method
>>> browser.close()
>>> playwright.stop()
check() 和 uncheck() 方法比 click() 方法对复选框更可靠。click() 方法将复选框切换到相反的状态,而 check() 和 uncheck() 方法无论之前的状态如何,都会保留复选框的选中或未选中状态。同样,set_checked() 方法允许你传递 True 来选中复选框或传递 False 来取消选中。
填写和提交表单
Locator 对象有一个 fill() 方法,它接受一个字符串并将文本填充到 <input> 或 <textarea> 元素中。这对于填写在线表单很有用,例如我们 example3.html 网页中的登录表单:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('#login_user').fill('`your_real_username_here`')
>>> page.locator('#login_pass').fill('`your_real_password_here`')
>>> page.locator('input[type=submit]').click()
>>> browser.close()
>>> playwright.stop()
此外,还有一个clear()方法,它将擦除元素中当前的所有文本。与 Selenium 不同,Playwright 中没有submit()方法,你需要在其Locator对象上调用click()来匹配提交按钮的元素。
发送特殊按键
你还可以使用Locator对象的press()方法在网页元素上模拟键盘按键。例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别滚动浏览器到页面的顶部和底部。在交互式外壳中输入以下内容,并注意press()调用如何滚动页面:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('html').press('End') # Scrolls to bottom
>>> page.locator('html').press('Home') # Scrolls to top
>>> browser.close()
>>> playwright.stop()
你传递给press()的字符串可以包括单个字符字符串(如'a'或'?');修改键'Shift'、'Control'、'Alt'或'Meta'(如在'Control+A'中,表示 CTRL-A);以及以下任何一种:
'Backquote' 'Escape' 'ArrowDown'
'Minus' 'End' 'ArrowRight'
'Equal' 'Enter' 'ArrowUp'
'Backslash' 'Home' 'F1' to 'F12'
'Backspace' 'Insert' 'Digit0' to 'Digit9'
'Tab' 'PageUp' 'KeyA' to 'KeyZ'
'Delete' 'PageDown'
戏剧作家可以做的远不止这里描述的功能。要了解更多这些功能,你可以访问playwright.dev上的 Playwright 文档。你还可以通过pyvideo.org搜索到关于 Playwright 的 Python 会议演讲。
摘要
最无聊的任务并不仅限于你电脑上的文件。能够以编程方式下载网页将扩展你的程序到互联网。requests模块使下载变得简单,并且通过一些基本的 HTML 概念和选择器知识,你可以利用BeautifulSoup模块来解析你下载的页面。
但要完全自动化任何基于 Web 的任务,你需要通过 Selenium 和 Playwright 包直接控制你的网络浏览器。这些包将允许你自动登录网站并填写表单。因为网络浏览器是发送和接收互联网上信息最常见的方式,所以这在你程序员的工具包中是一个很棒的能力。
实践问题
-
简要描述
webbrowser、requests和bs4模块之间的区别。 -
requests.get()返回什么类型的对象?你如何将下载的内容作为字符串值访问? -
哪个
requests方法检查下载是否成功? -
你如何获取
requests响应的 HTTP 状态码? -
你如何将
requests响应保存到文件中? -
大多数在线 API 以哪两种格式返回其响应?
-
打开浏览器开发者工具的键盘快捷键是什么?
-
你如何在开发者工具中查看网页上特定元素的 HTML?
-
什么 CSS 选择器字符串可以找到具有
id属性为main的元素? -
什么 CSS 选择器字符串可以找到具有
id属性为highlight的元素? -
假设你有一个 Beautiful Soup
Tag对象存储在变量spam中,对应元素<div>Hello, world!</div>。你如何从Tag对象中获取字符串'Hello, world!'? -
你会如何将 Beautiful Soup
Tag对象的所有属性存储在名为link_elem的变量中? -
运行
import selenium不起作用。你应该如何正确导入 Selenium? -
Selenium 中的
find_element()和find_elements()方法之间的区别是什么? -
Selenium 的
WebElement对象有哪些方法可以模拟鼠标点击和键盘按键? -
在 Playwright 中,哪种定位器方法调用可以模拟按下 CTRL-A 选择页面上的所有文本?
-
你如何使用 Selenium 模拟点击浏览器的“前进”、“后退”和“刷新”按钮?
-
你如何使用 Playwright 模拟点击浏览器的“前进”、“后退”和“刷新”按钮?
练习程序
为了练习,编写程序来完成以下任务。
图片网站下载器
编写一个程序,访问像 Flickr 或 Imgur 这样的照片分享网站,搜索照片类别,然后下载所有结果图像。你可以编写一个可以与任何具有搜索功能的照片网站一起工作的程序。
2048
游戏 2048 是一个简单的游戏,你可以通过使用箭头键向上、向下、向左或向右滑动瓷砖来组合它们。实际上,你可以通过随机方向滑动瓷砖获得相当高的分数。编写一个程序,该程序将打开游戏网站play2048.co,并持续发送上、右、下和左的按键操作来自动玩游戏。
链接验证
编写一个程序,给定网页的 URL,将找到页面上的每个 <a> 链接并测试链接的 URL 是否导致“404 未找到”状态码。程序应打印出任何断开的链接。
- 1 答案是否定的。
HTTP 和 HTTPS
当你访问一个网站时,它的网址,例如autbor.com/example3.html,被称为统一资源定位符(URL)。URL 中的HTTPS代表超文本传输协议安全(HyperText Transfer Protocol Secure),这是你的网络浏览器用来访问网站的协议。本章中的包允许你的脚本通过此协议访问网络服务器。
更确切地说,HTTPS 是 HTTP 的加密版本,因此它在您使用互联网时保护您的隐私。如果您使用 HTTP,身份盗贼、国家情报机构和您的互联网服务提供商可以查看您访问的网页的内容,包括您提交的任何密码和信用卡信息。使用虚拟私人网络(VPN)可以防止您的互联网服务提供商查看您的互联网流量;然而,现在 VPN 提供商将能够查看您的流量。一个无良的 VPN 提供商可能会然后将有关您访问的网站的信息出售给数据经纪人。(Tom Scott 在他的视频“这个视频由 VPN 赞助”中讨论了 VPN 能做什么和不能做什么。)
>>> import webbrowser
>>> webbrowser.open('https://inventwithpython.com/')
C:\Users\al> showmap 777 Valencia St, San Francisco, CA 94110
要做到这一点,你需要确定给定街道地址应使用哪个 URL。当你将 www.openstreetmap.org 加载到浏览器中并搜索地址时,地址栏中的 URL 看起来像这样:www.openstreetmap.org/search?query=777%20Valencia%20St%2C%20San%20Francisco%2C%20CA%2094110#map=19/37.75897/-122.42142。
我们可以通过从地址栏中移除 #map 部分来测试 URL 是否不需要该部分,通过访问该网站来确认它是否仍然可以正确加载。因此,你的程序可以被设置为打开一个网络浏览器,以访问 www.openstreetmap.org/search?query=<your_address_string>(其中 <your_address_string> 是你想要映射的地址)。请注意,你的浏览器会自动处理任何必要的 URL 编码,例如将 URL 中的空格字符转换为 %20。
第 2 步:处理命令行参数
让你的代码看起来像这样:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
# TODO: Get address from clipboard.
# TODO: Open the web browser.
首先,你需要导入 webbrowser 模块以启动浏览器和 sys 模块以读取潜在的命令行参数。sys.argv 变量存储程序的文件名和命令行参数作为一个列表。如果这个列表中除了文件名之外还有其他内容,那么 len(sys.argv) 计算出的整数大于 1,这意味着确实提供了命令行参数。
命令行参数通常由空格分隔,但在这个情况下,你希望将所有参数解释为一个单独的字符串。因为 sys.argv 是一个字符串列表,你可以将其传递给 join() 方法,该方法返回一个单独的字符串值。你不想在这个字符串中包含程序名称,所以你应该传递 sys.argv[1:] 而不是 sys.argv 来移除数组的第一个元素。这个表达式计算出的最终字符串存储在 address 变量中。
如果你通过命令行输入此程序
showmap 777 Valencia St, San Francisco, CA 94110
sys.argv 变量将包含以下列表值:
['showmap.py', '777', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110']
在将 sys.argv[1:] 与空格字符连接后,address 变量将包含字符串 '777 Valencia St, San Francisco, CA 94110'。
第 3 步:检索剪贴板内容
要从剪贴板获取 URL,让你的代码看起来像以下这样:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys, pyperclip
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
else:
# Get address from clipboard.
address = pyperclip.paste()
# Open the web browser.
webbrowser.open('https://www.openstreetmap.org/search?query=' + address)
如果没有命令行参数,程序将假设地址存储在剪贴板上。你可以使用 pyperclip.paste() 获取剪贴板内容,并将其存储在名为 address 的变量中。最后,为了使用 OpenStreetMap URL 启动网络浏览器,调用 webbrowser.open()。
虽然您编写的一些程序将执行巨大的任务,可以节省您数小时,但使用一个程序方便地每次执行常见任务(如获取地址地图)节省几秒钟也同样令人满意。表 13-1 比较了使用和未使用showmap.py显示地图所需的步骤。
表 13-1:使用和未使用 showmap.py 获取地图的步骤
| 手动获取地图 | 使用 showmap.py |
|---|---|
| 1. 高亮显示地址。 | 1. 高亮显示地址。 |
| 2. 复制地址。 | 2. 复制地址。 |
| 3. 打开网络浏览器。 | 3. 运行 showmap.py。 |
4. 前往www.openstreetmap.org |
|
| 5. 点击地址文本框。 | |
| 6. 粘贴地址。 | |
| 7. 按下 ENTER 键。 |
我们很幸运,OpenStreetMap 网站获取地图不需要任何交互;我们只需直接将地址信息放入 URL 中。showmap.py脚本使这项任务变得不那么繁琐,尤其是如果您经常这样做。
相似程序的想法
只要您有一个 URL,webbrowser模块就允许用户省略打开浏览器并自行导航到网站的一步。其他程序可以使用此功能执行以下操作:
-
在单独的浏览器标签页中打开页面上的所有链接。
-
打开浏览器到您当地天气网站的 URL。
-
打开您经常检查的几个社交网络站点或书签站点。
-
在您的硬盘上打开本地的.html文件。
最后一个建议对于显示帮助文件很有用。虽然您的程序可以使用print()向用户显示帮助页面,但调用webbrowser.open()以打开包含帮助信息的.html文件可以让页面有不同的字体、颜色、表格和图片。而不是使用https://前缀,请使用file://前缀。例如,您的桌面文件夹应该在 Windows 上位于file:///C:/Users/al/Desktop/help.html,在 macOS 上位于file:///Users/al/Desktop/ help.html。
第 1 步:确定 URL
通过遵循第十二章中的说明,设置一个showmap.py文件,以便当您从命令行运行它时,如下所示
C:\Users\al> showmap 777 Valencia St, San Francisco, CA 94110
如果脚本使用命令行参数而不是剪贴板。如果没有命令行参数,则程序将知道要使用剪贴板的内容。
要这样做,您需要确定给定街道地址应使用的 URL。当您在浏览器中加载www.openstreetmap.org并搜索地址时,地址栏中的 URL 看起来像这样:www.openstreetmap.org/search?query=777%20Valencia%20St%2C%20San%20Francisco%2C%20CA%2094110#map=19/37.75897/-122.42142。
我们可以通过从地址栏中移除 #map 部分并访问该网站来确认它仍然可以正确加载,以测试 URL 不需要 #map 部分。因此,你的程序可以设置为打开网络浏览器到 www.openstreetmap.org/search?query=<your_address_string>(其中 <your_address_string> 是你想要映射的地址)。请注意,你的浏览器会自动处理任何必要的 URL 编码,例如将 URL 中的空格字符转换为 %20。
第 2 步:处理命令行参数
让你的代码看起来像这样:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
# TODO: Get address from clipboard.
# TODO: Open the web browser.
首先,你需要导入 webbrowser 模块以启动浏览器,以及 sys 模块以读取潜在的命令行参数。sys.argv 变量存储程序的文件名和命令行参数作为一个列表。如果这个列表中包含的不仅仅是文件名,那么 len(sys.argv) 的值将大于 1 的整数,这意味着确实提供了命令行参数。
命令行参数通常由空格分隔,但在这个情况下,你将想要将所有参数解释为一个单独的字符串。因为 sys.argv 是一个字符串列表,你可以将其传递给 join() 方法,该方法返回一个单个字符串值。你不想在这个字符串中有程序名称,所以你应该传递 sys.argv[1:] 而不是 sys.argv 来移除数组的第一元素。这个表达式评估出的最终字符串存储在 address 变量中。
如果你通过在命令行中输入以下内容来运行程序
showmap 777 Valencia St, San Francisco, CA 94110
sys.argv 变量将包含以下列表值:
['showmap.py', '777', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110']
在你用空格字符连接 sys.argv[1:] 之后,address 变量将包含字符串 '777 Valencia St, San Francisco, CA 94110'。
第 3 步:检索剪贴板内容
要从剪贴板获取 URL,让你的代码看起来如下所示:
# showmap.py - Launches a map in the browser using an address from the
# command line or clipboard
import webbrowser, sys, pyperclip
if len(sys.argv) > 1:
# Get address from command line.
address = ' '.join(sys.argv[1:])
else:
# Get address from clipboard.
address = pyperclip.paste()
# Open the web browser.
webbrowser.open('https://www.openstreetmap.org/search?query=' + address)
如果没有命令行参数,程序将假设地址存储在剪贴板上。你可以使用 pyperclip.paste() 获取剪贴板内容,并将其存储在名为 address 的变量中。最后,为了使用 OpenStreetMap URL 打开网络浏览器,调用 webbrowser.open()。
虽然你编写的一些程序将执行巨大的任务,可以节省你数小时,但使用一个程序方便地每次执行常见任务(如获取地址的地图)节省几秒钟也同样令人满意。表 13-1 比较了使用和未使用 showmap.py 显示地图所需的步骤。
表 13-1:使用和未使用 showmap.py 获取地图
| 手动获取地图 | 使用 showmap.py |
|---|---|
| 1. 高亮显示地址。 | 1. 高亮显示地址。 |
| 2. 复制地址。 | 2. 复制地址。 |
| 3. 打开网络浏览器。 | 3. 运行 showmap.py。 |
4. 前往 www.openstreetmap.org |
|
| 5. 点击地址文本字段。 | |
| 6. 粘贴地址。 | |
| 7. 按下 ENTER 键。 |
我们很幸运,OpenStreetMap 网站不需要任何交互就能获取地图;我们只需将地址信息直接放入 URL 中。showmap.py 脚本使得这项任务不再那么繁琐,尤其是如果你经常这样做的话。
类似程序的创意
只要你有 URL,webbrowser 模块就允许用户跳过打开浏览器并自行导航到网站这一步骤。其他程序可以使用此功能执行以下操作:
-
在单独的浏览器标签页中打开页面上的所有链接。
-
打开浏览器到您当地天气网站的 URL。
-
打开几个您经常检查的社交网络站点或书签站点。
-
在您的硬盘上打开一个本地的 .html 文件。
最后一个建议对于显示帮助文件很有用。虽然你的程序可以使用 print() 函数向用户显示帮助页面,但调用 webbrowser.open() 打开包含帮助信息的 .html 文件可以让页面有不同的字体、颜色、表格和图片。不要使用 https:// 前缀,而是使用 file:// 前缀。例如,在 Windows 上,你的 桌面 文件夹应该有一个本地的 help.html 文件,路径为 file:///C:/Users/al/Desktop/help.html;在 macOS 上,路径为 file:///Users/al/Desktop/ help.html。
使用 requests 模块从网络上下载文件
requests 模块让你能够轻松地从网络上下载文件,无需担心复杂的网络错误、连接路由和数据压缩等问题。该模块不是 Python 的内置模块,因此在使用之前,你需要按照附录 A 中的说明进行安装。
下载网页
requests.get() 函数接受一个表示要下载的 URL 的字符串。通过在函数的返回值上调用 type(),您可以看到它返回一个 Response 对象,该对象包含服务器对您的请求给出的响应。我将在稍后更详细地解释 Response 对象,但现在,当您的计算机连接到互联网时,请在交互式外壳中输入以下内容:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt') # ❶
>>> type(response)
<class 'requests.models.Response'>
>>> response.status_code == requests.codes.ok # ❷
True
>>> len(response.text)
178978
>>> print(response.text[:210])
The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
该 URL 将您带到包含《罗密欧与朱丽叶》全文的网页 ❶。您可以通过检查 Response 对象的 status_code 属性来判断网页请求是否成功。如果它等于 requests.codes.ok 的值,那么一切顺利 ❷。(顺便说一句,HTTP 中“OK”的状态码是 200。您可能已经熟悉“未找到”的 404 状态码了。)
如果请求成功,下载的网页将作为字符串存储在 Response 对象的 text 变量中。这个大字符串包含了整个剧本;len(response.text) 的调用显示它有超过 178,000 个字符长。最后,调用 print(response.text[:210]) 只会显示前 210 个字符。
如果请求失败并显示错误消息,例如“无法建立新的连接”或“最大重试次数超出”,请检查您的互联网连接。连接到服务器可能相当复杂,我无法在此列出所有可能的问题。您可以通过在引号中搜索错误消息来找到错误的原因。此外,请注意,如果您使用 requests 下载网页,您将仅获得网页的 HTML 内容。您必须单独下载图像和其他媒体。
检查错误
正如您所看到的,Response 对象有一个 status_code 属性,您可以将其与 requests.codes.ok 进行比较,以查看下载是否成功。检查成功的一个更简单的方法是在 Response 对象上调用 raise_for_status() 方法。如果下载文件时发生错误,此方法将引发异常;如果下载成功,则不会执行任何操作。在交互式外壳中输入以下内容:
>>> response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
>>> response.raise_for_status()
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
File "C:\Users\Al\AppData\Local\Programs\Python\Python`XX`\lib\site-packages\
requests\models.py", line 940, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
raise_for_status() 方法是一种简单的方法,可以确保在发生下载错误时程序停止。通常,您希望程序在出现任何意外错误时立即停止。如果失败的下载不是决定性的,您可以使用 try 和 except 语句将 raise_for_status() 行包装起来,以处理此错误情况而不会崩溃:
import requests
response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
try:
response.raise_for_status()
except Exception as exc:
print(f'There was a problem: {exc}')
此 raise_for_status() 方法调用会导致程序输出以下内容:
There was a problem: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
在调用 requests.get() 之后,始终调用 raise_for_status()。您应该确保在程序继续之前下载已经成功。
将下载的文件保存到硬盘
从这里,您可以使用标准的 open() 函数和 write() 方法将网页保存到您的硬盘上的文件。但是,您必须通过将字符串 'wb' 作为 open() 的第二个参数来以 写入二进制 模式打开文件。即使页面是纯文本(例如您之前下载的 罗密欧与朱丽叶 文本),您也需要写入二进制数据而不是文本数据,以保持文本的 Unicode 编码。
要将网页写入文件,您可以使用带有 Response 对象的 iter_content() 方法的 for 循环:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt')
>>> response.raise_for_status()
>>> with open('RomeoAndJuliet.txt', 'wb') as play_file:
... for chunk in response.iter_content(100000):
... play_file.write(chunk)
...
100000
78978
iter_content() 方法在循环的每次迭代中返回内容的一部分。每个块都是 bytes 数据类型,您可以指定每个块将包含多少字节。一百万字节通常是一个很好的大小,因此将 100000 作为 iter_content() 的参数传递。
文件 RomeoAndJuliet.txt 现已存在于当前工作目录中。请注意,虽然网站上的文件名为 rj.txt,但您硬盘上的文件具有不同的文件名。
write() 方法返回写入文件的字节数。在前面的例子中,第一个块中有 100,000 字节,文件剩余部分只需要 78,978 字节。
这就是requests模块的全部内容!你可以在这里了解模块的其他功能:requests.readthedocs.io/en/latest/。
如果你想要从 YouTube、Facebook、X(前身为 Twitter)或其他网站下载视频文件,请使用第二十四章中介绍的yt-dlp模块。
下载网页
requests.get()函数接受一个表示要下载的 URL 的字符串。通过在函数的返回值上调用type(),你可以看到它返回一个Response对象,该对象包含服务器对你的请求给出的响应。我将在稍后更详细地解释Response对象,但现在,当你的电脑连接到互联网时,请在交互式外壳中输入以下内容:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt') # ❶
>>> type(response)
<class 'requests.models.Response'>
>>> response.status_code == requests.codes.ok # ❷
True
>>> len(response.text)
178978
>>> print(response.text[:210])
The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
该 URL 将带你去一个包含《罗密欧与朱丽叶》全文的网页 ❶。你可以通过检查Response对象的status_code属性来判断网页请求是否成功。如果它等于requests.codes.ok的值,那么一切顺利 ❷。(顺便说一句,HTTP 中“OK”的状态码是 200。你可能已经熟悉“未找到”的 404 状态码了。)
如果请求成功,下载的网页将作为字符串存储在Response对象的text变量中。这个大字符串包含了整个剧本;调用len(response.text)会显示它超过 178,000 个字符长。最后,调用print(response.text[:210])只会显示前 210 个字符。
如果请求失败并显示错误消息,如“无法建立新的连接”或“最大重试次数超出”,请检查你的互联网连接。连接到服务器可能相当复杂,我无法在这里列出所有可能的问题。你可以通过在引号内搜索错误消息来找到你错误的常见原因。另外,请注意,如果你使用requests下载网页,你将只得到网页的 HTML 内容。你必须单独下载图片和其他媒体。
检查错误
正如你所见,Response对象有一个status_code属性,你可以将其与requests.codes.ok进行比较,以查看下载是否成功。检查成功的一个更简单的方法是在Response对象上调用raise_for_status()方法。如果在下载文件时发生错误,该方法将引发异常;如果下载成功,则不执行任何操作。请在交互式外壳中输入以下内容:
>>> response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
>>> response.raise_for_status()
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
File "C:\Users\Al\AppData\Local\Programs\Python\Python`XX`\lib\site-packages\
requests\models.py", line 940, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
raise_for_status()方法是一种简单的方法,可以确保在发生下载错误时程序停止。通常,你希望程序在发生一些意外错误时立即停止。如果失败的下载不是决定性的,你可以用try和except语句包裹raise_for_status()行来处理这个错误情况,而不会崩溃:
import requests
response = requests.get('https://inventwithpython.com/page_that_does_not_exist')
try:
response.raise_for_status()
except Exception as exc:
print(f'There was a problem: {exc}')
这个raise_for_status()方法调用会导致程序输出以下内容:
There was a problem: 404 Client Error: Not Found for url:
https://inventwithpython.com/page_that_does_not_exist.html
在调用requests.get()之后,始终调用raise_for_status()。在您的程序继续之前,您应该确保下载已经成功。
将下载的文件保存到硬盘驱动器
从这里,您可以使用标准的open()函数和write()方法将网页保存到您的硬盘驱动器上的文件。但是,您必须通过将字符串'wb'作为open()的第二个参数来以写入二进制模式打开文件。即使页面是纯文本(例如您之前下载的罗密欧与朱丽叶文本),您也需要写入二进制数据而不是文本数据,以保持文本的 Unicode 编码。
要将网页写入文件,您可以使用Response对象的iter_content()方法进行for循环:
>>> import requests
>>> response = requests.get('https://automatetheboringstuff.com/files/rj.txt')
>>> response.raise_for_status()
>>> with open('RomeoAndJuliet.txt', 'wb') as play_file:
... for chunk in response.iter_content(100000):
... play_file.write(chunk)
...
100000
78978
iter_content()方法在循环的每次迭代中返回内容的一部分。每个块都是字节数据类型,您可以指定每个块将包含多少字节。一般来说,一百万字节是一个很好的大小,所以将100000作为iter_content()的参数传递。
文件RomeoAndJuliet.txt现在存在于当前工作目录中。请注意,虽然网站上的文件名为rj.txt,但您硬盘驱动器上的文件名不同。
write()方法返回写入文件的字节数。在上一个例子中,第一个块中有 100,000 字节,文件剩余部分只需要 78,978 字节。
这就是requests模块的全部内容!您可以在requests.readthedocs.io/en/latest/了解该模块的其他功能。
如果您想从 YouTube、Facebook、X(前身为 Twitter)或其他网站下载视频文件,请使用第二十四章中介绍的yt-dlp模块。
访问天气 API
您使用的应用程序旨在与人类用户交互。但是,您可以编写程序通过它们的应用程序编程接口(API)与其他程序交互,该接口定义了如何让一块软件(例如您的 Python 程序)与另一块软件(例如天气网站的 Web 服务器)通信。在线服务通常有 API。例如,您可以编写一个 Python 脚本来发布到您的社交媒体账户或下载新照片。在本节中,我们将编写一个脚本,从免费的 OpenWeather 网站获取天气信息。
几乎所有在线服务都要求您注册一个电子邮件地址才能使用其 API。即使 API 是免费的,它们也可能对您每小时或每天可以进行的 API 请求次数有限制。如果您担心收到垃圾邮件,可以使用临时、一次性电子邮件地址服务,如10minutemail.com。请记住,您应该只使用此类服务注册您不关心的在线账户,因为一个不道德的电子邮件服务可能会通过您的名义发起密码重置请求来控制您的在线账户。
要开始,请在openweathermap.org注册一个免费账户。免费账户级别限制您每分钟只能进行 60 次 API 请求。这对于您的小型或中型编程项目来说已经足够了。如果您的程序需要超过这个限制(比如说,因为您的网站需要处理来自数百个同时访问者的请求),您可以购买付费账户级别。在线服务将为您提供API 密钥,这相当于一个密码,用于在您的 API 请求中识别您的账户。请保密这个 API 密钥!任何拥有这个密钥的人都可以使用您的账户进行 API 请求。如果您编写了一个使用 API 密钥的程序,考虑让程序读取一个包含密钥的文本文件,而不是直接在源代码中包含 API 密钥。这样,您就可以与他人分享您的程序(他们可以注册自己的 API 密钥),而不用担心超出您账户的 API 请求限制。
许多 HTTP API 将它们的响应作为一个大字符串提供。这个字符串通常格式化为 JSON 或 XML。第十八章将更详细地介绍 JSON 和 XML,但就目前而言,您只需要知道json.loads(response.text)返回一个包含response.text中 JSON 数据的 Python 数据结构(列表和字典)。本章中的示例将此数据存储在名为response_data的变量中,但这是一个任意的选择,您可以使用任何您喜欢的变量名。
所有在线服务都提供了如何使用其 API 的文档。OpenWeather 在其openweathermap.org/api提供了文档。在您登录账户并从home.openweathermap.org/api_keys的“我的 API 密钥”页面获取您的 API 密钥后,请在以下交互式 shell 代码中使用它。在这个例子中,我将使用'30ee784a80d81480dab1749d33980112'作为伪造的 API 密钥。请不要在您的代码中使用这个伪造的 API 密钥示例;它不会工作。
首先,您将使用 OpenWeather 来查找旧金山的纬度和经度:
>>> import requests
>>> city_name = 'San Francisco'
>>> state_code = 'CA'
>>> country_code = 'US'
>>> API_key = '30ee784a80d81480dab1749d33980112' # Not a real API key
>>> response = requests.get(f'https://api.openweathermap.org/geo/1.0/
direct?q={city_name},{state_code},{country_code}&appid={API_key}')
>>> response.text # This is a Python string.
'[{"name":"San Francisco","local_names":{"id":"San Francisco",
# --snip--
,"lat":37.7790262,"lon":-122.419906,"country":"US","state":"California"}]'
>>> import json
>>> response_data = json.loads(response.text)
>>> response_data # This is a Python data structure.
[{"name":"San Francisco","local_names":{"id":"San Francisco",
# --snip--
,"lat":37.7790262,"lon":-122.419906,"country":"US","state":"California"}]
为了理解响应中的数据,你应该查看 OpenWeather 的在线 API 文档或检查交互式外壳中的response_data字典。你会了解到响应是一个列表,其第一个项目(索引0)是一个包含键'lat'和'lon'的字典。这些键的值是经纬度的浮点值:
>>> response_data[0]['lat']
37.7790262
>>> response_data[0]['lon']
-122.419906
用于发起 API 请求的特定 URL 称为 端点。在这个示例中的 f-strings 将花括号内的部分替换为变量的值。上一个示例中的direct?q={city_name},{state_code},{country_code}&appid={API_key}'变为direct?q=San Francisco,CA,US&appid=30ee784a80d81480dab1749d33980112'。
接下来,你可以使用这些经纬度信息来查找旧金山的当前温度:
>>> lat = json.loads(response.text)[0]['lat']
>>> lon = json.loads(response.text)[0]['lon']
>>> response = requests.get(f'https://api.openweathermap.org/data/2.5/
weather?lat={lat}&lon={lon}&appid={API_key}')
>>> response_data = json.loads(response.text)
>>> response_data
{'coord': {'lon': -122.4199, 'lat': 37.779}, 'weather': [{'id': 803,
# --snip--
'timezone': -25200, 'id': 5391959, 'name': 'San Francisco', 'cod': 200}
>>> response_data['main']['temp']
285.44
>>> round(285.44 - 273.15, 1) # Convert Kelvin to Celsius.
12.3
>>> round(285.44 * (9 / 5) - 459.67, 1) # Convert Kelvin to Fahrenheit.
54.1
注意,OpenWeather 返回的温度是以开尔文为单位的,所以你需要做一些数学运算来得到摄氏度或华氏度的温度。
让我们分解上一个示例中地理位置端点的完整 URL:
https:// 用来访问服务器的 方案,即协议名称(对于在线 API 几乎总是 HTTPS),后面跟着一个冒号和两个正斜杠。
api.openweathermap.org 处理 API 请求的 Web 服务器的域名。
/geo/1.0/direct API 的路径。
?q={city_name},{state_code},{country_code}&appid={API_key} URL 的查询字符串。花括号内的部分需要替换为实际值;你可以把它们看作是函数调用的参数。在 URL 编码中,参数名称和参数值由等号分隔,多个参数-参数对由与号分隔。
你可以将端点 URL(带有完整的查询字符串)粘贴到你的网络浏览器中,直接查看响应文本。当你刚开始学习如何使用 API 时,这通常是一个好习惯。基于 Web 的 API 的响应文本通常格式化为 JSON 或 XML。
为了在更新 API 时避免混淆,大多数在线服务都将版本号作为 URL 的一部分。随着时间的推移,一个服务可能会发布 API 的新版本并弃用旧版本。在这种情况下,你将不得不更新脚本中的代码,以便继续使用它们。
OpenWeather 的免费层也提供五天预报以及有关降水、风速和空气污染的信息。文档网页显示了获取这些数据的 URL,以及这些 API 调用 JSON 响应的结构。接下来的几节代码假设你已经运行了response_data = json.loads(response.text),将网站返回的文本转换为 Python 数据结构。
请求经纬度
获取城市纬度和经度坐标的服务端点是 api.openweathermap.org/geo/1.0/direct?q={city_name},{state_code},{country_code}&appid={API_key}。州代码是指州的缩写,仅适用于美国城市。国家代码是两到三位字母的 ISO 3166 代码,可在 en.wikipedia.org/wiki/List_of_ISO_3166_country_codes 上找到。例如,使用代码 'US' 表示美国或 'NZ' 表示新西兰。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data[0]['lat'] 保存城市纬度,以浮点值表示
response_data[0]['lon'] 保存城市经度,以浮点值表示
如果城市名称匹配多个响应,response_data 中的列表将包含不同的字典,位于 response_data[0]、response_data[1] 等位置。如果 OpenWeather 无法定位城市,response_data 将是一个空列表。
获取当前天气
根据某些纬度和经度获取当前天气信息的服务端点是 api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['weather'][0]['main'] 保存一个字符串描述,例如 'Clear'、'Rain' 或 'Snow'
response_data['weather'][0]['description'] 保存一个更详细的字符串,例如 'light rain'、'moderate rain' 或 'extreme rain'
response_data['main']['temp'] 保存当前温度,单位为开尔文
response_data['main']['feels_like'] 保存人类对温度的感觉,单位为开尔文
response_data['main']['humidity'] 保存湿度百分比
如果您提供了错误的纬度或经度参数,response_data 将是一个字典,例如 {"cod":"400","message":"wrong latitude"}。
获取天气预报
根据某些经纬度获取五天预报的端点是 api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['list'] 存储包含给定时间天气预报的字典列表。
response_data['list'][0]['dt'] 存储一个以 Unix 时间戳浮点数形式的时间戳。将此值作为参数传递给 datetime.datetime.fromtimestamp() 以获取 datetime 对象形式的时间戳。第十九章更详细地讨论了 Python 的 datetime 模块。
response_data['list'][0]['main'] 存储一个包含类似 'temp'、'feels_like'、'humidity' 等键的字典。
response_data['list'][0]['weather'][0] 存储一个包含类似 'main'、'description' 等键的描述字典。
response_data['list'] 中的列表包含 40 个字典,这些字典以三小时为增量预测未来五天的天气预报,尽管这可能在 API 的未来版本中发生变化。
探索 API
其他网站,如 weather.gov 和 www.weatherapi.com/,提供它们自己的免费天气 API。每个 API 都有所不同,但它们通常通过 HTTPS 请求访问,在这种情况下,您可以使用 Requests 库,并将响应格式化为 JSON 或 XML 文本。但是,有人可能已经创建了一个第三方 Python 包,以使使用这些 API 更容易,其中包含处理访问端点和解析响应的 Python 函数。您可以在 pypi.org 上找到这些包;阅读包文档以了解它们的使用方法。
请求经纬度
获取城市经纬度坐标的端点是 api.openweathermap.org/geo/1.0/direct?q={city_name},{state_code},{country_code}&appid={API_key}。州代码是指州的缩写,仅适用于美国城市。国家代码是两到三位字母的 ISO 3166 代码,可在 en.wikipedia.org/wiki/List_of_ISO_3166_country_codes 上找到。例如,使用代码 'US' 表示美国或 'NZ' 表示新西兰。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data[0]['lat'] 保存了城市纬度的浮点数值
response_data[0]['lon'] 保存了城市经度的浮点数值
如果城市名称与多个响应匹配,response_data 中的列表将包含不同的字典,如 response_data[0]、response_data[1] 等。如果 OpenWeather 无法定位城市,response_data 将为空列表。
获取当前天气
获取基于某些纬度和经度的当前天气信息的端点是 api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['weather'][0]['main'] 保存了一个字符串描述,例如 'Clear'、'Rain' 或 'Snow'
response_data['weather'][0]['description'] 保存了一个更详细的字符串,例如 'light rain'、'moderate rain' 或 'extreme rain'
response_data['main']['temp'] 保存了当前温度的绝对温度(开尔文)
response_data['main']['feels_like'] 保存了人类对温度的感知值(开尔文)
response_data['main']['humidity'] 保存了湿度百分比
如果您提供了错误的纬度或经度参数,response_data 将是一个字典,例如 {"cod":"400","message":"wrong latitude"}。
获取天气预报
获取基于某些纬度和经度的五天预报的端点是 api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&appid={API_key}。在将响应 JSON 文本转换为名为 response_data 的 Python 数据结构后,您可以检索以下信息:
response_data['list'] 保存了一个包含给定时间天气预测的字典列表。
response_data['list'][0]['dt'] 保存了一个 Unix 时间戳浮点数形式的日期时间。将此值作为参数传递给 datetime.datetime.fromtimestamp() 函数,以获取 datetime 对象形式的日期时间。第十九章更详细地讨论了 Python 的 datetime 模块。
response_data['list'][0]['main'] 保存了一个包含如 'temp'、'feels_like'、'humidity' 等键的字典。
response_data['list'][0]['weather'][0] 保存了一个包含如 'main'、'description' 等键的描述字典。
response_data['list'] 中的列表包含 40 个字典,这些字典以每 3 小时为间隔预测未来五天的天气,尽管在 API 的未来版本中这可能会改变。
探索 API
其他网站,如weather.gov和www.weatherapi.com/,提供它们自己的免费天气 API。每个 API 的工作方式都不同,但它们通常通过 HTTPS 请求访问,在这种情况下,你可以使用 Requests 库,并将响应格式化为 JSON 或 XML 文本。然而,有人可能已经创建了一个第三方 Python 包,以便更容易地使用这些 API,其中包含处理访问端点和解析响应的 Python 函数。你可以在pypi.org上找到这些包;阅读包文档以了解它们的使用。
理解 HTML
在你拆解网页之前,你必须学习一些超文本标记语言(HTML)的基础知识。HTML 是网页的编写格式,而层叠样式表(CSS)是一种对网页中 HTML 元素的外观进行分类更改的方法。本章假设你有一些基本的 HTML 经验,但如果你需要入门教程,我建议以下网站之一:
在本节中,你还将学习如何访问您网络浏览器的强大开发者工具,这些工具使从网络中抓取信息变得容易得多。
探索格式
HTML 文件是一个带有.html文件扩展名的纯文本文件。这些文件中的文本被 HTML 标签包围,标签是括在尖括号(<>)中的单词。标签告诉浏览器如何格式化网页。起始标签和结束标签可以包含一些文本,形成一个 HTML 元素。要显示的文本是起始和结束标签之间的内容。例如,以下 HTML 将在浏览器中显示Hello, world!,其中的Hello为粗体:
<b>Hello</b>, world!
在浏览器中,这段 HTML 将看起来如图 13-1 所示。

图 13-1:在浏览器中渲染的“Hello, world!”
开头 <b> 标签表示包含的文本将以粗体显示。结尾 </b> 标签告诉浏览器粗体文本的结束位置。它们一起形成一个元素:<b>Hello</b>。
HTML 中有许多不同的标签。其中一些标签在尖括号内具有额外的属性,形式为 属性。例如,<a> 标签包含应作为链接的文本,而 href 属性确定链接到哪个 URL。以下是一个例子:
<a href="https://inventwithpython.com”>This text is a link</a>
一些元素有一个 id 属性,用于在页面中唯一标识该元素。你通常会指示你的程序通过其 id 属性查找元素,因此在使用浏览器开发者工具查找此属性是编写网络爬虫程序时的常见任务。
查看网页的源代码
你需要查看你的程序将与之工作的网页的 HTML,称为 源代码。为此,在网页浏览器中右键点击任何网页(或在 macOS 上 CTRL 点击它),然后选择 查看源代码 或 查看页面源代码(图 13-2)。源代码是浏览器实际接收的文本。浏览器知道如何从这些 HTML 中显示或 渲染 网页。

图 13-2:查看网页的源代码
好吧,查看你最喜欢的网站的一些源 HTML。如果你不完全理解你所看到的内容,那没关系。你不需要完全掌握 HTML 来编写简单的网络爬虫程序。你只需要足够的知识来从现有的网站上挑选数据。
打开你的浏览器开发者工具
除了查看网页的源代码外,你还可以使用浏览器的开发者工具查看页面的 HTML。在 Firefox、Chrome 和 Microsoft Edge 中,你可以按 F12 使工具出现(图 13-3)。再次按 F12 将使其消失。

图 13-3:Chrome 浏览器中的开发者工具窗口
右键点击网页的任何部分,从上下文菜单中选择 Inspect Element 以显示该部分页面的 HTML。这将有助于你解析用于你的网络爬虫程序的 HTML。
查找 HTML 元素
一旦你的程序使用 requests 模块下载了一个网页,你将获得该网页的 HTML 内容作为一个单独的字符串值。现在你需要弄清楚 HTML 的哪一部分对应于你感兴趣的网页信息。
这就是浏览器开发者工具可以帮助的地方。比如说,你想编写一个程序从 weather.gov 拉取天气预报数据。在编写任何代码之前,先做一些研究。如果你访问该网站并搜索 94105 ZIP 代码,它应该会带你到一个显示该地区预报的页面。
如果你感兴趣的是抓取该 ZIP 代码的天气信息呢?在页面上的该信息上右键单击(或在 macOS 上 CTRL-单击)并从出现的上下文菜单中选择检查元素。这会打开开发者工具窗口,显示生成该网页特定部分的 HTML。图 13-4 显示了开发者工具打开以查看最近的预报的 HTML。请注意,如果 weather.gov 网站更改其网页的设计,你需要重复此过程来检查新元素。

图 13-4:检查包含预报文本的元素
从开发者工具中,你可以看到负责网页预报部分的 HTML 是这样的:
<div class="col-sm-10 forecast-text">Sunny, with a high near 64.
West wind 11 to 16 mph, with gusts as high as 21 mph.</div>
这正是你想要的!看起来预报信息包含在一个具有 forecast-text CSS 类的 <div> 元素中。
在浏览器开发者控制台中右键单击此元素,并从出现的上下文菜单中选择复制CSS 选择器。此选项将一个类似于 'div.row-odd:nth-child(1) > div:nth-child(2)' 的字符串复制到剪贴板。你可以将它传递给 Beautiful Soup 的 select() 方法或 Selenium 的 find_element() 方法,正如本章后面所解释的,以在字符串中找到该元素。
在此字符串中使用的 CSS 选择器 语法指定了要从网页中检索哪些 HTML 元素。完整的选择器语法超出了本书的范围,但你可以从浏览器开发者工具中获取选择器,就像我们在这里做的那样。XPath 是另一种选择 HTML 元素的语法,但这也超出了本书的范围。
请记住,当网站更改其布局时,你需要更新你的脚本检查的 HTML 标签。这可能会在几乎没有警告的情况下发生,所以请确保密切关注你的程序,以防它突然显示无法找到元素的错误。一般来说,如果网站提供 API,最好使用网站 API,因为它比网站本身更不可能更改。
探索格式
HTML 文件是一个带有 .html 文件扩展名的纯文本文件。这些文件中的文本被 HTML 标签 所包围,这些标签是括在尖括号 (<>) 中的单词。标签告诉浏览器如何格式化网页。一个起始标签和结束标签可以包围一些文本以形成一个 HTML 元素。要显示的文本是起始和结束标签之间的内容。例如,以下 HTML 将在浏览器中显示 Hello, world!,其中 Hello 是粗体的:
<b>Hello</b>, world!
在浏览器中,此 HTML 将看起来如图 13-1 所示。

图 13-1:在浏览器中渲染的“Hello, world!”
开头 <b> 标签表示包含的文本将以粗体显示。关闭 </b> 标签告诉浏览器粗体文本的结束位置。它们共同构成一个元素:<b>Hello</b>。
HTML 中有许多不同的标签。其中一些标签在尖括号内具有额外的属性,称为属性。例如,<a>标签包含应作为链接的文本,href属性确定链接到哪个 URL。以下是一个示例:
<a href="https://inventwithpython.com”>This text is a link</a>
一些元素有一个id属性,用于在页面中唯一标识该元素。您通常会指示程序通过其id属性查找元素,因此在使用浏览器开发者工具查找此属性是编写网络爬虫程序时的常见任务。
查看网页源代码
您需要查看程序将与之工作的网页的 HTML,称为源代码。为此,在您的网页浏览器中右键单击任何网页(或在 macOS 上按 CTRL 单击),然后选择查看源代码或查看页面源代码(图 13-2)。源代码是浏览器实际接收的文本。浏览器知道如何从 HTML 中显示或渲染网页。

图 13-2:查看网页源代码
好吧,查看您最喜欢的网站的一些源 HTML。如果您不完全理解您所看到的内容,那没关系。您不需要完全掌握 HTML 来编写简单的网络爬虫程序。您只需要足够的知识来从现有网站中提取数据即可。
打开浏览器开发者工具
除了查看网页的源代码外,您还可以使用浏览器的开发者工具查看网页的 HTML。在 Firefox、Chrome 和 Microsoft Edge 浏览器中,您可以按 F12 键使工具出现(图 13-3)。再次按 F12 键将使它们消失。

图 13-3:Chrome 浏览器中的开发者工具窗口
右键单击网页的任何部分,从上下文菜单中选择检查元素以显示该部分页面的 HTML。这将帮助您为网络爬虫程序解析 HTML。
查找 HTML 元素
一旦您的程序使用requests模块下载了网页,您将获得该网页的 HTML 内容作为一个单独的字符串值。现在您需要确定 HTML 的哪一部分对应于您感兴趣的网页信息。
这正是浏览器开发者工具能帮到的地方。比如说,你想编写一个程序从weather.gov抓取天气预报数据。在编写任何代码之前,先做一些研究。如果你访问该网站并搜索 94105 ZIP 代码,它应该会带你到一个显示该地区预报的页面。
如果你对抓取该 ZIP 代码的天气信息感兴趣呢?在页面上的该信息上右键点击(或在 macOS 上按 CTRL 点击)并从出现的上下文菜单中选择Inspect Element。这会打开开发者工具窗口,显示生成该网页特定部分的 HTML。图 13-4 显示了开发者工具打开到最近的预报的 HTML。请注意,如果weather.gov网站更改了其网页的设计,你需要重复此过程来检查新元素。

图 13-4:检查包含预报文本的元素
从开发者工具中,你可以看到负责网页预报部分的 HTML 如下所示:
<div class="col-sm-10 forecast-text">Sunny, with a high near 64.
West wind 11 to 16 mph, with gusts as high as 21 mph.</div>
这正是你正在寻找的!看起来预报信息包含在一个具有forecast-text CSS 类的<div>元素中。
在浏览器开发者控制台中右键点击此元素,并从出现的上下文菜单中选择复制CSS 选择器。此选项将一个如'div.row-odd:nth-child(1) > div:nth-child(2)'的字符串复制到剪贴板。你可以将它传递给 Beautiful Soup 的select()方法或 Selenium 的find_element()方法,正如本章后面解释的那样,以在字符串中找到该元素。
在这个字符串中使用的CSS 选择器语法指定了从网页中检索哪些 HTML 元素。完整的选择器语法超出了本书的范围,但你可以在浏览器开发者工具中获取选择器,就像我们在这里做的那样。XPath是另一种选择 HTML 元素的语法,但这也超出了本书的范围。
请记住,当网站更改其布局时,你需要更新你的脚本检查的 HTML 标签。这可能会在没有警告或仅有少量警告的情况下发生,所以请确保密切关注你的程序,以防它突然显示无法找到元素的错误。一般来说,如果网站提供 API,最好使用它,因为它比网站本身更不可能更改。
使用 Beautiful Soup 解析 HTML
Beautiful Soup 是一个用于从 HTML 页面提取信息的包。你将使用 beautifulsoup4 来安装该包,但使用较短的模块名 bs4 来导入它。在本节中,我们将使用 Beautiful Soup 来 解析(即分析并提取)位于 autbor.com/example3.html 的 HTML 文件,其内容如下:
<!-- This is an HTML comment. -->
<html>
<head>
<title>Example Website Title</title>
<style>
.slogan {
color: gray;
font-size: 2em;
}
</style>
</head>
<body>
<h1>Example Website</h1>
<p>This <p> tag puts <b>content</b> into a <i>single</i> paragraph.</p>
<p><a href="https://inventwithpython.com”>This text is a link</a> to books by <span id=
"author">Al Sweigart</span>.</p>
<p><img src="wow_such_zophie_thumb.webp" alt="Close-up of my cat Zophie." /></p>
<p class="slogan">Learn to program in Python!</p>
<form>
<p><label>Username: <input id="login_user" placeholder="admin" /></label></p>
<p><label>Password: <input id="login_pass" type="password" placeholder="swordfish" />
</form>
</label></p>
<p><label>Agree to disagree: <input type="checkbox" /></label><input type="submit"
value="Fake Button" /></p>
</body>
</html>
注意,这个页面上的登录表单是假的,仅用于美观。
即使是一个简单的 HTML 文件也涉及许多不同的标签和属性,当涉及到复杂的网站时,事情会很快变得混乱。幸运的是,Beautiful Soup 使得处理 HTML 变得容易得多。
创建 Beautiful Soup 对象
bs4.BeautifulSoup() 函数接受一个包含要解析的 HTML 的字符串,然后返回一个 BeautifulSoup 对象。例如,当你的计算机连接到互联网时,在交互式外壳中输入以下内容:
>>> import requests, bs4
>>> res = requests.get('https://autbor.com/example3.html')
>>> res.raise_for_status()
>>> example_soup = bs4.BeautifulSoup(res.text, 'html.parser')
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
此代码使用 requests.get() 下载 Automate the Boring Stuff 网站的首页,然后将响应的 text 属性传递给 bs4.BeautifulSoup()。Beautiful Soup 可以解析不同的格式,'html.parser' 参数告诉它我们正在解析 HTML。最后,代码将返回的 BeautifulSoup 对象存储在名为 example_soup 的变量中。
你还可以通过将 File 对象传递给 bs4.BeautifulSoup() 来从你的硬盘加载 HTML 文件。在确保 example3.html 文件位于工作目录后,在交互式外壳中输入以下内容:
>>> import bs4
>>> with open('example3.html') as example_file:
... example_soup = bs4.BeautifulSoup(example_file, 'html.parser')
...
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
一旦你有了 BeautifulSoup 对象,你可以使用它的方法来定位 HTML 文档的特定部分。
查找元素
你可以通过调用 select() 方法并传递一个用于查找元素的 CSS 选择器字符串,从 BeautifulSoup 对象中检索网页元素。该方法返回一个 Tag 对象的列表,这些对象代表匹配的 HTML 元素。表 13-2 展示了使用 select() 的最常见 CSS 选择器模式的示例。
表 13-2:CSS 选择器示例
| 传递给 select() 方法的选择器 | 将匹配 ... |
|---|---|
soup.select('div') |
所有名为 <div> 的元素 |
soup.select('#author') |
具有名为 author 的 id 属性的元素。 |
soup.select('.notice') |
所有使用名为 notice 的 CSS class 属性的元素 |
soup.select('div span') |
所有名为 <span> 的元素,它们位于名为 <div> 的元素内部 |
soup.select('div > span') |
所有名为 <span> 的元素,它们直接位于名为 <div> 的元素内部,中间没有其他元素。 |
soup.select('input[name]') |
所有名为 <input> 的元素,它们具有任何值的 name 属性。 |
soup.select('input[type="button"]') |
所有名为 <input> 的元素,其 type 属性的值为 button |
您可以将各种选择器模式组合起来以进行复杂的匹配。例如,soup.select('p #author')匹配任何具有id属性为author的元素,只要它也在<p>元素内部。
您可以将标签值传递给str()函数以显示它们所代表的 HTML 标签。标签值还有一个attrs属性,它包含所有它们的 HTML 属性作为字典。例如,下载autbor.com/example3.html页面作为example3.html,然后输入以下内容到交互式 shell 中:
>>> import bs4
>>> example_file = open('example3.html')
>>> example_soup = bs4.BeautifulSoup(example_file.read(), 'html.parser')
>>> elems = example_soup.select('#author')
>>> type(elems) # elems is a list of Tag objects.
<class 'bs4.element.ResultSet'>
>>> len(elems)
1
>>> type(elems[0])
<class 'bs4.element.Tag'>
>>> str(elems[0]) # The Tag object as a string
'<span id="author">Al Sweigart</span>'
>>> elems[0].get_text() # The inner text of the element
'Al Sweigart'
>>> elems[0].attrs
{'id': 'author'}
此代码在我们的示例 HTML 中查找具有id="author"的元素。我们使用select('#author')来返回所有具有id="author"的元素列表。然后我们将这个Tag对象列表存储在变量elems中。运行len(elems)告诉我们列表中有一个Tag对象,这意味着有一个匹配项。
将元素传递给str()返回一个包含起始和结束标签以及元素文本的字符串。在元素上调用get_text()返回元素的文本,或起始和结束标签之间的内容:在这种情况下,'Al Sweigart'。最后,attrs给我们一个包含元素属性'id'和id属性值的字典,'author'。
您还可以从BeautifulSoup对象中提取所有<p>元素。在交互式 shell 中输入以下内容:
>>> p_elems = example_soup.select('p')
>>> str(p_elems[0])
'<p>This <p> tag puts <b>content</b> into a <i>single</i> paragraph.</p>'
>>> p_elems[0].get_text()
'This <p> tag puts content into a single paragraph.'
>>> str(p_elems[1])
'<p> <a href="https://inventwithpython.com/”>This text is a link</a> to books by
<span id="author">Al Sweigart</span>.</p>'
>>> p_elems[1].get_text()
'This text is a link to books by Al Sweigart.'
>>> str(p_elems[2])
'<p><img alt="Close-up of my cat Zophie." src="wow_such_zophie_thumb.webp"/></p>'
>>> p_elems[2].get_text()
''
这次,select()给我们一个包含三个匹配项的列表,我们将其存储在p_elems中。使用str()在p_elems[0]、p_elems[1]和p_elems[2]上显示每个元素作为字符串,并使用get_text()在每个元素上显示其文本。
从元素的属性获取数据
Tag对象的get()方法允许您从元素访问 HTML 属性值。您将传递一个属性名作为字符串并接收该属性的值。使用autbor.com/example3.html中的example3.html,在交互式 shell 中输入以下内容:
>>> import bs4
>>> soup = bs4.BeautifulSoup(open('example3.html'), 'html.parser')
>>> span_elem = soup.select('span')[0]
>>> str(span_elem)
'<span id="author">Al Sweigart</span>'
>>> span_elem.get('id')
'author'
>>> span_elem.get('some_nonexistent_addr') == None
True
>>> span_elem.attrs
{'id': 'author'}
在这里,我们使用select()来查找任何<span>元素,然后将第一个匹配的元素存储在span_elem中。将属性名'id'传递给get()返回该属性的值,'author'。
项目 7:打开所有搜索结果
当我在搜索引擎上查找一个主题时,我不会一次只查看一个搜索结果。通过中间点击搜索结果链接(或按住 CTRL 点击),我会打开几个新标签页以供稍后阅读。我经常上网搜索,这个工作流程——打开我的浏览器,搜索一个主题,然后逐个中间点击几个链接——是繁琐的。如果能简单地输入一个术语到命令行,让我的电脑自动打开顶部搜索结果在新浏览器标签页中,那就太好了。
让我们编写一个脚本来完成 Python 包索引的搜索结果页面pypi.org。你可以将这样的程序改编到许多其他网站上,尽管 Google、DuckDuckGo、Amazon 和其他大型网站通常采取一些措施,使得抓取它们的搜索结果页面变得困难。
这个程序应该做以下事情:
-
从命令行参数获取搜索关键词
-
获取搜索结果页面
-
为每个结果打开一个浏览器标签
这意味着你的代码需要执行以下操作:
-
从
sys.argv中读取命令行参数。 -
使用
requests模块获取搜索结果页面。 -
找到每个搜索结果对应的链接。
-
调用
webbrowser.open()函数打开网络浏览器。
打开一个新的文件编辑标签,并将其保存为 searchpypi.py。
第 1 步:获取搜索页面
在编写代码之前,你首先需要知道搜索结果页面的 URL。在搜索后查看浏览器的地址栏,你可以看到结果页面有一个看起来像这样的 URL:pypi.org/search/?q=<SEARCH_TERM_HERE>。requests 模块可以下载这个页面;然后,你可以使用 Beautiful Soup 在 HTML 中找到搜索结果链接。最后,你将使用 webbrowser 模块在浏览器标签中打开这些链接。
让你的代码看起来像以下这样:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
print('Searching...') # Display text while downloading the search results page.
res = requests.get('https://pypi.org/search/?q=' + ' '.join(sys.argv[1:]))
res.raise_for_status()
# TODO: Retrieve top search result links.
# TODO: Open a browser tab for each result.
用户在启动程序时将指定搜索词作为命令行参数,代码将这些参数作为字符串存储在 sys.argv 列表中。
第 2 步:查找所有结果
现在你需要使用 Beautiful Soup 从你下载的 HTML 中提取顶级搜索结果链接。但你怎么确定正确的选择器呢?例如,你不能仅仅搜索所有的 <a> 标签,因为 HTML 中有很多你不需要的链接。相反,你必须使用浏览器的开发者工具检查搜索结果页面,试图找到一个选择器,只选择你想要的链接。
在搜索了 pyautogui 之后,你可以打开浏览器的开发者工具并检查页面上的某些链接元素。它们可能看起来很复杂,就像这样:<a class="package-snippet" href="/project/pyautogui" >。但元素看起来多么复杂并不重要。你只需要找到所有搜索结果链接共有的模式。
让你的代码看起来像以下这样:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
# --snip--
# Retrieve top search result links.
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
如果你查看 <a> 元素,你会看到搜索结果链接都带有 class="package-snippet"。浏览其余的 HTML 源代码,看起来 package-snippet 类仅用于搜索结果链接。你不需要知道 CSS 类 package-snippet 是什么或者它做什么。你只是用它作为你正在寻找的 <a> 元素的标记。
你可以从下载的页面的 HTML 文本创建一个 BeautifulSoup 对象,然后使用选择器 '.package-snippet' 来查找所有位于具有 package-snippet CSS 类的元素内的 <a> 元素。请注意,如果 PyPI 网站更改了其布局,你可能需要更新此程序,以使用新的 CSS 选择器字符串传递给 soup.select()。程序的其他部分应保持最新。
第 3 步:为每个结果打开网页浏览器
最后,你必须告诉程序为结果打开网页浏览器标签。将以下内容添加到你的程序末尾:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
--`snip`--
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
num_open = min(5, len(link_elems))
for i in range(num_open):
url_to_open = 'https://pypi.org' + link_elems[i].get('href')
print('Opening', url_to_open)
webbrowser.open(url_to_open)
默认情况下,程序使用 webbrowser 模块在新标签页中打开前五个搜索结果。然而,用户可能搜索到的结果少于五个。soup.select() 调用返回一个匹配你的 '.package-snippet' 选择器的所有元素的列表,因此你想要打开的标签页数量是 5 或这个列表的长度(取较小者)。
Python 内置函数 min() 返回传入的整数或浮点数参数中的最小值。(还有一个内置的 max() 函数,它返回传入的最大参数。)你可以使用 min() 来判断列表中是否有少于五个链接,并将打开的链接数量存储在名为 num_open 的变量中。然后,你可以通过调用 range(num_open) 来运行一个 for 循环。
在循环的每次迭代中,代码使用 webbrowser.open() 在网页浏览器中打开一个新的标签页。请注意,返回的 <a> 元素的 href 属性值没有初始的 pypi.org 部分,所以你必须将其连接到 href 属性的字符串值。
现在,你可以在命令行上运行 searchpypi boring stuff 来立即打开前五个 PyPI 搜索结果,例如 无聊的内容!请参阅第十二章,了解如何在你的操作系统上轻松运行程序。
类似程序的思路
分页浏览的好处是你可以轻松地在新标签页中打开链接以供稍后查看。一个可以一次性打开多个链接的程序可以是一个很好的快捷方式来完成以下操作:
-
在像 Amazon 这样的购物网站上搜索后打开所有产品页面。
-
打开单个产品的所有评论链接。
-
在像 Flickr 或 Imgur 这样的照片网站上搜索后打开结果链接到照片。
项目 8:下载 XKCD 漫画
博客、网络漫画和其他定期更新的网站通常有一个首页,显示最新的帖子,以及一个“上一页”按钮,该按钮将你带到上一页。那个帖子也将有一个“上一页”按钮,依此类推,从最新页面创建一条通往网站第一个帖子的路径。如果你想在离线时阅读网站的内容副本,你可以手动浏览每个页面并保存每个页面。但这是一项相当无聊的工作,所以让我们编写一个程序来代替它。
如图 13-5 所示的 XKCD,是一个流行的极客网络漫画,其网站结构符合这一要求。首页xkcd.com有一个“上一页”按钮,它引导用户浏览之前的漫画。手动下载每一幅漫画将花费很长时间,但你可以编写一个脚本在几分钟内完成这项工作。

图 13-5:XKCD,“一个关于浪漫、讽刺、数学和语言的网络漫画”
你的程序应该执行以下操作:
-
加载 XKCD 主页。
-
在该页面上保存漫画图像。
-
跟随“上一页漫画”链接。
-
重复直到达到第一幅漫画或最大下载限制。
这意味着你的代码需要执行以下操作:
-
使用
requests模块下载网页。 -
使用 Beautiful Soup 找到页面中漫画图片的 URL。
-
使用
iter_content()将漫画图像下载并保存到硬盘。 -
找到“上一页漫画”链接的 URL,并重复。
在新的文件编辑标签中打开并保存为downloadXkcdComics.py。
第 1 步:设计程序
如果你打开浏览器的开发者工具并检查页面上的元素,你应该会发现以下情况是真实的:
-
<img>元素的src属性存储了漫画图片文件的 URL。 -
<img>元素位于<div id="comic">元素内。 -
“上一页”按钮有一个值为
prev的relHTML 属性。 -
最古老的漫画的“上一页”按钮链接到
xkcd.com/#URL,表示没有更多的上一页。
为了防止本书的读者消耗过多的 XKCD 网站带宽,让我们默认限制下载次数为 10 次。让你的代码看起来像下面这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# TODO: Download the page.
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
程序创建一个以'https://xkcd.com'开始的url变量,并在while循环中重复更新它(使用当前页面的“上一页”链接的 URL)。在循环的每一步中,你将下载url处的漫画。当url以'#'结尾或你已下载MAX_DOWNLOADS幅漫画时,循环停止。
你将下载图像文件到当前工作目录下的xkcd文件夹。os.makedirs()调用确保该文件夹存在,exist_ok=True关键字参数防止函数在文件夹已创建时抛出异常。
第 2 步:下载网页
让我们实现下载页面的代码。让你的代码看起来像下面这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# Download the page.
print(f'Downloading page {url}...')
res = requests.get(url)
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
首先,打印url,这样用户就知道程序将要下载哪个 URL;然后,使用requests模块的requests.get()函数下载它。像往常一样,你应该立即调用Response对象的raise_for_status()方法,如果下载过程中出现问题,则抛出异常并结束程序。否则,从下载页面的文本创建一个BeautifulSoup对象。
第 3 步:查找并下载漫画图像
要下载每页的漫画,让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Find the URL of the comic image.
comic_elem = soup.select('#comic img')
if comic_elem == []:
print('Could not find comic image.')
else:
comic_URL = 'https:' + comic_elem[0].get('src')
# Download the image.
print(f'Downloading image {comic_URL}...')
res = requests.get(comic_URL)
res.raise_for_status()
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
由于你已经使用开发者工具检查了 XKCD 主页,你知道漫画图像的 <img> 元素位于另一个具有 id 属性设置为 comic 的元素内部,因此选择器 '#comic img' 将从 BeautifulSoup 对象中获取正确的 <img> 元素。
一些 XKCD 页面有特殊内容,不是简单的图像文件。这没关系;你只需跳过那些页面。如果你的选择器没有找到任何元素,soup.select('#comic img') 将返回一个空列表的 ResultSet 对象。当这种情况发生时,程序可以打印一条错误消息并继续执行,而无需下载图像。
否则,选择器将返回一个包含一个 <img> 元素的列表。你可以从这个 <img> 元素获取 src 属性并将其传递给 requests.get() 以下载漫画的图像文件。
第 4 步:保存图像并查找上一则漫画
到这一点,漫画的图像文件存储在 res 变量中。你需要将此图像数据写入硬盘上的文件。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Save the image to ./xkcd.
image_file = open(os.path.join('xkcd', os.path.basename(comic_URL)), 'wb')
for chunk in res.iter_content(100000):
image_file.write(chunk)
image_file.close()
# Get the Prev button's URL.
prev_link = soup.select('a[rel="prev"]')[0]
url = 'https://xkcd.com' + prev_link.get('href')
num_downloads += 1
time.sleep(1) # Pause so we don't hammer the web server.
print('Done.')
你还需要一个本地图像文件的文件名,以便传递给 open()。comic_URL 将具有类似 'https://imgs.xkcd.com/comics/heartbleed_explanation.png' 的值,你可能已经注意到它看起来很像一个文件路径。实际上,你可以使用 os.path.basename() 和 comic_URL 来返回 URL 的最后一部分,即 'heartbleed_explanation.png',并在保存图像到硬盘时使用这个名称。使用 os.path.join() 将此名称与你的 xkcd 文件夹名称连接起来,这样你的程序在 Windows 上使用反斜杠(\),在 macOS 和 Linux 上使用正斜杠(/)。现在你终于有了文件名,你可以调用 open() 以 'wb' 模式打开一个新文件。
记住,在本章前面提到,为了保存使用 requests 下载的文件,你需要遍历 iter_content() 方法的返回值。for 循环中的代码将图像数据块写入文件。然后,代码关闭文件,将图像保存到你的硬盘上。
之后,选择器 'a[rel="prev"]' 识别具有 rel 属性设置为 prev 的 <a> 元素。你可以使用这个 <a> 元素的 href 属性来获取上一则漫画的 URL,该 URL 被存储在 url 中。
循环代码的最后部分将 num_downloads 增加 1,这样它就不会默认下载所有漫画。它还通过 time.sleep(1) 引入了一秒的暂停,以防止脚本“猛击”网站(即不礼貌地尽可能快地下载漫画,这可能会对其他网站访客造成性能问题)。然后,while 循环再次开始整个下载过程。
这个程序的输出将看起来像这样:
Downloading page https://xkcd.com...
Downloading image https://imgs.xkcd.com/comics/phone_alarm.png...
Downloading page https://xkcd.com/1358/...
Downloading image https://imgs.xkcd.com/comics/nro.png...
Downloading page https://xkcd.com/1357/...
Downloading image https://imgs.xkcd.com/comics/free_speech.png...
Downloading page https://xkcd.com/1356/...
Downloading image https://imgs.xkcd.com/comics/orbital_mechanics.png...
Downloading page https://xkcd.com/1355/...
Downloading image https://imgs.xkcd.com/comics/airplane_message.png...
Downloading page https://xkcd.com/1354/...
Downloading image https://imgs.xkcd.com/comics/heartbleed_explanation.png...
--`snip`--
这个项目是一个很好的例子,说明了一个程序可以自动跟随链接以从网络中抓取大量数据。您可以从其文档中了解 Beautiful Soup 的其他功能,该文档位于 www.crummy.com/software/BeautifulSoup/bs4/doc/。
类似程序的思路
许多网络爬虫程序涉及下载页面和跟随链接。类似的程序可以执行以下操作:
-
通过跟随所有链接来备份整个网站。
-
复制一个网络论坛上的所有消息。
-
复制在线商店上出售的商品目录。
requests 和 bs4 模块很棒,只要您能找出需要传递给 requests.get() 的 URL。然而,这个 URL 并非总是那么容易找到。或者,可能您想要程序导航的网站要求您先登录。Selenium 将赋予您的程序执行此类复杂任务的能力。
创建 Beautiful Soup 对象
bs4.BeautifulSoup() 函数接受一个包含要解析的 HTML 的字符串,然后返回一个 BeautifulSoup 对象。例如,当您的计算机连接到互联网时,将以下内容输入到交互式外壳中:
>>> import requests, bs4
>>> res = requests.get('https://autbor.com/example3.html')
>>> res.raise_for_status()
>>> example_soup = bs4.BeautifulSoup(res.text, 'html.parser')
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
此代码使用 requests.get() 下载 Automate the Boring Stuff 网站的首页,然后将响应的 text 属性传递给 bs4.BeautifulSoup()。Beautiful Soup 可以解析不同的格式,'html.parser' 参数告诉它我们正在解析 HTML。最后,代码将返回的 BeautifulSoup 对象存储在名为 example_soup 的变量中。
您也可以通过将 File 对象传递给 bs4.BeautifulSoup() 来从您的硬盘加载一个 HTML 文件。在确保 example3.html 文件位于工作目录后,将以下内容输入到交互式外壳中:
>>> import bs4
>>> with open('example3.html') as example_file:
... example_soup = bs4.BeautifulSoup(example_file, 'html.parser')
...
>>> type(example_soup)
<class 'bs4.BeautifulSoup'>
一旦您有一个 BeautifulSoup 对象,您就可以使用其方法来定位 HTML 文档的特定部分。
查找元素
您可以通过调用 BeautifulSoup 对象的 select() 方法并传递您要查找的元素的 CSS 选择器字符串来从 BeautifulSoup 对象中检索网页元素。该方法返回一个 Tag 对象的列表,这些对象代表匹配的 HTML 元素。表 13-2 显示了使用 select() 的最常见 CSS 选择器模式的示例。
表 13-2:CSS 选择器示例
| 传递给 select() 方法的选择器 | 将匹配 ... |
|---|---|
soup.select('div') |
所有名为 <div> 的元素 |
soup.select('#author') |
具有名为 author 的 id 属性的元素 |
soup.select('.notice') |
使用名为 notice 的 CSS class 属性的所有元素 |
soup.select('div span') |
位于名为 <div> 的元素内的所有名为 <span> 的元素 |
soup.select('div > span') |
所有直接位于名为 <div> 的元素内的名为 <span> 的元素,中间没有其他元素 |
soup.select('input[name]') |
所有具有任何值 name 属性的 <input> 元素 |
soup.select('input[type="button"]') |
所有具有名为 type 的属性且其值为 button 的 <input> 元素 |
您可以将各种选择器模式组合起来以进行复杂匹配。例如,soup.select('p #author') 匹配任何具有 id 属性为 author 的元素,只要它也位于 <p> 元素内部。
您可以将标签值传递给 str() 函数以显示它们所代表的 HTML 标签。标签值还具有一个 attrs 属性,它包含所有它们的 HTML 属性作为字典。例如,将 autbor.com/example3.html 页面下载为 example3.html,然后在交互式外壳中输入以下内容:
>>> import bs4
>>> example_file = open('example3.html')
>>> example_soup = bs4.BeautifulSoup(example_file.read(), 'html.parser')
>>> elems = example_soup.select('#author')
>>> type(elems) # elems is a list of Tag objects.
<class 'bs4.element.ResultSet'>
>>> len(elems)
1
>>> type(elems[0])
<class 'bs4.element.Tag'>
>>> str(elems[0]) # The Tag object as a string
'<span id="author">Al Sweigart</span>'
>>> elems[0].get_text() # The inner text of the element
'Al Sweigart'
>>> elems[0].attrs
{'id': 'author'}
此代码在我们的示例 HTML 中查找具有 id="author" 的元素。我们使用 select('#author') 返回一个包含所有具有 id="author" 的元素的列表。然后我们将这个 Tag 对象的列表存储在变量 elems 中。运行 len(elems) 告诉我们列表中有一个 Tag 对象,这意味着有一个匹配项。
将元素传递给 str() 返回一个包含起始和结束标签以及元素文本的字符串。在元素上调用 get_text() 返回元素的文本,即打开和关闭标签之间的内容:在这种情况下,'Al Sweigart'。最后,attrs 给我们一个包含元素的属性 'id' 和 id 属性的值的字典。
您还可以从 BeautifulSoup 对象中提取所有 <p> 元素。将以下内容输入到交互式外壳中:
>>> p_elems = example_soup.select('p')
>>> str(p_elems[0])
'<p>This <p> tag puts <b>content</b> into a <i>single</i> paragraph.</p>'
>>> p_elems[0].get_text()
'This <p> tag puts content into a single paragraph.'
>>> str(p_elems[1])
'<p> <a href="https://inventwithpython.com/”>This text is a link</a> to books by
<span id="author">Al Sweigart</span>.</p>'
>>> p_elems[1].get_text()
'This text is a link to books by Al Sweigart.'
>>> str(p_elems[2])
'<p><img alt="Close-up of my cat Zophie." src="wow_such_zophie_thumb.webp"/></p>'
>>> p_elems[2].get_text()
''
这次,select() 给我们三个匹配项的列表,我们将其存储在 p_elems 中。使用 str() 对 p_elems[0]、p_elems[1] 和 p_elems[2] 进行操作,您可以看到每个元素作为字符串,并且使用 get_text() 对每个元素进行操作,您可以看到其文本。
从元素的属性中获取数据
Tag 对象的 get() 方法允许您从元素访问 HTML 属性值。您需要将一个属性名作为字符串传递给该方法,并接收该属性的值。使用 example3.html 从 autbor.com/example3.html,将以下内容输入到交互式外壳中:
>>> import bs4
>>> soup = bs4.BeautifulSoup(open('example3.html'), 'html.parser')
>>> span_elem = soup.select('span')[0]
>>> str(span_elem)
'<span id="author">Al Sweigart</span>'
>>> span_elem.get('id')
'author'
>>> span_elem.get('some_nonexistent_addr') == None
True
>>> span_elem.attrs
{'id': 'author'}
在这里,我们使用 select() 来查找任何 <span> 元素,然后将第一个匹配的元素存储在 span_elem 中。将属性名 'id' 传递给 get() 返回该属性的值,即 'author'。
项目 7:打开所有搜索结果
当我在搜索引擎上查找一个主题时,我不会一次只查看一个搜索结果。通过 中间点击 搜索结果链接(或按住 CTRL 点击),我会在许多新标签页中打开前几个链接以供稍后阅读。我经常上网搜索,这个工作流程——打开我的浏览器,搜索一个主题,并逐个中间点击几个链接——是繁琐的。如果能简单地输入一个术语到命令行,让我的电脑自动在新的浏览器标签页中打开顶级搜索结果,那就太好了。
让我们编写一个脚本来完成 Python 包索引的搜索结果页面的工作。pypi.org。你可以将这样的程序适应到许多其他网站上,尽管 Google、DuckDuckGo、Amazon 和其他大型网站通常采取一些措施,使得抓取它们的搜索结果页面变得困难。
这就是程序应该执行的操作:
-
从命令行参数获取搜索关键词
-
获取搜索结果页面
-
为每个结果打开一个浏览器标签
这意味着你的代码需要执行以下操作:
-
从
sys.argv中读取命令行参数。 -
使用
requests模块获取搜索结果页面。 -
找到每个搜索结果的链接。
-
调用
webbrowser.open()函数打开网页浏览器。
打开一个新的文件编辑标签页并将其保存为 searchpypi.py。
第一步:获取搜索页面
在编写代码之前,你首先需要知道搜索结果页面的 URL。通过在搜索后查看浏览器的地址栏,你可以看到结果页面有一个看起来像这样的 URL:pypi.org/search/?q=<SEARCH_TERM_HERE>。requests 模块可以下载这个页面;然后,你可以使用 Beautiful Soup 在 HTML 中找到搜索结果链接。最后,你将使用 webbrowser 模块在浏览器标签页中打开这些链接。
使你的代码看起来像以下这样:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
print('Searching...') # Display text while downloading the search results page.
res = requests.get('https://pypi.org/search/?q=' + ' '.join(sys.argv[1:]))
res.raise_for_status()
# TODO: Retrieve top search result links.
# TODO: Open a browser tab for each result.
用户在启动程序时将指定搜索术语作为命令行参数,代码将这些参数作为字符串存储在 sys.argv 中的列表中。
第二步:查找所有结果
现在你需要使用 Beautiful Soup 从你下载的 HTML 中提取顶级搜索结果链接。但你是如何确定正确的选择器的?例如,你不能仅仅搜索所有的 <a> 标签,因为在 HTML 中有许多你不需要的链接。相反,你必须使用浏览器的开发者工具检查搜索结果页面,试图找到一个选择器,它只会选择你想要的链接。
在搜索 pyautogui 之后,你可以打开浏览器的开发者工具并检查页面上的某些链接元素。它们可能看起来很复杂,就像这样:<a class="package-snippet" href="/project/pyautogui" >。但元素看起来非常复杂并不重要。你只需要找到所有搜索结果链接的模式。
让你的代码看起来如下所示:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
# --snip--
# Retrieve top search result links.
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
如果你查看 <a> 元素,你会看到搜索结果链接都具有 class="package-snippet"。浏览 HTML 源代码的其余部分,看起来 package-snippet 类仅用于搜索结果链接。你不必知道 CSS 类 package-snippet 是什么或它做什么。你只是用它作为你正在寻找的 <a> 元素的标记。
你可以从下载的页面的 HTML 文本创建一个 BeautifulSoup 对象,然后使用选择器 '.package-snippet' 来找到所有位于具有 package-snippet CSS 类的元素内的 <a> 元素。请注意,如果 PyPI 网站更改了其布局,你可能需要更新此程序,以使用新的 CSS 选择器字符串传递给 soup.select()。程序的其余部分应保持最新。
第 3 步:为每个结果打开网页浏览器
最后,你必须告诉程序为结果打开网页浏览器标签。将以下内容添加到程序的末尾:
# searchpypi.py - Opens several search results on pypi.org
import requests, sys, webbrowser, bs4
--`snip`--
# Open a browser tab for each result.
link_elems = soup.select('.package-snippet')
num_open = min(5, len(link_elems))
for i in range(num_open):
url_to_open = 'https://pypi.org' + link_elems[i].get('href')
print('Opening', url_to_open)
webbrowser.open(url_to_open)
默认情况下,程序使用 webbrowser 模块在新标签页中打开前五个搜索结果。然而,用户可能搜索的内容结果少于五个。soup.select() 调用返回一个匹配你的 '.package-snippet' 选择器的所有元素的列表,因此你想要打开的标签页数量是 5 或这个列表的长度(取较小者)。
内置的 Python 函数 min() 返回它传递的整数或浮点数中的最小值。(还有一个内置的 max() 函数,它返回它传递的最大值。)你可以使用 min() 来找出列表中是否有少于五个链接,并将打开的链接数量存储在名为 num_open 的变量中。然后,你可以通过调用 range(num_open) 来运行 for 循环。
在循环的每次迭代中,代码使用 webbrowser.open() 在网页浏览器中打开一个新的标签页。请注意,返回的 <a> 元素的 href 属性值没有初始的 pypi.org 部分,所以你必须将其连接到 href 属性的字符串值。
现在,你可以在命令行上运行 searchpypi boring stuff 来立即打开关于 无聊事物 的前五个 PyPI 搜索结果!请参阅第十二章,了解如何在你的操作系统上轻松运行程序。
类似程序的思路
分页浏览的好处是你可以轻松地在新标签页中打开链接以供稍后查看。一个可以一次性打开多个链接的程序可以是一个很好的快捷方式来做以下事情:
-
在像 Amazon 这样的购物网站上搜索后,打开所有产品页面。
-
打开单个产品的所有评论链接。
-
在像 Flickr 或 Imgur 这样的照片网站上搜索后,打开结果链接到照片。
项目 8:下载 XKCD 漫画
博客、网络漫画和其他定期更新的网站通常有一个首页,上面有最新的帖子,以及一个页面上的上一页按钮,该按钮将用户带回到之前的帖子。那个帖子也将有一个上一页按钮,依此类推,从最新页面到网站上的第一篇帖子创建一条路径。如果你想在离线时阅读网站的内容,你可以手动浏览每个页面并保存每个页面。但这是一项相当无聊的工作,所以让我们编写一个程序来代替。
如图 13-5 所示,XKCD 是一个流行的极客网络漫画,其网站结构符合此模式。xkcd.com 的首页有一个上一页按钮,引导用户浏览之前的漫画。手动下载每篇漫画将花费很长时间,但你可以编写一个脚本在几分钟内完成这项工作。

图 13-5:XKCD,“一个关于浪漫、讽刺、数学和语言的网络漫画”
你的程序应该执行以下操作:
-
加载 XKCD 主页。
-
在该页面上保存漫画图像。
-
点击上一则漫画链接。
-
重复此操作,直到达到第一篇漫画或最大下载限制。
这意味着你的代码需要执行以下操作:
-
使用
requests模块下载页面。 -
使用 Beautiful Soup 查找页面中漫画图像的 URL。
-
使用
iter_content()将漫画图像下载并保存到硬盘上。 -
查找上一则漫画链接的 URL,并重复此操作。
打开一个新的文件编辑标签页,并将其保存为 downloadXkcdComics.py。
第一步:设计程序
如果你打开浏览器的开发者工具并检查页面上的元素,你应该会发现以下情况是真实的:
-
<img>元素的src属性存储了漫画图像文件的 URL。 -
<img>元素位于<div id="comic">元素内部。 -
上一页按钮有一个值为
prev的relHTML 属性。 -
最古老的漫画的上一页按钮链接到
xkcd.com/#URL,表示没有更多的上一页。
为了防止本书的读者过多消耗 XKCD 网站的带宽,让我们默认将下载次数限制为 10 次。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# TODO: Download the page.
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
程序创建一个以 'https://xkcd.com' 为起始值的 url 变量,并在 while 循环中反复更新它(使用当前页面的上一页链接的 URL)。在循环的每一步,你都会下载 url 上的漫画。当 url 以 '#' 结尾或你已下载 MAX_DOWNLOADS 漫画时,循环停止。
你将下载图像文件到当前工作目录下的 xkcd 文件夹中。调用 os.makedirs() 确保该文件夹存在,exist_ok=True 关键字参数防止函数在文件夹已创建时抛出异常。
第二步:下载网页
让我们实现下载页面的代码。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
url = 'https://xkcd.com' # Starting URL
os.makedirs('xkcd', exist_ok=True) # Store comics in ./xkcd
num_downloads = 0
MAX_DOWNLOADS = 10
while not url.endswith('#') and num_downloads < MAX_DOWNLOADS:
# Download the page.
print(f'Downloading page {url}...')
res = requests.get(url)
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, 'html.parser')
# TODO: Find the URL of the comic image.
# TODO: Download the image.
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
首先,打印 url,这样用户就知道程序将要下载哪个 URL;然后,使用 requests 模块的 requests.get() 函数下载它。像往常一样,你应该立即调用 Response 对象的 raise_for_status() 方法,如果下载过程中出现问题,则抛出异常并结束程序。否则,从下载的页面文本创建一个 BeautifulSoup 对象。
第 3 步:查找并下载漫画图像
要下载每页的漫画,让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Find the URL of the comic image.
comic_elem = soup.select('#comic img')
if comic_elem == []:
print('Could not find comic image.')
else:
comic_URL = 'https:' + comic_elem[0].get('src')
# Download the image.
print(f'Downloading image {comic_URL}...')
res = requests.get(comic_URL)
res.raise_for_status()
# TODO: Save the image to ./xkcd.
# TODO: Get the Prev button's url.
print('Done.')
因为你已经使用开发者工具检查了 XKCD 主页,你知道漫画图像的 <img> 元素位于具有 id 属性设置为 comic 的另一个元素内部,所以选择器 '#comic img' 将从 BeautifulSoup 对象中获取正确的 <img> 元素。
几个 XKCD 页面有特殊内容,不是简单的图像文件。这没关系;你只需跳过这些页面。如果你的选择器没有找到任何元素,soup.select('#comic img') 将返回一个空的 ResultSet 对象。当这种情况发生时,程序可以打印一条错误消息并继续执行,而无需下载图像。
否则,选择器将返回一个包含一个 <img> 元素的列表。你可以从这个 <img> 元素获取 src 属性,并将其传递给 requests.get() 以下载漫画的图像文件。
第 4 步:保存图像并查找上一期漫画
到目前为止,漫画的图像文件存储在 res 变量中。你需要将此图像数据写入硬盘上的文件。让你的代码看起来像以下这样:
# downloadXkcdComics.py - Downloads XKCD comics
import requests, os, bs4, time
--`snip`--
# Save the image to ./xkcd.
image_file = open(os.path.join('xkcd', os.path.basename(comic_URL)), 'wb')
for chunk in res.iter_content(100000):
image_file.write(chunk)
image_file.close()
# Get the Prev button's URL.
prev_link = soup.select('a[rel="prev"]')[0]
url = 'https://xkcd.com' + prev_link.get('href')
num_downloads += 1
time.sleep(1) # Pause so we don't hammer the web server.
print('Done.')
你还需要为本地图像文件提供一个文件名,以便传递给 open() 函数。comic_URL 将具有类似于 'https://imgs.xkcd.com/comics/heartbleed_explanation.png' 的值,你可能已经注意到它看起来很像一个文件路径。实际上,你可以使用 os.path.basename() 函数和 comic_URL 来返回 URL 的最后一部分,即 'heartbleed_explanation.png',并在将图像保存到硬盘时使用这个文件名。使用 os.path.join() 将此名称与你的 xkcd 文件夹名称连接起来,这样你的程序在 Windows 上使用反斜杠(\),在 macOS 和 Linux 上使用正斜杠(/)。现在你终于有了文件名,你可以调用 open() 来以 'wb' 模式打开一个新文件。
记得在本章前面提到,为了保存使用 requests 下载的文件,你需要遍历 iter_content() 方法的返回值。for 循环中的代码将图像数据块写入文件。然后,代码关闭文件,将图像保存到你的硬盘上。
之后,选择器 'a[rel="prev"]' 识别具有 rel 属性设置为 prev 的 <a> 元素。你可以使用此 <a> 元素的 href 属性来获取上一期漫画的 URL,该 URL 被存储在 url 中。
循环代码的最后部分将 num_downloads 增加 1,这样就不会默认下载所有漫画。它还通过 time.sleep(1) 引入了一秒的暂停,以防止脚本“猛击”网站(即不礼貌地尽可能快地下载漫画,这可能会对其他网站访客造成性能问题)。然后,while 循环再次开始整个下载过程。
这个程序的输出将看起来像这样:
Downloading page https://xkcd.com...
Downloading image https://imgs.xkcd.com/comics/phone_alarm.png...
Downloading page https://xkcd.com/1358/...
Downloading image https://imgs.xkcd.com/comics/nro.png...
Downloading page https://xkcd.com/1357/...
Downloading image https://imgs.xkcd.com/comics/free_speech.png...
Downloading page https://xkcd.com/1356/...
Downloading image https://imgs.xkcd.com/comics/orbital_mechanics.png...
Downloading page https://xkcd.com/1355/...
Downloading image https://imgs.xkcd.com/comics/airplane_message.png...
Downloading page https://xkcd.com/1354/...
Downloading image https://imgs.xkcd.com/comics/heartbleed_explanation.png...
--`snip`--
这个项目是一个很好的例子,说明了一个可以自动跟随链接从网络上抓取大量数据的程序。你可以从其文档中了解 Beautiful Soup 的其他功能,文档地址为 www.crummy.com/software/BeautifulSoup/bs4/doc/。
相似程序的创意
许多网络爬虫程序涉及下载页面和跟随链接。类似的程序可以执行以下操作:
-
通过跟随所有链接来备份整个网站。
-
复制一个网络论坛上的所有消息。
-
复制在线商店上出售的商品目录。
requests 和 bs4 模块只要你能找出需要传递给 requests.get() 的 URL 就很棒。然而,这个 URL 并非总是那么容易找到。或者,也许你想要你的程序导航的网站要求你先登录。Selenium 将赋予你的程序执行此类复杂任务的能力。
使用 Selenium 控制浏览器
Selenium 允许 Python 通过程序化点击链接和填写表单直接控制浏览器,就像一个人类用户一样。使用 Selenium,你可以以比 requests 和 Beautiful Soup 更高级的方式与网页交互;但因为它启动了一个网络浏览器,所以它稍微慢一些,如果你只是需要从网络上下载一些文件,那么在后台运行可能会有些困难。
尽管如此,如果你需要以某种方式与网页交互,例如依赖于更新网页的 JavaScript 代码,那么你需要使用 Selenium 而不是 requests。这是因为像亚马逊这样的主要电子商务网站几乎肯定有软件系统来识别他们怀疑是脚本正在收集他们的信息或注册多个免费账户的流量。这些网站可能会在一段时间后拒绝为你提供服务,破坏你制作的任何脚本。与 requests 相比,Selenium 在这些网站上长期运行的可能性要大得多。
一个主要的“迹象”表明您正在使用脚本的是用户代理字符串,它标识了网络浏览器,并包含在所有 HTTP 请求中。例如,requests模块的用户代理字符串可能类似于'python-requests/XX.XX.X'。您可以通过访问如www.whatsmyua.info的网站来查看您的用户代理字符串。使用 Selenium,您更有可能被当作人类,因为 Selenium 的用户代理与常规浏览器相同(例如,' Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0'),并且具有相同的流量模式:Selenium 控制的浏览器会像常规浏览器一样下载图片、广告、cookies 和侵犯隐私的跟踪器。然而,网站仍然可以找到方法检测 Selenium,并且主要票务和电子商务网站通常阻止它以防止其页面的网络抓取。
启动 Selenium 控制的浏览器
以下示例将向您展示如何控制 Firefox 的浏览器。如果您还没有 Firefox,您可以从getfirefox.com免费下载。
导入 Selenium 的模块稍微有些棘手。您不能使用import selenium,而必须运行from selenium import webdriver。(Selenium 为何设置为这种方式的具体原因超出了本书的范围。)之后,您可以使用 Selenium 启动 Firefox 浏览器。在交互式 shell 中输入以下内容:
>>> from selenium import webdriver
>>> browser = webdriver.Firefox()
>>> type(browser)
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
>>> browser.get('https://inventwithpython.com')
您会注意到,当调用webdriver.Firefox()时,Firefox 浏览器会启动。对webdriver.Firefox()的值调用type()会显示它属于WebDriver数据类型。并且调用browser.get('https://inventwithpython.com')会将浏览器导向inventwithpython.com。您的浏览器应该看起来像图 13-6 所示。

图 13-6:在 Mu 中调用 webdriver.Firefox()和 get()之后,Firefox 浏览器出现。
如果遇到错误信息“geckodriver 可执行文件需要位于 PATH 中”,在使用 Selenium 控制它之前,您需要手动下载 Firefox 的 WebDriver。如果您为其他浏览器安装了 WebDriver,您也可以控制这些浏览器,并且您可以使用来自pypi.org/project/webdriver-manager/的 webdriver-manager 包来代替手动下载浏览器 WebDriver。
点击浏览器按钮
Selenium 可以通过以下方法模拟点击各种浏览器按钮:
browser.back() 点击后退按钮
browser.forward() 点击前进按钮
browser.refresh() 点击刷新/重新加载按钮
browser.quit() 点击关闭窗口按钮
在页面中查找元素
WebDriver 对象具有 find_element() 和 find_elements() 方法,用于在网页上查找元素。find_element() 方法返回一个代表页面中第一个与查询匹配的元素的 WebElement 对象。find_elements() 方法返回一个 WebElement 对象列表,包含页面上的所有匹配元素。
您可以通过元素的类名、CSS 选择器、ID 或其他方式来查找元素。首先,运行 from selenium.webdriver.common.by import By 以获取 By 对象。By 对象有几个常量可以传递给 find_element() 和 find_elements() 方法。表 13-3 列出了这些常量。
| 表 13-3:Selenium 的 By 常量用于查找元素 |
| 常量名称 | 返回的 WebElement 对象/列表 |
|---|---|
By.CLASS_NAME |
使用 CSS 类名的元素 |
By.CSS_SELECTOR |
与 CSS 选择器匹配的元素 |
By.ID |
匹配 id 属性值的元素 |
By.LINK_TEXT |
完全匹配提供文本的 <a> 元素 |
By.PARTIAL_LINK_TEXT |
包含提供文本的 <a> 元素 |
By.NAME |
匹配 name 属性值的元素 |
By.TAG_NAME |
匹配标签名的元素(不区分大小写;<a> 元素可以通过 'a' 和 'A' 匹配) |
如果页面上不存在与该方法查找匹配的元素,Selenium 会引发 NoSuchElement 异常。如果您不希望这个异常使您的程序崩溃,请在代码中添加 try 和 except 语句。
一旦您有了 WebElement 对象,您可以通过阅读表 13-4 中的属性或调用方法来了解更多关于它的信息。
| 表 13-4:WebElement 属性和方法 |
| 属性或方法 | 描述 |
|---|---|
tag_name |
标签名,例如 <a> 元素的 'a'。 |
get_attribute(name) |
元素的 name 属性值,例如 <a> 元素中的 href。 |
get_property(name) |
元素的属性值,该属性值不出现在 HTML 代码中。一些 HTML 属性的例子有 innerHTML 和 innerText。 |
text |
元素内的文本,例如以下示例中的 'hello':<span>hello</span> |
clear() |
对于文本字段或文本区域元素,清除其中输入的文本。 |
is_displayed() |
如果元素可见则返回 True,否则返回 False。 |
is_enabled() |
对于输入元素,如果元素处于启用状态则返回 True,否则返回 False。 |
is_selected() |
对于复选框或单选按钮元素,如果元素被选中则返回 True,否则返回 False。 |
location |
一个包含键 'x' 和 'y' 的字典,用于表示元素在页面中的位置。 |
size |
一个包含键 'width' 和 'height' 的字典,用于表示元素在页面中的大小。 |
例如,打开一个新的文件编辑标签并输入以下程序:
from selenium import webdriver
from selenium.webdriver.common.by import By
browser = webdriver.Firefox()
browser.get('https://autbor.com/example3.html')
elems = browser.find_elements(By.CSS_SELECTOR, 'p')
print(elems[0].text)
print(elems[0].get_property('innerHTML'))
在这里,我们打开 Firefox 并将其指向一个 URL。在这个页面上,我们获取一个 <p> 元素的列表,查看索引为 0 的第一个元素,然后获取该 <p> 元素内部的文本字符串。接下来,我们获取其 innerHTML 属性的字符串。此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
元素的 text 属性显示的文本就像我们在网页浏览器中看到的那样:“这个 <p> 标签将内容放入一个单独的段落。”我们还可以通过调用 get_property() 方法来检查元素的 innerHTML 属性,这包括带有标签和 HTML 实体的 HTML 源代码。(< 和 > 是 HTML 转义字符,分别代表小于 [<] 和大于 [>] 符号。)
注意,text 属性只是调用 get_property('innerText') 的快捷方式。innerHTML 和 innerText 是 HTML 元素的属性的标准名称。简而言之,这些元素属性是通过 JavaScript 代码和 Web 驱动程序访问的,而元素属性是 HTML 源代码的一部分,就像 <a href="https://inventwithpython.com"> 中的 href 一样。
在页面上点击元素
从 find_element() 和 find_elements() 方法返回的 WebElement 对象有一个 click() 方法,它模拟在该元素上点击鼠标。此方法可用于跟随链接、在单选按钮上做出选择、点击提交按钮或触发鼠标点击元素时可能发生的任何其他操作。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> link_elem = browser.find_element(By.LINK_TEXT, 'This text is a link')
>>> type(link_elem)
<class 'selenium.webdriver.remote.webelement.WebElement'>
>>> link_elem.click() # Follows the "This text is a link" link
此代码打开 Firefox 到 autbor.com/example3.html,获取文本为 This is a link 的 <a> 元素的 WebElement 对象,然后模拟点击该 <a> 元素,就像您亲自点击链接一样;浏览器随后会跟随该链接。
填写和提交表单
向网页上的文本字段发送按键是找到该文本字段的 <input> 或 <textarea> 元素,然后调用 send_keys() 方法的问题。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> user_elem = browser.find_element(By.ID, 'login_user')
>>> user_elem.send_keys('`your_real_username_here`')
>>> password_elem = browser.find_element(By.ID, 'login_pass')
>>> password_elem.send_keys('`your_real_password_here`')
>>> password_elem.submit()
只要登录页面的用户名和密码 <input> 元素的 id 没有改变,之前的代码就会用提供的文本填写这些文本字段。(您始终可以使用浏览器的检查器来验证 id。)在任意元素上调用 submit() 方法将产生与点击该元素所在表单的提交按钮相同的结果。(您同样可以调用 user_elem.submit(),代码会做同样的事情。)
警告
尽可能避免在源代码中放置您的密码。当它们在硬盘上未加密时,很容易不小心将您的密码泄露给他人。
发送特殊按键
Selenium 有一个模块,selenium.webdriver.common.keys,用于表示键盘键,并将其存储在属性中。由于该模块名称较长,在程序顶部运行 from selenium.webdriver.common.keys import Keys 会更加方便;如果你这样做,你可以在任何需要写入 selenium.webdriver.common.keys 的地方简单地写入 Keys。
你可以向 send_keys() 传递以下任何常量:
Keys.ENTER Keys.PAGE_UP Keys.DOWN
Keys.RETURN Keys.ESCAPE Keys.LEFT
Keys.HOME Keys.BACK_SPACE Keys.RIGHT
Keys.END Keys.DELETE Keys.TAB
Keys.PAGE_DOWN Keys.UP Keys.F1 to Keys.F12
你也可以向该方法传递一个字符串,例如 'hello' 或 '?'。
例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别将浏览器滚动到页面的顶部和底部。在交互式壳中输入以下内容,并注意 send_keys() 调用如何滚动页面:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> from selenium.webdriver.common.keys import Keys
>>> browser = webdriver.Firefox()
>>> browser.get('https://nostarch.com')
>>> html_elem = browser.find_element(By.TAG_NAME, 'html')
>>> html_elem.send_keys(Keys.END) # Scrolls to bottom
>>> html_elem.send_keys(Keys.HOME) # Scrolls to top
<html> 标签是 HTML 文件中的基本标签:HTML 文件的全部内容都包含在 <html> 和 </html> 标签之间。调用 browser.find_element(By.TAG_NAME, 'html') 是通过主要的 <html> 标签向整个网页发送键值的好方法。例如,当你滚动到页面底部后,新内容被加载,这将非常有用。
Selenium 可以做比这里描述的函数更多的事情。它可以修改你的浏览器 cookies,对网页进行截图,并运行自定义 JavaScript。要了解更多关于这些功能的信息,你可以访问 Selenium 文档 selenium-python.readthedocs.io。你还可以通过搜索网站 pyvideo.org 来找到有关 Selenium 的 Python 会议演讲。
启动 Selenium 控制的浏览器
以下示例将展示如何控制 Firefox 的网络浏览器。如果你还没有 Firefox,你可以从 getfirefox.com 免费下载。
导入 Selenium 的模块稍微有些复杂。不是 import selenium,你必须运行 **from selenium import webdriver**。 (Selenium 为什么这样设置的原因超出了本书的范围。)之后,你可以使用 Selenium 启动 Firefox 浏览器。在交互式壳中输入以下内容:
>>> from selenium import webdriver
>>> browser = webdriver.Firefox()
>>> type(browser)
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
>>> browser.get('https://inventwithpython.com')
你会注意到,当调用 webdriver.Firefox() 时,Firefox 网络浏览器会启动。在 webdriver.Firefox() 的值上调用 type() 会显示它属于 WebDriver 数据类型。调用 browser.get('https://inventwithpython.com') 将浏览器导向 inventwithpython.com。你的浏览器应该看起来像图 13-6 所示。

图 13-6:在 Mu 中调用 webdriver.Firefox() 和 get() 后,Firefox 浏览器出现。
如果您遇到错误消息“geckodriver 可执行文件需要位于 PATH 中”,在您可以使用 Selenium 控制它之前,您需要手动下载 Firefox 的 WebDriver。如果您为其他浏览器安装了 WebDriver,您也可以控制这些浏览器,并且您可以使用来自pypi.org/project/webdriver-manager/的 webdriver-manager 包来代替手动下载浏览器 WebDriver。
点击浏览器按钮
Selenium 可以通过以下方法模拟点击各种浏览器按钮:
browser.back() 点击后退按钮
browser.forward() 点击前进按钮
browser.refresh() 点击刷新/重新加载按钮
browser.quit() 点击关闭窗口按钮
在页面上查找元素
WebDriver对象具有find_element()和find_elements()方法,用于在网页上查找元素。find_element()方法返回一个代表页面上与您的查询匹配的第一个元素的WebElement对象。find_elements()方法返回一个包含页面上所有匹配元素的WebElement对象列表。
您可以通过元素的类名、CSS 选择器、ID 或其他方式来查找元素。首先,运行from selenium.webdriver.common.by import By以获取By对象。By对象有几个常量,您可以将它们传递给find_element()和find_elements()方法。表 13-3 列出了这些常量。
表 13-3:Selenium 查找元素的By常量
| 常量名称 | 返回的WebElement对象/列表 |
|---|---|
By.CLASS_NAME |
使用 CSS 类名的元素 |
By.CSS_SELECTOR |
匹配 CSS 选择器的元素 |
By.ID |
匹配 id 属性值的元素 |
By.LINK_TEXT |
完全匹配提供的文本的<a>元素 |
By.PARTIAL_LINK_TEXT |
包含提供的文本的<a>元素 |
By.NAME |
匹配 name 属性值的元素 |
By.TAG_NAME |
匹配标签名的元素(不区分大小写;<a>元素可以通过'a'和'A'匹配) |
如果页面上没有元素与该方法查找的内容匹配,Selenium 将引发NoSuchElement异常。如果您不希望这个异常导致程序崩溃,请在您的代码中添加try和except语句。
一旦您有了WebElement对象,您可以通过阅读表 13-4 中的属性或调用方法来了解更多关于它的信息。
表 13-4:WebElement属性和方法
| 属性或方法 | 描述 |
|---|---|
tag_name |
标签名,例如<a>元素的'a'。 |
get_attribute(name) |
元素的name属性值,例如<a>元素中的href。 |
get_property(name) |
元素的属性值,该属性值不出现在 HTML 代码中。一些 HTML 属性的示例是innerHTML和innerText。 |
text |
元素内的文本,例如以下示例中的 'hello':<span>hello</span> |
clear() |
对于文本字段或文本区域元素,清除其中输入的文本。 |
is_displayed() |
如果元素可见,则返回 True;否则,返回 False。 |
is_enabled() |
对于输入元素,如果元素是启用的,则返回 True;否则,返回 False。 |
is_selected() |
对于复选框或单选按钮元素,如果元素被选中,则返回 True;否则,返回 False。 |
location |
一个包含 'x' 和 'y' 键的字典,用于表示元素在页面中的位置。 |
size |
一个包含 'width' 和 'height' 键的字典,用于表示元素在页面中的大小。 |
例如,打开一个新的文件编辑标签,并输入以下程序:
from selenium import webdriver
from selenium.webdriver.common.by import By
browser = webdriver.Firefox()
browser.get('https://autbor.com/example3.html')
elems = browser.find_elements(By.CSS_SELECTOR, 'p')
print(elems[0].text)
print(elems[0].get_property('innerHTML'))
在这里,我们打开 Firefox 并将其指向一个 URL。在这个页面上,我们获取一个 <p> 元素的列表,查看索引为 0 的第一个元素,然后获取该 <p> 元素内部的文本字符串。接下来,我们获取其 innerHTML 属性的字符串。此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
元素的 text 属性显示的文本是我们会在网页浏览器中看到的样子:“这个 <p> 标签将内容放入一个单独的段落。” 我们还可以通过调用 get_property() 方法来检查元素的 innerHTML 属性,这是包含标签和 HTML 实体的 HTML 源代码。(< 和 > 是 HTML 转义字符,分别代表小于 [<] 和大于 [>] 符号。)
注意,text 属性只是调用 get_property('innerText') 的快捷方式。innerHTML 和 innerText 是 HTML 元素的标准化属性名称。简而言之,这些元素属性通过 JavaScript 代码和 Web 驱动程序访问,而元素属性是 HTML 源代码的一部分,如 <a href="https://inventwithpython.com"> 中的 href。
点击页面上的元素
从 find_element() 和 find_elements() 方法返回的 WebElement 对象具有一个 click() 方法,该方法模拟在该元素上执行鼠标点击。此方法可用于跟随链接、在单选按钮上做出选择、点击提交按钮或触发鼠标点击元素时可能发生的任何其他操作。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> link_elem = browser.find_element(By.LINK_TEXT, 'This text is a link')
>>> type(link_elem)
<class 'selenium.webdriver.remote.webelement.WebElement'>
>>> link_elem.click() # Follows the "This text is a link" link
此代码打开 Firefox 并导航到 autbor.com/example3.html,获取文本为 This is a link 的 <a> 元素的 WebElement 对象,然后模拟点击该 <a> 元素,就像您亲自点击链接一样;浏览器随后会跟随该链接。
填写和提交表单
向网页上的文本字段发送按键操作是找到该文本字段的 <input> 或 <textarea> 元素,然后调用 send_keys() 方法的问题。例如,在交互式外壳中输入以下内容:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> browser = webdriver.Firefox()
>>> browser.get('https://autbor.com/example3.html')
>>> user_elem = browser.find_element(By.ID, 'login_user')
>>> user_elem.send_keys('`your_real_username_here`')
>>> password_elem = browser.find_element(By.ID, 'login_pass')
>>> password_elem.send_keys('`your_real_password_here`')
>>> password_elem.submit()
只要登录页面没有更改用户名和密码 <input> 元素的 id,之前的代码就会用提供的文本填充这些文本字段。(您始终可以使用浏览器的检查器来验证 id。)在任意元素上调用 submit() 方法将产生与点击该元素所在表单的提交按钮相同的结果。(您同样可以调用 user_elem.submit(),代码会执行相同的功能。)
警告
尽可能避免在源代码中放置密码。当密码在您的硬盘上未加密时,很容易不小心将密码泄露给他人。
发送特殊按键
Selenium 拥有一个模块,selenium.webdriver.common.keys,用于表示键盘按键,并将其存储在属性中。由于该模块名称较长,因此在程序顶部运行 from selenium.webdriver.common.keys import Keys 会更加方便;如果您这样做,就可以在任何需要输入 selenium.webdriver.common.keys 的地方简单地写 Keys。
您可以向 send_keys() 传递以下任何常量:
Keys.ENTER Keys.PAGE_UP Keys.DOWN
Keys.RETURN Keys.ESCAPE Keys.LEFT
Keys.HOME Keys.BACK_SPACE Keys.RIGHT
Keys.END Keys.DELETE Keys.TAB
Keys.PAGE_DOWN Keys.UP Keys.F1 to Keys.F12
您还可以向该方法传递一个字符串,例如 'hello' 或 '?'。
例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别将浏览器滚动到页面顶部和底部。在交互式外壳中输入以下内容,并注意 send_keys() 调用如何滚动页面:
>>> from selenium import webdriver
>>> from selenium.webdriver.common.by import By
>>> from selenium.webdriver.common.keys import Keys
>>> browser = webdriver.Firefox()
>>> browser.get('https://nostarch.com')
>>> html_elem = browser.find_element(By.TAG_NAME, 'html')
>>> html_elem.send_keys(Keys.END) # Scrolls to bottom
>>> html_elem.send_keys(Keys.HOME) # Scrolls to top
<html> 标签是 HTML 文件中的基本标签:HTML 文件的全部内容都包含在 <html> 和 </html> 标签内。调用 browser.find_element(By.TAG_NAME, 'html') 是通过主要的 <html> 标签向一般网页发送按键的好方法。例如,当您滚动到页面底部后,新内容被加载,这将非常有用。
Selenium 可以执行比这里描述的功能多得多的操作。它可以修改您的浏览器 cookies,截取网页截图,并运行自定义 JavaScript。要了解更多关于这些功能的信息,您可以访问 Selenium 文档,网址为 selenium-python.readthedocs.io。您还可以通过搜索网站 pyvideo.org 来找到有关 Selenium 的 Python 会议演讲。
使用 Playwright 控制浏览器
Playwright 是一个类似于 Selenium 的浏览器控制库,但它更新。虽然它可能目前没有 Selenium 那样的广泛受众,但它确实提供了一些值得学习的新特性。其中最重要的新特性是能够在 无头模式 下运行,这意味着您可以在屏幕上不打开浏览器窗口的情况下模拟浏览器。这使得它在后台运行自动化测试或网络爬取作业时非常有用。Playwright 的完整文档可在 playwright.dev/python/docs/intro 找到。
此外,与 Selenium 相比,使用 Playwright 安装单个浏览器的网络驱动程序更容易:只需在 Windows 上运行 python -m playwright install,在 macOS 和 Linux 上从终端窗口运行 python3 –m playwright install,即可安装 Firefox、Chrome 和 Safari 的网络驱动程序。由于 Playwright 在其他方面与 Selenium 类似,因此我不会在本节中介绍通用的网络爬取和 CSS 选择器信息。
启动 Playwright 控制的浏览器
一旦安装了 Playwright,您可以使用以下程序进行测试:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch()
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
print(page.title())
browser.close()
当程序运行时,它会暂停以加载 Firefox 浏览器和 [autbor.com/example3.html](https://autbor.com/example3.html) 网站页面,然后打印其标题,“示例网站。”您还可以使用 playwright.chromium.launch() 或 playwright.webkit.launch() 来分别使用 Chrome 和 Safari 浏览器。
当执行进入和退出 with 语句的块时,Playwright 会自动调用 start() 和 stop() 方法。Playwright 有一个同步模式,其中其函数在操作完成之前不会返回。这样,您就不会不小心告诉浏览器在页面加载完成之前查找元素。Playwright 的异步功能超出了本书的范围。
您可能已经注意到,根本没有任何浏览器窗口出现,因为默认情况下,Playwright 以无头模式运行。这,加上 Playwright 将其代码放在 with 语句中,可能会使调试变得复杂。要逐步运行 Playwright,请在交互式外壳中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> browser.close()
>>> playwright.stop()
playwright.firefox.launch() 函数中的 headless=False 和 slow_mo=50 关键字参数使得浏览器窗口显示在您的屏幕上,并在其操作中添加 50 毫秒的延迟,这样您就能更容易地看到正在发生的事情。您无需担心添加暂停以给网页加载时间:Playwright 在完成上一个操作之前不会继续进行新操作,在这方面比 Selenium 更出色。
new_page() 浏览器方法返回的 Page 对象代表一个新浏览器窗口中的新标签页。使用 Playwright 时,您可以同时打开多个浏览器窗口。
点击浏览器按钮
Playwright 可以通过在 browser.new_page() 返回的 Page 对象上调用以下方法来模拟点击浏览器按钮:
page.go_back() 点击后退按钮
page.go_forward() 点击前进按钮
page.reload() 点击刷新/重新加载按钮
page.close() 点击关闭窗口按钮
在页面上查找元素
Playwright 有 Page 对象方法,俗称为 定位器,这些方法返回 Locator 对象,代表网页上的可能 HTML 元素。我说 可能 是因为,虽然 Selenium 如果找不到你请求的元素会立即抛出错误,但 Playwright 理解页面可能会稍后动态创建该元素。这很有用,但也有一些不太幸运的副作用:如果你指定的元素不存在,Playwright 会暂停 30 秒,等待元素出现。
但如果你只是犯了一个拼写错误,那么这 30 秒的暂停就会变得很繁琐。为了立即检查元素是否存在于页面上并且可见,请在定位器对象上调用 is_visible() 方法。你也可以调用 page.query_selector('selector'),其中 selector 是元素的 CSS 或 XPath 选择器字符串。page.query_selector() 方法会立即返回,如果它返回 None,则表示当前页面上不存在该元素。一个 Locator 对象可能匹配网页上的一个或多个 HTML 元素。表 13-5 包含了 Playwright 的定位器。
表 13-5:Playwright 用于查找元素的定位器
| 定位器 | 返回的定位器对象 |
|---|---|
page.get_by_role(role, name=label) |
通过其角色和可选的标签定位元素 |
page.get_by_text(text) |
其内部文本包含文本的元素 |
page.get_by_label(label) |
与标签文本匹配的元素 |
page.get_by_placeholder(text) |
与提供的文本匹配的 <input> 和 <textarea> 元素 |
page.get_by_alt_text(text) |
与提供的 alt 属性值匹配的 <img> 元素 |
page.locator(selector) |
与匹配的 CSS 或 XPath 选择器匹配的元素 |
get_by_role() 方法利用了 可访问的富互联网应用程序 (ARIA) 角色,这是一套标准,它使软件能够识别网页内容,以便为有视觉或其他残疾的用户进行适配。例如,“标题”角色适用于 <h1> 到 <h6> 标签,其中 <h1> 和 </h1> 之间的文本是你可以用 get_by_role() 方法的 name 关键字参数识别的文本。(ARIA 角色的内容远不止这些,但这个主题超出了本书的范围。)
你可以使用起始和结束标签之间的文本来定位元素。调用 page.get_by_text('is a link') 会定位到 <a href="https://inventwithpython.com”>This text is a link</a> 中的 <a> 元素。通常,部分、不区分大小写的文本匹配就足够定位元素。
page.get_by_label() 方法通过 <label> 和 </label> 标签之间的文本来定位元素。例如,page.get_by_label('Agree') 会定位到 <label>Agree to disagree: <input type="checkbox" /></label> 中的 <input> 复选框元素。
<input>和<textarea>标签可以有一个placeholder属性来显示占位文本,直到用户输入实际文本。例如,page.get_by_placeholder('admin')将定位到<input>元素<input id="login_user" placeholder="admin" />。
网页上的图片可以在它们的alt属性中包含 alt 文本来描述图片内容,以供视力受损用户使用。一些浏览器在将鼠标光标悬停在图片上时显示 alt 文本作为工具提示。page.get_by_alt_text('Zophie')调用将返回<img>元素<img src="wow_such_zophie_thumb.webp" alt="Close-up of my cat Zophie." />。
如果你只需要通过 CSS 选择器获取一个Locator对象,请调用locator()定位器并传递选择器字符串。这与 Selenium 的find_elements()方法与By.CSS_SELECTOR常量类似。
表 13-6:Locator方法
| 方法 | 描述 |
|---|---|
| --- | --- |
get_attribute(name) |
返回元素名称属性的值,例如<a href="https://nostarch.com">元素中href属性的'https://nostarch.com'。 |
count() |
返回整数,表示此Locator对象中匹配元素的数量。 |
nth(索引) |
返回由索引指定的匹配元素的Locator对象。例如,nth(3)返回第四个匹配元素,因为索引0是第一个匹配元素。 |
first |
第一个匹配元素的Locator对象。这相当于nth(0)。 |
last |
最后匹配元素的Locator对象。如果有,比如说有五个匹配元素,这相当于nth(4)。 |
all() |
返回一个包含每个单个匹配元素的Locator对象的列表。 |
inner_text() |
返回元素内的文本,例如<b>hello</b>中的'hello'。 |
inner_html() |
返回元素内的 HTML 源代码,例如<b>hello</b>中的'<b>hello</b>'。 |
click() |
模拟对元素的点击,这对于链接、复选框和按钮元素很有用。 |
is_visible() |
如果元素是可见的,则返回True;否则,返回False。 |
is_enabled() |
对于输入元素,如果元素是启用的,则返回True;否则,返回False。 |
is_checked() |
对于复选框或单选按钮元素,如果元素被选中,则返回True;否则,返回False。 |
bounding_box() |
返回一个字典,包含键'x'和'y',表示元素左上角在页面中的位置,以及键'width'和'height',表示元素的大小。 |
由于Locator对象可以表示多个元素,你可以通过nth()方法获取单个元素的Locator对象,传递零基索引。例如,打开一个新的文件编辑标签并输入以下程序:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch(headless=False, slow_mo=50)
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
elems = page.locator('p')
print(elems.nth(0).inner_text())
print(elems.nth(0).inner_html())
类似于 Selenium 示例,此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
page.locator('p') 代码返回一个匹配网页中所有 <p> 元素的 Locator 对象,而 nth(0) 方法调用则返回仅匹配第一个 <p> 元素的 Locator 对象。Locator 对象还有一个 count() 方法,用于返回定位器中匹配元素的数量(类似于 Python 列表的 len() 函数)。此外,还有 first 和 last 属性,它们包含匹配第一个或最后一个元素的定位器。如果您想要每个单独匹配元素的 Locator 对象列表,请调用 all() 方法。
一旦您有了元素的 Locator 对象,您就可以按照下一节所述在它们上执行鼠标点击和按键操作。
在页面上点击元素
Page 对象有 click()、check()、uncheck() 和 set_checked() 方法,用于模拟对链接、按钮和复选框元素的点击。您可以调用这些方法并传递元素的 CSS 或 XPath 选择器字符串,或者您可以使用表 13-6 中的 Playwright 的 Locator 函数。在交互式外壳中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.click('input[type=checkbox]') # Checks the checkbox
>>> page.click('input[type=checkbox]') # Unchecks the checkbox
>>> page.click('a') # Clicks the link
>>> page.go_back()
>>> checkbox_elem = page.get_by_role('checkbox') # Calls a Locator method
>>> checkbox_elem.check() # Checks the checkbox
>>> checkbox_elem.uncheck() # Unchecks the checkbox
>>> checkbox_elem.set_checked(True) # Checks the checkbox
>>> checkbox_elem.set_checked(False) # Unchecks the checkbox
>>> page.get_by_text('is a link').click() # Uses a Locator method
>>> browser.close()
>>> playwright.stop()
check() 和 uncheck() 方法比 click() 对复选框更可靠。click() 方法将复选框切换到相反的状态,而 check() 和 uncheck() 不论之前的状态如何,都会将其保留为选中或未选中。同样,set_checked() 方法允许您传递 True 来选中复选框或 False 来取消选中。
填写和提交表单
Locator 对象有一个 fill() 方法,它接受一个字符串并将文本填充到 <input> 或 <textarea> 元素中。这对于填写在线表单很有用,例如我们 example3.html 网页中的登录表单:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('#login_user').fill('`your_real_username_here`')
>>> page.locator('#login_pass').fill('`your_real_password_here`')
>>> page.locator('input[type=submit]').click()
>>> browser.close()
>>> playwright.stop()
此外,还有一个 clear() 方法,用于清除元素中当前的所有文本。与 Selenium 不同,Playwright 中没有 submit() 方法,您需要在其 Locator 对象上调用 click() 来匹配提交按钮的元素。
发送特殊按键
您还可以使用 Locator 对象的 press() 方法在网页元素上模拟键盘按键。例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别将浏览器滚动到页面顶部和底部。在交互式外壳中输入以下内容,并注意 press() 调用如何滚动页面:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('html').press('End') # Scrolls to bottom
>>> page.locator('html').press('Home') # Scrolls to top
>>> browser.close()
>>> playwright.stop()
您传递给 press() 的字符串可以包括单个字符字符串(例如 'a' 或 '?');修改键 'Shift'、'Control'、'Alt' 或 'Meta'(例如 'Control+A',即 CTRL-A);以及以下任何内容:
'Backquote' 'Escape' 'ArrowDown'
'Minus' 'End' 'ArrowRight'
'Equal' 'Enter' 'ArrowUp'
'Backslash' 'Home' 'F1' to 'F12'
'Backspace' 'Insert' 'Digit0' to 'Digit9'
'Tab' 'PageUp' 'KeyA' to 'KeyZ'
'Delete' 'PageDown'
Playwright 除了这里描述的功能之外,还能做更多的事情。要了解更多关于这些功能的信息,您可以访问 Playwright 文档,网址为 playwright.dev。您还可以通过搜索 pyvideo.org 来找到关于 Playwright 的 Python 会议演讲。
启动 Playwright 控制的浏览器
一旦安装了 Playwright,你可以使用以下程序进行测试:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch()
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
print(page.title())
browser.close()
当运行此程序时,它会暂停以加载 Firefox 浏览器和 autbor.com/example3.html 网站,然后打印其标题,“示例网站。”你也可以使用 playwright.chromium.launch() 或 playwright.webkit.launch() 来分别使用 Chrome 和 Safari 浏览器。
当执行进入和退出 with 语句块时,Playwright 会自动调用 start() 和 stop() 方法。Playwright 有一个同步模式,其中其函数在操作完成之前不会返回。这样,你就不会不小心告诉浏览器在页面加载完成之前查找元素。Playwright 的异步特性超出了本书的范围。
你可能已经注意到根本没有任何浏览器窗口出现,因为默认情况下,Playwright 以无头模式运行。这一点,加上 Playwright 将其代码放在 with 语句块中,可能会使调试变得困难。要逐个步骤运行 Playwright,请在交互式外壳中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> browser.close()
>>> playwright.stop()
playwright.firefox.launch() 函数的 headless=False 和 slow_mo=50 关键字参数使得浏览器窗口出现在你的屏幕上,并在其操作中添加 50 毫秒的延迟,这样你更容易看到正在发生的事情。你不必担心添加暂停以给网页加载时间:Playwright 在上一个操作完成之前不会继续进行新操作,这一点比 Selenium 做得更好。
new_page() 浏览器方法返回的 Page 对象代表一个新浏览器窗口中的新标签页。在使用 Playwright 时,你可以同时打开多个浏览器窗口。
点击浏览器按钮
Playwright 可以通过在 browser.new_page() 返回的 Page 对象上调用以下方法来模拟点击浏览器按钮:
page.go_back() 点击后退按钮
page.go_forward() 点击前进按钮
page.reload() 点击刷新/重新加载按钮
page.close() 点击关闭窗口按钮
在页面上查找元素
Playwright 拥有被称为 定位器 的 Page 对象方法,这些方法返回 Locator 对象,代表网页上可能的 HTML 元素。我说 可能 是因为,虽然 Selenium 如果找不到你请求的元素会立即抛出错误,但 Playwright 理解页面可能会稍后动态创建该元素。这很有用,但也有一些不太幸运的副作用:如果你指定的元素不存在,Playwright 会暂停 30 秒,等待元素出现。
但如果你只是犯了一个拼写错误,那么这 30 秒的暂停就会显得很繁琐。为了立即检查某个元素是否存在于页面上并且可见,请在由定位器返回的Locator对象上调用is_visible()方法。你也可以调用page.query_selector('selector'),其中selector是元素的 CSS 或 XPath 选择器字符串。page.query_selector()方法会立即返回,如果它返回None,则表示当前页面上不存在该元素。Locator对象可能匹配网页上的一个或多个 HTML 元素。表 13-5 包含了 Playwright 的定位器。
表 13-5:Playwright 用于查找元素的定位器
| 定位器 | 定位器对象返回 |
|---|---|
page.get_by_role(role, name=label) |
通过其角色和可选的标签定位元素 |
page.get_by_text(text) |
包含文本作为其内部文本一部分的元素 |
page.get_by_label(label) |
与匹配的<label>文本作为标签的元素 |
page.get_by_placeholder(text) |
与提供的placeholder属性值匹配的<input>和<textarea>元素 |
page.get_by_alt_text(text) |
与提供的alt属性值匹配的<img>元素 |
page.locator(selector) |
与匹配的 CSS 或 XPath 选择器匹配的元素 |
get_by_role()方法利用了可访问的富互联网应用(ARIA)角色,这是一套标准,它使软件能够识别网页内容,以便为有视力或其他残疾的用户进行适配。例如,“标题”角色适用于<h1>到<h6>标签,其中<h1>和</h1>之间的文本可以通过get_by_role()方法的name关键字参数进行识别。(ARIA 角色远不止这些,但这个主题超出了本书的范围。)
你可以使用起始和结束标签之间的文本来定位元素。调用page.get_by_text('is a link')将定位到<a href="https://inventwithpython.com”>This text is a link</a>中的<a>元素。通常,部分、不区分大小写的文本匹配就足够用来定位元素。
page.get_by_label()方法通过<label>和</label>标签之间的文本来定位元素。例如,page.get_by_label('Agree')将定位到<label>Agree to disagree: <input type="checkbox" /></label>中的<input>复选框元素。
<input>和<textarea>标签可以有一个placeholder属性来显示占位文本,直到用户输入实际文本。例如,page.get_by_placeholder('admin')将定位到<input id="login_user" placeholder="admin" />中的<input>元素。
网页上的图片可以在它们的alt属性中包含替代文本,以描述图片内容,供视力受损用户使用。一些浏览器在将鼠标光标悬停在图片上时显示替代文本作为工具提示。page.get_by_alt_text('Zophie')调用将返回<img>元素<img src="wow_such_zophie_thumb.webp" alt="Close-up of my cat Zophie." />。
如果你只需要通过 CSS 选择器获取Locator对象,请调用locator()定位器并传递选择器字符串。这与 Selenium 的find_elements()方法与By.CSS_SELECTOR常量类似。
表 13-6:Locator方法
| 方法 | 描述 |
|---|---|
get_attribute(name) |
返回元素名称属性的值,例如<a href="https://nostarch.com">元素中的href属性的'https://nostarch.com'。 |
count() |
返回此Locator对象中匹配元素的数量。 |
nth(索引) |
返回由索引指定的匹配元素的Locator对象。例如,nth(3)返回第四个匹配元素,因为索引0是第一个匹配元素。 |
first |
第一个匹配元素的Locator对象。这等同于nth(0)。 |
last |
最后一个匹配元素的Locator对象。如果有五个匹配元素,这等同于nth(4)。 |
all() |
返回一个包含每个单独匹配元素的Locator对象的列表。 |
inner_text() |
返回元素内的文本,例如<b>hello</b>中的'hello'。 |
inner_html() |
返回元素内的 HTML 源代码,例如<b>hello</b>中的'<b>hello</b>'。 |
click() |
模拟对元素的点击,这对于链接、复选框和按钮元素非常有用。 |
is_visible() |
如果元素是可见的,则返回True;否则,返回False。 |
is_enabled() |
对于输入元素,如果元素是启用的,则返回True;否则,返回False。 |
is_checked() |
对于复选框或单选按钮元素,如果元素被选中,则返回True;否则,返回False。 |
bounding_box() |
返回一个字典,包含键'x'和'y',表示元素左上角在页面中的位置,以及键'width'和'height'表示元素的大小。 |
由于Locator对象可以表示多个元素,你可以通过nth()方法获取单个元素的Locator对象,传递零基索引。例如,打开一个新的文件编辑标签并输入以下程序:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.firefox.launch(headless=False, slow_mo=50)
page = browser.new_page()
page.goto('https://autbor.com/example3.html')
elems = page.locator('p')
print(elems.nth(0).inner_text())
print(elems.nth(0).inner_html())
与 Selenium 示例类似,此程序输出以下内容:
This <p> tag puts content into a single paragraph.
This <p> tag puts <b>content</b> into a <i>single</i> paragraph.
page.locator('p') 代码返回一个 Locator 对象,该对象匹配网页中所有的 <p> 元素,而 nth(0) 方法调用返回一个 Locator 对象,仅针对第一个 <p> 元素。Locator 对象还具有一个 count() 方法,用于返回定位器中匹配元素的数量(类似于 Python 列表的 len() 函数)。还有 first 和 last 属性,它们包含匹配第一个或最后一个元素的定位器。如果你想要每个单独匹配元素的 Locator 对象列表,请调用 all() 方法。
一旦你有了元素的 Locator 对象,你可以像下一几节中描述的那样在它们上执行鼠标点击和按键操作。
在页面上点击元素
Page 对象具有 click()、check()、uncheck() 和 set_checked() 方法,用于模拟对链接、按钮和复选框元素的点击。你可以调用这些方法并传递元素的 CSS 或 XPath 选择器字符串,或者你可以使用 Playwright 的 Locator 函数,如表 13-6 所示。在交互式外壳中输入以下内容:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.click('input[type=checkbox]') # Checks the checkbox
>>> page.click('input[type=checkbox]') # Unchecks the checkbox
>>> page.click('a') # Clicks the link
>>> page.go_back()
>>> checkbox_elem = page.get_by_role('checkbox') # Calls a Locator method
>>> checkbox_elem.check() # Checks the checkbox
>>> checkbox_elem.uncheck() # Unchecks the checkbox
>>> checkbox_elem.set_checked(True) # Checks the checkbox
>>> checkbox_elem.set_checked(False) # Unchecks the checkbox
>>> page.get_by_text('is a link').click() # Uses a Locator method
>>> browser.close()
>>> playwright.stop()
check() 和 uncheck() 方法比 click() 方法对复选框更可靠。click() 方法将复选框切换到相反的状态,而 check() 和 uncheck() 不论它们之前的状态如何,都会使它们保持选中或未选中。同样,set_checked() 方法允许你传递 True 来选中复选框或 False 来取消选中。
填写和提交表单
Locator 对象有一个 fill() 方法,它接受一个字符串并将文本填充到 <input> 或 <textarea> 元素中。这对于填写在线表单很有用,例如我们 example3.html 网页中的登录表单:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('#login_user').fill('`your_real_username_here`')
>>> page.locator('#login_pass').fill('`your_real_password_here`')
>>> page.locator('input[type=submit]').click()
>>> browser.close()
>>> playwright.stop()
此外,还有一个 clear() 方法,它将删除元素中当前的所有文本。与 Selenium 不同,Playwright 中没有 submit() 方法,你必须调用匹配提交按钮元素的 Locator 对象的 click()。
发送特殊按键
你还可以使用 Locator 对象的 press() 方法在网页元素上模拟键盘按键。例如,如果光标当前不在文本字段中,按下 HOME 和 END 键将分别将浏览器滚动到页面的顶部和底部。在交互式外壳中输入以下内容,并注意 press() 调用如何滚动页面:
>>> from playwright.sync_api import sync_playwright
>>> playwright = sync_playwright().start()
>>> browser = playwright.firefox.launch(headless=False, slow_mo=50)
>>> page = browser.new_page()
>>> page.goto('https://autbor.com/example3.html')
<Response url='https://autbor.com/example3.html' request=<Request
url='https://autbor.com/example3.html' method='GET'>>
>>> page.locator('html').press('End') # Scrolls to bottom
>>> page.locator('html').press('Home') # Scrolls to top
>>> browser.close()
>>> playwright.stop()
你传递给 press() 的字符串可以包括单个字符字符串(例如 'a' 或 '?');修改键 'Shift'、'Control'、'Alt' 或 'Meta'(例如 'Control+A',即 CTRL-A);以及以下内容中的任何一项:
'Backquote' 'Escape' 'ArrowDown'
'Minus' 'End' 'ArrowRight'
'Equal' 'Enter' 'ArrowUp'
'Backslash' 'Home' 'F1' to 'F12'
'Backspace' 'Insert' 'Digit0' to 'Digit9'
'Tab' 'PageUp' 'KeyA' to 'KeyZ'
'Delete' 'PageDown'
Playwright 可以执行比这里描述的更多功能。要了解更多关于这些功能的信息,你可以访问 Playwright 文档,网址为 playwright.dev。你还可以通过搜索 pyvideo.org 来找到关于 Playwright 的 Python 会议演讲。
概述
大多数无聊的任务不仅限于你电脑上的文件。能够以编程方式下载网页将扩展你的程序到互联网。requests模块使下载变得简单,并且通过一些基本的 HTML 概念和选择器知识,你可以利用BeautifulSoup模块来解析你下载的页面。
但要完全自动化任何基于 Web 的任务,你需要通过 Selenium 和 Playwright 包直接控制你的 Web 浏览器。这些包将允许你自动登录网站和填写表单。因为 Web 浏览器是发送和接收互联网上信息最常见的方式,所以这在你程序员的工具包中是一个非常有用的能力。
实践问题
-
简要描述
webbrowser、requests和bs4模块之间的区别。 -
requests.get()返回什么类型的对象?如何将下载的内容作为字符串值访问? -
哪个
requests方法检查下载是否成功? -
如何获取
requests响应的 HTTP 状态码? -
如何将
requests响应保存到文件中? -
大多数在线 API 以哪两种格式返回它们的响应?
-
打开浏览器开发者工具的快捷键是什么?
-
如何在开发者工具中查看网页上特定元素的 HTML?
-
什么 CSS 选择器字符串可以找到具有
id属性为main的元素? -
什么 CSS 选择器字符串可以找到具有
id属性为highlight的元素? -
假设你有一个 Beautiful Soup 的
Tag对象存储在变量spam中,对应元素<div>Hello, world!</div>。你如何从Tag对象中获取字符串'Hello, world!'? -
你会如何将 Beautiful Soup 的
Tag对象的所有属性存储在名为link_elem的变量中? -
运行
import selenium不工作。如何正确导入 Selenium? -
Selenium 中的
find_element()和find_elements()方法有什么区别? -
Selenium 的
WebElement对象有哪些方法可以模拟鼠标点击和键盘按键? -
在 Playwright 中,哪个定位器方法调用模拟按 CTRL-A 选择页面上的所有文本?
-
如何使用 Selenium 模拟点击浏览器的“前进”、“后退”和“刷新”按钮?
-
如何使用 Playwright 模拟点击浏览器的“前进”、“后退”和“刷新”按钮?
实践程序
为了练习,编写程序来完成以下任务。
图片网站下载器
编写一个程序,访问像 Flickr 或 Imgur 这样的照片分享网站,搜索照片类别,然后下载所有结果图片。你可以编写一个可以与任何具有搜索功能的照片网站一起工作的程序。
2048
游戏 2048 是一个简单的游戏,你可以通过使用箭头键向上、向下、向左或向右滑动瓷砖来组合它们。实际上,你可以通过随机方向滑动瓷砖获得相当高的分数。编写一个程序,打开play2048.co上的游戏,并持续发送向上、向右、向下和向左的按键操作来自动玩游戏。
链接验证
编写一个程序,给定一个网页的 URL,它会找到页面上的所有<a>链接,并测试链接的 URL 是否会导致“404 Not Found”状态码。程序应打印出任何断链。
- 1 答案是否定的。
图片网站下载器
编写一个程序,访问像 Flickr 或 Imgur 这样的照片分享网站,搜索照片类别,然后下载所有结果图片。你可以编写一个与任何具有搜索功能的照片网站一起工作的程序。
2048
游戏 2048 是一个简单的游戏,你可以通过使用箭头键向上、向下、向左或向右滑动瓷砖来组合它们。实际上,你可以通过随机方向滑动瓷砖获得相当高的分数。编写一个程序,打开play2048.co上的游戏,并持续发送向上、向右、向下和向左的按键操作来自动玩游戏。
链接验证
编写一个程序,给定一个网页的 URL,它会找到页面上的所有<a>链接,并测试链接的 URL 是否会导致“404 Not Found”状态码。程序应打印出任何断链。
- 1 答案是否定的。


浙公网安备 33010602011771号