Scrapy框架(下载项目图片以及实现爬虫数据持久化保存)scr
安装 Scrapy 框架
pip3 install Scrapy
Scrapy架构图(绿线是数据流向):
scrapy架构.png-
Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。
-
Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
-
Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
-
Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
-
Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.
-
Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。
-
Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
新建项目(scrapy startproject)
在开始爬取之前,必须创建一个新的Scrapy项目。进入自定义的项目目录中,运行下列命令:
scrapy startproject myspider
新建爬虫文件
** scrapy genspider jobbole jobbole.com**
关于yeild函数
参考资料说明:https://blog.csdn.net/u013205877/article/details/70332612 https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/
简单地讲,yield 的作用就是把一个函数变成一个 generator(生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,带有yeild的函数遇到yeild的时候就返回一个迭代值,下次迭代时, 代码从 yield 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行, 直到再次遇到 yield。
通俗的讲就是:在一个函数中,程序执行到yield语句的时候,程序暂停,返回yield后面表达式的值,在下一次调用的时候,从yield语句暂停的地方继续执行,如此循环,直到函数执行完。
settings.py文件设置参考
- 爬虫的文件路径
SPIDER_MODULES = ['ziruproject.spiders']
NEWSPIDER_MODULE = 'ziruproject.spiders' - 用户代理,一般设置这个参数用来伪装浏览器请求
USER_AGENT = '' - 是否遵守ROBOT协议,为False时,表示不遵守, 为True时表示遵守(默认为True)
ROBOTSTXT_OBEY = True
Scrapy downloader(下载器) 处理的最大的并发请求数量。 默认: 16
CONCURRENT_REQUESTS - 下载延迟的秒数,用来限制访问的频率
默认为:0
DOWNLOAD_DELAY
scrapy案例
以'http://chinaz.com/'为例
(下载项目图片以及实现爬虫数据持久化保存)
chinaz.py
# -*- coding: utf-8 -*-
import scrapy
from chinaz.items import ChinazprojectItem, ChinazprojectWebInfoItem
class ChinazSpider(scrapy.Spider):
#爬虫名称
name = 'china'
#设置允许爬取的域
allowed_domains = ['chinaz.com']
#设置起始urls
start_urls = ['http://top.chinaz.com/hangyemap.html']
# 可以根据不同的爬虫文件做自定义的参数设置,会覆盖settings.py中的相关设置
custom_settings = {
'USER_AGENT' : 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
}
def parse(self, response):
"""
在parse回调方法中
step1:提取目标数据
step2:获取新的url
:param response: 请求的响应结果
:return:
"""
print(response.status)
# response.xpath():使用xpath语法,得到的是SelectorList对象
# response.css():使用css选择器,得到的是SelectorList对象
# extract(): 将selector 序列化为unicode字符串
# step1: 提取目标数据
# 获取分类列表
tags = response.xpath('//div[@class="Taright"]/a')
# tags = response.css('.Taright a')
for tag in tags:
#实例化一个item,用来存储数据
tag_item = ChinazprojectItem()
#获取网站分类的名称
# tagName = tag.xpath('./text()')[0].extract()
tagName = tag.xpath('./text()').extract_first('')
tag_item['tagName'] = tagName
# 使用css取值(文本)
# tagName = tag.css('::text').extract_first('')
#获取网站分了的首页url地址
# first_url = tag.xpath('./@href')[0].extract()
first_url = tag.xpath('./@href').extract_first('')
tag_item['firsturl'] = first_url
#css语法取值(属性)
# first_url = tag.css('::attr(href)').extract_first('')
# print(tag_item)
#将获取到的数据交给管道处理
yield tag_item
# http://top.chinaz.com/hangye/index_yule_yinyue.html
'''
url : 设置需要发起请求的url地址
callback=None, : 设置请求成功后的回调方法
method='GET', : 请求方式,默认为get请求
headers=None, : 设置请求头,字典类型
cookies=None, : 设置cookie信息,模拟登陆用户,字典类型
meta=None, : 传参,字典类型
encoding='utf-8', : 设置编码
dont_filter=False, : 是否要去重,默认False表示去重
errback=None, : 设置请求失败后的回调
'''
yield scrapy.Request(first_url,callback=self.parse_tags_page)
def parse_tags_page(self,response):
'''
解析分类分页的网站信息
:param response : 响应结果
:return:
'''
print('分页请求', response.status, response.url)
# 列表
webInfos = response.xpath('//ul[@class="listCentent"]/li')
for webinfo in webInfos:
web = ChinazprojectWebInfoItem()
# 封面图片
web['coverImage'] = webinfo.xpath('//div[@class="leftImg"]/a/img/@src').extract_first('')
# web['coverImages'] = ['']
# 标题
web['title'] = webinfo.xpath('//div[@class="CentTxt"]/h3/a/text()').extract_first('')
# 域名
web['domenis'] = webinfo.xpath('//div[@class="CentTxt"]/h3/span/text()').extract_first('')
# 周排名
web['weekRank'] = webinfo.xpath('//div[@class="CentTxt"]/div[@class="RtCPart clearfix"]/p[0]//a/text()').extract_first('')
# 反链数
web['ulink'] = webinfo.xpath('//div[@class="CentTxt"]/div[@class="RtCPart clearfix"]//p[3]//a/text()').extract_first('')
# 网站简介
web['info'] = webinfo.xpath('//div[@class="CentTxt"]/p/text()').extract_first('')
# 得分
web['score'] = webinfo.xpath('//div[@class="RtCRateCent"]/span/text()').extract_first('')
# 排名
web['rank'] = webinfo.xpath('//div[@class="RtCRateCent"]/strong/text()').extract_first('')
# print(web)
yield web
#发起其他页面请求
next_urls = response.xpath('//div[@class="ListPageErap"]/a/@href').extract()[1:]
for next_url in next_urls:
# 使用urljoin方法将不完整的url拼接完整
next_url = response.urljoin(next_url)
yield scrapy.Request(next_url,callback = self.parse_tags_page)
items.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
class ChinazprojectItem(scrapy.Item):
'''
存储网页分类信息
'''
# 分类名称
tagName = scrapy.Field()
# 分类首页url地址
firsturl = scrapy.Field()
def get_insert_spl_data(self, dataDict):
'''
step1 : 创建SQL语句
step2 : 返回要存储的数据
:param dataDict:
:return:
'''
# 往数据库写
sql = """
INSERT INTO tags(%s)
VALUES (%s)
""" % (
','.join(dataDict.keys()),
','.join(['%s'] * len(dataDict))
)
# 需要往数据库中存储的数据
data = list(dataDict.values())
return sql, data
class ChinazprojectWebInfoItem(scrapy.Item):
# 封面图片
coverImage = scrapy.Field()
# 标题
title = scrapy.Field()
# 域名
domenis = scrapy.Field()
# 周排名
weekRank = scrapy.Field()
# 反链接数
ulink = scrapy.Field()
# 网站简介
info = scrapy.Field()
# 得分
score = scrapy.Field()
# 排名
rank = scrapy.Field()
# 图片本地存储路径
locakImagePath = scrapy.Field()
pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
import mysql.connector as c
import pymongo
from scrapy.contrib.pipeline.images import ImagesPipeline
import scrapy
from chinaz.items import ChinazprojectWebInfoItem, ChinazprojectItem
from scrapy.utils.project import get_project_settings
import os
images_store = get_project_settings().get('IMAGES_STORE')
class ChinazProjectImagePipeline(ImagesPipeline):
# 实现2个方法
def get_media_requests(self, item, info):
'''
根据图片的url地址构造request请求
:param item:
:param info:
:return:
'''
if isinstance(item,ChinazprojectWebInfoItem):
# 获取图片地址
image_url = 'http:' + item['coverImage']
print('获取到图片地址', image_url)
yield scrapy.Request(image_url)
# 如果有多个图片地址item['coverImage']对应一个列表
# image_urls = 'http:' + item['coverImage']
# return [scrapy.Request(x) for x in image_urls]
def item_completed(self, results, item, info):
'''
图片下载之后的回调方法
:param results:[(True(表示图片是否下载成功),{'path':'图片下载之后的存储路径','url'.'图片url地址','checksum':'经过hash加密的一个字符串'})]
:param item:
:param info:
:return:
'''
if isinstance(item,ChinazprojectWebInfoItem):
paths = [result['path'] for status,result in results if status]
print('图片下载成功', paths)
if len(paths) > 0:
print('图片获取成功')
# 使用rname方法修改图片名称
os.rename(images_store + '/' + paths[0],images_store + '/' + item['title'] + '.jpg')
image_path = images_store + '/' + item['title'] + '.jpg'
print('修改后的图片路径', image_path)
item['localImagepath'] = image_path
else:
# 如果没有获取到图片吧这个item丢弃
from scrapy.exceptions import DropItem
raise DropItem('没有获取到图片,丢弃item')
return item
# class ChinazprojectPipeline(object):
#
# def __init__(self):
# """
# 初始化方法
# """
# # self.file = open('chainz.json','a')
# # 创建数据库连接
# self.client = c.Connect(
# host = '127.0.0.1', user = 'root', password = '123456',
# database = 'chinaz', port = 3306, charset='utf8'
# )
# #创建游标
# self.cursor = self.client.cursor()
#
# def open_spider(self,spider):
# """
# 爬虫启动的时候会调用一次
# :param spider:
# :return:
# """
# print('爬虫开启')
#
# def process_item(self, item, spider):
# """
# 这个方法是必须实现的,爬虫文件中所有的item
# 都会经过这个方法
# :param item: 爬虫文件传递过来的item对象
# :param spider: 爬虫文件实例化的对象
# :return:
# """
# #存储到本地json文件
# data_dict = dict(item)
#
# # import json
# # json_data = json.dumps(data_dict,ensure_ascii=False)
# # self.file.write(json_data+'\n')
# # 使用isinstance判断item要存储的表
# # if isinstance(item, ChinazprojectWebInfoItem):
# # print('网站信息')
# # tablename = 'webinfo'
# # elif isinstance(item, ChinazprojectItem):
# # print('网站分类信息')
# # tablename = 'tags'
# #往数据库写
# # sql = """
# # INSERT INTO tags(%s)
# # VALUES (%s)
# # """ %(
# # ','.join(data_dict.keys()),
# # ','.join(['%s']*len(data_dict))
# # )
# if data_dict:
#
# sql, data = item.get_insert_spl_data(data_dict)
# try:
# # self.cursor.execute(sql,list(data_dict.values()))
# self.cursor.execute(sql, data)
# self.client.commit()
# except Exception as err:
# self.client.rollback()
# print(err)
#
# #如果有多个管道文件,一定要注意return item,
# #否则下一个管道无法接收到item
# print('经过了管道文件')
# return item
#
# def close_spider(self,spider):
# """
# 爬虫结束的时候会调用一次
# :param spider:
# :return:
# """
# # self.file.close()
# self.cursor.close()
# self.client.close()
# print('爬虫结束')
# 往MongoDB中插入数据
# class ChinazprojectPipeline(object):
#
# def __init__(self,host,port,db):
# #创建MongoDB的数据库链接
# self.mongo_client = pymongo.MongoClient(
# host='127.0.0.1',port=27017
# )
# # 获取要操作的数据库
# self.db = self.mongo_client['db']
#
# @classmethod
# def from_crawler(cls,crawler):
# '''
# MONGODB_HOST = '127.0.0.1'
# MONGODB_PORT= 27017
# MONGODB_DB = "chinaz"
# :param crawler:
# :return:
# '''
# host = crawler.settings['MONGODB_HOST']
# port = crawler.settings['MONGODB_PORT']
# db = crawler.settings['MONGODB_DB']
# return cls(host,port,db)
#
# def process_item(self, item, spider):
# '''
# 这个方法是必须实现的,爬虫文件中所有的item
# 都会经过这个方法
# :param item: 爬虫文件传递过来的item对象
# :param spider: 爬虫文件实例化的对象
# :return:
# '''
# # 往哪个集合下插入数据
# # 往集合下插入什么数据
# col_name = item.get_mongodb_collectionName()
# col = self.db[col_name]
# dict_data = dict(item)
# try:
# col.insert(dict_data)
# print('数据插入成功')
# except Exception as err:
# print('数据插入失败',err)
# return item
#
# def open_spider(self,spider):
# print(spider.name,'爬虫开始运行')
#
# def close_spider(self, spider):
# # self.file.close()
# self.cursor.close()
# self.client.close()
# print('爬虫结束')
#实现mysql数据库的异步插入(要插入的数据量非常大的情况下)
from twisted.enterprise import adbapi
class ChinazprojectPipeline(object):
def __init__(self,dbpool):
self.dbpool = dbpool
@classmethod
def from_crawler(cls,cralwer):
"""
MYSQL_HOST = '127.0.0.1'
MYSQL_USER = 'root'
MYSQL_PWD = 'ljh1314'
MYSQL_DB = 'chainz'
MYSQL_PORT = 3306
MYSQL_CHARSET = 'utf8'
:param cralwer:
:return:
"""
db_parmars = {
'host':cralwer.settings['MYSQL_HOST'],
'user':cralwer.settings['MYSQL_USER'],
'passwd':cralwer.settings['MYSQL_PWD'],
'db':cralwer.settings['MYSQL_DB'],
'port':cralwer.settings['MYSQL_PORT'],
'charset':cralwer.settings['MYSQL_CHARSET'],
}
dbpool = adbapi.ConnectionPool('pymysql',**db_parmars)
return cls(dbpool)
def process_item(self,item,spider):
query = self.dbpool.runInteraction(
self.insert_data_to_mysql,
item
)
query.addErrback(
self.insert_err,
item
)
return item
def insert_data_to_mysql(self,cursor,item):
data_dict = dict(item)
sql,data = item.get_insert_sql_data(data_dict)
cursor.execute(sql,data)
def insert_err(self,failure,item):
print(failure,'插入失败',item)
scrapy shell
Scrapy终端是一个交互终端,我们可以在未启动spider的情况下尝试及调试代码,也可以用来测试XPath或CSS表达式,查看他们的工作方式,方便我们爬取的网页中提取的数据。
启动Scrapy Shell
scrapy shell "http://www.baidu.com/"
scrapy shell -s USER_AGENT=' '
Scrapy Shell根据下载的页面会自动创建一些方便使用的对象,例如 Response 对象,以及 Selector 对象 (对HTML及XML内容)。
当shell载入后,将得到一个包含response数据的本地 response 变量,输入 response.body将输出response的包体,输出 response.headers 可以看到response的包头。
输入 response.selector 时, 将获取到一个response 初始化的类 Selector 的对象,此时可以通过使用 response.selector.xpath()或response.selector.css() 来对 response 进行查询。
Scrapy也提供了一些快捷方式, 例如 response.xpath()或response.css()同样可以生效(如之前的案例)。
Selectors选择器 Scrapy Selectors 内置 XPath 和 CSS Selector 表达式机制 Selector有四个基本的方法,最常用的还是xpath:
xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表
extract(): 序列化该节点为字符串并返回list
css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4
re(): 根据传入的正则表达式对数据进行提取,返回字符串list列表
仅为个人学习小结,若有错处,欢迎指正~