geekdoc-python-zh/docs/realpython/python-web-scraping-practic...

42 KiB
Raw Permalink Blame History

Python 中 Web 抓取的实用介绍

原文:https://realpython.com/python-web-scraping-practical-introduction/

Web 抓取是从 Web 上收集和解析原始数据的过程Python 社区已经开发出一些非常强大的 Web 抓取工具。

互联网可能是这个星球上最大的信息源。许多学科,如数据科学、商业智能和调查报告,可以从收集和分析网站数据中受益匪浅。

在本教程中,您将学习如何:

  • 使用字符串方法正则表达式解析网站数据
  • 使用 HTML 解析器解析网站数据
  • 表单和其他网站组件交互

**注:**本教程改编自 Python 基础知识:Python 实用入门 3 中“与 Web 交互”一章。

这本书使用 Python 内置的 IDLE 编辑器来创建和编辑 Python 文件,并与 Python shell 进行交互,因此在整个教程中,你会偶尔看到对 IDLE 的引用。然而,从您选择的编辑器环境运行示例代码应该没有问题。

源代码: 点击这里下载免费的源代码,你将使用它来收集和解析来自网络的数据。

从网站上抓取并解析文本

使用自动化过程从网站收集数据被称为网络搜集。一些网站明确禁止用户使用自动化工具抓取他们的数据,就像你将在本教程中创建的工具一样。网站这样做有两个可能的原因:

  1. 该网站有充分的理由保护其数据。例如,谷歌地图不会让你太快地请求太多结果。
  2. 向网站的服务器发出多次重复请求可能会耗尽带宽,降低其他用户访问网站的速度,并可能使服务器过载,从而导致网站完全停止响应。

在使用 Python 技能进行 web 抓取之前,您应该始终检查目标网站的可接受使用政策,以确定使用自动化工具访问网站是否违反了其使用条款。从法律上讲,违背网站意愿的网络抓取是一个灰色地带。

**重要提示:**请注意,在禁止抓取网页的网站上使用以下技术可能是非法的

对于本教程,您将使用一个托管在 Real Python 服务器上的页面。您将访问的页面已经设置为与本教程一起使用。

既然你已经阅读了免责声明,你可以开始有趣的事情了。在下一节中,您将开始从单个 web 页面获取所有 HTML 代码。

Remove ads

打造你的第一台网络刮刀

你可以在 Python 的标准库中找到的一个有用的 web 抓取包是urllib,它包含了处理 URL 的工具。特别是, urllib.request 模块包含一个名为urlopen()的函数,您可以用它来打开程序中的 URL。

在 IDLE 的交互窗口中,键入以下内容以导入urlopen():

>>> from urllib.request import urlopen

您将打开的网页位于以下 URL:

>>> url = "http://olympus.realpython.org/profiles/aphrodite"

要打开网页,请将url传递给urlopen():

>>> page = urlopen(url)

urlopen()返回一个HTTPResponse对象:

>>> page
<http.client.HTTPResponse object at 0x105fef820>

要从页面中提取 HTML首先使用HTTPResponse对象的.read()方法,该方法返回一个字节序列。然后使用.decode()将字节解码成使用 UTF-8 的字符串:

>>> html_bytes = page.read()
>>> html = html_bytes.decode("utf-8")

现在你可以打印HTML 来查看网页的内容:

>>> print(html)
<html>
<head>
<title>Profile: Aphrodite</title>
</head>
<body bgcolor="yellow">
<center>
<br><br>
<img src="/static/aphrodite.gif" />
<h2>Name: Aphrodite</h2>
<br><br>
Favorite animal: Dove
<br><br>
Favorite color: Red
<br><br>
Hometown: Mount Olympus
</center>
</body>
</html>

你看到的输出是网站的 HTML 代码,当你访问http://olympus.realpython.org/profiles/aphrodite时,你的浏览器会显示出来:

Screenshot of the Aphrodite Website

使用urllib,您可以像在浏览器中一样访问网站。但是,您没有可视化地呈现内容,而是将源代码作为文本获取。既然已经有了文本形式的 HTML就可以用几种不同的方法从中提取信息。

用字符串方法从 HTML 中提取文本

从网页的 HTML 中提取信息的一种方法是使用字符串方法。例如,您可以使用.find()在 HTML 文本中搜索<title>标签,并提取网页的标题。

首先,您将提取您在前一个示例中请求的 web 页面的标题。如果你知道标题的第一个字符的索引和结束标签</title>的第一个字符的索引,那么你可以使用一个字符串切片来提取标题。

因为.find()返回一个子串第一次出现的索引,所以可以通过将字符串"<title>"传递给.find()来获得开始<title>标签的索引:

>>> title_index = html.find("<title>")
>>> title_index
14

