第 5 章:抓取实时站点

在数据侦探的眼里,几乎每一段网络内容都是一个需要收集的信息宝库。想想一系列 Tumblr 帖子,或对 Yelp 上列出的企业的评论。每天,使用在线帐户的人都会产生越来越多的内容,这些内容会显示在网站和应用程序上。一切都是等待结构化的数据。

在上一章中,我们讨论了网页抓取,或使用 HTML 元素的标签和属性从 HTML 元素中提取数据。在那一章中,我们从从 Facebook 下载的存档文件中抓取数据,但在本章中,我们将把注意力转向直接从网络上的实时站点抓取数据。

杂乱的数据

网站是为使用它们的人制作的,而不是为像我们这样想要挖掘它们以获取数据的人制作的。出于这个原因,许多网站的功能虽然让消费者更容易阅读和使用它们,但可能并不适合我们的数据侦查目的。

例如,具有 4,532 条反应的 Facebook 帖子可能会显示 4.5K 条反应的缩写标签。而且,Facebook 不会显示完整的时间戳和数据,而是通常只显示帖子是在多少小时前创建的。特别是在社交媒体平台上,在线内容通常被优化为有用和有趣的,但不一定要有完整的信息。

就我们而言,这意味着我们收集的数据可能不规则、混乱且可能不完整。这也意味着我们可能需要找到一些方法来解决网站的结构以获取信息。

您可能想知道为什么我们会在可能有可用 API 的情况下投入如此多的工作来获取数据。在某些情况下,可在实时网站上轻松访问的数据不是通过 API 提供的。例如,Twitter 允许我们在滚动提要时查看三个月的数据,但只允许我们通过 API 访问大约 3,200 条推文。在 Facebook 上,与公共群组和页面相关的数据可通过 API 获得,但它可能与填充我们的新闻提要的数据不同。

对于 BuzzFeed 新闻的一个故事,我们分析了 Katherine Cooper 和 Lindsey Linder 这对政治上存在分歧的母女对的 Facebook 新闻提要中的 2,367 个帖子,以向他们展示他们的网络世界有多么不同。 Cooper 和 Linder 说,在 Facebook 上,他们的政治分歧导致了激烈和丑陋的争吵,这在他们离线交谈时不会发生。查看各自的提要帮助我们阐明了每个女性的信息世界是如何由最常出现的帖子构成的(见图 5-1)。

这些信息是针对每个 Facebook 帐户量身定制的,这意味着只有通过查看 Linder 和 Cooper 的 Facebook 新闻提要才能获得这些信息。

注意您可以阅读来自 BuzzFeed 新闻的文章,“这位保守的妈妈和自由主义的女儿对他们的 Facebook 提要的不同感到惊讶”,网址为 https://www.buzzfeed.com/lamvo/facebook-filter-bubbles-liberal-daughter-conservative-mom/.

图 5-1:分别在 Linder 和 Cooper 的提要上显示谁的帖子最多的图表

数据抓取的道德考虑

社交媒体公司根据他们认为适合其商业利益、用户隐私问题和其他原因设置数据限制。在某种程度上,抓取是绕过这些限制的一种方式。

但是,不应掉以轻心地决定抓取网站。未经许可从网站上抓取信息或重新发布抓取的数据可能违反公司的服务条款,并可能使您被禁止使用该平台,或者更糟的是,导致法律诉讼。

那么什么时候可以从网站上抓取数据呢?

