Python爬虫之页面js渲染(scrapy+scrapy-splash+Docker实现)

论坛 期权论坛 期权     
鱼少侠   2019-7-28 23:18   4790   0
发现问题
    早些时候,笔者初学网络爬虫,想要做一个小爬虫,小试牛刀。分析网页时,用Chrome或者FireFox浏览器(个人推荐用FireFox,抓包效果更好)获取网页数据进行分析,这时数据都是非常完整的,如下图所示:




    然后屁颠颠去写代码,什么requests、urllib的各种各样的库,再用xpath、正则表达式、beautifulsoup去解析网页,结果发现屁数据也没有,如下图所示:



    然后就只能辛辛苦苦地去抓包,看数据都放哪儿了,而且还不一定能找到,简直无奈啊,真是气skr人。

找到原因
    这种网站的内容由前端的JS动态生成,由于呈现在网页上的内容是由JS生成而来,我们能够在浏览器上看得到,但是在HTML源码中却发现不了。

解决办法
    有三种方法,分别是:
(1)Selenium+webdriver(Chrome或者FireFox):这要求你系统有对应浏览器,并且过程中要全程开浏览器。说白了,就是你通过浏览器能看到啥,就能抓到啥。一般遇到特别复杂的验证码时,这个方法是有必要的,当然,开着浏览器爬虫的效率可想而知。

(2)selenium+phantomjs。PhantomJS是一个WebKit,他的使用方法和webdriver一样,但是他不需要开浏览器,简单地讲,它PhantomJS就是一个无界面的浏览器,你可以直接跑在无需GUI的linux服务器上,这点很赞。
(3)第三种方法就是本文主要阐述的内容,那就是基于Docker容器的scrapy-splash JS渲染服务和python分布式爬虫框架Scrapy,scrapy-splash作为js渲染服务,是基于Twisted和QT开发的轻量浏览器引擎,并且提供直接的http api,快速、轻量的特点使其容易进行分布式开发。 splash和scrapy融合,两种互相兼容彼此的特点,抓取效果甚佳。
    那么接下来笔者就手把手教大家第三种方法
首先,安装Docker容器,以及安装、开启Splash服务
    如何安装笔者就不详述,自行百度教程,非常简单。安装好Docker,进入有小鲸鱼的终端,输入如下指令安装splash服务(安装过程比较慢,请大家耐心等待)
  1. dock pull scrapinghub/splash
复制代码
如下图:




安装成功后,输入如下指令开启splash(指定服务端口为8050):
  1. docker run -p 8050:8050 scrapinghub/splash
复制代码
如下图:




编写爬虫
    笔者在这里以爬取去哪儿旅行官网南昌到杭州的火车票的详细数据,使用的开发工具为pycharm,指定框架为Scrapy。
首先,我们配置settings.py一些重要参数,如下代码,相应的参数解释已在代码中注明。设置渲染服务的url一定不能写错,协议(http),IP(开启Docker默认的IP),端口号(8050,开启splash服务指定的端口号),如果写错,电脑会积极拒绝。
  1. # 最好将机器协议修改为False,如果为true,大概率爬取不到东西
复制代码
  1. ROBOTSTXT_OBEY = False
复制代码
  1. # 开启ITEM_PIPELINES,使其可以ITEM_PIPELINES处理item数据
复制代码
  1. ITEM_PIPELINES = {
复制代码
  1.    'GoWhere.pipelines.GowherePipeline': 300,
复制代码
  1. }
复制代码
  1. # 设置爬虫中间件
复制代码
  1. SPIDER_MIDDLEWARES = {
复制代码
  1. #    'Douban.middlewares.DoubanSpiderMiddleware': 543,
复制代码
  1.      'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
复制代码
  1. }
复制代码
  1. [/code][code]#设置渲染服务的url,这是js渲染的关键之所在,这里的url即为刚刚开启Docker的Ip加splash服务指定的端口
复制代码
  1. SPLASH_URL="http://192.168.99.100:8050"
复制代码
  1. [/code][code]
复制代码
  1. # 添加Splash中间件,指定相应的优先级
复制代码
  1. #这里配置了三个下载中间件( DownloadMiddleware),是scrapy-splash的核心部分,我们不需要
复制代码
  1. #像对接selenium那样自己定制中间件,scrapy-splash已经为我们准备好了,直接配置即可
