总述

2020年7月22日。今天主要讲的内容有:

  • 使用Requests包和BeautifulSoup4进行简单的网页爬虫

爬虫

网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

我们知道,在WWW上我们访问的网页基本上都是用HTML文档来进行描述的。在浏览器按下F12键,可以打开开发者工具,选择Elements页签,即可查看HTML的文档内容。如图所示:

我们就可以从HTML中获取想要的数据/信息。

如果我们要进行对某个网页的爬虫,需要解决的问题有两个:

  • 获取HTML文档的内容
  • 解析这个HTML文档,按照一定规则获取其中的信息/数据

对于上面两个问题,进行针对性的解决:

  • 用Requests包来获取HTML文档
  • 用BeautifulSoup4来解析HTML文档

Requests


在线文档

简单介绍

引用以下来自官方文档的内容:

Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。
警告:非专业使用其他 HTTP 库会导致危险的副作用,包括:安全缺陷症、冗余代码症、重新发明轮子症、啃文档症、抑郁、头疼、甚至死亡。
Requests 允许你发送纯天然,植物饲养的 HTTP/1.1 请求,无需手工劳动。你不需要手动为 URL 添加查询字串,也不需要对 POST 数据进行表单编码。Keep-alive 和 HTTP 连接池的功能是 100% 自动化的,一切动力都来自于根植在 Requests 内部的 urllib3。

简单使用

如果你还没有安装Requests包,可以在命令行使用如下命令进行安装。

1
pip install requests

安装完毕后,键入如下代码:

1
2
3
import requests
response = requests.get('https://www.baidu.com')
print(response.status_code) #HTTP状态码

这是以GET请求方式来访问https://www.baidu.com,然后返回一个HTTP状态码。

复习一下一些常见的HTTP状态码

200 请求成功

301/302 重定向

403 禁止访问 404 找不到资源 405 Method not allow

500 服务器问题 502 Bad Gateway 网关错误

上图返回的代码是200,即请求成功了。
请求成功后,可以获取一些其他信息。读者可以自行尝试输出查看内容。
这里主要说明response.textresponse.content
前者这个属性,获取了HTML文档,就解决了我们在前文提出的爬虫的第一个问题。
后者主要被用于存储图片和文件,例如访问的URL为https://zoruasama.gitee.io/img/zorua.png

1
2
3
4
5
print(response.text) #获取HTML文档
print(response.url) #获取请求的URL
print(response.headers) #获取请求头
print(response.cookies) #获取cookies # cookies和session的概念
print(response.content) #以字节流返回数据 存图片或文件用

如图所示是response.text返回的字符串内容。

接下来说明如何使用BeautifulSoup4来获取HTML文档中的信息。

BeautifulSoup4

简单介绍

在线文档
Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。Beautiful Soup会帮你节省数小时甚至数天的工作时间。
HTML 文档本身是结构化的文本,有一定的规则,通过它的结构可以简化信息提取。于是,就有了lxml、pyquery、BeautifulSoup等网页信息提取库。一般我们会用这些库来提取网页信息。
在下文中BeautifulSoup4将被简称为BS4。

简单使用

我们引用官方的例子来介绍如何使用BS4来进行工作。
先安装BS4,在命令行键入如下语句:

1
pip install beautifulsoup4

假如我们已经使用某种方式获取到了某个网页的HTML文档(例如上文介绍的Requests),然后将这个文档字符串存储在html_doc这个变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

而后从bs4中引入BeautifulSoup,解析上面的文档,就能得到一个BeautifulSoup对象,并将之赋值给soup变量。

1
2
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser') #参数1是解析的文档内容, 参数2是选择的解析器

使用对象的prettify()方法可以以标准缩进的形式呈现soup的HTML文档内容。
以下的内容需要一定的HTML基础。需要知道什么是标签IDClass等知识。
由于这不是本文主要内容,因此不再这里赘述,只会在下文简要说明。

我们再来看看关于BeautifulSoup对象的其他方法:

1
2
3
4
print(soup.title)        #获取标题标签
print(soup.title.text) #获取内容
print(soup.title.attrs) #获取属性
print(soup.title.get('class')) #获取属性的值

接下来来解释一下上面这些名词。

soup.title

我们可以观察一下被解析的这个HTML文档,里面有一行:<title>The Dormouse's story</title>,两边用title标签裹着的。
因此soup.title就是在原先的整个文档中截取了这一部分
同理,如果选择soup.body,那么就是输出body标签裹着的内容。

进一步再选择soup.body.p,这会输出body标签下的p标签,而p标签有多个,所以只会选择第一个并输出。

soup.title.text

虽然本节的标题是soup.title.text,但是text属性并不是title独有的,这一点在接下来的属性或方法介绍也是如此。只要是BeautifulSoup对象,都有这些方法

.text属性是用于获取标签内部的“正文”,如下图的橙色部分
它没有在< >这对括号内,所以把它视为“正文”。

上面这段字符串,实际是通过soup.body.a来获取的。
接下来获取其中的“正文”。

soup.title.attrs

在上文演示这个方法时,输出的是一个空字典。
这是因为soup.title并不具备属性。在本节我们拿上一节的soup.body.a作演示。

可以看到,在< >这对括号里的内容,以字典的形式被返回了。如果读者还没有遗忘如何从字典从取值的方法的话,很容易可以联想到:

可以这样做,但是事实上,BS4还提供了一个优雅的做法:

soup.title.get(‘class’)

接续上节,获取< >内的内容,还可以使用本节标题的形式。
例如:soup.body.a.get('href')

查找标签

大多数情况下,上述的简单使用总是只能获取到第一个匹配项,显得有些鸡肋、不够用。
而且对于标签的选择总是不够精准。
因此我们在这里介绍查找标签的方法。查找标签的方法主要有两个:.find().findall()
前者也是查找第一个满足条件的标签并以字符串的形式返回,后者是查找所有满足条件的标签并以列表的形式返回。
除此以外,这两个方法在参数上大同小异。

  • name,标签的名字,例如p,a,title等。
  • **kwargs,利用属性键值对来进行过滤。如idclass_等。
  • 其他的参数不再作解释。读者若感兴趣可以自行查阅文档。

查找指定id的标签

我们再次使用上面的HTML文档为例进行演示。

为了获取其中标为橙色的内容,我们可以soup.find('a',id="link2")

查找指定class的标签

为了获取其中所有绿色的内容,我们可以soup.find_all('a',class_='sister')

返回的内容是一个含有三个元素的列表

CSS选择器

如果您对CSS有了解的话,您肯定对 id 和 class 选择器不陌生。
使用soup.select()方法就可以像CSS的规则一样进行选择。
此处不再赘述。
例如

1
2
3
print(soup.select('a'))
print(soup.select('.sister')) # class是用 . 号
print(soup.select('#link1')) # id是用 # 号

实战演练

爬取百度贴吧帖子中的图片并下载

首先,我们先找一个百度贴吧帖子作为目标。
笔者选择了火影忍者吧的这一帖子作为目标 链接

获取HTML文档

先使用requests获取文档。

1
2
3
4
import requests
html = requests.get('https://tieba.baidu.com/p/6970148254')
print(html.status_code) # 返回200即为请求成功
print(html.text) # html.text 即为我们想要的HTML文档

用BeautifulSoup解析

1
2
from bs4 import BeautifulSoup
soup = BeautifulSoup(html.text,'html.parser')

对网页元素进行选择

为了方便对HTML文档的阅读,我们使用浏览器的F12工具进行阅读。这个工具只需在浏览器中按下F12键即可打开。

观察文档可知,第一张图片的标签为<img class="BDE_Image" src="http://tiebapic.baidu.com/forum/w%3D580/sign=219b01688545d688a302b2ac94c37dab/f3e3241f95cad1c845e0347c683e6709c83d5144.jpg" size="905080" changedsize="true" width="560" height="817">

