scrapy 自定义命令

前言

scrapy有不少的内置命令,可是有时候咱们会想要自定义一些命令,由于写脚本不如敲个命令来的有逼格,也更方便。python

不过scrapy官网并无对自定义命令的文档,有的只是一句话:您也可使用该COMMANDS_MODULE设置添加自定义项目命令 。有关如何实现命令的示例,请参见scrapy / commands中的 Scrapy命令。说白了就是让咱们本身看源码。react

脚本方式启动爬虫

源码等下看,先看一下如何使用在python文件中启动爬虫,而不是使用scrapy crawl XXX,看示例:程序员

import scrapy
from scrapy.crawler import CrawlerProcess

class MySpider(scrapy.Spider):
    # Your spider definition
    ...
process = CrawlerProcess(settings={
    'FEED_FORMAT': 'json',
    'FEED_URI': 'items.json'
})
process.crawl(MySpider)
#process.crawl(MySpider1) 能够运行多个,而且是同时运行的
process.start()

这里 CrawlerProcess的参数是爬虫启动时的配置,应该相似于scrapy crawl XXX -o 后面的参数。json

也可使用CrawlerRunner来实现bootstrap

from twisted.internet import reactor
import scrapy
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging

class MySpider(scrapy.Spider):
    ...

configure_logging({'LOG_FORMAT': '%(levelname)s: %(message)s'})
runner = CrawlerRunner()
d = runner.crawl(MySpider)
d.addBoth(lambda _: reactor.stop()) # 关闭twisted的reactor
# d1 = runner.crawl(MySpider1)
# d1.addBoth(lambda _: reactor.stop())
# 固然也能够这样写:
# runner.crawl(MySpider)
# runner.crawl(MySpider1)
# d = runner.join()
# d.addBoth(lambda _: reactor.stop())
reactor.run()

若是不想同时运行,就像一个一个运行(另外代码中出现的...效果相似于pass):scrapy

from twisted.internet import reactor, defer
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging

class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...

class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...

configure_logging()
runner = CrawlerRunner()

@defer.inlineCallbacks
def crawl():
    yield runner.crawl(MySpider1)
    yield runner.crawl(MySpider2)
    reactor.stop()

crawl()
reactor.run()

自定义命令

crawlall

先看一个用的最多的命令,运行全部爬虫文件crawlall.pyide

# -*- coding: utf-8 -*-
from scrapy.commands import ScrapyCommand

class Command(ScrapyCommand):
    requires_project = True

    def syntax(self):
        return '[options]'

    def short_desc(self):
        return 'Runs all of the spiders'

    def run(self, args, opts):
        spider_list = self.crawler_process.spiders.list()
        for name in spider_list:
            self.crawler_process.crawl(name, **opts.__dict__)
        self.crawler_process.start()

这在百度随便搜一下就出来了,并且基本上代码都不会变。
在settings.py同级目录下建立个commands的文件夹,把crawlall.py放在文件夹下,接着在settings.py中添加COMMANDS_MODULE = "newspider.commands"其中newspider为scrapy的项目名称,而commands就是咱们建立的目录了
接着就能够在命令行使用scrapy crawlall来运行全部爬虫了函数

假设需求

假设如今有个需求:写一个通用爬虫来抓取一些静态网页,数据解析部分能够由其余人来作,可是这里的其余人不懂scrapy是啥,他们只是负责写xpath和正则的。测试

这样的爬虫使用scrapy应该很简单,最开始想到的是给他们一个模板文件叫他们把一些须要修改的内容修改一下,可是实际操做时可能并不顺利,由于即便只须要修改部份内容,‘其余人’ 看到这么多代码也会说我不会啥啥啥,这就致使任务没法进行,并且他们若是不当心动了相关代码也不知道,也很差管理。ui

怎么办呢?能够将模板文件精简,去掉代码部分只留下须要修改的内容字典(固然不同是字典,某种约定的格式就行,字典只是方便管理)。好比:{'标题':['//title/text()',]},这样就看起来很简单,只须要让他们注意一下括号成对和逗号就行。接着咱们只要根据这些来建立爬虫文件就好了,可是新的问题又出现了,他们怎么测试本身的xpath写对了没有?总不能让咱们来测试在给他们来重写吧,这效率也过低了。

终于引出正题了,有两种办法,其一就是上面的自定义脚本,其二就是自定义命令。虽然自定义脚本更简单,但这里为了说明自定义命令怎么使用仍是使用自定义命令吧。