对于从实时站点抓取数据的决定以及我们如何构建我们的网络抓取工具,有多种考虑因素。数据记者 Roberto Rocha 在他的博客文章“On the Ethics of Web Scraping” (https://robertorocha.info/on-the-ethics-of-web-scraping/) 中列出了四个可以作为很好指导的问题:

  • 我可以拿这个数据吗?
  • 我可以重新发布这些数据吗?
  • 我是否使网站的服务器超载?
  • 我可以用这些数据做什么?

我们当然不是第一个,也不会是最后一个对从实时网站上抓取信息感兴趣的人。鉴于这一事实,公司可能会围绕这种做法制定政策,通常采用两份文件的形式:

  • 机器人排除协议
  • 服务条款

接下来,我们将从机器人排除协议(通常称为 robots.txt 文件)开始,深入研究这两种策略。

机器人排除协议

机器人排除协议是一个文本文件,通常托管在平台的服务器上。您可以通过输入网站的 URL(例如 http://facebook.com/)并将 robots.txt 附加到末尾来找到它,例如:http://facebook.com/robots.txt。

许多网站和平台所有者使用本文档来解决网络机器人——以自动方式浏览网络的程序和脚本,或者更简单地说,它们不是人类。这些机器人通常也被称为爬虫、蜘蛛,或者,如果您更喜欢更诗意的术语,则称为网络漫游者。机器人排除协议文件以标准化方式构建,基本上就像试图访问网站的网络机器人的规则手册。

并非每个刮刀都会遵守这些规则。垃圾邮件程序或恶意软件可能会无视协议,但这样做的风险是从平台启动。我们希望确保我们尊重每个网站的规则,以免遭受同样的命运。

一个非常基本的 robots.txt 文件可能如下所示:

User-agent: *
Disallow: /

并遵循以下结构:

User-agent: [whom this applies to]
Disallow/Allow: [the directory or folder that should not be crawled or scraped]

术语用户代理指定规则适用于谁。 在我们的示例中,用户代理是 *,这意味着该规则适用于任何机器人,包括我们可能编写的机器人。 不允许意味着机器人可能不会抓取列出的目录或文件夹。 在这种情况下,正斜杠 (/) 指定机器人可能不会抓取网站根文件夹中的任何内容。 根文件夹包含网站的所有文件,这意味着该 robots.txt 文件禁止所有机器人抓取网站的任何部分。

这并不意味着不可能从网站上抓取数据。 这只是意味着网站对抓取不屑一顾,这样做可能会给我们带来网站所有者的麻烦。

robots.txt 文件也可以有更具体的规则。 例如,Facebook 的 robots.txt 文件包含专门适用于名为 Googlebot 的用户代理的条款(参见清单 5-1)。

--snip--
User-agent: Googlebot
Disallow: /ajax/
Disallow: /album.php
Disallow: /checkpoint/
Disallow: /contact_importer/
Disallow: /feeds/
Disallow: /file_download.php
Disallow: /hashtag/
Disallow: /l.php
Disallow: /live/
Disallow: /moments_app/
Disallow: /p.php
Disallow: /photo.php
Disallow: /photos.php
Disallow: /sharer/
--snip--

清单 5-1:Facebook 更复杂的 robots.txt 文件

这意味着名为 Googlebot 的机器人不得访问 Facebook 网站的任何部分,包括 facebook.com/ajax/、facebook.com/album.php、facebook.com/checkpoint/ 等 URL。

服务条款

网站的服务条款文件是另一种确定网站所有者是否允许机器人抓取或抓取其网站的方法。服务条款可能会规定允许网络机器人做什么,或者如何重用或不重用来自网站的信息。

社交媒体用户产生的数据对于提供这些在线服务的公司来说非常有价值和重要。我们的共享行为、浏览和搜索历史允许平台为用户构建数据配置文件并向他们推销产品。许多社交媒体公司禁止其他人收集这些数据有明显的经济动机。

公司还必须保护用户的数据和隐私。如果垃圾邮件机器人或其他有问题的机器人收集用户信息,可能会疏远平台用户并导致他们离开服务。由于这两个原因以及其他各种原因,社交媒体公司非常重视他们的服务条款。

数据抓取的技术考虑

除了围绕网络抓取的道德考虑之外,还需要考虑技术因素。在上一章中,我们从下载到本地计算机的 Web 存档中抓取了数据。这意味着我们没有使用我们的脚本连接到互联网,也没有访问实时网站。

一旦我们使用爬虫打开了一个网站,我们就应该考虑这会如何影响托管内容的服务器。每次打开网站时,我们都会访问托管在服务器上的信息。每个请求都需要网站的脚本来获取数据,将其转换为 HTML 格式,然后将其传输到我们的浏览器。这些操作中的每一个都只花费很少的钱——就像在手机上传输几兆字节的信息一样,我们也要花钱。

在浏览器中打开一个网站,等待它加载,然后像人类用户那样滚动浏览它是一回事。对机器人进行编程以在几秒钟内打开 1,000 个网站是另一回事。想象一下,一台服务器必须一次处理一千次这样的传输。它可能会在这些请求的速度和重量下崩溃——换句话说,服务器会变得过载。

这意味着当我们编写一个爬虫或机器人时,我们应该通过指示它在打开每个网站之间等待几秒钟来减慢它的速度。在本章稍后我们编写刮刀时,您将看到如何做到这一点。

抓取数据的原因

最后但并非最不重要的一点是,思考我们需要信息的原因会有所帮助。没有万无一失的方法可以避免与拥有和运营平台的公司、我们希望抓取的网站以及人们发布的内容发生冲突,但是如果我们决定询问社交媒体,那么深思熟虑的抓取理由可能是有益的。媒体公司从他们的网站收集数据的许可,或者如果我们决定继续并自行承担风险来抓取网站。

透明和清楚地说明你为什么要抓取信息可以帮助公司决定是否允许你继续。例如,研究 Facebook 上侵犯人权行为的学者可能能够为非商业目的抓取某些 Facebook 个人资料提供更好的理由,而为与 Facebook 竞争的商业服务重新发布抓取数据的公司更有可能面临法律后果。行动。影响抓取信息行为的法律的因素有很多——您的位置、您公司或组织的位置、所发布内容的版权、您尝试抓取的平台的服务条款、您的数据收集可能引发的隐私问题——当您想要抓取信息时,所有这些都必须成为您决策过程的一部分。每个数据收集案例都足够独特,您应该确保在开始编写代码之前进行法律和道德研究。

牢记这些道德和技术方面的考虑,现在我们将开始从实时网站上抓取数据。

从实时网站抓取

在本示例中,我们将从维基百科中抓取女性计算机科学家的列表,其中包含一个 robots.txt 文件,允许良性机器人抓取其内容。

我们将抓取的页面的 URL,如图 5-2 所示,是 https://en.wikipedia.org/wiki/Category:Women_computer_scientists.

图 5-2:维基百科女计算机科学家名单

正如我们在前几章所做的那样,我们通过加载我们需要的所有库来开始我们的脚本。 打开您的文本编辑器并将名为 wikipediascraper.py 的新文件保存在您可以轻松访问的文件夹中。 然后将清单 5-2 中的代码输入到该文件中。

# Import our modules or packages that we will need to scrape a website
import csv 
from bs4 import BeautifulSoup
import requests
# make an empty array for your data
rows = []

清单 5-2:设置脚本

我们导入 CSV、requests 和 beautifulsoup4 库,我们在前面的章节中使用过。 接下来,就像我们在之前的脚本中所做的那样,我们为变量 rows 分配一个空列表的值,稍后我们将向其中添加我们的数据行。

然后是一个稍微新的任务:打开一个实时网站。 同样,这个过程与我们在第 2 章中打开基于 URL 的 API 提要时所做的非常相似,但这次我们将打开包含我们想要收集的信息的维基百科页面。 将清单 5-3 中的代码输入到您的 Python 文件中。

# open the website
url = "https://en.wikipedia.org/wiki/Category:Women_computer_scientists"①
page = requests.get(url)②
page_content = page.content③
# parse the page with the Beautiful Soup library
soup = BeautifulSoup(page_content, "html.parser")

清单 5-3:从 URL 中检索内容

我们设置的第一个变量 url ① 是一个字符串,其中包含我们要使用脚本打开的 URL。然后我们设置变量 page ② 以包含我们使用请求库的 get() 函数打开的 HTML 页面;此功能从网络上抓取站点。之后,我们使用请求中的 content ③ 属性将我们刚刚打开并在上一行中摄取的页面的 HTML 编码为可由 Beautiful Soup 解释的字节。然后我们使用 Beautiful Soup 的 HTML 解析器来帮助我们的脚本区分 HTML 和网站内容。

分析页面内容

正如我们对 Facebook 档案的 HTML 页面所做的那样,我们需要分析包含我们想要通过 Python 脚本收集的内容的 HTML 标签。

和以前一样,Web Inspector 是一个有用的工具,它允许我们隔离相关代码。在这种情况下,我们需要一个由维基百科作者和编辑收集的女性计算机科学家名单,我们可以在图 5-3 中看到。

图 5-3:我们要抓取的 Wikipedia 页面,显示为 Web Inspector 打开

正如您在页面上看到的那样,计算机科学家的名字被细分为按每个女性姓氏排序的列表,并按字母顺序显示。除此之外,我们的列表分布在两页上。我们可以通过单击下一页链接访问第二页。

要显示哪些元素包含我们的内容,我们可以右键单击要抓取的列表中的名称并访问页面的 HTML。请记住,我们感兴趣的是检测我们在收集数据时可以利用的模式。

右键单击“A”组中的名称,然后在下拉菜单中选择“检查”选项。这应该会更改 Web Inspector 内的视图:HTML 视图应该跳转到包含您选择的“A”组中的名称的代码部分,并突出显示包含它的标签。网站可能很长并且包含数百个 HTML 标签,Inspect 允许您在网站的 HTML 中找到您在页面上看到的特定元素。您可以将其视为地图上的“您在这里”标记。

现在您已经找到了“A”组中的一个名称在 HTML 中的位置,您可以开始更仔细地检查包含“A”组中所有其他名称的 HTML 标记结构。为此,您可以滚动代码直到到达父 标记,,它突出显示了页面的相应部分,如图 5-4 所示。此标签包含以“A”开头的姓氏列表。

图 5-4:在突出显示的字母“A”下归档的女性计算机科学家名单的父标签

现在右键单击父标签并选择编辑为 HTML。 这应该允许您复制和粘贴父标记以及嵌套在父标记内的每个 HTML 标记,如图 5-5 所示。

图 5-5:选择 Edit as HTML

如果将我们刚刚复制的代码粘贴到文本编辑器中的一个空文件中,它应该如清单 5-4 所示。

<div class="mw-category-group"><h3>A</h3>
<ul><li><a href="/wiki/Janet_Abbate" title="Janet Abbate">Janet Abbate</a></li>
<li><a href="/wiki/T%C3%BClay_Adal%C4%B1" title="Tülay Adalı">Tülay Adalı</a></li>
<li><a href="/wiki/Sarita_Adve" title="Sarita Adve">Sarita Adve</a></li>
<li><a href="/wiki/Dorit_Aharonov" title="Dorit Aharonov">Dorit Aharonov</a></li>
<li><a href="/wiki/Anastasia_Ailamaki" title="Anastasia Ailamaki">Anastasia Ailamaki</a></li>
<li><a href="/wiki/Susanne_Albers" title="Susanne Albers">Susanne Albers</a></li>
<li><a href="/wiki/Frances_E._Allen" title="Frances E. Allen">Frances E. Allen</a></li>
<li><a href="/wiki/Sarah_Allen_(software_developer)" title="Sarah Allen (software developer)">Sarah Allen (software developer)</a></li>
<li><a href="/wiki/Nancy_M._Amato" title="Nancy M. Amato">Nancy M. Amato</a></li>
<li><a href="/wiki/Pat_Fothergill" title="Pat Fothergill">Pat Fothergill</a></li>
<li><a href="/wiki/Nina_Amenta" title="Nina Amenta">Nina Amenta</a></li>
<li><a href="/wiki/Dana_Angluin" title="Dana Angluin">Dana Angluin</a></li>
<li><a href="/wiki/Annie_Ant%C3%B3n" title="Annie Antón">Annie Antón</a></li>
<li><a href="/wiki/Cecilia_R._Aragon" title="Cecilia R. Aragon">Cecilia R. Aragon</a></li>
<li><a href="/wiki/Gillian_Arnold_(technologist)" title="Gillian Arnold (technologist)">Gillian Arnold (technologist)</a></li>
<li><a href="/wiki/Chieko_Asakawa" title="Chieko Asakawa">Chieko Asakawa</a></li>
<li><a href="/wiki/Winifred_Asprey" title="Winifred Asprey">Winifred Asprey</a></li>
<li><a href="/wiki/Stella_Atkins" title="Stella Atkins">Stella Atkins</a></li>
<li><a href="/wiki/Hagit_Attiya" title="Hagit Attiya">Hagit Attiya</a></li>
<li><a href="/wiki/Terri_Attwood" title="Terri Attwood">Terri Attwood</a></li>
<li><a href="/wiki/Donna_Auguste" title="Donna Auguste">Donna Auguste</a></li>
<li><a href="/wiki/Chrisanthi_Avgerou" title="Chrisanthi Avgerou">Chrisanthi Avgerou</a></li>
<li><a href="/wiki/Henriette_Avram" title="Henriette Avram">Henriette Avram</a></li></ul></div>

清单 5-4:没有缩进的维基百科 HTML

正如您在此处看到的,网站上的 HTML 代码通常在呈现时几乎没有缩进或空格。那是因为浏览器从上到下读取代码,从左到右读取每一行。不同代码行之间的空格越少,浏览器读取它们的速度就越快。

然而,这种精简的代码比缩进的代码更难于人类阅读。使用空格和制表符允许编码人员指示代码不同部分内的层次结构和嵌套。由于大多数网页会缩小或最小化代码以满足浏览器的需求,因此我们有时需要将其取消缩小。如果这段代码被缩进,我们将更容易阅读和理解包含我们所需信息的 HTML 元素的层次结构和模式。

网络上有很多免费工具可以为缩小代码重新引入缩进和空格,包括 Unminify (http://unminify.com/)。要使用这些工具,您通常只需要复制缩小后的 HTML,将其粘贴到该工具提供的窗口中,然后单击按钮取消缩小!

清单 5-5 显示了与清单 5-4 相同的代码,现在未缩小。

<div class="mw-category-group">①
    <h3>A</h3>②
    <ul>③
      ④ <li><a href="/wiki/Janet_Abbate" title="Janet Abbate"⑤>Janet Abbate</a></li>
        <li><a href="/wiki/T%C3%BClay_Adal%C4%B1" title="Tülay Adalı">Tülay Adalı</a></li>
        <li><a href="/wiki/Sarita_Adve" title="Sarita Adve">Sarita Adve</a></li>
        <li><a href="/wiki/Dorit_Aharonov" title="Dorit Aharonov">Dorit Aharonov</a></li>
        <li><a href="/wiki/Anastasia_Ailamaki" title="Anastasia Ailamaki">Anastasia Ailamaki</a></li>
        <li><a href="/wiki/Susanne_Albers" title="Susanne Albers">Susanne Albers</a></li>
        <li><a href="/wiki/Frances_E._Allen" title="Frances E. Allen">Frances E. Allen</a></li>
        <li><a href="/wiki/Sarah_Allen_(software_developer)" title="Sarah Allen (software developer)">Sarah Allen (software developer)</a></li>
        <li><a href="/wiki/Nancy_M._Amato" title="Nancy M. Amato">Nancy M. Amato</a></li>
        <li><a href="/wiki/Pat_Fothergill" title="Pat Fothergill">Pat Fothergill</a></li>
        <li><a href="/wiki/Nina_Amenta" title="Nina Amenta">Nina Amenta</a></li>
        <li><a href="/wiki/Dana_Angluin" title="Dana Angluin">Dana Angluin</a></li>
        <li><a href="/wiki/Annie_Ant%C3%B3n" title="Annie Antón">Annie Antón</a></li>
        <li><a href="/wiki/Cecilia_R._Aragon" title="Cecilia R. Aragon">Cecilia R. Aragon</a></li>
        <li><a href="/wiki/Gillian_Arnold_(technologist)" title="Gillian Arnold (technologist)">Gillian Arnold (technologist)</a></li>
        <li><a href="/wiki/Chieko_Asakawa" title="Chieko Asakawa">Chieko Asakawa</a></li>
        <li><a href="/wiki/Winifred_Asprey" title="Winifred Asprey">Winifred Asprey</a></li>
        <li><a href="/wiki/Stella_Atkins" title="Stella Atkins">Stella Atkins</a></li>
        <li><a href="/wiki/Hagit_Attiya" title="Hagit Attiya">Hagit Attiya</a></li>
        <li><a href="/wiki/Terri_Attwood" title="Terri Attwood">Terri Attwood</a></li>
        <li><a href="/wiki/Donna_Auguste" title="Donna Auguste">Donna Auguste</a></li>
        <li><a href="/wiki/Chrisanthi_Avgerou" title="Chrisanthi Avgerou">Chrisanthi Avgerou</a></li>
        <li><a href="/wiki/Henriette_Avram" title="Henriette Avram">Henriette Avram</a></li>
    </ul>
</div>

清单 5-5:带有缩进的维基百科代码

如您所见,HTML 页面的内容包含具有类 mw-category-group ① 的父 标签、 标签内的标题 ② 包含字母表、标签 ③ 和 标签 ④ 包含在该列表中,其中包含姓氏以 标题中指定的字母开头的所有女性 - 在本例中为 A。还有指向每个女性的相关维基百科页面的链接带有每个名称和 `` 标签 ⑤。

将页面内容存储在变量中

鉴于我们现在对代码的了解,让我们回顾一下打开页面后必须做的事情:

  • 从第一页获取所有无序列表的名字。
  • 从每个列表项中提取信息,包括女计算机科学家的姓名、指向她的个人资料的链接以及她所属的字母。
  • 根据此信息创建一行数据,并将每一行写入一个 .csv 文件。

为了帮助我们可视化该数据集的外观以及我们将如何组织它,图 5-6 中的电子表格根据我们的说明构建了我们的数据。

图 5-6:可以帮助我们构建刮板的模型电子表格

好的,现在我们准备开始编写从 HTML 中获取信息的脚本部分!

我们将首先用我们的脚本抓取每个按字母顺序排列的列表。 返回到您的 Python 文件并输入清单 5-6 中的代码。

--snip--
soup = BeautifulSoup(page_content, "html.parser")
content = soup.find("div", class_="mw-category")①
all_groupings = content.find_all("div", class_="mw-category-group")②

清单 5-6:使用 Beautiful Soup 检索 HTML

在这段代码中,我们使用 find() ① 函数来查找包含类 mw-category标签。这是整个 标签,包含我们想要抓取的内容,正如我们之前通过 Web Inspector 查看我们的页面时看到的那样。我们将这个值——意味着包含所有列表的整个 HTML 块——分配给变量内容。下一行将所有包含类 mw-category-group ② 的 标签放入变量 `all_groupings`。这个任务最好使用函数 find_all() 来完成,它根据我们指定的特征在 HTML 中搜索元素并为我们创建一个列表。我们向 find_all() 函数传递两个参数:字符串“div”,它告诉 `find_all()` 我们希望它找到什么样的 HTML 元素,以及 `class_` 参数 `"mw-category-group"`。这意味着`find_all()` 函数将获取每个具有`"mw-category-group"` 类的 并创建它们的列表。然后,变量 all_groupings 将保存一个 HTML 元素列表,其中包含一个按字母顺序排序的名字列表,以及每个女人姓氏的第一个字母。

接下来,我们需要遍历每个按字母顺序排列的列表并从每个列表中收集名称,如清单 5-7 所示。将此代码也输入到您的 Python 文件中。

--snip--
all_groupings = content.find_all("div", class_="mw-category-group")
for grouping in all_groupings:①
    names_list = grouping.find("ul")②
    category = grouping.find("h3").get_text()③
    alphabetical_names = names_list.find_all("li")④

清单 5-7:使用 for 循环收集每个名称

首先,我们需要编写一个 for 循环 ① 来遍历我们刚刚添加到变量“all_groupings”中的每个分组。然后我们使用 grouping.find() 函数在每个字母列表中收集无序列表标签 ②,并将所有列表放在我们定义的变量 `names_list` 中。接下来,我们再次使用`grouping.find()` 函数收集标题标签。代码grouping.find("h3") 包含所有标题,但我们的.csv 文件所需的只是与每个标题相关联的文本。为了收集文本,我们使用`get_text()` 函数来提取属于每个组的字母。我们可以在一行中完成所有这些并将结果存储在变量类别中。最后但并非最不重要的一点是,我们抓取 无序列表中的每个列表项标签 。由于我们将所有 分组存储在 names_list 中,我们可以直接在变量 ④ 上使用 find_all() 函数。这应该允许我们获得字母和所有相关名称的列表。

脚本的最后一步是创建一行信息,其中包含姓名、链接和与每个姓名相连的字母,如清单 5-8 所示。

--snip--
    category = grouping.find("h3").get_text()
    alphabetical_names = names_list.find_all("li")
    for alphabetical_name in alphabetical_names:①
        # get the name
        name = alphabetical_name.text②
        # get the link
        anchortag = alphabetical_name.find("a",href = True)③
        link = anchortag["href"]④
        # get the letter
        letter_name = category

清单 5-8:将每个名称的信息分配给变量以准备在 .csv 文件中创建行

这可能是我们的代码第一次变得稍微复杂一点:我们需要在另一个循环中编写一个循环!首先,我们必须遍历每个按字母顺序排列的列表。在示例 5-8 的代码中,我们现在在示例 5-7 中编写的循环内编写另一个 for 循环①——非常元!在示例 5-8 的嵌套循环中,我们遍历列表项标签 列表中的每个字母名称,这些名称当前存储在变量字母表名称中。

每个 列表项标签都包含一个名称,我们使用文本属性②提取该名称。此列表项还包含链接,我们使用其他两个函数抓取这些链接。为了抓取锚标签,我们使用find()函数③。 find() 函数有两个参数。第一个参数是我们要查找的标签,简单的“a”代表锚标签。第二个参数是可选的,这意味着我们不必总是向它传递参数。可选参数有一个默认值,我们可以在需要时更改。在这种情况下,可选参数是href,默认情况下通常设置为值False。通过设置href = True,我们告诉函数只有当标签具有与之关联的href属性或链接时才抓取锚标签。否则——也就是说,如果我们不向可选参数传递一个参数——find() 函数将默认获取每个锚标签。

我们将检索到的锚标签存储在变量 anchortag 中,该变量现在将包含我们检索到的锚标签的所有信息。我们想要获取锚标记内的链接,因此我们需要访问标记的 href 值,该值作为属性存储在标记中。我们通过使用包含字符串 "href" ④ 的括号获取 href 属性来实现这一点。之后,就像我们在前一章所做的那样,我们创建了一个字典,我们可以用它来构建我们收集的数据,如清单 5-9 所示。

--snip--
        # make a dictionary that will be written into the csv
        row = {"name": name,①
               "link": link,②
               "letter_name": letter_name}③
        rows.append(row)④

清单 5-9:创建一个字典来存储数据

每次 for 循环迭代时,我们将创建一行数据。 在第一行代码中,我们使用在行①中打开并在行③中关闭的大括号为变量行分配一个字典。 然后我们继续为每个键(名称 ①、链接 ② 和 letter_name ③)分配值,该值包含我们之前在脚本中收集的相应数据。 最后,我们将数据行附加到我们的列表变量行④。

到目前为止,您编写的脚本应该如清单 5-10 所示。

# Import our modules or packages that we will need to scrape a website
import csv 
from bs4 import BeautifulSoup
import requests
# make an empty list for your data
rows = []
# open the website
url = "https://en.wikipedia.org/wiki/Category:Women_computer_scientists"
page = requests.get(url)
page_content = page.content
# parse the page through the BeautifulSoup library
soup = BeautifulSoup(page_content, "html.parser")
content = soup.find("div", class_="mw-category")
all_groupings = content.find_all("div", class_="mw-category-group")
for grouping in all_groupings:
    names_list = grouping.find("ul")
    category = grouping.find("h3").get_text()
    alphabetical_names = names_list.find_all("li")
    for alphabetical_name in alphabetical_names:
        # get the name
        name = alphabetical_name.text
        # get the link
        anchortag = alphabetical_name.find("a",href=True)
        link = anchortag["href"]
        # get the letter
        letter_name = category
        # make a data dictionary that will be written into the csv
        row = { "name": name,
                "link": link,
                "letter_name": letter_name}
        rows.append(row)

清单 5-10:到目前为止您编写的整个脚本

如果您只想从一页收集数据,此脚本非常有用,但如果您需要从数十甚至数百页收集数据,则它没有那么有用。 还记得我一直强调我们应该编写可重用的代码吗? 在我们的练习中,我们终于有了一个用例!

使脚本可重用

我们现在有一个脚本,可以在一个页面上获取每个女性计算机科学家的名字。 但是页面只包含一半的名字,因为有太多重要的女性计算机科学家,维基百科不得不将列表分成两个网页。

这意味着,要获得完整列表,我们需要从下一页获取其余名称。 我们可以通过将刚刚编写的所有代码包装到一个可以重用的函数中来实现这一点,如清单 5-11 所示。

# Import our modules or packages that we will need to scrape a website
import csv 
from bs4 import BeautifulSoup
import requests
# make an empty list for your data
rows = []
① def scrape_content(url):
    ② page = requests.get(url)
    page_content = page.content
    # parse the page through the BeautifulSoup library
    soup = BeautifulSoup(page_content, "html.parser")
    content = soup.find("div", class_="mw-category")
    all_groupings = content.find_all("div", class_="mw-category-group")
    for grouping in all_groupings:
        names_list = grouping.find("ul")
        category = grouping.find("h3").get_text()
        alphabetical_names = names_list.find_all("li")
        for alphabetical_name in alphabetical_names:
        # get the name
            name = alphabetical_name.text
            # get the link
            anchortag = alphabetical_name.find("a",href=True)
            link = anchortag["href"]
            # get the letter
            letter_name = category
            # make a data dictionary that will be written into the csv
            row = { "name": name,
                    "link": link,
                    "letter_name": letter_name}
            rows.append(row)

清单 5-11:将脚本放入函数中以供重用

要在您自己的文件中重现清单 5-11,首先从您当前的脚本中删除以下代码行:

url = "https://en.wikipedia.org/wiki/Category:Women_computer_scientists"

您不再需要为 url 变量赋值,因为我们需要在多个 URL 上运行代码。

接下来,在①处,我们告诉 Python 我们正在创建一个名为 scrape_content() 的函数,它接受参数 url。然后我们在②处添加构成scrape_content()函数内容的其余代码。该函数中的所有代码都与您在示例 5-10 中编写的代码相同,只是现在缩进了。 (如果您使用的代码编辑器没有自动缩进您的代码,您通常可以突出显示您想要包含在函数中的每一行代码,然后按 Tab 键。)您会注意到在②处,我们打开了一个 URL使用 requests.get(url) 函数。函数中的url变量指的是①处的url。

用简单的英语,当我们调用函数 scrape_content(url) 时,我们只是为 Python 提供了我们想做的所有事情的说明手册。我们将使用我们希望脚本打开和抓取的实际 URL 替换 url 参数。例如,要在 Wikipedia 页面上运行该函数,我们只需将以下行添加到我们的脚本中:

scrape_content("<https://en.wikipedia.org/wiki/Category:Women_computer_scientists>")

但是,我们需要多次运行该函数。 如果我们想在一个或两个 URL 上运行它,这很有效,但我们可能需要在数百个 URL 上运行它。 要为多个 URL 运行它,我们可以创建一个包含每个 URL 作为字符串的列表; 这样,我们就可以遍历列表来为每个链接运行函数。 为此,将清单 5-12 中的代码添加到您的脚本中的代码清单 5-11 之后。

# open the website
urls = ①["https://en.wikipedia.org/wiki/Category:Women_computer_scientists", "https://en.wikipedia.org/w/index.php?title=Category:Women_computer_scientists&pagefrom=Lin%2C+Ming+C.%0AMing+C.+Lin#mw-pages"]
def scrape_content(url):
--snip--
            rows.append(row)
for url in urls:②
    scrape_content(url)③

清单 5-12:使用循环在 URL 上运行该函数

变量 urls ① 包含一个包含两个字符串的列表:第一个 Wikipedia 页面的 URL,其中包含女性计算机科学家姓名的前半部分,然后是包含其余姓名的第二页的链接。 然后我们编写一个 for 循环,循环遍历 urls 列表 ② 中的每个 URL,并为每个 URL 运行 scrape_content() 函数 ③。 如果您想在更多 Wikipedia 列表页面上运行抓取工具,只需将它们作为链接添加到 urls 列表即可。

现在我们已将所有数据放入行变量中,是时候将数据输出为电子表格了。 将清单 5-13 中的代码行添加到您的脚本中,这将完成这项工作。

--snip--
# make a new csv into which we will write all the rows
with open("all-women-computer-scientists.csv", "w+") as csvfile:①
    # these are the header names:
    fieldnames = ["name", "link", "letter_name"]②
    # this creates your csv
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)③
    # this writes in the first row, which are the headers
    writer.writeheader()④
    # this loops through your rows (the array you set at the beginning and have updated throughtout)
    for row in rows:⑤
        # this takes each row and writes it into your csv
        writer.writerow(row)⑥

清单 5-13:根据收集的数据创建一个 .csv 文件

正如我们之前所做的那样,我们使用 with open() as csvfile ① 语句来创建并打开一个名为 all-women-computer-scientists.csv 的空 .csv 文件。 由于我们使用字典来收集我们的数据,我们需要为我们的电子表格②指定一个标题名称列表,然后使用csv库③中的DictWriter()函数将每个标题写入第一行 ④.

最后,我们需要遍历我们在行列表 ⑤ 中编译的每一行,并将每一行写入我们的电子表格 ⑥。

练习礼貌刮痧

快完成了!我们现在已经编写了一个可以有效收集数据的工作脚本。但是有两件事我们应该考虑添加到脚本中,既要使我们的工作透明,又要避免承载我们要抓取的数据的服务器过载。

首先,为您的抓取工具提供联系方式总是很有帮助的,这样您抓取的网站的所有者可以在出现任何问题时与您联系。在某些情况下,如果您的抓取工具出现问题,您可能会从网站启动。但是,如果网站所有者能够与您联系并告诉您调整抓取工具,则您更有可能继续在该网站上工作。

我们为这个抓取工具安装的 Python 库 requests 带有一个名为 headers 的有用参数,我们可以在从 Web 访问页面时设置它。我们可以用它来创建一个数据字典来保存对网站所有者来说很重要的信息。将清单 5-14 中的代码添加到您的抓取工具中,用您自己的信息替换我的信息。

--snip--
headers = {①"user-agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
            ② "from": "Your name example@domain.com"}
--snip--
    page = requests.get(url, headers=headers)③

清单 5-14:向刮板添加标题

清单 5-14 中的代码必须放在我们导入库的脚本的前几行之后,因为我们需要先加载 requests 库,然后才能利用它。由于标题是您设置的变量并且不会在整个脚本中更改,因此您可以将它们放置在脚本顶部附近,最好是在导入行之后。调用我们要抓取的页面的行 page = requests.get(url, headers=headers) 是对您已经编写的行的修改。将当前读取 page = requests.get(url) 的行替换为新的页面调用代码。每次您请求加载要抓取的网站的 URL 时,这都会提醒网站所有者您的信息。

分配给 headers 变量的信息是您在通过脚本打开 URL 时与服务器交换的信息。如您所见,所有这些信息再次以 JSON 结构化,其中包含表示键("user-agent""from")和值("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36","Your name example@domain.com")。

首先,我们可以向网站所有者提供有关我们正在使用的用户代理类型①的信息。虽然此信息对于机器人来说不是必需的,但它可能允许您的抓取工具打开通常无法在 Web 浏览器之外打开的网站。您的抓取工具还可以使用您计算机已安装的不同浏览器来访问网站。用户代理标头可以传达有关我们的机器人可能用于在浏览器中打开页面的浏览器功能的信息(在本示例中我们不这样做,但这可能是您在编写其他机器人时尽早采用的有用习惯)。要了解您使用的其他用户代理,您可以找到各种在线工具,包括此处的一个:[https://www.whoishostingthis.com/tools/user-agent/](https://www.whoishostingthis.com /工具/用户代理/)。

然后我们可以在分配给键的字符串中指定我们是谁②。在这种情况下,您可以将您的姓名和电子邮件地址写入字符串中作为您的联系信息。要使用这些标头,我们将它们分配给 requests.get() ③ 函数中的 headers 参数。

最后但并非最不重要的一点是,我们还应该避免使托管我们正在抓取的网站的服务器负担过重。如前所述,当抓取器快速连续打开多个页面而不在每个请求之间中断时,通常会发生这种情况。

为此,我们可以使用名为 time 的库,它是 Python 标准库的一部分,因此它已经安装。将清单 5-15 中的代码添加到您的脚本中。

--snip--
# Import our libraries that we will need to scrape a website
import csv 
import time①
from bs4 import BeautifulSoup
import requests
time.sleep(2)②

清单 5-15:在爬虫的代码中添加暂停

要使用时间库,首先我们需要导入它①。 然后,我们可以使用它的一个名为 sleep() ② 的函数,它基本上告诉我们的刮板休息一下。 sleep() 函数将一个数字作为参数——一个整数(整数)或浮点数(带小数的数字)——它表示休息的时间量,以秒为单位。 在第②行,我们的脚本被指示在恢复和抓取数据之前等待 2 秒。

如果我们现在将本章中编写的所有代码片段拼接在一起,我们的脚本应该类似于示例 5-16。

# Import our modules or packages that we will need to scrape a website
import csv
import time
from bs4 import BeautifulSoup
import requests
# Your identification
headers = {"user-agent" : "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36;",
            "from": "Your name <example@domain.com>"}
# make an empty array for your data
rows = []
# open the website
urls = ["https://en.wikipedia.org/wiki/Category:American_women_computer_scientists", "https://en.wikipedia.org/w/index.php?title=Category:American_women_computer_scientists&pagefrom=Lehnert%2C+Wendy%0AWendy+Lehnert#mw-pages"]
def scrape_content(url):
    time.sleep(2)
    page = requests.get(url, headers= headers)
    page_content = page.content
    # parse the page through the BeautifulSoup library
    soup = BeautifulSoup(page_content, "html.parser")
    content = soup.find("div", class_="mw-category")
    all_groupings = content.find_all("div", class_="mw-category-group")
    for grouping in all_groupings:
        names_list = grouping.find("ul")
        category = grouping.find("h3").get_text()
        alphabetical_names = names_list.find_all("li")
        for alphabetical_name in alphabetical_names:
            # get the name
            name = alphabetical_name.text
            # get the link
            anchortag = alphabetical_name.find("a",href=True)
            link = anchortag["href"]
            # get the letter
            letter_name = category
            # make a data dictionary that will be written into the csv
            row = { "name": name,
                    "link": link,
                    "letter_name": letter_name}
            rows.append(row)
for url in urls:
    scrape_content(url)
# make a new csv into which we will write all the rows
with open("all-women-computer-scientists.csv", "w+") as csvfile:
    # these are the header names:
    fieldnames = ["name", "link", "letter_name"]
    # this creates your csv
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
    # this writes in the first row, which are the headers
    writer.writeheader()
    # this loops through your rows (the array you set at the beginning and have updated throughtout)
    for row in rows:
        # this takes each row and writes it into your csv
        writer.writerow(row)

清单 5-16:完成的爬虫脚本

要运行和测试您的抓取工具,请确保您已连接到互联网并保存您的文件。 打开命令行界面 (CLI),导航到包含该文件的文件夹,然后根据您使用的 Python 版本在 CLI 中运行以下命令之一。 在 Mac 上,使用以下命令:

python3 wikipediascraper.py

在 Windows 机器上,使用以下命令:

python wikipediascraper.py

这应该会在包含您的“wikipediascraper.py”文件的文件夹中生成一个“.csv”文件。

概括

总而言之,这些实践不仅教会了你刮刮的力量,还教会了你的行为可能产生的道德后果。 仅仅因为您有能力做某事并不总是意味着您可以自由地这样做。 阅读本章后,您应该具备负责任地收集数据所需的所有知识。