复制代码
  1. DOWNLOADER_MIDDLEWARES = {
复制代码
  1.    # 'Douban.middlewares.DoubanDownloaderMiddleware': 543,
复制代码
  1.     'scrapy_splash.SplashCookiesMiddleware': 723,
复制代码
  1.     'scrapy_splash.SplashMiddleware': 725,
复制代码
  1.     'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
复制代码
  1. }
复制代码
  1. [/code][code]HTTPCACHE_ENABLED = True
复制代码
  1. HTTPCACHE_EXPIRATION_SECS = 0
复制代码
  1. HTTPCACHE_DIR = 'httpcache'
复制代码
  1. [/code][code]# 去重过滤器
复制代码
  1. DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
复制代码
  1. #使用splash的http缓存
复制代码
  1. HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
复制代码
接下来,编写items.py文件
  1. class GowhereItem(scrapy.Item):
复制代码
  1.     # 车次
复制代码
  1.     train = scrapy.Field()
复制代码
  1.     # 起始站
复制代码
  1.     where = scrapy.Field()
复制代码
  1.     # 终点站
复制代码
  1.     to = scrapy.Field()
复制代码
  1.     # 发车起始时间
复制代码
  1.     time_from = scrapy.Field()
复制代码
  1.     # 到站时间
复制代码
  1.     time_to = scrapy.Field()
复制代码
  1.     # 乘车时间
复制代码
  1.     time = scrapy.Field()
复制代码
  1.     # 票价详情
复制代码
  1.     price1 = scrapy.Field()
复制代码
  1.     num1 = scrapy.Field()
复制代码
  1.     price2 = scrapy.Field()
复制代码
  1.     num2 = scrapy.Field()
复制代码
  1.     price3 = scrapy.Field()
复制代码
  1.     num3 = scrapy.Field()
复制代码
  1.     price4 = scrapy.Field()
复制代码
  1.     num4 = scrapy.Field()
复制代码
然后,爬虫的核心,编写spider,请求网页,清洗数据。强调一点,start_request需要重写,而且请求方法必须使用SplashRequest,否则前功尽弃。:
  1. import scrapy
复制代码
  1. from scrapy_splash import SplashRequest
复制代码
  1. from GoWhere.items import GowhereItem
复制代码
  1. import re
复制代码
  1. [/code][code]class GowhereSpider(scrapy.Spider):
复制代码
  1.     name = 'gowhere'
复制代码
  1.     allowed_domains = ['qunar.com']
复制代码
  1.     start_urls = ['https://train.qunar.com/stationToStation.htm?fromStation=%E5%8D%97%E6%98%8C&toStation=%E6%9D%AD%E5%B7%9E&date=2019-07-27']
复制代码
  1. [/code][code]    def start_requests(self):
复制代码
  1.         headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36'}
复制代码
  1.         for url in self.start_urls:
复制代码
  1.             yield SplashRequest(url,callback=self.parse,headers = headers ,args={"wait":5})
复制代码
  1. [/code][code]    def parse(self, response):
复制代码
  1.         # 获取列车所有信息列表
复制代码
  1.         results = response.xpath("//div[@class='js_listinfo']")
复制代码
  1.         for result in results:
复制代码
  1.             list = GowhereItem()
复制代码
  1.             # 获取车次
复制代码
  1.             train = result.xpath('./div[1]/h3/text()').extract_first()
复制代码
  1.             # 获取出发站
复制代码
  1.             where = result.xpath('./div[2]/p[1]/span/text()').extract_first()
复制代码
  1.             # 获取终点站
复制代码
  1.             to = result.xpath('./div[2]/p[2]/span/text()').extract_first()
复制代码
  1.             # 发车时间
复制代码
  1.             time_form = result.xpath('./div[3]/time[1]/text()').extract_first()
复制代码
  1.             # 到站时间
复制代码
  1.             time_to = result.xpath('./div[3]/time[2]/text()').extract_first()
复制代码
  1.             # 乘车时间
复制代码
  1.             time = result.xpath('./div[4]/time/text()').extract_first()
复制代码
  1.             # 票价
复制代码
  1.             price1 =result.xpath("./div[5]/p[1]/text()").extract_first() +" ¥" + result.xpath('./div[5]/p[1]/span/text()').extract_first()
