一. 环境搭建

在安装 scrapy 之前,先安装 scrapy 的辅助模块,lxml,twisted,pyOpenSSL 等,lxml 用于解析 XML 和 HTML,twisted 是一个异步的网络框架,pyOpenSSL 用于处理网络级的安全需求,直接通过 pip 进行安装即可,安装 twisted 可能会连接超时,可以通过 whl 文件进行安装,我这边提供 python 3.6 下 twisted 的 whl 文件,twisted-17.9.0-cp36 ,提取码:169a。下载完 whl 文件,通过命令行进入下载 whl 的文件夹,然后命令行 pip install Twisted-17.9.0-cp36-cp36m-win_amd64.whl 即可。做好准备工作后,就可以通过 pip 一路畅通的安装 scrapy 啦~ 否则会遇到一些不可描述的问题~

pip 安装 scrapy

二. 创建 Scrapy 项目

先通过命令行切换到要创建项目的文件夹下,然后输入命令行 scrapy startproject first_spider[项目名] 看到这是不是发现和 django 的创建方式差不多~ 创建完项目,就可以创建具体的爬虫啦,进入项目文件,输入命令行 scrapy genspider blog[爬虫名] blog.scrapinghub.com/[爬取的网址] 这边的网址我选择了官网首页提供的 ‘The Scrapinghub Blog’ 网址

scrapy 创建爬虫

接着打开项目下的 ‘spiders’ 文件夹,里面就多了一个 ‘blog.py’ 文件,里面就是 scrapy 自动生成的爬虫内容啦

其中 name 属性就是爬虫名,allowed_domains 属性是爬取的域名,start_urls 属性是爬取的网址,parse 方法是用来写爬虫爬取的逻辑的。需要注意的是:

  1. 爬虫必须继承 scrapy.Spider 类,实现 parse 方法,这是一个爬虫的基础类
  2. 爬虫名在整个项目中都是不可重复的,再定义一个爬虫文件,就不可再用 ‘blog’ 作为爬虫名了
  3. 其中 name 属性和 start_urls 属性是必须要有的,allowed_domains 可以省略,start_urls 还可以放到 start_requests 方法中,这个方法后面会提到
  4. 检查下网址,可能会多 ‘/‘ 或者别的什么问题

接着我们先来编写一个简单的爬虫(这边我借助 pycharm 来写,相对比较友好),然后一步步分析实现的过程,再编写前,先解决下编码的问题,打开 settings.py 文件加入如下代码,防止爬取的汉字会乱码

# 解决编码 unicode 问题
FEED_EXPORT_ENCODING = 'utf-8'

然后就开始写第一个爬虫文件,爬取 blog 的标题,发表日期,作者,评论数量,以及概要,首先通过 css 方式

class BlogSpider(scrapy.Spider):
name = 'blog'
allowed_domains = ['blog.scrapinghub.com/']
start_urls = ['https://blog.scrapinghub.com//']

def parse(self, response):
for article in response.css('article'):
yield {
# 爬取标题
'title': article.css('h2.entry-title a::text').extract_first(),
# 爬取发表日期
'pub_date': article.css('h5.entry-date a time::text').extract_first(),
# 爬取作者
'author': article.css('span.byline span a::text').extract_first(),
# 爬取评论数量
'comment_num': article.css('a.comments-link::text').extract_first(),
# 爬取概要
'summary': article.css('div.entry-summary p::text').extract_first()
}

或者通过 xpath 爬取(结果是一样一样的~)

class BlogSpider(scrapy.Spider):
name = 'blog'
allowed_domains = ['blog.scrapinghub.com/']
start_urls = ['http://blog.scrapinghub.com/']

def parse(self, response):
for article in response.xpath('//main[@id="main"]/article'):
yield {
'title': article
.xpath('.//h2[@class="entry-title"]/a/text()')
.extract_first(),
'pub_date': article
.xpath('.//h5[@class="entry-date"]/a/time/text()')
.extract_first(),
'author': article
.xpath('.//span[@class="byline"]/span/a/text()')
.extract_first(),
'comment_num': article
.xpath('.//a[@class="comments-link"]/text()')
.extract_first(),
'summary': article
.xpath('.//div[@class="entry-summary"]/p/text()')
.extract_first()
}

