scrapy爬虫框架(六)Item Pipeline的使用
Item Pipeline即项目管道,它的调用发生在Spider产生Item之后。当Spider解析完Response,Item就会被Engine传递到Item Pipeline,被定义的Item Pipeline组件会顺次被调用,完成一连串的处理过程,比如数据清洗、存储等。
Item Pipeline的主要功能如下:
- 清洗HTML数据。
- 验证爬取数据,检查爬取字段。
- 查重并丢弃重复内容。
- 将爬取结果存储到数据库中。
一、核心方法。
我们可以自定义Item Pipeline,只需要实现指定的方法就好,其中必须实现的一个方法是:
- process_item(item, spider)
另外还有几个比较使用的方法,它们分别是:
- open_spider(spider)
- close_spider(spider)
- from_crawler(cls, crawler)
1、process_item(item, spider)
process_item是必须实现的方法,被定义的Item Pipeline会默认调用这个方法对Item进行处理,比如进行数据处理或者将数据写入数据库等操作。
(1)参数
process_item方法的参数有两个。
- item:Item对象,即被处理的Item。
- spider:Spider对象,即生成该Item的Spider。
(2)返回值
process_item方法必须返回Item类型的值或者抛出一个DropItem异常。该方法的返回类型如下:
- Item:如果返回的是Item对象,那么此Item会接着被低优先级的Item Pipeline的process_item方法处理,直到所有的方法被调用完毕。
- DropItem异常:如果抛出DropItem异常,那么此Item就会被丢弃,不再进行处理。
2、open_spider(self, spider)
open_spider方法是在Spider开启的时候被自动调用的,在这里,我们可以做一些收尾工作,如关闭数据库连接等。其中参数spider就是被开启的Spider对象。
3、close_spider(spider)
close_spider方法是在Spider关闭的时候自动调用,在这里,我们可以做一些收尾工作,如关闭数据库连接等,其中参数spider就是被关闭的Spider对象。
4、from_crawler(cls, crawler)
from_crawler方法是一个类方法,用@classmethod标识,它接受一个参数crawler。通过crawler对象,我们可以拿到Scrapy的所有核心组件,如全局配置的每个信息。然后可以在这个方法里面创建一个Pipeline实例。参数cls就是Class,最后返回一个Class实例。
二、实例演示
本节我们的目标网站是:https://ssr1.scrape.center/,我们需要把每部电影的名称、类别、评分、简介、导演、演员的信息以及相关图片爬取下来,同时把每部电影的导演、演员的相关图片保存成一个文件夹,并将每部电影的完整数据保存到Mysql和images文件夹内。
在这里我们使用scrapy来实现这个电影数据爬虫,了解Item Pipeline的用法。
1、创建项目
我们首先创建一个项目,取名为testItemPipeline,命令如下:
- scrapy startproject testItemPipeline
进入该项目,在项目中创建一个spider,名叫movie_spider。
- scrapy genspider movie_spider https://ssr1.scrape.center/
这样我们就成功创建了一个Spider,,接下来我们来实现列表页的抓取。本站点一共有10页数据,所以我们可以新建10个初始请求。实现start_requests方法的代码如下:
在这里,修改了start_urls,使其形成单个初始url,随后,通过start_requests函数构造了十个初始url,随后返回了构造后的十个url,并通过Request的回调方法修改为了parse_index,最后我们暂时在parse_index方法里面打印输出了response对象。运行spider后结果如下:
可以看到对应的列表页的数据就被爬取下来了,Response的状态码为200。
接着我们可以在parse_index方法里对response的内容进行解析,提取每部电影的详情页连接,在这里我们通过xpath进行数据的提取。提取链接之后生成详情页的Request,可以把parse_index方法改写如下:
这里,我们首先筛选了每部电影所对应的节点,然后遍历这些节点,提取其中的href来提取详情页链接,接着通过response的urljoin方法拼接成完整的详情页URL,最后构造新的详情页Request,回调方法设置为parse_detail,同时在parse_detail方法里面打印输出response,重新运行,我们可以看到详情页的内容就被爬取下来了,类似下面的输出:
其实现在parse_detail里面的response就是详情页的内容了,我们可以进一步对详情页的内容进行解析,提取了每部电影的名称、类别、评分、简介、导演、演员等信息:
首先让我们先修改一下items.py的内容,修改的Item类叫作TestitempipelineItem。
新建文件时就是以下样式:
我们只需要在里面添加几个需要爬取的字段就可以了。
这里我们定义了几个字段name、categories、score、drama、directors、actors分别代表电影名称、类别、评分、简介、导演、演员。接下来我们就可以提取详情页了,修改parse_detail方法如下:
def parse_detail(self, response):
item = TestitempipelineItem()
item["name"] = response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/a/h2/text()').extract_first()
item["categories"] = ','.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/div[@class="categories"]/button/span/text()').extract())
item["score"] = ''.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="el-col el-col-24 el-col-xs-8 el-col-sm-4"]/p/text()').extract_first()).replace("\n","").replace(" ","")
item["drama"] = ''.join(response.xpath('//div[@class="el-card__body"]/div[@class="item el-row"]/div[@class="p-h el-col el-col-24 el-col-xs-16 el-col-sm-12"]/div[@class="drama"]/p/text()').extract_first()).replace("\n","")
item["directors"] = ''.join(response.xpath('//div[@class="directors el-row"]/div[@class="director el-col el-col-4"]/div[@class="el-card is-hover-shadow"]/div[@class="el-card__body"]/p/text()').extract())
item["actors"] = []
ss = response.xpath('//div[@class="actors el-row"]//div[@class="actor el-col el-col-4"]')
for data in ss:
actors_image = ''.join(data.xpath('./div/div/img/@src').extract())
actors_name = ''.join(data.xpath('./div/div/p/text()').extract())
item["actors"].append({
"name":actors_name,
"actors_image":actors_image
})
yield item
在这里我们首先创建了一个TestitempipelineItem对象,赋值为item,然后我们使用xpath方法提取了name、categories、score、drama、directors、actors六个字段,最后组合成了一个列表赋值给actors,其中的actors信息包括image和name两个信息,以字典的形式添加进actors里面。运行以下,结果如下:
可以看到我们这里已经成功提取了各个字段然后生成了MovieItem对象了。那么下一步就是我们的重点了,我们需要将爬取到的内容存储到MongoDB和本地文件夹中。
如果不知道MongoDB的环境怎么配置可以参考这篇博客:MongoDB - 乐之之 - 博客园 (cnblogs.com)
2、MongoDB Pipeline
那么我们需要实现一个MongoDBPipeline,将信息保存到MongoDB,在pipelines.py里添加如下类的实现:
这里我们首先利用from_crawler获取了全局配置MONGODB_CONNECTION_STRING、MONGODB_DATABASE和MONGODB_COLLECTION,即MongoDB连接字符串、数据库名称、集合名词,然后将三者赋值为类属性。
接着我们实现了open_spider方法,该方法就是利用from_crawler赋值的connection_string创建一个MongoDB连接对象,然后声明数据库操作对象,close_spider则是在Spider运行结束时关闭MongoDB连接。
接着最重要的就是process_item方法了,这个方法接收的参数item就是从Spider生成的Item对象,该方法需要将此Item存储到MongoDB中,这里我们使用update_one方法实现了存在即更新。不存在则插入的功能。
接下来我们需要在settngs.py里面添加MONGODB_CONNECTION_STRING、MONGODB_DATABASE和MONGODB_COLLECTION这三个变量,相关代码如下:
MONGODB_CONNECTION_STRING = os.getenv('MONGODB_CONNECTION_STRING')
# 数据库
MONGODB_DATABASE = 'spider'
# 表名
MONGODB_COLLECTION = 'movies'
这里可以将MONGODB_CONNECTION_STRING设置为从环境变量中读取,而不用将明文、密码等信息写到代码里。
如果是本地无密码的MongoDB,直接写为如下内容即可:
MONGODB_CONNECTION_STRING = 'mongodb://localhost:27017'
如下所示:
这样,一个保存到MongoDB的Pipeline就创建好了,利用process_item方法我们即可完成数据插入到MongoDB的操作,最后会返回Item对象。
3、image Pipeline
Scrapy 提供了专门处理下载的 Pipeline,包括文件下载和图片下载。下载文件和图片的原理与抓取页面的原理一样,因此下载过程支持异步和多线程,十分高效。下面我们来看看具体的实现过程。
官方文档地址为:https://doc.scrapy.org/en/latest/topics/media-pipeline.html。
首先定义存储文件的路径,需要定义一个IMAGES_STORE 变量,在settingspy 中添加如下代码:
IMAGES_STORE='./images'
在这里我们将路径定义为当前路径下的images子文件夹,即下载的图片都会保存到本项目的images文件夹中。
内置的 ImagesPipeline 会默认读取Item 的 image_urls 字段,并认为它是列表形式,接着遍历该字段后取出每个URL进行图片下载。
但是现在生成的Item的图片链接字段并不是 image_urls 字段表示的,我们是想下载directors和actors的每张图片。所以为了实现下载,我们需要重新定义下载的部分逻辑,即自定义ImagePipeline继承内置的ImagesPipeline,重写几个方法。
我们定义的ImagePipeline代码如下:
class ImagePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
for director in item['directors']:
director_name= director['name']
director_image =director['image']
yield Request(director_image, meta={
'name': director_name,
'type':'director',
'movie':item['name']
})
for actor in item['actors']:
actor_name = actor['name']
actor_image = actor['image']
yield Request(actor_image,meta= {
'name': actor_name,
'type': 'actor',
'movie': item['name']
})
def file_path(self, request, response=None, info=None):
movie = request.meta['movie']
print(f"movie:<{movie}>")
type = request.meta['type']
print(f"type:<{type}>")
name = request.meta['name']
print(f"name:<{name}>")
file_name = f'{movie}/{type}/{name}.jpg'.replace(" ","")
print(f"file_name:<{file_name}>")
return file_name
def item_completed(self, results,item, info):
print("item:",f'<{item}>')
image_paths=[x['path'] for ok,x in results if ok]
if not image_paths:
raise DropItem('Image Downloaded Failed')
return item
在这里我们实现了 ImagePipeline,继承Scrapy内置的ImagesPipeline,重写下面几个方法。
- get _media requests:第一个参数item是爬取生成的Item 对象、我们要下载的图片链接保存在Item的directors和actors每个元素的image字段中。所以我们将URL逐个取出,然后构造 Request 发起下载请求。同时我们指定了 meta信息,方便构造图片的存储路径,以便在下载完成时使用。
- file_path:第一个参数request就是当前下载对应的Request对象。这个方法用来返回保存的文件名,在这里我们获取了刚才生成的 Request 的 meta 信息,包括 movie (电影名称)、type(电影类型)和name(导演或演员姓名),最终三者拼合为 file name作为最终的图片路径。
- item_completed:单个Item完成下载时的处理方法。因为并不是每张图片都会下载成功,所以我们需要分析下载结果并剔除下载失败的图片。如果某张图片下载失败,那么我们就不需将此Item保存到数据库。item_ompleted方法的第一个参数results就是该Item对应的下载结果它是一个列表,列表的每个元素是一个元组,其中包含了下载成功或失败的信息。这里我们遍历下载结果,找出所有成功的下载列表。如果列表为空,那么该 Item 对应的图片下载失败,随即抛出DropItem异常,忽略该Item;否则返回该Item,说明此Item有效。
现在为止,2个Item Pipeline 的定义就完成了。最后只需要启用就可以了,修改settings.py,设置ITEMPIPELINES的代码如下所示:
这里要注意调用的顺序。我们需要优先调用ImagePipeline对Item做下载后的筛选,下载失败的Item就直接忽略,它们不会保存到 MongoDB和MySQL里。随后再调用其他两个存储的 Pipeline,这样就能确保存人数据库的图片都是下载成功的。
接下来运行程序,命令如下:
- scrapy crawl movie_spider
爬虫一边爬取一遍爬取,速度非常快。
接下来,我们来查看一下本地images文件夹,发现图片都已经下载成功,如下图所示:
可以看到图片已经分路径存储了,一部电影一个文件夹,演员和导演分二级文件夹,图片名直接以演员和导演名命名。
然后我们再来查看一下MongoDB,下载成功的图片信息同样已经成功保存,如下图所示
这样我们就成功的实现图片的下载并把图片的信息存入数据库里了。
三、常用的Item Pipeline保存方式
1、MySQL保存方式
- 省事简便,较为常用,细节性的处理有所不足
- 只用到了process_item一种方法。
import pymysql
class MysqlPipeline:
def __init__(self):
self.coon = pymysql.Connect(
host="127.0.0.1",
port=3306,
user="root",
passwd="******",
db="demo"
)
self.cursor = self.coon.cursor()
def process_item(self,item,spider):
print(item)
try:
sql = "insert into caipu_spider(title,major,score,intro) VALUES (%s,%s,%s,%s)"
params = [(item["title"],item["major"],item["score"],item["intro"])]
self.cursor.executemany(sql,params)
self.coon.commit()
except Exception:
# 回滚
self.coon.rollback()
finally:
return item
2、MongoDB保存方式
import pymongo
class MongoPipeline:
def __init__(self):
self.client = pymongo.MongoClient(host="localhost",port=27017)
self.db = self.client["douguo"]
def process_item(self,item,spider):
print(item)
self.db.douguo_data.insert_one(dict(item))
3、图片常用保存方式
- 先在setting.py文件中加入IMAGES_STORE = './images'
import scrapy
from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
class DuitangPipeline:
def process_item(self, item, spider):
return item
class ImagePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
yield scrapy.Request(item["href"])
def file_path(self, request, response=None, info=None, *, item=None):
img_name=request.url.split("/")[-1]
return img_name
def item_completed(self, results, item, info):
print(item)
return item