02_Python|02_Python Scrapy网络爬虫学习

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

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

02_Python|02_Python Scrapy网络爬虫学习
文章图片
异步与非阻塞的区别.png 2. scrapy 的工作流程
参考:scrapy中文文档
  1. 图示
    02_Python|02_Python Scrapy网络爬虫学习
    文章图片
    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. 组件的图示
02_Python|02_Python Scrapy网络爬虫学习
文章图片
scrapy组件说明.png
  1. scrapy的目录结构
  1. 创建一个scrapy项目(在命令行内输入)
    scrapy startproject

    便能创建一个名为priject_name(自己定义)的scrapy项目,其目录有特定的结构。
  1. 目录结构
    tutorial 项目为例(源自Python数据分析老师丁烨的pdf文件),scrapy框架会为项目生成以下文件

    02_Python|02_Python 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

  • 爬行结果
    02_Python|02_Python Scrapy网络爬虫学习
    文章图片
    爬虫运行结果01.png
    02_Python|02_Python Scrapy网络爬虫学习
    文章图片
    爬虫运行结果02.png
    02_Python|02_Python Scrapy网络爬虫学习
    文章图片
    爬虫运行结果03.png
【02_Python|02_Python Scrapy网络爬虫学习】(ps:成功爬取之后,会生成以上的文件(json文件除外))
  1. 将爬取网页中的内容保存到一个json文件中(不一定要这么做)
  • 运行命令(命令行输入)
    scrapy crawl quotes -o quotes.json

  • json文件内容
    02_Python|02_Python Scrapy网络爬虫学习
    文章图片
    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'), }

    推荐阅读