使用Scrapy框架抓取豆瓣电影数据

2021-06-28

Scrapy框架是最成熟,使用很广泛的Python爬虫框架。本文将带领大家用Scrapy框架编写简单的爬虫来采集豆瓣电影的数据。

Scrapy框架是最成熟,非常流行的爬虫框架,github上40.9K的star数量足以说明其流行程度。其背后的维护者是Zyte(前身是Scrapinghub)也是提供数据采集相关服务的商业公司。项目的迭代也一直非常活跃。Scrapy经过多年的迭代,已经覆盖了编写爬虫需要考虑的方方面面: - 多线程 - 抽取数据 - 数据校验 - 保存数据 - 等等

对比其他的请求库,比如Request或BeautifulSoup,Scrapy的主要优势是爬虫需要考虑的各个方面都提供了相对成熟的处理方法。相对的,要使用好Scrapy也需要学习更多的知识。现在我们开始用Scrapy来采集豆瓣电影的数据实际感受一下Scrapy框架。

安装并创建项目

安装Scrapy非常简单:

pip install Scrapy

请注意,Python社区推荐使用虚拟环境来避免冲突,关于虚拟环境不是本文关注的重点。如果想要了解Python的虚拟环境,请参考这里

安装好后,我们可以用Scrapy提供的命令来创建项目:

scrapy startproject douban

我们用tree命令看看项目的初始结构是什么样的:

➜  movie tree
.
├── movie
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       └── __init__.py
└── scrapy.cfg

2 directories, 7 files

解释一下这些文件的作用:

  • items.py:定义抓取数据的模型,对于数据简单的场景可以不需要定义模型。但是定义模型的好处是可以利用Scrapy内置的item_loader相关的能力方便抽取数据和处理数据。
  • middlewares.py:自定义中间件,中间件可以控制请求/响应过程的一些行为,Scrapy本身也已经内置了很多中间件。
  • pipelines.py:自定义流水线,流水线主要用于处理抓取好的数据,抓取好的item都会经过配置好的流水线处理。比如存储到数据库等等。
  • spiders:存放爬虫的文件夹,我们的主要的代码编写工作就在编写爬虫。
  • scrapy.cfg:项目配置文件。

开始抓取页面数据

我们的目标是抓取豆瓣上影片的主页上的数据,那我们就来抓取最近的这部高分电影:

目标页面

网址是:https://movie.douban.com/subject/33432655/

接下来我们要从这个页面采集电影名称,电影年份,导演,类型,豆瓣评分等信息。

创建爬虫

我们现在创建我们的爬虫(当然也可以手动创建,放在/spiders目录下就行):

scrapy genspider movie movie.douban.com

打开自动创建的爬虫文件:

import scrapy


class MovieSpider(scrapy.Spider):
    name = 'movie'
    allowed_domains = ['movie.douban.com']
    start_urls = ['http://movie.douban.com/']

    def parse(self, response):
        pass

代码很简单,但是体现了整个爬虫运行的主体过程。采集数据的过程其实可以简单的分成三个阶段:请求页面,获取数据,抽取数据。

解释一下代码:

  • name:爬虫的名字,一个Scrapy项目可以定义多个爬虫,可以用scrapy list查看项目内的爬虫。
  • allowed_domains:定义允许采集的域名,如果不想限制可以不定义。
  • start_urls:爬虫启动后的初始访问页面地址,是个数组结构,可以定义多个初始地址。这个初始地址的默认处理函数即parse函数。更进一步的,你可以定义函数start_request更加灵活的创建要访问的页面地址。

我们现在把目标页面地址替换默认生成的地址

    allowed_domains = ['movie.douban.com']
    start_urls = ['https://movie.douban.com/subject/33432655/']

豆瓣会检测请求的User Agent的设置,所以我们还需要打开settings.py,填上一个比较常用的UA设置:

# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36'

好了,这时候我们可以开始试试我们的爬虫是不是能够运行起来。当然我们还没有开始抽取数据,我们主要是验证是否能正常请求到页面数据:

scrapy crawl movie

可以从日志里面观察到,我们请求目标网页,已经返回了200的请求状态了。

编写数据抽取规则

页面我们已经请求回来了,那么Scrapy就会调用默认的parse函数,并且response对象就携带了目标网页的内容了。我们现在需要开始编写数据抽取规则了。

Scrapy支持CSS和XPATH两种抽取数据的模式。CSS相对用得多一点。我们先开始处理电影标题。

我们在谷歌浏览器中打开目标网页,然后打开开发者工具,然后选择网页中电影标题的元素:

电影标题

分析DOM的结构,我们可以用一下规则抽取电影的标题内容出来:

    def parse(self, response):
        m = {}
        m["name"] = response.css('div#content > h1 > span:first-child::text').get()
        yield m

