Python爬虫第十课:Scrapy框架(1)

前面的关卡中,我们学习了如何用协程来提升爬虫的速度,并且通过项目实操,将协程运用于抓取HI运动的食物数据。

不知道你会不会有这样一种感觉:要写出一个完整的爬虫程序需要做很多琐碎的工作。比如,要针对不同的网站制定不同的解析方式;要导入不同功能的模块;还要编写各种爬取流程的代码。

我们在日常工作中会使用PPT模板来制作PPT。那么有没有一个现成的爬虫模板,让我们能够改之即用,也就是说对这个模板进行适当的修改,就能完成一个爬虫项目的开发呢?

这种模板在Python中还真的存在,只不过我们一般称之为框架。

就像PPT模板会帮你设置好背景、字体和颜色一样,一个爬虫框架里包含了能实现爬虫整个流程的各种模块,。

这一关,我们要学习的就是一个功能强大的爬虫框架——Scrapy。

一、Scrapy是什么

在我们之前的课程中,我们需要不同模块实现不同的功能:使用requests模块进行请求、使用csv模块存储数据等。而在Scrapy里,你不需要这么做,因为很多爬虫需要涉及的功能,比如麻烦的异步,在Scrapy框架都自动实现了

我们之前编写爬虫的方式,就相当于我们在自己装修一个毛坯房。需要自己去找施工队来「水电施工」「泥工砌墙贴砖」「木工吊顶打柜子」「油漆工刷乳胶漆」等等…而框架就相当于一个精装房,拎包入住!

1)Scrapy的结构

在这里插入图片描述
上面的这张图是Scrapy的整个结构。你可以把整个Scrapy框架当成是你所在的部门。最中心位置的Scrapy Engine(引擎)就是你所在部门的大boss,负责统筹规划你这个部门的四大职能。

以爬虫流程的顺序来依次介绍Scrapy爬虫部门的4大职能:

  • Scheduler(调度器)主要负责处理引擎发送过来的requests对象(即网页请求的相关信息集合,包括params,data,cookies,request headers…等),会把请求的url以有序的方式排列成队,并等待引擎来提取(功能上类似于gevent库的queue模块)。

  • Downloader(下载器)则是负责处理引擎发送过来的requests,进行网页爬取,并将返回的response(爬取到的内容)交给引擎。它对应的是爬虫流程【获取数据】这一步。

  • Spiders(爬虫)的主要任务是创建requests对象和接受引擎发送过来的response(Downloader部门爬取到的内容),从中解析并提取出有用的数据。它对应的是爬虫流程【解析数据】和【提取数据】这两步。

  • Item Pipeline(数据管道)负责存储和处理Spiders提取到的有用数据。这个对应的是爬虫流程【存储数据】这一步。

  • Downloader Middlewares(下载中间件)的工作相当于下载器的助手,比如会提前对引擎发送的诸多requests做出处理。

  • Spider Middlewares(爬虫中间件)的工作则相当于爬虫的助手,比如会提前接收并处理引擎发送来的response,过滤掉一些重复无用的东西。

在这里插入图片描述

3)Scrapy的工作原理

在这里插入图片描述
从这个图中可以看出 Scrapy 框架的工作原理,即是——引擎就是大Boss,统领其他成员协同工作。

在使用 Scrapy 的时候。我们不需要去关心爬虫的每个流程。并且Scrapy中的网络请求都是默认异步模式,请求和返回都会由引擎去自动分配处理。

如果某个请求出现异常,框架也会帮我们做异常处理,跳过这个异常的请求,继续去执行后面的程序。

所以说,我们使用 Scrapy 可以省掉很多的时间和工作!就像我们直接住进精装房一样!

二、Scrapy的用法

现在,我们已经初步了解Scrapy的结构以及工作原理。接下来,为了熟悉Scrapy的用法,我们使用它来完成一个小项目——爬取豆瓣Top250图书。

1)明确目标与分析过程

我们还是来遵循「项目实现」的三步骤:、

  1. 明确目标
  2. 分析过程
  3. 代码实现

我们在代码实现部分,重点讲解Scrapy的使用方法。

明确目标,让我们观察一下要爬取的网站:https://book.douban.com/top250。豆瓣Top250图书一共有10页,每页有25本书籍。

我们的初步目标是:先只爬取前三页书籍的信息,总共是 25 x 3 = 75 本,包含书名、出版信息和书籍评分。

那就是分析要爬取的网页结构。 既然我们要爬取书籍信息,我们就得先判断这些信息被存在了哪里。

右击打开“检查”工具,点开Network,刷新页面,然后点击第0个请求top250,看Response.

我们能在里面找到书名、出版信息,说明我们想要的书籍信息就藏在这个网址的HTML里。

https://book.douban.com/top250?start=25

我们可以看到,网址发生了变化,后面多了?start=25。现在的你,应该一眼就能猜到后面的数字应该是代表一页的25本书籍吧?

你可以翻到第3页,验证一下猜想是不是正确的。

事实证明,我们猜对了。每翻一页,网址后面的数字都会增加25,说明这个start的参数就是代表每页的25本书籍。

我们要爬取的网址的构造规律就出来了。只要改变?start=后面的数字(翻一页加25),我们就能得到每一页的网址。

找到了网址的构造规律之后,我们就可以重点来分析HTML的结构,看看等下怎么才能提取出我们想要的书籍信息。

还是是右击打开“检查”工具,点击Elements,再点击光标,把鼠标依次移到书名、出版信息、评分处,就能在HTML里找到这些书籍信息。如下图,《追风筝的人》的书籍信息就全部放在

标签里。
在这里插入图片描述
仔细观察你就会发现,其实每一页的25本书籍信息都分别藏在了一个
标签里。不过这个标签没有class属性,也没有id属性,不方便我们提取信息。

我们得继续再找一个既方便我们提取,又能包含所有书籍信息的标签。

我们可以看看

标签下的元素,好像刚好都能满足我们的要求,既有class属性,又包含了书籍的信息。

我们只要取出元素下元素的title属性的值、

元素、元素,就能得到书名、出版信息和评分的数据。

页面分析完毕,接着进入代码实现的步骤,要开始写 Scrapy 的代码啦。

2)代码实现——创建项目

如果你想着自己本地的电脑使用Scrapy,需要提前安装好它。

  • Windows:在终端输入命令:pip install scrapy;
  • mac:在终端输入命令:pip3 install scrapy

创建Scrapy项目

首先,在本地电脑打开终端,然后跳转到你想要保存项目的目录下。

  • windows:Win+R,输入cmd;
  • mac:command+空格,搜索“终端”

假设你想跳转到D盘里名为Python文件夹中的scrapy_project子文件夹。

  1. 你需要再命令行输入d:,就会跳转到D盘
  2. 再输入cd Python,就能跳转到Python文件夹。
  3. 接着输入cd scrapy_project,就能跳转到Python文件夹里的scrapy_project子文件夹。
  4. 再输入一行能帮我们创建Scrapy项目的命令:scrapy startproject douban(douban就是Scrapy项目的名字)。按下enter键,一个Scrapy项目就创建成功了。

在mac系统中的终端里,也是使用cd命令来跳转到你要的文件夹位置

在这里插入图片描述
整个scrapy项目的结构,如下图所示:
在这里插入图片描述
Scrapy项目里每个文件都有它对应的具体功能。例如:

  • settings.py 是scrapy里的各种设置
  • items.py是用来定义数据的
  • pipelines.py是用来处理数据的,它们对应的就是Scrapy的结构中的Item Pipeline(数据管道)。

如前所述,spiders是放置爬虫的目录。我们可以在spiders这个文件夹里创建爬虫文件。

我们来把这个文件,命名为Book_douban_Top250。后面的大部分代码都需要在这个Book_douban_Top250.py文件里编写。
在这里插入图片描述

代码实现——编辑爬虫

先在Book_douban_Top250.py文件里导入我们需要的模块。

