搭建一个基础爬虫
1.新建项目:
scrapy startproject scrapyuniversaldemo
2.查看可用模版并指定crawl模版创建爬虫
scrapy genspider -l
# 查看模版非必要
scrapy genspider -t crawl movie ssr1.scrape.center
3.在爬虫的rules中使用Rule来定义index页中的爬取逻辑和解析逻辑
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import MovieItem
class MoiveSpider(CrawlSpider):
name = 'movie'
allowed_domains = ['ssr1.scrape.center']
start_urls = ['http://ssr1.scrape.center/']
rules = (
Rule(LinkExtractor(restrict_css='.item .name'), callback='parse_detail', follow=True),
Rule(LinkExtractor(restrict_css='.next'), follow=True)
)
这里用到了Rule的三个参数:link_extractor、callback、follow
- link_extractor:一个LinkExtractor对象,指向需要提取的链接,提取出的链接会自动生成Request;
- callback:指向负责处理response的方法,该方法会返回包含Item或Request对象的列表(不要使用parse方法);
- follow:?一个布尔值,指定从response提取的链接是否需要跟进爬取(即进一步生成Request),如果不跟进,一般可以定义回调方法解析内容,生成Item。
在LinkExtractor中用到了一个参数:restrict_css
- restrict_css:表示从当前页面中与CSS选择器匹配的区域提取链接
- restrict_xpath:表示从当前页面中与XPath匹配的区域提取链接
- tags:指定从某个节点中提取链接,默认是('a','area')
- attrs:指定从节点的某个属性中提取链接,默认是('href',),搭配tags使用
- unique:是否需要对提取到的链接进行去重,默认是True
- strip:是否需要对提取到的结果进行去掉首尾空格的处理,默认是True
- allow:一个正则表达式(或列表),规定提取链接的白名单
- deny:一个正则表达式(或列表),规定提取链接的黑名单
- allow_domains:定义域名白名单
- deny_domains:定义域名黑名单
- deny_extensions:定义链接后缀黑名单(默认值包括7z、7zip、apk、dmg、ico、iso、tar等)
4.在爬虫中创建parse_detail方法,定义detail页中的爬取逻辑
-省略-
class MoiveSpider(CrawlSpider):
-省略-
def parse_detail(self, response):
item = MovieItem()
item['name'] = response.css('.item h2::text').get()
item['categories'] = response.css('.categories .category span::text').getall()
item['cover'] = response.css('.cover::attr(src)').get()
item['published_at'] = response.css('.info span::text').re_first('(\d{4}-\d{2}-\d{2})\s?上映')
item['score'] = response.css('.score::text').get().strip()
item['drama'] = response.css('.drama p::text').get().strip()
yield item
5.在items中定义MovieItem并指定好需要的字段
import scrapy
class MovieItem(scrapy.Item):
name = scrapy.Field()
cover = scrapy.Field()
categories = scrapy.Field()
published_at = scrapy.Field()
drama = scrapy.Field()
score = scrapy.Field()
至此,可以实现到以下效果:?
改进成半规则化爬虫
但现在这种实现方式并不能实现可配置化,需要进行修改:
6.将parse_detail方法改写为Item?Loaders来实现
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import MovieItem
from ..loaders import MovieItemLoader
class MoiveSpider(CrawlSpider):
name = 'movie'
allowed_domains = ['ssr1.scrape.center']
start_urls = ['http://ssr1.scrape.center/']
rules = (
Rule(LinkExtractor(restrict_css='.item .name'), callback='parse_detail', follow=True),
Rule(LinkExtractor(restrict_css='.next'), follow=True)
)
def parse_detail(self, response):
loader = MovieItemLoader(item=MovieItem(), response=response)
# 声明一个MovieItem,用该Item和Response对象实例化MovieItemLoader
loader.add_css('name', '.item h2::text')
# 调用add_css方法将数据提取出来,分配给name属性
loader.add_css('categories', '.categories .category 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_css('score', '.score::text')
loader.add_css('drama', '.drama p::text')
yield loader.load_item()
# 调用load_item方法实现对Item的解析
这里用到了Item?Loader的两个参数:item和response
- item:Item对象,可以调用add_xpath、add_css、add_value等方法来填充Item对象
- response:Response对象,用于使用构造选择器的Response
- selector:Selector对象,用来提取填充数据的选择器
7.创建loaders.py(和items.py同级),?在内定义ItemLoader的子类?
- 默认的ItemLoader在这里不够好使,通过继承、创建一个它的子类,来加上项目需要的功能;
- Item?Loader的每个字段中都包含了一个Input?Processor和一个Out?Processor;
- Input?Processor(输入处理器):收到数据时立刻提取数据,结果收集起来并保存在ItemLoader内,不会分配给Item;
- Output?Processor(输出处理器):收集到所有数据后,load_item方法会被调用来填充再生成Item对象——在此之前,会先调用Output?Processor对数据进行处理。
from scrapy.loader import ItemLoader
from itemloaders.processors import TakeFirst, Identity, Compose
class MovieItemLoader(ItemLoader):
default_output_processor = TakeFirst()
# 设置默认的输出处理器
# 代替get()操作,以每个字段的第一个提取结果作为最终结果
categories_out = Identity()
# 保持原来的结果不变(列表),覆盖前面的默认值
score_out = Compose(TakeFirst(), str.strip)
drama_out = Compose(TakeFirst(), str.strip)
# 取出第一个结果并作去除前后空格处理,覆盖前面的默认值
这里用到了两种Scrapy提供的Processor:TakeFirst、Identity
- TakeFirst:返回列表的第一个非空值,类似get()的功能
- Identity:不进行任何处理,直接返回原来的数据
- Join:把列表拼接成字符串(字符串默认用空格分隔)
- Compose:可以传入多个函数对一个输入值进行处理
- MapCompose:可以传入多个函数对一个列表输入值进行处理
- SelectJmes:可以查询JSON(传入Key,返回查询所得的Value;需要先安装jmespath库)
?
输出结果和前面的一致。至此,实现了爬虫的半规则化。
改进成完全规则化爬虫
8.创建一个通用的Spider(universal.py)
scrapy genspider -t crawl universal universal
9.创建configs目录(和spiders目录同级),在configs目录下创建movie.json,将Spider内的属性抽离并配置到这个JSON文件内
{
"spider": "universal",
"type": "电影",
"home": "https://ssr1.scrape.center/",
"settings": {
"USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36 Edg/98.0.1108.43"
},
"start_urls": [
"https://ssr1.scrape.center/"
],
"allowed_domains": [
"ssr1.scrape.center"
],
"rules": [
{
"link_extractor": {
"restrict_css": ".item .name"
},
"follow": true,
"callback": "parse_detail"
},
{
"link_extractor": {
"restrict_css": ".next"
},
"follow": true
}
]
}
10.创建utils.py(和items.py同级),用于读取前面定义的JSON,然后动态加载到Spider中
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())
11.在项目根目录创建入口run.py(和scrapy.cfg同级)
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
# 使用argparse要求运行时指定name参数(即对应的JSON配置文件的名称)
def run():
config = get_config(name)
# 利用config传入JSON配置文件
spider = config.get('spider', 'universal')
# 获取爬取使用的Spider名称
project_settings = get_project_settings()
settings = dict(project_settings.copy())
# 获取配置文件中的settings配置
settings.update(config.get('settings'))
# 将获取到的settings配置和项目全局的settings配置进行合并
process = CrawlerProcess(settings)
# 新建一个CrawlerProcess,通过代码更加灵活自定义需要的Spider和启动配置
process.crawl(spider, **{'name': name})
process.start()
if __name__ == '__main__':
run()
12.在universal.py中新建__init__方法,进行初始化配置
import scrapy
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)
# 接收name参数并通过get_config方法读取配置文件的内容
self.config = config
self.start_urls = config.get('start_urls')
self.allowed_domains = config.get('allowed_domains')
rules = []
# 分别将start_urls、allowed_domains、rules进行初始化
for rule_kwargs in config.get('rules'):
# 遍历rules配置
link_extractor = LinkExtractor(**rule_kwargs.get('link_extractor'))
# 每个rule的配置赋值为rule_kwargs字典,
# 然后读取rule_kwargs的link_extractor属性,将其构造为LinkExtractor对象
rule_kwargs['link_extractor'] = link_extractor
# 将link_extractor属性赋值到rule_kwargs字典中
rule = Rule(**rule_kwargs)
# 使用rule_kwargs初始化一个Rule对象
rules.append(rule)
# 将多个Rule对象构造成一个rules列表
self.rules = rules
super(UniversalSpider, self).__init__(*args, **kwargs)
# 将rules列表赋值给CrawlSpider
13.将原Spider文件中的解析部分抽离并配置到JSON文件中
{
"spider": "universal",
"type": "电影",
"home": "https://ssr1.scrape.center/",
"settings": {
"USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36 Edg/98.0.1108.43"
},
"start_urls": [
"https://ssr1.scrape.center/"
],
"allowed_domains": [
"ssr1.scrape.center"
],
"rules": [
{
"link_extractor": {
"restrict_css": ".item .name"
},
"follow": true,
"callback": "parse_detail"
},
{
"link_extractor": {
"restrict_css": ".next"
},
"follow": true
}
],
"item": {
//item和rules同级并列
"class": "MovieItem",
"loader": "MovieItemLoader",
//分别代表Item和ItemLoader的所使用的类
"attrs": {
//定义attrs属性来定义每个字段的提取规则
"name": [
{
"method": "css",
"arg": ".item h2::text"
}
],
"categories": [
{
"method": "css",
"args": ".categories button span::text"
}
],
"cover": [
{
"method": "css",
"arg": ".cover::attr(src)"
}
],
"published_at": [
{
"method": "css",
"arg": ".info span::text",
"re": "(\\d{4}-\\d{2}-\\d{2})\\s?上映"
}
],
"score": [
{
"method": "css",
"arg": ".score::text"
}
],
"drama": [
{
"method": "css",
"arg": ".drama p::text"
}
]
}
}
}
14.将以上配置动态加载到parse_detail方法里
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..utils import get_config
from .. import items
from .. import loaders
class UniversalSpider(CrawlSpider):
name = 'universal'
def __init__(self, name, *args, **kwargs):
-省略-
def parse_detail(self, response):
item = self.config.get('item')
# 通过utils(get_config)获取JSON文件中的item配置信息
if item:
cls = getattr(items, item.get('class'))()
# 获取class的配置(代表Item使用的类),将Item进行初始化
loader = getattr(loaders, item.get('loader'))(cls, response=response)
# 获取loader的配置(代表ItemLoader使用的类),将ItemLoader进行初始化
for key, value in item.get('attrs').items():
# 遍历Item的attrs代表的各个属性
for extractor in value:
if extractor.get('method') == 'xpath':
# 判断method字段,调用对应的处理方法进行处理
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('arg'), **{'re': extractor.get('re')})
yield loader.load_item()
# 所有配置动态加载完毕之后,调用load_item方法将Item提取出来
和前面的普通爬虫相比,主要增加了这几个部分:movie.json,universal.py,loaders.py,utils.py,run.py
movie.json:其包含的内容其实都是从原来的movie.py迁移过来的
universal.py:是一只通用爬虫
utils.py:用来帮助爬虫去读取JSON文件的
loaders.py:相当于爬虫和Item之间的“中间件”,对数据作进一步处理(因为在JSON里面,只定义了数据怎么爬取;而在爬虫中,为了保持通用性,不能把处理方法写死,所以要靠loaders来打补丁——我是这么理解的)
run.py:项目启动器,用来匹配爬虫和JSON配置文件的。
<完>
|