在项目中体会爬虫的真谛
本文章是基于上一篇文章为基础的进阶版本!
文章大题概述:上一篇文章只是采集到了链接一级页面的电影数据,以及将采集的数据写入了单表数据库;因此,既然本文章做为进阶版,那么其内容在上一篇的基础上将进一步充实,不仅包括了采集二级子页面的电影详情信息,同时包括了对电影评分数据的优化,还包括了将数据写入三张数据表以便于进一步分析数据,还有就是将SQL语句筛选出来的结果以简单的图表的形式进行展示。以上就是本文章与上一篇的主要进阶之处。
从概述就看的出来这篇文章的内容也不少,兄弟别慌,继续跟着我的节奏慢慢来!相信你一定会有所收获!!!
项目文件指南,概述了该项目中的每个.py文件直接的关系:
百度网盘链接:https://pan.baidu.com/s/13ntcJC9BGECpKbM-m9zjRg
提取码:1rug
爬虫主程序
为了照顾之前看过上一篇文章的兄弟,本次在编写新的程序是特意加上了#!
的字样以示区分。
(1)分析网站子页面,如下图:
由于上篇文章中已经获取了每个电影的子页面链接(即播放地址),所以本文章代码中使用的xpath表达式,与播放链接的一致(祥见下面的主程序代码)
思路:参考上图界面,可确定本次要用xpath提取的电影详细信息为:主演名,导演名,电影类型,电影简介。这四个内容,之后要新建存储电影详情的数据表,由于是项目,为了符合其规范,还要创建采集记录的数据表,因此,加上上篇文章的数据表一共为三张数据表。
如下图所示,分析子页面以便于编写爬虫主程序中的xpath表达式采集相应的电影数据
思路:上图标记的ul标签为主演,导演以及类型的父路径 ,xpath表达式为xpath(’//ul[@class=“txtList clearfix”]’);主演信息在标签li ,,xpath表达式为xpath(‘li[@class=“liActor li_3”]/a/text()’)。(备注:找父路径的好处就是变相的重用父路径表达式代码。因为在一个父路径中,可能有很多标签中的信息要采集,这样编写xpath表达式时,就会写很多次xpath的父路径代码)
如下图是采集导演信息:
思路:如上图,在采集导演信息时,我发现它的标签和类型的标签一致,由此,可以一起爬取下来,导演xpath表达式xpath(‘li[@class=“li_3”]/a[@data ajax83=“ys_dy_2015_detail_daoy”]/text()’).extract()[0],除了获取内容并解析外,加上一个取第一个列表元素的操作,这样就取出了导演信息,因为我发现每部电影的导演都是一位,即便导演和类型的标签相同也不影响;类型xpath表达式xpath(‘li[@class=“li_3”]/a/text()’).extract(),这个表达式一定会把导演信息取出来,又因为类型数据有多个,所以将解析的列表做切片(即切除导演信息)再进行遍历其各个类型即可,祥见爬虫主程序代码。
如下图,分析采集电影简介信息的xpath:
这个xpath表达式就比较简单,.xpath(’//p[@class=“pIntro pHide”]’) 是简介父路径,xpath(“span/text()”).extract()[0]为子路径(当然写在一起也可以,但我这样写也有一定道理,详见爬虫主程序代码。)
番外:分析了半天网页,就是为了编写重要的xpath表达式获取相应的电影详情信息。由于上篇文章没有太多的介绍xpath表达式的书写,所以本文章给出了相应的表达式,以及分析思路,就算是之前不会xpath表达式也不要紧,相信聪明的你应该体会到了xpath表达式编写规则是什么样的了。那么接下来就要揭晓升级版的爬虫主程序代码以及更多进阶后的内容。
各文件代码详细如下:
爬虫主程序jobspider.py具体代码如下:
# -*- coding: utf-8 -*-
import scrapy
from ..items import SpidermovieprojectItem
class JobspiderSpider(scrapy.Spider):
name = 'jobspider'
#allowed_domains = ['http://dianying.2345.com/list/-------2.html?1']
#start_urls = ['http://dianying.2345.com/list/-------2.html?1/']
start_urls = []
#!定义一个构造方法,目的是在外部设定url信息以及初始化采集记录!!!
def __init__(self,start_urls=None,taskid=0,*args,**kwargs):
super(JobspiderSpider,self).__init__(*args,**kwargs)
self.start_urls.append(start_urls)
self.taskid = int(taskid)
pass
def parse(self, response):
movieItems = response.xpath("//ul[@class='v_picTxt pic180_240 clearfix']/li")
movieLen = len(movieItems)
movieCount = 0
#!分页(对分页进行了访问到最后一页时的优化)
nextURL = response.xpath("//div[@class='v_page']/a/@href").extract()
nextText = response.xpath("//div[@class='v_page']/a/text()").extract()
realURL = ""
if nextURL and nextText[-1].strip() == "下一页>":
realURL = response.urljoin(nextURL[-1])
pass
pass
for movieItem in movieItems:
movieCount += 1 #!
sItem = SpidermovieprojectItem()
sItem['taskId']=self.taskid #!
#extract()解析 strip()去掉多余空格
#解析电影名
movieName = movieItem.xpath("div[@class='txtPadding']/span/em[@class='emTit']/a/text()")
if movieName:
sItem['movieName']=movieName.extract()[0].strip()
#!取点击时的子页链接:(返回的是迭代器)
moviePositionDetail = movieItem.xpath("div[@class='txtPadding']/span/em/a/@href")
#解析主演
movieActor = movieItem.xpath("div[@class='txtPadding']/span[@class='sDes']/em/a/text()")
if movieActor:
sItem['movieActor']=movieActor.extract()[0].strip()
#解析电影评分
movieGrade = movieItem.xpath("div[@class='pic']/span/em/text()")
if movieGrade:
sItem['movieGrade']=movieGrade.extract()[0].strip()
#解析电影链接
movieUrl = movieItem.xpath("div[@class='txtPadding']/span/em/a/@href")
if movieUrl:
sItem['movieUrl']=movieUrl.extract()[0].strip()
#!
if movieName and movieActor and movieGrade and movieUrl and moviePositionDetail:
mDeUrl = moviePositionDetail.extract()[0] #!
sItem['nextURL'] = realURL #!
#访问二级页面 !
yield scrapy.Request(url="http:"+mDeUrl,callback=self.parse_detail,meta={'item':sItem,'movieLen':movieLen,'movieCount':movieCount},dont_filter=True)
pass
pass
#!定义爬取电影详情(点进去的子页面)的方法
def parse_detail(self,response):
sItem = response.meta['item']
movieLen = response.meta['movieLen']
movieCount = response.meta['movieCount']
txtListData = response.xpath('//ul[@class="txtList clearfix"]') #主演、导演、类型的父路径
print(txtListData)
mainActor = "" #多个人保存为字符串形式
typemovie = "" #多个类型保存为字符串形式
if txtListData:
mainActors = txtListData.xpath('li[@class="liActor li_3"]/a/text()').extract() #主演 mainActors是列表
for mainactor in mainActors:
mainActor += mainactor+" "
# print(mainActor)
director = txtListData.xpath('li[@class="li_3"]/a[@data-ajax83="ys_dy_2015_detail_daoy"]/text()').extract()[0] #导演
typeMovies = txtListData.xpath('li[@class="li_3"]/a/text()').extract() #类型
for typeMovie in typeMovies[1:]: #遍历多个类型并连接成新的字符串
typemovie += typeMovie+" "
# print(typemovie)
sItem['mainActor']=mainActor
sItem['director'] =director
sItem['typemovie'] =typemovie #sItem是items.py中的实例化对象
detailData = response.xpath('//p[@class="pIntro pHide"]') #简介父路径
print(detailData)
if detailData:
contents = detailData.xpath("span/text()").extract()[0]
sItem['movieDetail'] = contents
yield sItem #保顺序
pass
#!判断是否爬取完成,如果爬取完成就爬取下一页
if movieLen == movieCount:
if sItem['nextURL']:
yield scrapy.Request(sItem['nextURL'],self.parse,dont_filter=False)
pass
pass
pass
在之前的items.py文件中,加入新的条目(#!)如下:
import scrapy
class SpidermovieprojectItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
movieName = scrapy.Field()
movieActor = scrapy.Field()
movieGrade = scrapy.Field()
movieUrl = scrapy.Field()
nextURL = scrapy.Field() #!
movieDetail =scrapy.Field() #!存放详情页 movieDetail在pipelines.py中有改动
mainActor=scrapy.Field() #!
director=scrapy.Field() #!
typemovie=scrapy.Field() #!
taskId = scrapy.Field() #! 在mysqlpipelines.py中有改动,nextURL和movieDetail没有改动
pass
如下mysqlpipelines.py数据库管道文件中,包含优化分数操作以及输送电影详情信息:
from .dao.jobpositiondao import JobPositionDao #导入后文中的dao包中的模块的类
class SpidermoviemysqlPipeline(object):
def process_item(self, item, spider):
jobPositionDao = JobPositionDao() 实例化类的对象
try:
movieGrade=item['movieGrade']
FloatMovieGrade=0
FloatMovieGrade = float(movieGrade.replace("分","")) #将str型的评分转换为Float型,有助于之后的数据查询处理
result,lastRowId=jobPositionDao.create((item['movieName'],item['movieActor'],item['movieGrade'],item['movieUrl'],item['taskId'],FloatMovieGrade))#!将转换之后的评分加入数据库字段
if result:
jobPositionDao.createDetail((item['mainActor'],item['director'],item['typemovie'],item['movieDetail'],lastRowId)) #将其传入jobPositionDao.createDetail中的方法,用于将其写入数据库
pass
except Exception as e:
print(e)
finally:
jobPositionDao.close() #关闭连接很重要
return item
更新原来的piplines.py中的系统管道信息(#!)如下:
class SpidermovieprojectPipeline(object):
def process_item(self, item, spider):
print("通过管道输出电影数据:")
print(item['movieName'])
print(item['movieActor'])
print(item['movieGrade'])
print(item['movieUrl'])
print(item['movieDetail']) #!详情:简介内容
print(item['mainActor'])#! 电影详情信息
print(item['director']) #!
print(item['typemovie']) #!
return item
配置文件setting.py和middlewares.py始终不变,如果setting.py未配置,请前往上一篇文章。
在上篇文章的dao包中,新建taskdao.py,用于向 movie_collect_task数据表中插入数据。其具体代码如下:
from .basedao import BaseDao #BaseDao中的 getLastRowId() 方法下方可见
class TaskDao(BaseDao):
def create(self,params):
sql = "insert into movie_collect_task (task_title,task_url) values (%s,%s)"
result = self.execute(sql,params) #执行sql语句
lastRowId = self.getLastRowId() #相当于标识数据(两个表的数据一一对应)
self.commit()
self.close() #提交事务后要关闭连接
return result,lastRowId
pass
在上篇文章中的basedao.py文件中的BaseDao类中,添加 getLastRowId() 方法即可,代码如下:
def getLastRowId(self):
if self.__cursor:
return self.__cursor.lastrowid
在jobpositiondao.py文件中加入了插入电影详情的方法以及sql的数据库查询操作,具体代码(#!)如下:
from .basedao import BaseDao # . 代表当前目录
#定义一个movie数据操作的数据库访问类
class JobPositionDao(BaseDao):
def __init__(self):
super().__init__()
#向数据库插入movie信息
def create(self,params):
sql = "insert into movie_position (movieName,movieActor,movieGrade,movieUrl,movieTaskid,FloatMovieGrade) values(%s,%s,%s,%s,%s,%s)"
result = self.execute(sql,params)
lastRowId = self.getLastRowId()
self.commit()
return result,lastRowId
pass
#!向movie_position_detail 中插入电影详情信息
def createDetail(self,params):
sql = "insert into movie_position_detail (mainActor,director,typemovie,detail_desciption,detail_positionid) values (%s,%s,%s,%s,%s)"
result = self.execute(sql,params)
self.commit()
return result
pass
#!单表查询大于等于8.5评分的电影信息
def findGrade(self):
sql = "select movieName as '电影名称',movieActor as '主演',movieGrade as '评分',movieUrl as '播放地址' from " \
"movie_position where FloatmovieGrade >= 8.5;"
result = self.execute(sql,params=None)
self.commit()
return self.fetch()
#!大于等于8.5评分的电影信息
def findManyGrade(self):
sql = "select m1.movieName as '电影名称',m2.mainActor as '主演',m1.movieGrade as '评分'," \
"m1.movieUrl as '播放地址' from movie_position m1,movie_position_detail m2 " \
"where m1.movie_id=m2.detail_id and FloatmovieGrade >= 8.5;"
result = self.execute(sql,params=None)
self.commit()
return self.fetch()
#!各分类大于等于9.0评分的电影信息(三表联查)
def pfGrade(self):
sql = "select m2.task_title,m1.movieName,m3.mainActor,m1.movieGrade,m1.movieUrl from " \
"movie_position m1,movie_collect_task m2,movie_position_detail m3 " \
"where m1.movieTaskid=m2.task_id and m1.movie_id=m3.detail_positionid and m1.FloatmovieGrade>= 9.0;"
result = self.execute(sql,params=None)
self.commit()
return self.fetch()
#!各分类大于等于9.0评分的电影个数
def pfManyGrade(self):
sql = "select m2.task_title,count(*) as '大于9.0的电影的个数' from " \
"movie_position m1,movie_collect_task m2 " \
"where m1.movieTaskid=m2.task_id " \
"and m1.FloatmovieGrade>=9.0 group by m2.task_id;"
result =self.execute(sql,params=None)
self.commit()
return self.fetch()
#此处为了使代码简洁,在startmysql.py文件中调用完毕后,再在其文件中关闭连接
至此,dao包中的文件更新完成。
其中,jobpositiondao.py文件中sql的数据库查询操作是在startmysql.py中进行调用执行的。其中,startmysql.py除了执行sql语句查询以外,还有将结果进行图表显示的代码(是通过pygal模块实现的)
startmysql.py具体代码如下:
from Include.spidermovieproject.spidermovieproject.dao.jobpositiondao import JobPositionDao
import pygal #用于绘制图表
from pygal.style import LightColorizedStyle as LCS,LightenStyle as LS
jp =JobPositionDao() #实例化
print(jp.findGrade()) #调用其方法打印输出结果
print(jp.findManyGrade())
print(jp.pfGrade())
print(jp.pfManyGrade()) #显示的是此条查询结果
my_style = LS("#333366",base_style=LCS)
chart = pygal.Bar(my_style = my_style,x_label_rotation = 45,show_legend = False)
chart.title = '各电影分类中评分大于9.0的电影数量对比'
chart.x_labels = [jp.pfManyGrade()[0][0],jp.pfManyGrade()[1][0],jp.pfManyGrade()[2][0]]
plot_dicts = [{'value':jp.pfManyGrade()[0][1],'lable':'评分大于9.0的个数'},
{'value':jp.pfManyGrade()[1][1],'lable':'评分大于9.0的个数'},
{'value':jp.pfManyGrade()[2][1],'lable': '评分大于9.0的个数'},
]
chart.add('',plot_dicts)
chart.render_to_file('C:\\Users\\Administrator\\Desktop\\bar_descriptions.svg')
jp.close() #在此处关闭连接
最后,在启动文件startspider.py中,添加新的初始链接,具体代码如下:
#启动爬虫的脚本
from scrapy.cmdline import execute #execute执行
from Include.spidermovieproject.spidermovieproject.dao.taskdao import TaskDao
#启动爬虫
td = TaskDao()
# #(1)免费版
result,taskId = td.create(('2345电影免费版数据采集','http://dianying.2345.com/list/-------2.html?1/'))
if result>0:
execute(['scrapy','crawl','jobspider',
'-a','start_urls=http://dianying.2345.com/list/-------2.html?1/',
'-a','taskid='+str(taskId)])
#(2)会员版
# result,taskId = td.create(('2345电影会员版数据采集','http://dianying.2345.com/list/-------2.html?3'))
# if result>0:
# execute(['scrapy','crawl','jobspider',
# '-a','start_urls=http://dianying.2345.com/list/-------2.html?3',
# '-a','taskid='+str(taskId)])
#(3)预告版
# result,taskId = td.create(('2345电影预告版数据采集','http://dianying.2345.com/list/-------2.html?2'))
#
# if result>0:
# execute(['scrapy','crawl','jobspider',
# '-a','start_urls=http://dianying.2345.com/list/-------2.html?2',
# '-a','taskid='+str(taskId)])
以下为三张mysql数据表执行后的实现,截图如下:
movie_collect_task数据表主要存储采集电影分类的记录
movie_position数据表主要存储第一界面电影的采集数据
movie_position_detail数据表主要存储第一界面中的子界面(第二界面)电影的采集信息
给出数据表的形式主要是让sql语句查询更容易理解和扩展。
下面是sql筛选查询电影评分大于等于9.0的显示信息图表:
事实证明经典的电影不一定都氪金,哈哈哈!!!
趁阳光不燥,微风正好,趁你还年轻,别想太多,去爬你想要的网站吧!