import scrapy
import bs4
  • 导入BeautifulSoup用于解析和提取数据;
  • 导入scrapy是待会我们要用创建类的方式写这个爬虫,我们所创建的类将直接继承scrapy中的scrapy.Spider类。这样,有许多好用的属性和方法,就能够直接使用。

接着我们开始编写爬虫的核心代码。

在Scrapy中,每个爬虫的代码结构基本都如下所示:

import scrapy
import bs4

class DoubanSpider(scrapy.Spider):
    name = 'book_douban'
    allowed_domains = ['book.douban.com']
    start_urls = ['https://book.douban.com/top250?start=0']
    
    def parse(self, response):
        print(response.text)

代码解释

  • 第1行代码:定义一个爬虫类DoubanSpider。就像我刚刚讲过的那样,DoubanSpider类继承自scrapy.Spider类。

  • 第2行代码:name是定义爬虫的名字,这个名字是爬虫的唯一标识。name = 'book_douban’意思是定义爬虫的名字为book_douban。等会我们启动爬虫的时候,要用到这个名字。

  • 第3行代码:allowed_domains是定义允许爬虫爬取的网址域名(不需要加https://)。如果网址的域名不在这个列表里,就会被过滤掉。

    为什么会有这个设置呢?
    当你在爬取大量数据时,经常是从一个URL开始爬取,然后关联爬取更多的网页。
    假设我们在今天的爬虫目标是爬豆瓣top250每本书籍的书评信息,我们会先爬取书单,再找到每本书的URL,再进入每本书的详情页面去抓取评论。
    allowed_domains就限制了我们这种关联爬取的URL,一定在book.douban.com这个域名之下,不会跳转到某个奇怪的广告页面。

  • 第4行代码:start_urls是定义起始网址,就是爬虫从哪个网址开始抓取。在此,allowed_domains的设定对start_urls里的网址不会有影响。

  • 第5行代码:parse是Scrapy里默认处理response的一个方法。

是不是觉得和之前自己手动写爬虫的完全不一样了呢?怎么连 requests.get() 都没有了呀?其实在框架里,我们并不需要写这一句。

scrapy框架会为我们代劳做这件事,写好你的请求,接下来你就可以直接写对 response 如何做处理了。

豆瓣Top250图书一共有10页,每一页的网址我们都知道。我们可以把10页网址都塞进start_urls的列表里。

在这里插入图片描述
我们可以利用豆瓣Top250图书的网址规律,用for循环构造出每个网址,再把网址添加进start_urls的列表里。
在这里插入图片描述
完善后的代码如下:

import scrapy
import bs4


class DoubanSpider(scrapy.Spider):
    name = 'book_douban'
    allowed_domains = ['book.douban.com']
    start_urls = []
    for i in range(3):
        url = 'https://book.douban.com/top250?start={}'.format(i * 25)
        start_urls.append(url)

    def parse(self, response):
        print(response.text)

接下来,只要再借助DoubanSpider(scrapy.Spider)中的 parse 方法处理 response,借助 BeautifulSoup 来取出我们想要的书籍信息的数据。这些在前面的课程我们都使用过,所以肯定是很熟练啦!

我们前面在分析项目过程的时候,已经知道书籍信息都藏在了哪些元素里,现在可以利用find_all和find方法提取出来。比如,书名是元素下元素的title属性的值;出版信息在

元素里;评分在元素里。

import scrapy
import bs4


# 定义一个爬虫类DoubanSpider
class DoubanSpider(scrapy.Spider):
    # 定义爬虫的名字
    name = 'book_douban'
    # 定义爬虫的爬取的网址的域名
    allowed_domains = ['book.douban.com']
    # 定义起始网址
    start_urls = []
    # 将豆瓣Top250图书的前3页网址添加进start_urls列表中
    for i in range(3):
        url = 'https://book.douban.com/top250?start={}'.format(i * 25)
        start_urls.append(url)

    # parse是默认处理response的方法
    def parse(self, response):
        # 使用BeautifulSoup解析response
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用find_all提取<tr class="item">元素,这个元素里含有书籍信息。
        datas = bs.find_all('tr', class_='item')

        # 遍历书籍信息列表
        for data in datas:
            # 取出书名
            title = data.find_all('a')[1].find('title')
            # 取出出版信息
            publish = data.find('p', class_='pl').text
            # 取出评分
            score = data.find('span', class_='rating_nums').text
            # 打印书籍信息
            print([title, publish, score])

按照之前写爬虫的方式,我们会把书名、出版信息、评分,分别赋值,然后统一做处理——或是打印,或是存储。但在scrapy这里,事情却有所不同。

  • spiders(如Book_douban_Top250.py)只干spiders应该做的事。对数据的后续处理,另有其他“部门”负责。

在scrapy中,我们会专门定义一个用于记录数据的类。

当我们需要记录数据的时候,比如前面在每一个最小循环里,都要记录“书名”,“出版信息”,“评分”。我们会实例化一个对象,利用这个对象来记录数据。

定义这些数据类的python文件,正是items.py。

我们已经知道,我们要爬取的数据是书名、出版信息和评分。接下来让我们在 items.py 中编写这部分的代码。

# 导入scrapy
import scrapy


# 定义一个类BookDoubanItem,它继承自scrapy.Item
class BookDoubanItem(scrapy.Item):
    # 定义书名的数据属性
    title = scrapy.Field()
    # 定义出版信息的数据属性
    publish = scrapy.Field()
    # 定义评分的数据属性
    score = scrapy.Field()

代码解释

  • 第1行代码,我们导入了scrapy。因为我们等会所创建的类将直接继承scrapy中的scrapy.Item类。这样,有很多好用属性和方法,就能够直接使用。比如,引擎能将item类的对象发给Item Pipeline(数据管道)处理。

  • 第3行代码:我们定义了一个BookDoubanItem类。它继承自scrapy.Item类。

  • 第5、7、9行代码:我们定义了书名、出版信息和评分三种数据。scrapy.Field()这行代码实现的是,让数据能以类似字典的形式记录。

下面的代码展示了一下如何使用 BookDoubanItem:

# 导入scrapy
import scrapy


# 定义一个类BookDoubanItem,它继承自scrapy.Item
class BookDoubanItem(scrapy.Item):
    # 定义书名的数据属性
    title = scrapy.Field()
    # 定义出版信息的数据属性
    publish = scrapy.Field()
    # 定义评分的数据属性
    score = scrapy.Field()


# 实例化一个DoubanbookItem对象
book = BookDoubanItem()
book['title'] = '追风筝的人'
book['publish'] = '[美]卡勒德·胡塞尼 / 李继宏 / 上海人民出版社 / 2006-5 / 29.00元'
book['score'] = '8.9'

print(book)
print(type(book))

# 输出结果:
{'publish': '[美]卡勒德·胡塞尼 / 李继宏 / 上海人民出版社 / 2006-5 / 29.00元',
 'score': '8.9',
 'title': '追风筝的人'}
<class '__main__.BookDoubanItem'>

你会看到打印出来的结果的确和字典非常相像,但它却并不是dict,它的数据类型是我们定义的DoubanItem,属于“自定义的Python字典”。

我们可以利用类似上述代码的样式,去重新写Book_douban_Top250.py。如下所示:

import scrapy
import bs4
from ..items import BookDoubanItem

# 定义一个爬虫类DoubanSpider
class DoubanSpider(scrapy.Spider):
    # 定义爬虫的名字
    name = 'book_douban'
    # 定义爬虫的爬取的网址的域名
    allowed_domains = ['book.douban.com']
    # 定义起始网址
    start_urls = []
    # 将豆瓣Top250图书的前3页网址添加进start_urls列表中
    for i in range(3):
        url = 'https://book.douban.com/top250?start={}'.format(i * 25)
        start_urls.append(url)

    # parse是默认处理response的方法
    def parse(self, response):
        # 使用BeautifulSoup解析response
        bs = bs4.BeautifulSoup(response.text, 'html.parser')
        # 用find_all提取<tr class="item">元素,这个元素里含有书籍信息。
        datas = bs.find_all('tr', class_='item')

        # 遍历书籍信息列表
        for data in datas:
            # 实例化BookDoubanItem类
            item = BookDoubanItem()
            # 提取书名,并把这个数据放回BookDoubanItem类的title属性里
            item['title'] = data.find_all('a')[1]['title']
            # 提取出版信息,并把这个数据放回BookDoubanItem类的title属性里
            item['publish'] = data.find('p',class_='pl').text
            # 提取评分,并把这个数据放回BookDoubanItem类的title属性里
            item['score'] = data.find('span',class_='rating_nums').text
            # 打印书名
            print(item['title'])

            # yield item是把获得的item传递给引擎
            yield item

当我们需要记录一次数据的时候,比如前面在每一个最小循环里,都要记录“书名”,“出版信息”,“评分”。我们会实例化一个item对象,利用这个对象来记录数据。一个对象对应一次数据。

在Scrapy框架里,每一次当数据完成记录,它会离开spiders,来到Scrapy Engine(引擎),引擎将它送入Item Pipeline(数据管道)处理。这里,要用到yield语句。

yield语句可以简单理解为:它有点类似return,不过它和return不同的点在于,它不会结束函数,且能多次返回信息。
在这里插入图片描述
如果用可视化的方式来呈现程序运行的过程,就如同上图所示:爬虫(Spiders)会把豆瓣的10个网址封装成requests对象,引擎会从爬虫(Spiders)里提取出requests对象,再交给调度器(Scheduler),让调度器把这些requests对象排序处理。

然后引擎再把经过调度器处理的requests对象发给下载器(Downloader),下载器会立马按照引擎的命令爬取,并把response返回给引擎。

紧接着引擎就会把response发回给爬虫(Spiders),这时爬虫会启动默认的处理response的parse方法,解析和提取出书籍信息的数据,使用item做记录,返回给引擎。引擎将它送入Item Pipeline(数据管道)处理。

代码实操——设置

到这里,我们就用代码编写好了一个爬虫。不过,如果现在就运行的话,可能还是会报错。

原因在于Scrapy里的默认设置没被修改。比如我们需要修改请求头(User-Agent需要设置)。点击settings.py文件,你能在里面找到如下的默认设置代码:

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'douban (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True

请把USER _AGENT的注释取消(删除#),然后替换掉user-agent的内容,就是修改了请求头。

又因为Scrapy是遵守robots协议的,如果是robots协议禁止爬取的内容,Scrapy也会默认不去爬取,所以我们还得修改Scrapy中的默认设置。

把ROBOTSTXT_OBEY=True改成ROBOTSTXT_OBEY=False,就是把遵守robots协议换成无需遵从robots协议,这样Scrapy就能不受限制地运行。

修改后的代码如下所示:

# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36'}

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

现在,我们已经编写好了spider文件,也修改好了setting文件。最后,我们要来运行整个 scrapy 工程。

代码实操——运行

想要运行Scrapy有两种方法:

  • 一种是在本地电脑的终端跳转到scrapy项目的文件夹(跳转方法:cd+文件夹的路径名),然后输入命令行:scrapy crawl book_douban(book_douban就是我们爬虫的名字)。
  • 另一种运行方式需要我们在最外层的大文件夹里新建一个main.py文件(与scrapy.cfg同级)。
# 导入cdmline模块,可以实现控制终端命令行
from scrapy import cmdline

# 用execute()方法,输入运行scrapy命令
cmdline.execute(['scrapy', 'crawl', 'book_douban'])

代码解释

  • 第1行代码:在Scrapy中有一个可以控制终端命令的模块cmdline。导入了这个模块,我们就能操控终端。

  • 第3行代码:在cmdline模块中,有一个execute方法能执行终端的命令行,不过这个方法需要传入参数。我们输入运行Scrapy的代码scrapy crawl book_douban,就需要写成[‘scrapy’,‘crawl’,‘book_douban’]这样。

需要注意的是:为了便于理解和掌握,在这次的内容中,我们是先写了爬虫,再定义数据。但是,在我们平常实际做爬虫项目的时候,顺序却是相反的——先定义数据,再写爬虫。所以,流程图应如下:
在这里插入图片描述

三、练习

1.之前的内容中, 我们没有使用任何框架来爬取豆瓣新片榜。在本次的练习中,我们使用Scrapy来爬取豆瓣新片榜。 地址:https://movie.douban.com/。

实现步骤如下:

1. 创建Scrapy

在命令行中,cd到对应的文件夹路径

# 打开对应的文件夹路径
scrapy startproject douban_movie

2. 定义Items

获取电影标题、导演、评分、简介信息


import scrapy


class DoubanMovieItem(scrapy.Item):
    title = scrapy.Field()
    director = scrapy.Field()
    score = scrapy.Field()
    info = scrapy.Field()

3. 编写spiders

在spiders文件夹下方,创建“douban_movie.py"文件,编辑如下代码

import scrapy
import bs4
from ..items import DoubanMovieItem

# 定义一个爬虫类Douban_moiveSpider
class Douban_movieSpider(scrapy.Spider):
    # 定义爬虫的名字
    name = 'douban_movie'
    # 定义爬虫爬取的网址的域名
    allowed_domains = ['movie.douban.com']
    # 定义起始网址
    start_urls = []
    for i in range(3):
        url = 'https://movie.douban.com/top250?start={}&filter='.format(i*25)
        start_urls.append(url)

    # 定义处理返回数据的类
    def parse(self, response):
        # 使用Beautifulsoup解析response
        bs = bs4.BeautifulSoup(response.text,'html.parser')
        datas = bs.find_all('div',class_='info')

        for data in datas:
            # 实例化DoubanMovieItem类
            item = DoubanMovieItem()
            # 爬下来的电影标题、导演文本中,有“&nbsp”及"\xa0",使用replace替换掉
            item['title'] = data.find('div', class_='hd').find('a').text.replace(u'\xa0','').replace('\n','')
            item['director'] = data.find('p').text.replace(' ', '').replace(u'\xa0','').replace('\n', '')
            # 评分内容有评分和评价人数,中间有换行符和空格,先替换掉空格,再将换行符替换为空格,后去掉头尾的空格
            item['score'] = data.find('div', class_='star').text.replace(' ', '').replace('\n', ' ').strip()
            item['info'] = data.find('p', class_='quote').text.replace('\n', '')

            # yield item 把获得的item传递给引擎
            yield item


网页源代码中的&nbsp; 的utf-8 编码是:\xc2\xa0,解析后,转换为Unicode字符为:\xa0,当使用print() 显示到DOS窗口上的时候,转换为GBK编码的字符串,但是\xa0这个Unicode字符没有对应的 GBK 编码的字符串,所以出现错误。
解决方法:用空格 来替换 \xa0 (&nbsp;)

  • 方法1:在网页原码上替换&nbsp;
soup = BeautifulSoup(html.replace('&nbsp;', ' '), 'html.parser')
  • 方法2:在解析为Unicode之后替代\xa0
job_detail = soup.select('.job-detail')[0].text.replace(u'\xa0', ' ')

4. 修改settings文件

BOT_NAME = 'douban_movie'

SPIDER_MODULES = ['douban_movie.spiders']
NEWSPIDER_MODULE = 'douban_movie.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36'}


# Obey robots.txt rules
ROBOTSTXT_OBEY = False

5. 运行scrapy

  • 在命令行中,cd到项目所在文件夹,运行“scrapy crawl douban_movie"
  • 在与"scray.cfg"文件同级的文件夹中,新建文件"main.py"
# 导入cdmline模块,可以实现控制终端命令行
from scrapy import cmdline

#用execute()方法,输入运行scrapy的命令
cmdline.execute(['scrapy','crawl','douban_movie'])
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章