scrapy结合selenium解析动态页面的实现


Posted in Python onSeptember 28, 2020

 1. 问题

虽然scrapy能够完美且快速的抓取静态页面,但是在现实中,目前绝大多数网站的页面都是动态页面,动态页面中的部分内容是浏览器运行页面中的JavaScript脚本动态生成的,爬取相对困难;

比如你信心满满的写好了一个爬虫,写好了目标内容的选择器,一跑起来发现根本找不到这个元素,当时肯定一万个黑人问号

scrapy结合selenium解析动态页面的实现

于是你在浏览器里打开F12,一顿操作,发现原来这你妹的是ajax加载的,不然就是硬编码在js代码里的,blabla的…

然后你得去调ajax的接口,然后解析json啊,转成python字典啊,然后才能拿到你想要的东西

妹的就不能对我们这些小爬爬友好一点吗?

于是大家伙肯定想过,“为啥不能浏览器看到是咋样的html页面,我们爬虫得到的也是同样的html页面呢? 要是可以,那得多么美滋滋啊”

scrapy结合selenium解析动态页面的实现

2. 解决方案

既然是想要得到和浏览器一模一样的html页面,那我们就先用浏览器渲染一波目标网页,然后再将浏览器渲染后的html拿给scrapy进行进一步解析不就好了吗

scrapy结合selenium解析动态页面的实现

2.1 获取浏览器渲染后的html

有了思路,肯定是网上搜一波然后开干啊,搜python操作浏览器的库啊

货比三家之后,找到了selenium这货

selenium可以模拟真实浏览器,自动化测试工具,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题。

卧槽,这就是我们要的东西啦

先试一波看看效果如何,目标网址http://quotes.toscrape.com/js/

scrapy结合selenium解析动态页面的实现

别着急,先来看一下网页源码

scrapy结合selenium解析动态页面的实现

我们想要的div.quote被硬编码在js代码中

用selenium试一下看能不能获取到浏览器渲染后的html

scrapy结合selenium解析动态页面的实现

from selenium import webdriver

# 控制火狐浏览器
browser = webdriver.Firefox()

# 访问我们的目标网址
browser.get("http://quotes.toscrape.com/js/")

# 获取渲染后的html页面
html = browser.page_source

perfect,到这里我们已经顺利拿到浏览器渲染后的html了,selenium大法好啊?

2.2 通过下载器中间件返回渲染过后html的Response

这里先放一张scrapy的流程图

scrapy结合selenium解析动态页面的实现

所以我们只需要在scrapy下载网页(downloader下载好网页,构造Response返回)之前,通过下载器中间件返回我们自己<通过渲染后html构造的Response>不就可以了吗?

scrapy结合selenium解析动态页面的实现

道理我都懂,关键是在哪一步使用浏览器呢?

分析:

(1)我们的scrapy可能是有很多个爬虫的,有些爬虫处理的是纯纯的静态页面,而有些是处理的纯纯的动态页面,又有些是动静态结合的页面(有可能列表页是静态的,正文页是动态的),如果把<浏览器调用代码>放在下载器中间件中,那么除非特别区分哪些爬虫需要selenium,否则每一个爬虫都用selenium去下载解析页面的话,实在是太浪费资源了,就相当于杀鸡用牛刀了,所以得出结论,<浏览器调用代码>应该是放置于Spider类中更好一点;

(2)如果放置于Spider类中,就意味着一个爬虫占用一个浏览器的一个tab页,如果这个爬虫里的某些Request需要selenium,而某些不需要呢? 所以我们还要在区分一下Request;

结论:

SeleniumDownloaderMiddleware(selenium专用下载器中间件):负责返回浏览器渲染后的ResponseSeleniumSpider(selenium专用Spider):一个spider开一个浏览器SeleniumRequest:只是继承一下scrapy.Request,然后pass,好区分哪些Request需要启用selenium进行解析页面,相当于改个名

3. 撸代码,盘他

 3.1 自定义Request

#!usr/bin/env python 
# -*- coding:utf-8 _*-
""" 
@author:Joshua
@description:
  只是继承一下scrapy.Request,然后pass,好区分哪些Request需要启用selenium进行解析页面,相当于改个名
"""
import scrapy

class SeleniumRequest(scrapy.Request):
  """
  selenium专用Request类
  """
  pass

3.2 自定义Spider

