本文概述
你可以找到我们将在此处编写的脚本的完整示例。
什么是网络爬虫?
对, 那么网页抓取到底是什么?顾名思义, 它是一种”抓取”或从网页提取数据的方法。你可以使用浏览器在互联网上看到的任何内容(包括本教程)都可以刮到本地硬盘上。
网页抓取有很多用途。对于任何数据分析, 第一步都是数据采集。互联网是人类所有历史和知识的巨大储存库, 你可以提取所需的任何内容, 并根据自己的意愿进行处理。
在我们的教程中, 我们将使用Python和BeautifulSoup 4包从subreddit获取信息。我们对datascience subreddit感兴趣。我们想要在subreddit上获取前1000个帖子, 并将其导出到CSV文件。我们想知道谁发布了它, 以及它有多少喜欢和评论。
我们将在本教程中介绍:
- 使用请求获取网页
- 在浏览器中分析网页以获取信息
- 使用BeautifulSoup从原始HTML中提取信息
注意:我们将使用旧版本的Reddit网站, 因为它加载起来更轻巧, 因此在你的计算机上的工作量也更少。
先决条件
本教程假定你了解以下内容:
- 在计算机上运行Python脚本
- HTML结构的基础知识
你可以在srcmini的Python入门课程中学习上述技能。话虽这么说, 这里使用的概念非常少, 并且你可以了解很少的Python知识。
既然完成了, 我们就可以进入制作网络刮板的第一部分了。实际上, 编写任何Python脚本的第一部分是:import。
在我们的刮板中, 我们将使用以下软件包:
- 要求
- 美丽的汤4
你当然可以使用pip安装这些软件包, 如下所示:
pip install package_name
下载完软件包后, 继续并将其导入代码中。
import requests
import csv
import time
from bs4 import BeautifulSoup4
我们将使用Python的内置csv模块将结果写入CSV文件。你可能已经注意到上面的片段中有一些古怪的地方。也就是说, 我们下载了一个名为beautifulsoup4的软件包, 但是我们是从一个名为bs4的模块中导入的。这在Python中是合法的, 尽管人们通常对此表示反对, 但这并不完全违法。
第一步
因此, 我们已经设置好环境并准备就绪。接下来, 我们需要要抓取的网页的URL。对于我们的教程, 我们使用Reddit的’datascience’subreddit。在开始编写脚本之前, 我们需要做一些现场工作。打开网络浏览器, 然后转到有问题的subreddit。
请注意, 我们将使用较旧版本的subreddit作为我们的抓取工具。较新的版本在网页的底部隐藏了一些关键信息。也可以从新站点中提取此信息, 但为简单起见, 我们将使用较旧的版本, 其中列出了所有内容。
打开链接后, 你会遇到一堆信息超载的情况。我们在所有这一切中到底需要什么?好了, 研究完所有链接后, 你会发现Reddit帖子有两种类型:
- 入站
- 出站
入站链接是指向Reddit本身上的内容的链接, 而出站则恰好相反。这很重要, 因为我们只需要入站链接。帖子包含用户撰写的文字, 而不仅仅是指向其他网站的链接。
所以, 我们知道我们想要什么, 我们如何去提取它?如果你查看帖子的标题, 你会发现它的后面是括号内的一些文本。我们感兴趣的帖子后跟”(self.datascience)”。从逻辑上讲, 我们可以假设” self”是指Reddit根目录, “。datascience”是指subreddit。
太好了, 因此我们有一种方法可以识别哪些帖子是入站的, 哪些帖子是出站的。现在, 我们需要在DOM结构中进行标识。有没有一种方法可以通过搜索DOM结构中的标签, 类或ID在DOM中找到这些标识符?当然!
在网络浏览器中, 右键单击任何” self.datascience”链接, 然后单击”检查元素”。大多数现代的Web浏览器都具有此强大的工具, 可让Web开发人员动态地遍历网页源代码中有意义的部分。如果你使用的是Safari, 请确保已在Safari偏好设置中启用了开发者工具。你会看到一个窗格, 该窗格向下滚动到源代码中负责该[self.datascience]标识符的部分。
在窗格中, 你可以看到标识符实际上是由锚标记加载的。
<a href="/r/datascience">self.datascience</a>
但这是用span标签括起来的。
<span class="domain">
"("
<a href="/r/datascience/">self.datascience</a>
")"
</span>
这个span标记包含了我们在页面上看到的文本, 即”(self.datascience)”。如你所见, 它标有”域”类。你可以检查所有其他链接, 以查看它们是否遵循相同的格式, 并且确定是否足够;他们是这样。
获取页面
我们知道页面上想要的东西, 这一切都很好, 但是我们如何使用Python读取页面的内容?嗯, 它的工作方式几乎与人类从网络浏览器读取页面内容的方式相同。
首先, 我们需要使用”请求”库请求网页。
url = "https://old.reddit.com/r/datascience/"
# Headers to mimic a browser visit
headers = {'User-Agent': 'Mozilla/5.0'}
# Returns a requests.models.Response object
page = requests.get(url, headers=headers)
现在, 我们有了一个Response对象, 其中包含网页的原始文本。到目前为止, 我们对此无能为力。我们所拥有的只是一个巨大的字符串, 其中包含HTML文件的整个源代码。为此, 我们需要使用BeautifulSoup 4。
标头将使我们能够模仿浏览器的访问。由于对漫游器的响应与对浏览器的响应不同, 并且我们的参考点是浏览器, 因此最好获得浏览器的响应。
BeautifulSoup将允许我们通过搜索类, id或标签名称的任意组合来查找特定的标签。这是通过创建语法树来完成的, 但是语法树与我们的目标无关(不在本教程的讨论范围之内)。
因此, 让我们继续创建该语法树。
soup = BeautifulSoup(page.text, 'html.parser')
汤只是通过获取一串原始源代码创建的BeautifulSoup对象。请记住, 我们需要指定html解析器。这是因为BeautifulSoup也可以使用XML创建汤。
寻找我们的标签
我们知道我们想要什么标签(带有” domain”类的span标签), 并且有汤。接下来是遍历汤并找到这些标签的所有实例。你可能会因为使用BeautifulSoup这么简单而笑。
domains = soup.find_all("span", class_="domain")
我们刚刚做了什么?我们在汤对象上调用了” find_all”方法, 该方法将查找所有锚点标签, 并将这些参数作为第二个参数传入。我们只传递了一个约束(类必须是”域”), 但是我们可以将其与许多其他东西结合在一起, 甚至可以使用id。
我们使用” class_ =”, 因为” class”是Python保留的用于定义类等的关键字
如果要传递多个参数, 则要做的就是使第二个参数成为要包含的参数的字典, 如下所示:
soup.find_all("span", {"class": "domain", "height", "100px"})
我们的”域”列表中包含所有span标签, 但是我们想要的是”(self.datascience)”域。
for domain in domains:
if domain != "(self.datascience)":
continue
print(domain.text)
正确, 现在你应该会在页面上看到列表中的信息类型列表, 但不包括不是”(self.datascience)”信息的信息类型。但是我们基本上拥有了我们想要的一切。现在, 我们可以引用所有入站的帖子。
查找我们的信息
很棒, 我们正在印制两行”(self.datascience)”, 但是接下来呢?好吧, 请回想一下我们最初的目标。获取帖子标题, 作者, 喜欢和评论数。
为此, 我们必须返回浏览器。如果再次使检查器显示在我们的标识符上, 你会看到我们的span标签嵌套在div标签中……它本身嵌套在另一个div标签中, 依此类推。继续前进, 你将获得上一篇文章的父级div。
<div class=" thing id-t3_8qccuv even link self" ...>
...
<div class="entry unvoted">
<div class="top-matter">
<p class="title">
...
<span class="domain">
...
</p>
</div>
</div>
</div>
如你所见, 公共父级在DOM结构中位于其上方4个级别。省略号(…)表示不必要的颤动。但是, 我们现在需要的是对域列表中每个帖子的post div的引用。通过使用BeautifulSoup的方法, 我们可以找到汤中任何元素的父元素。
for domain in soup.find_all("span", class_="domain"):
if domain != "(self.datascience)":
continue
parent_div = domain.parent.parent.parent.parent
print(parent_div.text)
运行脚本将打印所有入站帖子的所有文本。但是你可能认为这看起来像是错误的代码。你说得对。仅依靠DOM的结构完整性永远是不安全的。这是一种需要不断更新的破解和斜线解决方案, 其代码等同于定时炸弹。
相反, 让我们看一下每个帖子的父级div, 看看是否可以将入站和出站链接与父级本身分开。每个父div都有一个名为”数据域”的属性, 其值正是我们想要的!所有入站帖子的数据域均设置为” self.datascience”。
如前所述, 我们可以使用BeautifulSoup搜索具有属性组合的标签。对我们来说幸运的是, Reddit选择将每个帖子的父div类别都用”事物”分类。
attrs = {'class': 'thing', 'data-domain': 'self.datascience'}
for post in soup.find_all('div', attrs=attrs):
print(post.attrs['data-domain'])
” attrs”变量是一个字典, 其中包含我们要搜索的属性及其值。这是一种更安全的寻找职位的方法, 因为它涉及的运动部件更少。由于我们已经在搜索时添加了’self.datascience’作为参数, 因此我们不必使用if语句来跳过任何迭代, 因为我们保证只接收带有’self.datascience’数据域的帖子属性。
现在我们有了所需的所有信息, 剩下的就是从孩子那里提取信息。顺便说一下, 这就像你认为的那样简单。
提取我们的信息
对于每个帖子, 我们需要4条信息。
- 标题
- 作者
- 喜欢
- 注释
如果我们看一下post div的结构, 我们可以找到所有这些信息。
标题
这是迄今为止最简单的一种。在每个post div中, 都有一个嵌套在div几层下的段落标签。它很容易找到, 因为它附加了” title”类。
title = post.find('p', class_="title").text
post对象位于我们之前的for循环中。它也是BeautifulSoup对象, 但它仅包含post div中的DOM结构。 find方法仅返回一个对象, 而find_all则返回满足条件的对象列表。找到标签后, 我们只想要包含标题的字符串, 因此我们使用它的text属性读取它。我们还可以使用” object.attrs [attribute]]来读取其他属性。
作者
这也相对简单, 请在帖子标题下使用任何作者的名字打开检查器。你会看到作者名称在分类为”作者”的锚标记中。
author = post.find('a', class_='author').text
评论
这需要一些额外的工作, 但仍然很简单。我们可以按照找到标题和作者的方式找到评论。
comments = post.find('a', class_='comments').text
运行此命令时, 你会看到类似” 49条评论”的信息。可以, 但是如果我们只知道这个号码, 那就更好了。为此, 我们需要使用更多的Python。
如果我们使用Python的” str.split()”函数, 它将返回字符串中所有元素的数组, 并用空格分隔。在我们的例子中, 我们将得到列表[[” 49″, ” comments”]’。太好了!现在, 我们要做的就是获取第一个元素并存储它, 我们要做的就是将函数附加到我们的线。
comments = post.find('a', class_='comments').text.split()[0]
但是我们仍然没有完成, 因为有时我们没有电话号码。我们得到”评论”。当帖子没有评论时, 就会发生这种情况。既然我们知道这一点, 我们要做的就是检查结果是否为” comments”, 并将其替换为” 0″。
if comments == "comment":
comments = 0
喜欢
找到喜欢的次数是小菜一碟, 与我们上面使用的逻辑相符。
likes = post.find("div", attrs={"class": "score likes"}).text
不过在这里, 你可能会注意到我们使用了两个类的组合。这仅仅是因为还有其他多个div类别为”得分”或”喜欢”的div, 但只有一个具有”得分”和”喜欢”的组合的div。
如果点赞的次数为0, 则我们得到0。但是, 如果帖子太新以至于没有点赞, 就会发生一些奇怪的事情。我们得到”•”。让我们将其替换为”无”, 这样就不会在最终结果中造成混淆。
if likes == "•":
likes = "None"
将结果写入CSV
到目前为止, 我们已经有了循环, 可以提取网页上每个帖子的标题, 作者, 喜欢和评论。 Python有一个很棒的内置模块, 可以按照pythonic的方式写入和读取名为” csv”的CSV文件:保持简单。无论如何, 我们要做的就是在循环块的末尾添加一行, 以将帖子的详细信息附加到CSV文件中。
counter = 1
for post in posts:
...
post_line = [counter, title, author, likes, comments]
with open('output.csv', 'a') as f:
writer = csv.writer(f)
writer.writerow(post_line)
counter += 1
很简单吧? post_line用于创建一个数组, 该数组存储需要用逗号分隔的元素。 counter变量用于跟踪我们记录了多少帖子。
移至下一页
由于我们正在计算要存储的帖子数, 因此我们要做的就是将整个逻辑封装在另一个循环中, 该循环请求新页面, 直到记录了一定数量的帖子为止。
counter = 1
while (counter <= 100):
for post in posts:
# Getting the title, author, ...
# Writing to CSV and incrementing counter...
next_button = soup.find("span", class_="next-button")
next_page_link = next_button.find("a").attrs['href']
time.sleep(2)
page = requests.get(next_page_link, headers=headers)
soup = BeautifulSoup(page.text, 'html.parser')
我们只是做了很多事情, 不是吗?让我们一次看看它。首先, 我们将计数器变量上移了一个块, 以便while循环可以使用它。
接下来, 我们知道for循环的作用, 但其他循环又如何。 ” next_button”变量用于查找和存储” next”按钮。之后, 我们可以在其中找到锚标记并获取’href’属性;我们将其存储在” next_page_link”中。
我们可以使用此链接请求下一页并将其存储回”页面”中, 并使用BeautifulSoup制作另一种汤。
上面的代码段在100条后停止, 但你可以在任意数量的帖子后停止它。
负责任地刮
我们上面没有谈论的那一行是” time.sleep(2)”。那是因为该行应有其自己的部分。任何网络服务器的资源都是有限的, 因此我们有责任确保不会耗尽所有资源。它不仅使你被禁止在几秒钟内请求数百个页面, 而且也不好。
重要的是要记住, 即使允许你抓取它们, 网站也非常不错, 因为如果他们愿意, 他们可以在前10到20个请求中检测到漫游器, 甚至可以根据Python发送的请求对象来捕获你。它们非常好用, 它们使你无需旋转IP即可抓取, 因此你应该为它们提供降低机器人速度的服务。
你可以通过查看网站的robots.txt文件来查找网站的抓取策略。通常可以在网站的根目录(例如http://www.reddit.com/robots.txt)找到该文件。
接下来是什么?
好吧, 无论你想要什么。正如我们刚刚证明的那样, 你在网络上看到的所有内容都可以在本地抓取和存储。你可以将我们刚刚获得的信息用于多种目的。仅凭我们发现的四点信息, 我们可以得出很多结论。
通过进一步分析我们的信息, 我们可以确定哪种帖子最受喜欢, 它们包含的单词以及发布者。或者相反, 我们可以通过分析喜欢与评论的比率来找到一个有争议的计算器。
考虑一个收到很多评论但没有喜欢的帖子。这最有可能意味着该帖子包含了引起人们共鸣的内容, 但不一定是他们喜欢的内容。
可能性很多, 由你自己决定如何使用这些信息。
相关课程
如果你想了解有关Python的更多信息, 请查阅srcmini的以下课程:
数据科学Python简介
用Python导入数据(第1部分)
评论前必须登录!
注册