scrapy杂记

2 篇文章 0 订阅
2 篇文章 0 订阅

scrapy杂记

一、前言

了解到爬虫技术大概有18个月了,这期间自己写过几个爬虫,也fork过几个流行的爬虫repo,包括bilibili-user、iquery、WechatSogou等,但一直没系统的写过爬虫,上一次心血来潮(17年10月),想要爬下关于英雄联盟的数据,主要想获得皮肤原画数据。
当时决定的目标网站是英雄联盟中文官网,数据准确且更新快。LOL数据库,但苦于该网页全篇使用js载入数据,无法直接读取,就退而求其次,改变目标网址多玩-英雄联盟-英雄数据库,技术使用混杂,主要利用requests+bs4完成爬取,部分内容使用scrapy抓取,逻辑结构混乱,代码可见spider-on-lol/v1.0/,功能基本实现,但不够美。七天之前,看到了这些代码,决定用scrapy重新实现功能,完成整个逻辑设计,这中间遇到了很多问题,从中得到了学习,完整代码可见spider-on-lol

二、爬虫设计全思路

  1. 目标网站:LOL数据库
  2. 爬取内容:
    • 英雄姓名
    • 英雄头像
    • 物品名称、id
    • 物品图片
    • 英雄皮肤原画
    • 英雄背景故事
  3. 技术:scrapy,splash,docker
  4. 难点
    • 图片获取
    • 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、如何有效的调试程序,还有待学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值