#!usr/bin/env python 
# -*- coding:utf-8 _*-
""" 
@author:Joshua 
@description:
  一个spider开一个浏览器
"""
import logging
import scrapy
from selenium import webdriver


class SeleniumSpider(scrapy.Spider):
  """
  Selenium专用spider

  一个spider开一个浏览器

  浏览器驱动下载地址:http://www.cnblogs.com/qiezizi/p/8632058.html
  """
  # 浏览器是否设置无头模式,仅测试时可以为False
  SetHeadless = True

  # 是否允许浏览器使用cookies
  EnableBrowserCookies = True

  def __init__(self, *args, **kwargs):
    super(SeleniumSpider, self).__init__(*args, **kwargs)
    
    # 获取浏览器操控权
    self.browser = self._get_browser()

  def _get_browser(self):
    """
    返回浏览器实例
    """
    # 设置selenium与urllib3的logger的日志等级为ERROR
    # 如果不加这一步,运行爬虫过程中将会产生一大堆无用输出
    logging.getLogger('selenium').setLevel('ERROR')
    logging.getLogger('urllib3').setLevel('ERROR')
    
    # selenium已经放弃了PhantomJS,开始支持firefox与chrome的无头模式
    return self._use_firefox()

  def _use_firefox(self):
    """
    使用selenium操作火狐浏览器
    """
    profile = webdriver.FirefoxProfile()
    options = webdriver.FirefoxOptions()
    
    # 下面一系列禁用操作是为了减少selenium的资源耗用,加速scrapy
    
    # 禁用图片
    profile.set_preference('permissions.default.image', 2)
    profile.set_preference('browser.migration.version', 9001)
    # 禁用css
    profile.set_preference('permissions.default.stylesheet', 2)
    # 禁用flash
    profile.set_preference('dom.ipc.plugins.enabled.libflashplayer.so', 'false')
    
    # 如果EnableBrowserCookies的值设为False,那么禁用cookies
    if hasattr(self, "EnableBrowserCookies") and self.EnableBrowserCookies:
      # •值1 - 阻止所有第三方cookie。
      # •值2 - 阻止所有cookie。
      # •值3 - 阻止来自未访问网站的cookie。
      # •值4 - 新的Cookie Jar策略(阻止对跟踪器的存储访问)
      profile.set_preference("network.cookie.cookieBehavior", 2)
    
    # 默认是无头模式,意思是浏览器将会在后台运行,也是为了加速scrapy
    # 我们可不想跑着爬虫时,旁边还显示着浏览器访问的页面
    # 调试的时候可以把SetHeadless设为False,看一下跑着爬虫时候,浏览器在干什么
    if self.SetHeadless:
      # 无头模式,无UI
      options.add_argument('-headless')

    # 禁用gpu加速
    options.add_argument('--disable-gpu')

    return webdriver.Firefox(firefox_profile=profile, options=options)

  def selenium_func(self, request):
    """
    在返回浏览器渲染的html前做一些事情
      1.比如等待浏览器页面中的某个元素出现后,再返回渲染后的html;
      2.比如将页面切换进iframe中的页面;
    
    在需要使用的子类中要重写该方法,并利用self.browser操作浏览器
    """
    pass

  def closed(self, reason):
    # 在爬虫关闭后,关闭浏览器的所有tab页,并关闭浏览器
    self.browser.quit()
    
    # 日志记录一下
    self.logger.info("selenium已关闭浏览器...")

之所以不把获取浏览器的具体代码写在__init__方法里,是因为笔者之前写的代码里考虑过

  • 两种浏览器的调用(支持firefox与chrome),虽然后来感觉还是firefox比较方便,因为所有版本的火狐浏览器的驱动都是一样的,但是谷歌浏览器是不同版本的浏览器必须用不同版本的驱动(坑爹啊- -'')
  • 自动区分不同的操作系统并选择对应操作系统的浏览器驱动

额… 所以上面spider的代码是精简过的版本

备注: 针对selenium做了一系列的优化加速,启用了无头模式,禁用了css、flash、图片、gpu加速等… 因为爬虫嘛,肯定是跑的越快越好啦?

3.3 自定义下载器中间件

