Scrapy阅读源码分析<二>
来源:互联网 发布:科比数据 编辑:程序博客网 时间:2024/05/17 23:05
scrapy命令
当用scrapy写好一个爬虫后,使用scrapy crawl <spider_name>
命令就可以运行这个爬虫,那么这个过程中到底发生了什么?
scrapy
命令从何而来?
实际上,当你成功安装scrapy后,使用如下命令,就能找到这个命令:
12
$ which scrapy/usr/local/bin/scrapy
使用vim
或其他编辑器打开它:
1
$ vim /usr/local/bin/scrapy
其实它就是一个python脚本,而且代码非常少。
1234567891011
#!/usr/bin/python# -*- coding: utf-8 -*-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
中,声明了程序的入口处:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
from os.path import dirname, joinfrom setuptools import setup, find_packageswith open(join(dirname(__file__), 'scrapy/VERSION'), 'rb') as f:version = f.read().decode('ascii').strip()setup(name='Scrapy',version=version,url='http://scrapy.org',description='A high-level Web Crawling and Screen Scraping framework',long_description=open('README.rst').read(),author='Scrapy developers',maintainer='Pablo Hoffman',maintainer_email='pablo@pablohoffman.com',license='BSD',packages=find_packages(exclude=('tests', 'tests.*')),include_package_data=True,zip_safe=False,entry_points={'console_scripts': ['scrapy = scrapy.cmdline:execute']},classifiers=['Framework :: Scrapy','Development Status :: 5 - Production/Stable','Environment :: Console','Intended Audience :: Developers','License :: OSI Approved :: BSD License','Operating System :: OS Independent','Programming Language :: Python','Programming Language :: Python :: 2','Programming Language :: Python :: 2.7','Topic :: Internet :: WWW/HTTP','Topic :: Software Development :: Libraries :: Application Frameworks','Topic :: Software Development :: Libraries :: Python Modules',],install_requires=['Twisted>=10.0.0','w3lib>=1.8.0','queuelib','lxml','pyOpenSSL','cssselect>=0.9','six>=1.5.2',],)
entry_points
指明了入口是cmdline.py
的execute
方法,在安装过程中,setuptools
这个包管理工具,就会把上述那一段代码生成放在可执行路径下。
这里也有必要说一下,如何用python编写一个可执行文件,其实非常简单,只需要以下几步即可完成:
- 编写一个带有
main
方法的python模块(首行必须注明python执行路径) - 去掉
.py
后缀名 - 修改权限为可执行:
chmod +x
脚本
这样,你就可以直接使用文件名执行此脚本了,而不用通过python <file.py>
的方式去执行,是不是很简单?
入口(execute.py)
既然现在已经知道了scrapy的入口是scrapy/cmdline.py
的execute
方法,我们来看一下这个方法。
主要的运行流程已经加好注释,这里我总结出了每个流程执行过程:
下面会对每个流程进行分析讲解。
流程解析
初始化项目配置
这个流程比较简单,主要是根据环境变量和scrapy.cfg
初始化环境,最终生成一个Settings
实例,来看代码get_project_settings
方法:
12345678910111213141516171819202122
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() ifk.startswith('SCRAPY_')}if env_overrides:settings.setdict(env_overrides, priority='project')return settings
这个过程中进行了Settings
配置初始化:
1234567891011
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
是非常重要的,个人认为还是有必要看一下里面的内容,这里包含了所有默认的配置,例如调度器类、爬虫中间件类、下载器中间件类、下载处理器类等等。
在这里就能隐约发现,scrapy的架构是非常低耦合的,所有组件都是可替换的,什么是可替换呢?
例如,你觉得默认的调度器功能不够用,那么你就可以按照它定义的接口标准,自己实现一个调度器,然后在自己的配置文件中,注册自己写的调度器模块,那么scrapy的运行时就会用上你新写的调度器模块了!
只要在默认配置文件中配置的模块,都是可替换的。
检查环境是否在项目中
123456789101112
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命令有的是依赖项目运行的,有的命令则是全局的,不依赖项目的。这里主要通过就近查找scrapy.cfg
文件来确定是否在项目环境中。
获取可用命令并组装成名称与实例的字典
12345678910111213141516171819202122232425262728
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}
字典集合,如果用户在配置文件中配置了自定义的命令类,也追加进去。也就是说,自己也可以编写自己的命令类,然后追加到配置文件中,之后就可以使用自己自定义的命令了。
解析执行的命令并找到对应的命令实例
1234567
def _pop_command_name(argv):i = 0for arg in argv[1:]:if not arg.startswith('-'):del argv[i]return argi += 1
这个过程就是解析命令行,例如scrapy crawl <spider_name>
,解析出crawl
,通过上面生成好的命令字典集合,就能找到commands
模块下的crawl.py
下的Command
类的实例。
scrapy命令实例解析命令行参数
找到对应的命令实例后,调用cmd.process_options
方法:
1234567891011121314151617181920212223
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
方法。
12
cmd.crawler_process = CrawlerProcess(settings)_run_print_help(parser, _run_command, cmd, args, opts)
如果运行命令是scrapy crawl <spider_name>
,则运行的就是commands/crawl.py
的run
:
123456789
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
初始化:
12345678
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
的构造:
123456789
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
方法:
123456789101112131415161718192021
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
:
123456789101112131415161718
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}
的字典。
执行crawl和start方法
CrawlerProcess
初始化完之后,调用crawl
方法:
12345678910111213141516171819202122232425262728
def crawl(self, crawler_or_spidercls, *args, **kwargs):# 创建crawlercrawler = 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
方法:
1234567891011121314151617181920212223242526272829
def 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
方法:
12345678910111213
def start(self, stop_after_crawl=True):if stop_after_crawl:d = self.join()if d.called:returnd.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
是个什么东西呢?它是Twisted
模块的事件管理器,只要把需要执行的事件方法注册到reactor
中,然后调用它的run
方法,它就会帮你执行注册好的事件方法,如果遇到网络IO等待,它会自动帮你切换可执行的事件方法,非常高效。
大家不用在意reactor
是如何工作的,你可以把它想象成一个线程池,只是采用注册回调的方式来执行事件。
到这里,爬虫的之后调度逻辑就交由引擎ExecuteEngine
处理了。
在每次执行scrapy命令时,主要经过环境、配置初始化,加载命令类和爬虫模块,最终实例化执行引擎,交给引擎调度处理的流程,下篇文章会讲解执行引擎是如何调度和管理各个组件工作的。
- Scrapy阅读源码分析<二>
- Scrapy源码阅读分析<一>
- Scrapy阅读源码分析<三>
- Scrapy阅读源码分析<三>
- Scrapy阅读源码分析<四>
- scrapy源码阅读
- Scrapy源码分析(二):Setting相关类定义
- scrapy源码分析(二)----------ExecutionEngine(一)主循环
- scrapy-redis源码分析
- Scrapy-settings源码分析
- Scrapy-redis源码分析
- Scrapy Redis源码 spider分析
- scrapy启动过程源码分析
- scrapy中crwalspider源码分析
- TCMalloc源码阅读(二)--线程局部缓存ClassSize分析
- TCMalloc源码阅读(二)--线程局部缓存ClassSize分析
- Xwork2 源码阅读(二)
- live555源码阅读二
- 关于Android 支持 4K 视频显示的问题总结
- odoo之修改test案例为班级管理模块
- cookie的相关操作
- vc++汇总
- Android中控制点击EditText输入框右边清空图标的显示与隐藏.
- Scrapy阅读源码分析<二>
- 中缀表达式转换为后缀表达式
- mongodb_修改器($inc/$set/$unset/$push/$pop/upsert......)
- IntelliJ Idea 2017 免费激活方法
- Mifare S50与S70的存取控制
- 做技术的你是如何平衡工作与生活的?
- 获取数组最大连续增长子数组
- 对Simulink中scope进行进一步的设置
- 23. Merge k Sorted Lists