[Python] CrawlSpider框架爬取数码宝贝全图鉴

写在开头:本文运行爬虫的示例网站为 数码兽数据库http://digimons.net/digimon/chn.html

文章提及的细节参见我的另外三篇简文
MySQL本地类细节配置:
[Python] 爬取bioinfo帖子相关信息 (requests + openpyxl + pymysql)
Scrapy各组件详细配置:
[Python] 爬虫 Scrapy框架各组件详细设置
SQL命令基础:
[SQL] MySQL基础+Python交互

转载请注明:陈熹 chenx6542@foxmail.com (简书号:半为花间酒)
若公众号内转载请联系公众号:早起Python

需求分析

- 主页面分析

首先点击http://digimons.net/digimon/chn.html

进入中文检索页面

查看页面源码

有两点发现:

  1. 数据不是通过Ajax加载
  2. 获得全部数据不需要什么翻页逻辑,所有数码兽的后半url都在当前源码里。href中的格式是数码兽英文名/index.html

接下来分析几个数码兽详情页的url:
http://digimons.net/digimon/agumon_yuki_kizuna/index.html
http://digimons.net/digimon/herakle_kabuterimon/index.html
http://digimons.net/digimon/mugendramon/index.html
http://digimons.net/digimon/king_etemon/index.html

根据这个情况有一种思路:利用正则或者其他(超)文本解析工具获取源码中的所有href,然后利用urllib.parse.urljoin和父目录路径http://digimons.net/digimon/ 拼起来构成完整url,再访问详情页

但本文换了一种思路

许多爬虫的数据采集工作都是类似的,因此Scrapy提供了若干个 更高程度封装的通用爬虫类

可用以下命令查看:

# 查看scrapy提供的通用爬虫(Generic Spiders)
scrapy genspider -l

CrawlSpider 是通用爬虫里最常用的一个

通过一套规则引擎,它自动实现了页面链接的搜索跟进,解决了包含但不限于自动采集详情页、跟进分类/分页地址等问题。主要运行逻辑是深度优先

这个网站的设计非常简单,因此可以考虑用便捷的全网爬取框架。这个框架的前提是:无关url和需要url有明显差别,可以利用正则获取其他方式区别开

简而言之,可以想象给定爬虫一个一只url以后,爬虫会继续访问从这个url出发能访问到的新url,然后爬虫需要根据预设的语法判断这个url是不是所需的,如果是则先解析后延伸访问新url,如果不是则继续访问新url,假如没有新url该叉结束

文章中的外链比较多是wikipedia,url差别较大。通过对比和数码兽详情页的url,可以总结出所需url的格式:

http://digimons.net/digimon/.*/index.html

利用正则替换中间的英文名

- 详情页分析

爬取的需求如下:

基本一只数码兽所有的资料都会爬取下来,但需要注意不同的数码兽资料不一定完整,故写代码需要留意(举两个例子)

需求分析差不多了,可以着手写代码

代码实战

- 创建项目

# scrapy startproject <Project_name>
scrapy startproject Digimons

# scrapy genspider <spider_name> <domains>
scrapy genspider –t crawl digimons http://digimons.net/digimon/chn.html

- spiders.py

依次打开项目文件夹Digimons - Digimons - spiders,创建digimons.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import DigimonsItem