#!usr/bin/env python 
# -*- coding:utf-8 _*-
""" 
@author:Joshua 
@description:
  负责返回浏览器渲染后的Response
"""
import hashlib
import time
from scrapy.http import HtmlResponse
from twisted.internet import defer, threads
from tender_scrapy.extendsion.selenium.spider import SeleniumSpider
from tender_scrapy.extendsion.selenium.requests import SeleniumRequest


class SeleniumDownloaderMiddleware(object):
  """
  Selenium下载器中间件
  """
  
  def process_request(self, request, spider):
    # 如果spider为SeleniumSpider的实例,并且request为SeleniumRequest的实例
    # 那么该Request就认定为需要启用selenium来进行渲染html
    if isinstance(spider, SeleniumSpider) and isinstance(request, SeleniumRequest):
      # 控制浏览器打开目标链接
      browser.get(request.url)
      
      # 在构造渲染后的HtmlResponse之前,做一些事情
      #1.比如等待浏览器页面中的某个元素出现后,再返回渲染后的html;
      #2.比如将页面切换进iframe中的页面;
      spider.selenium_func(request)
      
      # 获取浏览器渲染后的html
      html = browser.page_source
      
      # 构造Response
      # 这个Response将会被你的爬虫进一步处理
      return HtmlResponse(url=browser.current_url, request=request, body=html.encode(), encoding="utf-8")

这里要说一下下载器中间件的process_request方法,当每个request通过下载中间件时,该方法被调用。

  1. process_request() 必须返回其中之一: 返回 None 、返回一个 Response 对象、返回一个 Request 对象或raise IgnoreRequest 。
  2. 如果其返回 Response 对象,Scrapy将不会调用 任何 其他的 process_request() 或 process_exception() 方法,或相应地下载函数; 其将返回该response。 已安装的中间件的 process_response() 方法则会在每个response返回时被调用。

更详细的关于下载器中间件的资料 -> https://scrapy-chs.readthedocs.io/zh_CN/0.24/topics/downloader-middleware.html#id2

3.4 额外的工具

眼尖的读者可能注意到SeleniumSpider类里有个selenium_func方法,并且在SeleniumDownloaderMiddleware的process_request方法返回Resposne之前调用了spider的selenium_func方法

scrapy结合selenium解析动态页面的实现

这样做的好处是,我们可以在构造渲染后的HtmlResponse之前,做一些事情(比如…那种…很骚的那种…你懂的)

  • 比如等待浏览器页面中的某个元素出现后,再返回渲染后的html;
  • 比如将页面切换进iframe中的页面,然后返回iframe里面的html(够骚吗);

等待某个元素出现,然后再返回渲染后的html这种操作很常见的,比如你访问一篇文章,它的正文是ajax加载然后js添加到html里的,ajax是需要时间的,但是selenium并不会等待所有请求都完毕后再返回

解决方法:

  1. 您可以通过browser.implicitly_wait(30),来强制selenium等待30秒(无论元素是否加载出来,都必须等待30秒)
  2. 可以通过等待,直到某个元素出现,然后再返回html

所以笔者对<等待某个元素出现>这一功能做了进一步的封装,代码如下

#!usr/bin/env python 
# -*- coding:utf-8 _*-
""" 
@author:Joshua 
@description:
"""
import functools
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC


def waitFor(browser, select_arg, select_method, timeout=2):
  """
  阻塞等待某个元素的出现直到timeout结束

  :param browser:浏览器实例
  :param select_method:所使用的选择器方法
  :param select_arg:选择器参数
  :param timeout:超时时间
  :return:
  """
  element = WebDriverWait(browser, timeout).until(
    EC.presence_of_element_located((select_method, select_arg))
  )


# 用xpath选择器等待元素
waitForXpath = functools.partial(waitFor, select_method=By.XPATH)

# 用css选择器等待元素
waitForCss = functools.partial(waitFor, select_method=By.CSS_SELECTOR)

waitForXpath与waitForCss 是waitFor函数的两个偏函数,意思这两个偏函数是设置了select_method参数默认值的waitFor函数,分别应用不同的选择器来定位元素

4. 中间件当然要在settings中激活一下

scrapy结合selenium解析动态页面的实现

在我们scrapy项目的settings文件中的DOWNLOADER_MIDDLEWARES字典中添加到适当的位置即可

5. 使用示例

5.1一个完整的爬虫示例

# -*- coding: utf-8 -*-
"""
@author:Joshua
@description:
  整合selenium的爬虫示例
"""
import scrapy
from my_project.requests import SeleniumRequest
from my_project.spider import SeleniumSpider
from my_project.tools import waitForXpath