写完爬虫文件,先检查是否有语句错误,scrapy check blog,如果没有错误就可以通过命令行 scrapy crawl blog -o blog.json 来执行爬虫文件,其中 -o blog.json 是将爬取的信息写入到 blog.json 文件中,除了 .json 文件还可以有 .jl,.csv 文件等等,.json 文件不能够在末尾继续添加,而 .jl,.csv 文件则可以,爬取过程大概如下(复制的打印信息,删除了部分,否则太多了……),这里提下 scrapy 的命令行,还是很适合调试的,这边附上官网网址Scrapy Commands

scrapy 爬取数据

三. xpath, css 选择器

xpath 和 css 具体的内容我们不多介绍,这边直接给我学习的网址,然后通过官网提供的例子,用两种方式实现并解释下常用的几个功能 xpath tutorialcss 选择器 在官方的文档中,有这么个例子 Example website 打开网址后,通过 F12 打开网页的开发者模式功能,会高亮显示选中的内容,接着打开终端,输入命令行 scrapy shell https://doc.scrapy.org/en/latest/_static/selectors-sample1.html,scrapy 会返回一个 response 的 shell 变量,并绑定一个 selector 选择器。接着一段代码来袭,跟着一起敲吧~

# 获取 response 下 title 文本
# xpath 通过 '//element' 表示任意位置下的某个节点,这里表示 title 节点;css 直接通过 element 表示任意节点
# 不管 response.xpath(...) 还是 response.css(...) 都会返回一个 Selector,通过 extract() 方法进行内容提取,返回一个内容的列表
# 使用 extract_first() 而不使用 extract()[0] 是为了防止未找到该节点,如果不存在 extract() 会直接报错,而 extract_first() 会返回 null,或者也可以通过 default 属性定义返回空的默认值
response.xpath('//title/text()').extract_first()
response.css('title::text').extract_first()

# 获取链接属性
# xpath 通过 '@xxx' 获取某个节点的属性值,而 css 通过 attr(xxx) 来获取
response.xpath('//a/@href').extract()
response.css('a::attr(href)').extract()

# 获取图片链接
response.xpath('//img/@src').extract()
response.css('img::attr(src)').extract()

# 通过指定某个属性值进行进一步的数据筛选
# 假设在该例子上增加一个标签 <div id="imgs">...</div>,我们需要获取该 <images> 标签内的内容
# xpath 通过 [@xx="xxx"] 来进一步删选,css 则不同,'#' 用来表示 id 属性,'.' 用来表示 class 属性,后面跟属性值即可
response.xpath('//div[@id="images"]').extract()
response.css('div#images').extract()

# 获取图片名
# xpath 通过 '/' 表示路径分割,css 通过 ' ' 表示后代选择器
response.xpath('//div[@id="images"]/a/text()').extract()
response.css('div#images a::text').extract()

# 获取 href 中包含 image 的所有图片名
# xpath 通过 contains 表示属性包含,css 通过 *=
response.xpath('//div/a[contains(@href, "image")]/text()').extract()
response.css('div a[href*=image]::text').extract()

附上添加后的 html 文件以及部分内容爬取的实现

介绍完 xpath 和 css 之后,之前的爬虫逻辑也就能个看懂了。接着,在前面的例子只做了当前页的内容爬取,实际还有很多也的内容可以爬取,但是我们不可能去把每页的 url 都去写出来,那样太麻烦了,这里去寻找一个突破口,就是“下一页”按钮,打开开发者模式功能,可以发现“下一页”按钮如下图

blog_next

那就可以很容易获取到下一页的 url,在 BlogSpider 文件中 parse 加入如下代码,可以根据喜好选择 xpath 或者 css 选择器

def parse(self, response):
# ....
# next_page = response.xpath('//div[@class="nav-links"]//div/a/@href').extract_first()
next_page = response.css('div.nav-links div a::attr(href)').extract_first()
if next_page is not None:
# 通过将响应 url 与可能的相对 URL 组合构造绝对 url
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)

添加完后重新运行爬虫文件 scrapy crawl blog -o blog_all.json 就可以继续爬取别的数据了。

四. 使用 Item

