起点小说爬取--scrapy/redis/scrapyd

之前写了一篇网络字体反爬之pyspider爬取起点中文小说
可能有人看了感觉讲的太模糊了,基本上就是一笔带过,一点也不详细。这里要说明一下,上一篇主要是因为有字体反爬,所以我才写了那篇文章,所以主要就是提一个字体反爬的概念让大家知道,其中并没有涉及到其他比较难的知识点,所以就是大概介绍一下。

今天依然是起点小说爬取。不过我们今天换一个框架,我们使用scrapy加上redis去重过滤和scrapyd远程部署,所以主要的爬取代码基本与上篇一致,在文章最后我会把git地址贴上,大家看看源码。

scrapy

官方文档

安装scrapy pip install scrapy
安装完后我们简单介绍一下scrapy的部分配置。

setting配置文件

ROBOTSTXT_OBEY = Ture,是否遵守 robots.txt,一般修改为False
DEFAULT_REQUEST_HEADERS : 设置默认的请求headers
SPIDER_MIDDLEWARES:爬虫中间层
DOWNLOADER_MIDDLEWARES:下载中间层

# pipeline里面可以配置多个,每一个spider都会调用所有配置的pipeline,后面配置的数字表示调用的优先级,数字越小,调用越早
ITEM_PIPELINES = {'项目名.pipelines.PipeLine类名': 300,}

# 开发模式时,启用缓存,可以提高调试效率。同样的请求,如果缓存当中有保存内容的话,不会去进行网络请求,直接从缓存中返回。**部署时一定要注释掉!!!**
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0
HTTPCACHE_DIR = 'httpcache'
HTTPCACHE_IGNORE_HTTP_CODES = []
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

# 日志管理
LOG_ENABLED 默认: True,启用logging
LOG_ENCODING 默认: 'utf-8',logging使用的编码
LOG_FILE 默认: None,在当前目录里创建logging输出文件的文件名,例如:LOG_FILE = 'log.txt'
    配置了这个文件,就不会在控制台输出日志了
LOG_LEVEL 默认: 'DEBUG',log的最低级别,会打印大量的日志信息,如果我们不想看到太多的日志,可以提高log等级
共五级:
CRITICAL - 严重错误
ERROR - 一般错误
WARNING - 警告信息
INFO - 一般信息
DEBUG - 调试信息

LOG_STDOUT 默认: False 如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
例如,执行 print("hello") ,其将会显示到日志文件中

# 并发(下面都是默认值)
CONCURRENT_ITEMS = 100 #  并发处理 items 的最大数量
CONCURRENT_REQUESTS = 16  #  并发下载request页面的最大数量
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 并发下载任何单域的最大数量
CONCURRENT_REQUESTS_PER_IP = 0 # 并发每个IP请求的最大数量
DOWNLOAD_DELAY = 0.25 # 单位秒,支持小数,一般都是随机范围:0.5*DOWNLOAD_DELAY 到 1.5*DOWNLOAD_DELAY 之间
CONCURRENT_REQUESTS_PER_IP 不为0时,这个延时是针对每个IP,而不是每个域

爬虫类

属性

name:爬虫的名字,必须唯一  ,必须写!
start_urls:爬虫初始爬取的链接列表
custom_setting = {} # 自定义的setting配置

方法

start_requests:启动爬虫的时候调用,爬取urls的链接,可以省略

"""
如果配置了start_urls属性,并且没有实现start_requests方法,就会默认调用parse函数
如果在Request对象配置了callback函数,则不会调用,parse方法可以迭代返回Item或Request对象,
如果返回Request对象,则会进行增量爬取
"""
parse:response到达spider的时候默认调用,如果自定义callback方法,尽量不要使用这个名字

items

items实际就是要爬取的字段定义,一般情况我们写scrapy时,首先就要确定自己需要获取那些数据
定义:

class Product(scrapy.Item):
    name = scrapy.Field()
    title = scrapy.Field()

调用:

# 可以像dict一样的调用
product = Product(name='Desktop PC', title='pc title')

