总览
- 环境
- 分析
- 猜想
- 实施
- 总结
环境
- mac or ubuntu 16.04
- docker
分析
上一章节,==> [可视化抓取]portia2.0尝鲜体验以及自动化畅想-数据输出以及原理分析 <== 我们已经分析了portia 数据 输出的方式,以及从代码的层面来分析它实际的运作原理,群里有小伙伴以及微信私信问我:大鱼,这个怎么和我们的数据库正常的结合呢?一般我们遇到问题,先对比之前的处理经验
回顾
我们在以前正常的scrapy数据入库,都是写好一个pipeline ,然后写好我们的业务代码,加载pipeline就可以写入到数据库里面了,OK,那大方向确定了,我们就正常的从这个方面着手。
准备
还是拿我们上次教程的cnblogs 里面的代码来做例子,目前,我们本地的portia docker 已经起来了,我们看一下状态,使用命令docker ps -a
1 2 |
5ea71b2e3f6d scrapinghub/portia:portia-2.0.8 "/app/docker/entry" 8 days ago Up 8 days 0.0.0.0:9001->9001/tcp 5ea71b2e3f6d_portia_portia_1 |
当前将宿主机的9001 和docker 内部的 9001 绑定,暴露出http服务,我们在打开浏览器http://127.0.0.1:9001/
是可以看到我们熟悉的服务界面了,这里就不详细的介绍了,关于使用和环境的搭建以及项目的生成,我们可以好好的参考我前面的两篇文章。
探究
我们首先进入docker 内部 ,使用命令
1 2 |
docker exec -it 5ea71b2e3f6d_portia_portia_1 bash |
进入目录/app/data/projects
找到我们熟悉cnblogs项目,我们首先看一下整体的目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
. |____extractors.json |____items.json |____project.json |____scrapy.cfg |____setup.py |____spiders | |______init__.py | |____settings.py | |____www.cnblogs.com | | |____dd60-46f2-bbea | | | |____original_body.html | | | |____rendered_body.html | | |____dd60-46f2-bbea.json | |____www.cnblogs.com.json |
对比一下我们之前的scrapy项目,我们只要写好pipeline ,在settings.py 里面加载好就ok了
在settings.py
的同级目录下,建立pipelines.py
文件,我们这次先正常的输出内容就好,暂时不存入到任何的数据库
1 2 3 4 5 6 7 8 9 10 11 12 |
# -*- coding: utf-8 -*- class MongoPipeline(object): def process_item(self, item, spider): print 'i got the spider from the spider' return item |
Ok ,我们添加到settings.py
里面添加我们的写好的pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#coding:utf-8 # Automatically created by: slyd import os SPIDER_MANAGER_CLASS = 'slybot.spidermanager.ZipfileSlybotSpiderManager' EXTENSIONS = {'slybot.closespider.SlybotCloseSpider': 1} ITEM_PIPELINES = { 'slybot.dupefilter.DupeFilterPipeline': 1, 'cnblogs.spiders.pipelines.MongoPipeline': 2, # 这里加入当前的项目路径 } SPIDER_MIDDLEWARES = {'slybot.spiderlets.SpiderletsMiddleware': 999} # as close as possible to spider output DOWNLOADER_MIDDLEWARES = { 'slybot.pageactions.PageActionsMiddleware': 700, 'slybot.splash.SlybotJsMiddleware': 725 } PLUGINS = [ 'slybot.plugins.scrapely_annotations.Annotations', 'slybot.plugins.selectors.Selectors' ] SLYDUPEFILTER_ENABLED = True DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter' PROJECT_ZIPFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) try: from local_slybot_settings import * except ImportError: pass |
这样,我们就把cnblogs项目里面的pipelines.py 正常的加入到了我们的配置选项里面了,接下来我们来在里面运行我们的项目,切换到/app/data/projects
目录 ,使用命令:
1 2 |
portiacrawl cnblogs www.cnblogs.com |
文档所在:http://portia.readthedocs.io/en/latest/spiders.html#running-a-spider
结果输出
结果显示,这个文件压根就没有加载进去 ,下面就是本次章节的精彩猜想以及如何去分析一个项目了
猜想
命令
我们来仔细研究一下 portiacrawl
到底是一个怎么样的过程,既然有这个命令,我们直接拿到portia 的官方git里面搜一下 https://github.com/scrapinghub/portia/search?utf8=%E2%9C%93&q=portiacrawl&type=
,通过筛查,发现是如下的文件使用
1 2 3 4 5 6 7 8 |
slybot/setup.py packages=find_packages(exclude=('tests', 'tests.*')), platforms=['Any'], scripts=['bin/slybot', 'bin/portiacrawl'], install_requires=install_requires, |
我们进一步查找bin/portiacrawl 这个脚本 https://github.com/scrapinghub/portia/blob/master/slybot/bin/portiacrawl
,代码层里面 如下的代码值得查看
1 2 3 4 5 6 7 8 9 10 11 12 13 |
command_spec = ["scrapy", "crawl", args[1]] if len(args) == 2 else ["scrapy", "list"] if project_specs.endswith(".zip"): command_spec.extend([ "-s", "PROJECT_ZIPFILE=%s" % project_specs, "-s", "SPIDER_MANAGER_CLASS=slybot.spidermanager.ZipfileSlybotSpiderManager", ]) else: command_spec.extend([ "-s", "PROJECT_DIR=%s" % project_specs, "-s", "SPIDER_MANAGER_CLASS=slybot.spidermanager.SlybotSpiderManager", ]) |
我们通过代码查看,基本所谓的portiacrawl
其实就是就是套了一层scrapy crawl 的命令
,那这样,我们之前scrapy crawl spider_name -o result.json -t json
命令是不是也可以正常使用呢?直接将结果输出呢?
实践
还是一样,使用命令
1 2 |
portiacrawl cnblogs www.cnblogs.com -o test.json -t json |
奇迹出现了!我们看看本地文件夹 ,出现了test.json
结果就是我们所爬取的内容,这个时候,我们可以直接将test.json 导入到mongodb,其实也可以算是一种数据库输入的方法了,当然,我们要进一步的去研究pipelines.py
的使用
深入
我们注意到 我们每次使用 portiacrawl
这个命令的时候,都会根据当前的 文件类似(是否为zip文件) 来加入一个这个命令
1 2 |
SPIDER_MANAGER_CLASS=slybot.spidermanager.SlybotSpiderManager |
我们进一步去查看scrapy里面的command 关于-s
的用法是什么意思 ,参考文件 https://github.com/scrapy/scrapy/blob/master/scrapy/commands/__init__.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
group = OptionGroup(parser, "Global Options") group.add_option("--logfile", metavar="FILE", help="log file. if omitted stderr will be used") group.add_option("-L", "--loglevel", metavar="LEVEL", default=None, help="log level (default: %s)" % self.settings['LOG_LEVEL']) group.add_option("--nolog", action="store_true", help="disable logging completely") group.add_option("--profile", metavar="FILE", default=None, help="write python cProfile stats to FILE") group.add_option("--pidfile", metavar="FILE", help="write process ID to FILE") group.add_option("-s", "--set", action="append", default=[], metavar="NAME=VALUE", help="set/override setting (may be repeated)") group.add_option("--pdb", action="store_true", help="enable pdb on failure") |
参考里面的-s ,简单来看就是替换默认的原来settings.py 里面的配置选项为我们自己的值,OK,我们重新回到docker 内部,我们来找找这个SPIDER_MANAGER_CLASS
到底啥时候用到,因为portia docker内部的 scrapy版本和线上的是不一样的,所以我们直接来看docker 内部的scrapy ,路径如下
1 2 |
/usr/local/lib/python2.7/dist-packages/scrapy |
我们来找找这个SPIDER_MANAGER_CLASS
使用命令
1 2 |
grep -r 'SPIDER_MANAGER_CLASS' |
我们得知是在 crawler.py
中使用的,进一步去查找相关的代码,得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def _get_spider_loader(settings): """ Get SpiderLoader instance from settings """ if settings.get('SPIDER_MANAGER_CLASS'): warnings.warn( 'SPIDER_MANAGER_CLASS option is deprecated. ' 'Please use SPIDER_LOADER_CLASS.', category=ScrapyDeprecationWarning, stacklevel=2 ) cls_path = settings.get('SPIDER_MANAGER_CLASS', settings.get('SPIDER_LOADER_CLASS')) loader_cls = load_object(cls_path) try: verifyClass(ISpiderLoader, loader_cls) except DoesNotImplement: warnings.warn( 'SPIDER_LOADER_CLASS (previously named SPIDER_MANAGER_CLASS) does ' 'not fully implement scrapy.interfaces.ISpiderLoader interface. ' 'Please add all missing methods to avoid unexpected runtime errors.', category=ScrapyDeprecationWarning, stacklevel=2 ) return loader_cls.from_settings(settings.frozencopy()) |
原来本质上就是使用了 SPIDER_LOADER_CLASS
,关于这个键是如何使用的,我们可以反查scrapy的官方github分析https://github.com/scrapy/scrapy/search?utf8=%E2%9C%93&q=SPIDER_LOADER_CLASS&type=
简单来说,当我们每次使用命令的时候,加载配置的动作都有一个预加载的过程,这个加载的过程将我们用户自己写好的方法,逻辑,配置加载到写好的模版里面,这样就可以通过命令来执行我们的爬虫了。
结果
既然已经分析到这一步了,我们直接来看看今天的主角slybot.spidermanager.SlybotSpiderManager
=> https://github.com/scrapinghub/portia/blob/master/slybot/slybot/spidermanager.py
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from zope.interface import implementer from scrapy.interfaces import ISpiderManager from scrapy.utils.misc import load_object from scrapy.utils.project import get_project_settings from slybot.spider import IblSpider from slybot.utils import open_project_from_dir, load_plugins @implementer(ISpiderManager) class SlybotSpiderManager(object): def __init__(self, datadir, spider_cls=None, settings=None, **kwargs): logging.info('Slybot %s Spider', slybot.__version__) if settings is None: settings = get_project_settings() self.spider_cls = load_object(spider_cls) if spider_cls else IblSpider self._specs = open_project_from_dir(datadir) settings = settings.copy() settings.frozen = False settings.set('LOADED_PLUGINS', load_plugins(settings)) self.settings = settings |
在这里,我们看spider_cls
,这里我们直接加载了写好的IblSpider , IblSpider 是直接加载我们的json规则(上篇文章已经分析了相关的规则),然后直接执行,走到这里,我们终于明白原先在项目文件夹下写pipelines.py
为什么没有毛用了,原来本质上还是跑的这个/app/slybot/slybot/spider.py
的IblSpider,所以如果要加载我们自己写好的pipeline 呢,直接在这个项目slybot里面加入我们的pipeline ,修改settings.py 就可以了。
实施
修改pipelines
首先,我们将写好的pipelines.py
放在/app/slybot/slybot/
目录下,然后修改同目录下的settings.py
文件
1 2 3 4 |
ITEM_PIPELINES = {'slybot.dupefilter.DupeFilterPipeline': 1, 'slybot.pipelines.MongoPipeline':2, } |
在ITEM_PIPELINES 节点,加入我们的slybot.pipelines.MongoPipeline
OK ,这一切做好,切回到/app/data/projects
目录,使用命令portiacrawl cnblogs www.cnblogs.com
,我们来看看效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
i got the spider from the spider DEBUG:scrapy.core.scraper:Scraped from <200 https://www.cnblogs.com/> {u'_index': 19, u'_template': 'dd60-46f2-bbea', u'_type': u'content', u'art_url': ['http://www.cnblogs.com/CloudMan6/p/7637361.html'], u'author': [u'CloudMan'], u'comment_count': [u'\u8bc4\u8bba(0)'], u'desc': [u'\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u8ba8\u8bba\u51e0\u4e2a\u76ee\u524d\u6bd4\u8f83\u5e38\u7528\u7684\u5bb9\u5668\u76d1\u63a7\u5de5\u5177\u548c\u65b9\u6848\uff0c\u4e3a\u5927\u5bb6\u6784\u5efa\u81ea\u5df1\u7684\u76d1\u63a7\u7cfb\u7edf\u63d0\u4f9b\u53c2\u8003\u3002 ...'], u'read_count': [u'\u9605\u8bfb(171)'], u'url': 'https://www.cnblogs.com/'} 2017-10-09 03:09:22 [scrapy] DEBUG: Scraped from <200 https://www.cnblogs.com/> {u'_index': 19, u'_template': 'dd60-46f2-bbea', u'_type': u'content', u'art_url': ['http://www.cnblogs.com/CloudMan6/p/7637361.html'], u'author': [u'CloudMan'], u'comment_count': [u'\u8bc4\u8bba(0)'], u'desc': [u'\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u8ba8\u8bba\u51e0\u4e2a\u76ee\u524d\u6bd4\u8f83\u5e38\u7528\u7684\u5bb9\u5668\u76d1\u63a7\u5de5\u5177\u548c\u65b9\u6848\uff0c\u4e3a\u5927\u5bb6\u6784\u5efa\u81ea\u5df1\u7684\u76d1\u63a7\u7cfb\u7edf\u63d0\u4f9b\u53c2\u8003\u3002 ...'], u'read_count': [u'\u9605\u8bfb(171)'], u'url': 'https://www.cnblogs.com/'} i got the spider from the spider DEBUG:scrapy.core.scraper:Scraped from <200 https://www.cnblogs.com/> {u'_index': 20, u'_template': 'dd60-46f2-bbea', u'_type': u'content', u'art_url': ['http://www.cnblogs.com/morang/p/7639033.html'], u'author': [u'\u6613\u58a8'], u'comment_count': [u'\u8bc4\u8bba(2)'], |
i got the spider from the spider
已经正常的打印出来了
安装pymongo
我们在portia docker 里面使用pip 安装pymongo
1 2 |
pip install pymongo==3.2 |
修改pipelines.py
文件内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# -*- coding: utf-8 -*- from pymongo import MongoClient class MongoPipeline(object): def __init__(self): self.client = MongoClient('mongodb://192.168.1.52:27017') self.db = self.client.get_database('portia') self.collection = self.db.get_collection('cnblogs') def process_item(self, item, spider): if item: self.collection.save(dict(item)) return item |
注意:我这里使用的地址是192.168.1.52,因为我局域网内的机器(192.168.1.52)已经起了mongo 这样一个实例,所以可以直接从docker 里面直接访问,如果你要绑定在另外一个docker 数据 库,请参考docker link
的相关语法以及怎么把容器的网络连接在一起
运行
跳转到 /app/data/projects
使用命令
1 2 |
portiacrawl cnblogs www.cnblogs.com |
运行结束,用mongo链接 192.168.1.52的mongo实例,查看内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
> show dbs local 0.000GB portia 0.000GB > use portia switched to db portia > show collections cnblogs > db.cnblogs.count() 20 > db.cnblogs.findOne() { "_id" : ObjectId("59daec586073800080798b7c"), "art_url" : [ "http://www.cnblogs.com/yitouniu/p/7640079.html" ], "_type" : "content", "_index" : 1, "url" : "https://www.cnblogs.com/", "author" : [ "飞·羽" ], "read_count" : [ "阅读(13)" ], "comment_count" : [ "评论(0)" ], "_template" : "dd60-46f2-bbea", "desc" : [ "本文简单举例说明如何使用wsimport和JAX-WS调用WebService接口 ..." ] } > |
OK ,数据已经存入到里面了
总结
本次主要从
- 分析问题
- 猜想问题
- 原理分析
- 大胆实践
这几个步骤着手,拿到一个框架不要慌,源代码都摆在那里了,你还怕啃不下来,多分析,多动手,问题都会解决的,OK
很有启发,谢谢!
感谢
哈哈 受益匪浅
因为portia生成的scrapy爬虫可以用命令行启动
用scrapyd部署之后
由于scrapy的设置支持在命令行传参(优先级也是最高的) 所以可以使用如下命令:
curl http://localhost:6800/schedule.json -d project=XXX -d spider=XXX -d setting=FEED_URI=file:/Users/XXX/out/%\(name\)s/%\(time\)s.json -d setting=FEED_FORMAT=jsonlines -d setting=FEED_EXPORT_ENCODING=utf-8
就能实现用默认jsonlines管道输出到指定目录
受到博主文章的启发 我也畅想一下 是不是可以通过替换portia输出scrapy的模版 将其复用scrapy-redis的组件 从而实现自动部署可视化分布式爬虫呢
666 联想的不错,然后可以使用elk 里面的 logstash 自动收集入库到es 里面,这样也满不错的,scrapy-redis 是驻留模式的,可以考虑将规则也存入到redis 里面,爬虫拿到规则就直接执行。
大神真是字字珠玑
我之前设想是把数据收集起来再集中解析 但这样就没法利用每个丛机的处理能力 就是分布式的wget 没想到还可以把规则存到redis 真是醍醐灌顶
规则需要持久化在一个稳定的数据库里面,比如mysql 里面,不然一当机就规则全消失了,这还涉及到一个规则的同步问题,总之长久的规则需要有一种方式持久化保存起来。
所言极是,昨天说到将解析规则一起存到redis 我设想有两种方案 一种是把url和解析规则捆绑成一条数据 这样虽然可以保持解析规则与url的一致性 但是redis的数据读写量会很大 另一种是单独把解析规则按task_id构建键值存入redis spider每次解析response还是要访问redis 依然会增加redis负荷 而在从机持久化地存储解析规则就需要额外维护一个数据库或者配置文件 redis只需要用来共享待爬队列就可以了
这个方式是可行的,有一些类似配置的爬虫就是采用这种方式来爬取的 ,666