前面初步学习requests库、了解基本HTML的内容和解析页面常用的lxml、Beautiful Soup模块的内容,下面我们就可以使用这些内容在互联网上爬取一些数据,为下一步的数据分析提供原材料。
写好一个爬虫最基本的是做好页面分析,找到链接和规律,这样在写爬虫的时候就可以有方向和目的性。接下来,我们就以爬虫最常用的豆瓣评分TOP250的内容作为爬虫的demo,以此来学习使用相关知识。
制定需求
在开始编写爬虫之前,第一步的操作并不是直接写代码,而是做好前面的分析,就像是项目开发一样,最重要的其实并不是写代码的环节,而是前期的需求调研和功能分析。下面我们打开豆瓣电影TOP250的链接:https://movie.douban.com/top250
在点开第一页评分最高的这部电影《肖申克的救赎》,这里面我们可以看到电影导演、评分、类型等等一些的信息,在页面下方还有一些剧情简介,影片评论等等。
所以,在开始之前我们制定好需求,爬取豆瓣TOP250电影的信息,内容包括电影名称、导演、电影类型(多类型按第一个)、制片国家/地区(多国家按第一个)、语言、上映年份、电影评分,评价人数。
在这些信息中我们就可以做一些简单的数据分析,比如说:什么样的类型的电影评分高。哪个国家的电影制作水平高等,在这之前虽然或多或少的知道一些大体的结论,但是如果让你拿出数据来证明你的结论,还真的未必可以有相关的数据,那么现在我们就可以通过自己抓取相关信息,来进行数据分析。
首先要做的是分析每一个页面的内容以及如何翻页,翻页就是当前页面内容抓取完了再如何抓取下一页的内容。首页的页面拉到最下面的,我们可以看到页码的标签,如下图所示,并且能知每一页中有25部电影的链接。
可以在浏览器中右键查看页面源代码,找到页面位置的代码,代码内容:
<div class="paginator"> | |
<span class="prev"> | |
<前页 | |
</span> | |
<span class="thispage">1</span> | |
<a href="?start=25&filter=" >2</a> | |
<a href="?start=50&filter=" >3</a> | |
<a href="?start=75&filter=" >4</a> | |
<a href="?start=100&filter=" >5</a> | |
<a href="?start=125&filter=" >6</a> | |
<a href="?start=150&filter=" >7</a> | |
<a href="?start=175&filter=" >8</a> | |
<a href="?start=200&filter=" >9</a> | |
<a href="?start=225&filter=" >10</a> | |
<span class="next"> | |
<link rel="next" href="?start=25&filter="/> | |
<a href="?start=25&filter=" >后页></a> | |
</span> | |
<span class="count">(共250条)</span> | |
</div> | |
</div> |
从中可以得到,当前网页翻页的方式使用的start参数来控制,每一页固定的25条。所以翻页可以使用页码就是start参数为25*(n-1),n为页数。
找到翻页的方法后,在去寻找每一页的详情怎么获取,在首页中是25部电影的list,而我们想获取的信息是这25部电影详情的链接,找到之前《肖申克救赎》的源代码,部分截取如下,可以发现a标签中href属性值就是电影详情页的链接https://movie.douban.com/subject/1292052/
<div class="hd"> | |
<a href="https://movie.douban.com/subject/1292052/" class=""> | |
<span class="title">肖申克的救赎</span> | |
<span class="title">TheShawshank Redemption</span> | |
<span class="other"> / 月黑高飞(港) / 刺激1995(台)</span> | |
</a> | |
<span class="playable">[可播放]</span> | |
</div> |
往下翻看查看一些其他的电影详情链接:比如《霸王别姬》的链接为https://movie.douban.com/subject/1291546/,简单分析一下,可以发现电影的详情页面链接前面都是以 https://movie.douban.com/subject/开头,后面的数字是电影在豆瓣中的id,链接使用的是restful风格的API。
编写链接爬虫
现在我们可以开始编写爬虫,但是现在不能把全部的内容都写完,现在先把需要爬取的链接拿到,然后在每个链接进行爬取。在前面的分析中知道,其实就是获取页面中https://movie.douban.com/subject/开头的链接就可以了。这里选择是用BeautifulSoup模块,在find_all()方法,搜索所有a标签,而且href符合要求的内容。在之前章节已经学习了requests库,所以可以使用requests和BeautifulSoup来完整,示例代码如下:
爬取豆瓣电影TOP250
import re | |
from bs4 import BeautifulSoup | |
import requests | |
# 请求链接 | |
url = 'https://movie.douban.com/top250' | |
# 设置请求头 | |
headers = { | |
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36' | |
} | |
# 添加headers参数 | |
response = requests.get(url, headers=headers) | |
# 生成一个BeautifulSoup对象 | |
soup = BeautifulSoup(response.text, 'html.parser') | |
# 搜索所有的符合要求的a标签 | |
links = soup.find_all('a', href=re.compile("^https://movie.douban.com/subject/")) | |
# 创建一个结果url的存放容器,set方便去重 | |
linkList = set() | |
# 遍历标签集合去除链接 | |
for link in links: | |
linkList.add(link['href']) | |
linkList = list(linkList) | |
# 打印结果 | |
print(len(linkList)) | |
print(linkList) |
代码结果:
25 | |
['https://movie.douban.com/subject/25662329/', ……s] |
我们通过一段简洁的代码获取到首页中25部电影的详情链接,但是还有剩余10页的内容,不能每次改变参数重新运行一次,这样不符合代码的开发规范,而这个方法可以提取成为一个公共方法,只需要将url作为传参,返回的是当前url页面中的所有电影详情的链接的list。所以还得需要将此方法改造一下:
# 导入相关库 | |
import re | |
import time | |
from bs4 import BeautifulSoup | |
import requests | |
# 公共参数 请求头 | |
headers = { | |
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0' | |
} | |
# 爬取url中的详情链接方法 | |
def getDetailLinks(url): | |
# 添加headers参数 | |
response = requests.get(url, headers=headers) | |
# 生成一个BeautifulSoup对象 | |
soup = BeautifulSoup(response.text, 'html.parser') | |
# 搜索符合要求的标签 | |
links = soup.find_all('a', href=re.compile("^https://movie.douban.com/subject/")) | |
linkList = set() | |
for link in links: | |
linkList.add(link['href']) | |
linkList = list(linkList) | |
return linkList | |
# mian方法 | |
if __name__ == '__main__': | |
# 创建一个空list存放url | |
allDetailLinks = [] | |
for i in range(10): | |
# 拼接每一页的url | |
url = 'https://movie.douban.com/top250?start=' + str(25 * i) + '&filter=' | |
print("当前抓取的页数:{},抓取链接为:{}".format(str(i + 1), url)) | |
allDetailLinks.extend(getDetailLinks(url)) | |
# 增加延迟,防止被ban | |
time.sleep(5) | |
print("抓取链接总数:" + str(len(allDetailLinks))) | |
print(allDetailLinks) |
代码结果:
当前抓取的页数:1,抓取链接为:https://movie.douban.com/top250?start=0&filter= | |
当前抓取的页数:2,抓取链接为:https://movie.douban.com/top250?start=25&filter= | |
……. | |
抓取链接总数:250 | |
['https://movie.douban.com/subject/1292722/',…… |
分析详情页面
获取到页面链接后下面就是按照详情页面中寻找信息,一般的分析思路上,先在浏览器页面中找信息的位置,然后在找到源代码中的对应的位置,然后在按照标签和属性中的值来一一分析需要的内容怎么获取。
找到当前位置的源代码,电影信息和评分信息的代码内容如下
<!-- 电影名称--> | |
<h1> | |
<span property="v:itemreviewed">肖申克的救赎 The Shawshank Redemption</span> | |
<span class="year">(1994)</span> | |
</h1> | |
<!-- 电影相关信息--> | |
<div id="info"> | |
<span><span class='pl'>导演</span>:<span class='attrs'><a href="/celebrity/1047973/" rel="v:directedBy">弗兰克·德拉邦特</a></span></span><br/> | |
<span ><span class='pl'>编剧</span>: <span class='attrs'><a href="/celebrity/1047973/">弗兰克·德拉邦特</a> / <a href="/celebrity/1049547/">斯蒂芬·金</a></span></span><br/> | |
<span class="actor"><span class='pl'>主演</span>: <span class='attrs'><a href="/celebrity/1054521/" rel="v:starring">蒂姆·罗宾斯</a> /……. | |
<span class="pl">类型:</span> <span property="v:genre">剧情</span> / <span property="v:genre">犯罪</span><br/> | |
<span class="pl">制片国家/地区:</span> 美国<br/> | |
<span class="pl">语言:</span> 英语<br/> | |
<span class="pl">上映日期:</span> <span property="v:initialReleaseDate" content="1994-09-10(多伦多电影节)">1994-09-10(多伦多电影节)</span> / <span property="v:initialReleaseDate" content="1994-10-14(美国)">1994-10-14(美国)</span><br/> | |
<span class="pl">片长:</span> <span property="v:runtime" content="142">142分钟</span><br/> | |
<span class="pl">又名:</span> 月黑高飞(港) / 刺激1995(台) / 地狱诺言 / 铁窗岁月 / 消香克的救赎<br/>……. | |
</div> | |
<!-- 电影评分信息--> | |
<div class="rating_self clearfix" typeof="v:Rating"> | |
<strong class="ll rating_num" property="v:average">9.7</strong> | |
<span property="v:best" content="10.0"></span> | |
<div class="rating_right "> | |
<div class="ll bigstar bigstar50"></div> | |
<div class="rating_sum"> | |
<a href="comments" class="rating_people"> | |
<span property="v:votes">2288098</span>人评价 | |
</a> | |
</div> | |
</div> | |
</div> |
根据需求中的内容我们需要获得内容是:电影名称、导演、电影类型(多类型按第一个)、制片国家/地区(多国家按第一个)、上映年份、电影评分,评价人数。
下面我们一一分析各个元素在页面中的位置并且确定获取值的方法
电影名称:在span标签并且属性property="v:itemreviewed",可以使用BeautifulSoup.find()
上映年份:在span标签并且属性class="year",可以使用BeautifulSoup.select()
导演:在a标签并且属性rel="v:directedBy",可以使用BeautifulSoup.find()
电影类型:在span标签并且属性property="v:genre",可以使用BeautifulSoup.find()
电影评分:在strong标签并且属性property="v:average",可以使用BeautifulSoup.find()
评价人数:在span标签并且属性property="v:votes",可以使用BeautifulSoup.find()
制片国家/地区和语言并没有直接在标签中,只是在id为info的div中的文本信息。所以无法使用标签定位的方法获取到,但是可以通过把info中的文本信息通过换行符切分成一个字符串list,然后遍历这个list按照指定字符串匹配的方法来确定这些信息。示例代码如下:
爬取《肖申克的救赎》相关信息
from bs4 import BeautifulSoup | |
import requests | |
# 公共参数 请求头 | |
headers = { | |
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0' | |
} | |
url = 'https://movie.douban.com/subject/1292052/' | |
response = requests.get(url, headers=headers) | |
# 生成一个BeautifulSoup对象 | |
soup = BeautifulSoup(response.text, 'html.parser') | |
info = soup.select("#info") | |
# 电影名称 | |
name = soup.find('span', attrs={"property": "v:itemreviewed"}) | |
print(name.string) | |
# 上映年份 | |
year = soup.select('.year') | |
year = year[0].get_text() | |
print(year.replace("(", "").replace(")", "")) | |
# 导演 | |
directedBy = soup.find('a', attrs={"rel": "v:directedBy"}) | |
print(directedBy.string) | |
# 类型 | |
genre = soup.find('span', attrs={"property": "v:genre"}) | |
print(genre.string) | |
# 分数 | |
rating_num = soup.find('strong', attrs={"property": "v:average"}) | |
print(rating_num.string) | |
# 评价人数 | |
rating_people = soup.find('span', attrs={"property": "v:votes"}) | |
print(rating_people.string) | |
infos = info[0].get_text().split("\n") | |
for info in infos: | |
if info.startswith("制片国家/地区"): | |
# 取第一个国家的值 | |
print(info.replace("制片国家/地区:", "").replace(" ", "").split("/")[0]) | |
if info.startswith("语言"): | |
print(info.replace("语言: ", "")) |
代码结果:
肖申克的救赎 The Shawshank Redemption | |
1994 | |
弗兰克·德拉邦特 | |
剧情 | |
9.7 | |
2288215 | |
美国 | |
英语 |
有时候find()方法和select()方法都可以达到相同的目标,选择其中一个使用即可,以上仅仅举例使用,并不代表唯一方法,感兴趣的读者可以使用其他的方式抓取。
现在的方法每次也只能爬取一个页面链接,但是我们需要爬取25页,所以需要先将此方法封装成通用的方法。为了方便后面的储存,我们先设计一个豆瓣电影的类。
电影信息对象
class doubanMovie(): | |
def __init__(self, name, year, directedBy, genre, rating_num, rating_people, area, language): | |
self.name = name | |
self.year = year | |
self.directedBy = directedBy | |
self.genre = genre | |
self.rating_num = rating_num | |
self.rating_people = rating_people | |
self.area = area | |
self.language = language | |
# 打印对象的主要信息,方便查看 | |
def __str__(self): | |
return self.name + "-" + self.year + "-" + self.directedBy + "-" + self.area + "-" + self.rating_num |
然后把前面的信息封装到一个方法中,设置好入参和出参,供其他的函数调用。
def getMovieDetail(url): | |
response = requests.get(url, headers=headers) | |
soup = BeautifulSoup(response.text, 'html.parser') | |
info = soup.select("#info") | |
name = soup.find('span', attrs={"property": "v:itemreviewed"}) | |
name = name.string | |
year = soup.select('.year') | |
year = year[0].get_text().replace("(", "").replace(")", "") | |
directedBy = soup.find('a', attrs={"rel": "v:directedBy"}) | |
directedBy = directedBy.string | |
genre = soup.find('span', attrs={"property": "v:genre"}) | |
genre = genre.string | |
rating_num = soup.find('strong', attrs={"property": "v:average"}) | |
rating_num = rating_num.string | |
rating_people = soup.find('span', attrs={"property": "v:votes"}) | |
rating_people = rating_people.string | |
infos = info[0].get_text().split("\n") | |
area = "" | |
language = "" | |
for info in infos: | |
if info.startswith("制片国家/地区"): | |
area = info.replace("制片国家/地区:", "").replace(" ", "").split("/")[0] | |
if info.startswith("语言"): | |
language = info.replace("语言: ", "") | |
movie = doubanMovie(name, year, directedBy, genre, rating_num, rating_people, area, language) | |
return movie |
完成最终爬虫
上面分别进行了对url的获取和页面详细信息的获取,并且对每一块功能进行封装,每个功能模块都有入参和出参,这样才符合完整的开发规范。
但是作为一个完整的爬虫程序来说,只需要有一个main方法入口,然后可以将所有需要的信息都爬取完成,所以我们还需要将上面的两个小节的内容合成起来,做到一个完整的流程,写好一个完整的爬虫。
# 爬取豆瓣电影TOP250 | |
import re | |
import time | |
from bs4 import BeautifulSoup | |
import requests | |
# 公共参数 请求头 | |
headers = { | |
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0' | |
} | |
# 电影信息对象 | |
class doubanMovie(): | |
def __init__(self, name, year, directedBy, genre, rating_num, rating_people, area, language): | |
self.name = name | |
self.year = year | |
self.directedBy = directedBy | |
self.genre = genre | |
self.rating_num = rating_num | |
self.rating_people = rating_people | |
self.area = area | |
self.language = language | |
def __str__(self): | |
return self.name + "-" + self.year + "-" + self.directedBy + "-" + self.area + "-" + self.rating_num | |
# 爬取list页面中的详情链接 | |
def getDetailLinks(url): | |
# 添加headers参数 | |
response = requests.get(url, headers=headers) | |
# 生成一个BeautifulSoup对象 | |
soup = BeautifulSoup(response.text, 'html.parser') | |
# 搜索符合要求的标签 | |
links = soup.find_all('a', href=re.compile("^https://movie.douban.com/subject/")) | |
linkList = set() | |
for link in links: | |
linkList.add(link['href']) | |
linkList = list(linkList) | |
return linkList | |
# 爬取详情链接中信息,并且返回一个对象 | |
def getMovieDetail(url): | |
response = requests.get(url, headers=headers) | |
# 生成一个BeautifulSoup对象 | |
soup = BeautifulSoup(response.text, 'html.parser') | |
info = soup.select("#info") | |
# 电影名称 | |
name = soup.find('span', attrs={"property": "v:itemreviewed"}) | |
name = name.string | |
# 上映年份 | |
year = soup.select('.year') | |
year = year[0].get_text().replace("(", "").replace(")", "") | |
# 导演 | |
directedBy = soup.find('a', attrs={"rel": "v:directedBy"}) | |
directedBy = directedBy.string | |
# 类型 | |
genre = soup.find('span', attrs={"property": "v:genre"}) | |
genre = genre.string | |
# 分数 | |
rating_num = soup.find('strong', attrs={"property": "v:average"}) | |
rating_num = rating_num.string | |
# 评价人数 | |
rating_people = soup.find('span', attrs={"property": "v:votes"}) | |
rating_people = rating_people.string | |
infos = info[0].get_text().split("\n") | |
area = "" | |
language = "" | |
for info in infos: | |
if info.startswith("制片国家/地区"): | |
area = info.replace("制片国家/地区:", "").replace(" ", "").split("/")[0] | |
if info.startswith("语言"): | |
language = info.replace("语言:", "").replace(" ", "").split("/")[0] | |
movie = doubanMovie(name, year, directedBy, genre, rating_num, rating_people, area, language) | |
return movie | |
# 获取全部的电影的链接 | |
def getAllLinkList(): | |
# 创建一个空list存放url | |
allDetailLinks = [] | |
for i in range(10): | |
url = 'https://movie.douban.com/top250?start=' + str(25 * i) + '&filter=' | |
print("当前抓取的页数:{},抓取链接为:{}".format(str(i + 1), url)) | |
allDetailLinks.extend(getDetailLinks(url)) | |
time.sleep(2) | |
return allDetailLinks | |
if __name__ == '__main__': | |
# 存放全部电影对象的容器 | |
allMovies = [] | |
allDetailLinks = getAllLinkList() | |
for detailLink in allDetailLinks: | |
print("当前抓取链接为:{}".format(detailLink)) | |
allMovies.append(getMovieDetail(detailLink)) | |
time.sleep(2) | |
print("豆瓣电影TOP250信息抓取完毕") | |
for movie in allMovies: | |
print(movie) |
至此,我们已经完成了最终的爬虫,这样只需要运行main方法,就可以实现对豆瓣电影TOP250的信息的采集。
从上面的内容中我们可以梳理出基础爬虫的编写思路,大致分为四部分:
- 确定需要抓取的页面中的信息
- 确定列表页面的翻页方式
- 确定详情页面的信息元素位置和方式
- 梳理完成整个爬虫的流程