之前的爬虫,通过 yield 一个 dict 来实现,但是缺少结构性,容易打错字段,所以 scrapy 提供 Item 类来解决这个问题。打开项目下的 items.py,加入如下代码

import scrapy

class BlogItem(scrapy.Item):
title = scrapy.Field()
pub_date = scrapy.Field()
author = scrapy.Field()
comment_num = scrapy.Field()
summary = scrapy.Field()

是不是很像 django 的 Model,但是没有那么多不同的字段类型。接着对爬虫的代码进行部分修改。

def parse(self, response):
item = BlogItem()
for article in response.css('article'):
item['title'] = article.css('h2.entry-title a::text').extract_first()
item['pub_date'] = article.css('h5.entry-date a time::text').extract_first()
item['author'] = article.css('span.byline span a::text').extract_first()
item['comment_num'] = article.css('a.comments-link::text').extract_first()
item['summary'] = article.css('div.entry-summary p::text').extract_first()
yield item

next_page = response.css('div.nav-links div a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)

修改完后,重新运行爬虫(如果之前写入的是 json 文件,记得先删除),又会得到相同的结果。

五. 通过 Pipeline 将数据写入 MySql

相信很多朋友都喜欢看电影,这边就通过爬取豆瓣 Top250 作为例子来实现写入 MySql 的功能(为什么要选取豆瓣呢,因为反爬做的简单,容易爬取啊~)豆瓣网址为:豆瓣 Top250,打开开发者模式,编写爬取的 Item

class MovieItem(scrapy.Item):
# 排名
rank = scrapy.Field()
# 图片链接
img_link = scrapy.Field()
# 影片名
chinese_title = scrapy.Field()
foreign_title = scrapy.Field()
other_title = scrapy.Field()
# 播放状态
playable = scrapy.Field()
# 评分
avg_rating = scrapy.Field()
# 评论数量
comment_num = scrapy.Field()
quote = scrapy.Field()
detail_link = scrapy.Field()

编写完 Item 后就可以来写爬虫的逻辑了,这边要注意下,需要将爬虫伪装成浏览器,否则是爬取不到数据的

class DoubanMovieSpider(scrapy.Spider):
name = 'douban_movie'
url = "https://movie.douban.com/top250"
# 伪装成浏览器
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36"
}

def start_requests(self):
yield scrapy.Request(url=self.url, headers=self.header, callback=self.parse)

def parse(self, response):
item = MovieItem()

# 获取影片信息列表
for movie in response.css('ol.grid_view li'):
item['rank'] = movie.css('div.pic em::text').extract_first()

try:
item['img_link'] = movie.css('div.pic a img::attr(src)').extract_first()
except:
item['img_link'] = ''

# 将 "'" 替换掉,否则写入数据库会出问题
try:
item['chinese_title'] = movie.css('div.info a span.title::text') \
.extract_first().strip() \
.replace(r"'", " ")
except Exception as e:
item['chinese_title'] = ''
print('get chinese title Error:', e)

# 防止空数据,将 "'" 替换掉,否则写入数据库会出问题
try:
item['foreign_title'] = movie.css('div.info a span.title::text') \
.extract()[1].strip().split('/')[-1] \
.strip().replace(r"'", " ")
except Exception as e:
item['foreign_title'] = ''
print('get foreign title Error:', e)

# 可能存在多个,发现由 "/" 进行分割,但是首位也是 "/",通过 "/" 分割字符串后再重新合成
ot = ''
delimiter = '/'
try:
for other in movie.css('div.info a span.other::text').extract_first(default='').strip().split('/'):
if other:
ot = ot + other.strip().replace(r"'", " ") + delimiter
except Exception as e:
print('get other title Error:', e)

# 去除最末尾的分隔符
item['other_title'] = ot.rstrip(delimiter)

item['playable'] = movie.css('div.info span.playable::text') \
.extract_first(default='[]').lstrip('[').rstrip(']')

item['avg_rating'] = movie.css('div.star span.rating_num::text').extract_first()

# re_first() 类似 extract_first(),不过 re_first() 通过正则进行数据匹配
item['comment_num'] = movie.css('div.star span::text').re_first(r'[0-9]+人评价')

