学会用scrapy爬虫

1. 什么是爬虫

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

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

2. Scrapy

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

2.1 Scrapy安装

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

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

  • 新建文件夹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,该文件夹下包含如下文件:json

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的终端调试功能来熟悉咱们要爬取的网页。markdown

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
  • 调节爬虫脚本对网站数据的获取速度
  • 爬取豆瓣电影评分

    但愿这篇文章对你们的爬虫启蒙可以有所帮助。