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基础教程之分支、循环简单用法
Jun 16 Python
Python中异常重试的解决方案详解
May 05 Python
python中ASCII码字符与int之间的转换方法
Jul 09 Python
python ipset管理 增删白名单的方法
Jan 14 Python
浅谈pandas筛选出表中满足另一个表所有条件的数据方法
Feb 08 Python
Django使用中间键实现csrf认证详解
Jul 22 Python
python加载自定义词典实例
Dec 06 Python
django下创建多个app并设置urls方法
Aug 02 Python
Windows环境下Python3.6.8 importError: DLLload failed:找不到指定的模块
Nov 01 Python
python中pickle模块浅析
Dec 29 Python
Pytorch - TORCH.NN.INIT 参数初始化的操作
Feb 27 Python
python中subplot大小的设置步骤
Jun 28 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 mssql 分页SQL语句优化 持续影响
2009/04/26 PHP
Search File Contents PHP 搜索目录文本内容的代码
2010/02/21 PHP
ThinkPHP使用UTFWry地址库进行IP定位实例
2014/04/01 PHP
php采用file_get_contents代替使用curl实例
2014/11/07 PHP
浅谈PHP中pack、unpack的详细用法
2018/03/12 PHP
利用PHP如何统计Nginx日志的User Agent数据
2019/03/06 PHP
php封装的page分页类完整实例代码
2020/02/01 PHP
JavaScript判断两种格式的输入日期的正确性的代码
2007/03/25 Javascript
12个非常有创意的JavaScript小游戏
2010/03/18 Javascript
javascript 基础篇4 window对象,DOM
2012/03/14 Javascript
使用JavaScript修改浏览器URL地址栏的实现代码
2013/10/21 Javascript
json中换行符的处理方法示例介绍
2014/06/10 Javascript
用NODE.JS中的流编写工具是要注意的事项
2016/03/01 Javascript
Node 搭建一个静态资源服务器的实现
2019/05/20 Javascript
微信小程序实现订单倒计时
2020/11/01 Javascript
js实现九宫格布局效果
2020/05/28 Javascript
vue-cli 3如何使用vue-bootstrap-datetimepicker日期插件
2021/02/20 Vue.js
一个基于flask的web应用诞生 用户注册功能开发(5)
2017/04/11 Python
Django admin美化插件suit使用示例
2017/12/12 Python
基于python代码实现简易滤除数字的方法
2018/07/17 Python
使用Python+wxpy 找出微信里把你删除的好友实例
2019/02/21 Python
Python PyCharm如何进行断点调试
2019/07/05 Python
Python虚拟环境venv用法详解
2020/05/25 Python
pytorch 把图片数据转化成tensor的操作
2021/03/04 Python
使用HTML5和CSS3制作一个模态框的示例
2018/03/07 HTML / CSS
Vilebrequin欧洲官网:法国豪华泳装品牌(男士沙滩裤)
2018/04/14 全球购物
Kaufmann Mercantile官网:家居装饰、配件、户外及更多
2018/09/28 全球购物
天逸系统(武汉)有限公司Java笔试题
2015/12/29 面试题
数控个人求职信范文
2014/02/03 职场文书
协议书的格式
2014/04/23 职场文书
机关副主任个人四风问题整改措施
2014/09/26 职场文书
企业群众路线教育实践活动心得体会
2014/11/03 职场文书
践行党的群众路线心得体会
2014/11/05 职场文书
大学教师个人总结
2015/02/10 职场文书
个人先进事迹总结
2015/02/26 职场文书
体育教师研修感悟
2015/11/18 职场文书