前言
最近需要做一个通过神经网络(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"
|