使用pyspider抓取起点中文网小说数据

96
某杰
2017.02.22 11:51* 字数 1245

简介

pyspider是国人开发的相当好用的爬虫框架。虽然网上教程不是很多,但是文档详细,操作简单,非常适合用来做爬虫练习或者实现一些抓取数据的需求。

本文就以抓取起点中文小说网的小说作品基础信息作为目标,讲解如何使用pyspider框架采集数据。

关于为何要选择起点作为目标,其一、笔者作为网文爱好者,也想收集起点小说作品信息,找些热门小说看;其二、起点作为比较成熟的小说网站,再反爬虫方面应该有对应策略,刚好练习一下爬虫怎么规避这些策略。

在阅读本文之前,建议先看一下文档及框架作者本人写的中文教程
pyspider 爬虫教程(一):HTML 和 CSS 选择器
pyspider 爬虫教程(二):AJAX 和 HTTP
pyspider 爬虫教程(三):使用 PhantomJS 渲染带 JS 的页面

pyspider安装及启动

安装很简单,如果已安装pip,直接执行命令

pip install pyspider

由于目前很多网站都是动态js生成页面,需要安装PhantomJS来获得js执行后的页面,而不是原本静态的html页面,我们再来装一下

pip install  phantomjs

待安装完成后,我们先看一下pyspider对应的可执行命令

Usage: pyspider [OPTIONS] COMMAND [ARGS]...

  A powerful spider system in python.

Options:
  -c, --config FILENAME    a json file with default values for subcommands.
                           {“webui”: {“port”:5001}}
  --logging-config TEXT    logging config file for built-in python logging
                           module  [default: pyspider/pyspider/logging.conf]
  --debug                  debug mode
  --queue-maxsize INTEGER  maxsize of queue
  --taskdb TEXT            database url for taskdb, default: sqlite
  --projectdb TEXT         database url for projectdb, default: sqlite
  --resultdb TEXT          database url for resultdb, default: sqlite
  --message-queue TEXT     connection url to message queue, default: builtin
                           multiprocessing.Queue
  --amqp-url TEXT          [deprecated] amqp url for rabbitmq. please use
                           --message-queue instead.
  --beanstalk TEXT         [deprecated] beanstalk config for beanstalk queue.
                           please use --message-queue instead.
  --phantomjs-proxy TEXT   phantomjs proxy ip:port
  --data-path TEXT         data dir path
  --version                Show the version and exit.
  --help                   Show this message and exit.

在这里我们直接执行如下命令启动,更复杂的命令参看文档

pyspider  --data-path  数据存放路径  all

爬虫脚本编写&执行

首先看一下启动成功后,浏览器访问127.0.0.1:5000地址的界面如下

点击Create,新建项目

点击生成的项目名,进入脚本编写&调试页面

Paste_Image.png

先看一下对应的爬虫脚本

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Created on 2017-01-13 11:38:33
# Project: qidian_allbook
import time
from pyspider.libs.base_handler import *
import requests
import random
import re


PAGE_START = 1
PAGE_END = 27754

def get_proxy():
    return requests.get("http://127.0.0.1:5001/get/").content

def get_all_proxy():
    return requests.get("http://127.0.0.1:5001/get_all/").content

def delete_proxy(proxy):
    requests.get("http://127.0.0.1:5001/delete/?proxy={}".format(proxy))

class Handler(BaseHandler):
    headers= {
        #"Host": "book.qidian.com",
        #"Connection": "keep-alive",
        #"Accept-Encoding": "gzip, deflate, sdch",
        #"Accept-Language":"zh-CN,zh;q=0.8,en;q=0.6",
        #"Referer":"https://www.baidu.com",      "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        #"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"
    }

    crawl_config = {
        "headers" : headers,
        "timeout" : 1000
    }
    
    def __init__(self):
        self.baseUrl = "http://a.qidian.com/?size=-1&sign=-1&tag=-1&chanId=-1&subCateId=-1&orderId=&update=-1&month=-1&style=1&action=-1&vip=-1&page="
        self.page_num = PAGE_START
        self.total_num = PAGE_END
        #self.proxy_all = get_all_proxy()
        #pattern = re.compile(r'"(.+?)"',re.S)
        #self.proxy = pattern.findall(self.proxy_all)
        #self.index = 1
        

    @every(minutes=24 * 60)
    @config(priority=3)
    def on_start(self):
        while self.page_num <= self.total_num:
            url = self.baseUrl + str(self.page_num)
            self.crawl(url, callback=self.index_page)
            self.page_num += 1


    
    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        for each in response.doc('h4 > a').items():
            self.crawl(each.attr.href, callback=self.detail_page)


    def detail_page(self, response):
        url = response.url
        name = response.doc('h1 > em').text()
        author = response.doc('h1 a').text()
        
        word_count = response.doc('.book-info > p > em').eq(0).text()
        click_count = response.doc('.book-info > p > em').eq(1).text()
        recommend_count = response.doc('.book-info > p > em').eq(2).text()
            
        return {
            "url": url,
            "name": name,
            "author": author,
            "word_count": word_count,
            "click_count": click_count,
            "recommend_count": recommend_count
        }

脚本编写及测试抓取遇到的问题

1.测试抓取时,运行一段时间后出现所有抓取链接均FetchError的报错,抓取失败
失败原因:未设置User-Agent 及 抓取速率太快,导致IP被封禁