但是,您并不需要<title>标签的索引。您需要标题本身的索引。要获得标题中第一个字母的索引,可以将字符串"<title>"的长度加到title_index中:

>>> start_index = title_index + len("<title>")
>>> start_index
21

现在通过将字符串"</title>"传递给.find()来获取结束</title>标签的索引:

>>> end_index = html.find("</title>")
>>> end_index
39

最后,您可以通过切分html字符串来提取标题:

>>> title = html[start_index:end_index]
>>> title
'Profile: Aphrodite'

现实世界中的 HTML 可能比阿芙罗狄蒂个人资料页面上的 HTML 复杂得多,也不太容易预测。这里是另一个个人资料页面,你可以抓取一些更混乱的 HTML:

>>> url = "http://olympus.realpython.org/profiles/poseidon"

尝试使用与上例相同的方法从这个新 URL 中提取标题:

>>> url = "http://olympus.realpython.org/profiles/poseidon"
>>> page = urlopen(url)
>>> html = page.read().decode("utf-8")
>>> start_index = html.find("<title>") + len("<title>")
>>> end_index = html.find("</title>")
>>> title = html[start_index:end_index]
>>> title
'\n<head>\n<title >Profile: Poseidon'

哎呦!标题中夹杂了一点 HTML。为什么会这样

/profiles/poseidon页面的 HTML 看起来类似于/profiles/aphrodite页面,但是有一点小小的不同。开始的<title>标签在结束的尖括号(>)前有一个额外的空格,呈现为<title >

html.find("<title>")返回-1,因为精确的子串"<title>"不存在。当-1加到len("<title>")上,即7时,start_index变量被赋值6

字符串html的索引6处的字符是一个换行符(\n),正好在<head>标签的左尖括号(<)之前。这意味着html[start_index:end_index]返回所有从新行开始并在</title>标签之前结束的 HTML。

这类问题会以无数种不可预测的方式出现。您需要一种更可靠的方法来从 HTML 中提取文本。

Remove ads

了解正则表达式

正则表达式——或者简称为正则表达式——是可以用来在字符串中搜索文本的模式。Python 通过标准库的 re 模块支持正则表达式。

**注意:**正则表达式不是 Python 特有的。它们是一个通用的编程概念,许多编程语言都支持它们。

要使用正则表达式,您需要做的第一件事是导入re模块:

>>> import re

正则表达式使用称为元字符的特殊字符来表示不同的模式。例如,星号字符(*)代表零个或多个出现在星号前面的实例。

在下面的例子中,您使用.findall()来查找字符串中匹配给定正则表达式的任何文本:

>>> re.findall("ab*c", "ac")
['ac']

re.findall()的第一个参数是要匹配的正则表达式,第二个参数是要测试的字符串。在上面的例子中,您在字符串"ac"中搜索模式"ab*c"

正则表达式"ab*c"匹配字符串中以"a"开头、以"c"结尾并且在两者之间有零个或多个"b"实例的任何部分。re.findall()返回所有匹配的列表。字符串"ac"匹配这个模式,所以它被返回到列表中。

以下是应用于不同字符串的相同模式:

>>> re.findall("ab*c", "abcd")
['abc']

>>> re.findall("ab*c", "acc")
['ac']

>>> re.findall("ab*c", "abcac")
['abc', 'ac']

>>> re.findall("ab*c", "abdc")
[]

注意,如果没有找到匹配,那么.findall()返回一个空列表。

模式匹配区分大小写。如果您想匹配这个模式而不考虑大小写,那么您可以传递第三个参数,值为re.IGNORECASE:

>>> re.findall("ab*c", "ABC")
[]

>>> re.findall("ab*c", "ABC", re.IGNORECASE)
['ABC']

您可以使用句点(.)来代表正则表达式中的任何单个字符。例如,您可以找到包含由单个字符分隔的字母"a""c"的所有字符串,如下所示:

>>> re.findall("a.c", "abc")
['abc']

>>> re.findall("a.c", "abbc")
[]

>>> re.findall("a.c", "ac")
[]

>>> re.findall("a.c", "acc")
['acc']

正则表达式中的模式.*代表重复任意次的任意字符。例如,您可以使用"a.*c"来查找以"a"开始并以"c"结束的每个子串,而不管中间是哪个或哪些字母:

>>> re.findall("a.*c", "abc")
['abc']

>>> re.findall("a.*c", "abbc")
['abbc']

>>> re.findall("a.*c", "ac")
['ac']

>>> re.findall("a.*c", "acc")
['acc']

通常,您使用re.search()来搜索字符串中的特定模式。这个函数比re.findall()稍微复杂一些,因为它返回一个名为MatchObject的对象,该对象存储不同的数据组。这是因为在其他匹配中可能有匹配,而re.search()返回每一个可能的结果。