复制代码
  1.             num1 = result.xpath('./div[6]/p[1]/text()').extract_first()
复制代码
  1.             price2 =result.xpath('./div[5]/p[2]/text()').extract_first() +" ¥" + result.xpath('./div[5]/p[2]/span/text()').extract_first()
复制代码
  1.             num2 = result.xpath('./div[6]/p[2]/span/text()').extract_first()
复制代码
  1.             list['train'] = train
复制代码
  1.             list['where'] = where
复制代码
  1.             list['to'] = to
复制代码
  1.             list['time_from'] = time_form
复制代码
  1.             list['time_to'] = time_to
复制代码
  1.             list['time'] = time
复制代码
  1.             list['price1'] = price1
复制代码
  1.             list['num1'] = num1
复制代码
  1.             list['price2'] = price2
复制代码
  1.             list['num2'] = num2
复制代码
  1.             ok = result.xpath('./div[5]/p[3]/text()').extract_first()
复制代码
  1.             if ok:
复制代码
  1.                 price3 = result.xpath('./div[5]/p[3]/text()').extract_first()+ " ¥" + result.xpath(
复制代码
  1.                 './div[5]/p[3]/span/text()').extract_first()
复制代码
  1.                 num3 = result.xpath('./div[6]/p[3]/span/text()').extract_first()
复制代码
  1.                 list['price3'] = price3
复制代码
  1.                 list['num3'] = num3
复制代码
  1.             flag = result.xpath('./div[5]/p[4]/text()').extract_first()
复制代码
  1.             if flag:
复制代码
  1.                 price4 = result.xpath('./div[5]/p[4]/text()').extract_first() + " ¥" + result.xpath(
复制代码
  1.                 './div[5]/p[4]/span/text()').extract_first()
复制代码
  1.                 num4 = result.xpath('./div[6]/p[4]/text()').extract_first()
复制代码
  1.                 list['price4'] = price4
复制代码
  1.                 if num4:
复制代码
  1.                     list['num4'] = num4
复制代码
  1. [/code][code]            yield list
复制代码
  1. [/code]最后,那就是编写Item pipelines,对item数据进行储存,笔者在这里使用本地储存(指定文件格式为csv)和mysql存储,代码具体如下:
  2. [list][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][*][/list][code]class GowherePipeline(object):
复制代码
  1.     def __init__(self):
复制代码
  1.         # 以写模式打开csv格式的文件,若不存在,代码会自动创建该文件,利用第三个参数将把写入的数据项产生的空格删除
复制代码
  1.         self.file = open("Go.csv",'w',newline='',encoding="utf-8")
复制代码
  1.         # 设置文件第一行的名称,即数据列名称,要与spider传过来的字典key名称一致
复制代码
  1.         self.fieldnames = ['train','where','to','time_from','time_to','time','price1','num1','price2','num2','price3','num3','price4','num4']
复制代码
  1.         # 指定文件的写入方式为csv字典写入,参数1为指定具体文件,参数2为指定字段名
复制代码
  1.         self.writer = csv.DictWriter(self.file,fieldnames=self.fieldnames)
复制代码
  1.         # 写入第一行数据(列名称)
复制代码
  1.         self.writer.writeheader()
复制代码
  1. [/code][code]        self.db = connect('localhost','root','1234','book',charset = 'utf8')
复制代码
  1. [/code][code]
复制代码
  1. [/code][code]    def process_item(self, item, spider):
复制代码
  1.         self.writer.writerow(item)
复制代码
  1.         print(type(item['train']))
复制代码
  1.         print(type(item['where']))
复制代码
  1.         cursor = self.db.cursor()
复制代码
  1.         sql = """ INSERT INTO test(train,address) VALUES ('{}','{}')""".format(item['train'],item['where'])
复制代码
  1.         cursor.execute(sql)
复制代码
  1.         self.db.commit()
复制代码
  1.         print("数据存储成功")
复制代码
  1. [/code][code]    def spider_close(self):
复制代码
  1.         self.file.close()
复制代码
  1.         self.db.close()
复制代码
数据效果展示:






至此,笔者要跟你们说再见了。
初来乍到,编写文章经验不足,还望多多关照,如有阐述不清楚请下方留言。。。
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP