scrapy启动流程图(超详细)——cmdline.py解析

scrapy命令启动流程图

图片非常大,请耐心等一下。

图片详细介绍了 在使用 "scrapy crawl xxx" 启动命令背后的工作。

cmdline.py excute的这里可以看做整个scrapy项目的起点,

整体操作围绕着两个部分,一个是setting的配置

一个是crawler/crawprocess

大部分的需要配置的东西都在setting中传递。

带注释的代码:

import sys
import os
import optparse
import cProfile
import inspect
import pkg_resources

import scrapy
from scrapy.crawler import CrawlerProcess
from scrapy.commands import ScrapyCommand
from scrapy.exceptions import UsageError
from scrapy.utils.misc import walk_modules
from scrapy.utils.project import inside_project, get_project_settings
from scrapy.utils.python import garbage_collect


def _iter_command_classes(module_name):
    # TODO: add `name` attribute to commands and and merge this function with
    # scrapy.utils.spider.iter_spider_classes
    # XX实现从模块名字到模块的映射XX
    """ 判断传入的模块名称 ,在它的属性中判断
    是个类而且如果这个子模块是 Scrapycommand 的子类话 就抛出这个子模块
    其中walk_modules 是将一个模块内的所有可用部分列出,包括子模块的
    vars(k)函数返回的是一个ke包含的所有属性及其方法的字典 key是名字 value是值"""
    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__ # 是module下面的类 而不是其他模块下的类
                and not obj == ScrapyCommand #是继承后的而不是本体
            ):
                yield obj

# 给定一个模块 和是否在项目内的flag,检查_iter_command_classes(模块)返回的方法
# 也就是检查这个函数是否需要在 项目环境中运行
def _get_commands_from_module(module, inproject):
    d = {}
    for cmd in _iter_command_classes(module):
        # 在项目内时候,且目标函数不需要在项目内的时候 添加到函数字典中
        if inproject or not cmd.requires_project:#ScrapyCommand.requires_project 默认为false
            cmdname = cmd.__module__.split('.')[-1]
            d[cmdname] = cmd()
    return d

# 跟walkthrough很像,不过这里是给定一个group 返回他所有的 在group 下的 entry_point(类似于子类)
def _get_commands_from_entry_points(inproject, group='scrapy.commands'):
    cmds = {}
    # entry_point 相当于载入不同的类,比如一个类 可以由不同的参数初始化,这里就可以用
    # [qipaionweb.games]
    # doudizhu = doudizhu.game_impl:GameImpl
    # 方式来定义几个 entry_point
    # 后面函数需要调用某个不同的配置的类的时候就只需要
    # return pkg_resources.load_entry_point(doudizhu, qipaionweb.games, doudizhu) 就可以载入这个类 而不用在代码中
    # 自己做不同配置这麻烦事了
    # entrypoint 是在setup.py中定义的(不能确定)
    for entry_point in pkg_resources.iter_entry_points(group):
        obj = entry_point.load()
        if inspect.isclass(obj):
            cmds[entry_point.name] = obj()
        else:
            raise Exception(f"Invalid entry point {entry_point.name}")
    return cmds


def _get_commands_dict(settings, inproject):
    """ 从上面的两个方法中拿到所有的模块,同时如果setting
    中有 'COMMANDS_MODULE' 在通过方法把这些模块键入到模块列表中"""
    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 _pop_command_name(argv):
    i = 0
    for arg in argv[1:]:
        if not arg.startswith('-'):
            del argv[i]
            return arg
        i += 1


def _print_header(settings, inproject):
    version = scrapy.__version__
    if inproject:
        print(f"Scrapy {version} - project: {settings['BOT_NAME']}\n")
    else:
        print(f"Scrapy {version} - no active project\n")

#将对应所有的方法打印出来
def _print_commands(settings, inproject):
    _print_header(settings, inproject)
    print("Usage:")
    print("  scrapy <command> [options] [args]\n")
    print("Available commands:")
    cmds = _get_commands_dict(settings, inproject)
    for cmdname, cmdclass in sorted(cmds.items()):
        print(f"  {cmdname:<13} {cmdclass.short_desc()}")
    if not inproject:
        print()
        print("  [ more ]      More commands available when run from project directory")
    print()
    print('Use "scrapy <command> -h" to see more info about a command')


def _print_unknown_command(settings, cmdname, inproject):
    _print_header(settings, inproject)
    print(f"Unknown command: {cmdname}\n")
    print('Use "scrapy" to see available commands')

#调用函数 出错的话 用parser 传递相应问题
def _run_print_help(parser, func, *a, **kw):
    try:
        func(*a, **kw)
    except UsageError as e:
        if str(e):
            parser.error(str(e))
        if e.print_help:
            parser.print_help()
        sys.exit(2)

# *核心*
def execute(argv=None, settings=None):
    # 是否用其他方式传入命令参数,否的话使用命令行参数
    if argv is None:
        argv = sys.argv

    if settings is None:
        # 没指定setting的话,就调用默认方法
        settings = get_project_settings()# 从scrapy.cfg载入setting 再从环境中载入scrapy相关的setting到setting对象里
        # set EDITOR from environment if available 用于编辑文件
        try:
            editor = os.environ['EDITOR']
        except KeyError:
            pass
        else:
            settings['EDITOR'] = editor

    inproject = inside_project()#判断是否在项目内(用是否能找到scrapy.cfg来判断)
    cmds = _get_commands_dict(settings, inproject) #拿到当前状态下所有可用模块
    cmdname = _pop_command_name(argv) #从命令行里拿到指向模块那个
    parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(),
                                   conflict_handler='resolve')
    #指定一个 optparse 解析器
    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 = f"scrapy {cmdname} {cmd.syntax()}"
    parser.description = cmd.long_desc()
    #将命令中的设置弄到setting中
    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) #主要是写一些setting
    # 生成CrawlerProcess
    cmd.crawler_process = CrawlerProcess(settings)
    # 运行crawler_process 对应的命令
    _run_print_help(parser, _run_command, cmd, args, opts) #调用command的run启动这两个参数
    sys.exit(cmd.exitcode)


def _run_command(cmd, args, opts):
    if opts.profile:
        _run_command_profiled(cmd, args, opts)
    else:
        cmd.run(args, opts)

# 用cpython的porfiler 运行 cmd.run(args, opts) 并传入变量
def _run_command_profiled(cmd, args, opts):
    if opts.profile:
        sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
    loc = locals() #拿到所有变量
    p = cProfile.Profile()
    p.runctx('cmd.run(args, opts)', globals(), loc) #调用函数 并传入 global 和 locals 中的变量
    if opts.profile:
        p.dump_stats(opts.profile)


if __name__ == '__main__':
    try:
        execute()
    finally:
        # Twisted prints errors in DebugInfo.__del__, but PyPy does not run gc.collect() on exit:
        # http://doc.pypy.org/en/latest/cpython_differences.html
        # ?highlight=gc.collect#differences-related-to-garbage-collection-strategies
        garbage_collect()

代码难度不是特别大,但是深度很深,容易让人迷失。

其中有兴趣了解twisted框架的朋友可以看我上一篇文章,有crawler代码的注释和解释

不想了解twised可以理解成初始化CrawlProcess启动了另一个进程

其中spider和各种setting已经配置好了,准备开始爬了。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值