爬虫 | Beautiful Soup 初识

发布时间 2023-07-17 22:24:25作者: 张Zong在修行

本博客将学习用 Beautiful Soup 库来实现数据抓取。将会通过爬取世界大学校园排名的数据来讲解 Beautiful Soup 库的基础知识。它包括如何用 Beautiful Soup 库的解析器去解析页面内容、如何遍历和搜索标签树、如何提取出关键的数据并保存到列表或者字典里。

Beautiful Soup 简介

Beautiful Soup 库简单来说,就是一个可以从 HTML 或 XML 文件中提取数据的 Python 库。本次实验中,第一个例子是在 最好大学网上去爬取 2019 年软科类世界大学排名的数据。爬取的内容如下图所示:

获取 HTML 页面

我们要爬取上图中的三列数据,世界排名、学校名字和总分。首先我们用 Chrome 浏览器去访问这个页面。然后使用快捷键 F12 打开开发者工具来观察该页面中我们所要爬取数据的标签结构。

把鼠标移到要定位代码的数据上,右键鼠标 检查 就能定位到相应的代码,如下图所示。从下图可以看到一所大学的信息是存储在表格的一行里的。 我们需要获取每一行的第 1 列、第 2 列 和第 5 列的数据。

使用调试工具观察标签结构后,不难发现,我们想要获取的数据都是存储在表格中的,如下图所示。在本实验中最主要的任务就是要学会如何去遍历 tbody 标签的孩子标签,获取 tr 标签里 td 标签的数据。

确定好目标之后,接下来使用 requests.get() 来获取整个页面的内容。用 import requests 来导入 Requests 库。Requests 库简单来说,就是自动提交网络请求,爬取 HTML 页面。

获取世界大学排名的页面信息

import requests

r = requests.get("https://labfile.oss.aliyuncs.com/courses/2184/2019%E8%BD%AF%E7%A7%91%E4%B8%96%E7%95%8C%E5%A4%A7%E5%AD%A6%E5%AD%A6%E6%9C%AF%E6%8E%92%E5%90%8D.html")  # get 请求指定的页面信息
# print(r.status_code) # 检查状态码是否正确,状态为 200,说明访问成功
# 200

使用 r.encoding 来查看该页面的编码形式,如果页面的编码不是 utf-8,我们需要转换编码形式,以保证获取的数据不会发生乱码。

# print(r.encoding)  # 查看编码形式
# ISO-8859-1
r.encoding = r.apparent_encoding  # 转换成 utf-8 的编码
# print(r.encoding)
# utf-8

使用 r.text 来获取整个页面内容,使用 r.headers 来获取页面的头信息。

html = r.text  # 获取页面内容
# print(html)

解析 HTML 页面

Beautiful Soup 库有专门的 解析器。解析器的作用简单来说,就是用来解析和提取 HTML 或者 XML 页面的数据。它有四种解析器:

  • Python标准库
  • lxml HTML 解析器
  • lxml XML 解析器
  • html5lib

本次实验里使用的是 Python 标准库的解析器,只要安装了 Beautiful Soup 4,便可以使用,而另外三种解析器需要安装后才能使用。Python 标准库解析器就是 bs4 的 HTML 解析器,后续我们要用 bs4 去提取标签中的指定元素。

使用 from bs4 import BeautifulSoup 导入 BeautifulSoup 库。使用 BeautifulSoup(markup, "html.parser") 去解析页面。

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")  # 解析世界大学排名的页面
# print(soup)

遍历和搜索文档树

解析 HTML 页面之后,我们就可以利用 Beautiful Soup 库中的函数去遍历标签树。所谓的标签树,就是指 HTML 的标签之间具有一种层次关系,而这种关系可以用树形图的结构来表示,从下图可以看出标签之间的层次结构。

根据上图标签之间的层次结构可以画出一个标签树,如下图所示。

每个标签都有一个名字,每个标签可以有 0 个或者多个属性,标签之间的字符串是非属性字符串或者注释,如下图所示。

我们的实验任务是要获取页面上的三类数据,分别是学校排名、学校名字和学校总分。所以,我们需要遍历标签树,搜索到这三类数据并保存到列表中。从上面的内容我们已经知道,所需要的数据是在 tbody 的子标签里,所以我们需要使用 find() 去遍历 tbody 标签的孩子标签。

# 遍历搜索 `tbody` 标签的孩子节点,返回的是一个迭代器
data = soup.find("tbody").children
# print(data)
# <list_iterator object at 0x0000023F5F413880>

上面的代码中,find() 是搜索文档树中的一种方法,它能够返回第一个匹配的元素。它的完整参数形式为:find( name , attrs , recursive , string , **kwargs )。参数 name 是用标签名去检索字符串,参数 attrs 是用标签属性值去检索字符串,参数 recursive 是否对子孙全部检索,默认为 True,参数 string 是检索标签中的非属性字符串。

.children 是标签下行遍历的一种属性。标签一共有三种遍历形式,分别是下行遍历、上行遍历和平行遍历。

