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 条评论
登录 后参与评论

相关文章

来自专栏柠檬先生

Angularjs基础(一)

(一) 模型——视图——控制器     端对端的解决方案,AngularJS 试图成为WEB 应用中的一种段对端的解决方案。AngylarJS 的出众  ...

19010
来自专栏一个会写诗的程序员的博客

6.3 Spring Boot集成mongodb开发小结

本章我们通过SpringBoot集成mongodb,Java,Kotlin开发一个极简社区文章博客系统。

1073
来自专栏崔庆才的专栏

Scrapy框架的使用之Scrapy对接Selenium

2754
来自专栏张善友的专栏

MSBuild的简单介绍与使用

MSBuild 是 Microsoft 和 Visual Studio的生成系统。它不仅仅是一个构造工具,应该称之为拥有相当强大扩展能力的自动化平台。MSBui...

1915
来自专栏编程软文

1小时轻松上手springmvc,视频网站后台开发

1463
来自专栏非典型技术宅

Swift多线程之Operation:异步加载CollectionView图片1. Operation 设置依赖关系2. 前置知识点内容3. CollectionView中图片进行异步加载

1567
来自专栏月色的自留地

macOS webview编程

1764
来自专栏spring源码深度学习

Scrapy入门案例——腾讯招聘(CrawlSpider升级)

需求和上次一样,只是职位信息和详情内容分开保存到不同的文件,并且获取下一页和详情页的链接方式有改动。

1251
来自专栏GIS讲堂

Highcharts导出图片

Highcharts是在做项目涉及到统计图的时候大家的首选,同时也会用到highcharts的export功能,将统计图导出为图片,刚好,最近也遇到了这样的事情...

1002
来自专栏简书专栏

基于Scrapy爬取伯乐在线网站(进阶版)

标题中的英文首字母大写比较规范,但在python实际使用中均为小写。 爬取伯乐在线网站所有文章的详情页面

1025

扫码关注云+社区