美文网首页
scrapy源码学习笔记

scrapy源码学习笔记

作者: 异同 | 来源:发表于2019-11-19 20:57 被阅读0次

    0.execute

    观察execute源代码(\site-packages\scrapy\cmdline.py中的execute函数)

    def execute(argv=None, settings=None):
       ----part1----
      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)
        ----part2----
        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)
        ----part3----
        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)
    
    

    part1:

    例如在python中通过爬虫入口脚本execute(["scrapy","crawl","crawler-name"])爬取数据,此时argv=["scrapy","crawl","crawler-name"]settings=None
    第一部分中会对argv做空值判断处理,并通过settings对系统环境变量的一些配置进行编辑和修改(不进行设置的话,将会在下面的if条件中做判断,并返回默认项目设置,即get_project_settings())。

    part2:

    scrapy中是否在项目环境中运行代码,其具有的command命令也是不一样的。例如在命令行中敲入scrapy时,最下方会有一个[more],提示意思就是说如果当前命令行是在项目目录时,会具有除了benchfetchgenspiderrunspidersettingsshellstartprojectversionview这9个方法之外其他的方法的。一个简单的例子就是,当你在任意目录中输入scrapy crawl时会提示没有crawl这个command,而你在爬虫项目目录中时则会运行爬虫程序。

    普通目录下的命令行
    爬虫项目目录下的命令行

    inproject = inside_project():这里调用的函数,就是判断当前运行环境是否是在一个爬虫项目中(在inside_project()函数中,可以很清晰的看到返回的是一个布尔值)

    cmds = _get_commands_dict(settings, inproject):这里的_get_commands_dict函数写的比较复杂,不过不是说原理复杂,而是这个函数会进行很多层地调用,如果一层层地看很容易看晕。但是这个函数的目的简单说就是找到当前环境(目录或项目)所具有的全部命令,或着说找到当前具有使用权限的全部命令,这样可能更容易理解一些。

    cmdname = _pop_command_name(argv):这里调用了_pop_command_name()函数,这个函数是在传入的参数找到第一个非"-"开头的字符串,并返回这个字符串。我们看一下这个函数:

    def _pop_command_name(argv):
        i = 0
        for arg in argv[1:]:
            if not arg.startswith('-'):
                del argv[i]
                return arg
            i += 1
    

    这个函数设计的考量我目前不是很明白,但是功能还是很清晰的。我们在列表argv(例如["scrapy","crawl","xxx"])中,从第二个(因为根据scrapy命令行习惯,我们第一个字符串一般都是scrapy,第二个才是startproject、crawl等,第三个是其他字符串如路径、爬虫文件名等)开始查找第一个非"-"开头的字符串,作为控制命令返回回去。例如["scrapy","crawl","xxx"]我们将返回crawl。因为在命令行控制中,"-"一般多做为辅助参数来配合控制命令,例如scarpy crawl -nolog,这样将会指示爬虫在爬取时不记录日志信息。
    那这个函数简单理解就是在参数列表中从一个开始查找非"-"开头的字符串,并返回。其实我个人理解,是因为存在一个基础的命令"scrapy -h",需要把这里的-h排除出去,因此才有了这么一串代码。不过这个函数的写法感觉还是有点不太理解,而且根据这个写法以及下面几个部分的内容,我发现["scrapy","crawl","xxx"]里第一个字符串其实无关紧要,改成其他的一样可以正常运行。
    另外这个函数需要关注的一点是,它使用del方法,修改了argv列表,也就是说如果找到了命令对应的字符串,将会在返回这个字符串同时将argv中这个字符串删掉。例如["scrapy","crawl","xxx"]在执行了这个方法以后将会返回"crawl"字符串,而原始的列表将会变成["scrapy","xxx"]。

    parser = optparse.OptionParser(...):这里定义了一个parser,具体各个参数的意义目前先不用关注,只需要明白这是一个拿来做字符串命令解析用的就行了。
    part2中最后面的这个if,是通过之前获取的全部命令列表,以及我们输入的命令(argv列表的第二个字符串),进行错误检测的。例如我们输入的命令不在可用命令列表中打印某些信息,又例如没有提取到我们输入的命令是打印另外的一些信息。

    part3

    part3这里涉及了其他包其他类的使用方法:
    cmd = cmds[cmdname]cmds是一个字典,键代表这个命令的名称,值是ScrapyCommand类的一个对象(是当前环境具有的,scrapy预设的一系列命令类)。通过cmd = cmds[cmdname]获取到这个命令类,即通过我们输入的想要执行命令的字符串来获得对应的命令类。

    parser.usage = "scrapy %s %s" % (cmdname, cmd.syntax())
    parser.description = cmd.long_desc()
    上面这两行,根据我们的命令类cmd构建了parser对象,这个对象是用于检测我们输入的命令字符串是否是符合这个命令所规定的格式的。

    settings.setdict(cmd.default_settings, priority='command')
    这里是环境配置ssetting等有关内容,暂时不关心这行代码。

    cmd.settings = settings :环境变量等相关,暂不关注
    cmd.add_options(parser):给cmd对象的add_options添加一个parser,主要是用于后面的option参数处理使用的。
    opts, args = parser.parse_args(args=argv[1:]):使用parser解析argv列表,获取命令的option和args(还记得之前使用_pop_command_name函数将命令字符串删掉了么)。一个简单的例子,原始的argv=["scrapy","crawl","--nolog","xxxx"],经过_pop_command_name函数后抛出命令字符串"crawl",argv只剩下["scrapy","--nolog","xxxx"],而通过parser后解析option为"--nolog",args为"xxxx"。

    _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)
    这里的_run_print_help(parser, func, *a, **kw)函数内部很简单,就是尝试运行以a和kw为参数的函数func,如果运行失败,则通过parser.error解析错误并打印出来或通过parser.print_help直接打印出帮助文本。
    第一行的_run_print_help只处理了cmd.process_options,这个函数观察其代码,只是对cmd对象的环境变量settings进行了设置,并未执行cmd命令。
    第二行创建了一个CrawlerProcess对象,并把它绑定到了cmd的crawler_process属性上。
    第三行执行cmd命令,这时我们已经给cmd指定了环境变量settings,也指定了用于爬虫处理的CrawlerProcess对象。这也就将命令行与爬虫项目联系了起来。

    1.关于command

    在execute里,把命令行与爬虫项目关联起来了,那么具体一些来说是怎么运行的呢?
    观察execute()里cmd的来源:

    cmd = cmds[cmdname] 
    #cmd是cmds字典中的一个值
    cmds = _get_commands_dict(settings, inproject)
    #_get_commands_dict会从调用一个_get_commands_from_module()方法获得cmds
    def _get_commands_dict(settings, inproject):
        cmds = _get_commands_from_module('scrapy.commands', inproject)
        ...
    def _get_commands_from_module(module, inproject):
        ...
        for cmd in _iter_command_classes(module):
        ...
    def _iter_command_classes(module_name):
        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
    def walk_modules(path):
    """Loads a module and all its submodules from the given module path and
        returns them. If *any* module throws an exception while importing, that
        exception is thrown back.
    
        For example: walk_modules('scrapy.utils')
        """
    

    1).cmd通过cmds字典获得了我们指定的控制命令。
    2).cmds字典是由_get_commands_dict函数及传入的环境变量参数得到的。
    3).而_get_commands_dict函数是由_get_commands_from_module实现的,根据名字我们很清楚的知道这是通过模型module来获取一系列命令的函数。
    4).这个module是什么呢?就是我们在_get_commands_from_module里传入的参数'scrapy.commands'。
    5).而_get_commands_from_module这个函数,返回了一个封装成键为命令名称,值为_iter_command_classes函数返回值的字典。也就是cmds的雏形了(在_get_commands_dict里还有对这个字典的其他更新和加工,主要是和环境变量配置有关的,我们暂时不关心这个)。
    6)._iter_command_classes这个函数的返回值,很明显是一个object对象,看代码内容主要就是通过遍历walk_modules(module_name),并对walk_modules的返回结果进行加工,返回去一个object对象。
    到这里我们就知道了,cmds是键为控制命令字符串,值为object对象的一个字典。而根据代码,可以看到这是ScrapyCommand(子)类的一个对象。
    7).walk_modules(path)已经是一个非常靠近底层的函数了,根据其注释可以知道,这个函数参数其实是一个路径,返回的是在这个路径(包)下面的全部module(模块)的一个列表,每个模块下面都是有很多相关的实例对象(也因此可以看出来_iter_command_classes函数实际上就是从这些module中将所需的实例对象提取出来)。
    总结:实际上跟着_get_commands_from_module函数的第一个参数'scrapy.commands'一路走下去,就很清楚了。其实就是通过指定的路径去获得一系列模块,然后选择并返回我们所需的属于ScrapyCommand(子)类对象的那一部分,然后将其封装成一个字典,通过键返回值的形式来获得对应的cmd。

    walk_modules

    2.crawl

    假如我们需要进行爬取操作,按照标准用法需要通过命令行"scarpy crawl crawl_name",或是通过execute(["scrapy","crawl","crawl_name"])启动爬虫。根据我们在上面的分析,此时的cmd就是scrapy/commands/crawl.py模块的一个对象(class Command(ScrapyCommand),即继承ScrapyCommand的一个Command对象)。这个对象的方法有语法解析syntax、简介short_desc、变量配置add_options、处理配置信息process_options、运行run这几个方法。
    那execute()函数下面的几行也就说的通了:
    _run_print_help(parser, cmd.process_options, args, opts)这一行将执行参数变量的配置。
    cmd.crawler_process = CrawlerProcess(settings)这一行则会实例化一个CrawlerProcess对象。
    _run_print_help(parser, _run_command, cmd, args, opts),之所以上面要实例化这个CrawlerProcess对象,就是因为最后这一行将会调用cmd的run方法,而run方法实际上内部是调用了self.crawler_process.crawl(spname, **opts.spargs)
    self.crawler_process.start()这两个,来启动爬虫:

        def _run_command(cmd, args, opts):
              if opts.profile:
                _run_command_profiled(cmd, args, opts)
            else:
                cmd.run(args, opts)
        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()
    
            if self.crawler_process.bootstrap_failed:
                self.exitcode = 1
    

    3.CrawlerProcess

    绕了一大圈,爬虫的启动还是回到了CrawlerProcess对象的crawl和start方法。我们来看一下这个CrawlerProcess是什么样的。

    相关文章

      网友评论

          本文标题:scrapy源码学习笔记

          本文链接:https://www.haomeiwen.com/subject/cblwictx.html