您可以把鼠标悬浮在图片上,然后按下Ctrl+Shift+C进行快速定位。


观察后面几张图片,发现共性为img标签,class为BDE_Image,而我们只需要获得src链接即可。


橙色是共性,绿色是需要爬取的内容。

查找标签

由上面的分析易得,我们需要写的代码为:

1
2
3
images = soup.find_all('img',class_='BDE_Image') #这将返回含有每个图片元素的一个列表
for img in images:
print(img['src'])

可以看到我们获得了大量的链接。
接下来只需要下载这些链接的内容即可。

下载图片

此处先直接贴出代码再做解释。

1
2
3
4
5
6
7
import time
for img in images:
print('*'*100)
with open(str(int(time.time()))+'.jpg','wb') as f: # 使用with语句打开文件(文件不存在则会创建, 用时间戳命名文件)
f.write(requests.get(img['src']).content) # 写入字节集
# requests.get(img['src']) 是用requests访问目标链接 然后用 .content属性去获取二进制内容
time.sleep(4)

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
html = requests.get('https://tieba.baidu.com/p/6970148254')
print(html.status_code) # 返回200即为请求成功
print(html.text) # html.text 即为我们想要的HTML文档
from bs4 import BeautifulSoup
soup = BeautifulSoup(html.text,'html.parser')
images = soup.find_all('img',class_='BDE_Image') #这将返回含有每个图片元素的一个列表
import time
for img in images:
print('*'*100)
with open(str(int(time.time()))+'.jpg','wb') as f: # 使用with语句打开文件(文件不存在则会创建, 用时间戳命名文件)
f.write(requests.get(img['src']).content) # 写入字节集
# requests.get(img['src']) 是用requests访问目标链接 然后用 .content属性去获取二进制内容
time.sleep(4) # 等待4秒 以防止来不及下载

爬取豆瓣影评

1
2
3
4
# 先引用包
import requests
from bs4 import BeautifulSoup
import time


找到要爬取的目标,我选择了这个《我和我的家乡》的影评。链接

获取HTML文档

1
2
3
html = requests.get('https://movie.douban.com/subject/35051512/comments?limit=20&status=P&sort=new_score')
print(html.status_code) # 返回200即为请求成功
print(html.text) # html.text 即为我们想要的HTML文档


我们可以看到,直接进行GET请求,返回的状态号为418,并没有成功。
这可能是网站检测出非正常访问,所以我们要伪装成真实人物来访问。
这里就涉及一个User-Agent的概念了,简称UA。这里不解释UA是什么。只说明如何利用。
如何找到UA呢?下面介绍两个方法:

方法一

打开F12工具,刷新一下网页。选择Network页签,在左边的选项中随便点一个,在右边的Headers中找到user-agent字段。
Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36

方法二

打开浏览器,在地址栏输入about://version并回车。可以方便地找到UA字段。

headers

创建一个字典headers,将刚刚找到的UA,作为字段User-Agent的值写入其中。

1
2
3
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36',
}

接下来将之前的

1
html = requests.get('https://movie.douban.com/subject/35051512/comments?limit=20&status=P&sort=new_score')

修改为

1
html = requests.get('https://movie.douban.com/subject/35051512/comments?limit=20&status=P&sort=new_score',headers=headers)

这个时候就可以发现能成功请求了。

用BeautifulSoup解析

1
soup = BeautifulSoup(html.text,'html.parser')

对网页元素进行选择

我们要爬取的目标是,该电影的影评,最好爬取到尽可能多的内容。

拟定对图上每一个用户的昵称、评分、时间、评论内容、赞成数进行爬取。

打开F12工具进行分析,我们发现每一个用户的评论和其相关信息都在一个div标签,classcomment-item的容器(共同点)中。
注:容器是为方便陈述自己拟定的非正式称谓。

查找标签

思路:我们可以先把页面上所有的容器选出来,再从每个容器中选取自己想要的信息。

