美文网首页
scrapy框架总结

scrapy框架总结

作者: 瘦不下去了 | 来源:发表于2019-01-05 23:56 被阅读0次

    Scrapy 框架

    • Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛。

    • 框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便。

    • Scrapy 使用了 Twisted['twɪstɪd] 异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。


      5c17862075585.png
    • Scrapy Engine(引擎):负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。

    • Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。

    • Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,

    • Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),

    • Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.

    • Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。

    • Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)

      5c17863914b96.png
      注意!只有当调度器中不存在任何request了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy也会重新下载。)

    一、项目流程

    1. 新建scrapy项目(scrapy startproject)

    scrapy startproject 爬虫项目名称
    例:scrapy startproject jobboleproject

    5bd693ab48a5f.png

    2.新建爬虫文件

    scrapy genspider 爬虫名称 域名(.com前一个字符串)
    例:scrapy genspider jobbole jobbole.com

    3.明确目标:在items.py中添加字段
    根据目标网站分析需要提取的数据,在item.py文件中添加字段

    • 打开jobboleproject文件下的item.py文件
    • Item 定义结构化数据字段,用来保存爬取到的数据,有点像Python中的dict,但是提供了一些额外的保护减少错误。
    • 可以通过创建一个 scrapy.Item 类, 并且定义类型为 scrapy.Field的类属性来定义一个Item(可以理解成类似于ORM的映射关系)。
    • 接下来,创建一个JobboleprojectItem 类,和构建item模型(model)。
    class JobboleItem(scrapy.Item):
    
        # define the fields for your item here like:
        #标题
        title = scrapy.Field()
        #创建时间
        create_date = scrapy.Field()
        #文章地址
        url = scrapy.Field()
        #id
        url_object_id = scrapy.Field()
        #文章图片
        front_image_url = scrapy.Field()
        #文章图片地址
        front_image_path = scrapy.Field()
        # 点赞数
        praise_nums = scrapy.Field()
        #收藏数
        bookmark_nums = scrapy.Field()
        # 评论数
        comment_nums = scrapy.Field()
        #文章内容
        content = scrapy.Field()
        #标签
        tags = scrapy.Field()
    

    4.制作爬虫 (spiders/jobbole.py)
    打开 jobboleproject/spider目录里的 jobbole.py,默认增加了下列代码:

    import scrapy
    class JobboleSpider(scrapy.Spider):
    
        name = 'jobbole'
        allowed_domains = ['jobbole.com']
        start_urls = ['http://blog.jobbole.com/all-posts/']
    
    def parse(self, response):
        pass
    
    
    • name = "" :这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。
    • allow_domains = [] 是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。
    • start_urls = () :爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
    • parse(self, response) :解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数,
      1.负责解析返回的网页数据(response.body),提取结构化数据(生成item)
      2.生成需要下一页的URL请求。

    将start_urls(设置起始url)的值修改为需要爬取的第一个url

    5.分析网站结构取数据
    6.在parse方法中做数据的提取

    from jobboleproject.items import JobboleprojectItem
    
    1.获取图片和文章详情的链接
    def parse(self, response):
        
        # css选择器获取当前列表页面的所有的节点
        post_nodes = response.css("#archive .floated-thumb .post-thumb a")
    
        # 如果不是完整的域名 需要拼接完整 response.url + post_url
        # 获取到的URL可能不是一个域名,也可能是具体的文章需要使用parse函数from urllib import parse
        for post_node in post_nodes:
            image_url = post_node.css("img::attr(src)").extract_first("")
            post_url = post_node.css("::attr(href)").extract_first("")
            full_url = response.urljoin(post_url)
            #meta参数对应的是一个字典,用来传递数据
            yield scrapy.Request(url=full_url, meta={"front_image_url": image_url},
            callback=self.parse_detail)
    
    2.然后将我们得到的数据封装到一个 JobboleItem 对象中,可以保存每个文章的属性:
    def parse_detail(self,response):
        # print(response)
        # 使用xpath语法或者css语法提取网页的相关信息
        # extract() 串行化并将匹配到的节点返回一个unicode字符串列表
    
        item = JobboleprojectItem()
        #标题
        item['title'] = response.xpath('//div[@class="entry-header"]/h1/text()').extract_frist("")
    
        #发布日期
        item['create_date'] = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract_first("").strip().replace("·","").strip()
        
        #文章地址详情地址
        item['url'] = response.url
        # http://blog.jobbole.com/113949/
    
        #文章的id
        item['url_object_id'] = re.match(".*?(\d+).*", url).group(1)
    
        # 文章图片
        item['front_image_url'] = response.meta.get("front_image_url","")
    
        # 点赞数
        item['praise_nums'] = response.xpath("//span[contains(@class,'vote-post-up')]/h10/text()").extract_first("")
    
        # 收藏数
        bookmark_nums = response.xpath("//span[contains(@class,'bookmark-btn')]/text()").extract_first("")
        match_bookmark_nums = re.match(".*?(\d+).*",bookmark_nums)
        if match_bookmark_nums:
            item['bookmark_nums'] = int(match_bookmark_nums.group(1))
        else:
            item['bookmark_nums'] = 0
    
        # 评论数
        comment_nums = response.xpath("//a[@href='#article-comment']/span/text()").extract_first("")
        match_comment_nums = re.match(".*?(\d+).*",comment_nums)
        if match_comment_nums:
            item['comment_nums'] = int(match_comment_nums.group(1))
        else:
            item['comment_nums'] = 0
    
        # 文章内容
        item['content'] = response.xpath("//div[@class='entry']").extract_first("")
    
        # 过滤评论标签
        tag_list = response.xpath("//p[@class='entry-meta-hide-on-mobile']//a/text()").extract()
        tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
         # 标签
        item['tags'] = ",".join(tag_list)
    
        print(item)
    
        # return返回数据,不经过pipelines
        # return item
    
        # yield将获取的数据交给pipelines
        # yield item
    

    7.关于yeild函数介绍:
    简单地讲,yield 的作用就是把一个函数变成一个 generator(生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,带有yeild的函数遇到yeild的时候就返回一个迭代值,下次迭代时, 代码从 yield 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行, 直到再次遇到 yield。

    通俗的讲就是:在一个函数中,程序执行到yield语句的时候,程序暂停,返回yield后面表达式的值,在下一次调用的时候,从yield语句暂停的地方继续执行,如此循环,直到函数执行完。

    8.Item Pipeline
    当Item在Spider中被收集之后,它将会被传递到Item Pipeline,这些Item Pipeline组件按定义的顺序处理Item。

    每个Item Pipeline都是实现了简单方法的Python类,比如决定此Item是丢弃而存储。以下是item pipeline的一些典型应用:

    • 验证爬取的数据(检查item包含某些字段,比如说name字段)
    • 查重(并丢弃)
    • 将爬取结果保存到文件或者数据库中

    编写item pipeline

    import something
    
    class SomethingPipeline(object):
        def __init__(self):
            # 可选实现,做参数初始化等
            # doing something
    
        def process_item(self, item, spider):
            # item (Item 对象) – 被爬取的item
            # spider (Spider 对象) – 爬取该item的spider
            # 这个方法必须实现,每个item pipeline组件都需要调用该方法,
            # 这个方法必须返回一个 Item 对象,被丢弃的item将不会被之后的pipeline组件所处理。
            return item
    
        def open_spider(self, spider):
            # spider (Spider 对象) – 被开启的spider
            # 可选实现,当spider被开启时,这个方法被调用。
    
        def close_spider(self, spider):
            # spider (Spider 对象) – 被关闭的spider
            # 可选实现,当spider被关闭时,这个方法被调用
    

    item写入JSON文件 以下pipeline将所有(从所有'spider'中)爬取到的item,存储到一个独立地items.json 文件,每行包含一个序列化为'JSON'格式的'item':

    import json
    
    class JobboleprojectPipeline(object):
        
        def __init__(self):
            self.file = open(‘jobbole.json', 'w')
    
        def process_item(self, item, spider):
            content = json.dumps(dict(item), ensure_ascii=False) + "\n"
            self.file.write(content)
            return item
    
        def close_spider(self, spider):
            self.file.close()
    

    启用一个Item Pipeline组件 为了启用Item Pipeline组件,必须将它的类添加到 settings.py文件ITEM_PIPELINES 配置,就像下面这个例子:

    # Configure item pipelines
    # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
    
    ITEM_PIPELINES = {
        #'mySpider.pipelines.SomePipeline': 300,
        "mySpider.pipelines.JobboleprojectPipeline":300
    }
    
    分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,
    通过pipeline,通常将这些数字定义在0-1000范围内(0-1000随意设置,数值越低,组件的优先级越高)
    

    9.重新启动爬虫

    scrapy crawl jobbole

    二、定制图片下载管道

    Scrapy提供了一个 item pipeline ,来下载属于某个特定项目的图片,比如,当你抓取产品时,也想把它们的图片下载到本地。

    这条管道,被称作图片管道,在 ImagesPipeline 类中实现,提供了一个方便并具有额外特性的方法,来下载并本地存储图片:

    Pillow 是用来生成缩略图,并将图片归一化为JPEG/RGB格式,因此为了使用图片管道,你需要安装这个库。 Python Imaging Library (PIL) 在大多数情况下是有效的,但众所周知,在一些设置里会出现问题,因此我们推荐使用 Pillow 而不是 PIL.

    使用图片管道 当使用 ImagesPipeline ,典型的工作流程如下所示:

    • 在一个爬虫里,你抓取一个项目,把其中图片的URL放入 image_urls 组内。
    • 项目从爬虫内返回,进入项目管道。
    • 当项目进入 ImagesPipeline,image_urls 组内的URLs将被Scrapy的调度器和下载器(这意味着调度器和下载器的中间件可以复用)安排下载,当优先级更高,会在其他页面被抓取前处理。项目会在这个特定的管道阶段保持“locker”的状态,直到完成图片的下载(或者由于某些原因未完成下载)。
    • 当图片下载完,另一个组(images)将被更新到结构中。这个组将包含一个字典列表,其中包括下载图片的信息,比如下载路径、源抓取地址(从 image_urls 组获得)和图片的校验码。images 列表中的图片顺序将和源 image_urls 组保持一致。如果某个图片下载失败,将会记录下错误信息,图片也不会出现在 images 组中。 实现定制图片管道

    在settings.py中设置 IMAGES_STORE 设置为一个有效的文件夹,用来存储下载的图片

    IMAGES_STORE = '/xxx/xxx/xxx'

    from scrapy.contrib.pipeline.images import ImagesPipeline
    import scrapy
    from scrapy.exceptions import DropItem
    import os
    from scrapy.utils.project import get_project_settings
    
    #下载图片
    image_store = get_project_settings().get('IMAGES_STORE')
    class jobboleImagePipline(ImagesPipeline):
    
        def get_media_requests(self, item, info):
            #根据图片地址,构建请求
            yield scrapy.Request(item['coverImage'])
    
        def item_completed(self, results, item, info):
            print(results)
            #获取图片下载成功的请求路径
            image_paths = [x['path'] for ok, x in results if ok]
            #判断图片是否下载成功
            if not image_paths:
                #出现错误,例如没有图片,可以丢弃item
                raise DropItem("Item contains no images")
            else:
                #替换图片的名称,自定义图片名称
                os.rename(image_store+'/'+image_paths[0],
                        image_store+'/'+item['title']+'.jpg')
                #将图片地址赋值给item
                item['localImagePath'] = image_store+'/'+item['title']+'.jpg'
                return item
    解释
    
    item_completed() 接收的元组列表需要保证与 get_media_requests() 方法返回请求的顺序相一致。下面是 results 参数的一个典型值:
    
    [(True,
      {'checksum': '2b00042f7481c7b056c4b410d28f33cf',
       'path': 'full/7d97e98f8af710c7e7fe703abc8f639e0ee507c4.jpg',
       'url': 'http://www.example.com/images/product1.jpg'}),
     (True,
      {'checksum': 'b9628c4ab9b595f72f280b90c4fd093d',
       'path': 'full/1ca5879492b8fd606df1964ea3c1e2f4520f076f.jpg',
       'url': 'http://www.example.com/images/product2.jpg'}),
     (False,
      Failure(...))]
    

    默认 get_media_requests() 方法返回 None ,这意味着项目中没有图片可下载。

    三、Scrapy Shell

    Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。

    如果安装了 IPython ,Scrapy终端将使用 IPython (替代标准Python终端)。 IPython 终端与其他相比更为强大,提供智能的自动补全,高亮输出,及其他特性

    启动Scrapy Shell

    scrapy shell "http://hr.tencent.com/position.php?&start=0#a"

    Scrapy Shell根据下载的页面会自动创建一些方便使用的对象,例如 Response 对象,以及 Selector 对象 (对HTML及XML内容)

    • 当shell载入后,将得到一个包含response数据的本地 response 变量,输入 response.body将输出response的包体,输出 response.headers 可以看到response的包头。
    • 输入 response.selector 时, 将获取到一个response 初始化的类 Selector 的对象,此时可以通过使用 response.selector.xpath()或response.selector.css() 来对 response 进行查询。
    • Scrapy也提供了一些快捷方式, 例如 response.xpath()或response.css()同样可以生效(如之前的案例)。

    Selectors选择器 Scrapy Selectors 内置 XPath 和 CSS Selector 表达式机制

    • xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表
    • extract(): 序列化该节点为字符串并返回list
    • css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4
    • re(): 根据传入的正则表达式对数据进行提取,返回字符串list列表

    尝试使用Selector:

    scrapy shell "http://hr.tencent.com/position.php?&start=0#a"

    返回 xpath选择器对象列表

    response.xpath("//title")
    [<Selector xpath='//title' data='<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title'>]

    使用 extract()方法返回 Unicode字符串列表

    response.xpath('//title').extract()
    ['<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>']

    打印列表第一个元素,终端编码格式显示

    response.xpath('//title').extract()[0]
    '<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>'

    返回 xpath选择器对象列表

    response.xpath('//title/text()')
    [<Selector xpath='//title/text()' data='职位搜索 | 社会招聘 | Tencent 腾讯招聘'>]

    返回列表第一个元素的Unicode字符串

    response.xpath('//title/text()')[0].extract()
    '职位搜索 | 社会招聘 | Tencent 腾讯招聘'

    职位名称:

    response.xpath("//tr[@class='even']/td/a/text()").extract()[0]
    '15605-火影手游服务器开发工程师(深圳)'

    职位名称详情页:

    response.xpath("//tr[@class='even']/td/a/@href").extract()[0]
    'position_detail.php?id=40217&keywords=&tid=0&lid=0'

    职位类别:

    response.xpath("//tr[@class='even']/td[2]/text()").extract()[0]
    '技术类'

    其他:

    • scrapy -h 查看所有可用的命令:
    • scrapy view -h 查看view命令的详细内容:
    • scrapy list列出当前项目中所有可用的spider
    • scrapy runspider xxxx.py在未创建项目的情况下,运行一个编写在Python文件中的spider。
    • scrapy version输出Scrapy版本

    四、Scrapy Spider文件介绍

    Spider类定义了如何爬取某个(或某些)网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。 换句话说,Spider就是您定义爬取的动作及分析某个网页(或者是有些网页)的地方。
    scrapy.Spider是最基本的类,所有编写的爬虫必须继承这个类。
    主要用到的函数及调用顺序为:
    init() : 初始化爬虫名字和start_urls列表
    start_requests() 调用make_requests_from url():生成Requests对象交给Scrapy下载并返回response
    parse():

    • 解析response,并返回Item或Requests(需指定回调函数)。
    • Item传给Item pipline持久化 , 而Requests交由Scrapy下载,并由指定的回调函数处理(默认parse()),一直进行循环,直到处理完所有的数据为止。

    源码

    class Spider(object_ref):
    
        #定义spider名字的字符串(string)。spider的名字定义了Scrapy如何定位(并初始化)spider,所以其必须是唯一的。
        #name是spider最重要的属性,而且是必须的。
        #一般做法是以该网站(domain)(加或不加 后缀 )来命名spider。 例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite
        name = None
    
        #初始化,提取爬虫名字,start_ruls
        def __init__(self, name=None, **kwargs):
            if name is not None:
                self.name = name
            # 如果爬虫没有名字,中断后续操作则报错
            elif not getattr(self, 'name', None):
                raise ValueError("%s must have a name" % type(self).__name__)
    
            # python 对象或类型通过内置成员__dict__来存储成员信息
            self.__dict__.update(kwargs)
    
            #URL列表。当没有指定的URL时,spider将从该列表中开始
            进行爬取。 因此,第一个被获取到的页面的URL将是该列表
            之一。 后续的URL将会从获取到的数据中提取。
            if not hasattr(self, 'start_urls'):
                self.start_urls = []
    
        # 打印Scrapy执行后的log信息
        def log(self, message, level=log.DEBUG, **kw):
            log.msg(message, spider=self, level=level, **kw)
    
        # 判断对象object的属性是否存在
        def set_crawler(self, crawler):
            assert not hasattr(self, '_crawler'), "Spider already bounded to %s" % crawler
            self._crawler = crawler
    
        @property
        def crawler(self):
            assert hasattr(self, '_crawler'), "Spider not bounded to any crawler"
            return self._crawler
    
        @property
        def settings(self):
            return self.crawler.settings
    
        #该方法将读取start_urls内的地址,并为每一个地址
        生成一个Request对象,交给Scrapy下载并返回Response
        #该方法仅调用一次
        def start_requests(self):
            for url in self.start_urls:
                yield self.make_requests_from_url(url)
    
        #start_requests()中调用,实际生成Request的函数。
        #Request对象默认的回调函数为parse(),提交的方式为get
        def make_requests_from_url(self, url):
            return Request(url, dont_filter=True)
    
        #默认的Request对象回调函数,处理返回的response。
        #生成Item或者Request对象。用户必须实现这个类
        def parse(self, response):
            raise NotImplementedError
    
        @classmethod
        def handles_request(cls, request):
            return url_is_from_spider(request.url, cls)
    
        def __str__(self):
            return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))
    
        __repr__ = __str__
    

    主要属性和方法

    • name

    定义spider名字的字符串。
    例如,如果spider爬取 mywebsite.com ,该spider通常会被命名为 mywebsite

    • allowed_domains

    包含了spider允许爬取的域名(domain)的列表,可选。

    • start_urls

    初始URL元组/列表。当没有制定特定的URL时,spider将从该列表中开始进行爬取。

    • start_requests(self)

    该方法必须返回一个可迭代对象(iterable)。该对象包含了spider用于爬取(默认实现是使用 start_urls 的url)的第一个Request。
    当spider启动爬取并且未指定start_urls时,该方法被调用。

    • parse(self, response)

    当请求url返回网页没有指定回调函数时,默认的Request对象回调函数。用来处理网页返回的response,以及生成Item或者Request对象。

    • log(self, message[, level, component])

    使用 scrapy.log.msg() 方法记录(log)message。
    更多数据请参见 logging

    parse()方法的工作机制:
    1.因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型;
    2.如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息。
    3.scrapy取到第一部分的request不会立马就去发送这个request,只是把这个request放到队列里,然后接着从生成器里获取;
    4.取尽第一部分的request,然后再获取第二部分的item,取到item了,就会放到对应的pipeline里处理;
    5.parse()方法作为回调函数(callback)赋值给了Request,指定parse()方法来处理这些请求 scrapy.Request(url, callback=self.parse)
    6.Request对象经过调度,执行生成 scrapy.http.response()的响应对象,并送回给parse()方法,直到调度器中没有Request(递归的思路)
    7.取尽之后,parse()工作结束,引擎再根据队列和pipelines中的内容去执行相应的操作;
    8.程序在取得各个页面的items前,会先处理完之前所有的request队列里的请求,然后再提取items。
    9.这一切的一切,Scrapy引擎和调度器将负责到底。

    五、Scrapy CrawlSpiders

    scrapy通用爬虫
    CrawlSpider它是Spider的派生类,Spider类的设计原则是只爬取start_url列表中的网页,而CrawlSpider类定义了一些规则Rule来提供跟进链接的方便的机制,从爬取的网页结果中获取链接并继续爬取的工作.

    源码参考

    class CrawlSpider(Spider):
        rules = ()
        def __init__(self, *a, **kw):
            super(CrawlSpider, self).__init__(*a, **kw)
            self._compile_rules()
    
        #首先调用parse()来处理start_urls中返回的response对象
        #parse()则将这些response对象传递给了_parse_response()函数处理,并设置回调函数为parse_start_url()
        #设置了跟进标志位True
        #parse将返回item和跟进了的Request对象    
        def parse(self, response):
            return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)
    
        #处理start_url中返回的response,需要重写
        def parse_start_url(self, response):
            return []
    
        def process_results(self, response, results):
            return results
    
        #从response中抽取符合任一用户定义'规则'的链接,并构造成Resquest对象返回
        def _requests_to_follow(self, response):
            if not isinstance(response, HtmlResponse):
                return
            seen = set()
            #抽取之内的所有链接,只要通过任意一个'规则',即表示合法
            for n, rule in enumerate(self._rules):
                links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
                #使用用户指定的process_links处理每个连接
                if links and rule.process_links:
                    links = rule.process_links(links)
                #将链接加入seen集合,为每个链接生成Request对象,并设置回调函数为_repsonse_downloaded()
                for link in links:
                    seen.add(link)
                    #构造Request对象,并将Rule规则中定义的回调函数作为这个Request对象的回调函数
                    r = Request(url=link.url, callback=self._response_downloaded)
                    r.meta.update(rule=n, link_text=link.text)
                    #对每个Request调用process_request()函数。该函数默认为indentify,即不做任何处理,直接返回该Request.
                    yield rule.process_request(r)
    
        #处理通过rule提取出的连接,并返回item以及request
        def _response_downloaded(self, response):
            rule = self._rules[response.meta['rule']]
            return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)
    
        #解析response对象,会用callback解析处理他,并返回request或Item对象
        def _parse_response(self, response, callback, cb_kwargs, follow=True):
            #首先判断是否设置了回调函数。(该回调函数可能是rule中的解析函数,也可能是 parse_start_url函数)
            #如果设置了回调函数(parse_start_url()),那么首先用parse_start_url()处理response对象,
            #然后再交给process_results处理。返回cb_res的一个列表
            if callback:
                #如果是parse调用的,则会解析成Request对象
                #如果是rule callback,则会解析成Item
                cb_res = callback(response, **cb_kwargs) or ()
                cb_res = self.process_results(response, cb_res)
                for requests_or_item in iterate_spider_output(cb_res):
                    yield requests_or_item
    
            #如果需要跟进,那么使用定义的Rule规则提取并返回这些Request对象
            if follow and self._follow_links:
                #返回每个Request对象
                for request_or_item in self._requests_to_follow(response):
                    yield request_or_item
    
        def _compile_rules(self):
            def get_method(method):
                if callable(method):
                    return method
                elif isinstance(method, basestring):
                    return getattr(self, method, None)
    
            self._rules = [copy.copy(r) for r in self.rules]
            for rule in self._rules:
                rule.callback = get_method(rule.callback)
                rule.process_links = get_method(rule.process_links)
                rule.process_request = get_method(rule.process_request)
    
        def set_crawler(self, crawler):
            super(CrawlSpider, self).set_crawler(crawler)
            self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
    

    创建 CrawlSpider模板 的代码

    scrapy genspider -t crawl 爬虫文件 域名

    CrawlSpider继承于Spider类,除了继承过来的属性外(name、allow_domains),还提供了新的属性和方法:

    class XcfcrawlspiderSpider(CrawlSpider):
        #爬虫名称
        name = 'xcfCrawlSpider'
        #设置允许爬取的域
        allowed_domains = ['xiachufang.com']
        #设置起始的url
        start_urls = ['http://www.xiachufang.com/category/']
        rules = (
            Rule(
                LinkExtractor(allow=r'.*?/category/\d+/'),
                callback='parse_item',
                follow=True,
                process_links='check_category_url'
            ),
        )
    

    rules

    CrawlSpider使用rules属性来决定爬虫的爬取规则,并将匹配后的url请求提交给引擎,完成后续的爬取工作。

    在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了某种特定操作,比如提取当前相应内容里的特定链接,是否对提取的链接跟进爬取,对提交的请求设置回调函数等。

    注意:如果多个rule匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用。

    class scrapy.spiders.Rule(
            link_extractor,
            callback = None,
            cb_kwargs = None,
            follow = None,
            process_links = None,
            process_request = None
    )
    
    link_extractor:是一个Link Extractor对象,用于定义需要提取的链接。
    
    callback: 从link_extractor中每获取到链接得到Responses时,会调用参数所指定的值作为回调函数,该回调函数接收一个response作为其一个参数。
    
    follow:是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。如果callback为None,follow 默认设置为True ,否则默认为False。
    
    process_links:指定spider中哪个的函数将会被调用,从link_extractor中获取到链接列表时将会调用该函数。该方法主要用来过滤。
    
    process_request:指定处理函数,根据该Rule提取到的每个Request时,该函数将会被调用,可以对Request进行处理,该函数必须返回Request或者None
    

    注意:当编写爬虫规则时,避免使用parse作为回调函数。由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了 parse方法,crawl spider将会运行失败。

    LinkExtractors

    class scrapy.linkextractors.LinkExtractor

    目的是提取链接
    每个LinkExtractor有唯一的公共方法是 extract_links(),它接收一个 Response 对象,并返回一个 scrapy.link.Link 对象

    class scrapy.linkextractors.LinkExtractor(
        allow = (),
        deny = (),
        allow_domains = (),
        deny_domains = (),
        deny_extensions = None,
        restrict_xpaths = (),
        tags = ('a','area'),
        attrs = ('href'),
        canonicalize = True,
        unique = True,
        process_value = None
    )
    
    allow:满足括号中“正则表达式”的URL会被提取,如果为空,则全部匹配。
    
    deny:满足括号中“正则表达式”的URL一定不提取(优先级高于allow)。
    
    allow_domains:会提取的链接的domains。
    
    deny_domains:一定不会被提取链接的domains。
    
    restrict_xpaths:使用xpath表达式,和allow共同作用过滤链接。
    

    Scrapy Request和Response

    # 部分代码
    class Request(object_ref):
    
        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):
    
            self._encoding = encoding  # this one has to be set first
            self.method = str(method).upper()
            self._set_url(url)
            self._set_body(body)
            assert isinstance(priority, int), "Request priority not an integer: %r" % priority
            self.priority = priority
    
            assert callback or not errback, "Cannot use errback without a callback"
            self.callback = callback
            self.errback = errback
    
            self.cookies = cookies or {}
            self.headers = Headers(headers or {}, encoding=encoding)
            self.dont_filter = dont_filter
    
            self._meta = dict(meta) if meta else None
    
        @property
        def meta(self):
            if self._meta is None:
                self._meta = {}
            return self._meta
    
    • url: 就是需要请求,并进行下一步处理的url
    • callback: 指定该请求返回的Response,由那个函数来处理。
    • method: 请求一般不需要指定,默认GET方法,可设置为"GET", "POST", "PUT"等,且保证字符串大写
    • headers: 请求头
    • cookies: cookies,模拟用户登录需要指定用户的cookies,字典dict型
    • meta: 比较常用,在不同的请求之间传递数据使用的。字典dict型
      request_with_cookies = Request(
      url="http://www.example.com",
      cookies={'currency': 'USD', 'country': 'UY'},
      meta={'dont_merge_cookies': True}
      )
    • encoding: 编码类型,使用默认的 'utf-8' 就行。
    • dont_filter: 表明该请求不由调度器过滤。这是当你想使用多次执行相同的请求,忽略重复的过滤器。默认为False。
    • errback: 指定错误处理函数

    发送POST请求(补充)

    yield scrapy.FormRequest(url, formdata, callback)

    如果希望程序执行一开始就发送POST请求,可以重写Spider类的start_requests(self) 方法,并且不再调用start_urls里的url。

    class mySpider(scrapy.Spider):
    
        name = 'myspider'
        allow_domeas = ['renren.com']
        start_urls = ["http://www.renren.com/PLogin.do"]
    
        def start_requests(self):
            # FormRequest 是Scrapy发送POST请求的方法
            for url in self.start_urls:
    
                yield scrapy.FormRequest(
                    url = url,
                    formdata = {"email" : "mr_mao_hacker@163.com", "password" : "axxxxxxxe"},
                    callback = self.parse_page
                )
    
        def parse_page(self, response):
            """
            请求成的回调函数
            """
            pass
    

    Response相关参数介绍

    class Response(object_ref):
        def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
            self.headers = Headers(headers or {})
            self.status = int(status)
            self._set_body(body)
            self._set_url(url)
            self.request = request
            self.flags = [] if flags is None else list(flags)
    
        @property
        def meta(self):
            try:
                return self.request.meta
            except AttributeError:
                raise AttributeError("Response.meta not available, this response " \
                    "is not tied to any request")# 部分代码
    class Response(object_ref):
        def __init__(self, url, status=200, headers=None, body='', flags=None, request=None):
            self.headers = Headers(headers or {})
            self.status = int(status)
            self._set_body(body)
            self._set_url(url)
            self.request = request
            self.flags = [] if flags is None else list(flags)
    
        @property
        def meta(self):
            try:
                return self.request.meta
            except AttributeError:
                raise AttributeError("Response.meta not available, this response " \
                    "is not tied to any request")
    
    • status: 响应码
    • body: 响应体
    • url:响应url
    • self.request (request对象)
    • self.headers (响应头)

    Scrapy DOWNLOADER_MIDDLEWARE 的使用

    反反爬虫相关机制
    通常防止爬虫被反主要有以下几个策略:

    • 动态设置User-Agent(随机切换User-Agent,模拟不同用户的浏览器信息)
    • 禁用Cookies(前提是爬取的网站不需要cookies参数)(也就是不启用cookies middleware,不向Server发送cookies,有些网站通过cookie的使用发现爬虫行为)

    使用cookies池,自定义中间件
    除非特殊需要,否则禁用cookies,防止某些网站根据Cookie来封锁爬虫。
    COOKIES_ENABLED = False

    • 设置延迟下载(降低访问网站的频率)(设置为2秒或更高)

    DOWNLOAD_DELAY = 2

    • 使用IP代理地址池:VPN和代理IP,现在大部分网站都是根据IP来反爬的。

    针对于反爬手段

    1.使用 Crawlera(专用于爬虫的代理组件),正确配置和设置下载中间件后,项目所有的request都是通过crawlera发出。

    DOWNLOADER_MIDDLEWARES = {
        'scrapy_crawlera.CrawleraMiddleware': 600
    }
    
    CRAWLERA_ENABLED = True
    CRAWLERA_USER = '注册/购买的UserKey'
    CRAWLERA_PASS = '注册/购买的Password'
    

    2.自定义下载中间件(Downloader Middlewares)
    下载中间件是处于引擎(crawler.engine)和下载器(crawler.engine.download())之间的一层组件,可以用来修改Request和Response。

    • 当引擎传递请求(Request)给下载器的过程中,下载中间件可以对请求进行处理 (例如增加http header信息(User-Agent),增加proxy代理等);
    • 在下载器完成http请求,传递响应给引擎的过程中, 下载中间件可以对响应进行处理

    要激活下载器中间件组件,将其加入到 DOWNLOADER_MIDDLEWARES 设置中。 该设置是一个字典(dict),键为中间件类的路径,值为其中间件的优先级。值越低,代表优先级越高。

    每个中间件组件是一个定义了以下一个或多个方法

    def process_request(self, request, spider)

    当每个request对象通过下载中间件时该方法被调用。
    process_request() 必须返回以下其中之一:

    • None
      如果返回 None: Scrapy将继续处理request,执 行其他的中间件的相应方法。
    • Response 对象
      如果返回 Response 对象: Scrapy不会再调用任 何其他的中间件的 process_request() 或相应地下 载函数; 直接返回这个response对象。 已激活的中间件的 process_response()方法则会在 每个 response 返回时被调用。
    • Request 对象
      如果返回 Request 对象,Scrapy则停止调用 其他中间件的process_request方法,并重新将返回的 request对象放置到调度器等待下载。
    • IgnoreRequest异常
      如果返回raise IgnoreRequest 异常: 下载中间件的 process_exception() 方法会被用。 如果没有捕获该异常, 则request发情请求时设置的 errback(Request.errback)方法会被调用。如果也 没有设置异常回调,则该异常被忽略且不记录。

    process_request()有两个参数:

    • request (Request 对象) – 处理的request
    • spider (Spider 对象) – 该request对应的spider

    process_response(self, request, response, spider)

    当下载器完成http请求,传递Response给引擎的时调用
    process_response() 必须返回以下其中之一:

    • Response 对象
      如果返回 Request: 更低优先级的下载中间件的 process_response方法不会继续调用,该Request会被 重新放到调度器任务队列中等待调度,相当于一个新的 Request。
    • Request 对象
      如果返回 Response 对象,更低优先级的下载中间 件的process_response方法会被继续调用对Response对象进行处理
    • IgnoreRequest异常
      如果抛出 IgnoreRequest 异常,则调用request 设置的errback(Request.errback)函数。 如果异常没有 被处理,则该异常被忽略且不记录。

    process_response()有三个参数:

    • request (Request 对象) – response所对应的request
    • response (Response 对象) – 被处理的response
    • spider (Spider 对象) – response所对应的spider

    常见的中间件代码实例
    1.设置User-Agent中间件
    Scrapy代理IP、Uesr-Agent的切换都是通过DOWNLOADER_MIDDLEWARES进行控制,我们在settings.py同级目录下创建middlewares.py文件,包装所有请求。
    在settings.py文件中添加USER_AGENTS:

    USER_AGENTS = [
        "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
        "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
        "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
        "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
        "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    ]
    

    方式一

    import random
    class RandomUserAgent(object):
    
        def process_request(self, request, spider):
            
            #获取到代理的代理池
            useragents = spider.settings['USERAGENTS']
    
            #随机获取一个User-Agent
            user_agent = random.choice(useragents)
            print('执行下载中间件'+user_agent)
    
            if user_agent:
                #赋值的两种方式
                request.headers.setdefault(b'User-Agent', user_agent)
                # request.headers['User-Agent'] = user_agent
    

    方式二

    # 使用第三方插件来随机获取User-Agent
    from fake_useragent import UserAgent
    
    class RandomUserAgentMiddlewareTwo(object):
    
        def process_request(self, request, spider):
    
            user_agent = UserAgent().random
            print('执行下载中间件'+user_agent)
    
            if user_agent:
                #赋值的两种方式
                request.headers.setdefault(b'User-Agent', user_agent)
                # request.headers['User-Agent'] = user_agent
    

    最后设置setting.py里的DOWNLOADER_MIDDLEWARES,激活自己编写的下载中间件类。

    最后设置setting.py里的DOWNLOADER_MIDDLEWARES,激活自己编写的下载中间件类。
    

    2.设置代理中间件。

    • 添加代理IP设置PROXIES: 代理一般分为公开代理(不需要账号密码)和私密代理(需要账号密码) 代理获取方式:
    • 免费代理IP可以网上搜索,
    • 也可以付费购买一批可用的私密代理IP
    # 在settings.py文件中模拟一个代理池(对于少量代理可以这么做)
    PROXIES = [
        {'ip_port': '111.8.60.9:8123', 'user_pwd': 'user1:pass1'},
        {'ip_port': '101.71.27.120:80', 'user_pwd': 'user2:pass2'},
        {'ip_port': '122.96.59.104:80', 'user_pwd': None},
        {'ip_port': '122.224.249.122:8088', 'user_pwd': None},
    ]
    # middlewares.py
    
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    import random
    import base64
    
    #定义一个代理的中间件
    import base64
    class RandomProxyMiddleware(object):
        def process_request(self, request, spider):
    
            proxies = spider.settings['PROXIES']
            proxy = random.choice(self.proxies)
            if proxy['user_pwd'] is None:
                # 没有代理账户验证的代理使用方式
                request.meta['proxy'] = proxy['ip_port']
            else:
                #对账户密码进行base64编码
                user_pwd = base64.b64encode(proxy['user_pwd'].encode('utf-8')).decode('utf-8')
                #对应到代理服务器的信令格式里
                request.headers['Proxy-Authorization'] = 'Basic ' + user_pwd
                request.meta['proxy'] = proxy['ip_port']
    

    3. 设置cookies中间件。

    import random
    class RandomCookiesMiddleware(object):
        
        def process_request(self,request,spider):
            cookies = spider.settings['COOKIES']
            #随机获取一个cookies
            cookie = random.choice(cookies)
            if cookie:
                request.cookies = cookie
    
    
    **4. 设置selenium中间件。**
    from selenium import webdriver
    from selenium.common.exceptions import TimeoutException
    from scrapy.http import HtmlResponse
    
    class SeleniumMiddleware(object):
        def __init__(self):
    
            self.drive = webdriver.Chrome(executable_path='')
            self.drive.set_page_load_timeout(10)
    
        def process_request(self,request,spider):
            try:
                url = request.url
                self.drive.get(url)
                if self.drive.page_source:
                    return HtmlResponse(url=url,body=self.drive.page_source,status=200,encoding='utf-8',request=request)
            except TimeoutException:
                print('请求超时')
                return HtmlResponse(url=url,body=None,status=500)
    

    相关文章

      网友评论

          本文标题:scrapy框架总结

          本文链接:https://www.haomeiwen.com/subject/ahqjrqtx.html