scrapy 自定义命令
前言
scrapy有很多的内置命令,但是有时候我们会想要自定义一些命令,因为写脚本不如敲个命令来的有逼格,也更方便。
不过scrapy官网并没有对自定义命令的文档,有的只是一句话:您也可以使用该COMMANDS_MODULE设置添加自定义项目命令 。有关如何实现命令的示例,请参见scrapy / commands中的 Scrapy命令。说白了就是让我们自己看源码。
脚本方式启动爬虫
源码等下看,先看一下如何使用在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
后面的参数。
也可以使用CrawlerRunner来实现
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):
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.py
# -*- 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应该很简单,最开始想到的是给他们一个模板文件叫他们把一些需要修改的内容修改一下,但是实际操作时可能并不顺利,因为即使只需要修改部分内容,‘其他人’ 看到这么多代码也会说我不会啥啥啥,这就导致任务无法进行,而且他们如果不小心动了相关代码也不知道,也不好管理。
怎么办呢?可以将模板文件精简,去掉代码部分只留下需要修改的内容字典(当然不一样是字典,某种约定的格式就行,字典只是方便管理)。比如:{'标题':['//title/text()',]},这样就看起来很简单,只需要让他们注意一下括号成对和逗号就行。接着我们只要根据这些来创建爬虫文件就行了,但是新的问题又出现了,他们怎么测试自己的xpath写对了没有?总不能让我们来测试在给他们来重写吧,这效率也太低了。
终于引出正题了,有两种办法,其一就是上面的自定义脚本,其二就是自定义命令。虽然自定义脚本更简单,但这里为了说明自定义命令怎么使用还是使用自定义命令吧。
命令效果:根据字典文件来抓取相关内容,可以根据模板文件和字典文件来创建爬虫文件,然后在运行这个爬虫就达到效果了。
这种效果就像scrapy genspider
(根据模板创建爬虫)和scrapy runspider
(运行爬虫)的结合。所以我们直接看这两个命令的源码,代码很长就不放上来的,可以自己去本地看文件(如果是anaconda, Anaconda\Lib\site-packages\scrapy\commands
里面)
看完后我发现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的日志系统,让日志输出看起来更人性化,而不是更程序员化。
温馨提示:上面的代码只做参考,可能运行会报错,很大可能是因为目录处理的原因,我暂时还不知道怎么更合理的处理目录,上级目录直接+ '../'
好像不太优雅。