{T}

前文学习了如何通过 Python 下载网页内容,并保存为 html 文件。但保存下来的 html 文件还包括很多不需要的内容(复杂的代码标签等),需要从中提取感兴趣的内容,比如电视剧的名字或新闻标题等。

本文介绍如何通过 Python 从网页中获取感兴趣的数据。

HTML 基础

前文已知网页是一种由 HTML 规则标记的文本文件。要从 HTML 中过滤信息,首先需要了解 HTML 标记规则的基础知识。

HTML 标签基础

HTML 页面由很多基本的元素组合而成,这种元素一般叫作标签。形式如下:

code
<标签名> 网页内容 </标签名>

主要分为三个部分。

  • 开始标记:就是上面的 <标签名> 部分,代表接下来的内容是一个标签元素。
  • 标签内容:就是上面的网页内容部分,指的是在这个标签内部的网页内容,标签内部内容的样式会受到标签的影响。
  • 结束标记:也就是上面的 </标签名> 部分,代表这个标签已经结束。

HTML 正是通过各种各样不同的标签来把普通的文字内容变成丰富多彩的网页。

标签是如何对文本的格式产生影响的?下面用一个例子来说明。为了便于理解,这里的标签不是真实的 HTML 标签,不过工作原理是差不多的。

例如,展示一行文字:“今天星期三,天气晴”,但为了醒目希望将其颜色变为红色。可以增加红色的标签,如:

code
<red>今天星期三,天气晴</red>

<red> 就是开始标记,代表接下来是一个 red 标签;“今天星期三,天气晴”则是 red 标签内部的网页内容,会被 red 标签所影响(文字变为红色)。</red> 为结束标记,代表之后的文字不再是红色。

另外,强调天气晴,希望把这三个字加粗,可以把这三个字放在粗体的标签里。

code
<red>今天星期三,<bold>天气晴</bold></red>

bold 标签在 red 标签包含的内容里。这说明了标签的一个重要特性,即可以嵌套存在。简单地说,就是一个标签包含的内容里可以包含其他的标签。在上面的例子中,把 bold 标签叫作 red 标签的子标签,因为它被 red 标签包住。

现在,将这句话居中显示,可以把整段内容放在用来居中的标签中。为了方便查看,将开始标记、标签内容和结束标记分别放在新的行。

如下所示:

code
<center>
  <red>
    今天星期三,
    <bold>
      天气晴
    </bold>
  </red>
</center>

HTML 中,是否换行只是为了标签的美观,并不影响结果的正确性。

现在,回头来看之前下载的煎蛋网的主页。

用 VS Code 打开 chapter07 文件夹,并打开 jiandan.html,搜索“普通感冒”(由于更新有延迟,可使用第 07 节的暗号,即第 07 节看到的第一行标题)定位到新闻区域。如下图所示。

可以看到,来自真实世界的网页基本都是一个个标签互相嵌套而已(红框部分画出了一部分)。下一节将学习 HTML 标签的几个主要组成部分。

HTML 标签的属性

查看 jiandan.html,会发现标签与之前介绍的不太一样,具体表现为开始标签的尖括号内除了标签名,还多了一些内容。这个部分的内容叫作:标签的属性。

具体的形式如下所示:

一个标签里面可能会有一个或多个属性,每个属性的形式都是属性名 = 属性值这样的形式。

为什么光用标签还不够,还需要标签的属性?举个例子:比如给网页中的文字设定不同的字号(字体大小)。不能为每个字号都设置一个标签,而是设计一个“整合”的字体标签,把字号作为字体标签的属性来实现。比如这是一段 18 号大小的字:

code
<font size="18">这是一段18号大小的字</font>

常见标签属性

HTML 标签的属性是学习爬虫的重点之一。本文的核心任务是从一堆标签中选取出所需的内容,主要方法就是通过标签名搭配标签属性。有的属性是某个标签特有的,比如 font 标签的 size 属性。有的属性是所有标签都有的,最常见的是 id 和 class 这两个属性。

(1)id 属性

id 顾名思义,一般代表标签的“名字”,类似身份证号。比如下图中,div 标签的 id 为 wrapper,下面一个 div 标签的 id 则叫 header。虽然 HTML 的标准比较宽松,不是每个标签都会有 id 属性,而且 id 的值也可能会有重复,但 id 属性仍然是抓取内容时比较重要的参考。

(2)class 属性

