scrapy杂记
一、前言
了解到爬虫技术大概有18个月了,这期间自己写过几个爬虫,也fork过几个流行的爬虫repo,包括bilibili-user、iquery、WechatSogou等,但一直没系统的写过爬虫,上一次心血来潮(17年10月),想要爬下关于英雄联盟的数据,主要想获得皮肤原画数据。
当时决定的目标网站是英雄联盟中文官网,数据准确且更新快。LOL数据库,但苦于该网页全篇使用js载入数据,无法直接读取,就退而求其次,改变目标网址多玩-英雄联盟-英雄数据库,技术使用混杂,主要利用requests+bs4完成爬取,部分内容使用scrapy抓取,逻辑结构混乱,代码可见spider-on-lol/v1.0/,功能基本实现,但不够美。七天之前,看到了这些代码,决定用scrapy重新实现功能,完成整个逻辑设计,这中间遇到了很多问题,从中得到了学习,完整代码可见spider-on-lol。
二、爬虫设计全思路
- 目标网站:LOL数据库
- 爬取内容:
- 英雄姓名
- 英雄头像
- 物品名称、id
- 物品图片
- 英雄皮肤原画
- 英雄背景故事
- 技术:scrapy,splash,docker
- 难点
- 图片获取
- js页面数据的获取
- 中文编码
三、环境搭建
工欲善其事,必先利其器。
1、 开发语言:python v3.6.5
2、开发语言环境:anaconda v1.6.9 (非必须,但这是一个好习惯)
3、docker安装
deepin下安装docker
其他系统安装docker
4、splash
安装方法
5、一些第三方库:
# scrapy库
conda install Scrapy
# scrapy_splash库
conda install scrapy_splash
四、什么是Scrapy?什么是Splash?
1、Scrapy
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
简单的说,使用scrapy爬虫,只需要简单的代码可以完成复杂的爬虫工作。
其运行流程如下图所示:
来源:
通过这张图可以了解scrapy的执行过程,而理解这个过程对后面的编写以及排错有重要作用。
推荐几个几个关于scrapy的网站:
- Scrapy1.0中文文档(重要)
- Scrapy1.5官方文档
- Scrapy0.2.2中文文档
- Scrapy实战(重要)
- scrapy的教程要对应版本,新版本加了不少特性,新手容易在版本特性上出错
2、Splash
splash是一个JavaScript渲染服务,它可以渲染带有js的网页,并返还一个完整的页面。
它的优势在于:
- splash作为js渲染服务,是基于Twisted和QT开发的轻量浏览器引擎,并且提供直接的http api。快速、轻量的特点使其容易进行分布式开发。
- splash和scrapy融合,两种互相兼容彼此的特点,抓取效率较好。
splash基于docker技术,有多种功能,在本文中只用到了最基础的渲染js功能。
更多关于splash的资料,请参照:Splash官方文档
五、第一类爬虫
第一类爬虫,也是第一个爬虫,目的是爬取英雄名字及称号,看上去好像没那么复杂。
1、定义item
打开items,添加如下代码:
import scrapy
class LOLHeroNameSpiderItem(scrapy.Item):
'''继承自scrapy.Item,在spiders/lol-hero-name-spidder.py中使用'''
# name英雄名称,e_name英雄英文名称(即英雄id),title英雄称号
hero_name = scrapy.Field()
hero_e_name = scrapy.Field()
hero_title = scrapy.Field()
定义三个字段,作为后续获得的数据的容器,以便于进入pipeline操作。
2、编写pipeline
pipelines是对spider爬取到的item进行处理的过程,在这个爬虫中,我们需要对获得的数据进行转码并储存在json文件中。记得将LOLHeroNamePipeline添加到配置文件settings.py中。
import codecs
import json
class LOLHeroNamePipeline(object):
def __init__(self):
# codecs,python编解码器,以字节写方式打开一个文件,方便后面的转换中文
# 关于codecs可以查看https://docs.python.org/3/library/codecs.html
self.file = codecs.open('lol-hero-name.json', 'wb', encoding='utf-8')
def process_item(self, item, spider):
line = json.dumps(dict(item)) + '\n' # line是str类型
# 向codecs("wb")打开的文件写入line经过utf-8编码后再经过unicode编码,最后存储的字符是中文
self.file.write(line.encode('utf-8').decode('unicode_escape'))
return item
3、编写spider
from scrapy.http import Request
from scrapy.spiders import Spider
from scrapy.selector import Selector
from scrapy_splash import SplashRequest # 引入splash组件,获取js数据
from lolSpider.items import LOLHeroNameSpiderItem
class LOLHeroNameSpider(Spider):
'''
第一个爬虫类,爬取英雄联盟所有英雄名字及称号
'''
name = "LOL-Hero-Name" # 唯一的名字,命令行执行的时候使用
# 同一个项目下,多个spider对应不同的pipeline,需要在spider中修改配置,setting.py中不做pipeline配置
custom_settings = {
'ITEM_PIPELINES':{
'lolSpider.pipelines.LOLHeroNamePipeline': 300,
},
}
allowed_domains = ["http://lol.qq.com/web201310/info-heros.shtml"] # 允许爬取的网站域
start_urls = ["http://lol.qq.com/web201310/info-heros.shtml",] # 开始爬取的第一条url
def start_requests(self):
for url in self.start_urls:
# 处理start_urls,以splashRequest的方法执行parse方法
yield SplashRequest(url=url, callback=self.parse, args={'wait': 0.5},endpoint='render.html',)
# parse方法用于处理数据,传入一个response对象
def parse(self, response):
# sites是一个可迭代对象,它代表了从response对象中获取某些数据而形成的可迭代对象
# 使用xpath方法找到特定的元素组成的数据,在这里代表“找到一个id为jSearchHeroDiv的元素它的所有li元素,很多个li元素,所以成为一个可迭代对象
# XPath是一门在XML、HTML文档中查找信息的语言,它可以查找元素。教程可见http://www.w3school.com.cn/xpath/index.asp
# 之前的xpath很长,但不需要对着网页源代码查找,请见http://www.locoy.com/Public/guide/V9/HTML/XPath%E6%8F%90%E5%8F%96.html
sites = response.xpath("//*[@id=\"jSearchHeroDiv\"]/li")
items = []
# 对sites的每一个li元素进行循环
for site in sites:
item = LOLHeroNameSpiderItem()
item['hero_name'] = site.xpath("a/@title").extract_first().split(" ")[1]
item['hero_title'] = site.xpath("a/@title").extract_first().split(" ")[0]
item['hero_e_name'] = site.xpath("a/@href").extract_first()[21:]
yield {'name':item['hero_name'],'title':item['hero_title'],"e_name":item['hero_e_name']}
items.append(item)
# return 也可以完成yield的任务(在这个文件内),两者都可以输出到json文件(保留一个就好hh)
return items
4、解析爬取过程
一个spider文件中定义一个爬虫类,继承自scrapy.spiders,必须拥有类属性name和start_urls,前者是命令行启动爬虫的关键,后者是scrapy调度器开始的条件。
注意这里有个custom_settings,这个属性代表着爬虫自带的设置条件,在执行爬虫时,这里的属性值覆盖掉scrapy工程的默认设置,在多个爬虫共处一个工程的环境下,custom_settings非常重要。
在start_requests()方法中,返回一个SplashRequest()方法,方法传递参数url,parse()。
之后在parse()方法中,进行数据的提取,一般是通过选择器先获得一个可迭代对象,而选择器常用的有css,xpath两种,个人偏向于xpath,关于xpath的学习请见xpath学习。获得一个可迭代对象后,用for进行迭代,迭代过程中进行操作,此时就可以创建item对象,并作为容器装入取得的数据,再经过pipeline的操作,在这个爬虫中,最终的输出是一个特定格式的json文件。
同样工作流程的,还有LOLItemNameSpider,LOLHeroSkinSpider。
记得启动splash服务。
这部分爬虫没什么坑,理解了scrapy的原理就很明了了。
六、第二类爬虫
在这里,我们准备爬取图片,scrapy对图片的支持相当完善,在最新的版本(1.5)中,已经支持:
- 避免重新下载最近已经下载过的数据
- 将所有下载的图片转换成通用的格式(JPG)和模式(RGB)
- 缩略图生成
- 检测图像的宽/高,确保它们满足最小限制
这里我们以爬取并下载LOL英雄皮肤原画为例,简要说下,如何利用pipeline自动下载。
1、定义item
class LOLHeroSkinSpiderItem(scrapy.Item):
# image_urls图片链接,image_names图片名称,image_id图片保存名称
image_urls = scrapy.Field()
image_names = scrapy.Field()
image_id = scrapy.Field()
item的定义和第一类爬虫类似,其实这是因为python弱类型,作为容器,不分类型,具体涉及到的类型操作由人工完成。
2、编写pipeline
import json
import codecs
from scrapy import log
from scrapy import Request
from scrapy.http import Request
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline
class LOLHeroSkinInfoPipeline(object):
'''
pipelines是对spider爬取到的item进行处理的过程
关于scrapy的核心架构可参见https://blog.csdn.net/u012150179/article/details/34441655
'''
def __init__(self):
# codecs,python编解码器,以字节写方式打开一个文件,方便后面的转换中文
# 关于codecs可以查看https://docs.python.org/3/library/codecs.html
self.file = codecs.open('lol-skin-info.json', 'wb', encoding='utf-8')
def process_item(self, item, spider):
line = json.dumps(dict(item)) + '\n' # line是str类型
# 向codecs("wb")打开的文件写入line经过utf-8编码后再经过unicode编码,最后存储的字符是中文
self.file.write(line.encode('utf-8').decode('unicode_escape'))
return item
# 对应LOL-Hero-Skin
class LOLHeroSkinPipeline(ImagesPipeline):
# file_path()方法将保存的图片文件重命名,返回str
def file_path(self, request, response=None, info=None):
image_guid = request.url.split('/')[-1]
return 'full/%s' % (image_guid)
# 参加https://www.jianshu.com/p/c12d2ac7d55f
def get_media_requests(self, item, info):
for image_url in item['image_urls']:
yield Request(image_url)
def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem("Item contains no images")
return item
这里的pipeline就比第一类爬虫复杂点。
- LOLHeroSkinInfoPipeline类,没什么好说的,依旧保存下载文件的信息并输出到json。
- LOLHeroSkinPipeline类,这个类继承自ImagesPipeline,可以通过重写这个父类的几个方法,简单的实现图片下载的任务。
- scrapy保存图片是,默认以图片的sha1 hash值进行保存,通过file_path()方法可以制定待保存图片的名字,详情可见ImagesPipeline下载图片。
- 记得将新创建的两个类,添加到配置文件settings.py中,同时对于图片爬取,要在配置文件中设置属性IMAGES_STORE制定图片保存位置。
3、编写spider
# -*- coding:utf-8 -*-
import sys
import scrapy
from scrapy import Request
from scrapy.spiders import Spider
from scrapy_splash import SplashRequest
from lolSpider.items import LOLHeroSkinSpiderItem
class LOLHeroSkinSpider(scrapy.Spider):
'''第五个爬虫类,爬取所有英雄皮肤'''
name = 'LOL-Hero-Skin'
custom_settings = {
'ITEM_PIPELINES':{
'lolSpider.pipelines.LOLHeroSkinPipeline': 300,
'lolSpider.pipelines.LOLHeroSkinInfoPipeline':301,
},
'IMAGES_STORE':'../lolSpider/lolSpider/img/hero_skin_img',
}
# 从英雄列表中获取英文名,构造start_urls
start_urls = []
base_url = "http://lol.qq.com/web201310/info-defail.shtml?id="
with open('./lol-hero-name.json','r') as f:
for line in f.readlines():
i = line.find("e_name") +10
suf_url = line[i:-3]
start_urls.append(base_url+suf_url)
# http header
default_headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, sdch, br',
'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Host': 'http://lol.qq.com/web201310/info-defail.shtml?id=Aatrox',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
}
def start_requests(self):
for url in self.start_urls:
yield SplashRequest(url=url, callback=self.parse, args={'wait': 0.5},
endpoint='render.html'
)
def parse(self,response):
text = response.xpath("//*[@id=\"skinNAV\"]/li")
for i in text:
item = LOLHeroSkinSpiderItem()
# 获得图片id,为了进一步的重命名
item["image_id"] = i.xpath("a/img/@src").extract_first().split("/")[-1].replace("small","big")
# 动态页面,无法抓取到bigxxx.jpg,但可以通过replace构造完整url
image_urls = i.xpath("a/img/@src").extract_first()
image_urls = image_urls.replace("small","big")
temp = []
temp.append(image_urls)
item["image_urls"] = temp
# 皮肤的中文名称,若为“默认”,则添加英雄名称
image_names = i.xpath("a/@title").extract_first()
if image_names == "默认皮肤":
item['image_names'] = image_names + " " + response.xpath("//*[@id=\"DATAnametitle\"]/text()").extract_first().split(' ')[-1]
else:
item['image_names'] = image_names
yield item
4、解析爬取过程
- 一样的创建一个爬虫类,继承自scrapy.spiders,设置name属性。
- 注意这里的custom_settings多了一个IMAGES_STORE定义储存位置。
- 由于要渲染多次js画面,可以有两种方法。
- 第一种方法:利用splash函数,手动翻页。
- 第二种方法:将所有页面放入start_urls中,逐个的渲染,这里选择第二种方法。
- 读取第一个爬虫获得的lol-hero-name.json数据,构造140个url,添加到start_urls中。
- 在parse()方法中,遇到了一个大问题,查找了很多资料最终才得以解决。
- 错误提示Missing scheme in request url: h,这是因为scrapy1.0中,extract_first()返回的是byte,而scrapy pipeline准备下载需要的是列表,并且取列表第一个元素。例如:a = “http://demo.lightl.fun” ,b = [“http://demo.lightl.fun“] 。若是前者a[0]则为’h’,若为后者b[0]则是完整的网址。
- 简单的解决方法,若直接从xpath或css选择器中获取数据,而不用进行二次处理,直接将extract_first()替换为extract()返回列表。
- 复杂的解决方法,若从选择器中获取数据后进行二次处理,可以新建一个列表,使其append处理后的数据再交给item,也是返回列表。
七、关于设置settings.py
如果整个爬虫只有一个spider文件,那么可以直接在settings.py中修改配置,否则,使用custom_settings进行设置。
常见的设置项如下:
- splash相关设置
SPLASH_URL = 'http://172.17.0.2:8050/' # splash在docker下的url,由机器而异,查找ip可参见https://mozillazg.com/2016/01/docker-get-containers-ip-address.html
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashCookiesMiddleware': 723,
'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
# 爬虫中间件
SPIDER_MIDDLEWARES = {
'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter' # 去重过滤器(必须)
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage' # 使用http缓存
- 是否遵守ROBOT协议,默认是遵守
ROBOTSTXT_OBEY = True
- 下载延迟,防止被禁ip
DOWNLOAD_DELAY = 1
- 机器做了代理,可以这么设置让scrapy不走代理
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}
- 禁止cookies
COOKIES_ENABLED = False
八、scrapy防ban策略
主要参考如何防止被ban之策略大集合
- 设置download_delay
- 禁止cookies
- 设置header,必要的话可以多个header随机使用
- 使用ip池
- 分布式爬取
九、反思
1、代码是人写的,工具是人写的,现有的框架无法满足所有人,而能够修改框架满足需求很重要。
2、多用google、stackoverflow。
3、静下心,不能急于求成,如果一开始就认真的理解scrapy工作流程,事会少一半。
4、以官方英文文档为主,中文文档以辅,也不是说中文化不好,只是有时候更新慢,有些概念混乱。
5、如何有效的调试程序,还有待学习。