前言

最近需要做一个通过神经网络(LSTM)做情感分析的项目。第一步数据集就难住了,英文可以用 IMDB 的评论数据集,但是没有找到好用的中文数据集,就想着自己用爬虫爬一些数据。考虑了一下,决定用豆瓣的影评作为原始数据,一方面是和 IMDB 数据集类似,处理数据时可以借鉴一下,而且豆瓣影评带一个分数,可以方便标记数据,不用人工标记,但是得针对性的选一些电影的影评。

以前也写过爬虫,但是用得是最基本的 urllib 库,这次为了更好更快的爬取数据,决定学习 Scrapy 这个爬虫框架。本文开始会介绍 Scrapy 的基本的用法,然后给出爬虫代码,最后我会把我爬去的数据集预处理后分享出来。

Scrapy 使用

创建一个 Scrapy 工程

首先安装 Scrapy pip install scrapy

选择一个喜欢的目录,运行下面的命令,Scrapy 会根据模板创建一个 douban 工程。

1
scrapy startproject douban

目录结构如下:

douban/ scrapy.cfg # 部署的配置文件

douban/ # 工程的 Python 模块,在里面写代码 init.py

items.py

middlewares.py

pipelines.py

settings.py

spiders/ init.py

第一个爬虫

爬虫是一个继承自 scrapy.Spider 的类,Scrapy 通过定义的类爬取数据。文件应该保存在 spiders 文件夹里。下面是一个例子,爬取豆瓣首页内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-

import scrapy


class DoubanIndexSpider(scrapy.Spider):
    name = "DoubanIndex"

    def start_requests(self):
        urls = ["https://www.douban.com"]

        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        with open("douban_index.html", "wb") as f:
            f.write(response.body)

name 是一个爬虫的名字,在同一个工程内,它应该是独一无二的。

start_requests 请求发起请求 parse 解析响应数据

运行爬虫

到工程的根目录运行下面的命令:

1
scrapy crawl DoubanIndex

第一次运行爬不了,修改 settings.py 中的 User-Agent 为:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36

成功把豆瓣首页爬下来保存到 douban_index.html

可以不写 start_requests 方法,用 start_urls 代替

交互式解析数据

使用 Scrapy shell 方法可以交互式的解析数据,类似 IPython,在学习或者分析网页时非常方便:

1
scrapy shell "https://www.douban.com/"

可以使用各种选择器对 response 中的内容进行选择

1
2
response.css(".lnk-book").get()
response.css("div").getall()

定义 Item

Item 是保存爬取到的数据的容器。其使用方法和 python 字典类似, 并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。

我们在 items.py 内定义我们需要保存的数据。

1
2
3
4
import scrapy

class DoubanIndexItem(scrapy.item):
    url = scrapy.Field()

上面我们只保存 url

在代码中解析数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-

import scrapy
from douban.items import DoubanIndexItem


class DoubanIndexSpider(scrapy.Spider):
    name = "DoubanIndex"
    start_urls = ["https://douban.com"]

    def parse(self, response):
        # 解析豆瓣首页的 nav-bar 的link url
        for li in response.css("div#anony-nav div.anony-nav-links ul li"):
            item = DoubanIndexItem()
            item["url"] = li.css("a::attr(href)").get()

            yield item # 返回item

将输出保存在文件中。

1
scrapy crawl DoubanIndex -o nav-link.json

运行后,/nav-link.json/ 的内容如下:

[ {“url”: “https://book.douban.com”}, {“url”: “https://movie.douban.com”}, {“url”: “https://music.douban.com”}, {“url”: “https://www.douban.com/group/”}, {“url”: “https://www.douban.com/location/”}, {“url”: “https://douban.fm”}, {“url”: “https://time.douban.com/?dt_time_source=douban-web_anonymous_index_top_nav”}, {“url”: “https://market.douban.com?utm_campaign=anonymous_top_nav&utm_source=douban&utm_medium=pc_web”}

跟踪页面的链接

上一步已经获取了链接,下一部我们爬链接里的内容,即动态的添加链接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding: utf-8 -*-

import scrapy


class DoubanIndexSpider(scrapy.Spider):
    name = "DoubanIndex"
    start_urls = ["https://www.douban.com/"]

    def parse(self, response):
        # 解析豆瓣首页的 nav-bar 的link url
        urls = []
        for li in response.css("div#anony-nav div.anony-nav-links ul li"):
            url = li.css("a::attr(href)").get()
            urls.append(url)
            yield {
                "url": response.url
            }
        for url in urls:
            if url is not None:
                next_page = response.urljoin(url)
                yield scrapy.Request(next_page, callback=self.parse)

使用 response.follow 更加方便,可以直接传入 a 标签。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# -*- coding: utf-8 -*-

import scrapy

class DoubanIndexSpider(scrapy.Spider):
    name = "DoubanIndex"
    start_urls = ["https://www.douban.com/"]

    def parse(self, response):
        for li in response.css("div#anony-nav div.anony-nav-links ul li"):
            a = li.css("a")[0]
            yield response.follow(a, callback=self.parse)

上面这些内容足够我完成爬虫任务了,使用框架就是这么简单。

爬取豆瓣电影评论

明确目标

我需要爬豆瓣电影的评论,评论附带一个五星的评分。把 12 星看做负面情绪,3星看做中性, 45 星看做正面情绪。

我们先爬取所有电影的 ID, 然后通过分层加随机抽样的方法选出 100 部电影爬取该电影的评论。每一部的每一种情绪分别爬取 100 条。

保存方式:使用文本文件保存,三种情绪分为三个文件夹:每个文件夹一个 txt 文件保存一条评论,文件名为 id_rate.txt,id 为独一无二的数字。

Pipeline

上面要将爬取的评论放到不同的文件里,可以用 scrapy 的 pipeline 机制,scrapy 将爬取的 Item 送到 pipeline。在 pipelines.py 文件里定义 pipeline。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os

class DoubanMovieCommentPipeline(object):
    index_pos = 1
    index_nosup = 1
    index_neg = 1

    def create_dir(self, path):
        if not os.path.exists(path):
            os.makedirs(path)

    def process_item(self, item, spider):
        if item["short"] is None or len(item["short"]) <= 10:
            return item
        path = "doubancomment/" + item["type"]
        if item["type"] == "pos":
            filepath = path + "/" + str(self.index_pos) + "_" + item["rate"] + ".txt"
            self.index_pos += 1
        elif item["type"] == "nosup":
             filepath = path + "/" + str(self.index_nosup) + "_" + item["rate"] + ".txt"
             self.index_nosup += 1
        else:
            filepath = path + "/" + str(self.index_neg) + "_" + item["rate"] + ".txt"
            self.index_neg += 1
        self.create_dir(path)

        with open(filepath, "wb") as f:
            f.write(item["short"].encode("utf-8"))

        return item

process_item 负责处理传过来的 item。在 settings.py 里定义 pipeline 顺序。

1
ITEM_PIPELINES = {"douban.pipelines.DoubanMovieCommentPipeline": 300}

可以在里面定义多个 pipeline,item 会按照定义的顺序传到每个 pipeline 处理。

一些设置

settings.py 设值一些选项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 不遵守 robots.txt
ROBOTSTXT_OBEY = False

# 每一次下载页面时延迟,防止被封
DOWNLOAD_DELAY = 1.5

# 不使用cookie
COOKIES_ENABLED = False

# 设值 user-agent
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36"