用途

SpiderLoader用来加载Spider类的。每个Spider类都有一个name属性,它就是Spider的名称。SpiderLoader可以根据Spider的名称,来获取其对应的Spider类。


例子

[root@iZj6chejzrsqpclb7miryaZ aaa]# cat test_spider_loader.py 
from scrapy.spiderloader import SpiderLoader

class Settings:
    def getlist(self, key):
        return ["aaa.spiders"]

    def getbool(self, key):
        return True

loader = SpiderLoader(Settings())
for spider_name in loader.list():
    print "spider name is:", spider_name

    spider_class = loader.load(spider_name)
    print "spider class is:", spider_class
[root@iZj6chejzrsqpclb7miryaZ aaa]# tree .
.
├── aaa
│   ├── __init__.py
│   ├── __init__.pyc
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   ├── settings.pyc
│   └── spiders
│       ├── baidu_spider.py
│       ├── baidu_spider.pyc
│       ├── __init__.py
│       ├── __init__.pyc
│       ├── sina_spider.py
│       └── sina_spider.pyc
├── scrapy.cfg
└── test_spider_loader.py

2 directories, 15 files
[root@iZj6chejzrsqpclb7miryaZ aaa]# python test_spider_loader.py 
spider name is: sina_spider
spider class is: <class 'aaa.spiders.sina_spider.SinaSpiderSpider'>
spider name is: baidu_spider
spider class is: <class 'aaa.spiders.baidu_spider.BaiduSpiderSpider'>

SpiderLoader源码解析

1,如何递归地扫描包:

from pkgutil import iter_modules

if __name__ == "__main__":
    for importer, name, ispkg in iter_modules(["/root/aaa/"]):
        print importer, name, ispkg
        print importer.find_module(name).load_module(name)

pkgutil的iter_modules()函数用来扫描给定的目录列表中的每一个目录,返回它们下面的模块或子包。
其中:

可以看到,iter_modules()不会递归地扫描子包,因此对上面的程序进行改进:

import os
from pkgutil import iter_modules

def walk_modules(path, base_package=""):
    for _, name, ispkg in iter_modules([path]):
        fullname = name
        if base_package:
            fullname = base_package + "." + name
        print "\033[31m%s\033[0m" % fullname
        if ispkg:
            walk_modules(os.path.join(path, name), fullname)

if __name__ == "__main__":
    walk_modules("/root/aaa/")

上面的walk_modules()函数就会递归地遍历子包了。下面再改进一下:

import pprint
import os
from pkgutil import iter_modules

def walk_modules(path, base_package=""):
    mods = []
    for _, name, ispkg in iter_modules([path]):
        fullname = name
        if base_package:
            fullname = base_package + "." + name
        mods.append(fullname)
        if ispkg:
            mods.extend(walk_modules(os.path.join(path, name), fullname))
    return mods

if __name__ == "__main__":
    pprint.pprint(walk_modules("/root/aaa/"))

上面是我们自己实现的:递归地扫描包 的方法。下面看一下scrapy的实现:
scrapy.utils.misc.walk_modules(path)

def walk_modules(path):
    mods = []
    # 1,先导入“基包”
    mod = import_module(path)
    mods.append(mod)

    # 2,每个包都有一个__path__属性,其定义了包内的模块搜索路径(它是一个列表,并且包目录一定在其中)
    if hasattr(mod, '__path__'):
        # 3,遍历每个搜索路径,得到其下的模块和子包
        for _, subpath, ispkg in iter_modules(mod.__path__):
            fullpath = path + '.' + subpath
            if ispkg:
                # 4.1,如果是子包,那么递归地进行处理
                mods += walk_modules(fullpath)
            else:
                # 4.2,如果是模块,那么导入它
                submod = import_module(fullpath)
                mods.append(submod)
    return mods

2:什么样的类是爬虫类:
scrapy.utils.spider.iter_spider_class(module)

# 爬虫类必须满足下面的条件
# 1,必须是一个新式类或经典类
# 2,必须是Spider的子类
# 3,从其他模块导入进来的Spider子类,不会被当作爬虫类
# 4,必须有name属性,且非空
def iter_spider_classes(module):
    from scrapy.spiders import Spider

    for obj in six.itervalues(vars(module)):
        if inspect.isclass(obj) and \
           issubclass(obj, Spider) and \
           obj.__module__ == module.__name__ and \
           getattr(obj, 'name', None):
            yield obj 

最后,解析SpiderLoader的主要代码:

class SpiderLoader(object):
    def __init__(self, settings):
        # “基包”列表。SpiderLoader会递归地导入每个“基包”及其包内搜索路径下,所有的模块和子包
        self.spider_modules = settings.getlist('SPIDER_MODULES')
        # True表示:当导入包或模块时,如果出现异常,只显示警告信息;否则,抛出该异常
        self.warn_only = settings.getbool('SPIDER_LOADER_WARN_ONLY')
        # 保存爬虫名称和爬虫类之间的映射关系。其中,key是爬虫名称,value是爬虫类
        self._spiders = {}
        # 爬虫的名称不能重复,该字典就是用来查重的
        self._found = defaultdict(list)

        # 加载所有的爬虫类
        self._load_all_spiders()

    def _load_spiders(self, module):
        for spcls in iter_spider_classes(module):
            self._found[spcls.name].append((module.__name__, spcls.__name__))
            self._spiders[spcls.name] = spcls

    def _load_all_spiders(self):
        for name in self.spider_modules:
            try:
                # 递归地导入包name及其包内搜索路径下,所有的模块和子包
                for module in walk_modules(name):
                    # 加载 包或模块中的爬虫类 到 self._spiders字典
                    self._load_spiders(module)
            except ImportError as e:
                if self.warn_only:
                    # 导入包或模块时,如果出现异常,那么只显示警告信息,然后继续处理下一个“基包”
                    msg = ("\n{tb}Could not load spiders from module '{modname}'. "
                           "See above traceback for details.".format(
                                modname=name, tb=traceback.format_exc()))
                    warnings.warn(msg, RuntimeWarning)
                else:
                    # 抛出异常
                    raise
        ...

    # 根据爬虫名称获取爬虫类
    def load(self, spider_name):
        """ 
        Return the Spider class for the given spider name. If the spider
        name is not found, raise a KeyError.
        """
        try:
            return self._spiders[spider_name]
        except KeyError:
            raise KeyError("Spider not found: {}".format(spider_name))