乐趣区

Scrapy-实用的命令行工具实现方法

其实这篇文章是 scrapy 源码学习的(一),加载器那篇才是(二)
scrapy 的命令行工具

本文环境:

wind7 64bits
python 3.7
scrapy 1.5.1

scrapy 拥有非常灵活的低耦合的命令行工具,如果自己想要重新实现覆盖掉 scrapy 自带的命令也是可以的。使用它的命令行工具可以大致分为两种情况:

在创建的 project 路径下
不在 project 路径下

先看下不在 scrapy 项目路径下的命令行有哪些:
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 这些命令,具体:
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 下输入:
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 源码
找到入口
scrapy 是使用命令行来启动脚本的(当然也可以调用入口函数来启动),查看其命令行实现流程必须先找到命令行实行的入口点,这个从其安装文件 setup.py 中找到。打开 setup.py 找到 entry_points:

entry_points={
‘console_scripts’: [‘scrapy = scrapy.cmdline:execute’]
},

可以看到 scrapy 开头的命令皆由模块 scrapy.cmdline 的 execute 函数作为入口函数。
分析入口函数
先浏览一下 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 函数,大概了解了命令行实现的基本流程:
1. 获取命令参数
命令参数的获取可以通过两种方式传递:第一种是调用 execute, 比如:
from scrapy.cmdline import execute
execute(argv=[‘scrapy’,’startproject’,’myproject’,’-a’,’xxxx’])
这样就相当于第二种方式:命令控制台执行
scrapy startproject myproject -a xxxx
传递的参数都是
[‘scrapy’,’startproject’,’myproject’,’-a’,’xxxx’]
2. 获取 scrapy 项目配置
如果当前不是调用的方式传递 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 实例,然后再将设置模块加载进实例中完成项目配置的获取这一动作。
3. 判断是否在 scrapy 项目路径下
判断当前路径是否是 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())
4. 获取命令集合,命令解析
知道了当前是否在项目路径下,还有初始化了项目配置,这个时候就可以获取到在当前路径下能够使用的命令行有哪些了。获取当前可用命令集合比较简单,直接加载模块 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 来实现解析。
5. 判断当前命令是否可用
其实在加载解析器之前,会去判断当前的用户输入命令是否是合法的,是不是可用的,如果可用会接下去解析执行该命令,不可用便打印出相关的帮助提示。比如:
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 命令行工具实现方法来实现自己的命令行

退出移动版