学会用scrapy爬虫

881 阅读18分钟

1. 什么是爬虫

    说到爬虫,我们可能会想到利用互联网快速地获取我们需要的网络资源,例如图片、文字或者视频。这是真的,如果我们掌握了爬虫技术,我们就可以在短时间爬取网络上的东西。但是,我们能爬取的网上资源都是对方网站上直接摆出的内容,意味着我们可以手动的复制粘贴来获取,而爬虫只是代替了我们的人工,并加快这一过程而已。在进行编写网络爬虫之前,我们必须要确保我们所想获取的网络资源有迹可循,知道我们所要的网络资源在哪个网站或者请求接口,然后我们还要确保我们所需要的资源是可获取的,因为有时候如果对方不希望自己的资源被随意爬取,他们的网站可能存在一些验证机制,来拦截网络爬虫。

    进行网络爬虫的具体过程就是:批量获取网站网页的html内容(或者通过网站的资源接口获取数据)->编写数据处理代码处理数据转化为我们所需要的资源。如果有网站的数据接口,爬虫就会简单很多,我们一般使用requests就可以直接获取数据。然而大多数情况是,我们需要的数据(或数据接口)是在网页的html中,在正式获取数据之前我们还需要先进行html的页面内容解析及提取。html页面获取,内容解析,数据提取还有最后的数据存储,有时还需要考虑防爬虫机制,这看起来需要进行不少的工作,而python有个第三方库,可以帮助我们简化这些工作,并且提高爬虫效率。它就是scrapy。

2. Scrapy

    如Scrapy官网所说,Scrapy是一个用于爬取网站数据,提取结构性数据的应用框架。今天我们就用scrapy来开始尝试我们的第一次爬虫。

2.1 Scrapy安装

    我们新建一个文件夹用于开展我们的爬虫项目,名为douban_scrapy。这次我们要爬取的目标资源是豆瓣的高分电影,豆瓣有人做高分电影汇总,但是有点不好的是我们没有办法按时间或者分数高低对,也不能按按电影类型分类查看,所以我们可以将上面的数据爬取下来,自行整理。

    在开始安装scrapy前,我们在项目路径下新建个python环境(这是个好习惯,可以防止安装环境的冲突):

  • 新建文件夹myenv用于存储新python环境下的库
  • 执行命令创建新环境: python -m venv ./douban_scrapy_env
  • 激活新建环境 source ./douban_scrapy_env/bin/activate     然后执行pip install scrapy安装scrapy库

2.2 新建scrapy项目

    现在我们可以开始使用scrapy,执行scrapy startproject douban就会在当前路径下自动新建项目文件夹douban,该文件夹下包含以下文件:

douban/
    scrapy.cfg
    douban/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            

    其中:

  • scrapy.cfg 用于配置项目,例如项目设置文件路径,项目路径等
  • items.py 用于定义我们爬取的每个实体,例如title,image等
  • middlewares.py 用于定义一些中间件
  • settings.py 用于项目设置,例如爬虫机器人的命名,爬虫脚本路径等
  • spiders 用于放置爬虫脚本

    使用scrapy编写爬虫,要先定义items,然后再在spiders文件夹下写爬虫脚本。但是现在,我们还一头雾水,因为我们还没去了解我们要爬取的网页。下面我们试试用scrapy的终端调试功能来熟悉我们要爬取的网页。

2.3 scrapy shell

    我们可以用scrapy shell [url]的命令打开抓取对应网页的终端,同时爬取下来的抓取结果response,我们可以基于它来调试我们提取数据的规则。现在我们试试用scrapy shell https://www.douban.com/doulist/30299/命令来爬取一下豆瓣高分电影列表第一页:

图片已失效

    可以看到我们爬取的结果返回403,这意味着我们这样爬取他们的网页被他们的防爬虫机制拦截了。对此,我们试试在命令后加-s USER_AGENT='Mozilla/5.0':

图片已失效

    我们成功获取了正常的爬取结果,这说明对方是通过请求头信息判断出我们是爬虫脚本,通过改变USER_AGENT来伪装成我们是通过浏览器访问的。如果我们不想每次执行命令都加上USER_AGENT参数,我们可以在项目文件夹下的setting文件设定USER_AGENT='Mozilla/5.0'

    现在我们可以开始调试爬取规则。我们知道,response是我们爬取后的结果对象,当我们基于此对象创造选择器后,就可以通过这个选择器用scrapy的命令提取我们需要的内容,创造选择器有两种方式:

  • 引入Selector类创建Selector对象from scrapy.selector import Selector
  • 直接使用选择器response.selector

    在scrapy中,我们可以对选择器使用css或xpath方法来提取我们需要的数据。对于前端有所了解的话会知道css是前端定义样式的语言,而在这里,选择器对象中设定了css方法,参数是基于样式提取数据的规则语句。xpath也是选择器中定义的用于提取数据的函数,只是xpath的规则语法与css不同。本文主要使用xpath方法,下面介绍xpath语法的基本规则(用abc等单一字符代替html元素):

  • response.selector.xpath("a"), 提取指定环境下没有父元素的a元素
  • response.selector.xpath("a/b"), 提取a元素下的第一级元素b
  • response.selector.xpath("//a"), 提取页面中所有a元素不管元素位于什么位置
  • response.selector.xpath("a//b"), 提取a元素下所有b元素,不管b元素在a元素下第几级
  • response.selector.xpath("a/@b"), 提取a元素下的b属性
  • response.selector.xpath("a/b[1]"), 提取a元素下第一级b元素中的第一个
  • response.selector.xpath("//a[@b]") 提取所有含有b属性的a元素
  • response.selector.xpath("//a[@b='c']"), 提取所有b属性值为c的a元素
  • response.selector.xpath("//a[contains("b", "c")]"), 提取所有拥有b属性,且b属性值中包含c字符的a元素

    现在我们认识了xpath的部分语法规则,让我们试试提取出页面列表中所有电影的名字。我们先执行view(response)把网页在浏览器打开,然后用浏览器的开发者工具模式查看包含电影名的html内容。

图片已失效

    根据我们的观察,我们发现:电影列表的每一项都存放在类名为doulist-item的div里,该div下有个类名为mod的div存放内容,类名为mod的div下有个类名为bd doulist-subject的div存放主体内容,然后该div下类名为title的div里的a标签就存放着我们需要的电影名, 这页面内的电影名大都如此存放(我们得出如此结论是因为页面内电影列表里的每一项样式都是一样的),所以我可以用如下规则提取出页面中每个电影的名字:

response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']/div[@class='bd doulist-subject']/div[@class='title']/a/text()").extract()

    因为我们要的是a标签下的文本内容,所以我们在a标签路径后接text()表明我们要这个元素下的文本。xpath函数返回的是由一个个selector对象组成的列表,我们需要用extract函数,把selector对象转换为纯文本。

图片已失效

    上图就是我们提取出的页面中电影名,因为提取出的文本还带有一些额外的换行和退格符,所以我们再对每一项提取的字符串使用strip函数得到最后提取结果。我们对结果仔细观察发现提取的结果里有空白字符串,按理来说我们的提取路径应该是完全一致的,为何结果里会偶而穿插字符串,难道是页面内的电影名处有什么样式不一样的地方。

图片已失效

    我们看到第二个电影名处比第一个多了一个播放图标,难道这就是我们提取到空白字符串的原因?下面我们把第一个提取到的第二个包含电影名的selector展开看看:

图片已失效

    我们从图中看到,提取到的包含电影名的a标签元素,如果里面有电影图标,前面就会有一个包含换行符和空格的字符串,这就是我们提取到的电影名去除多余字符后有空白字符串的原因。当我们提取到的文本有两段,这意味着前一段是空白字符串,后一段才是我们要的电影名,那么我们每次都取最后一段文本就行了,所以我们可以把之前的xpath规则再做修改:

response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']/div[@class='bd doulist-subject']/div[@class='title']/a/text()[last()]").extract()

    这里我们在text()后面加上[last()]表明我们要的只是最后的文本。此外,因为我们使用xpath返回的是selector列表,我们是对selector对象使用xpath函数的,对selector对象列表也能使用。这意味着我们可以对xpath返回的结果再次使用xpath进行提取,这样可以让代码结构更清晰,以上电影名提取代码等同:

films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")

films_body = films_box.xpath("div[@class='bd doulist-subject']")

films_name = films_body.xpath("div[@class='title']/a/text()[last()]").extract()

    最后我们再总结一下得出用scrapy的xpath提取目标数据规则的流程:

  • 使用scrapy shell 爬取指定页面
  • 使用view(response)把爬取下来的页面内容从浏览器打开
  • 在浏览器打开开发者模式,观察目标数据在页面中的存放路径
  • 把目标数据在页面中的存放路径转换为xpath提取规则

2.4 items

    我们现在分析出了电影列表中电影名字的提取规则,这意味着我们可以开始写爬虫脚本了。在scrapy中,我们要写爬虫脚本钱,先要定义items,即需要爬取的目标实体。在scrapy中定义的item为一个个类,类名为item名,类中直接定义的类变量作为item的属性,例如:

import scrapy

class Film(scrapy.item):
    name = scrapy.Field()
    

    以上是我们定义的电影Film的item,具有电影名name这一属性值。创建的item类继承scrapy.item,在声明item属性时使用scrapy.Field,不需要指定类型。我们创建item主要目的是使用scrapy的自动保存数据功能,例如把提取结果按我们定义的item结构导出为excel文件或保存到数据库。

2.5 spider

    我们知道怎么定义item之后就可以开始考虑编写spider脚本了,编写spider有以下几个要点:

  • 引入Spider类import Spider from scrapy,编写爬虫类时需要继承此类
  • 为编写的爬虫类设置name类变量,这个name类变量定义了我们爬虫的名字,让scrapy执行爬虫时可以通过爬虫名字来指定,name的值一般为爬虫类名小写
  • 设定爬取url或请求对象,我们可以通过设定start_urls类变量来指定要爬取的url列表,或者通过编写名为start_requests的类函数来生成可迭代的请求对象
  • 编写爬取结果的解析函数,当scrapy把请求结果爬取下来了我们需要提供解析函数来做进一步处理,名为parse的类函数是scrapy爬取后如果没有指定其他回调函数会自动执行此函数

    下面我们编写豆瓣电影爬虫脚本:

import scrapy
from douban.items import Film

