之前写了一篇网络字体反爬之pyspider爬取起点中文小说 可能有人看了感觉讲的太模糊了,基本上就是一笔带过,一点也不详细。这里要说明一下,上一篇主要是因为有字体反爬,所以我才写了那篇文章,所以主要就是提一个字体反爬的概念让大家知道,其中并没有涉及到其他比较难的知识点,所以就是大概介绍一下。
今天依然是起点小说爬取。不过我们今天换一个框架,我们使用scrapy加上redis去重过滤和scrapyd远程部署,所以主要的爬取代码基本与上篇一致,在文章最后我会把git地址贴上,大家看看源码。
scrapy
官方文档
安装scrapy pip install scrapy
。
安装完后我们简单介绍一下scrapy的部分配置。
setting配置文件
代码语言:javascript复制ROBOTSTXT_OBEY = Ture,是否遵守 robots.txt,一般修改为FalseDEFAULT_REQUEST_HEADERS : 设置默认的请求headers
SPIDER_MIDDLEWARES:爬虫中间层
DOWNLOADER_MIDDLEWARES:下载中间层# pipeline里面可以配置多个,每一个spider都会调用所有配置的pipeline,后面配置的数字表示调用的优先级,数字越小,调用越早ITEM_PIPELINES = {'项目名.pipelines.PipeLine类名': 300,}# 开发模式时,启用缓存,可以提高调试效率。同样的请求,如果缓存当中有保存内容的话,不会去进行网络请求,直接从缓存中返回。**部署时一定要注释掉!!!**HTTPCACHE_ENABLED = TrueHTTPCACHE_EXPIRATION_SECS = 0HTTPCACHE_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,而不是每个域
爬虫类
属性
代码语言:javascript复制name:爬虫的名字,必须唯一 ,必须写!
start_urls:爬虫初始爬取的链接列表
custom_setting = {} # 自定义的setting配置
方法
代码语言:javascript复制start_requests:启动爬虫的时候调用,爬取urls的链接,可以省略"""
如果配置了start_urls属性,并且没有实现start_requests方法,就会默认调用parse函数
如果在Request对象配置了callback函数,则不会调用,parse方法可以迭代返回Item或Request对象,
如果返回Request对象,则会进行增量爬取
"""parse:response到达spider的时候默认调用,如果自定义callback方法,尽量不要使用这个名字
items
items实际就是要爬取的字段定义,一般情况我们写scrapy时,首先就要确定自己需要获取那些数据 定义:
代码语言:javascript复制class Product(scrapy.Item):
name = scrapy.Field()
title = scrapy.Field()
调用:
代码语言:javascript复制# 可以像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中,添加
代码语言:javascript复制ITEM_PIPELINES = { 'first_scrapy.pipelines.FirstScrapyPipeline': 300, # 优先级,数字越小,
优先级越高,越早调用范围 0-1000
}
对象如下:
代码语言:javascript复制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
@classmethodfrom_crawler(cls, crawler)
参数:
crawler (Crawler object) – 使用这个pipe的爬虫crawler`
运行
- 命令行中运行:
命令行 中 进入到 first_scrapy 目录中,执行: scrapy crawl qidian
- pycharm 运行 在 项目 根目录 添加 run.py 文件:
from first_scrapy.spiders.quotes import QidianSpiderfrom scrapy.crawler import CrawlerProcessfrom scrapy.utils.project import get_project_settings# 获取settings.py模块的设置settings = get_project_settings()
process = CrawlerProcess(settings=settings)# 可以添加多个spiderprocess.crawl(QidianSpider)# 启动爬虫,会阻塞,直到爬取完成process.start()
或者:
代码语言:javascript复制from scrapy.cmdline import execute#设置工程命令import sysimport 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
特点:
代码语言:javascript复制1、支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
2、不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
3、支持数据的备份,即master-slave模式的数据备份。
优势:
代码语言:javascript复制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 配置项说明如下:(我们使用的是默认配置哦)
代码语言:javascript复制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可以同时管理多个爬虫,每个爬虫还可以有多个版本。 特点:
代码语言:javascript复制1、可以避免爬虫源码被看到。
2、有版本控制。
3、可以远程启动、停止、删除
安装
pip install scrapyd
pip install scrapyd-deploy
配置scrapyd.conf 官方说明配置文档位置:
- /etc/scrapyd/scrapyd.conf (Unix)
- c:scrapydscrapyd.conf (Windows)
- /etc/scrapyd/conf.d/* (in alphabetical order, Unix)
- scrapyd.conf
- ~/.scrapyd.conf (users home directory)
default_scrapyd.conf
代码语言:javascript复制[scrapyd]# 项目的 eggs 存储位置eggs_dir = eggs# Scrapy日志的存储目录。如果要禁用存储日志,请将此选项设置为空,如下# logs_dir = logs_dir = logs# Scrapyitem将被存储的目录,默认情况下禁用此选项,如果设置了 值,会覆盖 scrapy的 FEED_URI 配置项items_dir =# 每个蜘蛛保持完成的工作数量。默认为5jobs_to_keep = 5# 项目数据库存储的目录dbs_dir = dbs# 并发scrapy进程的最大数量,默认为0,没有设置或者设置为0时,将使用系统中可用的cpus数乘以max_proc_per_cpu配置的值max_proc = 0# 每个CPU启动的进程数,默认4max_proc_per_cpu = 4# 保留在启动器中的完成进程的数量。默认为100finished_to_keep = 100# 用于轮询队列的时间间隔,以秒为单位。默认为5.0poll_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
发布项目
- 将/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没法尝试了。
- 修改 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] 可以配置多个。
修改后:
代码语言:javascript复制[deploy:pro_qidian]
url = http://localhost:6800/addversion.jsonproject = qidian
- 查看scrapd服务配置
打开控制台,切换到 scrapy 项目根目录,执行
scrapyd-deploy -l
- 发布爬虫
scrapyd-deploy <target> -p <project> --version <version>
- target:之前scrapy.cfg配置的 [deploy:127] 中的 127
- project:项目名称,一般使用和scrapy项目一个名字
- version:版本号,默认是当前时间戳
还有一些控制的API,可以查看官方文档。
- 启动爬虫
在控制台中执行:
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处理
- builtins.KeyError: 'project'
解决:
进行post提交时,需要将参数提交放入到 params 或 data 中,而不是json
如:
requests.post(url, params=params)
或requests.post(url, data=params)
- TypeError: init() missing 1 required positional argument: 'self' 修改 spider ,增加 :
def __init__(self, **kwargs):
super(DingdianSpider, self).__init__(self, **kwargs)
...
- redis.exceptions.ConnectionError: Error 10061 connecting to localhost:6379 有类似这样的错误,是由于项目中有连接其他服务,譬如这里是redis数据库,需要先启动 对应的服务
模块就介绍到这里,下面看下我们项目的处理。
创建项目:scrapy startproject qidian
创建爬虫:scrapy genspider qidian
在settings中设置如下,其他的保持默认
代码语言:javascript复制ROBOTSTXT_OBEY=FalseDEFAULT_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中定义我们要爬取的数据结构:
代码语言:javascript复制import scrapyclass 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进行爬取。这里为什么要给字体解析请求加上高优先级呢?
- 字体解析请求本来就不多,只有几种而已
- 我们在parse中把所有页的请求都添加到调度器中,大概有4万多页,也就是4万多个请求
- 如果按照添加顺序进行请求处理,那么爬虫必须先处理完4万多条请求后,再处理字体请求,处理了字体请求才能获取出数据,交给pipeline进行处理。我之前没有加优先级,所以导致运行很长时间MongoDB中都没有数据。
再看一下pipeline:
代码语言:javascript复制import pymongoimport 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 itemclass 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的使用比较简单,而且我已经部署了,没截图了,也就不详述了。 基本步骤:
- 修改项目scrapy.cfg文件,参见上面
- 在项目根目录执行scrapyd-deploy pro_qidian -p qidian --version v.0.1.0
- 启动爬虫:
curl http://localhost:6800/schedule.json -d project=myproject -d spider=somespider
- 浏览器中打开http://localhost:6800
- 选择job后可以查看爬虫状态
这一次概念比较多,写一下做个记录,增加自己的印象,以后也好查询。度娘上东西是不少,但是每次查询也挺麻烦。我以前不爱记录东西,感觉网上都能查到,这次能查出来,下次不是也能查出来。自从开始写爬虫实战后,看着阅读量和增加的关注度,就越有动力写。这真是一种良性循环。现在简书基本都变成我的笔记了,随时有东西想记录就打开记录,写好了能发布就发布,不能发布就保存自己看。算是我自己学习爬虫的一点点心得吧,鼓励大家多做笔记。