class 属性代表标签的类别。HTML 有一套机制可以创建一组样式,并给这组样式起一个名字,比如 style_a。只要有标签需要用到这组样式,只需将 class 属性的值写为 style_a,这组样式就会对该标签生效。编写爬虫不需要关心这套流程的具体实现,只需要知道 class 是所有标签都有的属性即可。

如下图所示:箭头所指的 div 标签的 class 为 logo,代表这个页面的某个地方定义了 logo 这个样式组。

至此,已学完 HTML 的基本要素,总结如下:

  • HTML 是由一个个标签组成的;
  • 标签可以嵌套,即每个标签内部可以有其他标签,里面的标签也叫作外面标签的子标签;
  • 在标签的开始标记中,可以指定标签的属性;
  • id 和 class 属性是使用最广泛的属性,每个标签都有这两个属性。

使用 BeautifulSoup 提取内容

BeautifulSoup 是一个用于分析 HTML 的 Python 库。本文通过使用 BeautifulSoup 从上一节课下载到的 html 文件 jiandan.html 中提取所有新闻的标题。

首先,在工作目录新建 chapter08,然后把 chapter07 目录中下载的 jiandan.html 拷贝过来。

打开 VS Code,点击文件 → 打开文件夹,打开刚才创建的 chapter08 文件夹。打开后应该可以在 VS Code 中看到 jiandan.html。

然后新建一个 Notebook,保存为 chapter08.ipynb。接下来将在这个 notebook 中对 jiandan.html 进行内容提取。

因为煎蛋网每天都在更新,如果希望使用和本文完全一样的 jiandan.html,可以直接去这里下载

安装 BeautifulSoup

与之前安装 selenium 的方式类似,去开始菜单 → Anaconda,选择 Anaconda Prompt,出现命令行界面。苹果系统(Mac)搜索终端即可。

在命令行界面,输入如下的安装命令(BeautifulSoup 的 Python 包名为 bs4,简写):

code
conda install bs4

回车执行,之后在询问是否确认时输入 y 回车,等待安装完毕,关闭即可。

读取 html 文件到 Python

数据提取的第一步,首先需要将 html 文件加载到 Python 的变量中。前文学习了通过文件对象把 Python 变量写进文件里,这里用类似的代码将文件中的内容读出来。

代码如下,可以看到与写入文件的代码非常类似:

code
# 打开 jiandan.html,第二个参数 r,代表 read
# open 函数返回一个文件对象,保存在变量 fo 中
fo = open("jiandan.html","r",encoding="utf-8")
# 调用文件对象的 read 函数,该函数将文件的内容读取到 Python 中
# read 函数的返回值就是文件内容,保存在 html_content 中
html_content = fo.read()
# 关闭文件对象
fo.close()
# 打印 html_content 变量,看内容是否被正确加载
print(html_content)

按 shift + enter 执行之后,输出结果如下所示(打印的 html 文本太长,这里仅截图示意)。

创建 BeautifulSoup 对象

此时已将文件的内容读了出来,但目前还是以一个超大的字符串的形式存储在变量中。想要对其进行有效分析,第一步先构造一个 BeautifulSoup 的对象。代码如下:

code
# 从 bs4 模块中导入 BeautifulSoup 类
from bs4 import BeautifulSoup
# 创建一个 BeautifulSoup 的对象,并将存储网页内容的变量作为参数
doc = BeautifulSoup(html_content)
# 打印 doc 对象的 title 属性。
print(doc.title)

执行上述代码之后,BeautifulSoup 对象就被创建并存在变量 doc 中。为了测试是否创建成功,打印了 doc 对象的 title 属性,输出如下:

code
<title>
煎蛋 - 地球上没有新鲜事
</title>

BeautifulSoup 对象的基本使用

刚才打印了 doc 对象的 title 属性来测试对象是否创建成功。从打印的内容来看,doc 对象的 title 属性对应了网页的 title 标签。但刚才打印的内容中,title 标签也被打印出来了。

那么是否可以只取标签里面的内容呢?

(1)get_text 方法

在 BeautifulSoup 中,通过标签对象的 get_text 方法获得标签中的内容。

code
# 将 title 属性保存在 title_label 的变量中
title_label = doc.title
# 调用 get_text 方法,并把返回值打印出来
print(title_label.get_text())

输出如下:

code
煎蛋 - 地球上没有新鲜事

可以看到,这次成功将标签内的文字提取了出来。

(2)find_all 方法