# 这个爬虫类继承了SeleniumSpider
# 在爬虫跑起来的时候,将启动一个浏览器
class SeleniumExampleSpider(SeleniumSpider):
  """
  这一网站,他的列表页是静态的,但是内容页是动态的
  所以,用selenium试一下,目标是扣出内容页的#content
  """
  name = 'selenium_example'
  allowed_domains = ['pingdingshan.hngp.gov.cn']
  url_format = 'http://pingdingshan.hngp.gov.cn/pingdingshan/ggcx?appCode=H65&channelCode=0301&bz=0&pageSize=20&pageNo={page_num}'

  def start_requests(self):
    """
    开始发起请求,记录页码
    """
    start_url = self.url_format.format(page_num=1)
    meta = dict(page_num=1)
    # 列表页是静态的,所以不需要启用selenium,用普通的scrapy.Request就可以了
    yield scrapy.Request(start_url, meta=meta, callback=self.parse)

  def parse(self, response):
    """
    从列表页解析出正文的url
    """
    meta = response.meta
    all_li = response.css("div.List2>ul>li")

    # 列表
    for li in all_li:
      content_href = li.xpath('./a/@href').extract()
      content_url = response.urljoin(content_href)
      # 内容页是动态的,#content是ajax动态加载的,所以启用一波selenium
      yield SeleniumRequest(url=content_url, meta=meta, callback=self.parse_content)

    # 翻页
    meta['page_num'] += 1
    next_url = self.url_format.format(page_num=meta['page_num'])
    # 列表页是静态的,所以不需要启用selenium,用普通的scrapy.Request就可以了
    yield scrapy.Request(url=next_url, meta=meta, callback=self.parse)

  def parse_content(self, response):
    """
    解析正文内容
    """
    content = response.css('#content').extract_first()
    yield dict(content=content)
   
  def selenium_func(self, request):
    # 这个方法会在我们的下载器中间件返回Response之前被调用
    
    # 等待content内容加载成功后,再继续
    # 这样的话,我们就能在parse_content方法里应用选择器扣出#content了
    waitForXpath(self.browser, "//*[@id='content']/*[1]")

5.2 更骚一点的操作…

假如内容页的目标信息处于iframe中,我们可以将窗口切换进目标iframe里面,然后返回iframe的html

要实现这样的操作,只需要重写一下SeleniumSpider子类中的selenium_func方法

要注意到SeleniumSpider中的selenium_func其实是啥也没做的,一个pass,所有的功能都在子类中重写

def selenium_func(self, request):
  # 找到id为myPanel的iframe
  target = self.browser.find_element_by_xpath("//iframe[@id='myPanel']")
  # 将浏览器的窗口切换进该iframe中
  # 那么切换后的self.browser的page_source将会是iframe的html
  self.browser.switch_to.frame(target)

6. selenium的一些替代(一些解决动态页面别的方法)

scrapy官方推荐的scrapy_splash

优点

  • 是异步的
  • 可以将部署scrapy的服务器与部署splash的服务器分离开
  • 留给读者遐想的空间

本人觉得的缺点

  • 喂喂,lua脚本很麻烦好吗…(大牛请别打我)

最新的异步pyppeteer操控浏览器

优点

  • 调用浏览器是异步的,操控的单位是tab页,速度更快
  • 留给读者遐想的空间

本人觉得的缺点

  • 因为pyppeteer是python版puppeteer,所以puppeteer的一些毛病,pyppeteer无可避免的完美继承
  • 笔者试过将pyppeteer整合至scrapy中,在异步中,scrapy跑起来爬虫,总会偶尔timeout之类的…

anyway,上面两个都是不错的替代,有兴趣的读者可以试一波

7. scrapy整合selenium的一些缺点

  • selenium是阻塞的,所以速度会慢些
  • 对于一些稍微简单的动态页面,最好还是自己去解析一下接口,不要太过依赖selenium,因为selenium带来便利的同时,是更多资源的占用
  • 整合selenium的scrapy项目不宜大规模的爬取,比如你在自己的机子上写好了一个一个的爬虫,跑起来也没毛病,速度也能接受,然后你很开心地在服务器上部署了你项目上的100+个爬虫(里面有50%左右的爬虫启用了selenium),当他们跑起来的时候,服务器就原地爆炸了… 为啥? 因为相当于服务器同时开了50多个浏览器在跑,内存顶不住啊(土豪忽略…)

