02_Python Scrapy网络爬虫学习

2019.3.30

虽然在寒假时已经自学过了网络爬虫的相关知识,但一是因为当时学习的是使用urllib(在Python3中已经将urliib2包整合到了urllib包中),二是希望通过在跟随老师的系统学习下能够有长足的进步,所以现在打算重头学习网络爬虫。这一篇博客只会讲解scrapy框架的一些知识,不涉及传统爬虫(request、beautiful soup、Xpath等),传统的爬虫之后会在爬虫学习系列详细展开。


Scrapy

一.scrapy框架


1. scrapy概述

scrapy官网

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了页面抓取 (更确切来说,网络抓取)所设计的, 也可以应用在获取API所返回的数据(例如Amazon Associates Web Services) 或者通用的网络爬虫。
Scrapy 使用了 Twisted异步网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。

异步与非阻塞的区别.png


2. scrapy 的工作流程

参考:scrapy中文文档


  1. 图示

    scarpy工作流程图.png

  1. 组件的说明(如果不喜欢看文字,下面有图示)
    参考:scrapy中文文档
  • Scrapy Engine
    引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。
  • 调度器(Scheduler)
    调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎。
  • 下载器(Downloader)
    下载器负责获取页面数据并提供给引擎,而后提供给spider。
  • Spiders
    Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。
  • Item Pipeline
    Item Pipeline负责处理被spider提取出来的item。典型的处理有清理、 验证及持久化(例如存取到数据库中)。
  • 下载器中间件(Downloader middlewares)
    下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。
  • Spider中间件(Spider middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

  • 数据流(Data flow)
  • Scrapy中的数据流由执行引擎控制,其过程如下:
  1. 引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。
  2. 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。
  3. 引擎向调度器请求下一个要爬取的URL。
  4. 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。
  5. 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。
  6. 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。
  7. Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。
  8. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。
  9. (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。
  • 事件驱动网络(Event-driven networking)[
    Scrapy基于事件驱动网络框架 Twisted编写。因此,Scrapy基于并发性考虑由非阻塞(即异步)的实现。

  1. 组件的图示
scrapy组件说明.png


  1. scrapy的目录结构

  1. 创建一个scrapy项目(在命令行内输入)

    scrapy startproject <project_name>
    

    便能创建一个名为priject_name(自己定义)的scrapy项目,其目录有特定的结构。


  1. 目录结构

    tutorial 项目为例(源自Python数据分析老师丁烨的pdf文件),scrapy框架会为项目生成以下文件

    scrapy项目的目录结构.png

    (ps:由于Python语言并没有类似于Java的打包package语句,所以Python通过一个目录有无init.py来判断一个文件夹是普通文件夹还是一个包,init.py的内容可以为空,存在即可)



二. scrapy爬虫实例


  1. 创建一个继承于scrapy.Spider的类来实现爬虫
    其中,start_request()和parse()都是需要通过重写来实现的函数
import scrapy

# 这个类可以获取html资源源码
class QuotesSpider(scrapy.Spider):
    # 这个类是继承于scrapy.Spider类的

    # 爬虫的名字,是服务器端识别爬虫的标识
    name = "quotes"

    # Overwrite the start_requests()
    def start_requests(self):
        # start_requests方法必须返回可迭代的Request对象,这个spider将这些对象作为起始初始化请求对象

        # 原始版本
        """
        urls = ['http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/']
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)
        """

        # 改进版本(添加分页识别机制)
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

  1. parse()函数,重写模板函数,用于解析html网页
def parse(self, response):
        # 在parse方法中每一个request请求发出之后会在这个方法中返回对应的response对象,
        # 这个response对象为textResponse对象,包含了界面的内容和其他一些处理的方法。
        # 在这个方法内部通常将数据分解为字典,还可以查找到新的url

        # 原始版本
        # 获取某些标签的内容
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css("span.text::text").get(),
                'author': quote.css("small.author::text").get(),
                'tags': quote.css("div.tags a.tag::text").getall(),
                # 这个逗号也是必要的
            }
  • 因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型。
  • 如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息。
  • scrapy取到第一部分的request不会立马就去发送这个request,只是把这个request放到队列里,然后接着从生成器里获取。
  • 取尽第一部分的request,然后再获取第二部分的item,取到item了,就会放到对应的pipeline里处理;
  • parse()方法作为回调函数(callback)赋值给了Request,指定parse()方法来处理这些请求 scrapy.Request(url, callback=self.parse)
  • Request对象经过调度,执行生成 scrapy.http.response()的响应对象,并送回给parse()方法,直到调度器中没有Request(递归的思路)
  • 取尽之后,parse()工作结束,引擎再根据队列和pipelines中的内容去执行相应的操作;
  • 程序在取得各个页面的items前,会先处理完之前所有的request队列里的请求,然后再提取items。
  • 这一切的一切,Scrapy引擎和调度器将负责到底。

  1. 爬虫的运行
  • 运行命令(命令行输入,quotes可变,指的是爬虫的名字,自己定义)
    scrapy crawl quotes
    
  • 爬行结果
    爬虫运行结果01.png

    爬虫运行结果02.png

    爬虫运行结果03.png

(ps:成功爬取之后,会生成以上的文件(json文件除外))


  1. 将爬取网页中的内容保存到一个json文件中(不一定要这么做)
  • 运行命令(命令行输入)
    scrapy crawl quotes -o quotes.json
    
  • json文件内容
    json文件内容.png

  1. 完整源代码
import scrapy

# 这个类可以获取html资源源码
class QuotesSpider(scrapy.Spider):
    # 这个类是继承于scrapy.Spider类的

    # 爬虫的名字,是服务器端识别爬虫的标识
    name = "quotes"

    # Overwrite the start_requests()
    def start_requests(self):
        # start_requests方法必须返回可迭代的Request对象,这个spider将这些对象作为起始初始化请求对象

        # 原始版本
        """
        urls = ['http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/']
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)
        """

        # 改进版本(添加分页识别机制)
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        # 在parse方法中每一个request请求发出之后会在这个方法中返回对应的response对象,
        # 这个response对象为textResponse对象,包含了界面的内容和其他一些处理的方法。
        # 在这个方法内部通常将数据分解为字典,还可以查找到新的url

        # 原始版本
        """
        # 获取某些标签的内容
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css("span.text::text").get(),
                'author': quote.css("small.author::text").get(),
                'tags': quote.css("div.tags a.tag::text").getall(),
                # 这个逗号也是必要的
            }

        # str.split(str="", num=string.count(str)), num默认-1, 即分割全部
        # 获得倒数第2个分割子串,也就是页数(1 或 2)
        page = response.url.split("/")[-2]
        filename = 'quotes-{}.html'.format(page)
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file {}'.format(filename))
        """

        # 改进版本(添加分页识别机制,可爬多个页面)
        """
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css("span.text::text").get(),
                'author': quote.css("small.author::text").get(),
            }
            next_page = response.css('li.next a::attr(href)').get()
            if next_page is not None:
                # 继续爬下一个页面
                yield response.follow(next_page, self.parse)
        """

        # 作业的代码(随着作者的信息返回页面的内容)
        for href in response.css('.author + a::attr(href)'):
            yield response.follow(href, self.parse_author)

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            # 继续爬下一个页面
            yield response.follow(next_page, self.parse)

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).get(default='').strip()
        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
         }


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容