【系列】scrapy启动流程源码分析(1)命令行启动

前言

虽然爬虫的入门级编写并不难,但要让爬虫真正稳定可靠的运行起来,真不是一件容易的事。
首先,要用到scrapy,就必须要读懂scrapy这个爬虫框架,如果连这个框架的执行逻辑都搞不懂,那么爬虫也很难写好。
下面,我将粗略研究一遍scrapy源码,把scrapy的启动和运行过程捋一遍。

1.命令行启动

这里先不谈使用了各种框架的复杂情况,比如scrapyd服务、redis分布式队列等。只看最简单的情况,假设只写了几个简单爬虫spider(中间件和管道写不写无影响)。

输入命令

通过命令行运行其中的某一个spider:

$ scrapy crawl myspider

首先,命令里的scrapy是一个可执行文件,后面的crawl myspider是scrapy的参数。
可执行文件scrapy在/usr/local/python/bin目录里,是一个python脚本,有效代码为:

from scrapy.cmdline import execute
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(execute())

这个文件的作用就是从命令行里读取命令,然后传递给scrapy.cmdline包的execute()方法进行下一步操作。

execute()方法

从python的第三方库目录里找到scrapy/cmdline.py文件,可以看到代码中有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)
    
def _run_command(cmd, args, opts):
    if opts.profile:
        _run_command_profiled(cmd, args, opts)
    else:
        cmd.run(args, opts)

此方法中的重要操作有:
1.执行get_project_settings()方法,导入全局配置文件scrapy.cfg,进而导入项目的settings .py。这里用到了scrapy/utils/project.py中的get_project_settings()方法,这里先不展开。
2.解析命令行scrapy之后的命令,并把命令中的配置项导入到settings中,中间的一大段代码就是执行此功能,每个有效的命令均对应scrapy/commands中以命令命名的文件中的Command类。
3.cmd.crawler_process = CrawlerProcess(settings)这句代码建立CrawlerProcess对象,并把所有的设置settings传递给此对象。
4._run_print_help(parser, _run_command, cmd, args, opts)调用_run_command执行命令。如果指定了profile命令行参数,则用cProfile运行命令,cProfile是一个标准模块,这里不必考虑。无论如何,最后都会执行命令的run()方法,也就是scrapy/commands/crawl.py中Command类的run()方法。

执行crawl命令

scrapy/commands/crawl.py#Command:

    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()

这里有2个重要操作:
1.调用CrawlerProcess的crawl方法,执行初始化(创建新爬虫对象)。
2.调用CrawlerProcess的start方法,正式运行。
CrawlerProcess便是scrapy运行过程中最根本的进程,是所有爬虫运行的基础。

附:get_project_settings()导入配置文件的过程

scrapy/utils/project.py:

...
ENVVAR = 'SCRAPY_SETTINGS_MODULE'
...
def get_project_settings():
    if ENVVAR not in os.environ:
        project = os.environ.get('SCRAPY_PROJECT', 'default')
        init_env(project)

    settings = Settings()
    settings_module_path = os.environ.get(ENVVAR)
    if settings_module_path:
        settings.setmodule(settings_module_path, priority='project')

    # XXX: remove this hack
    pickled_settings = os.environ.get("SCRAPY_PICKLED_SETTINGS_TO_OVERRIDE")
    if pickled_settings:
        settings.setdict(pickle.loads(pickled_settings), priority='project')

    # XXX: deprecate and remove this functionality
    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

get_project_settings会首先判断是否设置了SCRAPY_SETTINGS_MODULE环境变量,这个环境变量用来指定工程的配置模块。如果没有这个环境变量,则会调用init_env来初始化环境变量,由于我们没有设置SCRAPY_PROJECT,所以会用default默认值来执行init_env(在scrapy/utils/conf.py)。

scrapy/utils/conf.py:

def init_env(project='default', set_syspath=True):
    """Initialize environment to use command-line tool from inside a project
    dir. This sets the Scrapy settings module and modifies the Python path to
    be able to locate the project module.
    """
    cfg = get_config()
    if cfg.has_option('settings', project):
        os.environ['SCRAPY_SETTINGS_MODULE'] = cfg.get('settings', project)
    closest = closest_scrapy_cfg()
    if closest:
        projdir = os.path.dirname(closest)
        if set_syspath and projdir not in sys.path:
            sys.path.append(projdir)

def get_config(use_closest=True):
    """Get Scrapy config file as a SafeConfigParser"""
    sources = get_sources(use_closest)
    cfg = SafeConfigParser()
    cfg.read(sources)
    return cfg

def get_sources(use_closest=True):
    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

init_env首先调用get_config()获取cfg配置文件,这个配置文件获取的优先级是:
1./etc/scrapy.cfg,c:\scrapy\scrapy.cfg
2.XDG_CONFIG_HOME环境变量指定的目录下的scrapy.cfg
3.~/.scrapy.cfg
4.当前执行目录下的scrapy.cfg或者父目录中的scrapy.cfg
由于1,2,3默认都不设置,所以就使用当前执行命令下的scrapy.cfg,也就是工程目录下的scrapy.cfg。这个文件的内容很简单:

[settings]
default = myspider.settings
[deploy]
#url = http://localhost:6800/
project = myspider

根据default = myspider.settings找到对应的配置模块,后面会执行一系列导入settings.py配置项的操作,过程其实很复杂,这里不再详细说了。

下一篇

你可能感兴趣的:(Python,爬虫)