下行遍历有以下三个属性:

  • .children 是循环遍历儿子节点
  • .descendants 是循环遍历所有子孙节点
  • .contents 是将该标签中所有儿子节点存入列表

上行遍历有以下两个属性:

  • .parent 是访问该节点的父节点标签
  • .parents 是循环遍历所有先辈节点

平行遍历有以下四个属性:

  • .next_sibling 是返回下一个平行节点的标签
  • .previous_sibling 是返回上一个平行节点的标签
  • .next_siblings 是返回后面所有平行节点的标签
  • .previous_siblings 是返回前面所有平行节点的标签

现在我们已经获取了 tbody 标签中所有孩子标签了,由于返回的是一个迭代器,你可以使用 list(data) 以列表的形式输出看一看获取到的数据结果。我们这里直接使用 for 语句去遍历 tr 标签。

# for tr in data:
#     print(tr)
'''
# 一条数据
<tr class="alt"><td>1</td><td class="align-left">
<a href="World-University-Rankings/Harvard-University.html" target="_blank">哈佛大学</a></td>
<td><a href="World-University-Rankings-2019/USA.html" target="_blank" title="查看美国大学排名">
<img src="image/flag/USA.png"/></a></td><td class="hidden-xs">1</td>
<td>100.0</td>
<td class="hidden-xs need-hidden alumni">100.0</td>
<td class="hidden-xs need-hidden award" style="display:none;">100.0</td>
<td class="hidden-xs need-hidden hici" style="display:none;">100.0</td>
<td class="hidden-xs need-hidden ns" style="display:none;">100.0</td>
<td class="hidden-xs need-hidden pub" style="display:none;">100.0</td>
<td class="hidden-xs need-hidden pcp" style="display:none;">78.2</td>
</tr>
'''

运行结果后,可以看到每一对 tr 标签都被遍历了,但是我们需要的td标签中的非属性字符串。这里使用 isinstance(tr, bs4.element.Tag) 去判断 tr 是否是 bs4 标签类型的元素。再用搜索文档树的另一种方法 find_all() 查询到所有td标签并保存到列表 tds 里。

import bs4
info = []  # 定义一个列表去保存最后需要的数据
for tr in soup.find('tbody').children:
    # 用 isinstance() 函数来判断一个对象是否是一个已知的类型
    if isinstance(tr, bs4.element.Tag):  # 判断 tr 是否是 bs4 标签类型的元素
        tds = tr.find_all('td')  # 把 tr 标签中的所有 td 标签的内容存储在列表 tds
        # 把排名、大学名字、总分放入 info 列表
        info.append([tds[0].string, tds[1].string, tds[4].string])
print(info)
# [['1', '哈佛大学', '100.0'], ['2', '斯坦福大学', '75.1'], ['3', '剑桥大学', '72.3'], ['4', '麻省理工学院', '69.0'], ['5', '加州大学-伯克利', '67.9'], ['6', '普林斯顿大学', '60.0'], ['7', '牛津大学', '59.7'], ['8', '哥伦比亚大学', '59.1'], ['9', '加州理工学院', '58.6'], ['10', '芝加哥大学', '55.1'], ['11', '加州大学-洛杉矶', '50.8'], ['11', '耶鲁大学', '50.8'], ['13', '康奈尔大学', '49.8'], ['14', '华盛顿大学-西雅图', '48.7'], ['15', '伦敦大学学院', '47.9'], ['16', '约翰霍普金斯大学', '47.6'], ['17', '宾夕法尼亚大学', '47.3'], ['18', '加州大学-圣地亚哥', '47.1'], ['19', '苏黎世联邦理工学院', '46.1'], ['20', '加州大学-旧金山', '42.2'], ['20', '密歇根大学-安娜堡', '42.2'], ['22', '华盛顿大学-圣路易斯', '41.7'], ['23', '帝国理工学院', '41.6'], ['24', '多伦多大学', '41.4'], ['25', '东京大学', '40.7'], ['26', '哥本哈根大学', '40.2'], ['27', '威斯康星大学-麦迪逊', '38.8'], ['28', '杜克大学', '38.6'], ['29', '西北大学(埃文斯顿)', '38.4'], ['30', '纽约大学', '38.1'], ['31', '爱丁堡大学', '37.4'], ['32', '京都大学', '37.0'], ['33', '曼彻斯特大学', '36.9'], ['33', '北卡罗来纳大学-教堂山', '36.9'], ['35', '洛克菲勒大学', '36.2'], ['35', '英属哥伦比亚大学', '36.2']...]

find_all() 它能够查询所有符合条件的元素,以列表的形式返回查询结果。它的完整参数形式与 find() 相同,这里就不在赘述了,若忘记,请回看以上内容。现在所有的 td 标签都保存在 tds 里面,我们用 tag.string 去取出标签中的非属性字符串。这是 Tag 标签的基本属性之一,称为 NavigableString