起点小说爬取--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 <dbid>命令在连接上指定数据库id
    databases 16

9. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
    多个条件中,任意满足一个就会进行同步
    save <seconds> <changes>
    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 <masterip> <masterport>

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

15. 设置Redis连接密码,如果配置了连接密码,
    客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭
    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 <bytes>

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 <target> -p <project> –version <version>

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

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

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

  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》

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

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

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

《起点小说爬取--scrapy/redis/scrapyd》 公众号

《起点小说爬取--scrapy/redis/scrapyd》 微信号

    原文作者:星星在线
    原文地址: https://www.jianshu.com/p/6cc1434b1735
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