1.介绍
爬豆瓣一直是练习爬虫的经典项目,由于其无太多反爬措施,所以上手难度较低,比较适合入门选手练手,我之前就爬过了,但是一直没时间整理,最近放假了正好来复盘顺便完善一波
2.网站分析
豆瓣最近热映电影:https://movie.douban.com/cinema/nowplaying

正在上映的区域即是我们要爬取的区域,这里还是推荐先登录,因为不登陆会自动跳转到登录页面
在爬取时可以不加cookies,只加入user-agent
我们的思路主要是爬取数据->数据处理->数据可视化
主要用到的库有:requests,BeautifulSoup, csv,pandas,matplotlib
3.数据爬取
直接用审查定位到信息的源代码位置,标题和评分

在定位的时候可以发现,其实标题评分这些信息,在上面的<li>标签的属性中就已经出现了,我们直接使用li标签即可一次获取所有信息
先看一下主函数,主函数主要是创建列表,然后调用各个函数
def main():
print("------爬取豆瓣最近热映电影及评分------\n")
url = "https://movie.douban.com/cinema/nowplaying/tianjin"
indoList = [[ [] for i in range(2) ] for i in range(50) ] #创建50*2的二维列表
html = getHTMLText(url)
parsePage(indoList, html)
SaveCSV(indoList)
然后是爬取部分,主要是使用soup.findAll方法获取某个div下所有的li标签,使用.attrs方法获取属性。然后用if判断长度是否大于1,因为有些li标签的属性只有一个,是无效内容,然后把某个属性提取出来存入在主函数创建的二维列表中即可

def parsePage(ilt, html):
try:
cnt = 0
soup = BeautifulSoup(html, "html.parser")
for li in soup.find('div', class_='mod-bd').findAll('li'):
d = li.attrs # 获取li标签的属性
if len(d) > 1: # 筛选有效数据
ilt[cnt][0] = d['data-title'] # 将标题存入列表
ilt[cnt][1] = d['data-score'] # 将评分存入列表
cnt = cnt + 1
del ilt[cnt:] #删除空位置
except:
return ""
因为我们开始创建的是50*2的二维列表,长度是大于最近热映电影数量的,所以在循环完之后,直接用计数器得到的长度,把后面的空列表删除掉即可
爬取完数据之后以csv文件格式保存,便于后续的处理
然后是完整的数据爬取部分的函数
import requests
from bs4 import BeautifulSoup
import csv
def getHTMLText(url): #获得页面函数
try:
header = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0'}
# 加入浏览器头部信息,模拟浏览器访问
r = requests.get(url, headers=header, timeout=30)
r.raise_for_status() # 返回状态码
r.encoding = r.apparent_encoding #文本解码
return r.text
except:
return ""
def parsePage(ilt, html):
try:
cnt = 0
soup = BeautifulSoup(html, "html.parser")
for li in soup.find('div', class_='mod-bd').findAll('li'):
d = li.attrs # 获取li标签的属性
if len(d) > 1: # 筛选有效数据
ilt[cnt][0] = d['data-title'] # 将标题存入列表
ilt[cnt][1] = d['data-score'] # 将评分存入列表
cnt = cnt + 1
del ilt[cnt:] #删除空位置
except:
return ""
def SaveCSV(ilt):
try:
with open('DouBan_RecentMovie.csv', 'w', newline='') as f: #将数据以csv形式存储
writer = csv.writer(f)
writer.writerow(['片名', '评分'])
for i in ilt:
writer.writerow(i)
f.close()
# 已存入的CSV文件预览
print("已存入的CSV文件如下:\n")
for i in csv.reader(open('DouBan_RecentMovie.csv','r')):
print(i)
except:
return ""
def main():
print("------爬取豆瓣最近热映电影及评分------\n")
url = "https://movie.douban.com/cinema/nowplaying/tianjin"
indoList = [[ [] for i in range(2) ] for i in range(50) ] #创建50*2的二维列表
html = getHTMLText(url)
parsePage(indoList, html)
SaveCSV(indoList)
main()
4.数据分析
在数据分析时,我们先从csv文件读入爬取的数据,存入列表中,然后处理列表,之后用pandas处理数据,再写入新的csv文件
这里要注意在写入csv文件时,要加入 newline=” ,不然每行之后会有空一行,可能是因为在写入时编译器会自动在每一行数据之后加上换行