MatchObject的细节在这里无关紧要。现在,只需知道在MatchObject上调用.group()将返回第一个也是最具包容性的结果,这在大多数情况下正是您想要的:

>>> match_results = re.search("ab*c", "ABC", re.IGNORECASE)
>>> match_results.group()
'ABC'

re模块中还有一个对解析文本有用的函数。 re.sub() ,是 substitute 的简称,允许你用新文本替换匹配正则表达式的字符串中的文本。它的表现有点像 .replace() 弦法。

传递给re.sub()的参数是正则表达式,后面是替换文本,后面是字符串。这里有一个例子:

>>> string = "Everything is <replaced> if it's in <tags>."
>>> string = re.sub("<.*>", "ELEPHANTS", string)
>>> string
'Everything is ELEPHANTS.'

也许这并不是你所期望的。

re.sub()使用正则表达式"<.*>"查找并替换第一个<和最后一个>之间的所有内容,从<replaced>开始到<tags>结束。这是因为 Python 的正则表达式是贪婪的,这意味着当使用像*这样的字符时,它们试图找到最长的可能匹配。

或者,您可以使用非贪婪匹配模式*?,它的工作方式与*相同,只是它匹配可能的最短文本字符串:

>>> string = "Everything is <replaced> if it's in <tags>."
>>> string = re.sub("<.*?>", "ELEPHANTS", string)
>>> string
"Everything is ELEPHANTS if it's in ELEPHANTS."

这次,re.sub()找到了两个匹配项,<replaced><tags>,并用字符串"ELEPHANTS"替换这两个匹配项。

Remove ads

用正则表达式从 HTML 中提取文本

有了所有这些知识,现在试着从另一个个人资料页面解析出标题,其中包括这段相当粗心的 HTML 代码:

<TITLE >Profile: Dionysus</title  / >

.find()方法在处理这里的不一致时会有困难,但是通过巧妙使用正则表达式,您可以快速有效地处理这段代码:

# regex_soup.py

import re
from urllib.request import urlopen

url = "http://olympus.realpython.org/profiles/dionysus"
page = urlopen(url)
html = page.read().decode("utf-8")

pattern = "<title.*?>.*?</title.*?>"
match_results = re.search(pattern, html, re.IGNORECASE)
title = match_results.group()
title = re.sub("<.*?>", "", title) # Remove HTML tags

print(title)

通过将字符串分成三部分来仔细查看pattern中的第一个正则表达式:

  1. <title.*?> 匹配html中的开始<TITLE >标签。模式的<title部分与<TITLE匹配,因为re.search()是用re.IGNORECASE调用的,而.*?>匹配<TITLE之后直到第一个>实例的任何文本。

  2. .*? 非贪婪地匹配开头<TITLE >后的所有文本,停在第一个匹配的</title.*?>

  3. </title.*?> 与第一个模式的不同之处仅在于它使用了/字符,因此它匹配html中的结束</title / >标记。

第二个正则表达式,字符串"<.*?>",也使用非贪婪的.*?来匹配title字符串中的所有 HTML 标签。通过用""替换任何匹配,re.sub()删除所有标签,只返回文本。

**注意:**用 Python 或任何其他语言进行 Web 抓取可能会很乏味。没有两个网站是以相同的方式组织的HTML 通常是混乱的。此外,网站会随着时间而变化。今天有效的网页抓取工具不能保证明年或者下周也有效。

如果使用正确,正则表达式是一个强大的工具。在这篇介绍中,您仅仅触及了皮毛。有关正则表达式以及如何使用它们的更多信息,请查看由两部分组成的系列文章正则表达式:Python 中的正则表达式

检查你的理解能力

展开下面的方框,检查您的理解情况。

编写一个程序,从以下 URL 获取完整的 HTML:

>>> url = "http://olympus.realpython.org/profiles/dionysus"

然后使用.find()显示*名称:喜爱的颜色:*之后的文本(不包括任何可能出现在同一行的前导空格或尾随 HTML 标签)。

您可以展开下面的方框查看解决方案。

首先,从urlib.request模块导入urlopen函数:

from urllib.request import urlopen

然后打开 URL 并使用由urlopen()返回的HTTPResponse对象的.read()方法来读取页面的 HTML:

url = "http://olympus.realpython.org/profiles/dionysus"
html_page = urlopen(url)
html_text = html_page.read().decode("utf-8")

.read()方法返回一个字节串,所以使用.decode()通过 UTF-8 编码对字节进行解码。

现在您已经将 web 页面的 HTML 源代码作为一个字符串分配给了html_text变量,您可以从 Dionysus 的概要文件中提取他的名字和最喜欢的颜色。狄俄尼索斯个人资料的 HTML 结构与您之前看到的阿芙罗狄蒂个人资料的结构相同。