BeautifulSoup 对象另一个常用方法是 find_all,用来在 html 文档中查找特定标签名以及特定属性值的标签,下面举例说明。

在上面学习 html 标签的 class 属性时,截取了一张图:

这个截图同样来自 jiandan.html 文件,而目前 doc 对象已经包含了这些内容。现在尝试提取 class 等于 logo 的这个 div 标签,代码如下:

code
# find_all 是 BeautifulSoup 对象的常用方法,用于查找特定的标签
# 接受三个参数,第一个是要查找的标签名,第二个则是要匹配的属性名
# 比如这里,查找的是 class=logo 标签,则直接按如下写法即可。
# 因为 class 是 Python 的关键字,所以这里需要使用 class_ 以示区分
# 最终,find_all 函数返回所有满足条件的标签列表
logo_label = doc.find_all("div", class_="logo")
print(logo_label)

输出结果为:

code
[<div>
<h1><a href="/" onclick="ga('send', 'pageview','/header/logo');">煎蛋</a></h1>
</div>]

可以看到,截图中箭头所指的 div 标签被成功过滤了出来。

(3)获取标签对象的属性

通过标签对象的 attrs 属性,获取标签对象的属性的值。attrs 是一个字典,获取属性的值的方法如下:

code
# 取 logo_label 列表的第一个标签对象
label = logo_label[0]
# 打印这个对象的 class 属性
print(label.attrs["class"])

输出:

code
['logo']

过滤出煎蛋的新闻列表

下面使用之前的 doc 对象来过滤出新闻的标题列表。

(1)观察 html,提取初步的新闻标签列表

在使用 BeautifulSoup 进行内容过滤的第一步,首先要查看所需内容所在的标签。在 VS Code 中打开 jiandan.html,搜索“普通感冒”(暗号,在第 07 节看到的第一行标题关键词)到达新闻标题内容区域,如下图所示。

通过观察截图中的标签内容,可以发现每个新闻标题似乎都有一个对应的 div 标签,并且它的 class 是 indexs。沿着这个思路,先考虑使用 find_all 将其过滤出来。通过如下代码:

code
# 查找 class 为 indexs 的所有 div 标签,存储在 index_labels 中
index_labels = doc.find_all("div", class_="indexs")
# 打印符合条件的 div 标签的数量
print(len(index_labels))

输出:

code
24

说明找到了 24 个符合条件的 div 标签。一个网页的新闻也是 20 多条,说明有希望。

(2)提取单个标签的新闻标题

因为 index_labels 是一个标签对象的列表,距离过滤出新闻标题还有距离。下一步,分析如何从这些标签对象中抽取新闻标题。

首先,先取第一个来看一下。

code
first_object = index_labels[0]
print(first_object)

输出如下:

code
<div>
<span title="1小时 ago">8</span>
<h2><a href="http://jandan.net/p/108693" target="_blank">引发普通感冒的鼻病毒会将新冠病毒排挤出细胞!</a></h2>
<div><a href="http://jandan.net/p/author/majer">majer</a> · <strong><a href="http://jandan.net/p/tag/%e6%96%b0%e5%86%a0%e7%97%85%e6%af%92" rel="tag">新冠病毒</a></strong></div>
换而言之,如果你感冒了,在那段期间就不会被新冠病毒感染 </div>

现在问题转化为,如何从上述 html 中抽取出新闻的标题。

通过查看上述内容可以发现,标题在第三行,在一个 a 标签内部。那么只需过滤出这个 a 标签,再使用 get_text 拿到里面的内容即可。

如何过滤出第三行的 a 标签?可以看到这个 a 标签里有 target="_blank" 这样一个属性,可以使用 find_all 将其过滤出来。

code
# 从刚才的 first_object 标签对象中过滤出所有 target=_blank 的 a 标签
a_labels = first_object.find_all("a",target="_blank")
# 取第一个标签对象
my_label = a_labels[0]
# 打印该标签对象内的文字内容
print(my_label.get_text())

输出如下:

code
引发普通感冒的鼻病毒会将新冠病毒排挤出细胞!

新闻的标题被成功抽取了出来。

(3)提取新闻标题的列表

目前,已经实现了从第一个标签对象中提取新闻标题,但列表中有 24 个标签对象。可以将“从标签对象抽取标题”这段逻辑写成一个函数,然后通过一个循环对列表中的每个标签对象都调用这个函数。

代码如下:

code
# 从第一次 find_all 获取的标签对象中抽取标题
def get_title(label_object):
    # 从刚才的参数传入的标签对象中过滤出所有 target=_blank 的 a 标签
    a_labels = label_object.find_all("a",target="_blank")
    # 取第一个标签对象
    my_label = a_labels[0]
    # 将标签的文字内容作为返回值返回
    return my_label.get_text()
# 用循环针对每个 index_labels 列表中的标签对象
# 都将其作为参数传入 get_title 函数
# 并打印返回值
for i in range(0,len(index_labels)):
    title = get_title(index_labels[i])
    print(title)

执行后输出:

code
引发普通感冒的鼻病毒会将新冠病毒排挤出细胞!
无厘头研究:植入虚假的记忆再抹去它们
什么是仇恨犯罪?
突发:LHCb发现了违背标准模型的现象
今日带货 20210324
舌战裸猿:IBM搞出了可以打辩论赛的AI
大吐槽:「我没醉,醉的是世界」
今年世界总发电量的0.6%被用于挖比特币
接种疫苗后还是感染新冠?不要为此惊讶
今日带货:蛋友家的血橙
科学家首次在野外检测到抗多药的超级真菌
未在iPhone12盒中搭配充电器,苹果被巴西消协罚200万美元
工程师将解决城市陷坑的问题
今日带货:粉面专场
科学家在碟子里培育出了泪腺,并让它哭泣
疯狂实验进行时:把志愿者禁闭在黑暗的空间里40天
今日带货 20210321
我们已向外星人发送了哪些消息?
脑力小体操:石头+剪刀 VS 石头+布
发霉啦:今天,我终于向母亲摊牌了
普渡大学的经济学家计算出世界各地幸福的价格
人类首次观察到木星上极光黎明风暴的成形过程
为女儿出头,母亲编辑假裸照败坏高中啦啦队队员的名誉
今日带货:淘宝京东蛋友推荐

可以看到,jiandan.html 中的所有新闻标题被成功过滤了出来。至此,完成了使用 BeautifulSoup 对网页进行内容的提取。

小结

本文主要介绍了以下内容。

(1)HTML 基础

  • HTML 页面是由一个个 HTML 标签组成的。
  • HTML 标签分为开始标记、结束标记以及中间的内容三部分。每个标签内部可以嵌套其他的标签,也可以是普通的文本。
  • HTML 标签的开始标记中可以写标签的属性,id 和 class 是所有标签都有的、最常用的属性。

(2)BeautifulSoup 的使用

  • 首先需要将 html 文件读到 Python 中,再用存储着文件内容的变量构造 BeautifulSoup 对象。
  • 可以使用 BeautifulSoup 对象的 find_all 方法,找到标签名和属性值都符合条件的标签对象,比如 doc.find_all('div', class_="logo") 代表找到标签名为 div,class 属性为 logo 的标签。
  • 标签对象都有 get_text 方法,用于返回标签内部的文本。

(3)网页数据抓取的分析方法

  • 观察想要过滤内容所在的标签特征,过滤出包含文本的标签,然后调用 get_text 拿到文本内容。
  • 如果层次关系比较复杂,可以分步走,一步一步过滤出最终的内容。比如例子中,首先过滤了 class=indexs 的 div 标签,再从其中过滤出包含标题的 a 标签。

课后练习:

在课程代码的基础上,编写代码,提取出新闻的时间内容,如下图红框中部分。

提示:需要用到标签对象的 attrs 属性。


答案:

code
comments = doc.find_all("span",class_="comment-link")
for i in range(0,len(comments)):
    comment = comments[i]
    print(comment.attrs["title"])

输出

code
1小时 ago
4小时 ago
8小时 ago
12小时 ago
14小时 ago
23小时 ago
1天 ago
1天 ago
1天 ago
2天 ago
2天 ago
2天 ago
2天 ago
3天 ago
3天 ago
3天 ago
4天 ago
4天 ago
4天 ago
5天 ago
5天 ago
5天 ago
5天 ago
6天 ago

版本差异(爬虫技术栈 → 当前版本)

本文编写时当前稳定版
requests2.28/2.312.32.x
Scrapy1.x/2.02.11.x(API 稳定)
httpx0.240.28.x
Playwright1.3x1.6x(Python 版)
lxml/BeautifulSoup旧版保持稳定
Python3.8-3.123.14(推荐)

本文讲解的爬虫原理(HTTP、解析、反爬、存储)与核心 API 在最新版本中成立;注意 Python 3.9 及以下已 EOL,新项目使用 3.13/3.14。