Scrapy 规则化爬虫
前文我们了解了 Scrapy 中 Spider 的用法,在实现 Spider 的过程中,我们需要定义特定的方法完成一系列操作,比如生成 Response,解析 Response,生成 Item 等,由于整个过程是由代码实现的,所以逻辑控制比较灵活,但是可扩展性和可维护性相对比较差。
试想,如果我们现在要实现对非常多站点的爬取,比如爬取各大站点的新闻内容,那么可能需要为每个站点单独创建一个 Spider。然后在 Spider 中定义爬取列表页,详情页的逻辑。其实这些 Spider 的基本实现思路是差不多的,可能包含很多重复代码,因此可维护性就变得比较差。
如果我们可以保留各个站点的 Spider 的公共部分,提取不同的部分进行单独配置(如将爬取规则、页面解析方式等抽离出来,做成一个配置文件),那么我们在新增一个爬虫的时候,只需要实现这些网站的爬取规则和提取规则,而且还可以单独管理和维护这些规则。
本节,我们就来探究一下 Scrapy 规则化爬虫的实现方法。
CrawlSpider
在实现规则化爬虫之前,我们需要了解一下 CrawlSpider 用法。它是 Spider 类的子类,利用它我们可以方便地实现站点的规则化爬取,其官方文档链接为: http://scrapy.readthedoes.io/en/latest/topics/spiders.html#ctawlspider 。
在 CrawlSpider 里,我们可以指定特定的爬取规则来实现页面的解析和爬取逻辑,这些规则由一个专门的数据结构 Rule 表示。Rule 里包含提取和跟进页面的配置,CrawlSpider 会根据 Rule 来确定当前页面中些链接需要继续爬取,哪此页面的爬取结果需要用哪个方法解析等。
CrawlSpider 继承自 Spider 类,除了 Spider 类的所有方法和属性,它还提供了一个非常重要的属性 rules。rules 是爬取规则属性,是包含一个或多个 Rule 对象的列表。每个 Rule 对爬取网站的规则都做了定义,CrawlSpider 会读取 rules 的每一个 Rule 并执行对应的爬取逻辑。
它的定义和参数如下所示:
class scrapy.spiders.Rule(link_extractor=None, callback=None, cb_kwargs=None, follow=None, process_links=None,process_request=None, errback=None)
下面对其参数依次说明。
-
link_extractor:一个 LinkExtractor 对象。通过它,Spider 可以知道从爬取的页面中提取哪些链接进行后续爬取,提取出的链接会自动生成 Request,这些提取逻辑依赖 LinkExtractor 对象里面定义的各种属性,下文会具体介绍。
-
callback:回调方法,和之前定义 Request 的 callback 有相同的意义。每次从 link_extractor 中提取到链接时,该方法将会被调用。该回调方法接收 response 作为其第一个参数并返回一个包含 Item 或 Request 对象的列表。需要注意的是,避免使用 parse 方法作为回调方法,因为 CrawlSpider 使用 parse 方法来实现其解析逻辑,如果 parse 方法被重写了,CrawlSpider 可能无法正常运行。
-
cb_kwargs:一个字典类型,使用它我们可以定义传递给回调方法的参数。
-
follow:一个布尔值,它指定根据该规则从 response 提取的链接是否需要跟进爬取。跟进的意思就是将提取到的链接进一步生成 Request 进行爬取;如果不跟进的话,一般可以定义回调方法解析内容,生成 Item。如果 callback 参数为 None,follow 值默认设置为 True,否则默认为 False。
-
process_links:可以是一个 callable 方法,也可以是一个字符串(需要和 CrawlSpider 里面定义的方法名保持一致)。它用来处理该 Rule 中的 link_extractor 提取到的链接,比如可以进行链接的的过滤或对链接进行进一步修改。
-
process_request:可以是一个 callable 方法,也可以是一个字符串(需要和 CrawlSpider 里面定义的方法名保持一致)。根据该 Rule 提取到每个后续 Request 时,该方法都会被调用,该方法可以对 Request 进行进一步处理,必须返回 Request 对象或者 None。
-
Errback:该参数是 Scrapy 2.0 版本之后新增的参数,它也可以是一个 callable 方法,也可以是一个字符串(需要和 CrawlSpider 里面定义的方法名保持一致)。当该 Rule 提取出的 Request 在被处理的过程中发生错误时,该方法会被调用,该方法第一个参数接收一个 TwistedFailure 对象。
以上内容便是 CrawlSpider 中的核心数据结构 Rule 的基本用法,利用 Rule 我们可以方便地实现爬取逻辑的规则化。
LinkExtractor
上文我们了解了 Rule 的基本用法,其中一个重要的属性就是 link_extractor,下面我们再来专门了解一下它的用法。
LinkExtractor 定义了从 Response 中提取后续链接的逻辑,在 Scrapy 中它指的就是 scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor 这个类,为了方便调用,Scrapy 为其定义了一个别名,叫作 LinkExtractor,二者是指的都是 LxmlLinkExtractor 。
LxmlLinkExtractor 接收多个用于提取链接的参数,下面依次对其进行说明。
-
allow:一个正则表达式或正则表达式列表,它定义了从当前页面提取出的链接需要符合的规则,只有符合对应规则的链接才会被提取。
-
deny:和 allow 正好相反,它也是一个正则表达式或正则表达式列表,定义了从当前页面中禁止被提取的链接对应的规则,相当于黑名单,它的优先级比 allow 高。
-
allow_domains:定义了符合要求的域名,只有此域名的链接才会被提取出来,它相当于域名白名单。
-
deny_domains:和 allow_domains 相反,相当于域名黑名单,该域名所对应的链接都不会被提取出来。
-
deny_extensions:在提取链接的过程中可能会遇到一些特殊的后缀,即扩展名。deny_extensions 定义了后缀黑名单,包含这些后缀的链接都不会被提取出来。deny_extensions 的默认值由 scrapy.linkextractors.IGNORED_EXTENSIONS 变量定义,在 Scrapy 2.0 中,IGNORED_EXTENSIONS 包含了 7z、7zip、apk、bzz、cdr、dmg、ico、iso、tar、tar.gz,webm、xz 等类型,这些后缀的链接都会被忽略。
-
restrict_xpaths:如果定义了该参数,那么 Spider 将会从当前页面中 XPath 匹配的区域提取链接,其值是 XPath 表达式或 XPath 表达式列表。
-
restrict_css:和 restrict_xpaths 类似,如果定义了 restrict_css,Spider 将会从当前页面中 CSS 选择器匹配的区城提取链接,其值是 CSS 选择器或 CSS 选择器列表。
-
tags:指定了从什么节点中提取链接,默认是('a','area'),即从 a 节点和 area 节点中提取链接。
-
attrs:指定了从节点的什么属性中提取链接,默认是('href',),和 tags 属性配合起来,那将会从 a 节点和 area 节点的 href 属性中提取链接。比如我们需要从 img 节点的 src 属性中提取链接,那可以将 tags 定义为('a','area','img'),attrs 定义为('href','src')。
-
canonicalize:是否需要对提取到的链接进行规范化处理,处理流程借助 w3lib.url.canonicalize_url 模块,该参数默认为 False。
-
unique:是否需要对提取到的链接进行去重,默认是 True。
-
process_value:是一个 callable 方法,可以通过这个方法来定义一个逻辑,这个逻辑负责完成提取内容到最终链接的转换。比如说 href 属性里面的值是一段 JavaScript 变量,值为 javascript:goToPage('../other/page.html'),这明显不是一个有效的链接,process_value 对应的方法可以接收这个值并对这个值进行处理,提取真实的链接再返回。
-
strip:如果从节点对应的属性值中提取到了结果,是否要去掉首尾的空格,默认是 True。
以上便是 LinkExtractor 的一些参数的用法,其中前几个参数使用频率较高,可以重点关注。有关 LinkExtractor 更详细的介绍可以参考官方文档: http://scrapy.readthedocs.io/en/latest/topics/link-extractors.html#module-scrapy.linkextractors.Ixmlhtml 。
Item Loaders
我们了解了利用 CrawlSpider 的 Rule 来定义页面的爬取逻辑,这是可配置化的一部分内容,借助 Rule,我们可以实现页面内容的提取和爬取逻辑。但是,Rule 并没有对 Item 的提取方式做规则定义。对于 Item 的提取,我们需要借助另一个模块 Item Loaders 来实现。
可以这么理解,Item 提供的是保存抓取数据的容器,而 Item Loaders 提供的是填充容器的机制。
尽管 Item 可以直接由代码进行构造,但 Item Loaders 提供一种便捷的机制来帮助我们方便地提取 Item,它提供了更灵活、可扩展的机制来实现 Item 的提取逻辑,同时也有助于我们实现爬虫的规则化。
Item Loaders 的用法如下所示:
class scrapy.loader.ItemLoader([item, selector, response,] **kwargs)
这里我们使用的 Scrapy 提供的 ItemLoader 类,ItemLoader 的返回一个新的 ItemLoader 来填充给定的 Item。如果没有给出 Item,则使用 default_item_class 中的类自动实例化。另外,它传入 selector 和 response 参数来使用选择器或响应参数实例化。
下面将依次说明 Item Loader 的参数。
-
item:Item 对象,可以调用 add_xpath、add_css 或 add_value 等方法来填充 Item 对象。
-
selector:Selector 对象,用来提取填充数据的选择器。
-
response:Response 对象,用于使用构造选择器的 Response。
一个比较典型的 ItemLoader 实例如下:
from scrapy.loader import ItemLoader
from project.items import Product
def parse(self, response):
loader = ItemLoader(item=Product(),response=response)
loader.add_xpath('name','//div[@class="product_name"]')
loader.add_xpath('name','//div[@class="product_title"]')
loader.add_xpath('price', '//p[@id="price"]')
loader.add_css('stock', 'p#stock]')
loader.add_value('last_updated','today')
return loader.load_item()
这里首先声明一个 Product Item,用该 Item 和 Response 对象实例化ItemLoader,调用 add_xpath 方法把来自两个不同位置的数据提取出来,分配给 name 属性,再用 add_xpath、add_css、add_value 等方法对不同属性依次赋值,最后调用 load_item 方法实现对 Item 的解析。这种方式比较规则化,我们可以把一些参数和规则单独提取出来,做成配置文件或存到数据库,实现可配置化。
另外,Item Loader 的每个字段中都包含了一个 Input Processor(输入处理器)和一个 Output Processor(输出处理器),利用它们我们可以灵活地对 Item 的每个字段进行处理。Input Processor 收到数据时立刻提取数据,Input Processor 的结果被收集起来并且保存在 ItemLoader 内,但是不分配给 Item,收集到所有的数据后,load_item 方法被调用来填充再生成 Item 对象。在调用时会先调用 Output Processor 来处理之前收集到的数据,然后再存入 Item 中,这样就生成了 Item。
类似的用法如下:
from itemloaders.processors import TakeFirst,MapCompose,Join
from scrapy.loader import ItemLoader
class ProducuItemLoader(ItemLoader):
default_output_processor = TakeFirst()
name_in = MapCompose(unicode.title)
name_out = Join()
price_in = MapCompose(unicode.strip)
这里我们定义了一个 ProductItemLoader 继承了 ItemLoader 类,并定义了几个属性的 Input Processor 和 Output Processor,比如 name 属性的 Input Processor 就使用了 MapCompose,Output Processor 就使用了 Join,这样利用 ProductItemLoader,我们就可以灵活地实现特定属性的数据收集和处理。
另外可以看到这里用到了 TakeFirst、MapCompose、Join,这些都是 Scrapy 提供的一些 Processor,分别可以实现提取首个内容,选代处理,字符串拼接的操作,利用这些 Processor 的组合,我们可以灵活地实现对特定字段数据的处理。
其实 Scrapy 已经给我们提供了不少 Processor,我们来了解一下。
-
Identity Identity 是最简单的 Processor,不进行任何处理,直接返回原来的数据。
-
TakeFirst TakeFirst 返回列表的第一个非空值,类似 extract_first 的功能,常用作Output Processor,示例代码如下:
from scrapy.loader.processors import TakeFirst
processor = TakeFirst()
print(processor(['',1,2,3]))
输出结果如下所示:
1
经过此 Processor 处理后的结果返回了第一个不为空的值。
-
Join Join 方法相当于字符串的 join 方法,可以把列表拼合成字符串,字符串默认使用空格分隔,示例代码如下:
from scrapy.loader.processors import Join
processor = Join()
print(processor(['one','two','three']))
输出结果如下:
one two three
它也可以通过参数更改默认的分隔符,例如改成逗号:
from scrapy.loader.processors import Join
processor = Join(',')
print(processor(['one','two','three']))
输出结果如下:
one,two,three
-
Compose
Compose 是使用多个函数组合构造而成的 Processor,每个输入值被传递到第一个函数,其输出再传递到第二个函数,以此类推,直到最后一个函数返回整个处理器的输出,示例代码如下:
from scrapy.loader.processors import Compose
processor = Compose(str.upper,lambda s : s.strip))
print(processor('hello world'))
运行结果如下:
HELLO WORLD
在这里我们构造了一个 Compose Processor,传入一个开头带有空格的字符串。Compose Processor 的参数有两个:第一个是 str.upper,它可以将字母全部转为大写:第二个是一个匿名函数,它调用 strip 方法去除头尾空白字符。Compose 会顺次调用两个参数,最后返回结果的字符串全部转化为大写并且去除了开头的空格。
-
MapCompose
与 Compose 类似,MapCompose 可以选代处理一个列表输人值,示例代码如下:
froms crapy.loader.processors import MapCompose
processor=MapCompose(str.upper, lambda s : s.strip())
print(processor(['Hello','world','python']))
运行结果如下:
['HELLO', 'WORLD', 'PYTHON']
被处理的内容是一个可迭代对象,MapCompose 会将该对象遍历然后依次处理。
-
SelectJmes
SelectJmes 可以查询 JSON,传入 Key,返回查询所得的 Value。不过需要先安装 jmespath 库才可以使用它,安装命令如下:
pip3 install jmespath
安装好 jmespath 之后,便可以使用这个 Processor 了,示例代码如下:
from scrapy.loader.processors import SelectJmes
processor = SelectJmes('foo')
print(processor({'foo':'bar'}))
运行结果如下:
bar
以上内容便是 ItemLoader 和一些常用的 Processor 的用法。
我们一下子又接触了不少新概念,如 CrawlSpider、Rule、LinkExtractor、ItemLoaders、Processor,你可能感觉有点槽,不知道如何使用。不用担心,下面我们通过一个实例来将这此内容综合运用一下,实现一个规则化的 Scrapy 爬虫。
本节目标
本节我们以前文所爬取过的电影示例网站作为练习来实现一下 Scrapy 规则化爬虫的实现方式,爬取的目标站点是 https://ssr1.scrape.center/ ,如图 15-18 所示。
(图略)
和之前不同,这次我们需要利用 CrawlSpider、Rule、Item Loaders 等实现对该站点的爬取,最后我们还需要将爬取规则进行进一步的抽取,变成 JSON 文件,实现爬取规则的灵活可配置化。
在开始之前请确保已经安装好了 Scrapy 框架。
实战
首先新建一个 Scrapy 项目,名为 scrapyuniversaldemo,命令如下所示:
scrapy startproject scrapyuniversaldemo
然后我们进人到该文件夹,这次我们便需要创建一个 CrawlSpider,而不再单纯的是 Spider。要创建CrawlSpider,需要指定一个模板。
我们可以先看看有哪些可用模板,命令如下所示:
scrapy genspider -l
运行结果如下所示:
Availabletemplates:
basic
crawl
xmlfeed
csvfeed
之前创建 Spider 的时候,我们默认使用了第一个模板 basic。这次要创建 CrawlSpider,需要使用第二个模板 crawl,创建命令如下所示:
scrapy genspider -t crawl moviessr1.scrape.center
运行之后便会生成一个 CrawlSpider,其内容如下所示:
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class MovieSpider(CrawlSpider):
name = 'movie'
allowed_domains = ['ssr1.scrape.center']
start_urls = ['http://ssr1.scrape.center/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), follow=True, callback='parse_item'),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
这次生成的 Spider 内容多了一个对 rules 属性的定义。Rule 的第一个参数是 LinkExtractor,就是上文所说的 LxmlLinkExtractor。同时,默认的回调方法也不再是 parse,而是 parse_item。在 parse_item 里面定义了 Response 的解析逻辑,用于生成 Item 。
接下来我们需要完善一下 Rule,使用 Rule 来定义好爬取逻辑和解析逻辑,下面我们来一步步实现这个过程。
由于当前需要爬取的目标网站的首页就是第一页列表页,所以 start_urls 这边我们不需要做额外更改了。运行该 CrawlSpider,CrawlSpider 就会从首页开始爬取,得到首页 Response 之后,CrawlSpider 便会使用 rules 属性里面配置的 Rule 从 Response 中抽取下一步需要爬取的链接,生成进一步的 Request。所以,接下来我们就需要配置 Rule 来指定下一步的链接提取和爬取逻辑了。
我们再看下页面的源代码,如图15-19所示。
(图略)
我们要提取的详情页链接处于 class 为 item 对应的节点中,对应的是 class 为 name 的 a 节点,其中 href 属性就是需要提取的内容,每页有 10 个。
此处我们可以用 LinkExtractor 的 restrict_css 属性来指定要提取的链接所在的位置,之后 CrawlSpider 就会从这个区域提取所有的超链接并生成 Request。默认情况下会提取所有 a 节点和 area 节点的 href 属性,符合我们的需求,所以无须额外配置 tags、attrs。
接下来我们将 rules 修改为如下内容:
rules = (
Rule(LinkExtractor(restrict_css='.item .name'), follow=True, callback='parse_detail'),
)
这里我们指定了 LinkExtractor 并声明了 restrict_css 属性,另外 follow 属性设置为 True 代表 Spider 需要跟进这些提取到的链接进行爬取,同时还指定了 callback 为字符串 parse_detail,这样提取到的链接被爬取之后会回调 parse_detail 方法进行解析。
这里我们可以简单定义一个 parse_detail 方法打印输出被肥爬取到的链接内容:
def parse_detail(self,response):
print(response.url)
运行一下当前 CrawlSpider,命令如下:
scrapy crawl movie
便可以看到如下输出:
(省略)
由于内容过多,这里省略了部分输出结果。我们可以看到首页对应的 10 个详情页链接就被提取出来了,同时这些链接又被进一步构造成了 Request 执行了爬取,爬取成功后,通过回调 parse_detail 方法,打印输出了对应的链接。这些逻辑我们通过一个简单的 Rule 的配置就完成了,是不是感觉比之前方便多了?
好,到现在我们仅仅爬取了首页的内容,后续的列表页怎么力呢?不用担心,我们可以定义另外一个 Rule 来实现翻页。
我们再看下一页的页面源码,查看下一页链接对应的节点信息,如图 15-20 所示。
(图略)
这里可以观察到,下一页链接对应的是 class 为 next 的 a 节点,其 href 属性就是下一页的内容相似地,我们可以修改 Rule 为如下内容:
rules = (
Rule(LinkExtractor(restrict_css='.item .name'), follow=True, callback='parse_detail'),
Rule(LinkExtractor(restrict_css='.next'), follow=True),
)
这里我们又增加了一条 Rule,定义了 restrict_css 为 .next,同时指定了 follow 为 True。但因为这次我们不需要从列表页提取 Item,所以这里我们无须额外指定 callback。
这样整个爬取逻辑就已经定义好了,我们重新运行一下 CrawlSpider,可以看到 CrawlSpider 就可以爬取分页信息了,输出结果如下:
(省略)
接下来我们需要做的就是解析页面内容了,刚才我们只是简单定义了 parse_detail 方法,下面我们来使用 Item Loaders 实现内容的提取。
首先我们还是需要定义一个 MovieItem,内容如下:
from scrapy import Field, Item
class MovieItem(Item):
name = Field()
cover = Field()
categories = Field()
published_at = Field()
drama = Field()
score = Field()
这里的字段分别指电影名称、封面、类别、上映时间、剧情简介,评分,定义好 MovieItem 之后,我们如果不使用 ItemLoader 正常提取内容,就直接调用 response 变量的 xpath、css 等方法即可,parse_detail 方法可以实现为如下内容:
def parse_detail(self, response):
item = MovieItem()
item['name'] = response.css('.item h2::text').extract_first()
item['categories'] = response.css('.categories button span::text').extract()
item['cover'] = response.css('.cover::attr(src)').extract_first()
item['published_at'] = response.css('.info span::text').re_first('(\d{4}-\d{2}-\d{2})\s?上映')
item['score'] = response.xpath('//p[contains(@class, "score")]/text()').extract_first().strip()
item['drama'] = response.xpath('//div[contains(@class, "drama")]/p/text()').extract_first().strip()
yield item
这样我们就把每条新闻的信息提取形成了一个 MovieItem 对象。
这时实际上我们就已经完成了 Item 的提取。再运行一下 CrawlSpider:
scrapy crawl movie
可以看到一部部电影信息就被提取出来了,运行结果类似如下:
图略
但现在这种实现方式并不能实现可配置化,下面我们尝试将这个方法改写为 ItemLoaders 来实现。通过 add_xpath,add_css,add_value 等方式实现配置化提取。我们可以改写 parse_detail,如下所示:
def parse_detail(self, response):
loader = MovieItemLoader(item=MovieItem(), response=response)
loader.add_css('name', '.item h2::text')
loader.add_css('categories', '.categories button span::text')
loader.add_css('cover', '.cover::attr(src)')
loader.add_css('published_at', '.info span::text', re='(\d{4}-\d{2}-\d{2})\s?上映')
loader.add_xpath('score', '//p[contains(@class, "score")]/text()')
loader.add_xpath('drama', '//div[contains(@class, "drama")]/p/text()')
yield loader.load_item()
这单我们定义了一个 ItemLoader 的子类,名为 MovieItemLoader,其实现如下所示:
from scrapy.loader import ItemLoader
from itemloaders.processors import TakeFirst, Identity, Compose
class MovieItemLoader(ItemLoader):
default_output_processor = TakeFirst()
categories_out = Identity()
score_out = Compose(TakeFirst(), str.strip)
drama_out = Compose(TakeFirst(), str.strip)
这里我们定义了 4 个字段,说明如下:
-
default_output_processor:上文中,由于大多数字段需要利用 extract_first 方法来获得第一个提取结果,而在 parse_detail 方法中我们并没有指定抽取第一个结果,所以最终的结果仍然是一个列表形式。那 extract_first 方法对应的逻辑我们需要放到哪里实现呢?答案是需要 MovieItemLoader 来实现。这里我们定义了一个 default_output_processor,意思是通用的输出处理器,这里指定为了 TakeFirst。这样默认情况下,每个字段的第一个提取结果就会作为该字段的最终结果,相当于默认情况下每个字段提取完毕之后都调用了 extract_first 方 法比如 name 字段,原本捕取结果为 ['少年派的奇幻漂流-Life of Pi'],经过 TakeFirst 处理后,结果就是少年派的奇幻漂流-Life of Pi。
-
categories_out:原本的提取结果是一个列表,而我们希望最终获取的也是列表,所以需要保持原来的结果不变,而刚才我们已经定义了 default_output_processor 来提取第一个结果作为字段内容,这里我们需要将其覆盖,定义 categories_out 字段,覆盖默认的 default_output_processor,这里定义为 Identity,保持原结果不变。
-
score_out:使用默认的 TakeFirst 提取之后,结果前后包含一些空格信息,我们需要进一步将其去除,所以这里使用了 Compose,参数依次传人了 TakeFirst 和 str.strip:这样就能取出第一个结果并去除前后的空格了。
-
drama_out:和 score_out 也是一样的逻辑。
好,这时候我们重新运行一下 CrawlSpider,结果和刚才是完全一样的。
至此、我们已经实现了爬虫的半规则化。
配置抽取
为什么现在只做到了半规则化?一方面,我们在代码层面上使用了 Rule 将爬取逻辑进行了规则化,但这样可扩展性和维护性依然没有那么强。如果我们需要扩展其他站点,仍然需要创建一个新的 CrawlSpider,定义这个站点的 Rule,单独实现 parse_detail 方法。还有很多代码是重复的,如 CrawlSpider 的变量,方法名几乎都是一样的。那么我们可不可以把多个类似的几个爬虫的代码共用,把完全不相同的地方抽离出来,做成可配置文件呢?
当然可以。那我们可以抽离出哪些部分?所有的变量都可以抽取,如 name,allowed_domains、start_urls,rules 等。这些变量在 CrawlSpider 初始化的时候赋值即可。我们就可以新建一个通用的 Spider 来实现这个功能,命令如下所示:
scrapy genspider -t crawl universal universal
这个全新的 Spider 名为 universal。接下来,我们将刚才所写的 Spider 内的属性抽离出来配置成一个 JSON,命名为 movie.json,放到 configs 文件夹内,和 spiders 文件夹并列,JSON 文件内容如下所示:
省略
这里我们将一些配置进行了抽离,第一个字段 spider 即 Spider 的名称,在这里是 universal。然后定义了一些描述字段,比如 type、home 等说明爬取目标站点的类别、首页等。
然后就是一些重要配置了,比如可以使用 settings 定义 CrawlSpider 的 custom_settings 属性,使用 start_urls 定义初始爬取链接,使用 allowed_domains 定义允许爬取的域名,这些信息都会被读取然后初始化为 CrawlSpider 的属性。
另外我们还将 rules 进行了抽离,配置为了 JSON 形式,是列表类型,每个成员都代表一个 Rule 的配置。进一步地,每个 Rule 的配置又单独分离了 link_extractor 并配置上对应的属性,比如 restrict_css 代表LinkExtractor 的 restrict_css 属性。我们会使用 rules 字段的信息来初始化 CrawiSpider 的 rules 属性。
这样我们将基本的配置抽取出来。如果要启动爬虫,只需要从该配置文件中读取,然后动态加载到 Spider 中即可。所以我们需要定义一个读取该 JSON 文件的方法,新建一个 utils.py 文件,和 items.py 文件并列,内容如下所示:
from os.path import realpath, dirname, join
import json
def get_config(name):
path = join(dirname(realpath(__file__)), 'configs', f'{name}.json')
with open(path, 'r', encoding='utf-8') as f:
return json.loads(f.read())
定义了 get_config 方法之后,我们只需要向其传入 JSON 配置文件的名称即可获取此JSON 配置信息。随后我们定义入口文件 run.py,把它放在项目根目录下,命名为 run.py,它的作用是启动 Spider,代码如下所示:
from scrapy.utils.project import get_project_settings
from scrapyuniversaldemo.utils import get_config
from scrapy.crawler import CrawlerProcess
import argparse
parser = argparse.ArgumentParser(description='Universal Spider')
parser.add_argument('name', help='name of spider to run')
args = parser.parse_args()
name = args.name
def run():
config = get_config(name)
spider = config.get('spider', 'universal')
project_settings = get_project_settings()
settings = dict(project_settings.copy())
settings.update(config.get('settings'))
process = CrawlerProcess(settings)
process.crawl(spider, **{'name': name})
process.start()
if __name__ == '__main__':
run()
这里我们使用了 argparse 要求运行时指定 name 参数,即对应的 JSON 配置文件的名称。我们首先利用 get_config 方法传入该名称,读取刚才定义的配置文件。获取爬取使用的 Spider 的名称以及配置文件中的 settings 配置,然后将获取到的 settings 配置和项目全局的 settings 配置做了合并。
随后我们新建了一个 CrawlerProcess,利用 CrawlerProcess 我们可以通过代码更加灵活地自定义需要运行的 Spider 和启动配置,更加详细的用法可以参考官方文档: https://docs.scrapy.org/en/latest/topics/practices.html 。
在 universal.py 中,我们新建一个 __init__
方法,进行初始化配置,实现如下所示:
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..utils import get_config
class UniversalSpider(CrawlSpider):
name = 'universal'
def __init__(self, name, *args, **kwargs):
config = get_config(name)
self.config = config
self.start_urls = config.get('start_urls')
self.allowed_domains = config.get('allowed_domains')
rules = []
for rule_kwargs in config.get('rules'):
link_extractor = LinkExtractor(**rule_kwargs.get('link_extractor'))
rule_kwargs['link_extractor'] = link_extractor
rule = Rule(**rule_kwargs)
rules.append(rule)
self.rules = rules
super(UniversalSpider, self).__init__(*args, **kwargs)
在 __init__
方法中,我们接收了 name 参数,然后通过 get_config 方法读取了配置文件的内容。接着将 start_urls、allowed_domains,rules 进行了初始化。
其中 rules 的初始化过程相对复杂,这里首先遍历了 rules 配置,每个 rule 的配置赋值为 rule_kwargs 字典,然后读取了 rule_kwargs 的 link_extractor 属性,将其构造为 LinkExtractor 对象,接着将 link_extractor 属性赋值到 rule_kwargs 字典中,最后使用 rule_kwags 初始化一个 Rule 对象。多个 Rule 对象最终构造成一个列表赋值给 CrawlSpider,这样就完成了 rules 的初始化。
现在我们已经实现了 Spider 基础属性的可配置化。剩下的解析部分同样需要实现可配置化,原来的解析方法如下所示:
def parse_detail(self, response):
loader = MovieItemLoader(item=MovieItem(), response=response)
loader.add_css('name', '.item h2::text')
loader.add_css('categories', '.categories button span::text')
loader.add_css('cover', '.cover::attr(src)')
loader.add_css('published_at', '.info span::text', re='(\d{4}-\d{2}-\d{2})\s?上映')
loader.add_xpath('score', '//p[contains(@class, "score")]/text()')
loader.add_xpath('drama', '//div[contains(@class, "drama")]/p/text()')
yield loader.load_item()
我们需要将这些配置也抽离出来。这里的变量主要有 ItemLoader 类的选用,Item 类的选用,ItemLoader 方法参数的定义。我们将可变参数进行抽离,在 JSON 文件中添加 item 的配置,参考如下:
省略
注意,item 的配置和 rules 是并列的。在 item 中,我们定义了 class 和 loader 属性,它们分别代表 Item 和 ItemLoader 所使用的类。定义了 attrs 属性来定义每个字段的提取规则,例如,title 定义的每一项都包含一个 method 属性,它代表使用的提取方法,如 xpath 代表调用 Item Loader 的 add_xpath 方法。arg 即参数,它是 add_xpath 方法的第二个参数,代表的是 XPath 表达式。另外针对正则提取,这里还可以定义一个 re 参数来传递提取时所使用的正则表达式。
我们还要将这些配置动态加载到 parse_detail 方法里,实现 parse_detail 方法如下:
def parse_detail(self, response):
item = self.config.get('item')
if item:
cls = getattr(items, item.get('class'))()
loader = getattr(loaders, item.get('loader'))(cls, response=response)
for key, value in item.get('attrs').items():
for extractor in value:
if extractor.get('method') == 'xpath':
loader.add_xpath(key, extractor.get('arg'), **{'re': extractor.get('re')})
if extractor.get('method') == 'css':
loader.add_css(key, extractor.get('arg'), **{'re': extractor.get('re')})
if extractor.get('method') == 'value':
loader.add_value(key, extractor.get('args'), **{'re': extractor.get('re')})
yield loader.load_item()
这里首先获取 Item 的配置信息,然后获取 class 的配置,将 Item 进行初始化接着利用 Item 再初始化 ItemLoader,赋值为 loader 对象。
接下来我们遍历 Item 的 attrs 代表的各个属性依次进行提取。首先我们需要判断 method 字段,调用对应的处理方法进行处理。如 method 为 css,就调用 ItemLoader 的 add_css 方法进行提取。所有配置动态加载完毕之后,调用 load_item 方法将 Item 提取出来。
至此,Spider 的设置、起始链接,属性、提取方法全部实现了可配置化。
这时候我们就可以使用配置文件来启动 CrawlSpider 了,运行命令如下:
python3 run.py movie
运行结果如下:
省略
可以看到爬取结果和之前也是完全相同的,抽离规则成功!
综上所述,整个项自的规则化包括如下内容。
-
spider:指定所使用的 Spider 的名称。
-
settings:可以专门为 Spider 定制配置信息,会覆盖项目级别的配置
-
start_urls:指定爬虫爬取的起始链接。
-
allowed_domains:允许爬取的站点。
-
rules:站点的爬取规则
-
item:数据的提取规则。
到现在,就可以灵活地对爬取逻辑进行控制了。