解决办法:
1) 设置User-Agent,调整速率从1->0.7
2) 使用代理IP,防止被封禁,这里笔者尝试使用搭建简易免费代理IP池,但是由于免费代理大多不可用,会导致抓取不稳定,还是决定放弃使用

2.笔者本来是打算通过不断抓取下一页的链接,来遍历所有小说作品的,可是由于这部分是JS动态生成的,虽然使用phantomjs,能解决这个问题(具体见作者教程3),但是使用phantomjs会导致抓取效率变低,后来还是选择采用固定首尾页数(PAGE_START,PAGE_END)的方法

self.crawl(url, callback=self.index_page,fetch_type='js')

3.当使用css选择器有多个数据时,怎么获取自己想要的
比如在小说详细页,有字数,点击数,推荐数三个
其css selector均为 .book-info > p > em,要获取对应的次数只能使用pyquery的.eq(index)的方法去获取对应的文本数据了

word_count = response.doc('.book-info > p > em').eq(0).text()
click_count = response.doc('.book-info > p > em').eq(1).text()
recommend_count = response.doc('.book-info > p > em').eq(2).text()

4.如果遇到抓取的链接是https,而不是http的,使用self.crawl()方法时,需要加入参数validate_cert =False,同时需要确保pyspider --version 版本再0.3.6.0之上
具体解决方法,可以查看如下链接:
PySpider HTTP 599: SSL certificate problem错误的解决方法

执行结果及简单数据分析

共采集到328615部小说的作品信息

点击榜前十:
0: 斗破苍穹 点击次数为:150279300
1: 盘龙 点击次数为:94564599
2: 吞噬星空 点击次数为:84666200
3: 从零开始 点击次数为:64794200
4: 斗罗大陆 点击次数为:63158100
5: 遮天 点击次数为:62921100
6: 莽荒纪 点击次数为:61701100
7: 神墓 点击次数为:52844700
8: 星辰变 点击次数为:49905900
9: 阳神 点击次数为:49869399

推荐榜前十:
0: 从零开始 推荐次数为:13328400
1: 盘龙 推荐次数为:7716700
2: 吞噬星空 推荐次数为:7463500
3: 江山美人志 推荐次数为:7307300
4: 一念永恒 推荐次数为:6909400
5: 大主宰 推荐次数为:6804100
6: 星辰变 推荐次数为:6714600
7: 我真是大明星 推荐次数为:6689900
8: 斗破苍穹 推荐次数为:6644200
9: 完美世界 推荐次数为:6625200

字数榜前十:
0: 带着农场混异界 字数为:22389600
1: 重生之妖孽人生 字数为:21814900
2: 从零开始 字数为:20180800
3: 暗黑破坏神之毁灭 字数为:15993400
4: 修神外传 字数为:15944200
5: 煮酒点江山 字数为:15167600
6: 重生1991 字数为:13475700
7: 三国小兵之霸途 字数为:13108200
8: 校花的贴身高手 字数为:12295000
9: 重生之资源大亨 字数为:12141100

简单数据分析之二
采用SCWS 中文分词对所有作品名字进行分词统计,得到出现频率最高的排行

看起来如果写小说,起个『重生之我的神魔异世界』这类标题是不是吊炸天

词出现频率排行:
之  出现次数为:54104
的  出现次数为:28373
神  出现次数为:12308
异  出现次数为:12051
天  出现次数为:11507
界  出现次数为:11280
我  出现次数为:10813
魔  出现次数为:10008
仙  出现次数为:8543
世  出现次数为:6408
重生  出现次数为:6225
剑  出现次数为:6202
网游  出现次数为:5533
世界  出现次数为:5472
修  出现次数为:5372
录  出现次数为:4927
道  出现次数为:4658
战  出现次数为:4632
魂  出现次数为:4332
天下  出现次数为:4129

简单数据分析之三
简单统计一下起点作者的作品数排序
武侠精品应该是起点的官方作者号吧,不然194本作品也太恐怖了
也发现了不少熟悉的大神,比如唐家三少,流浪的蛤蟆,骷髅精灵等,有些作品还是可以看看的

0: 作者:武侠精品 发布作品数:194
1: 作者:石三 发布作品数:15
2: 作者:爱妻族 发布作品数:13
3: 作者:唐家三少 发布作品数:12
4: 作者:流浪的蛤蟆 发布作品数:12
5: 作者:庄小街 发布作品数:11
6: 作者:殷扬 发布作品数:11
7: 作者:沐轶 发布作品数:11
8: 作者:飘零幻 发布作品数:10
9: 作者:天净沙秋思 发布作品数:10
10: 作者:骷髅精灵 发布作品数:10

未完待续

  1. 由于要规避反爬虫策略,导致目前抓取效率偏低,目前一天只能抓取9W条数据,有没有方法优化?

  2. 目前抓取的数据保存在pyspider自带的result.db (sqlite3数据库)中,导致数据分析不太方便,可否有别的存储方式?mongodb?mysql?

  3. 抓取时设定的是总共有27754页,那按理来说总共应该有20*27754=55w数据才对,可是目前最后抓取出来总共只有328615的作品数据,哪里少了?是作品详情链接无效了?还是抓取失败了?

日记本
Web note ad 1