<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Scrapy源碼剖析:Scrapy是如何運(yùn)行起來的?

          共 15806字,需瀏覽 32分鐘

           ·

          2021-02-15 08:54

          閱讀本文大約需要 15 分鐘。
          本文章代碼較多,如果手機(jī)端閱讀體驗(yàn)不好,建議先收藏后在 PC 端閱讀。
          在上篇文章:Scrapy 源碼剖析:架構(gòu)概覽,我們主要從整體上了解了 Scrapy 的架構(gòu)和數(shù)據(jù)流轉(zhuǎn),并沒有深入分析每個(gè)模塊。從這篇文章開始,我將帶你詳細(xì)剖析 Scrapy 的運(yùn)行原理。
          這篇文章,我們先從最基礎(chǔ)的運(yùn)行入口來講,來看一下 Scrapy 究竟是如何運(yùn)行起來的。

          scrapy 命令從哪來?

          當(dāng)我們基于 Scrapy 寫好一個(gè)爬蟲后,想要把我們的爬蟲運(yùn)行起來,怎么做?非常簡單,只需要執(zhí)行以下命令就可以了。
          ?scrapy?crawl?
          通過這個(gè)命令,我們的爬蟲就真正開始工作了。那么從命令行到執(zhí)行爬蟲邏輯,這個(gè)過程中到底發(fā)生了什么?
          在開始之前,不知道你有沒有和我一樣的疑惑,我們執(zhí)行的?scrapy?命令從何而來?
          實(shí)際上,當(dāng)你成功安裝好 Scrapy 后,使用如下命令,就能找到這個(gè)命令文件,這個(gè)文件就是 Scrapy 的運(yùn)行入口:
          $?which?scrapy
          /usr/local/bin/scrapy
          使用編輯打開這個(gè)文件,你會(huì)發(fā)現(xiàn),它其實(shí)它就是一個(gè) Python 腳本,而且代碼非常少。
          import?re
          import?sys

          from?scrapy.cmdline?import?execute

          if?__name__?==?'__main__':
          ????sys.argv[0]?=?re.sub(r'(-script\.pyw|\.exe)?$',?'',?sys.argv[0])
          ????sys.exit(execute())
          安裝好 Scrapy 后,為什么入口點(diǎn)是這里呢?
          答案就在于 Scrapy 的安裝文件?setup.py?中,我們找到這個(gè)文件,就會(huì)發(fā)現(xiàn)在這個(gè)文件里,已經(jīng)聲明好了程序的運(yùn)行入口處:
          from?os.path?import?dirname,?join
          from?setuptools?import?setup,?find_packages

          setup(
          ????name='Scrapy',
          ????version=version,
          ????url='http://scrapy.org',
          ????...
          ????entry_points={??????#?運(yùn)行入口在這里:scrapy.cmdline:execute
          ????????'console_scripts':?['scrapy?=?scrapy.cmdline:execute']
          ????},
          ????classifiers=[
          ????????...
          ????],
          ????install_requires=[
          ????????...
          ????],
          )
          我們需要關(guān)注的是?entry_points?配置,它就是調(diào)用 Scrapy 開始的地方,也就是cmdline.py?的?execute?方法。
          也就是說,我們在安裝 Scrapy 的過程中,setuptools?這個(gè)包管理工具,就會(huì)把上述代碼生成好并放在可執(zhí)行路徑下,這樣當(dāng)我們調(diào)用?scrapy?命令時(shí),就會(huì)調(diào)用 Scrapy 模塊下的?cmdline.py?的?execute?方法。
          而且在這這里,我們可以學(xué)到一個(gè)小技巧——如何用 Python 編寫一個(gè)可執(zhí)行文件?其實(shí)非常簡單,模仿上面的思路,只需要以下幾步即可完成:
          1. 編寫一個(gè)帶有?main?方法的 Python 模塊(首行必須注明 Python 執(zhí)行路徑)
          2. 去掉.py后綴名
          3. 修改權(quán)限為可執(zhí)行(chmod +x?文件名)
          4. 直接用文件名就可以執(zhí)行這個(gè) Python 文件
          例如,我們創(chuàng)建一個(gè)文件?mycmd,在這個(gè)文件中編寫一個(gè)?main?方法,這個(gè)方法編寫我們想要的執(zhí)行的邏輯,之后執(zhí)行?chmod +x mycmd?把這個(gè)文件權(quán)限變成可執(zhí)行,最后通過?./mycmd?就可以執(zhí)行這段代碼了,而不再需要通過?python ?方式就可以執(zhí)行了,是不是很簡單?

          運(yùn)行入口(execute.py)

          現(xiàn)在,我們已經(jīng)知道了 Scrapy 的運(yùn)行入口是?scrapy/cmdline.py?的?execute?方法,那我們就看一下這個(gè)方法。
          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?conf
          ????????if?hasattr(conf,?'settings'):
          ????????????settings?=?conf.settings
          ????#?-----------------------------------------

          ?#?初始化環(huán)境、獲取項(xiàng)目配置參數(shù)?返回settings對(duì)象
          ????if?settings?is?None:
          ????????settings?=?get_project_settings()
          ????#?校驗(yàn)棄用的配置項(xiàng)
          ????check_deprecated_settings(settings)

          ????#?---?兼容低版本scrapy.conf.settings的配置?---
          ????import?warnings
          ????from?scrapy.exceptions?import?ScrapyDeprecationWarning
          ????with?warnings.catch_warnings():
          ????????warnings.simplefilter("ignore",?ScrapyDeprecationWarning)
          ????????from?scrapy?import?conf
          ????????conf.settings?=?settings
          ????#?---------------------------------------

          ????#?執(zhí)行環(huán)境是否在項(xiàng)目中?主要檢查scrapy.cfg配置文件是否存在
          ????inproject?=?inside_project()
          ????
          ????#?讀取commands文件夾?把所有的命令類轉(zhuǎn)換為{cmd_name:?cmd_instance}的字典
          ????cmds?=?_get_commands_dict(settings,?inproject)
          ????#?從命令行解析出執(zhí)行的是哪個(gè)命令
          ????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)

          ????#?根據(jù)命令名稱找到對(duì)應(yīng)的命令實(shí)例
          ????cmd?=?cmds[cmdname]
          ????parser.usage?=?"scrapy?%s?%s"?%?(cmdname,?cmd.syntax())
          ????parser.description?=?cmd.long_desc()
          ????#?設(shè)置項(xiàng)目配置和級(jí)別為command
          ????settings.setdict(cmd.default_settings,?priority='command')
          ????cmd.settings?=?settings
          ????#?添加解析規(guī)則
          ????cmd.add_options(parser)
          ????#?解析命令參數(shù),并交由Scrapy命令實(shí)例處理
          ????opts,?args?=?parser.parse_args(args=argv[1:])
          ????_run_print_help(parser,?cmd.process_options,?args,?opts)

          ????#?初始化CrawlerProcess實(shí)例?并給命令實(shí)例添加crawler_process屬性
          ????cmd.crawler_process?=?CrawlerProcess(settings)
          ????#?執(zhí)行命令實(shí)例的run方法
          ????_run_print_help(parser,?_run_command,?cmd,?args,?opts)
          ????sys.exit(cmd.exitcode)
          這塊代碼就是 Scrapy 執(zhí)行的運(yùn)行入口了,我們根據(jù)注釋就能看到,這里的主要工作包括配置初始化、命令解析、爬蟲類加載、運(yùn)行爬蟲這幾步。
          了解了整個(gè)入口的流程,下面我會(huì)對(duì)每個(gè)步驟進(jìn)行詳細(xì)的分析。

          初始化項(xiàng)目配置

          首先第一步,根據(jù)環(huán)境初始化配置,在這里有一些兼容低版本 Scrapy 配置的代碼,我們忽略就好。我們重點(diǎn)來看配置是如何初始化的。這主要和環(huán)境變量和?scrapy.cfg?有關(guān),通過調(diào)用 ?get_project_settings?方法,最終生成一個(gè)?Settings?實(shí)例。
          def?get_project_settings():
          ????#?環(huán)境變量中是否有SCRAPY_SETTINGS_MODULE配置
          ????if?ENVVAR?not?in?os.environ:
          ????????project?=?os.environ.get('SCRAPY_PROJECT',?'default')
          ????????#?初始化環(huán)境?找到用戶配置文件settings.py?設(shè)置到環(huán)境變量SCRAPY_SETTINGS_MODULE中
          ????????init_env(project)
          ????#?加載默認(rèn)配置文件default_settings.py?生成settings實(shí)例
          ????settings?=?Settings()
          ????#?取得用戶配置文件
          ????settings_module_path?=?os.environ.get(ENVVAR)
          ????#?如果有用戶配置?則覆蓋默認(rèn)配置
          ????if?settings_module_path:
          ????????settings.setmodule(settings_module_path,?priority='project')
          ????#?如果環(huán)境變量中有其他scrapy相關(guān)配置也覆蓋
          ????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
          在初始配置時(shí),會(huì)加載默認(rèn)的配置文件?default_settings.py,主要邏輯在?Settings?類中。
          class?Settings(BaseSettings):
          ????def?__init__(self,?values=None,?priority='project'):
          ????????#?調(diào)用父類構(gòu)造初始化
          ????????super(Settings,?self).__init__()
          ????????#?把default_settings.py的所有配置set到settings實(shí)例中
          ????????self.setmodule(default_settings,?'default')
          ????????#?把a(bǔ)ttributes屬性也set到settings實(shí)例中
          ????????for?name,?val?in?six.iteritems(self):
          ????????????if?isinstance(val,?dict):
          ????????????????self.set(name,?BaseSettings(val,?'default'),?'default')
          ????????self.update(values,?priority)
          可以看到,首先把默認(rèn)配置文件?default_settings.py?中的所有配置項(xiàng)設(shè)置到?Settings?中,而且這個(gè)配置是有優(yōu)先級(jí)的。
          這個(gè)默認(rèn)配置文件?default_settings.py?是非常重要的,我們讀源碼時(shí)有必要重點(diǎn)關(guān)注一下里面的內(nèi)容,這里包含了所有組件的默認(rèn)配置,以及每個(gè)組件的類模塊,例如調(diào)度器類、爬蟲中間件類、下載器中間件類、下載處理器類等等。
          #?下載器類
          DOWNLOADER?=?'scrapy.core.downloader.Downloader'
          #?調(diào)度器類
          CHEDULER?=?'scrapy.core.scheduler.Scheduler'
          #?調(diào)度隊(duì)列類
          SCHEDULER_DISK_QUEUE?=?'scrapy.squeues.PickleLifoDiskQueue'
          SCHEDULER_MEMORY_QUEUE?=?'scrapy.squeues.LifoMemoryQueue'
          SCHEDULER_PRIORITY_QUEUE?=?'scrapy.pqueues.ScrapyPriorityQueue'
          有沒有感覺比較奇怪,默認(rèn)配置中配置了這么多類模塊,這是為什么?
          這其實(shí)是 Scrapy 特性之一,它這么做的好處是:任何模塊都是可替換的。
          什么意思呢?例如,你覺得默認(rèn)的調(diào)度器功能不夠用,那么你就可以按照它定義的接口標(biāo)準(zhǔn),自己實(shí)現(xiàn)一個(gè)調(diào)度器,然后在自己的配置文件中,注冊自己的調(diào)度器類,那么 Scrapy 運(yùn)行時(shí)就會(huì)加載你的調(diào)度器執(zhí)行了,這極大地提高了我們的靈活性!
          所以,只要在默認(rèn)配置文件中配置的模塊類,都是可替換的。

          檢查運(yùn)行環(huán)境是否在項(xiàng)目中

          初始化完配置之后,下面一步是檢查運(yùn)行環(huán)境是否在爬蟲項(xiàng)目中。我們知道,scrapy?命令有的是依賴項(xiàng)目運(yùn)行的,有的命令則是全局的。這里主要通過就近查找?scrapy.cfg?文件來確定是否在項(xiàng)目環(huán)境中,主要邏輯在?inside_project?方法中。
          def?inside_project():
          ????#?檢查此環(huán)境變量是否存在(上面已設(shè)置)
          ????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
          ?#?如果環(huán)境變量沒有?就近查找scrapy.cfg?找得到就認(rèn)為是在項(xiàng)目環(huán)境中
          ????return?bool(closest_scrapy_cfg())
          運(yùn)行環(huán)境是否在爬蟲項(xiàng)目中的依據(jù)就是能否找到?scrapy.cfg?文件,如果能找到,則說明是在爬蟲項(xiàng)目中,否則就認(rèn)為是執(zhí)行的全局命令。

          組裝命令實(shí)例集合

          再向下看,就到了加載命令的邏輯了。我們知道?scrapy?包括很多命令,例如?scrapy crawl?、?scrapy fetch?等等,那這些命令是從哪來的?答案就在?_get_commands_dict?方法中。
          def?_get_commands_dict(settings,?inproject):
          ????#?導(dǎo)入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?cmds

          def?_get_commands_from_module(module,?inproject):
          ????d?=?{}
          ????#?找到這個(gè)模塊下所有的命令類(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?d

          def?_iter_command_classes(module_name):
          ????#?迭代這個(gè)包下的所有模塊?找到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
          這個(gè)過程主要是,導(dǎo)入?commands?文件夾下的所有模塊,最終生成一個(gè)?{cmd_name: cmd}?字典集合,如果用戶在配置文件中也配置了自定義的命令類,也會(huì)追加進(jìn)去。也就是說,我們自己也可以編寫自己的命令類,然后追加到配置文件中,之后就可以使用自己定義的命令了。

          解析命令

          加載好命令類后,就開始解析我們具體執(zhí)行的哪個(gè)命令了,解析邏輯比較簡單:
          def?_pop_command_name(argv):
          ????i?=?0
          ????for?arg?in?argv[1:]:
          ????????if?not?arg.startswith('-'):
          ????????????del?argv[i]
          ????????????return?arg
          ????????i?+=?1
          這個(gè)過程就是解析命令行,例如執(zhí)行?scrapy crawl ,這個(gè)方法會(huì)解析出?crawl,通過上面生成好的命令類的字典集合,就能找到?commands?目錄下的?crawl.py文件,最終執(zhí)行的就是它的?Command?類。

          解析命令行參數(shù)

          找到對(duì)應(yīng)的命令實(shí)例后,調(diào)用?cmd.process_options?方法解析我們的參數(shù):
          def?process_options(self,?args,?opts):
          ????#?首先調(diào)用了父類的process_options?解析統(tǒng)一固定的參數(shù)
          ????ScrapyCommand.process_options(self,?args,?opts)
          ????try:
          ????????#?命令行參數(shù)轉(zhuǎn)為字典
          ????????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')
          這個(gè)過程就是解析命令行其余的參數(shù),固定參數(shù)解析交給父類處理,例如輸出位置等。其余不同的參數(shù)由不同的命令類解析。

          初始化CrawlerProcess

          一切準(zhǔn)備就緒,最后初始化?CrawlerProcess?實(shí)例,然后運(yùn)行對(duì)應(yīng)命令實(shí)例的?run?方法。
          cmd.crawler_process?=?CrawlerProcess(settings)
          _run_print_help(parser,?_run_command,?cmd,?args,?opts)
          我們開始運(yùn)行一個(gè)爬蟲一般使用的是?scrapy crawl ,也就是說最終調(diào)用的是?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?方法中調(diào)用了?CrawlerProcess?實(shí)例的?crawl?和?start?方法,就這樣整個(gè)爬蟲程序就會(huì)運(yùn)行起來了。
          我們先來看CrawlerProcess初始化:
          class?CrawlerProcess(CrawlerRunner):
          ????def?__init__(self,?settings=None):
          ????????#?調(diào)用父類初始化
          ????????super(CrawlerProcess,?self).__init__(settings)
          ????????#?信號(hào)和log初始化
          ????????install_shutdown_handlers(self._signal_shutdown)
          ????????configure_logging(self.settings)
          ????????log_scrapy_info(self.settings)
          其中,構(gòu)造方法中調(diào)用了父類?CrawlerRunner?的構(gòu)造方法:
          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()
          初始化時(shí),調(diào)用了?_get_spider_loader方法:
          def?_get_spider_loader(settings):
          ????#?讀取配置文件中的SPIDER_MANAGER_CLASS配置項(xiàng)
          ????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())
          這里會(huì)讀取默認(rèn)配置文件中的?spider_loader項(xiàng),默認(rèn)配置是?spiderloader.SpiderLoader類,從名字我們也能看出來,這個(gè)類是用來加載我們編寫好的爬蟲類的,下面看一下這個(gè)類的具體實(shí)現(xiàn)。
          @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]?=?spcls

          ????def?_load_all_spiders(self):
          ????????for?name?in?self.spider_modules:
          ????????????for?module?in?walk_modules(name):
          ????????????????self._load_spiders(module)
          可以看到,在這里爬蟲加載器會(huì)加載所有的爬蟲腳本,最后生成一個(gè)?{spider_name: spider_cls}?的字典,所以我們在執(zhí)行?scarpy crawl ?時(shí),Scrapy 就能找到我們的爬蟲類。

          運(yùn)行爬蟲

          CrawlerProcess?初始化完之后,調(diào)用它的?crawl?方法:
          def?crawl(self,?crawler_or_spidercls,?*args,?**kwargs):
          ????#?創(chuàng)建crawler
          ????crawler?=?self.create_crawler(crawler_or_spidercls)
          ????return?self._crawl(crawler,?*args,?**kwargs)

          def?_crawl(self,?crawler,?*args,?**kwargs):
          ????self.crawlers.add(crawler)
          ????#?調(diào)用Crawler的crawl方法
          ????d?=?crawler.crawl(*args,?**kwargs)
          ????self._active.add(d)

          ????def?_done(result):
          ????????self.crawlers.discard(crawler)
          ????????self._active.discard(d)
          ????????return?result
          ????return?d.addBoth(_done)

          def?create_crawler(self,?crawler_or_spidercls):
          ????if?isinstance(crawler_or_spidercls,?Crawler):
          ????????return?crawler_or_spidercls
          ????return?self._create_crawler(crawler_or_spidercls)

          def?_create_crawler(self,?spidercls):
          ????#?如果是字符串?則從spider_loader中加載這個(gè)爬蟲類
          ????if?isinstance(spidercls,?six.string_types):
          ????????spidercls?=?self.spider_loader.load(spidercls)
          ????#?否則創(chuàng)建Crawler
          ????return?Crawler(spidercls,?self.settings)
          這個(gè)過程會(huì)創(chuàng)建?Cralwer?實(shí)例,然后調(diào)用它的?crawl?方法:
          @defer.inlineCallbacks
          def?crawl(self,?*args,?**kwargs):
          ????assert?not?self.crawling,?"Crawling?already?taking?place"
          ????self.crawling?=?True

          ????try:
          ????????#?到現(xiàn)在?才是實(shí)例化一個(gè)爬蟲實(shí)例
          ????????self.spider?=?self._create_spider(*args,?**kwargs)
          ????????#?創(chuàng)建引擎
          ????????self.engine?=?self._create_engine()
          ????????#?調(diào)用爬蟲類的start_requests方法
          ????????start_requests?=?iter(self.spider.start_requests())
          ????????#?執(zhí)行引擎的open_spider?并傳入爬蟲實(shí)例和初始請求
          ????????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?=?False
          ????????if?self.engine?is?not?None:
          ????????????yield?self.engine.close()

          ????????if?six.PY2:
          ????????????six.reraise(*exc_info)
          ????????raise
          ????????
          def?_create_spider(self,?*args,?**kwargs):
          ????return?self.spidercls.from_crawler(self,?*args,?**kwargs)
          到這里,才會(huì)對(duì)我們的爬蟲類創(chuàng)建一個(gè)實(shí)例對(duì)象,然后創(chuàng)建引擎,之后調(diào)用爬蟲類的?start_requests?方法獲取種子 URL,最后交給引擎執(zhí)行。
          最后來看?Cralwer?是如何開始運(yùn)行的額,也就是它的?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調(diào)整)
          ????tp?=?reactor.getThreadPool()
          ????tp.adjustPoolsize(maxthreads=self.settings.getint('REACTOR_THREADPOOL_MAXSIZE'))
          ????reactor.addSystemEventTrigger('before',?'shutdown',?self.stop)
          ????#?開始執(zhí)行
          ????reactor.run(installSignalHandlers=False)
          在這里有一個(gè)叫做?reactor?的模塊。reactor?是個(gè)什么東西呢?它是?Twisted?模塊的事件管理器,我們只要把需要執(zhí)行的事件注冊到?reactor?中,然后調(diào)用它的?run?方法,它就會(huì)幫我們執(zhí)行注冊好的事件,如果遇到網(wǎng)絡(luò)IO等待,它會(huì)自動(dòng)幫切換到可執(zhí)行的事件上,非常高效。
          在這里我們不用深究?reactor?是如何工作的,你可以把它想象成一個(gè)線程池,只是采用注冊回調(diào)的方式來執(zhí)行事件。
          到這里,Scrapy 運(yùn)行的入口就分析完了,之后爬蟲的調(diào)度邏輯就交由引擎?ExecuteEngine?處理了,引擎會(huì)協(xié)調(diào)多個(gè)組件,相互配合完成整個(gè)任務(wù)的執(zhí)行。

          總結(jié)

          總結(jié)一下,Scrapy 在真正運(yùn)行前,需要做的工作包括配置環(huán)境初始化、命令類的加載、爬蟲模塊的加載,以及命令類和參數(shù)解析,之后運(yùn)行我們的爬蟲類,最終,這個(gè)爬蟲類的調(diào)度交給引擎處理。
          這里我把整個(gè)流程也總結(jié)成了思維導(dǎo)圖,方便你理解:
          好了,Scrapy 是如何運(yùn)行的代碼剖析就先分析到這里,下篇文章我們會(huì)深入剖析各個(gè)核心組件,分析它們都是負(fù)責(zé)做什么工作的,以及它們之間又是如何協(xié)調(diào)完成抓取任務(wù)的,敬請期待。

          更多閱讀



          2020 年最佳流行 Python 庫 Top 10


          2020 Python中文社區(qū)熱門文章 Top 10


          5分鐘快速掌握 Python 定時(shí)任務(wù)框架

          特別推薦




          點(diǎn)擊下方閱讀原文加入社區(qū)會(huì)員

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲天堂黄片 | 国产操逼视频网站 | 久久无码在线观看 | 怡红院成人免费电影 | 大色播国产精品 |