class DigimonsSpider(CrawlSpider):
    name = 'digimons'
    allowed_domains = ['digimons.net']
    start_urls = ['http://digimons.net/digimon/chn.html']

    # 爬虫的规则是重点,把先前分析的结果url放进allow
    # callback='parse_item',符合规则的url回调该函数
    # follow = False则爬到该页面后不继续拓宽深度
    rules = (
        Rule(LinkExtractor(allow=r'http://digimons.net/digimon/.*/index.html'), callback='parse_item', follow=False),
    )

    # 按需求逐个解析,有的不存在需要判断
    def parse_item(self, response):
        # 名字列表
        name_lst = response.xpath('//*[@id="main"]/article/h2[1]//text()').extract()
        name_lstn = [i.replace('/', '').strip() for i in name_lst if i.strip() != '']
        # 中文名
        Cname = name_lstn[0].replace(' ', '-')
        # 日文名
        Jname = name_lstn[1]
        # 英文名
        Ename = name_lstn[2]
        # 等级
        digit_grade = response.xpath("//article/div[@class='data'][1]/table/tr[1]/td/text()").extract()
        digit_grade = '-' if digit_grade == [] else ''.join(digit_grade)
        # 类型
        digit_type = response.xpath("//article/div[@class='data'][1]/table/tr[2]/td/text()").extract()
        digit_type = '-' if digit_type == [] else ''.join(digit_type)
        # 属性
        digit_attribute = response.xpath("//article/div[@class='data'][1]/table/tr[3]/td/text()").extract()
        digit_attribute = '-' if digit_attribute == [] else ''.join(digit_attribute)
        # 所属
        belongs = response.xpath("//article/div[@class='data'][1]/table/tr[4]/td/text()").extract()
        belongs = '-' if belongs == [] else ''.join(belongs)
        # 适应领域
        adaptation_field = response.xpath("//article/div[@class='data'][1]/table/tr[5]/td/text()").extract()
        adaptation_field = '-' if adaptation_field == [] else ''.join(adaptation_field)
        # 首次登场
        debut = response.xpath("//article/div[@class='data'][1]/table/tr[6]/td/text()").extract()
        debut = '-' if debut == [] else ''.join(debut)
        # 名字来源
        name_source = response.xpath("//article/div[@class='data'][1]/table/tr[7]/td/text()").extract()
        name_source = '-' if name_source == [] else '/'.join(name_source).strip('/')
        # 必杀技
        nirvana = response.xpath("//article/div[@class='data'][2]/table/tr/td[1]/text()").extract()
        nirvana = '-' if nirvana == [] else '/'.join(nirvana).strip('/')
        # 介绍资料
        info_lst = response.xpath("//*[@id='cn']/p/text()").extract()
        info = ''.join([i.replace('/', '').strip() for i in info_lst if i.strip() != ''])
        # 图片url
        img_url = response.xpath('//*[@id="main"]/article/div[1]/a/img/@src').extract()
        img_url = response.url[:-10] + img_url[0] if img_url != [] else '-'
        
        # 个人习惯简单输出
        print(Cname, Jname, Ename)

        # 如果要持久化存储转向items
        item = DigimonsItem()
        item['Cname'] = Cname
        item['Jname'] = Jname
        item['Ename'] = Ename
        item['digit_grade'] = digit_grade
        item['digit_type'] = digit_type
        item['digit_attribute'] = digit_attribute
        item['belongs'] = belongs
        item['adaptation_field'] = adaptation_field
        item['debut'] = debut
        item['name_source'] = name_source
        item['nirvana'] = nirvana
        item['info'] = info
        item['img_url'] = img_url
        yield item

- items.py

关于MySQL存储的细节可以参考我的另一篇文章:
[Python] 爬取生信坑论坛 bioinfo帖子相关信息 (requests + openpyxl + pymysql)

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy

# 和spiders.py中的传入对应
class DigimonsItem(scrapy.Item):
    Cname = scrapy.Field()
    Jname = scrapy.Field()
    Ename = scrapy.Field()
    digit_grade = scrapy.Field()
    digit_type = scrapy.Field()
    digit_attribute = scrapy.Field()
    belongs = scrapy.Field()
    adaptation_field = scrapy.Field()
    debut = scrapy.Field()
    name_source = scrapy.Field()
    nirvana = scrapy.Field()
    info = scrapy.Field()
    img_url = scrapy.Field()

    # 注释部分是sql语法,需要在命令行运行
    def get_insert_sql_and_data(self):
    # CREATE TABLE digimons(
    # id int not null auto_increment primary key,
    # Chinese_name text, Japanese_name text, English_name text,
    # digit_grade text, digit_type text, digit_attribute text,
    # belongs text, adaptation_field text, debut text, name_source text,
    # narvana text, info text)ENGINE=INNODB DEFAULT CHARSET=UTF8mb4;
        insert_sql = 'insert into digimons(Chinese_name,Japanese_name,English_name,digit_grade,digit_type,' \
                     'digit_attribute,belongs,adaptation_field,debut,name_source,nirvana,info,img_url)' \
                     'values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
        data = (self['Cname'],self['Jname'],self['Ename'],self['digit_grade'],self['digit_type'],
                self['digit_attribute'],self['belongs'],self['adaptation_field'],self['debut'],
                self['name_source'],self['nirvana'],self['info'],self['img_url'])
        return (insert_sql, data)

