查看原文
其他

Scrapy对接Selenium

2017-08-18 崔庆才 Python爱好者社区


崔庆才,Python技术控,爬虫博文访问量已过百万。喜欢钻研,热爱生活,乐于分享。

个人博客:静觅 | http://cuiqingcai.com/


点击上方图片即可了解课程详情

点击文章末尾阅读原文即可购买课程


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。

Python3网络爬虫精华实战视频教程,大数据时代必备技能,重磅推荐!

点击文章末尾阅读原文即可购买课程

点击上图可查看详细课程说明信息


Python爱好者社区


为大家提供与Python相关的最新技术和资讯。

长按指纹 > 识别图中二维码 > 添加关注

100 31638 100 31638 0 0 7967 0 0:00:03 0:00:03 --:--:-- 7967 * Connection #0 to host 37.48.118.90 left intact >

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存