您可以通过在文本中找到字符串"Name:"并提取该字符串第一次出现之后和下一个 HTML 标签之前的所有内容来获得名称。也就是说,您需要提取冒号(:)之后和第一个尖括号(<)之前的所有内容。你可以用同样的技巧提取喜欢的颜色。

下面的 for循环为名称和喜爱的颜色提取文本:

for string in ["Name: ", "Favorite Color:"]:
    string_start_idx = html_text.find(string)
    text_start_idx = string_start_idx + len(string)

    next_html_tag_offset = html_text[text_start_idx:].find("<")
    text_end_idx = text_start_idx + next_html_tag_offset

    raw_text = html_text[text_start_idx : text_end_idx]
    clean_text = raw_text.strip(" \r\n\t")
    print(clean_text)

看起来在这个for循环中进行了很多工作,但这只是一点点计算提取所需文本的正确索引的运算。继续并分解它:

  1. 您使用html_text.find()来查找字符串的起始索引,可以是"Name:""Favorite Color:",然后将索引分配给string_start_idx

  2. 由于要提取的文本紧接在"Name:""Favorite Color:"中的冒号之后开始,所以通过将字符串的长度加到start_string_idx中可以获得紧跟在冒号之后的字符的索引,然后将结果赋给text_start_idx

  3. 通过确定第一个尖括号(<)相对于text_start_idx的索引,计算要提取的文本的结束索引,并将该值赋给next_html_tag_offset。然后将该值加到text_start_idx上,并将结果赋给text_end_idx

  4. 你通过从text_start_idxtext_end_idx分割html_text来提取文本,并将这个字符串分配给raw_text

  5. 使用.strip()删除raw_text开头和结尾的任何空白,并将结果赋给clean_text

在循环结束时,使用print()显示提取的文本。最终输出如下所示:

Dionysus
Wine

这个解决方案是解决这个问题的众多解决方案之一,所以如果你用不同的解决方案得到相同的输出,那么你做得很好!

当你准备好了,你可以进入下一部分。

在 Python 中使用 HTML 解析器进行 Web 抓取

虽然正则表达式通常非常适合模式匹配,但有时使用专门为解析 HTML 页面而设计的 HTML 解析器更容易。有许多 Python 工具是为此而编写的,但是 Beautiful Soup 库是一个很好的开始。

装美汤

要安装 Beautiful Soup您可以在终端中运行以下命令:

$ python -m pip install beautifulsoup4

使用这个命令,您可以将最新版本的 Beautiful Soup 安装到您的全局 Python 环境中。

Remove ads

创建一个BeautifulSoup对象

在新的编辑器窗口中键入以下程序:

# beauty_soup.py

from bs4 import BeautifulSoup
from urllib.request import urlopen

url = "http://olympus.realpython.org/profiles/dionysus"
page = urlopen(url)
html = page.read().decode("utf-8")
soup = BeautifulSoup(html, "html.parser")

这个程序做三件事:

  1. 使用urllib.request模块中的urlopen()打开网址http://olympus.realpython.org/profiles/dionysus

  2. 以字符串形式从页面中读取 HTML并将其赋给html变量

  3. 创建一个BeautifulSoup对象,并将其分配给soup变量

分配给soupBeautifulSoup对象是用两个参数创建的。第一个参数是要解析的 HTML第二个参数是字符串"html.parser",它告诉对象在后台使用哪个解析器。"html.parser"代表 Python 内置的 HTML 解析器。

使用一个BeautifulSoup对象

保存并运行上面的程序。当它完成运行时,您可以使用交互窗口中的soup变量以各种方式解析html的内容。

**注意:**如果你没有使用 IDLE那么你可以用-i标志运行你的程序进入交互模式。类似于python -i beauty_soup.py的东西将首先运行你的程序,然后让你进入一个 REPL在那里你可以探索你的对象。

例如,BeautifulSoup对象有一个.get_text()方法,可以用来从文档中提取所有文本,并自动删除任何 HTML 标签。

在 IDLE 的交互窗口中或编辑器中的代码末尾键入以下代码:

>>> print(soup.get_text())

Profile: Dionysus

Name: Dionysus

Hometown: Mount Olympus

Favorite animal: Leopard

Favorite Color: Wine

这个输出中有很多空行。这些是 HTML 文档文本中换行符的结果。如果需要,可以用.replace() string 方法删除它们。

通常,您只需要从 HTML 文档中获取特定的文本。首先使用 Beautiful Soup 提取文本,然后使用.find() string 方法,这有时比使用正则表达式更容易。

然而,其他时候 HTML 标签本身是指出您想要检索的数据的元素。例如,您可能想要检索页面上所有图像的 URL。这些链接包含在<img> HTML 标签的src属性中。

在这种情况下,您可以使用find_all()返回该特定标记的所有实例的列表:

