Scrapy框架的使用之Scrapy对接Selenium

Scrapy抓取页面的方式和requests库类似,都是直接模拟HTTP请求,而Scrapy也不能抓取JavaScript动态渲染的页面。在前文中抓取JavaScript渲染的页面有两种方式。一种是分析Ajax请求,找到其对应的接口抓取,Scrapy同样可以用此种方式抓取。另一种是直接用Selenium或Splash模拟浏览器进行抓取,我们不需要关心页面后台发生的请求,也不需要分析渲染过程,只需要关心页面最终结果即可,可见即可爬。那么,如果Scrapy可以对接Selenium,那Scrapy就可以处理任何网站的抓取了。

一、本节目标

本节我们来看看Scrapy框架如何对接Selenium,以PhantomJS进行演示。我们依然抓取淘宝商品信息,抓取逻辑和前文中用Selenium抓取淘宝商品完全相同。

二、准备工作

请确保PhantomJS和MongoDB已经安装好并可以正常运行,安装好Scrapy、Selenium、PyMongo库。

三、新建项目

首先新建项目,名为scrapyseleniumtest,命令如下所示:

scrapy startproject scrapyseleniumtest

新建一个Spider,命令如下所示:

scrapy genspider taobao www.taobao.com

修改ROBOTSTXT_OBEYFalse,如下所示:

ROBOTSTXT_OBEY = False

四、定义 Item

首先定义Item对象,名为ProductItem,代码如下所示:

from scrapy import Item, Field

class ProductItem(Item):

    collection = 'products'
    image = Field()
    price = Field()
    deal = Field()
    title = Field()
    shop = Field()
    location = Field()

这里我们定义了6个Field,也就是6个字段,跟之前的案例完全相同。然后定义了一个collection属性,即此Item保存的MongoDB的Collection名称。

初步实现Spider的start_requests()方法,如下所示:

from scrapy import Request, Spider
from urllib.parse import quote
from scrapyseleniumtest.items import ProductItem

class TaobaoSpider(Spider):
    name = 'taobao'
    allowed_domains = ['www.taobao.com']
    base_url = 'https://s.taobao.com/search?q='

    def start_requests(self):
        for keyword in self.settings.get('KEYWORDS'):
            for page in range(1, self.settings.get('MAX_PAGE') + 1):
                url = self.base_url + quote(keyword)
                yield Request(url=url, callback=self.parse, meta={'page': page}, dont_filter=True)

首先定义了一个base_url,即商品列表的URL,其后拼接一个搜索关键字就是该关键字在淘宝的搜索结果商品列表页面。

关键字用KEYWORDS标识,定义为一个列表。最大翻页页码用MAX_PAGE表示。它们统一定义在setttings.py里面,如下所示:

KEYWORDS = ['iPad']
MAX_PAGE = 100

start_requests()方法里,我们首先遍历了关键字,遍历了分页页码,构造并生成Request。由于每次搜索的URL是相同的,所以分页页码用meta参数来传递,同时设置dont_filter不去重。这样爬虫启动的时候,就会生成每个关键字对应的商品列表的每一页的请求了。

五、对接 Selenium

接下来我们需要处理这些请求的抓取。这次我们对接Selenium进行抓取,采用Downloader Middleware来实现。在Middleware里面的process_request()方法里对每个抓取请求进行处理,启动浏览器并进行页面渲染,再将渲染后的结果构造一个HtmlResponse对象返回。代码实现如下所示:

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from scrapy.http import HtmlResponse
from logging import getLogger

class SeleniumMiddleware():
    def __init__(self, timeout=None, service_args=[]):
        self.logger = getLogger(__name__)
        self.timeout = timeout
        self.browser = webdriver.PhantomJS(service_args=service_args)
        self.browser.set_window_size(1400, 700)
        self.browser.set_page_load_timeout(self.timeout)
        self.wait = WebDriverWait(self.browser, self.timeout)

    def __del__(self):
        self.browser.close()

    def process_request(self, request, spider):
        """
        用PhantomJS抓取页面
        :param request: Request对象
        :param spider: Spider对象
        :return: HtmlResponse
        """
        self.logger.debug('PhantomJS is Starting')
        page = request.meta.get('page', 1)
        try:
            self.browser.get(request.url)
            if page > 1:
                input = self.wait.until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form > input')))
                submit = self.wait.until(
                    EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form > span.btn.J_Submit')))
                input.clear()
                input.send_keys(page)
                submit.click()
            self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '#mainsrp-pager li.item.active > span'), str(page)))
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))
            return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding='utf-8', status=200)
        except TimeoutException:
            return HtmlResponse(url=request.url, status=500, request=request)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),
                   service_args=crawler.settings.get('PHANTOMJS_SERVICE_ARGS'))

首先我们在__init__()里对一些对象进行初始化,包括PhantomJSWebDriverWait等对象,同时设置页面大小和页面加载超时时间。在process_request()方法中,我们通过Request的meta属性获取当前需要爬取的页码,调用PhantomJS对象的get()方法访问Request的对应的URL。这就相当于从Request对象里获取请求链接,然后再用PhantomJS加载,而不再使用Scrapy里的Downloader。

随后的处理等待和翻页的方法在此不再赘述,和前文的原理完全相同。最后,页面加载完成之后,我们调用PhantomJS的page_source属性即可获取当前页面的源代码,然后用它来直接构造并返回一个HtmlResponse对象。构造这个对象的时候需要传入多个参数,如urlbody等,这些参数实际上就是它的基础属性。可以在官方文档查看HtmlResponse对象的结构:https://doc.scrapy.org/en/latest/topics/request-response.html。这样我们就成功利用PhantomJS来代替Scrapy完成了页面的加载,最后将Response返回即可。

有人可能会纳闷:为什么实现这么一个Downloader Middleware就可以了?之前的Request对象怎么办?Scrapy不再处理了吗?Response返回后又传递给了谁?

是的,Request对象到这里就不会再处理了,也不会再像以前一样交给Downloader下载。Response会直接传给Spider进行解析。

我们需要回顾一下Downloader Middleware的process_request()方法的处理逻辑,内容如下所示:

process_request()方法返回Response对象的时候,更低优先级的Downloader Middleware的process_request()process_exception()方法就不会被继续调用了,转而开始执行每个Downloader Middleware的process_response()方法,调用完毕之后直接将Response对象发送给Spider来处理。

这里直接返回了一个HtmlResponse对象,它是Response的子类,返回之后便顺次调用每个Downloader Middleware的process_response()方法。而在process_response()中我们没有对其做特殊处理,它会被发送给Spider,传给Request的回调函数进行解析。

到现在,我们应该能了解Downloader Middleware实现Selenium对接的原理了。

在settings.py里,我们设置调用刚才定义的SeleniumMiddleware,如下所示:

DOWNLOADER_MIDDLEWARES = {
    'scrapyseleniumtest.middlewares.SeleniumMiddleware': 543,
}

六、解析页面

Response对象就会回传给Spider内的回调函数进行解析。所以下一步我们就实现其回调函数,对网页来进行解析,代码如下所示:

def parse(self, response):
    products = response.xpath(
        '//div[@id="mainsrp-itemlist"]//div[@class="items"][1]//div[contains(@class, "item")]')
    for product in products:
        item = ProductItem()
        item['price'] = ''.join(product.xpath('.//div[contains(@class, "price")]//text()').extract()).strip()
        item['title'] = ''.join(product.xpath('.//div[contains(@class, "title")]//text()').extract()).strip()
        item['shop'] = ''.join(product.xpath('.//div[contains(@class, "shop")]//text()').extract()).strip()
        item['image'] = ''.join(product.xpath('.//div[@class="pic"]//img[contains(@class, "img")]/@data-src').extract()).strip()
        item['deal'] = product.xpath('.//div[contains(@class, "deal-cnt")]//text()').extract_first()
        item['location'] = product.xpath('.//div[contains(@class, "location")]//text()').extract_first()
        yield item

在这里我们使用XPath进行解析,调用response变量的xpath()方法即可。首先我们传递选取所有商品对应的XPath,可以匹配所有商品,随后对结果进行遍历,依次选取每个商品的名称、价格、图片等内容,构造并返回一个ProductItem对象。

七、存储结果

最后我们实现一个Item Pipeline,将结果保存到MongoDB,如下所示:

import pymongo

class MongoPipeline(object):
    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(mongo_uri=crawler.settings.get('MONGO_URI'), mongo_db=crawler.settings.get('MONGO_DB'))

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        self.db[item.collection].insert(dict(item))
        return item

    def close_spider(self, spider):
        self.client.close()

此实现和前文中存储到MongoDB的方法完全一致,原理不再赘述。记得在settings.py中开启它的调用,如下所示:

ITEM_PIPELINES = {
    'scrapyseleniumtest.pipelines.MongoPipeline': 300,
}

其中,MONGO_URIMONGO_DB的定义如下所示:

MONGO_URI = 'localhost'
MONGO_DB = 'taobao'

八、运行

整个项目就完成了,执行如下命令启动抓取即可:

scrapy crawl taobao

运行结果如下图所示。

查看MongoDB,结果如下图所示。

这样我们便成功在Scrapy中对接Selenium并实现了淘宝商品的抓取。

九、本节代码

本节代码地址为:https://github.com/Python3WebSpider/ScrapySeleniumTest。

十、结语

我们通过实现Downloader Middleware的方式实现了Selenium的对接。但这种方法其实是阻塞式的,也就是说这样就破坏了Scrapy异步处理的逻辑,速度会受到影响。为了不破坏其异步加载逻辑,我们可以使用Splash实现。下一节我们再来看看Scrapy对接Splash的方式。

原文发布于微信公众号 - 进击的Coder(FightingCoder)

原文发表时间:2018-05-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码洞

大厨小鲜——自己动手实现一个极简Web框架

上节课我们自己动手制作了一个RPC框架,本节课我们挑战一个稍有难度的一点的任务,手动制作一个Web框架。 我不太愿意叫什么MVC框架,因为我在制作这个小项目的时...

912
来自专栏Android 研究

OKHttp源码解析(九):OKHTTP连接中三个"核心"RealConnection、ConnectionPool、StreamAllocation

RealConnection是Connection的实现类,代表着链接socket的链路,如果拥有了一个RealConnection就代表了我们已经跟服务器有了...

1095
来自专栏FreeBuf

商业级别Fortify白盒神器介绍与使用分析

? 什么是fortify它又能干些什么? 答:fottify全名叫:Fortify SCA,是HP的产品,是一个静态的、白盒的软件源代码安全测试工具。 它通过...

3495
来自专栏微信小程序开发

微信小程序开发常见问题(四)

知晓程序员,专注微信小程序开发的程序员! 一、小程序不同页面之间的传值方式 a、URL传值 这种方式最常用,比如: wx.navigateTo({ url...

3645
来自专栏cnblogs

简单实现 C# 与 Javascript的兼容

本文章介绍下自己这刚实现的一个c#与js交互的插件。需求来源于一次与朋友的讨论。主要对话如下: 朋友:最近我想模拟一些数据,来测试我现在写的接口,但手工编写这些...

21310
来自专栏月牙寂

golang时间戳格式化与解析

第一时间获取文章,可以关注本人公众号 月牙寂道长 yueyajidaozhang

36811
来自专栏黑白安全

DLL注入新姿势:反射式DLL注入研究

在分析koadic渗透利器时,发现它有一个注入模块,其DLL注入实现方式和一般的注入方式不一样。搜索了一下发现是由HarmanySecurity的Stephen...

813
来自专栏大内老A

[WCF权限控制]WCF的三种授权模式

前面的两篇文章(《从两个重要的概念谈起:Identity与Principal[上篇]》和《从两个重要的概念谈起:Identity与Principal[下篇]》)...

1936
来自专栏linux驱动个人学习

Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

exit是c语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用atexit注册的函数等, 在c语言的main函数中调用return就等价...

1183
来自专栏分布式系统和大数据处理

Asp.Net 用户验证(自定义IPrincipal和IIdentity)

前一段时间有两个朋友问我,为什么在HttpModule中无法获得到Session值,因为他们希望自定义一个HttpModule,然后在其中获取Session来进...

923

扫码关注云+社区