在读入csv文件时要注意读取出的reader是个迭代类型,需要遍历一下才能把数据拿出来
然后先来说一下处理列表
def Deal_li(ilt): # 处理列表
del ilt[0] # 删除表头
for i in range(len(ilt)): #数据类型转化
t = float(ilt[i][1])
ilt[i][1] = t
dlt = [[] for i in range(len(ilt))]
for i in range(len(ilt)): #创建没有0值的数组用于统计
if ilt[i][1] != 0:
dlt[i].append(ilt[i][0])
dlt[i].append(ilt[i][1])
print(dlt)
return dlt
因为在写入时候为了看起来方便,所以我们加了一个表头,在统计数据的时候我们用不到,所以先把表头从列表中删除,注意:从csv文件中拿出来的数据时str类型,所以我们要先把评分转化为float类型
同时,数据中有一些0值,因为上映时间太短,所以还没有评分,统计的时候需要去除这部分数据,否则会增加样本数量,导致统计的平均值下降。所以我们要创建一个去掉这些0值的列表用于统计,而具有完整数据的列表可以用于评分排序。
而原本列表中带有0值的,我们可以放到后面利用pandas对其进行排序,根据分数把对应的电影名称排序做出来
然后利用pandas来处理数据
def Deal_pd(ilt, dlt): # 利用Pandas处理数据
array = pd.DataFrame(ilt)
countArray = pd.DataFrame(dlt)
newArray = array.sort_values(1, ascending=True) # 按第二列评分进行升序排序
print(newArray)
mean = countArray.describe().loc['mean']
std = countArray.describe().loc['std']
return mean, std, newArray
先利用传入的两个列表得到两个dataframe类型,把带有0值的类型用array.sort_values方法排序,然后把不含0值的类型用descibe方法获得统计数据,再用.loc方法拿出平均值和方差
最后再把排序后的数组直接写入为csv,在末尾追加一组统计数据即可
完整代码如下:
import pandas as pd
import csv
def in_stream(ilt): #读入CSV文件
with open('DouBan_RecentMovie.csv', 'r', ) as f_out:
reader = csv.reader(f_out)
for i in reader:
ilt.append(i)
f_out.close()
def Deal_li(ilt): # 处理列表
del ilt[0] # 删除表头
for i in range(len(ilt)): #数据类型转化
t = float(ilt[i][1])
ilt[i][1] = t
dlt = [[] for i in range(len(ilt))]
for i in range(len(ilt)): #创建没有0值的数组用于统计
if ilt[i][1] != 0:
dlt[i].append(ilt[i][0])
dlt[i].append(ilt[i][1])
print(dlt)
return dlt
def Deal_pd(ilt, dlt): # 利用Pandas处理数据
array = pd.DataFrame(ilt)
countArray = pd.DataFrame(dlt)
newArray = array.sort_values(1, ascending=True) # 按第二列评分进行升序排序
print(newArray)
mean = countArray.describe().loc['mean']
std = countArray.describe().loc['std']
return mean, std, newArray
def out_stream(mean, std, newArray): # 处理数据写入新的CSV文件
newArray.to_csv('DouBan_RecentMovie_Deal.csv', header=0, index=0, encoding='utf_8_sig')
with open('DouBan_RecentMovie_Deal.csv', 'a+', newline='') as f_in: # 追加统计数据放在表尾
writer = csv.writer(f_in)
writer.writerow(['mean', mean.values[0]])
writer.writerow(['std', std.values[0]])
f_in.close()
def main(): # 主函数
print("------豆瓣数据处理------\n")
li = []
in_stream(li)
lt = Deal_li(li)
mean, std, new_li = Deal_pd(li, lt)
out_stream(mean, std, new_li)
main()
5.数据可视化
数据可视化主要是用到了matplotlib库,绘制横向条形图,箭头注释,平均值分界线
因为第一次用matplotlib,我把数据分成了三个一维列表作为绘图数据,我也不知道有没有什么便捷的方法,因为在网上看了很久也没找到
然后先修改字体,绘制横向条形图,由于竖向条形图会把电影名称重叠在一起,所以用横向的,写主标题,写横竖轴标题,然后再在条形图上加上数据标签,绘制平均值分界线,进行箭头注释
这里需要注意就是,很多操作都需要知道坐标位置,只要运行之后,在图形右下角就能看到横纵坐标

完整代码如下:
import matplotlib.pyplot as plt
import matplotlib
import csv
def in_stream(ilt): #读入CSV文件
with open('DouBan_RecentMovie_Deal.csv', 'r', encoding='utf_8_sig') as f_out:
reader = csv.reader(f_out)
for i in reader:
ilt.append(i)
f_out.close()
def Deal_li(ilt): # 处理列表
del ilt[0] # 删除表头
for i in range(len(ilt)):
t = float(ilt[i][1])
ilt[i][1] = t
def Show(ilt):
li1 = []
li2 = []
li3 = []
Len = len(ilt)
# 创建三个一维列表进行数据存储
for i in range(Len-2):
t = ilt[i][0]
li1.append(t)
for i in range(Len-2):
t = ilt[i][1]
li2.append(t)
for i in range(Len-2, Len):
t = ilt[i][1]
li3.append(t)
matplotlib.rcParams['font.family'] = 'SimHei' #有中文,修改全局字体
y = [i for i in range(Len-2)] #给出在y轴上的位置,绘图时从下到上
plt.title('Recent Hot Movie in DouBan', fontsize=20, color='green')
plt.xlabel('横轴:评分', fontsize=13, color='orange')
plt.ylabel('纵轴:片名', fontsize=13, color='orange')
rects_Mov = plt.barh(y, li2, facecolor='orange', tick_label=li1)
for rect in rects_Mov: #添加数据标签
width = rect.get_width()
plt.text(width+0.05, rect.get_y()+0.3, width, ha='left', va='center')
rect.set_edgecolor('white')
plt.vlines(li3[0], 0, Len-2-0.5, color="#c72e29", linestyle='--') #平均值直线
plt.text(10, 0, "标准差:{:.2f}\n平均值:{:.2f}".format(li3[1], li3[0]), fontsize=13, color='green') #角落数据标注
plt.annotate("平均值:{:.2f}".format(li3[0]), xy=(li3[0], 3), xytext=(li3[0]+0.5, 0), #使用箭头进行注释
arrowprops=dict(facecolor='black', shrink=0.1, width=2))
plt.annotate("暂无数据", xy=(0.2, 3), xytext=(0.8, 0),
arrowprops=dict(facecolor='black', shrink=0.1, width=2))
plt.show()
def main():
print("------豆瓣数据展示------\n")
li = []
in_stream(li)
Deal_li(li)
Show(li)
main()