# 像字典一样的使用:
print(product['name'])
print(product.get('name'))
product['title'] = 'new title'

可以这样转换为字典:dict(product),主要是在一些必须使用dict类型的场景使用,比如MongoDB插入数据。

pipelines

必须在settings中,添加

ITEM_PIPELINES = {
    'first_scrapy.pipelines.FirstScrapyPipeline': 300, # 优先级,数字越小,
                                                    优先级越高,越早调用范围 0-1000
}

对象如下:

class FirstScrapyPipeline(object):
    def process_item(self, item, spider):
        return item
  • process_item
process_item(self, item, spider): 处理item的方法, 必须有的!!!

参数:
item (Item object or a dict) : 获取到的item
spider (Spider object) : 获取到item的spider
返回    一个dict或者item
  • open_spider
open_spider(self, spider) : 当spider启动时,调用这个方法
参数:
spider (Spider object) – 启动的spider
  • close_spider
close_spider(self, spider): 当spider关闭时,调用这个方法
参数:
spider (Spider object) – 关闭的spider
  • from_crawler
@classmethod
from_crawler(cls, crawler)
参数:
crawler (Crawler object) – 使用这个pipe的爬虫crawler`

运行

  • 命令行中运行:
    命令行 中 进入到 first_scrapy 目录中,执行: scrapy crawl qidian

  • pycharm 运行
    在 项目 根目录 添加 run.py 文件:

from first_scrapy.spiders.quotes import QidianSpider
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings

# 获取settings.py模块的设置
settings = get_project_settings()
process = CrawlerProcess(settings=settings)

# 可以添加多个spider
process.crawl(QidianSpider)

# 启动爬虫,会阻塞,直到爬取完成
process.start()

或者:

from scrapy.cmdline import execute

#设置工程命令
import sys
import os

#设置工程路径,在cmd 命令更改路径而执行scrapy命令调试
#获取run文件的父目录,os.path.abspath(__file__) 为__file__文件目录
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(["scrapy","crawl","qidian" ])

redis

Redis安装:https://www.jianshu.com/p/50694e644c25
官网文档:https://redis.io/documentation
中文文档:http://www.redis.cn/documentation.html

Redis数据库是内存数据库,性能极高,因此经常被用来配合其他非内存数据库使用,查询速度非常快,但是它是不安全的,因为数据在内存中,所以如果遇到异常会造成数据丢失。虽然它的数据也会保存在硬盘中,但是不是实时保存。总之一定要注意:
不要把 Redis 用作主要的数据存储数据库!!!!
不能存储太多的信息!!大数据量的信息不要存储到Redis

特点:

1、支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2、不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3、支持数据的备份,即master-slave模式的数据备份。

优势:

1、性能极高:Redis能读的速度是110000次/s,写的速度是81000次/s 。
2、丰富的数据类型:Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
3、原子:Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。
    单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
4、丰富的特性:Redis还支持 publish/subscribe, 通知, key 过期等等特性

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)

redis.conf 配置项说明如下:(我们使用的是默认配置哦)

1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
    daemonize no

2. 当Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
    pidfile /var/run/redis.pid

3. 指定Redis监听端口,默认端口为6379,作者选用6379作为默认端口,
    因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字
    port 6379

4. 绑定的主机地址,这个已经要注意,做测试都是绑定 127.0.0.1
    bind 127.0.0.1

5.当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
    timeout 300

6. 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为verbose
    loglevel verbose

7. 日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,
    而这里又配置为日志记录方式为标准输出,则日志将会发送给/dev/null
    logfile stdout

8. 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
    databases 16

9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
    多个条件中,任意满足一个就会进行同步
    save  
    Redis默认配置文件中提供了三个条件:
    save 900 1
    save 300 10
    save 60 10000

    分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。

10. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,
    如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
    rdbcompression yes

11. 指定本地数据库文件名,默认值为dump.rdb
    dbfilename dump.rdb

12. 指定本地数据库存放目录
    dir ./

13. 设置当本机为slave服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
    slaveof  

14. 当master服务设置了密码保护时,slave服务连接master的密码
    masterauth 

15. 设置Redis连接密码,如果配置了连接密码,
    客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭
    requirepass foobared

16. 设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,
    如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,
    Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
    maxclients 128

17. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,
    Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,
    将无法再进行写入操作,但仍然可以进行读取操作。
    Redis新的vm机制,会把Key存放内存,Value会存放在swap区
    maxmemory 

18. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,
    如果不开启,可能会在断电时导致一段时间内的数据丢失。
    因为 redis本身同步数据文件是按上面save条件来同步的,
    所以有的数据会在一段时间内只存在于内存中。默认为no
    appendonly no

19. 指定更新日志文件名,默认为appendonly.aof
    appendfilename appendonly.aof

20. 指定更新日志条件,共有3个可选值:
    no:表示等操作系统进行数据缓存同步到磁盘(快)
    always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
    everysec:表示每秒同步一次(折衷,默认值)
    appendfsync everysec

21. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,
    由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(
    vm-enabled no

22. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
    vm-swap-file /tmp/redis.swap

23. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,
    所有索引数据都是内存存储的(Redis的索引数据 就是keys),也就是说,
    当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0
    vm-max-memory 0

24. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不 确定,就使用默认值
    vm-page-size 32

25. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
    vm-pages 134217728

26. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
    vm-max-threads 4

27. 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启
    glueoutputbuf yes

28. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
    hash-max-zipmap-entries 64
    hash-max-zipmap-value 512

29. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
    activerehashing yes

30. 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,
    而同时各个实例又拥有自己的特定配置文件
    include /path/to/local.conf

scrapyd

官方文档:http://scrapyd.readthedocs.io/en/stable/

scrapyd是运行scrapy爬虫的服务程序,它支持以http命令方式发布、删除、启动、停止爬虫程序。而且scrapyd可以同时管理多个爬虫,每个爬虫还可以有多个版本。
特点:

1、可以避免爬虫源码被看到。
2、有版本控制。
3、可以远程启动、停止、删除

安装
pip install scrapyd
pip install scrapyd-client

配置scrapyd.conf
官方说明配置文档位置:

  • /etc/scrapyd/scrapyd.conf (Unix)
  • c:\scrapyd\scrapyd.conf (Windows)
  • /etc/scrapyd/conf.d/* (in alphabetical order, Unix)
  • scrapyd.conf
  • ~/.scrapyd.conf (users home directory)

default_scrapyd.conf

[scrapyd]
# 项目的 eggs 存储位置
eggs_dir    = eggs

# Scrapy日志的存储目录。如果要禁用存储日志,请将此选项设置为空,如下
# logs_dir = 
logs_dir    = logs

# Scrapyitem将被存储的目录,默认情况下禁用此选项,如果设置了 值,会覆盖 scrapy的 FEED_URI 配置项
items_dir   =

# 每个蜘蛛保持完成的工作数量。默认为5
jobs_to_keep = 5

# 项目数据库存储的目录
dbs_dir     = dbs

# 并发scrapy进程的最大数量,默认为0,没有设置或者设置为0时,将使用系统中可用的cpus数乘以max_proc_per_cpu配置的值
max_proc    = 0

# 每个CPU启动的进程数,默认4
max_proc_per_cpu = 4

# 保留在启动器中的完成进程的数量。默认为100
finished_to_keep = 100

# 用于轮询队列的时间间隔,以秒为单位。默认为5.0
poll_interval = 5.0

# webservices监听地址
bind_address = 127.0.0.1

# 默认 http 监听端口
http_port   = 6800

# 是否调试模式
debug       = off

# 将用于启动子流程的模块,可以使用自己的模块自定义从Scrapyd启动的Scrapy进程
runner      = scrapyd.runner
application = scrapyd.app.application
launcher    = scrapyd.launcher.Launcher
webroot     = scrapyd.website.Root

[services]
schedule.json     = scrapyd.webservice.Schedule
cancel.json       = scrapyd.webservice.Cancel
addversion.json   = scrapyd.webservice.AddVersion
listprojects.json = scrapyd.webservice.ListProjects
listversions.json = scrapyd.webservice.ListVersions
listspiders.json  = scrapyd.webservice.ListSpiders
delproject.json   = scrapyd.webservice.DeleteProject
delversion.json   = scrapyd.webservice.DeleteVersion
listjobs.json     = scrapyd.webservice.ListJobs
daemonstatus.json = scrapyd.webservice.DaemonStatus

发布项目

  1. 将/Library/Frameworks/Python.framework/Versions/3.6/bin目录下的scrapyd-deploy添加到环境变量
    ln -s /Library/Frameworks/Python.framework/Versions/3.6/bin/scrapyd-deploy /usr/local/bin/scrapyd-deploy。
    Windows下在python安装目录下找找吧,我用的Mac没法尝试了。

  2. 修改 scrapy.cfg
    修改前:

[deploy]
#url = http://localhost:6800/
project = qidian

去掉url前的注释符号,这里url就是你的scrapyd服务器的网址
修改为:url = http://localhost:6800/addversion.json
[deploy] 修改为 [deploy:pro_qidian],这个 target:pro_qidian是爬虫服务器的名称 ,这个 [deploy] 可以配置多个。

修改后:

[deploy:pro_qidian]
url = http://localhost:6800/addversion.json
project = qidian
  1. 查看scrapd服务配置
    打开控制台,切换到 scrapy 项目根目录,执行scrapyd-deploy -l

  2. 发布爬虫
    scrapyd-deploy -p --version

    • target:之前scrapy.cfg配置的 [deploy:127] 中的 127
    • project:项目名称,一般使用和scrapy项目一个名字
    • version:版本号,默认是当前时间戳

还有一些控制的API,可以查看官方文档。


起点小说爬取--scrapy/redis/scrapyd_第1张图片
  1. 启动爬虫
    在控制台中执行:
    curl http://localhost:6800/schedule.json -d project=myproject -d spider=somespider
    或者
import request
url = "http://localhost:6800/schedule.json"
data = {
        "project": project,
        "spider": spider
    }
resq = requests.post(url, data=data)
print(resq.json())

BUG处理

  1. builtins.KeyError: 'project'
    解决:
    进行post提交时,需要将参数提交放入到 params 或 data 中,而不是json
    如: requests.post(url, params=params)requests.post(url, data=params)

  2. TypeError: init() missing 1 required positional argument: 'self'
    修改 spider ,增加 :

def __init__(self, **kwargs):           
    super(DingdianSpider, self).__init__(self, **kwargs)
    ...
  1. redis.exceptions.ConnectionError: Error 10061 connecting to localhost:6379
    有类似这样的错误,是由于项目中有连接其他服务,譬如这里是redis数据库,需要先启动 对应的服务

模块就介绍到这里,下面看下我们项目的处理。
创建项目:scrapy startproject qidian
创建爬虫:scrapy genspider qidian

在settings中设置如下,其他的保持默认

ROBOTSTXT_OBEY=False
DEFAULT_REQUEST_HEADERS = {
  'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.109 Safari/537.36'
}

ITEM_PIPELINES = {
   'qidian.pipelines.QidianRedisPipeline': 1,
   'qidian.pipelines.QidianPipeline': 300,
}

MONGO_URI = "mongodb://localhost:27017"
MONGO_DATABASE = "scrapys"

先在item中定义我们要爬取的数据结构:

import scrapy

class QidianItem(scrapy.Item):
    # define the fields for your item here like:
    url = scrapy.Field()
    name = scrapy.Field()
    author = scrapy.Field()
    status = scrapy.Field()
    update = scrapy.Field()
    words = scrapy.Field()

具体代码在我的GitHub上。

Redis去重

在spider文件中初始化一下redis redis = redis.Redis(host='localhost', port=6379, db=0)

    def parse(self, response):
        html = etree.HTML(response.text)
        page = html.xpath('//a[@class="lbf-pagination-page  "]')[-1]
        total_pages = int(page.text)

        for i in range(1, total_pages + 1):
            uuid = md5()
            uuid.update(self.page_url.format(i).encode())
            # 添加一个集合qidian_url,记录请求url的md5信息,用来记录当前已访问过的url
            # 这里记录一方面可以防止重复请求,另外一方面也可以断点重爬,爬取一半中断后,重启后可以继续上次爬取的位置开始
            if self.redis.sismember("qidian_url", uuid.digest()):
                continue
            self.redis.sadd("qidian_url", uuid.digest())
            print(self.page_url.format(i))

            yield scrapy.Request(self.page_url.format(i), callback=self.parse_page)

还有一块解析字体的地方需要修改,增加priority参数: yield scrapy.Request(woff_url, callback=self.parse_detail, meta=item, priority=100),这里需要说明一下,我们用scrapy.Request创建的请求会通过控制中心,传递给调度队列,调度器会根据优先级把队列中请求交给spider进行爬取。这里为什么要给字体解析请求加上高优先级呢?

  1. 字体解析请求本来就不多,只有几种而已
  2. 我们在parse中把所有页的请求都添加到调度器中,大概有4万多页,也就是4万多个请求
  3. 如果按照添加顺序进行请求处理,那么爬虫必须先处理完4万多条请求后,再处理字体请求,处理了字体请求才能获取出数据,交给pipeline进行处理。我之前没有加优先级,所以导致运行很长时间MongoDB中都没有数据。

再看一下pipeline:

import pymongo
import redis

'''
根据settings中的设置,爬取的数据会先经过QidianRedisPipeline的处理,然后再交给QidianPipeline处理
这样就给我们提供了数据去重。如果在process_item中不返回item,那么数据就不会向下传递。
因为我在爬取的过程中发现起点首页提供的所有小说信息中,最后一些分页里的数据都是重复的,所以还是需要增加去重处理的。
'''
class QidianRedisPipeline(object):
    def open_spider(self, spider):
        self.redis = redis.Redis(host='localhost', port=6379, db=0)

    def process_item(self, item, spider):
        # qidian_data集合中记录所有小说的名称,如果有重复就直接返回
        if self.redis.sismember("qidian_data", item["name"]):
            return #这里返回就中断了pipeline的传递链,不会再将数据向下传递
        self.redis.sadd("qidian_data", item["name"])
        return item

class QidianPipeline(object):
    collection_name = 'qidian'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        #  必须在settings中 配置 MONGO_URI 和 MONGO_DATABASE
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            # items 是默认值,如果settings当中没有配置 MONGO_DATABASE ,那么 mongo_db = 'items'
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        self.db[self.collection_name].insert_one(dict(item))
        return item

scrapyd的使用比较简单,而且我已经部署了,没截图了,也就不详述了。
基本步骤:

  1. 修改项目scrapy.cfg文件,参见上面
  2. 在项目根目录执行scrapyd-deploy pro_qidian -p qidian --version v.0.1.0
  3. 启动爬虫:curl http://localhost:6800/schedule.json -d project=myproject -d spider=somespider
  4. 浏览器中打开http://localhost:6800
  5. 选择job后可以查看爬虫状态


    起点小说爬取--scrapy/redis/scrapyd_第2张图片
起点小说爬取--scrapy/redis/scrapyd_第3张图片

这一次概念比较多,写一下做个记录,增加自己的印象,以后也好查询。度娘上东西是不少,但是每次查询也挺麻烦。我以前不爱记录东西,感觉网上都能查到,这次能查出来,下次不是也能查出来。自从开始写爬虫实战后,看着阅读量和增加的关注度,就越有动力写。这真是一种良性循环。现在基本都变成我的笔记了,随时有东西想记录就打开记录,写好了能发布就发布,不能发布就保存自己看。算是我自己学习爬虫的一点点心得吧,鼓励大家多做笔记。


如果你觉得我的文章还可以,可以关注我的微信公众号:Python爬虫实战之路
也可以扫描下面二维码,添加我的微信号

起点小说爬取--scrapy/redis/scrapyd_第4张图片
公众号

起点小说爬取--scrapy/redis/scrapyd_第5张图片
微信号

你可能感兴趣的:(起点小说爬取--scrapy/redis/scrapyd)