>>> soup.find_all("img")
[<img src="/static/dionysus.jpg"/>, <img src="/static/grapes.png"/>]

这将返回 HTML 文档中所有<img>标签的列表。列表中的对象看起来可能是代表标签的字符串,但它们实际上是 Beautiful Soup 提供的Tag对象的实例。Tag对象为处理它们所包含的信息提供了一个简单的接口。

您可以先从列表中解包Tag对象,对此进行一些探索:

>>> image1, image2 = soup.find_all("img")

每个Tag对象都有一个.name属性,该属性返回一个包含 HTML 标签类型的字符串:

>>> image1.name
'img'

您可以通过将名称放在方括号中来访问Tag对象的 HTML 属性,就像属性是字典中的键一样。

例如,<img src="/static/dionysus.jpg"/>标签只有一个属性src,其值为"/static/dionysus.jpg"。同样,像链接<a href="https://realpython.com" target="_blank">这样的 HTML 标签有两个属性,hreftarget

要获得 Dionysus profile 页面中图像的来源,可以使用上面提到的字典符号访问src属性:

>>> image1["src"]
'/static/dionysus.jpg'

>>> image2["src"]
'/static/grapes.png'

HTML 文档中的某些标签可以通过Tag对象的属性来访问。例如,要获得文档中的<title>标签,可以使用.title属性:

>>> soup.title
<title>Profile: Dionysus</title>

如果您通过导航到个人资料页面来查看 Dionysus 个人资料的源代码,右键单击该页面,并选择查看页面源代码,那么您会注意到<title>标签全部用大写字母和空格书写:

Screenshot of Dionysos Website with Source Code.

Beautiful Soup 通过删除开始标签中多余的空格和结束标签中多余的正斜杠(/)自动为您清理标签。

您还可以使用Tag对象的.string属性来检索标题标签之间的字符串:

>>> soup.title.string
'Profile: Dionysus'

Beautiful Soup 的一个特性是能够搜索特定类型的标签,这些标签的属性与某些值相匹配。例如,如果您想要查找所有具有与值/static/dionysus.jpg相等的src属性的<img>标签,那么您可以向.find_all()提供以下附加参数:

>>> soup.find_all("img", src="/static/dionysus.jpg")
[<img src="/static/dionysus.jpg"/>]

这个例子有些武断,从这个例子中可能看不出这种技术的用处。如果你花一些时间浏览各种网站并查看它们的页面源代码,那么你会注意到许多网站都有极其复杂的 HTML 结构。

当使用 Python 从网站抓取数据时,您通常会对页面的特定部分感兴趣。通过花一些时间浏览 HTML 文档,您可以识别具有独特属性的标签,这些标签可用于提取您需要的数据。

然后,不用依赖复杂的正则表达式或使用.find()来搜索整个文档,您可以直接访问您感兴趣的特定标签并提取您需要的数据。

在某些情况下你可能会发现漂亮的汤并没有提供你需要的功能。lxml 库开始有点棘手,但是它提供了比解析 HTML 文档更大的灵活性。一旦你习惯了使用美丽的汤,你可能会想要检查一下。

**注意:**在定位网页中的特定数据时,像美人汤这样的 HTML 解析器可以节省您大量的时间和精力。然而,有时 HTML 写得很差,没有条理,甚至像 Beautiful Soup 这样复杂的解析器也不能正确地解释 HTML 标签。

在这种情况下,您通常需要使用.find()和正则表达式技术来解析出您需要的信息。

Beautiful Soup 非常适合从网站的 HTML 中抓取数据,但是它没有提供任何处理 HTML 表单的方法。例如,如果你需要在一个网站上搜索一些查询,然后搜索结果,那么单靠美丽的汤不会让你走得很远。

Remove ads

检查你的理解能力

展开下面的方框,检查您的理解情况。

编写一个程序,从 URL http://olympus.realpython.org/profiles页面获取完整的 HTML。

使用 Beautiful Soup通过查找名为a的 HTML 标记并检索每个标记的href属性所取的值,打印出页面上所有链接的列表。

最终输出应该如下所示:

http://olympus.realpython.org/profiles/aphrodite
http://olympus.realpython.org/profiles/poseidon
http://olympus.realpython.org/profiles/dionysus

确保在基本 URL 和相对 URL 之间只有一个斜杠(/)。

您可以展开下面的方框查看解决方案:

首先,从urlib.request模块中导入urlopen函数,从bs4包中导入BeautifulSoup类:

from urllib.request import urlopen
from bs4 import BeautifulSoup

/profiles页面上的每个链接 URL 是一个相对 URL ,所以用网站的基本 URL 创建一个base_url变量:

base_url = "http://olympus.realpython.org"

您可以通过连接base_url和一个相对 URL 来构建一个完整的 URL。

现在用urlopen()打开/profiles页面,使用.read()获取 HTML 源代码:

html_page = urlopen(base_url + "/profiles")
html_text = html_page.read().decode("utf-8")

下载并解码 HTML 源代码后,您可以创建一个新的BeautifulSoup对象来解析 HTML:

soup = BeautifulSoup(html_text, "html.parser")

返回 HTML 源代码中所有链接的列表。您可以遍历这个列表,打印出网页上的所有链接:

for link in soup.find_all("a"):
    link_url = base_url + link["href"]
    print(link_url)

您可以通过"href"下标访问每个链接的相对 URL。将该值与base_url连接,创建完整的link_url

当你准备好了,你可以进入下一部分。

与 HTML 表单交互

到目前为止,您在本教程中使用的urllib模块非常适合请求网页内容。但是,有时您需要与网页交互来获取您需要的内容。例如,您可能需要提交表单或单击按钮来显示隐藏的内容。

**注:**本教程改编自 Python 基础知识:Python 实用入门 3 中“与 Web 交互”一章。如果你喜欢你正在阅读的东西,那么一定要看看这本书的其余部分。

Python 标准库并没有提供一个内置的交互处理网页的方法,但是许多第三方包可以从 PyPI 获得。其中, MechanicalSoup 是一个流行且相对简单的软件包。

本质上MechanicalSoup 安装了所谓的无头浏览器,这是一个没有图形用户界面的网络浏览器。该浏览器通过 Python 程序以编程方式控制。

安装机械汤

您可以在您的终端中安装带有 pip 的机械汤:

$ python -m pip install MechanicalSoup

您需要关闭并重新启动您的空闲会话,以便 MechanicalSoup 在安装后加载并被识别。

创建一个Browser对象

在 IDLE 的交互窗口中键入以下内容:

>>> import mechanicalsoup
>>> browser = mechanicalsoup.Browser()

对象代表无头网络浏览器。您可以使用它们通过向它们的.get()方法传递一个 URL 来请求来自互联网的页面:

>>> url = "http://olympus.realpython.org/login"
>>> page = browser.get(url)

page是一个Response对象,存储从浏览器请求 URL 的响应:

>>> page
<Response [200]>

数字200代表请求返回的状态码。状态代码200表示请求成功。如果 URL 不存在,不成功的请求可能会显示状态代码404,如果请求时出现服务器错误,则可能会显示状态代码500

MechanicalSoup 使用 Beautiful Soup 来解析请求中的 HTMLpage有一个代表BeautifulSoup对象的.soup属性:

>>> type(page.soup)
<class 'bs4.BeautifulSoup'>

您可以通过检查.soup属性来查看 HTML:

>>> page.soup
<html>
<head>
<title>Log In</title>
</head>
<body bgcolor="yellow">
<center>
<br/><br/>
<h2>Please log in to access Mount Olympus:</h2>
<br/><br/>
<form action="/login" method="post" name="login">
Username: <input name="user" type="text"/><br/>
Password: <input name="pwd" type="password"/><br/><br/>
<input type="submit" value="Submit"/>
</form>
</center>
</body>
</html>

注意,这个页面上有一个<form>和用于用户名和密码的<input>元素。

Remove ads

提交带有机械汤的表格

在浏览器中打开上一个示例中的 /login 页面,在继续之前亲自查看一下:

Screenshot of Website with Login Form

尝试输入随机的用户名和密码组合。如果你猜错了,那么消息*错误的用户名或密码!*显示在页面底部。

但是,如果您提供了正确的登录凭证,那么您将被重定向到 /profiles 页面:

用户名 密码
zeus ThunderDude

在下一个例子中,您将看到如何使用 Python 来使用 MechanicalSoup 填写和提交这个表单!

HTML 代码的重要部分是登录表单——也就是说,<form>标签中的所有内容。该页面上的<form>name属性被设置为login。这个表单包含两个<input>元素,一个名为user,另一个名为pwd。第三个<input>元素是提交按钮。

既然您已经知道了登录表单的底层结构,以及登录所需的凭证,那么请看一个填写表单并提交表单的程序。

在新的编辑器窗口中,键入以下程序:

import mechanicalsoup

# 1
browser = mechanicalsoup.Browser()
url = "http://olympus.realpython.org/login"
login_page = browser.get(url)
login_html = login_page.soup

# 2
form = login_html.select("form")[0]
form.select("input")[0]["value"] = "zeus"
form.select("input")[1]["value"] = "ThunderDude"

# 3
profiles_page = browser.submit(form, login_page.url)

保存文件,按 F5 运行。要确认您已成功登录,请在交互式窗口中键入以下内容:

>>> profiles_page.url
'http://olympus.realpython.org/profiles'

