cmd怎么运行http_Scrapy源码剖析(二)Scrapy是如何运行起来的?
生活随笔
收集整理的這篇文章主要介紹了
cmd怎么运行http_Scrapy源码剖析(二)Scrapy是如何运行起来的?
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
閱讀本文大約需要 15 分鐘。本文章代碼較多,如果手機端閱讀體驗不好,建議先收藏后在 PC 端閱讀。在上篇文章:Scrapy源碼剖析(一)架構概覽,我們主要從整體上了解了 Scrapy 的架構和數據流轉,并沒有深入分析每個模塊。從這篇文章開始,我將帶你詳細剖析 Scrapy 的運行原理。這篇文章,我們先從最基礎的運行入口來講,來看一下 Scrapy 究竟是如何運行起來的。
/usr/local/bin/scrapy使用編輯打開這個文件,你會發現,它其實它就是一個 Python 腳本,而且代碼非常少。import?reimport?sysfrom?scrapy.cmdline?import?executeif?__name__?==?'__main__':
????sys.argv[0]?=?re.sub(r'(-script\.pyw|\.exe)?$',?'',?sys.argv[0])
????sys.exit(execute())安裝好 Scrapy 后,為什么入口點是這里呢?答案就在于 Scrapy 的安裝文件 setup.py中,我們找到這個文件,就會發現在這個文件里,已經聲明好了程序的運行入口處:from?os.path?import?dirname,?joinfrom?setuptools?import?setup,?find_packages
setup(
????name='Scrapy',
????version=version,
????url='http://scrapy.org',
????...
????entry_points={??????#?運行入口在這里:scrapy.cmdline:execute 'console_scripts':?['scrapy?=?scrapy.cmdline:execute']
????},
????classifiers=[
????????...
????],
????install_requires=[
????????...
????],
)我們需要關注的是 entry_points配置,它就是調用 Scrapy 開始的地方,也就是cmdline.py的 execute方法。也就是說,我們在安裝 Scrapy 的過程中,setuptools這個包管理工具,就會把上述代碼生成好并放在可執行路徑下,這樣當我們調用 scrapy命令時,就會調用 Scrapy 模塊下的 cmdline.py的 execute方法。而且在這這里,我們可以學到一個小技巧——如何用 Python 編寫一個可執行文件?其實非常簡單,模仿上面的思路,只需要以下幾步即可完成:編寫一個帶有 main 方法的 Python 模塊(首行必須注明 Python 執行路徑) 去掉.py后綴名 修改權限為可執行(chmod +x 文件名) 直接用文件名就可以執行這個 Python 文件 例如,我們創建一個文件 mycmd,在這個文件中編寫一個 main方法,這個方法編寫我們想要的執行的邏輯,之后執行 chmod +x mycmd把這個文件權限變成可執行,最后通過 ./mycmd就可以執行這段代碼了,而不再需要通過 python 方式就可以執行了,是不是很簡單?
????????argv?=?sys.argv#?---?兼容低版本scrapy.conf.settings的配置?---if?settings?is?None?and?'scrapy.conf'?in?sys.modules:from?scrapy?import?confif?hasattr(conf,?'settings'):
????????????settings?=?conf.settings#?-----------------------------------------#?初始化環境、獲取項目配置參數?返回settings對象if?settings?is?None:
????????settings?=?get_project_settings()#?校驗棄用的配置項
????check_deprecated_settings(settings)#?---?兼容低版本scrapy.conf.settings的配置?---import?warningsfrom?scrapy.exceptions?import?ScrapyDeprecationWarningwith?warnings.catch_warnings():
????????warnings.simplefilter("ignore",?ScrapyDeprecationWarning)from?scrapy?import?conf
????????conf.settings?=?settings#?---------------------------------------#?執行環境是否在項目中?主要檢查scrapy.cfg配置文件是否存在
????inproject?=?inside_project()#?讀取commands文件夾?把所有的命令類轉換為{cmd_name:?cmd_instance}的字典
????cmds?=?_get_commands_dict(settings,?inproject)#?從命令行解析出執行的是哪個命令
????cmdname?=?_pop_command_name(argv)
????parser?=?optparse.OptionParser(formatter=optparse.TitledHelpFormatter(),?\
????????conflict_handler='resolve')if?not?cmdname:
????????_print_commands(settings,?inproject)
????????sys.exit(0)elif?cmdname?not?in?cmds:
????????_print_unknown_command(settings,?cmdname,?inproject)
????????sys.exit(2)#?根據命令名稱找到對應的命令實例
????cmd?=?cmds[cmdname]
????parser.usage?=?"scrapy?%s?%s"?%?(cmdname,?cmd.syntax())
????parser.description?=?cmd.long_desc()#?設置項目配置和級別為command
????settings.setdict(cmd.default_settings,?priority='command')
????cmd.settings?=?settings#?添加解析規則
????cmd.add_options(parser)#?解析命令參數,并交由Scrapy命令實例處理
????opts,?args?=?parser.parse_args(args=argv[1:])
????_run_print_help(parser,?cmd.process_options,?args,?opts)#?初始化CrawlerProcess實例?并給命令實例添加crawler_process屬性
????cmd.crawler_process?=?CrawlerProcess(settings)#?執行命令實例的run方法
????_run_print_help(parser,?_run_command,?cmd,?args,?opts)
????sys.exit(cmd.exitcode)這塊代碼就是 Scrapy 執行的運行入口了,我們根據注釋就能看到,這里的主要工作包括配置初始化、命令解析、爬蟲類加載、運行爬蟲這幾步。了解了整個入口的流程,下面我會對每個步驟進行詳細的分析。
????????project?=?os.environ.get('SCRAPY_PROJECT',?'default')#?初始化環境?找到用戶配置文件settings.py?設置到環境變量SCRAPY_SETTINGS_MODULE中
????????init_env(project)#?加載默認配置文件default_settings.py?生成settings實例
????settings?=?Settings()#?取得用戶配置文件
????settings_module_path?=?os.environ.get(ENVVAR)#?如果有用戶配置?則覆蓋默認配置if?settings_module_path:
????????settings.setmodule(settings_module_path,?priority='project')#?如果環境變量中有其他scrapy相關配置也覆蓋
????pickled_settings?=?os.environ.get("SCRAPY_PICKLED_SETTINGS_TO_OVERRIDE")if?pickled_settings:
????????settings.setdict(pickle.loads(pickled_settings),?priority='project')
????env_overrides?=?{k[7:]:?v?for?k,?v?in?os.environ.items()?if
?????????????????????k.startswith('SCRAPY_')}if?env_overrides:
????????settings.setdict(env_overrides,?priority='project')return?settings在初始配置時,會加載默認的配置文件 default_settings.py,主要邏輯在 Settings類中。class?Settings(BaseSettings):def?__init__(self,?values=None,?priority='project'):#?調用父類構造初始化
????????super(Settings,?self).__init__()#?把default_settings.py的所有配置set到settings實例中
????????self.setmodule(default_settings,?'default')#?把attributes屬性也set到settings實例中for?name,?val?in?six.iteritems(self):if?isinstance(val,?dict):
????????????????self.set(name,?BaseSettings(val,?'default'),?'default')
????????self.update(values,?priority)可以看到,首先把默認配置文件 default_settings.py中的所有配置項設置到 Settings中,而且這個配置是有優先級的。這個默認配置文件 default_settings.py是非常重要的,我們讀源碼時有必要重點關注一下里面的內容,這里包含了所有組件的默認配置,以及每個組件的類模塊,例如調度器類、爬蟲中間件類、下載器中間件類、下載處理器類等等。#?下載器類
DOWNLOADER?=?'scrapy.core.downloader.Downloader'#?調度器類
CHEDULER?=?'scrapy.core.scheduler.Scheduler'#?調度隊列類
SCHEDULER_DISK_QUEUE?=?'scrapy.squeues.PickleLifoDiskQueue'
SCHEDULER_MEMORY_QUEUE?=?'scrapy.squeues.LifoMemoryQueue'
SCHEDULER_PRIORITY_QUEUE?=?'scrapy.pqueues.ScrapyPriorityQueue'有沒有感覺比較奇怪,默認配置中配置了這么多類模塊,這是為什么?這其實是 Scrapy 特性之一,它這么做的好處是:任何模塊都是可替換的。什么意思呢?例如,你覺得默認的調度器功能不夠用,那么你就可以按照它定義的接口標準,自己實現一個調度器,然后在自己的配置文件中,注冊自己的調度器類,那么 Scrapy 運行時就會加載你的調度器執行了,這極大地提高了我們的靈活性!所以,只要在默認配置文件中配置的模塊類,都是可替換的。
????scrapy_module?=?os.environ.get('SCRAPY_SETTINGS_MODULE')if?scrapy_module?is?not?None:try:
????????????import_module(scrapy_module)except?ImportError?as?exc:
????????????warnings.warn("Cannot?import?scrapy?settings?module?%s:?%s"?%?(scrapy_module,?exc))else:return?True#?如果環境變量沒有?就近查找scrapy.cfg?找得到就認為是在項目環境中return?bool(closest_scrapy_cfg())運行環境是否在爬蟲項目中的依據就是能否找到 scrapy.cfg文件,如果能找到,則說明是在爬蟲項目中,否則就認為是執行的全局命令。
????cmds?=?_get_commands_from_module('scrapy.commands',?inproject)
????cmds.update(_get_commands_from_entry_points(inproject))#?如果用戶自定義配置文件中有COMMANDS_MODULE配置?則加載自定義的命令類
????cmds_module?=?settings['COMMANDS_MODULE']if?cmds_module:
????????cmds.update(_get_commands_from_module(cmds_module,?inproject))return?cmdsdef?_get_commands_from_module(module,?inproject):
????d?=?{}#?找到這個模塊下所有的命令類(ScrapyCommand子類)for?cmd?in?_iter_command_classes(module):if?inproject?or?not?cmd.requires_project:#?生成{cmd_name:?cmd}字典
????????????cmdname?=?cmd.__module__.split('.')[-1]
????????????d[cmdname]?=?cmd()return?ddef?_iter_command_classes(module_name):#?迭代這個包下的所有模塊?找到ScrapyCommand的子類for?module?in?walk_modules(module_name):for?obj?in?vars(module).values():if?inspect.isclass(obj)?and?\
????????????????????issubclass(obj,?ScrapyCommand)?and?\
????????????????????obj.__module__?==?module.__name__:yield?obj這個過程主要是,導入 commands文件夾下的所有模塊,最終生成一個 {cmd_name: cmd}字典集合,如果用戶在配置文件中也配置了自定義的命令類,也會追加進去。也就是說,我們自己也可以編寫自己的命令類,然后追加到配置文件中,之后就可以使用自己定義的命令了。
????i?=?0for?arg?in?argv[1:]:if?not?arg.startswith('-'):del?argv[i]return?arg
????????i?+=?1這個過程就是解析命令行,例如執行 scrapy crawl ,這個方法會解析出 crawl,通過上面生成好的命令類的字典集合,就能找到 commands目錄下的 crawl.py文件,最終執行的就是它的 Command類。
????ScrapyCommand.process_options(self,?args,?opts)try:#?命令行參數轉為字典
????????opts.spargs?=?arglist_to_dict(opts.spargs)except?ValueError:raise?UsageError("Invalid?-a?value,?use?-a?NAME=VALUE",?print_help=False)if?opts.output:if?opts.output?==?'-':
????????????self.settings.set('FEED_URI',?'stdout:',?priority='cmdline')else:
????????????self.settings.set('FEED_URI',?opts.output,?priority='cmdline')
????????feed_exporters?=?without_none_values(
????????????self.settings.getwithbase('FEED_EXPORTERS'))
????????valid_output_formats?=?feed_exporters.keys()if?not?opts.output_format:
????????????opts.output_format?=?os.path.splitext(opts.output)[1].replace(".",?"")if?opts.output_format?not?in?valid_output_formats:raise?UsageError("Unrecognized?output?format?'%s',?set?one""?using?the?'-t'?switch?or?as?a?file?extension""?from?the?supported?list?%s"?%?(opts.output_format,
????????????????????????????????????????????????????????????????tuple(valid_output_formats)))
????????self.settings.set('FEED_FORMAT',?opts.output_format,?priority='cmdline')這個過程就是解析命令行其余的參數,固定參數解析交給父類處理,例如輸出位置等。其余不同的參數由不同的命令類解析。
_run_print_help(parser,?_run_command,?cmd,?args,?opts)我們開始運行一個爬蟲一般使用的是 scrapy crawl ,也就是說最終調用的是 commands/crawl.py的 run方法:def?run(self,?args,?opts):if?len(args)?1:raise?UsageError()elif?len(args)?>?1:raise?UsageError("running?'scrapy?crawl'?with?more?than?one?spider?is?no?longer?supported")
????spname?=?args[0]
????self.crawler_process.crawl(spname,?**opts.spargs)
????self.crawler_process.start()run方法中調用了 CrawlerProcess實例的 crawl和 start方法,就這樣整個爬蟲程序就會運行起來了。我們先來看CrawlerProcess初始化:class?CrawlerProcess(CrawlerRunner):def?__init__(self,?settings=None):#?調用父類初始化
????????super(CrawlerProcess,?self).__init__(settings)#?信號和log初始化
????????install_shutdown_handlers(self._signal_shutdown)
????????configure_logging(self.settings)
????????log_scrapy_info(self.settings)其中,構造方法中調用了父類 CrawlerRunner的構造方法:class?CrawlerRunner(object):def?__init__(self,?settings=None):if?isinstance(settings,?dict)?or?settings?is?None:
????????????settings?=?Settings(settings)
????????self.settings?=?settings#?獲取爬蟲加載器
????????self.spider_loader?=?_get_spider_loader(settings)
????????self._crawlers?=?set()
????????self._active?=?set()初始化時,調用了 _get_spider_loader方法:def?_get_spider_loader(settings):#?讀取配置文件中的SPIDER_MANAGER_CLASS配置項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項,默認配置是 spiderloader.SpiderLoader類,從名字我們也能看出來,這個類是用來加載我們編寫好的爬蟲類的,下面看一下這個類的具體實現。@implementer(ISpiderLoader)class?SpiderLoader(object):def?__init__(self,?settings):#?配置文件獲取存放爬蟲腳本的路徑
????????self.spider_modules?=?settings.getlist('SPIDER_MODULES')
????????self._spiders?=?{}#?加載所有爬蟲
????????self._load_all_spiders()def?_load_spiders(self,?module):#?組裝成{spider_name:?spider_cls}的字典for?spcls?in?iter_spider_classes(module):
????????????self._spiders[spcls.name]?=?spclsdef?_load_all_spiders(self):for?name?in?self.spider_modules:for?module?in?walk_modules(name):
????????????????self._load_spiders(module)可以看到,在這里爬蟲加載器會加載所有的爬蟲腳本,最后生成一個 {spider_name: spider_cls}的字典,所以我們在執行 scarpy crawl 時,Scrapy 就能找到我們的爬蟲類。
????crawler?=?self.create_crawler(crawler_or_spidercls)return?self._crawl(crawler,?*args,?**kwargs)def?_crawl(self,?crawler,?*args,?**kwargs):
????self.crawlers.add(crawler)#?調用Crawler的crawl方法
????d?=?crawler.crawl(*args,?**kwargs)
????self._active.add(d)def?_done(result):
????????self.crawlers.discard(crawler)
????????self._active.discard(d)return?resultreturn?d.addBoth(_done)def?create_crawler(self,?crawler_or_spidercls):if?isinstance(crawler_or_spidercls,?Crawler):return?crawler_or_spiderclsreturn?self._create_crawler(crawler_or_spidercls)def?_create_crawler(self,?spidercls):#?如果是字符串?則從spider_loader中加載這個爬蟲類if?isinstance(spidercls,?six.string_types):
????????spidercls?=?self.spider_loader.load(spidercls)#?否則創建Crawlerreturn?Crawler(spidercls,?self.settings)這個過程會創建 Cralwer實例,然后調用它的 crawl方法:@defer.inlineCallbacksdef?crawl(self,?*args,?**kwargs):assert?not?self.crawling,?"Crawling?already?taking?place"
????self.crawling?=?Truetry:#?到現在?才是實例化一個爬蟲實例
????????self.spider?=?self._create_spider(*args,?**kwargs)#?創建引擎
????????self.engine?=?self._create_engine()#?調用爬蟲類的start_requests方法
????????start_requests?=?iter(self.spider.start_requests())#?執行引擎的open_spider?并傳入爬蟲實例和初始請求yield?self.engine.open_spider(self.spider,?start_requests)yield?defer.maybeDeferred(self.engine.start)except?Exception:if?six.PY2:
????????????exc_info?=?sys.exc_info()
????????self.crawling?=?Falseif?self.engine?is?not?None:yield?self.engine.close()if?six.PY2:
????????????six.reraise(*exc_info)raisedef?_create_spider(self,?*args,?**kwargs):return?self.spidercls.from_crawler(self,?*args,?**kwargs)到這里,才會對我們的爬蟲類創建一個實例對象,然后創建引擎,之后調用爬蟲類的 start_requests方法獲取種子 URL,最后交給引擎執行。最后來看 Cralwer是如何開始運行的額,也就是它的 start方法:def?start(self,?stop_after_crawl=True):if?stop_after_crawl:
????????d?=?self.join()if?d.called:return
????????d.addBoth(self._stop_reactor)
????reactor.installResolver(self._get_dns_resolver())#?配置reactor的池子大小(可修改REACTOR_THREADPOOL_MAXSIZE調整)
????tp?=?reactor.getThreadPool()
????tp.adjustPoolsize(maxthreads=self.settings.getint('REACTOR_THREADPOOL_MAXSIZE'))
????reactor.addSystemEventTrigger('before',?'shutdown',?self.stop)#?開始執行
????reactor.run(installSignalHandlers=False)在這里有一個叫做 reactor的模塊。reactor是個什么東西呢?它是 Twisted模塊的事件管理器,我們只要把需要執行的事件注冊到 reactor中,然后調用它的 run方法,它就會幫我們執行注冊好的事件,如果遇到網絡IO等待,它會自動幫切換到可執行的事件上,非常高效。在這里我們不用深究 reactor是如何工作的,你可以把它想象成一個線程池,只是采用注冊回調的方式來執行事件。到這里,Scrapy 運行的入口就分析完了,之后爬蟲的調度邏輯就交由引擎 ExecuteEngine處理了,引擎會協調多個組件,相互配合完成整個任務的執行。
長按關注「水滴與銀彈」公眾號,7年資深后端研發,和你分享更多優質技術干貨。
scrapy 命令從哪來?
當我們基于 Scrapy 寫好一個爬蟲后,想要把我們的爬蟲運行起來,怎么做?非常簡單,只需要執行以下命令就可以了。?scrapy?crawl?通過這個命令,我們的爬蟲就真正開始工作了。那么從命令行到執行爬蟲邏輯,這個過程中到底發生了什么?在開始之前,不知道你有沒有和我一樣的疑惑,我們執行的 scrapy命令從何而來?實際上,當你成功安裝好 Scrapy 后,使用如下命令,就能找到這個命令文件,這個文件就是 Scrapy 的運行入口:$?which?scrapy/usr/local/bin/scrapy使用編輯打開這個文件,你會發現,它其實它就是一個 Python 腳本,而且代碼非常少。import?reimport?sysfrom?scrapy.cmdline?import?executeif?__name__?==?'__main__':
????sys.argv[0]?=?re.sub(r'(-script\.pyw|\.exe)?$',?'',?sys.argv[0])
????sys.exit(execute())安裝好 Scrapy 后,為什么入口點是這里呢?答案就在于 Scrapy 的安裝文件 setup.py中,我們找到這個文件,就會發現在這個文件里,已經聲明好了程序的運行入口處:from?os.path?import?dirname,?joinfrom?setuptools?import?setup,?find_packages
setup(
????name='Scrapy',
????version=version,
????url='http://scrapy.org',
????...
????entry_points={??????#?運行入口在這里:scrapy.cmdline:execute 'console_scripts':?['scrapy?=?scrapy.cmdline:execute']
????},
????classifiers=[
????????...
????],
????install_requires=[
????????...
????],
)我們需要關注的是 entry_points配置,它就是調用 Scrapy 開始的地方,也就是cmdline.py的 execute方法。也就是說,我們在安裝 Scrapy 的過程中,setuptools這個包管理工具,就會把上述代碼生成好并放在可執行路徑下,這樣當我們調用 scrapy命令時,就會調用 Scrapy 模塊下的 cmdline.py的 execute方法。而且在這這里,我們可以學到一個小技巧——如何用 Python 編寫一個可執行文件?其實非常簡單,模仿上面的思路,只需要以下幾步即可完成:
運行入口(execute.py)
現在,我們已經知道了 Scrapy 的運行入口是 scrapy/cmdline.py的 execute方法,那我們就看一下這個方法。def?execute(argv=None,?settings=None):if?argv?is?None:????????argv?=?sys.argv#?---?兼容低版本scrapy.conf.settings的配置?---if?settings?is?None?and?'scrapy.conf'?in?sys.modules:from?scrapy?import?confif?hasattr(conf,?'settings'):
????????????settings?=?conf.settings#?-----------------------------------------#?初始化環境、獲取項目配置參數?返回settings對象if?settings?is?None:
????????settings?=?get_project_settings()#?校驗棄用的配置項
????check_deprecated_settings(settings)#?---?兼容低版本scrapy.conf.settings的配置?---import?warningsfrom?scrapy.exceptions?import?ScrapyDeprecationWarningwith?warnings.catch_warnings():
????????warnings.simplefilter("ignore",?ScrapyDeprecationWarning)from?scrapy?import?conf
????????conf.settings?=?settings#?---------------------------------------#?執行環境是否在項目中?主要檢查scrapy.cfg配置文件是否存在
????inproject?=?inside_project()#?讀取commands文件夾?把所有的命令類轉換為{cmd_name:?cmd_instance}的字典
????cmds?=?_get_commands_dict(settings,?inproject)#?從命令行解析出執行的是哪個命令
????cmdname?=?_pop_command_name(argv)
????parser?=?optparse.OptionParser(formatter=optparse.TitledHelpFormatter(),?\
????????conflict_handler='resolve')if?not?cmdname:
????????_print_commands(settings,?inproject)
????????sys.exit(0)elif?cmdname?not?in?cmds:
????????_print_unknown_command(settings,?cmdname,?inproject)
????????sys.exit(2)#?根據命令名稱找到對應的命令實例
????cmd?=?cmds[cmdname]
????parser.usage?=?"scrapy?%s?%s"?%?(cmdname,?cmd.syntax())
????parser.description?=?cmd.long_desc()#?設置項目配置和級別為command
????settings.setdict(cmd.default_settings,?priority='command')
????cmd.settings?=?settings#?添加解析規則
????cmd.add_options(parser)#?解析命令參數,并交由Scrapy命令實例處理
????opts,?args?=?parser.parse_args(args=argv[1:])
????_run_print_help(parser,?cmd.process_options,?args,?opts)#?初始化CrawlerProcess實例?并給命令實例添加crawler_process屬性
????cmd.crawler_process?=?CrawlerProcess(settings)#?執行命令實例的run方法
????_run_print_help(parser,?_run_command,?cmd,?args,?opts)
????sys.exit(cmd.exitcode)這塊代碼就是 Scrapy 執行的運行入口了,我們根據注釋就能看到,這里的主要工作包括配置初始化、命令解析、爬蟲類加載、運行爬蟲這幾步。了解了整個入口的流程,下面我會對每個步驟進行詳細的分析。
初始化項目配置
首先第一步,根據環境初始化配置,在這里有一些兼容低版本 Scrapy 配置的代碼,我們忽略就好。我們重點來看配置是如何初始化的。這主要和環境變量和 scrapy.cfg有關,通過調用 ?get_project_settings方法,最終生成一個 Settings實例。def?get_project_settings():#?環境變量中是否有SCRAPY_SETTINGS_MODULE配置if?ENVVAR?not?in?os.environ:????????project?=?os.environ.get('SCRAPY_PROJECT',?'default')#?初始化環境?找到用戶配置文件settings.py?設置到環境變量SCRAPY_SETTINGS_MODULE中
????????init_env(project)#?加載默認配置文件default_settings.py?生成settings實例
????settings?=?Settings()#?取得用戶配置文件
????settings_module_path?=?os.environ.get(ENVVAR)#?如果有用戶配置?則覆蓋默認配置if?settings_module_path:
????????settings.setmodule(settings_module_path,?priority='project')#?如果環境變量中有其他scrapy相關配置也覆蓋
????pickled_settings?=?os.environ.get("SCRAPY_PICKLED_SETTINGS_TO_OVERRIDE")if?pickled_settings:
????????settings.setdict(pickle.loads(pickled_settings),?priority='project')
????env_overrides?=?{k[7:]:?v?for?k,?v?in?os.environ.items()?if
?????????????????????k.startswith('SCRAPY_')}if?env_overrides:
????????settings.setdict(env_overrides,?priority='project')return?settings在初始配置時,會加載默認的配置文件 default_settings.py,主要邏輯在 Settings類中。class?Settings(BaseSettings):def?__init__(self,?values=None,?priority='project'):#?調用父類構造初始化
????????super(Settings,?self).__init__()#?把default_settings.py的所有配置set到settings實例中
????????self.setmodule(default_settings,?'default')#?把attributes屬性也set到settings實例中for?name,?val?in?six.iteritems(self):if?isinstance(val,?dict):
????????????????self.set(name,?BaseSettings(val,?'default'),?'default')
????????self.update(values,?priority)可以看到,首先把默認配置文件 default_settings.py中的所有配置項設置到 Settings中,而且這個配置是有優先級的。這個默認配置文件 default_settings.py是非常重要的,我們讀源碼時有必要重點關注一下里面的內容,這里包含了所有組件的默認配置,以及每個組件的類模塊,例如調度器類、爬蟲中間件類、下載器中間件類、下載處理器類等等。#?下載器類
DOWNLOADER?=?'scrapy.core.downloader.Downloader'#?調度器類
CHEDULER?=?'scrapy.core.scheduler.Scheduler'#?調度隊列類
SCHEDULER_DISK_QUEUE?=?'scrapy.squeues.PickleLifoDiskQueue'
SCHEDULER_MEMORY_QUEUE?=?'scrapy.squeues.LifoMemoryQueue'
SCHEDULER_PRIORITY_QUEUE?=?'scrapy.pqueues.ScrapyPriorityQueue'有沒有感覺比較奇怪,默認配置中配置了這么多類模塊,這是為什么?這其實是 Scrapy 特性之一,它這么做的好處是:任何模塊都是可替換的。什么意思呢?例如,你覺得默認的調度器功能不夠用,那么你就可以按照它定義的接口標準,自己實現一個調度器,然后在自己的配置文件中,注冊自己的調度器類,那么 Scrapy 運行時就會加載你的調度器執行了,這極大地提高了我們的靈活性!所以,只要在默認配置文件中配置的模塊類,都是可替換的。
檢查運行環境是否在項目中
初始化完配置之后,下面一步是檢查運行環境是否在爬蟲項目中。我們知道,scrapy命令有的是依賴項目運行的,有的命令則是全局的。這里主要通過就近查找 scrapy.cfg文件來確定是否在項目環境中,主要邏輯在 inside_project方法中。def?inside_project():#?檢查此環境變量是否存在(上面已設置)????scrapy_module?=?os.environ.get('SCRAPY_SETTINGS_MODULE')if?scrapy_module?is?not?None:try:
????????????import_module(scrapy_module)except?ImportError?as?exc:
????????????warnings.warn("Cannot?import?scrapy?settings?module?%s:?%s"?%?(scrapy_module,?exc))else:return?True#?如果環境變量沒有?就近查找scrapy.cfg?找得到就認為是在項目環境中return?bool(closest_scrapy_cfg())運行環境是否在爬蟲項目中的依據就是能否找到 scrapy.cfg文件,如果能找到,則說明是在爬蟲項目中,否則就認為是執行的全局命令。
組裝命令實例集合
再向下看,就到了加載命令的邏輯了。我們知道 scrapy包括很多命令,例如 scrapy crawl、 scrapy fetch等等,那這些命令是從哪來的?答案就在 _get_commands_dict方法中。def?_get_commands_dict(settings,?inproject):#?導入commands文件夾下的所有模塊?生成{cmd_name:?cmd}的字典集合????cmds?=?_get_commands_from_module('scrapy.commands',?inproject)
????cmds.update(_get_commands_from_entry_points(inproject))#?如果用戶自定義配置文件中有COMMANDS_MODULE配置?則加載自定義的命令類
????cmds_module?=?settings['COMMANDS_MODULE']if?cmds_module:
????????cmds.update(_get_commands_from_module(cmds_module,?inproject))return?cmdsdef?_get_commands_from_module(module,?inproject):
????d?=?{}#?找到這個模塊下所有的命令類(ScrapyCommand子類)for?cmd?in?_iter_command_classes(module):if?inproject?or?not?cmd.requires_project:#?生成{cmd_name:?cmd}字典
????????????cmdname?=?cmd.__module__.split('.')[-1]
????????????d[cmdname]?=?cmd()return?ddef?_iter_command_classes(module_name):#?迭代這個包下的所有模塊?找到ScrapyCommand的子類for?module?in?walk_modules(module_name):for?obj?in?vars(module).values():if?inspect.isclass(obj)?and?\
????????????????????issubclass(obj,?ScrapyCommand)?and?\
????????????????????obj.__module__?==?module.__name__:yield?obj這個過程主要是,導入 commands文件夾下的所有模塊,最終生成一個 {cmd_name: cmd}字典集合,如果用戶在配置文件中也配置了自定義的命令類,也會追加進去。也就是說,我們自己也可以編寫自己的命令類,然后追加到配置文件中,之后就可以使用自己定義的命令了。
解析命令
加載好命令類后,就開始解析我們具體執行的哪個命令了,解析邏輯比較簡單:def?_pop_command_name(argv):????i?=?0for?arg?in?argv[1:]:if?not?arg.startswith('-'):del?argv[i]return?arg
????????i?+=?1這個過程就是解析命令行,例如執行 scrapy crawl ,這個方法會解析出 crawl,通過上面生成好的命令類的字典集合,就能找到 commands目錄下的 crawl.py文件,最終執行的就是它的 Command類。
解析命令行參數
找到對應的命令實例后,調用 cmd.process_options方法解析我們的參數:def?process_options(self,?args,?opts):#?首先調用了父類的process_options?解析統一固定的參數????ScrapyCommand.process_options(self,?args,?opts)try:#?命令行參數轉為字典
????????opts.spargs?=?arglist_to_dict(opts.spargs)except?ValueError:raise?UsageError("Invalid?-a?value,?use?-a?NAME=VALUE",?print_help=False)if?opts.output:if?opts.output?==?'-':
????????????self.settings.set('FEED_URI',?'stdout:',?priority='cmdline')else:
????????????self.settings.set('FEED_URI',?opts.output,?priority='cmdline')
????????feed_exporters?=?without_none_values(
????????????self.settings.getwithbase('FEED_EXPORTERS'))
????????valid_output_formats?=?feed_exporters.keys()if?not?opts.output_format:
????????????opts.output_format?=?os.path.splitext(opts.output)[1].replace(".",?"")if?opts.output_format?not?in?valid_output_formats:raise?UsageError("Unrecognized?output?format?'%s',?set?one""?using?the?'-t'?switch?or?as?a?file?extension""?from?the?supported?list?%s"?%?(opts.output_format,
????????????????????????????????????????????????????????????????tuple(valid_output_formats)))
????????self.settings.set('FEED_FORMAT',?opts.output_format,?priority='cmdline')這個過程就是解析命令行其余的參數,固定參數解析交給父類處理,例如輸出位置等。其余不同的參數由不同的命令類解析。
初始化CrawlerProcess
一切準備就緒,最后初始化 CrawlerProcess實例,然后運行對應命令實例的 run方法。cmd.crawler_process?=?CrawlerProcess(settings)_run_print_help(parser,?_run_command,?cmd,?args,?opts)我們開始運行一個爬蟲一般使用的是 scrapy crawl ,也就是說最終調用的是 commands/crawl.py的 run方法:def?run(self,?args,?opts):if?len(args)?1:raise?UsageError()elif?len(args)?>?1:raise?UsageError("running?'scrapy?crawl'?with?more?than?one?spider?is?no?longer?supported")
????spname?=?args[0]
????self.crawler_process.crawl(spname,?**opts.spargs)
????self.crawler_process.start()run方法中調用了 CrawlerProcess實例的 crawl和 start方法,就這樣整個爬蟲程序就會運行起來了。我們先來看CrawlerProcess初始化:class?CrawlerProcess(CrawlerRunner):def?__init__(self,?settings=None):#?調用父類初始化
????????super(CrawlerProcess,?self).__init__(settings)#?信號和log初始化
????????install_shutdown_handlers(self._signal_shutdown)
????????configure_logging(self.settings)
????????log_scrapy_info(self.settings)其中,構造方法中調用了父類 CrawlerRunner的構造方法:class?CrawlerRunner(object):def?__init__(self,?settings=None):if?isinstance(settings,?dict)?or?settings?is?None:
????????????settings?=?Settings(settings)
????????self.settings?=?settings#?獲取爬蟲加載器
????????self.spider_loader?=?_get_spider_loader(settings)
????????self._crawlers?=?set()
????????self._active?=?set()初始化時,調用了 _get_spider_loader方法:def?_get_spider_loader(settings):#?讀取配置文件中的SPIDER_MANAGER_CLASS配置項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項,默認配置是 spiderloader.SpiderLoader類,從名字我們也能看出來,這個類是用來加載我們編寫好的爬蟲類的,下面看一下這個類的具體實現。@implementer(ISpiderLoader)class?SpiderLoader(object):def?__init__(self,?settings):#?配置文件獲取存放爬蟲腳本的路徑
????????self.spider_modules?=?settings.getlist('SPIDER_MODULES')
????????self._spiders?=?{}#?加載所有爬蟲
????????self._load_all_spiders()def?_load_spiders(self,?module):#?組裝成{spider_name:?spider_cls}的字典for?spcls?in?iter_spider_classes(module):
????????????self._spiders[spcls.name]?=?spclsdef?_load_all_spiders(self):for?name?in?self.spider_modules:for?module?in?walk_modules(name):
????????????????self._load_spiders(module)可以看到,在這里爬蟲加載器會加載所有的爬蟲腳本,最后生成一個 {spider_name: spider_cls}的字典,所以我們在執行 scarpy crawl 時,Scrapy 就能找到我們的爬蟲類。
運行爬蟲
CrawlerProcess初始化完之后,調用它的 crawl方法:def?crawl(self,?crawler_or_spidercls,?*args,?**kwargs):#?創建crawler????crawler?=?self.create_crawler(crawler_or_spidercls)return?self._crawl(crawler,?*args,?**kwargs)def?_crawl(self,?crawler,?*args,?**kwargs):
????self.crawlers.add(crawler)#?調用Crawler的crawl方法
????d?=?crawler.crawl(*args,?**kwargs)
????self._active.add(d)def?_done(result):
????????self.crawlers.discard(crawler)
????????self._active.discard(d)return?resultreturn?d.addBoth(_done)def?create_crawler(self,?crawler_or_spidercls):if?isinstance(crawler_or_spidercls,?Crawler):return?crawler_or_spiderclsreturn?self._create_crawler(crawler_or_spidercls)def?_create_crawler(self,?spidercls):#?如果是字符串?則從spider_loader中加載這個爬蟲類if?isinstance(spidercls,?six.string_types):
????????spidercls?=?self.spider_loader.load(spidercls)#?否則創建Crawlerreturn?Crawler(spidercls,?self.settings)這個過程會創建 Cralwer實例,然后調用它的 crawl方法:@defer.inlineCallbacksdef?crawl(self,?*args,?**kwargs):assert?not?self.crawling,?"Crawling?already?taking?place"
????self.crawling?=?Truetry:#?到現在?才是實例化一個爬蟲實例
????????self.spider?=?self._create_spider(*args,?**kwargs)#?創建引擎
????????self.engine?=?self._create_engine()#?調用爬蟲類的start_requests方法
????????start_requests?=?iter(self.spider.start_requests())#?執行引擎的open_spider?并傳入爬蟲實例和初始請求yield?self.engine.open_spider(self.spider,?start_requests)yield?defer.maybeDeferred(self.engine.start)except?Exception:if?six.PY2:
????????????exc_info?=?sys.exc_info()
????????self.crawling?=?Falseif?self.engine?is?not?None:yield?self.engine.close()if?six.PY2:
????????????six.reraise(*exc_info)raisedef?_create_spider(self,?*args,?**kwargs):return?self.spidercls.from_crawler(self,?*args,?**kwargs)到這里,才會對我們的爬蟲類創建一個實例對象,然后創建引擎,之后調用爬蟲類的 start_requests方法獲取種子 URL,最后交給引擎執行。最后來看 Cralwer是如何開始運行的額,也就是它的 start方法:def?start(self,?stop_after_crawl=True):if?stop_after_crawl:
????????d?=?self.join()if?d.called:return
????????d.addBoth(self._stop_reactor)
????reactor.installResolver(self._get_dns_resolver())#?配置reactor的池子大小(可修改REACTOR_THREADPOOL_MAXSIZE調整)
????tp?=?reactor.getThreadPool()
????tp.adjustPoolsize(maxthreads=self.settings.getint('REACTOR_THREADPOOL_MAXSIZE'))
????reactor.addSystemEventTrigger('before',?'shutdown',?self.stop)#?開始執行
????reactor.run(installSignalHandlers=False)在這里有一個叫做 reactor的模塊。reactor是個什么東西呢?它是 Twisted模塊的事件管理器,我們只要把需要執行的事件注冊到 reactor中,然后調用它的 run方法,它就會幫我們執行注冊好的事件,如果遇到網絡IO等待,它會自動幫切換到可執行的事件上,非常高效。在這里我們不用深究 reactor是如何工作的,你可以把它想象成一個線程池,只是采用注冊回調的方式來執行事件。到這里,Scrapy 運行的入口就分析完了,之后爬蟲的調度邏輯就交由引擎 ExecuteEngine處理了,引擎會協調多個組件,相互配合完成整個任務的執行。
總結
總結一下,Scrapy 在真正運行前,需要做的工作包括配置環境初始化、命令類的加載、爬蟲模塊的加載,以及命令類和參數解析,之后運行我們的爬蟲類,最終,這個爬蟲類的調度交給引擎處理。這里我把整個流程也總結成了思維導圖,方便你理解:好了,Scrapy 是如何運行的代碼剖析就先分析到這里,下篇文章我們會深入剖析各個核心組件,分析它們都是負責做什么工作的,以及它們之間又是如何協調完成抓取任務的,敬請期待。近期文章推薦:Scrapy源碼剖析(一)架構概覽如何構建一個通用的垂直爬蟲平臺?如何搭建一個爬蟲代理服務?長按關注「水滴與銀彈」公眾號,7年資深后端研發,和你分享更多優質技術干貨。
總結
以上是生活随笔為你收集整理的cmd怎么运行http_Scrapy源码剖析(二)Scrapy是如何运行起来的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为主题包hwt下载_华为主题 | 星黛
- 下一篇: 农村小本创业项目有哪些 简单给大家介绍几