命令效果:根据字典文件来抓取相关内容,能够根据模板文件和字典文件来建立爬虫文件,而后在运行这个爬虫就达到效果了。

这种效果就像scrapy genspider(根据模板建立爬虫)和scrapy runspider(运行爬虫)的结合。因此咱们直接看这两个命令的源码,代码很长就不放上来的,能够本身去本地看文件(若是是anaconda, AnacondaLibsite-packagesscrapycommands
里面)

看完后我发现genspider命令使用的是string.Template这个方法来建立爬虫文件,使用也很简单,这其实就至关于format

import string
a = '$a dadafsfas $b'
d = {'a':1, 'b': 'dsada'}
new_a = string.Template(a).substitute(d)

接着看完runspider的代码,咱们的命令就能够这么写:

import sys
import os
import json
import string
import logging
from importlib import import_module
from scrapy.utils.spider import iter_spider_classes
from scrapy.commands import ScrapyCommand
from scrapy.exceptions import UsageError


logger = logging.getLogger(__name__)

def create_spider(setting_rule, fname):
    d = { 
            'spidername': fname, '标题': setting_rule.get('标题')
        }
    with open('../tempspider.py', 'r', encoding='utf-8') as f:
        tempstr = f.read()
    with open(f'../spiders/{fname}_spider.py', 'w', encoding='utf-8') as fw:
        fw.write(string.Template(tempstr).substitute(d).replace('true', 'True').replace('false', 'False').replace('null', 'None'))

def _import_file(filepath):
    abspath = os.path.abspath(filepath)
    dirname, file = os.path.split(abspath)
    logging.info(dirname) 
    fname, fext = os.path.splitext(file)
    if fext != '.py':
        raise ValueError("Not a Python source file: %s" % abspath)
    if dirname:
        sys.path = [dirname] + sys.path
    try:
        module = import_module(fname)
    except Exception as e:
        logger.error('模板文件可能有语法错误,请检查后重试!(%s)' % str(e))
    else:
        create_spider(module.setting_rule, fname)
        sys.path = [dirname+'/../spiders'] + sys.path
        spider_module = import_module(f'{fname}_spider')
        return spider_module
    finally:
        if dirname:
            sys.path.pop(0)
            sys.path.pop(0)


class Command(ScrapyCommand):

    requires_project = True
    
    def syntax(self):
        return "<spider_file>"

    def short_desc(self):
        return "Run a self-contained spider (without creating a project)"

    
    def run(self, args, opts):
        if len(args) != 1:
            raise UsageError()
        filename = args[0]
        if not os.path.exists(filename):
            raise UsageError("File not found: %s\n" % filename)
        try:
            spider_module = _import_file(filename)
        except (ImportError, ValueError) as e:
            raise UsageError("Unable to load %r: %s\n" % (filename, e))

        spclasses = list(iter_spider_classes(spider_module))
        if not spclasses:
            raise UsageError("No spider found in file: %s\n" % filename)
        spidercls = spclasses.pop()

        self.crawler_process.crawl(spidercls, **opts.__dict__)
        self.crawler_process.start()

        if self.crawler_process.bootstrap_failed:
            self.exitcode = 1

怎么看起来代码这么复杂呢?由于我直接复制的runspider.py的代码,其中包含了太多的异常处理,实际上runspider运行爬虫的核心代码就只有几句:

from importlib import import_module
from scrapy.utils.spider import iter_spider_classes

spider_module = import_module(模块名称) # 导入爬虫模块
# 返回模块中的爬虫类的迭代器,也就是只要爬虫类,去掉一些多余的函数和变量
spclasses = list(iter_spider_classes(spider_module))
spidercls = spclasses.pop() # 由于知道只有一个爬虫类
self.crawler_process.crawl(spidercls, **opts.__dict__)
self.crawler_process.start() # 运行

咱们将最上面的命令代码写入到test.py并放在commands目录下,接着scrapy test 模板字典.py就能够测试写的字典能不能解析出数据了,为了和假设的需求更贴切,咱们还能够改变scrapy的日志系统,让日志输出看起来更人性化,而不是更程序员化。

舒适提示:上面的代码只作参考,可能运行会报错,很大多是由于目录处理的缘由,我暂时还不知道怎么更合理的处理目录,上级目录直接+ '../'好像不太优雅。

相关文章
相关标签/搜索