现在分解上面的例子:

  1. 您创建了一个Browser实例,并使用它来请求 URL http://olympus.realpython.org/login。使用.soup属性将页面的 HTML 内容分配给login_html变量。

  2. login_html.select("form")返回页面上所有<form>元素的列表。因为页面只有一个<form>元素,所以可以通过检索列表中索引0处的元素来访问表单。当一页上只有一个表格时,你也可以使用login_html.form。接下来的两行选择用户名和密码输入,并将它们的值分别设置为"zeus""ThunderDude"

  3. 你用browser.submit()提交表格。注意,您向这个方法传递了两个参数,一个是form对象,另一个是通过login_page.url访问的login_page的 URL。

在交互窗口中,您确认提交成功地重定向到了/profiles页面。如果出了问题,那么profiles_page.url的值仍然是"http://olympus.realpython.org/login"

注意:黑客可以通过快速尝试许多不同的用户名和密码,直到他们找到一个有效的组合,使用类似上面的自动化程序来暴力破解登录。

除了这是高度非法的,几乎所有的网站这些天来锁定你,并报告你的 IP 地址,如果他们看到你做了太多失败的请求,所以不要尝试!

既然您已经设置了profiles_page变量,那么是时候以编程方式获取/profiles页面上每个链接的 URL 了。

为此,您再次使用.select(),这次传递字符串"a"来选择页面上所有的<a>锚元素:

>>> links = profiles_page.soup.select("a")

现在您可以迭代每个链接并打印出href属性:

>>> for link in links:
...     address = link["href"]
...     text = link.text
...     print(f"{text}: {address}")
...
Aphrodite: /profiles/aphrodite
Poseidon: /profiles/poseidon
Dionysus: /profiles/dionysus

每个href属性中包含的 URL 都是相对 URL如果您想稍后使用 MechanicalSoup 导航到它们,这些 URL 没有太大帮助。如果您碰巧知道完整的 URL那么您可以分配构建完整 URL 所需的部分。

在这种情况下,基本 URL 只是http://olympus.realpython.org。然后,您可以将基本 URL 与在src属性中找到的相对 URL 连接起来:

>>> base_url = "http://olympus.realpython.org"
>>> for link in links:
...     address = base_url + link["href"]
...     text = link.text
...     print(f"{text}: {address}")
...
Aphrodite: http://olympus.realpython.org/profiles/aphrodite
Poseidon: http://olympus.realpython.org/profiles/poseidon
Dionysus: http://olympus.realpython.org/profiles/dionysus

你可以只用.get().select().submit()做很多事情。也就是说,机械汤可以做得更多。要了解更多关于机械汤的信息,请查阅官方文件

Remove ads

检查你的理解能力

展开下面的方框,检查您的理解情况

使用 MechanicalSoup 向位于 URL http://olympus.realpython.org/login登录表单提供正确的用户名(zeus)和密码(ThunderDude)。

提交表单后,显示当前页面的标题,以确定您已经被重定向到 /profiles 页面。

你的程序应该打印文本<title>All Profiles</title>

您可以展开下面的方框查看解决方案。

首先,导入mechanicalsoup包并创建一个Broswer对象:

import mechanicalsoup

browser = mechanicalsoup.Browser()

通过将 URL 传递给browser.get()将浏览器指向登录页面,并获取带有.soup属性的 HTML:

login_url = "http://olympus.realpython.org/login"
login_page = browser.get(login_url)
login_html = login_page.soup

login_html是一个BeautifulSoup实例。因为页面上只有一个表单,所以您可以通过login_html.form访问该表单。使用.select(),选择用户名和密码输入,并填入用户名"zeus"和密码"ThunderDude":

form = login_html.form
form.select("input")[0]["value"] = "zeus"
form.select("input")[1]["value"] = "ThunderDude"

现在表单已经填写完毕,您可以使用browser.submit()提交它:

profiles_page = browser.submit(form, login_page.url)

如果您用正确的用户名和密码填写了表单,那么profiles_page实际上应该指向/profiles页面。你可以通过打印分配给profiles_page:的页面标题来确认这一点

print(profiles_page.soup.title)

您应该会看到以下显示的文本:

<title>All Profiles</title>

如果您看到文本Log In或其他东西,那么表单提交失败。

当你准备好了,你可以进入下一部分。

与网站实时互动

有时,您希望能够从提供持续更新信息的网站获取实时数据。

在你学习 Python 编程之前的黑暗日子里,你不得不坐在浏览器前,每当你想检查更新的内容是否可用时,点击刷新按钮来重新加载页面。但是现在您可以使用 MechanicalSoup Browser对象的.get()方法来自动化这个过程。

打开您选择的浏览器并导航至 URL http://olympus.realpython.org/dice:

Screenshot of Website with random number

这个 /dice 页面模拟一个六面骰子的滚动,每次刷新浏览器都会更新结果。下面,您将编写一个程序,反复抓取页面以获得新的结果。

