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.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的日誌系統,讓日誌輸出看起來更人性化,而不是更程序員化。
舒適提示:上面的代碼只作參考,可能運行會報錯,很大多是由於目錄處理的緣由,我暫時還不知道怎麼更合理的處理目錄,上級目錄直接+ '../'
好像不太優雅。