Scrapy

Scrapy框架

介绍

Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方

式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API
所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。整体架构大致如下:

img

框架构成

  1. 引擎(Engine)
    引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件.有关详细信息,请参见上面的数据流部分.
  2. 调度器(SCHEDULER)
    用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回.可以想像成一个URL的优先级队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址
  3. 下载器(DOWNLOADER)

用于下载网页内容,并将网页内容返回给引擎,下载器是建立在twisted这个搞笑的异步模型上的。

  1. 爬虫(spider)
    蜘蛛是开发人员自定义的类,用来解析响应,并且提取条目,或者发送新的请求
  2. 项目管道(ITEM PIPLINES)
    在项目被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
  3. 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间,主要用来处理从egine传到DOWLOADER的请求请求,已经从下载程序传到engine的响应response,

  1. 爬虫中间件(spider Middlewares)
    位于engine和spider之间,主要工作是处理spider的输入(即响应(和输出(即请求)

官网链接:https://docs.scrapy.org/en/latest/topics/architecture.html

流程解读

首先sprider部分就是我们要写的爬虫程序,item pipelines就是用来对数据进行持久化存储的。然后是两个中间件,这些是需要我们来写的。

流程:你写的spider,经过中间件后,交给引擎,引擎然后抛给调度器,让调度器来去重,结束后还给引擎,然后由引擎把这个爬虫通过中间件(和上面的不是同一个)发给下载器,从目标网站获取数据,然后再通过这个中间件给引擎,引擎在发送给管道来进行存储。

安装

#Windows平台
    1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    3、pip3 install lxml
    4、pip3 install pyopenssl
    5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    8、pip3 install scrapy


    只用执行以下命令就行了。
    pip install wheel
    pip install lxml
    pip install pyopenssl
    pip install pywin32
    pip install twisted
    pip install scrapy

以上就完成了安装scrapy。

命令

#1 查看帮助
    scrapy -h
    scrapy <command> -h

#2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要
    Global commands:
        startproject #创建项目 scrapy startproject 项目名
        genspider    #创建爬虫程序 scrapy genspider 爬虫名 目标url
        settings     #如果是在项目目录下,则得到的是该项目的配置
        runspider    #运行一个独立的python文件,不必创建项目
        shell        #scrapy shell url地址  在交互式调试,如选择器规则正确与否
        fetch        #独立于程单纯地爬取一个页面,可以拿到请求头
        view         #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
        version      #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
    Project-only commands:
        crawl        #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False
        check        #检测项目中有无语法错误
        list         #列出项目中所包含的爬虫名
        edit         #编辑器,一般不用
        parse        #scrapy parse url地址 --callback 回调函数  #以此可以验证我们的回调函数是否正确
        bench        #scrapy bentch压力测试,看你能爬多快。

#3 官网链接
    https://docs.scrapy.org/en/latest/topics/commands.html

项目文件

img

spiders:我们写的爬虫就是在这个文件夹下,进入项目根目录下然后执行命令

''scrapy genspider 爬虫名 目标url'',生成的py文件,会自动放进spiders里面。可以在这个文件夹里写多个爬虫。

middlewares:写中间件的地方。

pipelines:管道,来连接数据库的,做持久化存储。

settings:配置文件,如:递归的层数、并发数,延迟下载等。 强调:配置文件的选项必须大写否则视为无效 ,正确写法USER_AGENT='xxxx'

scrapy.cfg:项目的主配置信息,用来部署scrapy时使用,爬虫相关的配置信息在settings.py文件中。

items.py: 设置数据存储模板,用于结构化数据,如:Django的Model
pipelines 数据处理行为,如:一般结构化的数据持久化

每次想要启动这个爬虫文件都要输入"scrapy crawl 爬虫文件"命令行,会非常麻烦,所以可以写一个run文件,用来启动这个项目。

img

run.py

from scrapy.cmdline import execute

# "scrapy crawl 爬虫文件" 和这个一一对应
execute(['scrapy','crawl','tmall'])
#上面这个会把信息都打印出来,如果不想看日志信息,就加一个--nolog
execute(['scrapy','crawl','tmall','--nolog'])

然后右键运行这个run文件就好了。

注意:要去settings里面改一个参数 ROBOTSTXT_OBEY = True ,改为False,这个参数是问你是否遵循机器人协议(君子协议)的,如果遵循的话,爬虫都不用干了。

项目流程

# -*- coding: utf-8 -*-
import scrapy


class TmallSpider(scrapy.Spider):
    #指定这个爬虫的名字,启动的时候按照这个名字来,而不是文件名
    name = 'tmall'
    #允许的域名
    allowed_domains = ['www.tmall.com']
    #开始就会像这里面的路由发送get请求。
    start_urls = ['http://www.tmall.com/']
	
    #解析的时候就会调用这个函数。
    def parse(self, response):
        pass

这里的请求是由框架发送的,如果我们想在这个请求上实现一些逻辑,就目前而已不能实现,所以进Spider的源码看一下。看到一个start_requests方法。

    def start_requests(self):
        cls = self.__class__
        if method_is_overridden(cls, Spider, 'make_requests_from_url'):
            warnings.warn(
                "Spider.make_requests_from_url method is deprecated; it "
                "won't be called in future Scrapy releases. Please "
                "override Spider.start_requests method instead (see %s.%s)." % (
                    cls.__module__, cls.__name__
                ),
            )
            for url in self.start_urls:
                yield self.make_requests_from_url(url)
        else:
            #上面都不用去看,就直接看这里。循环start_urls,返回一个可迭代对象。dont_filter=True的意思就是让调度器不去重。
            for url in self.start_urls:
                yield Request(url, dont_filter=True)

所以如果我们想要在最开始的请求上实现逻辑,就重写这个方法即可。

# -*- coding: utf-8 -*-
import scrapy


class TmallSpider(scrapy.Spider):
    name = 'tmall'
    allowed_domains = ['www.tmall.com']
    start_urls = ['http://www.tmall.com/']

    def start_requests(self):
        for url in self.start_urls:
            #看一下这个Request都有些什么参数,进入源码,看下面就明白了,这里的callback调用了下面的parse,如果我们不指定callback的话,默认也是走parse。
            yield scrapy.Request(url=url,callback=self.parse,
                                 dont_filter=True)

    #这里的response可以使用各种框架的语法,这里用的是css选择器,也可以用xpath
    def parse(self, response):
        #这里只会返回一个css对象,如果想要拿到值,不许要加上.extract()
        # response.css('[name="totalPage"]::attr(value)')# css对象
        
        # response.css('[name="totalPage"]::attr(value)').extract() 		#['80']  	
        
        #response.css('[name="totalPage"]::attr(value)').
            #extract_first() # 80

Request

class Request(object_ref):
	#这里面我们先只看url和callback和meta和dont_filter以及errback就好了
    #url就是请求的url,callback就是回调函数,meta就是放代理ip的,errback是错误处理
    def __init__(self, url, callback=None, method='GET', headers=None,body=None,cookies=None, meta=None, encoding='utf-8', priority=0,dont_filter=False, errback=None, flags=None, cb_kwargs=None):

爬取天猫小案例

用一个案例来讲解以上的内容

# -*- coding: utf-8 -*-
import scrapy
from urllib.parse import urlencode
from xiaopapa import items

class TmallSpider(scrapy.Spider):
    name = 'tmall'
    allowed_domains = ['www.tmall.com']
    start_urls = ['http://www.tmall.com/']

    def __init__(self,*args,**kwargs):
        super(TmallSpider,self).__init__(*args,**kwargs)
        self.api = "http://list.tmall.com/search_product.htm?"


    def start_requests(self):
        self.param = {
            "q": "钢铁侠",
            "totalPage": 1,
            "jumpto": 1,
        }
        url = self.api + urlencode(self.param)
        yield scrapy.Request(url=url,callback=self.gettotalpage,
                             dont_filter=True)

    def gettotalpage(self, response):
        #获取“钢铁侠”搜索条件下所有的页码
        totalpage = response.css('[name="totalPage"]::attr(value)')
        .extract_first()
        
        #转化为整型
        self.param['totalPage'] = int(totalpage)
        
        #这里先不用真实的页数,因为请求次数太多了,你的ip会被天猫封掉。
        # for i in range(1,self.param['totalPage']+1):
        for i in range(1,3):
            #jumpto是跳转到下一页的参数,不需要登录就能查看^_^
            self.param['jumpto'] = i
            url = self.api + urlencode(self.param)
            #继续使用回调函数,调用下一个,其实可以都写在一个里面,这么分开写是				为了解耦合
            yield scrapy.Request(url=url,callback=self.get_info,
                                 dont_filter=True)

    def get_info(self,response):
        #拿到包含说有产品标签的列表
        product_list = response.css('.product')
        for product in product_list:
            
            title = product.css('.productTitle a::attr(title)')
            .extract_first()
            
            price = product.css('.productPrice em::attr(title)')
            .extract_first()
            
            status = product.css('.productStatus em::text')
            .extract_first()

            item = items.XiaopapaItem()
            item['title'] = title
            item['price'] = price
            item['status'] = status

            yield item

目前就到这里,可以看到这里使用item,看一下item

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class XiaopapaItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass

把它写成这样,固定写法

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class XiaopapaItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    price = scrapy.Field()
    status = scrapy.Field()

再回到上面返回一个item对象。返回到了哪里?

答案就是,返回到了pipelines管道里,做持久化存储,因为我们已经在爬虫文件里做完了数据分析,拿到了想要的数据了,通过item发送给了管道。

接下来看pipelines怎么操作

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


class XiaopapaPipeline(object):



    def process_item(self, item, spider):
        return item

以上是原本的样子,我们应该自己来写

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import pymongo

class XiaopapaPipeline(object):

    def __init__(self,host,port,db,table):
        self.host = host
        self.port = port
        self.db = db
        self.table = table

    @classmethod
    def from_crawler(cls,crawl):
        #这里是从配置文件中拿参数,所以需要我们现在settings里配置好数据库参数
        port = crawl.settings.get('PORT')
        host = crawl.settings.get('HOST')
        db = crawl.settings.get('DB')
        table = crawl.settings.get('TABLE')
        #这里调用上面的__init__,当然也可以不写这个类方法,
        #直接在上面的init里面全操作完,这么写主要是为了解耦合
        return cls(host,port,db,table)

	
    #在这里开启数据库
    def open_spider(self,crawl):
        self.client = pymongo.MongoClient(port=self.port
                                          ,host=self.host)
        
        db_obj = self.client[self.db]self.table_obj= db_obj
													[self.table]
	#在这里关闭数据库
    def close_spider(self,crawl):
        self.client.close()

	
    def process_item(self, item, spider):
        #保存数据到mongodb数据库
        self.table_obj.insert(dict(item))
        return item

settings

除了配置数据库参数之外,一个非常重要的点,就是一定要把一个参数的注释解开。

HOST = '127.0.0.1'
PORT = 27017
DB = 'tmall'
TABLE = 'products'

#这个一定要解开,不然无法保存数据,这个是配置管道的优先级的,数字越小,优先级越高。
ITEM_PIPELINES = {
   'xiaopapa.pipelines.XiaopapaPipeline': 300,
}


为什么需要多个管道?

因为有时候你可能需要把所有数据在mongodb存一份,在mysql存一份,在文件里存一份,而且你想要先在mongodb里存,然后再mysql,最后再在文件里存,这时候就可以写三个管道,,如下,就是写一份到文件。

class MyxiaopapaPipeline1(object):
    def open_spider(self, crawl):
        self.f = open("xxx.txt","at",encoding="utf-8")

    def close_spider(self, crawl):
        self.f.close()

    def process_item(self, item, spider):

        self.f.write(json.dumps(dict(item)))

        return item

注意:比如你爬了10000条数据,不是10000条先全部存到mongodb再存下一个数据库,因为他是基于多线程的,所以爬取的数据都是异步的,是一条一条从爬虫文件里yield出来被管道接收的,所以是一条先给优先级最高的存,然后再优先级第二的,以此类推。

请求头配置

settings

#这是全局的请求头配置
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
}

中间件配置

DOWNLOADER_MIDDLEWARES = {
   'myxiaopapa.middlewares.xiaopapaDownloaderMiddleware': 300, # 数值越小,优先级越高
   # 'myxiaopapa.middlewares.xiaopapaDownloaderMiddleware1': 400, # 数值越小,优先级越高
   # 'myxiaopapa.middlewares.xiaopapaDownloaderMiddleware2': 500, # 数值越小,优先级越高
}

通常不太可能出现三个中间件,两个就足够了,一个用来走代理池,一个用来搞请求头。

img

posted @ 2020-01-07 08:27  chanyuli  阅读(317)  评论(0编辑  收藏  举报