您需要做的第一件事是确定页面上的哪个元素包含掷骰子的结果。现在,右键单击页面上的任意位置并选择查看页面源即可。HTML 代码中间多一点的地方是一个类似下面的<h2>标签:

<h2 id="result">3</h2>

对于您来说,<h2>标签的文本可能不同,但是这是抓取结果所需的页面元素。

**注意:**对于这个例子,您可以很容易地检查出页面上只有一个带有id="result"的元素。尽管id属性应该是惟一的,但实际上您应该总是检查您感兴趣的元素是否被惟一标识。

现在开始编写一个简单的程序,打开 /dice 页面,抓取结果,并打印到控制台:

# mech_soup.py

import mechanicalsoup

browser = mechanicalsoup.Browser()
page = browser.get("http://olympus.realpython.org/dice")
tag = page.soup.select("#result")[0]
result = tag.text

print(f"The result of your dice roll is: {result}")

这个例子使用了BeautifulSoup对象的.select()方法来查找带有id=result的元素。传递给.select()的字符串"#result"使用 CSS ID 选择器 #来表示result是一个id值。

为了定期获得新的结果,您需要创建一个循环,在每一步加载页面。因此,上面代码中位于行browser = mechanicalsoup.Browser()之下的所有内容都需要放在循环体中。

对于本例,您希望以 10 秒的间隔掷出 4 次骰子。为此,代码的最后一行需要告诉 Python 暂停运行十秒钟。你可以用 Python 的 time模块中的 .sleep() 来做到这一点。.sleep()方法采用单个参数,表示以秒为单位的睡眠时间。

这里有一个例子来说明sleep()是如何工作的:

import time

print("I'm about to wait for five seconds...")
time.sleep(5)
print("Done waiting!")

当您运行这段代码时,您将会看到在第一个print()函数被执行 5 秒钟后才会显示"Done waiting!"消息。

对于掷骰子的例子,您需要将数字10传递给sleep()。以下是更新后的程序:

# mech_soup.py

import time
import mechanicalsoup

browser = mechanicalsoup.Browser()

for i in range(4):
    page = browser.get("http://olympus.realpython.org/dice")
    tag = page.soup.select("#result")[0]
    result = tag.text
    print(f"The result of your dice roll is: {result}")
    time.sleep(10)

当您运行该程序时,您将立即看到打印到控制台的第一个结果。十秒钟后,显示第二个结果,然后是第三个,最后是第四个。打印第四个结果后会发生什么?

程序继续运行十秒钟,然后最终停止。那有点浪费时间!您可以通过使用一个 if语句来阻止它这样做,只对前三个请求运行time.sleep():

# mech_soup.py

import time
import mechanicalsoup

browser = mechanicalsoup.Browser()

for i in range(4):
    page = browser.get("http://olympus.realpython.org/dice")
    tag = page.soup.select("#result")[0]
    result = tag.text
    print(f"The result of your dice roll is: {result}")

    # Wait 10 seconds if this isn't the last request
    if i < 3:
        time.sleep(10)

有了这样的技术,你可以从定期更新数据的网站上抓取数据。但是,您应该意识到,快速连续多次请求某个页面可能会被视为对网站的可疑甚至恶意使用。

**重要提示:**大多数网站都会发布使用条款文档。你经常可以在网站的页脚找到它的链接。

在试图从网站上抓取数据之前,请务必阅读本文档。如果您找不到使用条款,那么尝试联系网站所有者,询问他们是否有关于请求量的任何政策。

不遵守使用条款可能会导致您的 IP 被封锁,所以要小心!

过多的请求甚至有可能使服务器崩溃,所以可以想象许多网站都很关心对服务器的请求量!始终检查使用条款,并在向网站发送多个请求时保持尊重。

Remove ads

结论

虽然可以使用 Python 标准库中的工具解析来自 Web 的数据,但是 PyPI 上有许多工具可以帮助简化这个过程。

在本教程中,您学习了如何:

  • 使用 Python 内置的 urllib 模块请求网页
  • 使用美汤解析 HTML
  • 使用 MechanicalSoup 与 web 表单交互
  • 反复从网站请求数据以检查更新

编写自动化的网络抓取程序很有趣,而且互联网上不缺乏可以带来各种令人兴奋的项目的内容。

请记住,不是每个人都希望你从他们的网络服务器上下载数据。在你开始抓取之前,一定要检查网站的使用条款,尊重你的网络请求时间,这样你就不会让服务器流量泛滥。

源代码: 点击这里下载免费的源代码,你将使用它来收集和解析来自网络的数据。

额外资源

有关使用 Python 进行 web 抓取的更多信息,请查看以下资源:

注意:如果你喜欢在这个例子中从 Python 基础知识:Python 3 实用介绍中所学到的东西,那么一定要看看本书的其余部分****