class DouBan(scrapy.Spider):
    name = "douban"
    start_urls=[""https://www.douban.com/doulist/30299/""]
    
    def parse(self, response):
        films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")
        films_body = films_box.xpath("div[@class='bd doulist-subject']")
        films_name = films_body.xpath("div[@class='title']/a/text()[last()]").extract()
        for name in films_name:
            name = name.strip()
            film = Film()
            film["name"] = name
            yield film

    在以上爬虫脚本中我们设置爬虫的名字为douban,要爬取的页面以及在parse函数中设定解析爬取结果并提取需要数据的方法。我们完成爬虫脚本后就可以执行scrapy crawl douban -o 文件名把提取到的数据保存到文件中,scrapy支持保存的文件类型有'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle'里面一开始是不包含excel文件格式的,但是导出为excel文件是我们最想要的(利用excel功能直接筛选或者排序电影结果)。

2.6 scrapy-xlsx

    为此我们可以安装python库scrapy-xlsx,它的主要功能就是让我们在scrapy项目中可以把我们的结果导出为excel文件。在项目环境下执行pip install scrapy-xlsx安装库,后在项目文件夹下的settings.py文件里设置:

FEED_EXPORTERS = {
    'xlsx': 'scrapy_xlsx.XlsxItemExporter',
}
            

    现在我们可以在项目文件夹路径下执行scrapy crawl douban -o douban_films.xlsx把结果导出成excel文件

2.7 item pipeline

    我们之前是在xpath中使用last()方法只取最后一段文本来过滤空白的电影名,如果我们遇到无法使用xpath语法来处理无用文本时,我们是否需要在spider脚本里进行处理?其实,scrapy提供了我们方便处理和保存提取结果的方式,名为pipeline。pipeline意思为管道,这里我们使用pipeline就像是构建新的产品流水线环节,在这流水线上我们会对产品进行检测处理,或者将产品如何封装保存。所以,我们定义pipeline主要是为了以下四点:

  • 清理数据
  • 丢弃不可用数据
  • 数据查重
  • 数据保存

    我们之前说过项目文件夹下有个pipelines.py文件,我们就是在这里定义我们的pipeline。每个定义的pipeline为一个类,类名可以随意,但是里面必须要包含方法process_item(item, spider), 在这里我们可以获取scrapy提取到的item并进行处理。现在,我们试试编写DoubanPipeline,丢失电影名为空白字符串的item:

from scrapy.exceptions import DropItem

class DoubanPipeline:
    def process_item(self, item, spider):
        if item["name"]:
            return item
        else:
            raise DropItem("丢弃空白电影名")
            

    现在我们编写了一个Pipeline,我们要让scrapy使用它,在settings.py里设置:

ITEM_PIPELINES = {
    'douban.pipelines.DoubanPipeline': 300,
}

    settings.py里的ITEM_PIPELINES项可以定义提取数据时需要使用的pipeline,里面的key是pipeline的路径,值是0-1000范围内的数字,用于决定pipeline的启用顺序。

    现在我们可以修改一下我们原来的爬虫脚本:

def parse(self, response):
    films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")
    films_body = films_box.xpath("div[@class='bd doulist-subject']")
    films_name = films_body.xpath("div[@class='title']/a/text()").extract()
    for name in films_name:
        name = name.strip()
        film = Film()
        film["name"] = name
        yield film
        

    我们在parse函数里没有在xpath语句中使用last(), 也没有再判断和过滤空白的电影名,因为我们已经有了一个用于过滤电影名的pipeline,现在我们再次执行scrapy crawl douban -o douban_films.xlsx

图片已失效

    可以看到后台输出的信息里显示,当提取到的电影名为空白字符串,就会抛弃并且输出我们自定义的警告信息,并且最后得到的输出文件结果也是和我们原来的一致。

2.8 提取每一页

    现在我们知道如何从页面内提取结果,但是我们目标的豆瓣电影列表可不止一页。为了爬取所有电影列表数据,我们需要知道每一页对应的链接。

图片已失效

    我们观察了页面下面带数字123页面跳转标签看到了他们的每一个标签对应链接,发现每个链接除了start=后面的数字不一样其他都是一致的,并且每个链接的start=后面的值以25单调递增,第一页电影列表项为25,所以我们就知道每一个链接start=后面数字应该是页数乘以25的积。

    现在我们知道了怎么生成每一个要爬取网页的链接,现在我们还要知道什么时候停下。我们可以看到在跳转页面的标签里代表当前页的span标签有个属性值为data-total-page,这就是我们所需要的。

    我们知道,当我们设定了start_urls后,scrapy会爬取里面所有连接并且默认执行parse函数,所以我们可以在start_urls里放置第一页的链接,然后在parse函数里提取data-total-page值,然后返回所以要爬取的scrapy.Request对象(带我们生成的链接),scrapy,Request对象的回调函数为parse_page,我们自定义的用于提取电影信息的函数:

class DouBan(scrapy.Spider):
  name = "douban"
  start_urls = ["https://www.douban.com/doulist/30299/"]

  def parse(self, response):
      page_total = response.selector.xpath("//span[@class='thispage']/@data-total-page").extract()
      if page_total:
          page_total = int(page_total[0])
          for i in range(page_total):
              url = "https://www.douban.com/doulist/30299/?start={}&sort=seq&playable=0&sub_type=".format(i*25)
              yield scrapy.Request(url, callback=self.parse_page)

  def parse_page(self, response):
      films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")
      films_body = films_box.xpath("div[@class='bd doulist-subject']")
      films_name = films_body.xpath("div[@class='title']/a/text()").extract()
      for name in films_name:
          name = name.strip()
          film = Film()
          film["name"] = name
          yield film

    此外,我们这次爬取的数据量大,如果持续爬取可能会被对方检测出来,他们应该也不希望自己的网站被这样无意义的密集访问。对此我们可以减慢我们的爬虫脚本访问对方数据库的速度,在项目文件夹中的settings.py文件中设置:

AUTOTHROTTLE_ENABLED = True
DOWNLOAD_DELAY = 3

    其中:

  • AUTOTHROTTLE_ENABLED设置为True后开启自动限速,可以自动调整scrapy并发请求数和下载延迟,AUTOTHROTTLE_ENABLED调整的下载延迟不会低于设置的DOWNLOAD_DELAY

  • DOWNLOAD_DELAY用于设置下载延迟,即下次下载距上次下载需要等待的时间,单位为秒

2.9 爬取所有需要的数据

    现在我们知道了如何调试xpath提取数据的路径、定义item、编写spider、借助pipeline处理数据、爬取每一页。我们基本上是了解了如何用scrapy爬虫,但我们还有个初始的目标: 把每个电影的信息(名字、评分、评价人数、导演、主演、类型、制片国家或地区和时间)爬取下来,好方便我们在线下自己对这些数据进行分类排序以找到我们自己喜欢看的电影。

    在我们开始写这些信息的提取路径之前,我们先在原来的item类Film里定义新的信息字段:

class Film(scrapy.Item):
  name = scrapy.Field()
  score = scrapy.Field()
  rating_users = scrapy.Field()
  director = scrapy.Field()
  starring = scrapy.Field()
  film_type = scrapy.Field()
  country_regin = scrapy.Field()
  year = scrapy.Field()

    现在我们再在shell中调试每个信息的xpath提取路径:

films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")
films_body = films_box.xpath("div[@class='bd doulist-subject']")
# 电影评分
films_score = films_body.xpath("div[@class='rating']/span[@class='rating_nums']/text()").extract()
# 评价人数
rating_users = films_body.xpath("div[@class='rating']/span[3]/text()").extract()
number_pattern = "[0-9]+"
rating_users = [re.search(number_pattern, i).group() for i in rating_users]
# 电影主要信息
films_abstract = films_body.xpath("div[@class='abstract']")

    电影主要信息里包含了我们需要的电影导演、主演、类型等信息,现在我们再在原来的spider脚本进行补充:

...
import re
...
def parse_page(self, response):
    films_box = response.selector.xpath("//div[@class='doulist-item']/div[@class='mod']")
    films_body = films_box.xpath("div[@class='bd doulist-subject']")
    films_name = films_body.xpath("div[@class='title']/a/text()[last()]").extract()
    films_score = films_body.xpath("div[@class='rating']/span[@class='rating_nums']/text()").extract()
    
    rating_users = films_body.xpath("div[@class='rating']/span[3]/text()").extract()
    number_pattern = "\d+"
    rating_users = [re.search(number_pattern, i).group() for i in rating_users]
    films_abstract = films_body.xpath("div[@class='abstract']")
    for idx, name in enumerate(films_name):
        name = name.strip()
        film = Film()
        film["name"] = name
        film["score"] = films_score[idx]
        film["rating_users"] = rating_users[idx]
        info_list = films_abstract[idx].xpath("text()").extract()
        info_list = [i.strip() for i in info_list]
        for s in info_list:
            if s.startswith("导演"):
                film["director"] = s[4:]
            elif s.startswith("主演"):
                film["starring"] = s[4:]
            elif s.startswith("类型"):
                film["film_type"] = s[4:]
            elif s.startswith("制片国家/地区"):
                film["country_regin"] = s[9:]
            elif s.startswith("年份"):
                film["year"] = s[4:]
        yield film

    现在我们再次在项目文件夹路径下执行scrapy crawl douban -o douban_films.xlsx就可以得到一份包含我们所有需要信息的excel。

图片已失效

    以上就是我们在结果excel文件中筛选出来的评分大于8.9,以评价人数降序排列后的电影信息,也就是大家一致认为好看的电影哦。

3. 总结

    这篇文章内容到此就要结束了,本文至此主要讲了用scrapy的15点内容:

  • 什么是爬虫
  • 什么是scrapy
  • scrapy的安装
  • 启动一个scrapy项目
  • 一个初始scrapy项目的架构
  • 使用scrapy shell调试爬取路径
  • 什么是xpath以及它的基本语法
  • 编写items
  • 编写spider
  • scrapy的文件输出
  • 如何让scrapy可以输出excel
  • 使用pipeline
  • 如何持续获取每一个需要的url
  • 调节爬虫脚本对网站数据的获取速度
  • 爬取豆瓣电影评分

    希望这篇文章对大家的爬虫启蒙能够有所帮助。