类似的,从页面的DOM元素中找到一些关键特征,比如class,id,位置等等,把其他的数据抽取出来。最终的parse函数是这样的:

    def parse(self, response):
        m = {}
        m["name"] = response.css('div#content > h1 > span:first-child::text').get()
        m["year"] = response.css('div#content > h1 > span.year::text').get()
        m["director"] = response.css('div#info > span > span.attrs > a::text').get()
        m["genre"] = response.css('span[property="v:genre"]::text').get()
        m["rating"] = response.css('div.rating_self > strong.rating_num::text').get()
        m["rating_count"] = response.css('a.rating_people > span::text').get()
        yield m

好的,让我们再次运行我们的爬虫:

scrapy crawl movie

从日志里面我们已经看到我们抽取出来的数据了:

2021-06-28 14:20:26 [scrapy.core.scraper] DEBUG: Scraped from <200 https://movie.douban.com/subject/33432655/>
{'name': '困在时间里的父亲 The Father', 'year': '(2020)', 'director': '佛罗莱恩·泽勒', 'genre': '剧情', 'rating': '8.7', 'rating_count': '103517'}

有时候我们想要抽取数据并没有这么明显的DOM特征,比如一些老的网站,标签使用非常不正规的情况下,抽取数据可能会比较困难,需要不停的尝试。 这种情况下,频繁的运行我们的爬虫来观察效果并不效率。建议使用scrpay shell来寻找抽取数据的规则。

抓取多个数据

我们现在已经可以抓取一个页面的电影基本信息了,我们现在想要一次多抓取一些数据,所以我们需要规划整个爬虫的抓取路径,来依次的抓取电影。

我们这次从豆瓣电影TOP250页面入手,尝试遍历榜单,抓取每部电影的基本信息。

第一步,我们需要更改我们的初始访问页面:

    allowed_domains = ['movie.douban.com']
    start_urls = ['https://movie.douban.com/top250']

那么默认的parse函数将要处理的不再是单个电影页面,而是一个列表页面,我们需要处理好两个事情:1、获取当前列表的电影链接,继续叫给爬虫抓取抽取电影信息。2、处理好分页,访问列表下一页,直到最后一页。

我们把之前parse函数重命名为parse_movie函数,负责抽取电影页面的信息。然后我们新建一个parse函数:

    def parse(self, response):
        for url in response.css('div.item > div.info > div.hd > a::attr(href)').extract():
            yield response.follow(url, self.parse_movie)

        next_page = response.css('div.paginator > span.next > a::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

这里有几点说明:

  1. 如果需要继续访问某个页面,只需要返回一个Request对象即可,Scrapy会负责继续请求。
  2. 在返回Request的时候你可以指定需要处理请求返回数据的函数,比如我们指定parse_movie来处理具体的电影页面。

那么我们开始运行我们的爬虫,因为这次的数据较多,我们把抓取的数据按照json格式输出到文件。Scrapy内置了这样的支持,只需要加上参数就行:

scrapy crawl movie -o top250.json

运行完以后,可以在当前目录下找到top250.json文件,我们抓取的电影基本数据都存在这个文件里面了。

请注意:因为抓取的数据不少,运行这个爬虫可能会被豆瓣的反爬机制检测出来,可以把并发降低,加入适当的延迟避免过快的请求豆瓣的数据。

Scrapy进阶话题

上文我们只是创建了一个最简单的爬虫,也只是用到了Scrapy最基本的一些特性。但是可以感觉出来,Scrapy已经把相当多的工作标准化了,我们只需要关注如何抽取数据,如何控制页面抓取逻辑就行。

但是Scrapy还有很多非常有用的特性,能帮助你更好的处理这些任务:

Item和ItemLoader

我们抽取的电影数据,是以Hash字典的形式返回的,在复杂的数据抽取情况下,会比较麻烦。Scrapy定义了Item类型,配合ItemLoader的特性使用非常方便。 比如,我们上面的爬虫并没有处理电影年份的括号,也没有处理电影名里面的中文和英文名。用ItemLoader来处理就非常简单。

具体的请查看Scrapy的相关文档

Middleware

在爬取数据的时候,我们经常要对请求和返回做一些处理以避免被反爬机制侦测出来,比如设置代理IP,动态调整UA等等。 这些都可以通过编写(或使用内置的)Scrapy中间件完成。具体请参考文档

Pipeline

我们目前对抓取回来的数据没有做过多的处理,仅仅是以json格式保存到文件。如果需要保存到数据库,那么我们需要编写存储数据到数据库的pipeline。 Scrapy会把抽取的数据Item都放入pipeline处理。具体请参考文档

Scrapy官方文档非常详尽,也有很多最佳实践的指导。可以仔细阅读。

本文介绍了Scrapy框架,并用Scrapy编写了抓取豆瓣电影TOP250的简单爬虫,希望对你们有帮助。