微信搜索关注「水滴与银弹」公众号,第一时间获取优质技术干货。7年资深后端研发,用简单的方式把技术讲清楚。
在上篇文章:Scrapy源码分析(一)架构概览,我们主要从整体上了解了 Scrapy 的架构和数据流转,并没有深入分析每个模块。从这篇文章开始,我将带你详细剖析 Scrapy 的运行原理。
这篇文章,我们先从最基础的运行入口来讲,来看一下 Scrapy 究竟是如何运行起来的。
scrapy 命令从哪来?
当我们基于 Scrapy 写好一个爬虫后,想要把我们的爬虫运行起来,怎么做?非常简单,只需要执行以下命令就可以了。
scrapy crawl <spider_name>
通过这个命令,我们的爬虫就真正开始工作了。那么从命令行到执行爬虫逻辑,这个过程中到底发生了什么?
在开始之前,不知道你有没有和我一样的疑惑,我们执行的 scrapy
命令从何而来?
实际上,当你成功安装好 Scrapy 后,使用如下命令,就能找到这个命令文件,这个文件就是 Scrapy 的运行入口:
$ which scrapy
/usr/local/bin/scrapy
使用编辑打开这个文件,你会发现,它其实它就是一个 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 后,为什么入口点是这里呢?
答案就在于 Scrapy 的安装文件 setup.py
中,我们找到这个文件,就会发现在这个文件里,已经声明好了程序的运行入口处:
from os.path import dirname, join
from 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 <file.py>
方式就可以执行了,是不是很简单?
运行入口(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 conf
if hasattr(conf, 'settings'):
settings = conf.settings
# -----------------------------------------
# 初始化环境、获取项目配置参数 返回settings对象
if settings is None:
settings = get_project_settings()
# 校验弃用的配置项
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
# ---------------------------------------
# 执行环境是否在项目中 主要检查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=arg