# 防止空数据,将 "'" 替换掉,否则写入数据库会出问题
quote = movie.xpath('.//p[@class="quote"]/span/text()').extract_first()
if quote:
item['quote'] = quote.strip().replace(r"'", " ")
else:
item['quote'] = ''

item['detail_link'] = movie.css('div.pic a::attr(href)').extract_first()
yield item

# 获取下一页 url
next_page = response.css('span.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, headers=self.header, callback=self.parse)

如果需要通过 shell 命令行进行调试,添加头部信息方法如下 scrapy shell -s USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36" "https://movie.douban.com/top250"

写完逻辑后,可以运行爬虫查看是否正确获取到数据,如果获取的数据是乱码,那就是编码问题,在 settings.py 文件中加入编码配置

# settings.py

FEED_EXPORT_ENCODING = 'utf-8'

运行没问题后我们通过 Pipeline 将获取到的数据写入 MySql 了,自定义 Pipeline 需要继承 object ,实现 process_item(self, item, spider) 方法,Pipeline 还有好多方法,例如 open_spider(self, spider) 在爬虫打开的时候执行,close_spider(self, spider) 在爬取完数据时候执行等等,接着就来定义写数据的 Pipeline

import pymysql

class DoubanMoviePipeline(object):
def __init__(self):
# 连接数据库,需要先创建好数据库
self.db = pymysql.connect('localhost', user='root', password='123456',
db='douban_movie', use_unicode=True, charset="utf8")
# 获取操作的游标
self.cursor = self.db.cursor()

def open_spider(self, spider):
# 创建对应的表
create_sql = """
CREATE TABLE
IF NOT EXISTS movie (
id INT (20) NOT NULL PRIMARY KEY AUTO_INCREMENT,
rank INT (10) NOT NULL,
img_link CHAR (255),
chinese_title CHAR (255) NOT NULL,
foreign_title CHAR (255),
other_title CHAR (255),
playable CHAR (30),
avg_rating CHAR (10),
quote CHAR (255),
detail_link CHAR (255)
)
"""
self.cursor.execute(create_sql)

def process_item(self, item, spider):
rank = item['rank']
img_link = item['img_link']
chinese_title = item['chinese_title']
foreign_title = item['foreign_title']
other_title = item['other_title']
playable = item['playable']
avg_rating = item['avg_rating']
quote = item['quote']
detail_link = item['detail_link']

# 爬取的数据插入数据库
insert_sql = """
INSERT INTO movie (rank, img_link, chinese_title, foreign_title, other_title, playable, avg_rating, quote, detail_link)
VALUES ({},'{}', '{}', '{}', '{}', '{}', '{}', '{}', '{}')
""".format(rank, img_link, chinese_title, foreign_title,
other_title, playable, avg_rating, quote, detail_link)

try:
self.cursor.execute(insert_sql)
# 插入后需要提交,否则数据插入不了
self.db.commit()
except Exception as e:
# 如果出错了回滚操作
self.db.rollback()
return item

def close_spider(self, spider):
# 结束后关闭游标和数据库
self.cursor.close()
self.db.close()

最后将写好的 Pipeline 写到 settings.py 文件中,当打开爬虫的时候,会自动通过 pipeline 进行操作

# settings.py

ITEM_PIPELINES = {
'first_spider.pipelines.DoubanMoviePipeline': 300,
}

展示下结果图:

结果1

结果2

六. 通过 Pipeline 下载图片

在接触 scrapy 之前,下载图片,一般都是通过 urlliburlretrieve 方法进行下载,例如我们要下载一张图片,网址为 “https://upload-images.jianshu.io/upload_images/2888797-8f49bb4c2965249e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240",我们一般都会这样下载(需要先创建好文件夹才可以下载)

import os
import urllib

image_url = 'https://upload-images.jianshu.io/upload_images/2888797-8f49bb4c2965249e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'

image_path = os.path.join('C:\\test', '1.png')

urllib.request.urlretrieve(image_url, image_path)

这样就可以将图片下载下来了,但是 scrapy 自带图片下载功能,只要通过 ImagesPipeline 即可实现(不需要手动创建文件夹的喔~)。按照惯例,先创建 item

class MovieImageItem(scrapy.Item):
# 图片 urls, 存放所有的 url 的列表
image_urls = scrapy.Field()
# 文件路径,存放所有的图片文件路径
image_paths = scrapy.Field()

然后来编辑 Spider 的逻辑

class DoubanMovieImageSpider(scrapy.Spider):
name = 'movie_image'
url = "https://movie.douban.com/top250"
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36"
}

def start_requests(self):
yield scrapy.Request(url=self.url, headers=self.header, callback=self.parse)

def parse(self, response):
item = MovieImageItem()
# 这里存放所有的 url,直接通过 extract() 方法获取列表即可
item['image_urls'] = response.css('ol.grid_view li div.pic img::attr(src)').extract()
yield item

# 获取下一页 url
next_page = response.css('span.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, headers=self.header, callback=self.parse)

定义完爬取逻辑后,就需要来定义 Pipeline 了,在这之前,我们先安装下 Pillow 模块 pip install pillow,下载图片的 Pipeline 通过继承 scrapy 自带的 ImagesPipeline 类,然后实现 get_media_requests(self, item, info)item_completed(self, results, item, info) 方法即可

class DoubanMovieImagePipeline(ImagesPipeline):
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36"
}

def get_media_requests(self, item, info):
# 将存放在 item 里面的图片链接传给 Request
for url in item['image_urls']:
# 如果在爬取的时候加入了头部,这边需要将头部也加入
yield scrapy.Request(url=url, headers=self.header)

def item_completed(self, results, item, info):
# 爬完一个 item 后会调用这个方法,通过 results 中的参数,对爬取的结果进行判断
# results 是一个元组,包含一个下载状态值和一个存放信息的 dict,大概如下:
# (True, {'url': 'https://img3.doubanio.com/view/photo/s_ratio_poster/public/p804938713.jpg',
# 'path': 'full/bd7353dc69401a1581cf1c8abca75c5395d9965d.jpg', 'checksum': '60d0d761584f35f2ea9d86cce79b7d1b'})
# True 表示下载图片成功,False 表示失败,dict 中的 url 是图片的下载 url,path 是保存的地址,checksum 是一个 MD5 hash 值
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem("Item contains no images")
# 将图片的保存地址写入 item 中
item['image_paths'] = image_paths
return item

然后和写入数据库的操作一样,把定义好的 Pipeline 设置到 settings.py 文件中,再加入一些必要的设置

# settings.py

ITEM_PIPELINES = {
'first_spider.pipelines.DoubanMovieImagePipeline': 300,
}

# 存放的文件目录,图片下载完后会放到目录下的 full 文件夹中,如果定义了缩略图,则缩略图会放在 thumbs 文件夹中
IMAGES_STORE = 'C:\\Users\\kuky\\Desktop\\MovieImages'

# 失效时间
IMAGES_EXPIRES = 90

# 设置缩略图
IMAGES_THUMBS = {
# 定义不同的缩略图文件大小
'small-thumbs': (50, 50),
'big-thumbs': (250, 250),
}

然后通过运行爬虫,就可以看到爬取下来的图片啦,如果在爬取过程中遇到如下问题 [scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1454261925.jpg> ,打开 settings.py 文件,然后将里面的 ROBOTSTXT_OBEY 属性设置为 True 即可,最后展示下爬取的结果
爬取图片

通过 scrapy 可以很简单的爬取图片文件,至于妹子图什么的,你懂得(项目中有,项目中有,项目中有)

七. Spider 调试

Spider 除了用 scrapy 自带的命令行调试外,如果你用的是 pyCharm,这里再推荐一种,首先在项目下创建一个 debug.py 文件,然后加入如下代码

from scrapy import cmdline

# 爬虫名
name = 'movie_image'
cmd = 'scrapy crawl {}'.format(name)
cmdline.execute(cmd.split())

然后根据要调试的 Spider 修改 name 的值,在对应的 Spider 中打断点,然后右键 debug.py 选择 “ Debug‘debug’ ” 就可以进行断点调试了。

Scrapy 入门的知识大概就这么多,要掌握还是得多练习多踩坑才行。最后附上 scrapy 官方文档链接:Scrapy 官方文档 和项目链接:项目地址