- pipelines.py

借助items.py和Mysqlhelper完成存储

# -*- coding: utf-8 -*-
from mysqlhelper import Mysqlhelper
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


class DigimonsPipeline(object):
    def __init__(self):
        self.mysqlhelper = Mysqlhelper()

    def process_item(self, item, spider):
        if 'get_insert_sql_and_data' in dir(item):
            (insert_sql, data) = item.get_insert_sql_and_data()
            self.mysqlhelper.execute_sql(insert_sql, data)
        return item

- settings.py

没有特别修改,下面的代码默认是注释状态,需要打开

ITEM_PIPELINES = {
   'Digimons.pipelines.DigimonsPipeline': 300,
}

- extends.py

自定义拓展这部分内容不看也可以,我实现的功能就是在爬虫运行结束的时候自动给微信推送消息(借助喵提醒

喵提醒需要申请账号获得自己的id,官方已经给了一个API可以包装在本地

from urllib import request, parse
import json

class Message(object):
    def __init__(self,text):
        self.text = text
    def push(self):
        # 重要,在id中填写自己绑定的id
        page = request.urlopen("http://miaotixing.com/trigger?" + parse.urlencode({"id": "xxxxxx", "text": self.text, "type": "json"}))
        result = page.read()
        jsonObj = json.loads(result)
        if (jsonObj["code"] == 0):
            print("\nReminder message was sent successfully")
        else:
            print("\nReminder message failed to be sent,wrong code:" + str(jsonObj["code"]) + ",describe:" + jsonObj["msg"])

写在另外的py文件里命名为message.py

接下来写extends.py,需要自己新建,位置和pipelines.py,items.py同级

from scrapy import signals
from message import Message


class MyExtension(object):
    def __init__(self, value):
        self.value = value

    @classmethod
    def from_crawler(cls, crawler):
        val = crawler.settings.getint('MMMM')
        ext = cls(val)

        crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
        crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)

        return ext

    def spider_opened(self, spider):
        print('spider running')

    def spider_closed(self, spider):
        # 推送的消息可以自定义,可以再给出获取了多少条数据
        text = 'DigimonsSpider运行结束'
        message = Message(text)
        message.push()
        print('spider closed')

重要的是如果添加了自定义拓展,需要在settings中也打开
默认是:

# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

需要修改并打开:

# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
EXTENSIONS = {
   'Digimons.extends.MyExtension': 500,
}

- running.py

最后就是运行这个项目了,也可以直接在命令行中cd到项目位置后运行

scrapy crawl digimons

个人比较喜欢在py文件中运行,新建一个running.py(和items.py同级目录)

from scrapy.cmdline import execute

execute('scrapy crawl digimons'.split())

最后运行runnings.py这个项目就启动了

项目运行

运行中如图:

运行完成共获得1179条数据,进入Navicat(MySQL等数据库的图形交互界面):

数据非常好的储存了,可以看到不是所有的数码兽都有所属阵营

可以做一些简单的查询,比如七大魔王阵营里都有谁:

查询结果对比确实只有7只,重复出现的都是形态变化或者变体:

对其中重复的一只数码兽再次查询,的确简介都不同

如果觉得用MySQL查看不方便,可以在Navicat中转出成EXCEL

在命令行中导出详情参见:[SQL] MySQL基础+Python交互


爬取的数据EXCEL形式供大家下载:
https://pan.baidu.com/s/1oKFsw3at4cF5p4WpW7ZRcA
提取码:1wvs

写在最后
数码兽的资料里数字信息较少,可供挖掘和造模的信息有限
后续爬取口袋妖怪小精灵们的数据就有意思多了 : )

关于数据分析的部分以后会涉及,欢迎继续关注

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容