到此这篇关于scrapy结合selenium解析动态页面的实现的文章就介绍到这了,更多相关scrapy selenium解析动态页面内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python的dict,set,list,tuple应用详解
Jul 24 Python
Python常见工厂函数用法示例
Mar 21 Python
Python字符串、整数、和浮点型数相互转换实例
Aug 04 Python
Python常用爬虫代码总结方便查询
Feb 25 Python
Python TestCase中的断言方法介绍
May 02 Python
python实现windows倒计时锁屏功能
Jul 30 Python
django drf框架自带的路由及最简化的视图
Sep 10 Python
基于Python解密仿射密码
Oct 21 Python
Python+opencv+pyaudio实现带声音屏幕录制
Dec 23 Python
对Tensorflow中Device实例的生成和管理详解
Feb 04 Python
python GUI库图形界面开发之PyQt5控件QTableWidget详细使用方法与属性
Feb 25 Python
sklearn中的交叉验证的实现(Cross-Validation)
Feb 22 Python
互斥锁解决 Python 中多线程共享全局变量的问题(推荐)
Sep 28 #Python
python 常见的反爬虫策略
Sep 27 #Python
python 5个实用的技巧
Sep 27 #Python
Python日志器使用方法及原理解析
Sep 27 #Python
python 爬取免费简历模板网站的示例
Sep 27 #Python
python如何提升爬虫效率
Sep 27 #Python
python操作链表的示例代码
Sep 27 #Python
You might like
php 异常处理实现代码
2009/03/10 PHP
destoon利用Rewrite规则设置网站安全
2014/06/21 PHP
Thinkphp模板标签if和eq的区别和比较实例分析
2015/07/01 PHP
Symfony2函数用法实例分析
2016/03/18 PHP
PHP高精确度运算BC函数库实例详解
2017/08/15 PHP
js获取IP和PcName(IE)在vs中可用
2013/08/02 Javascript
js实现仿Windows风格选项卡和按钮效果实例
2015/05/13 Javascript
JavaScript几种数组去掉重复值的方法推荐
2016/04/12 Javascript
jQuery实现响应鼠标事件的图片透明效果【附demo源码下载】
2016/06/16 Javascript
浅谈js数据类型判断与数组判断
2016/08/29 Javascript
jQuery实现的自定义滚动条实例详解
2016/09/20 Javascript
微信小程序 ecshop地址三级联动实现实例代码
2017/02/28 Javascript
jQuery插件jsonview展示json数据
2018/05/26 jQuery
微信小程序实现红包功能(后端PHP实现逻辑)
2018/07/11 Javascript
微信小程序基于picker实现级联菜单
2019/02/15 Javascript
JavaScript 预解析的4种实现方法解析
2019/09/03 Javascript
[06:40]2014DOTA2西雅图国际邀请赛 DK战队巡礼
2014/07/07 DOTA
低版本中Python除法运算小技巧
2015/04/05 Python
说一说Python logging
2016/04/15 Python
Python编程之字符串模板(Template)用法实例分析
2017/07/22 Python
python3+PyQt5实现支持多线程的页面索引器应用程序
2018/04/20 Python
python删除文本中行数标签的方法
2018/05/31 Python
Django组件之cookie与session的使用方法
2019/01/10 Python
解决win7操作系统Python3.7.1安装后启动提示缺少.dll文件问题
2019/07/15 Python
python3实现将json对象存入Redis以及数据的导入导出
2020/07/16 Python
Python confluent kafka客户端配置kerberos认证流程详解
2020/10/12 Python
python+requests实现接口测试的完整步骤
2020/10/27 Python
倩碧香港官方网站:Clinique香港
2017/11/13 全球购物
幼儿教师个人求职信范文
2013/09/21 职场文书
幼师求职自荐信范文
2014/01/26 职场文书
2014年预备党员学习两会心得体会
2014/03/17 职场文书
亲子活动总结
2014/04/26 职场文书
考试作弊检讨书
2015/01/27 职场文书
数据库之SQL技巧整理案例
2021/07/07 SQL Server
Go 通过结构struct实现接口interface的问题
2021/10/05 Golang
MSSQL基本语法操作
2022/04/11 SQL Server