Scrapy-實用的命令行工具實現方法

其實這篇文章是scrapy源碼學習的(一),加載器那篇纔是(二)

scrapy的命令行工具

本文環境:python

  • wind7 64bits
  • python 3.7
  • scrapy 1.5.1

scrapy擁有很是靈活的低耦合的命令行工具,若是本身想要從新實現覆蓋掉scrapy自帶的命令也是能夠的。
使用它的命令行工具能夠大體分爲兩種狀況:shell

  • 在建立的project路徑下
  • 不在project路徑下

先看下不在scrapy項目路徑下的命令行有哪些:api

Scrapy 1.5.1 - no active project

Usage:
  scrapy <command> [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 <command> -h" to see more info about a command

在項目路徑下的命令行新增了check、crawl、edit、list、parse這些命令,具體:app

Scrapy 1.5.1 - project: myspider01

Usage:
  scrapy <command> [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 <command> -h" to see more info about a command

也便是說scrapy能夠根據當前路徑是不是scrapy項目路徑來判斷提供可用的命令給用戶。scrapy

建立一個scrapy項目

在當前路徑下建立一個scrapy項目,DOS下輸入:ide

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函數做爲入口函數。fetch

分析入口函數

先瀏覽一下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:\scrapy\scrapy.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_name> [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命令行工具實現方法來實現本身的命令行

相關文章
相關標籤/搜索