其实这篇文章是scrapy源码学习的(一),加载器那篇才是(二)
本文环境:python
- wind7 64bits
- python 3.7
- scrapy 1.5.1
scrapy拥有很是灵活的低耦合的命令行工具,若是本身想要从新实现覆盖掉scrapy自带的命令也是能够的。
使用它的命令行工具能够大体分为两种状况:shell
先看下不在scrapy项目路径下的命令行有哪些:api
Scrapy 1.5.1 - no active project Usage: scrapy <command> [options] [args] Available commands: bench Run quick benchmark test fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates runspider Run a self-contained spider (without creating a project) settings Get settings values shell Interactive scraping console startproject Create new project version Print Scrapy version view Open URL in browser, as seen by Scrapy [ more ] More commands available when run from project directory Use "scrapy <command> -h" to see more info about a command
在项目路径下的命令行新增了check、crawl、edit、list、parse这些命令,具体:app
Scrapy 1.5.1 - project: myspider01 Usage: scrapy <command> [options] [args] Available commands: bench Run quick benchmark test check Check spider contracts crawl Run a spider edit Edit spider fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates list List available spiders parse Parse URL (using its spider) and print the results runspider Run a self-contained spider (without creating a project) settings Get settings values shell Interactive scraping console startproject Create new project version Print Scrapy version view Open URL in browser, as seen by Scrapy Use "scrapy <command> -h" to see more info about a command
也便是说scrapy能够根据当前路径是不是scrapy项目路径来判断提供可用的命令给用户。scrapy
在当前路径下建立一个scrapy项目,DOS下输入:ide
scrapy startproject myproject
能够查看刚刚建立的项目myproject的目录结构:函数
├── scrapy.cfg //scrapy项目配置文件 ├── myproject ├── spiders // 爬虫脚本目录 ├── __init__.py ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py // 项目设置
能够判定,在咱们使用"startproject"这个scrapy命令时,scrapy会把一些项目默认模板拷贝到咱们建立项目的路径下,从而生成咱们看到的相似上面的目录结构。咱们能够打开scrapy的包,看看这些模板在哪一个地方。切换至scrapy的安装路径(好比:..Python37Libsite-packagesscrapy),能够看到路径下有templates文件夹,而此文件夹下的project文件夹即是建立项目时拷贝的默认模板存放目录。
那么scrapy是怎么实现相似“startproject”这样的命令的呢?工具
scrapy是使用命令行来启动脚本的(固然也能够调用入口函数来启动),查看其命令行实现流程必须先找到命令行实行的入口点,这个从其安装文件setup.py中找到。
打开setup.py 找到entry_points:学习
... entry_points={ 'console_scripts': ['scrapy = scrapy.cmdline:execute'] }, ...
能够看到scrapy开头的命令皆由模块scrapy.cmdline的execute函数做为入口函数。fetch
先浏览一下execute函数源码,这里只贴主要部分:
def execute(argv=None, settings=None): if argv is None: argv = sys.argv ... #主要部分:获取当前项目的设置 if settings is None: settings = get_project_settings() # set EDITOR from environment if available try: editor = os.environ['EDITOR'] except KeyError: pass else: settings['EDITOR'] = editor #检查提醒已不被支持的设置项目 check_deprecated_settings(settings) ... #主要部分:判断是否在项目路径下,加载可见命令,解析命令参数 inproject = inside_project() 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() settings.setdict(cmd.default_settings, priority='command') cmd.settings = settings cmd.add_options(parser) opts, args = parser.parse_args(args=argv[1:]) _run_print_help(parser, cmd.process_options, args, opts) cmd.crawler_process = CrawlerProcess(settings) _run_print_help(parser, _run_command, cmd, args, opts) sys.exit(cmd.exitcode)
阅读cmdline.py的execute函数,大概了解了命令行实现的基本流程:
命令参数的获取能够经过两种方式传递:
第一种是调用execute,好比:
from scrapy.cmdline import execute execute(argv=['scrapy','startproject','myproject','-a','xxxx'])
这样就至关于第二种方式:命令控制台执行
scrapy startproject myproject -a xxxx
传递的参数都是
['scrapy','startproject','myproject','-a','xxxx']
若是当前不是调用的方式传递settings给execute入口,而是通常的命令控制台启动scrapy,那么scrapy会在当前路径下搜索加载可能存在的项目配置文件。主要是经过函数get_project_settings执行。
ENVVAR = 'SCRAPY_SETTINGS_MODULE' def get_project_settings(): #获取配置 if ENVVAR not in os.environ: #初始化获取项目的default级配置,便是scrapy生成的默认配置 project = os.environ.get('SCRAPY_PROJECT', 'default') #初始化项目环境,设置系统环境变量SCRAPY_SETTINGS_MODULE的值为配置模块路径 init_env(project) settings = Settings() settings_module_path = os.environ.get(ENVVAR) if settings_module_path: settings.setmodule(settings_module_path, priority='project') ... return settings
获取的配置文件主要是scrapy.cfg,咱们能够看下他的内容:
[settings] default = myproject.settings [deploy] #url = http://localhost:6800/ project = myproject
在生成项目myproject的时候,这个配置文件就已经指定了项目设置模块的路径"myproject.settings",因此上面的get_project_settings函数获取即是配置文件settings字段中的default键值,而后导入该设置模块来生成配置。具体实如今init_env函数中。
def init_env(project='default', set_syspath=True): """在当前项目路径下初始化项目环境. 而且经过配置系统环境来让python可以定位配置模块 """ #在项目路径下进入命令行,才能准确获取配置 #获取可能存在scrapy.cfg配置文件的模块路径 cfg = get_config() #获取到配置文件后设置系统环境变量SCRAPY_SETTINGS_MODULE为配置模块路径, #如: myproject.settings,默认项目级别均为default,便是配置文件字段settings中的键 if cfg.has_option('settings', project): os.environ['SCRAPY_SETTINGS_MODULE'] = cfg.get('settings', project) #将最近的scrapy.cfg模块路径放入系统路径使Python可以找到该模块导入 closest = closest_scrapy_cfg() if closest: projdir = os.path.dirname(closest) if set_syspath and projdir not in sys.path: #加入项目设置模块路径到系统路径让Python可以定位到 sys.path.append(projdir) def get_config(use_closest=True): """ SafeConfigParser.read(filenames) 尝试解析文件列表,若是解析成功返回文件列表。若是filenames是string或Unicode string, 将会按单个文件来解析。若是在filenames中的文件不能打开,该文件将被忽略。这样设计的目的是, 让你能指定本地有多是配置文件的列表(例如,当前文件夹,用户的根目录,及一些全系统目录), 因此在列表中存在的配置文件都会被读取。""" sources = get_sources(use_closest) cfg = SafeConfigParser() cfg.read(sources) return cfg def get_sources(use_closest=True): '''先获取用户的根目录,及一些全系统目录下的有scrapy.cfg的路径加入sources 最后若是使用最靠近当前路径的scrapy.cfg的标志use_closest为True时加入该scrapy.cfg路径''' xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ os.path.expanduser('~/.config') sources = ['/etc/scrapy.cfg', r'c:\scrapy\scrapy.cfg', xdg_config_home + '/scrapy.cfg', os.path.expanduser('~/.scrapy.cfg')] if use_closest: sources.append(closest_scrapy_cfg()) return sources def closest_scrapy_cfg(path='.', prevpath=None): """ 搜索最靠近当前当前路径的scrapy.cfg配置文件并返回其路径。 搜索会按照当前路径-->父路径的递归方式进行,到达顶层没有结果则返回‘’ """ if path == prevpath: return '' path = os.path.abspath(path) cfgfile = os.path.join(path, 'scrapy.cfg') if os.path.exists(cfgfile): return cfgfile return closest_scrapy_cfg(os.path.dirname(path), path)
经过init_env来设置os.environ['SCRAPY_SETTINGS_MODULE']的值,这样的话
#将项目配置模块路径设置进系统环境变量 os.environ['SCRAPY_SETTINGS_MODULE'] = 'myproject.settings'
初始化后返回到原先的get_project_settings,生成一个设置类Settings实例,而后再将设置模块加载进实例中完成项目配置的获取这一动做。
判断当前路径是不是scrapy项目路径,其实很简单,由于前面已经初始化过settings,若是在项目路径下,那么
os.environ['SCRAPY_SETTINGS_MODULE']的值就已经被设置了,如今只须要判断这个值是否存在即可以判断是否在项目路径下。具体实如今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 return bool(closest_scrapy_cfg())
知道了当前是否在项目路径下,还有初始化了项目配置,这个时候就能够获取到在当前路径下可以使用的命令行有哪些了。
获取当前可用命令集合比较简单,直接加载模块scrapy.commands下的全部命令行类,判断是否须要在项目路径下才能使用该命令,是的话直接实例化加入一个字典(格式:<命令名称>:<命令实例>)返回,具体实现经过_get_commands_dict:
def _get_commands_dict(settings, inproject): cmds = _get_commands_from_module('scrapy.commands', inproject) cmds.update(_get_commands_from_entry_points(inproject)) #若是有新的命令行模块在配置中设置,会自动载入 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 = {} for cmd in _iter_command_classes(module): #判断是否须要先建立一个项目才能使用该命令, #即目前是否位于项目路径下(inproject)的可用命令有哪些,不是的有哪些 if inproject or not cmd.requires_project: cmdname = cmd.__module__.split('.')[-1] #获取该命令名称并实例化 加入返回字典 #返回{<命令名称>:<命令实例>} d[cmdname] = cmd() return d def _iter_command_classes(module_name): #获取scrapy.commands下全部模块文件中属于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__ and \ not obj == ScrapyCommand: yield obj
其中判断是不是命令类的关键在于该命令模块中的命令类是否继承了命令基类ScrapyCommand,只要继承了该基类就能够被检测到。这有点相似接口的做用,ScrapyCommand基类其实就是一个标识类(该类比较简单,能够查看基类代码)。而该基类中有一个requires_project标识,标识是否须要在scrapy项目路径下才能使用该命令,判断该值就能够得到当前可用命令。
获取到了可用命令集合,接下来会加载Python自带的命令行解析模块optparser.OptionParser的命令行参数解析器,经过实例化获取该parser,传入当前命令实例的add_options属性方法中来加载当前命令实例附加的解析命令,如:-a xxx, -p xxx, --dir xxx 之类的相似Unix命令行的命令。这些都是经过parser来实现解析。
其实在加载解析器以前,会去判断当前的用户输入命令是不是合法的,是否是可用的,若是可用会接下去解析执行该命令,不可用便打印出相关的帮助提示。好比:
Usage ===== scrapy startproject <project_name> [project_dir] Create new project Options ======= --help, -h show this help message and exit Global Options -------------- --logfile=FILE log file. if omitted stderr will be used --loglevel=LEVEL, -L LEVEL log level (default: DEBUG) --nolog disable logging completely --profile=FILE write python cProfile stats to FILE --pidfile=FILE write process ID to FILE --set=NAME=VALUE, -s NAME=VALUE set/override setting (may be repeated) --pdb enable pdb on failure
至此,scrapy命令行工具的实现流程基本结束。
scrapy的命令行工具实现了低耦合,须要删减增长哪一个命令行只须要在scrapy.commands模块中修改增删就能够实现。可是实现的关键在于该模块下的每个命令行类都得继承ScrapyCommand这个基类,这样在导入的时候才能有所判断,因此我说ScrapyCommand是个标识类。基于标识类来实现模块的低耦合。
下一篇将会记录根据借鉴scrapy命令行工具实现方法来实现本身的命令行