1
boxes = soup.find_all('div',class_='comment-item')

这样就找到了所有的容器,并以列表的形式返回。
然后我们遍历每个容器,提取自己想要的信息。

1
2
for box in boxes:
# 在box中提取信息

这个时候,每个容器中的内容应类似这样(部分内容被隐去):

容器内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div class="comment-item " data-cid="2455502478">
<div class="comment-item " data-cid="2538318284">
<div class="avatar">
<a title="Username" href="https://www.douban.com/people/0000000/">
<img src="https://img1.doubanio.com/icon/u0000000-17.jpg" class="">
</a>
</div>
<div class="comment">
<h3>
<span class="comment-vote">
<span class="votes vote-count">2217</span>

<input value="2538318284" type="hidden">
<a href="javascript:;" data-id="2538318284" class="j a_show_login" onclick="">有用</a>
<!-- 删除短评 -->
</span>
<span class="comment-info">
<a href="https://www.douban.com/people/0000000/" class="">Username</a>
<span>看过</span>
<span class="allstar50 rating" title="力荐"></span>
<span class="comment-time " title="2020-09-20 20:16:27">
2020-09-20
</span>
</span>
</h3>
<p class=" comment-content">
<class="short">徐峥的才华确实被低估了。“乡村教师”这个题材的电影展现出他某个程度的细腻——他很擅长从一个小的切角去展开一个宏大叙题,这样的融合不会过于煽情,却分外让人感动。</span>
</p>
</div>
</div>

然后我们再对每个容器进行分析:

赞成数

1
box.find('span',class_='votes')

注:我们观察到上面的类写的是 class=“votes vote-count”,这不是一个类的名字,而是两个类的名字,也就是说这个span既属于votes也属于vote-count。

为了提取其中的正文,我们加上.text属性。

1
box.find('span',class_='votes').text

用户名

那么如何提取用户名呢?用户名的class为空。和上面提取赞成数类似。

1
box.find('a',class_='').text

但是不建议使用这个方法,似乎是会出现BUG。(这个条件太弱了,满足条件可能不止这一个)
所以我们可以采用连续查找的方式,先查找span,class_="comment-info",再在里面查找a标签。

1
box.find('span',class_='comment-info').find('a').text

评分

关于评分,我们可以在文档中观察到,没有直接的数字或文本来表示,它是用一个类名来表示的。
所以我们可以想办法获取到这个类名。

1
box.find('span',class_='rating').get('class')


在上面那个语句后加一个[0]来获取第一个元素,然后把allstar去掉。

1
box.find('span',class_='rating').get('class')[0].replace("allstar")

通过观察可知,30其实是三颗星。所以我们可以简单地换算一下。

1
int(box.find('span',class_='rating').get('class')[0].replace("allstar",""))//10

评论内容

这个也非常简单。
只需要:

1
box.find('span',class_='short').text

时间

1
2
box.find('span',class_='comment-time').text.strip()
#不使用strip()的话两边会有空格或换行, 用这个方法来去掉这些我们不要的东西。


也可以从属性中取得更详细的时间。

1
box.find('span',class_='comment-time')['title']

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from bs4 import BeautifulSoup
import time
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36',
}
html = requests.get('https://movie.douban.com/subject/35051512/comments?limit=20&status=P&sort=new_score',headers=headers)
print(html.status_code) # 返回200即为请求成功
print(html.text) # html.text 即为我们想要的HTML文档
soup = BeautifulSoup(html.text,'html.parser')
boxes = soup.find_all('div',class_='comment-item')
for box in boxes:
print("用户名",box.find('span',class_='comment-info').find('a').text)
print("赞成数",box.find('span',class_='votes').text)
print("时间",box.find('span',class_='comment-time').text.strip())
print("星数",int(box.find('span',class_='rating').get('class')[0].replace("allstar",""))//10)
print("评论内容",box.find('span',class_='short').text)
print("-"*10) #分隔符

运行效果如下图所示: