Scrapy 对接 Selenium

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

本节我们来看一下 Scrapy 框架中如何对接 Selenium,这次我们依然是抓取淘宝商品信息,抓取逻辑和前文中用 Selenium 抓取淘宝商品一节完全相同。

首先新建项目,名称叫做scrapyseleniumtest,命令如下:

scrapy startproject scrapyseleniumtest

随后新建一个Spider,命令如下:

scrapy genspider taobao www.taobao.com

接下来把ROBOTSTXT_OBEY修改一下,修改为False。

ROBOTSTXT_OBEY = False

接下来首先定义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进行抓取,在这里采用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()里面对一些对象进行初始化,包括PhantomJS、WebDriverWait等对象,同时设置了页面大小和页面加载超时时间,随后在process_request()方法中我们首先通过Request的meta属性获取当前需要爬取的页码,然后调用PhantomJS对象的get()方法访问Request的对应的URL,这也就相当于从Request对象里面获取了请求链接然后再用PhantomJS去加载,而不再使用Scrapy里的Downloader。随后的处理等待和翻页的方法在此不再赘述,和前文的原理完全相同。最后等待页面加载完成之后,我们调用PhantomJS的page_source属性即可获取当前页面的源代码,然后用它来直接构造了一个HtmlResponse对象并返回,构造它的时候需要传入多个参数,如url、body等,这些参数实际上就是它的一些基础属性,可以查看官方文档看下它的结构: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里面开启它的调用:

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_URI和MONGO_DB的定义如下:

MONGO_URI = 'localhost'
MONGO_DB = 'taobao'

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

scrapy crawl taobao

运行结果如下:

再查看一下MongoDB,结果如下:

这样我们便成功在Scrapy中对接Selenium并实现了淘宝商品的抓取,本节代码:https://github.com/Python3WebSpider/ScrapySeleniumTest

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏从零开始学 Web 前端

从零开始学 Web 之 CSS(一)选择器

CSS 全称为 Cascading Style Sheets,中文翻译为“层叠样式表”,简称 CSS 样式表,所以称之为层叠样式表(Cascading Styl...

754
来自专栏Java帮帮-微信公众号-技术文章全总结

与Ajax同样重要的jQuery(1)

jQuery框架 jQuery 1.4 是企业主流版本,从jQuery1.6 开始引入大量新特性。最新版本 2.1.1,这里讲解以1.8.3为主(新版本主要是浏...

2756
来自专栏有趣的django

Django+Bootstrap+Mysql 搭建个人博客(三)

1550
来自专栏吾爱乐享

php学习之css小结1

1153
来自专栏河湾欢儿的专栏

2.布局解决方案- 水平布局<1>

水平布局 写出两个盒子并满足以下条件条件: 1.里面的宽度盒子不固定 2.外面盒子的宽度不固定

793
来自专栏cnblogs

CSS3新特性应用之字体排印

一、插入换行 ~:表示同辈元素之后指定类型的元素,如;elm1 ~ elm2表示,elm1之后的所有elm2元素,且elm1与elm2都是在同一个父级元素。 +...

1867
来自专栏前端小叙

绝对定位多个字居中显示的css

在工作中遇到一种情况,例如把一个div元素绝对定位到一个位置,但是该元素中的文字个数不确定,还要保证始终该文字是居中显示,则可以定义两个div,外层div绝对定...

2559
来自专栏阮一峰的网络日志

CSS使用技巧

在修改模板的过程中,需要重写CSS样式表。正好看到instantshift.com有一篇CSS常用技巧的总结文章,我就把它整理出来,供自己参考,也希望对大家有用...

681
来自专栏偏前端工程师的驿站

javascript实例:逐条记录停顿的走马灯

效果: ? 需求: 1.记录循环滚动; 2.每组记录之间不能有留白; 3.每条记录上移到容器的顶部时要停顿一下; 4.鼠标移入容器时停止滚动,移出时继续滚动。 ...

1715
来自专栏小白客

Web前端基础【2】--CSS基础

CSS指层叠样式表,用来定义如何显示HTML元素,一般和HTML配合使用。CSS样式表的目的是为了解决内容与表现分离的问题:即使同一个HTML文档也能表现出外观...

3136

扫码关注云+社区