资讯专栏INFORMATION COLUMN

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

silenceboy / 992人阅读

摘要:学习点的命令行工具实现了低耦合,需要删减增加哪个命令行只需要在模块中修改增删就可以实现。下一篇将会记录根据借鉴命令行工具实现方法来实现自己的命令行

其实这篇文章是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  [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  -h" to see more info about a command

在项目路径下的命令行新增了check、crawl、edit、list、parse这些命令,具体:

Scrapy 1.5.1 - project: myspider01

Usage:
  scrapy  [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  -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:scrapyscrapy.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_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命令行工具实现方法来实现自己的命令行

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/42650.html

相关文章

  • 从零开始写爬虫

    摘要:几个朋友对爬虫很感兴趣,他们也都是开发人员,一个开发两个开发,都没有过项目开发经验,正好其中一个最近要爬一个网店的产品信息,所以希望我能拿这网站当写一个爬虫来给他们参考学习。我们就在这个文件里开发爬虫的相关逻辑。 几个朋友对爬虫很感兴趣,他们也都是开发人员,一个PHP开发两个JAVA开发,都没有过python项目开发经验,正好其中一个最近要爬一个网店的产品信息,所以希望我能拿这网站当d...

    wwq0327 评论0 收藏0
  • 基于 Python Scrapy 爬虫入门:环境搭建

    摘要:一基础环境由于不是职业的开发者,因此环境是基于的。二安装打开命令行工具创建虚拟环境,默认情况下会创建目录,所有的虚拟环境都会产生一个子目录保存在此,里面包含基本程序文件以及库文件。 目录 基于 Python 的 Scrapy 爬虫入门:环境搭建 基于 Python 的 Scrapy 爬虫入门:页面提取 基于 Python 的 Scrapy 爬虫入门:图片处理 作为一个全栈工程师(...

    Gu_Yan 评论0 收藏0
  • scrapy提升篇之配置

    摘要:提升篇之配置增加并发并发是指同时处理的的数量。其有全局限制和局部每个网站的限制。使用级别来报告这些信息。在进行通用爬取时并不需要,搜索引擎则忽略。禁止能减少使用率及爬虫在内存中记录的踪迹,提高性能。 scrapy提升篇之配置 增加并发 并发是指同时处理的request的数量。其有全局限制和局部(每个网站)的限制。Scrapy默认的全局并发限制对同时爬取大量网站的情况并不适用,因此您需要...

    刘永祥 评论0 收藏0
  • 网络爬虫介绍

    摘要:什么是爬虫网络爬虫也叫网络蜘蛛,是一种自动化浏览网络的程序,或者说是一种网络机器人。 什么是爬虫 网络爬虫也叫网络蜘蛛,是一种自动化浏览网络的程序,或者说是一种网络机器人。它们被广泛用于互联网搜索引擎或其他类似网站,以获取或更新这些网站的内容和检索方式。它们可以自动采集所有其能够访问到的页面内容,以供搜索引擎做进一步处理(分检整理下载的页面),而使得用户能更快的检索到他们需要的信息。简...

    sf190404 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<