diff --git a/downKsVideo/app.js b/downKsVideo/app.js index b4035bc..6bcf6a8 100644 --- a/downKsVideo/app.js +++ b/downKsVideo/app.js @@ -21,13 +21,31 @@ app.use((req, res, next) => { }) app.get('/', async (req, res) => { + let { page } = req.query let data = await request.post(config.visionProfileLikePhotoList, { page: "profile", - pcursor: "空" + pcursor: page }); res.json(data) }) -app.listen(3000, () => { - console.log(`http://127.0.0.1:3000`) +app.get('/tj', async (req, res) => { + let { feeds } = await request.post(config.visionNewRecoFeed, { + dailyFirstPage: false + }); + let result = [] + feeds.forEach(v => { + result.push({ + video_introduce: v.photo.caption, + video_url: v.photo.photoH265Url + }) + }); + + res.json(result) + + +}) + +app.listen(3030, () => { + console.log(`http://127.0.0.1:3030`) }) diff --git a/downKsVideo/data.js b/downKsVideo/data.js new file mode 100644 index 0000000..f877f4e --- /dev/null +++ b/downKsVideo/data.js @@ -0,0 +1,55 @@ +const request = require("./src/http/http"); +const config = require("./src/config/config"); +const fs = require('fs'); + +let result = [] + +// 延迟函数,单位为毫秒 +const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + +const main = async () => { + for (let index = 1; index <= 50; index++) { + try { + let { feeds } = await request.post(config.visionNewRecoFeed, { dailyFirstPage: false }); + console.log(`第: ${index} 次爬`); + console.log(`爬到的数据长度: ${feeds.length}`); + console.log(``); + + feeds.forEach(v => { + result.push({ + video_introduce: v.photo.caption, + video_url: v.photo.photoUrl, + video_img: v.photo.coverUrl + }); + }); + + // 设置延迟时间(例如3000毫秒=3秒) + await delay(1000); // 你可以调整这个值 + + } catch (error) { + console.error(`第 ${index} 次请求出错:`, error); + // 出错时也等待一段时间再继续 + await delay(3000); + } + } + + // 先读取现有数据,然后追加新数据 + fs.readFile('data.json', 'utf8', (err, data) => { + if (err) { + console.error('读取文件出错:', err); + return; + } + const existingData = JSON.parse(data); + console.log(`data.json 原文件数据长度: ${existingData.length}`); + console.log(`爬到的新数据长度: ${result.length}`); + const updatedData = [...existingData, ...result]; + console.log(`合并后总数据长度: ${updatedData.length}`); + + fs.writeFile('data.json', JSON.stringify(updatedData, null, 2), (err) => { + if (err) console.error('写入文件出错:', err); + else console.log('数据已成功追加到data.json'); + }); + }); +} + +main(); \ No newline at end of file diff --git a/downKsVideo/index.html b/downKsVideo/index.html index 95b1424..0765239 100644 --- a/downKsVideo/index.html +++ b/downKsVideo/index.html @@ -4,6 +4,7 @@ Document + @@ -33,29 +36,129 @@ diff --git a/downKsVideo/lazyload.min.js b/downKsVideo/lazyload.min.js new file mode 100644 index 0000000..15a0b42 --- /dev/null +++ b/downKsVideo/lazyload.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).LazyLoad=t()}(this,(function(){"use strict";const e="undefined"!=typeof window,t=e&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),a=e&&window.devicePixelRatio>1,n={elements_selector:".lazy",container:t||e?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_bg_set:"bg-set",data_poster:"poster",class_applied:"applied",class_loading:"loading",class_loaded:"loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1,restore_on_error:!1},s=e=>Object.assign({},n,e),l=function(e,t){let a;const n="LazyLoad::Initialized",s=new e(t);try{a=new CustomEvent(n,{detail:{instance:s}})}catch(e){a=document.createEvent("CustomEvent"),a.initCustomEvent(n,!1,!1,{instance:s})}window.dispatchEvent(a)},o="src",r="srcset",i="sizes",d="poster",c="llOriginalAttrs",_="data",u="loading",g="loaded",b="applied",h="error",m="native",p="data-",f="ll-status",v=(e,t)=>e.getAttribute(p+t),E=e=>v(e,f),I=(e,t)=>((e,t,a)=>{const n=p+t;null!==a?e.setAttribute(n,a):e.removeAttribute(n)})(e,f,t),y=e=>I(e,null),k=e=>null===E(e),A=e=>E(e)===m,L=[u,g,b,h],w=(e,t,a,n)=>{e&&"function"==typeof e&&(void 0===n?void 0===a?e(t):e(t,a):e(t,a,n))},x=(t,a)=>{e&&""!==a&&t.classList.add(a)},C=(t,a)=>{e&&""!==a&&t.classList.remove(a)},O=e=>e.llTempImage,M=(e,t)=>{if(!t)return;const a=t._observer;a&&a.unobserve(e)},z=(e,t)=>{e&&(e.loadingCount+=t)},N=(e,t)=>{e&&(e.toLoadCount=t)},T=e=>{let t=[];for(let a,n=0;a=e.children[n];n+=1)"SOURCE"===a.tagName&&t.push(a);return t},R=(e,t)=>{const a=e.parentNode;a&&"PICTURE"===a.tagName&&T(a).forEach(t)},G=(e,t)=>{T(e).forEach(t)},D=[o],H=[o,d],V=[o,r,i],F=[_],j=e=>!!e[c],B=e=>e[c],J=e=>delete e[c],S=(e,t)=>{if(j(e))return;const a={};t.forEach((t=>{a[t]=e.getAttribute(t)})),e[c]=a},P=(e,t)=>{if(!j(e))return;const a=B(e);t.forEach((t=>{((e,t,a)=>{a?e.setAttribute(t,a):e.removeAttribute(t)})(e,t,a[t])}))},U=(e,t,a)=>{x(e,t.class_applied),I(e,b),a&&(t.unobserve_completed&&M(e,t),w(t.callback_applied,e,a))},$=(e,t,a)=>{x(e,t.class_loading),I(e,u),a&&(z(a,1),w(t.callback_loading,e,a))},q=(e,t,a)=>{a&&e.setAttribute(t,a)},K=(e,t)=>{q(e,i,v(e,t.data_sizes)),q(e,r,v(e,t.data_srcset)),q(e,o,v(e,t.data_src))},Q={IMG:(e,t)=>{R(e,(e=>{S(e,V),K(e,t)})),S(e,V),K(e,t)},IFRAME:(e,t)=>{S(e,D),q(e,o,v(e,t.data_src))},VIDEO:(e,t)=>{G(e,(e=>{S(e,D),q(e,o,v(e,t.data_src))})),S(e,H),q(e,d,v(e,t.data_poster)),q(e,o,v(e,t.data_src)),e.load()},OBJECT:(e,t)=>{S(e,F),q(e,_,v(e,t.data_src))}},W=["IMG","IFRAME","VIDEO","OBJECT"],X=(e,t)=>{!t||(e=>e.loadingCount>0)(t)||(e=>e.toLoadCount>0)(t)||w(e.callback_finish,t)},Y=(e,t,a)=>{e.addEventListener(t,a),e.llEvLisnrs[t]=a},Z=(e,t,a)=>{e.removeEventListener(t,a)},ee=e=>!!e.llEvLisnrs,te=e=>{if(!ee(e))return;const t=e.llEvLisnrs;for(let a in t){const n=t[a];Z(e,a,n)}delete e.llEvLisnrs},ae=(e,t,a)=>{(e=>{delete e.llTempImage})(e),z(a,-1),(e=>{e&&(e.toLoadCount-=1)})(a),C(e,t.class_loading),t.unobserve_completed&&M(e,a)},ne=(e,t,a)=>{const n=O(e)||e;ee(n)||((e,t,a)=>{ee(e)||(e.llEvLisnrs={});const n="VIDEO"===e.tagName?"loadeddata":"load";Y(e,n,t),Y(e,"error",a)})(n,(s=>{((e,t,a,n)=>{const s=A(t);ae(t,a,n),x(t,a.class_loaded),I(t,g),w(a.callback_loaded,t,n),s||X(a,n)})(0,e,t,a),te(n)}),(s=>{((e,t,a,n)=>{const s=A(t);ae(t,a,n),x(t,a.class_error),I(t,h),w(a.callback_error,t,n),a.restore_on_error&&P(t,V),s||X(a,n)})(0,e,t,a),te(n)}))},se=(e,t,n)=>{(e=>W.indexOf(e.tagName)>-1)(e)?((e,t,a)=>{ne(e,t,a),((e,t,a)=>{const n=Q[e.tagName];n&&(n(e,t),$(e,t,a))})(e,t,a)})(e,t,n):((e,t,n)=>{(e=>{e.llTempImage=document.createElement("IMG")})(e),ne(e,t,n),(e=>{j(e)||(e[c]={backgroundImage:e.style.backgroundImage})})(e),((e,t,n)=>{const s=v(e,t.data_bg),l=v(e,t.data_bg_hidpi),r=a&&l?l:s;r&&(e.style.backgroundImage=`url("${r}")`,O(e).setAttribute(o,r),$(e,t,n))})(e,t,n),((e,t,n)=>{const s=v(e,t.data_bg_multi),l=v(e,t.data_bg_multi_hidpi),o=a&&l?l:s;o&&(e.style.backgroundImage=o,U(e,t,n))})(e,t,n),((e,t,a)=>{const n=v(e,t.data_bg_set);if(!n)return;let s=n.split("|").map((e=>`image-set(${e})`));e.style.backgroundImage=s.join(),U(e,t,a)})(e,t,n)})(e,t,n)},le=e=>{e.removeAttribute(o),e.removeAttribute(r),e.removeAttribute(i)},oe=e=>{R(e,(e=>{P(e,V)})),P(e,V)},re={IMG:oe,IFRAME:e=>{P(e,D)},VIDEO:e=>{G(e,(e=>{P(e,D)})),P(e,H),e.load()},OBJECT:e=>{P(e,F)}},ie=(e,t)=>{(e=>{const t=re[e.tagName];t?t(e):(e=>{if(!j(e))return;const t=B(e);e.style.backgroundImage=t.backgroundImage})(e)})(e),((e,t)=>{k(e)||A(e)||(C(e,t.class_entered),C(e,t.class_exited),C(e,t.class_applied),C(e,t.class_loading),C(e,t.class_loaded),C(e,t.class_error))})(e,t),y(e),J(e)},de=["IMG","IFRAME","VIDEO"],ce=e=>e.use_native&&"loading"in HTMLImageElement.prototype,_e=(e,t,a)=>{e.forEach((e=>(e=>e.isIntersecting||e.intersectionRatio>0)(e)?((e,t,a,n)=>{const s=(e=>L.indexOf(E(e))>=0)(e);I(e,"entered"),x(e,a.class_entered),C(e,a.class_exited),((e,t,a)=>{t.unobserve_entered&&M(e,a)})(e,a,n),w(a.callback_enter,e,t,n),s||se(e,a,n)})(e.target,e,t,a):((e,t,a,n)=>{k(e)||(x(e,a.class_exited),((e,t,a,n)=>{a.cancel_on_exit&&(e=>E(e)===u)(e)&&"IMG"===e.tagName&&(te(e),(e=>{R(e,(e=>{le(e)})),le(e)})(e),oe(e),C(e,a.class_loading),z(n,-1),y(e),w(a.callback_cancel,e,t,n))})(e,t,a,n),w(a.callback_exit,e,t,n))})(e.target,e,t,a)))},ue=e=>Array.prototype.slice.call(e),ge=e=>e.container.querySelectorAll(e.elements_selector),be=e=>(e=>E(e)===h)(e),he=(e,t)=>(e=>ue(e).filter(k))(e||ge(t)),me=function(t,a){const n=s(t);this._settings=n,this.loadingCount=0,((e,t)=>{ce(e)||(t._observer=new IntersectionObserver((a=>{_e(a,e,t)}),(e=>({root:e.container===document?null:e.container,rootMargin:e.thresholds||e.threshold+"px"}))(e)))})(n,this),((t,a)=>{e&&(a._onlineHandler=()=>{((e,t)=>{var a;(a=ge(e),ue(a).filter(be)).forEach((t=>{C(t,e.class_error),y(t)})),t.update()})(t,a)},window.addEventListener("online",a._onlineHandler))})(n,this),this.update(a)};return me.prototype={update:function(e){const a=this._settings,n=he(e,a);var s,l;N(this,n.length),t?this.loadAll(n):ce(a)?((e,t,a)=>{e.forEach((e=>{-1!==de.indexOf(e.tagName)&&((e,t,a)=>{e.setAttribute("loading","lazy"),ne(e,t,a),((e,t)=>{const a=Q[e.tagName];a&&a(e,t)})(e,t),I(e,m)})(e,t,a)})),N(a,0)})(n,a,this):(l=n,(e=>{e.disconnect()})(s=this._observer),((e,t)=>{t.forEach((t=>{e.observe(t)}))})(s,l))},destroy:function(){this._observer&&this._observer.disconnect(),e&&window.removeEventListener("online",this._onlineHandler),ge(this._settings).forEach((e=>{J(e)})),delete this._observer,delete this._settings,delete this._onlineHandler,delete this.loadingCount,delete this.toLoadCount},loadAll:function(e){const t=this._settings;he(e,t).forEach((e=>{M(e,this),se(e,t,this)}))},restoreAll:function(){const e=this._settings;ge(e).forEach((t=>{ie(t,e)}))}},me.load=(e,t)=>{const a=s(t);se(e,a)},me.resetStatus=e=>{y(e)},e&&((e,t)=>{if(t)if(t.length)for(let a,n=0;a=t[n];n+=1)l(e,a);else l(e,t)})(me,window.lazyLoadOptions),me})); diff --git a/downKsVideo/package.json b/downKsVideo/package.json index f038818..1c31435 100644 --- a/downKsVideo/package.json +++ b/downKsVideo/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "dev": "node --max-old-space-size=20240 src/index.js", - "http": "node-dev app.js" + "http": "node-dev app.js", + "tj": "node-dev src/tuijian.js" }, "keywords": [], "author": "", @@ -15,4 +16,4 @@ "graphql": "^15.5.0", "graphql-request": "^3.4.0" } -} +} \ No newline at end of file diff --git a/downKsVideo/src/config/config.js b/downKsVideo/src/config/config.js index 92720b6..ddbbd23 100644 --- a/downKsVideo/src/config/config.js +++ b/downKsVideo/src/config/config.js @@ -81,5 +81,21 @@ module.exports = { feeds { ...feedContentWithLiveInfo __typename } hostName pcursor __typename } } + `, + visionNewRecoFeed: gql` + fragment photoContent on PhotoEntity { __typename id duration caption originCaption likeCount viewCount commentCount realLikeCount coverUrl photoUrl photoH265Url manifest manifestH265 videoResource coverUrls { url __typename } timestamp expTag animatedCoverUrl distance videoRatio liked stereoType profileUserTopPhoto musicBlocked riskTagContent riskTagUrl + } + + fragment recoPhotoFragment on recoPhotoEntity { __typename id duration caption originCaption likeCount viewCount commentCount realLikeCount coverUrl photoUrl photoH265Url manifest manifestH265 videoResource coverUrls { url __typename } timestamp expTag animatedCoverUrl distance videoRatio liked stereoType profileUserTopPhoto musicBlocked riskTagContent riskTagUrl + } + + fragment feedContentWithLiveInfo on Feed { type author { id name headerUrl following livingInfo headerUrls { url __typename } __typename } photo { ...photoContent ...recoPhotoFragment __typename } canAddComment llsid status currentPcursor tags { type name __typename } __typename + } + + fragment photoResult on PhotoResult { result llsid expTag serverExpTag pcursor feeds { ...feedContentWithLiveInfo __typename } webPageArea __typename + } + + query visionNewRecoFeed($semKeyword: String, $semCrowd: String, $utmSource: String, $utmMedium: String, $utmCampaign: String, $dailyFirstPage: Boolean) { visionNewRecoFeed(semKeyword: $semKeyword, semCrowd: $semCrowd, utmSource: $utmSource, utmMedium: $utmMedium, utmCampaign: $utmCampaign, dailyFirstPage: $dailyFirstPage) { ...photoResult __typename } + } ` }; \ No newline at end of file diff --git a/downKsVideo/src/http/http.js b/downKsVideo/src/http/http.js index 0192f8f..b36fcd6 100644 --- a/downKsVideo/src/http/http.js +++ b/downKsVideo/src/http/http.js @@ -5,19 +5,19 @@ class GraphqlRequest { constructor() { this.client = new GraphQLClient(config.url, { headers: { - Cookie: 'kpf=PC_WEB; clientid=3; did=web_4abc14d7790d18335fedc13249834c42; userId=58344290; kuaishou.server.webday7_st=ChprdWFpc2hvdS5zZXJ2ZXIud2ViZGF5Ny5zdBKwARn6brG-fMigWK_p8OkNIz3auEEilOc5O0XadgJ_dkmu41zuO2zSfxcmYBuyqph0KnFS-cVu-YbgPb2HqxwbgaznjeTKtJzieVkfoYClBCJy2Hojx7euihIStw4izkYtP7ztIaIAOK5_kiGFvS2_6V4jAXupKegEjQeM9nd2vxpkSuw63dHwZDzfN1EmTr5__nCExUlhCcxi-vyBl446ReOLlywiTjMe5AGI2_I1uz4dGhLcWLZU0wMn7F5ONAeuFgyQkCwiIFEFfJL9dH1pCQZANFR6CCZP1SaZ_xw7ZpeNre2m31gjKAUwAQ; kuaishou.server.webday7_ph=07c02e409d3bb524d0531c7d3e2d8b539eb6; kpn=KUAISHOU_VISION' + Cookie: 'did=web_8e01e1d7a6199e8ad21de2e5f486f2db; clientid=3; kpf=PC_WEB; kpn=KUAISHOU_VISION' }, }) } async post(document, variables) { - try { - let res = await this.client.request(document, variables); - return res.visionProfileLikePhotoList || res.visionProfilePhotoList; - } catch (error) { - console.log("===========================请求出错==========================="); - console.log(error.message); - console.log("===========================请求出错==========================="); - } + try { + let res = await this.client.request(document, variables); + return res.visionProfileLikePhotoList || res.visionProfilePhotoList || res.visionNewRecoFeed; + } catch (error) { + console.log("===========================请求出错==========================="); + console.log(error.message); + console.log("===========================请求出错==========================="); + } } } diff --git a/downKsVideo/src/utils/utils.js b/downKsVideo/src/utils/utils.js index b4a2914..dedc6e1 100644 --- a/downKsVideo/src/utils/utils.js +++ b/downKsVideo/src/utils/utils.js @@ -14,81 +14,81 @@ pr.on('uncaughtException', err => { class Utils { - TotalFeeds = [] - TotalCount = 0 - principalId = "" - type = "" - userId + TotalFeeds = [] + TotalCount = 0 + principalId = "" + type = "" + userId - init (type = "visionProfileLikePhotoList", userId) { - this.type = type - this.userId = userId - } + init(type = "visionProfileLikePhotoList", userId) { + this.type = type + this.userId = userId + } - async start() { - let params = { page: "profile", pcursor: this.principalId } + async start(params) { + let params = { page: "profile", pcursor: this.principalId } - this.userId ? params.userId = this.userId : null - let { pcursor, feeds } = await request.post(config[this.type], params); - - this.principalId = pcursor; + this.userId ? params.userId = this.userId : null + let { pcursor, feeds } = await request.post(config[this.type], params); - console.log(""); - console.log(`1:本单次请求到:${feeds.length} 条视频`); - - // console.log(`下一页页码为:${principalId}`); - this.TotalFeeds = [...this.TotalFeeds, ...feeds]; - console.log("2:当前总视频数为:", this.TotalFeeds.length); - console.log(""); - console.log("-----------视频下载进度-------------"); - feeds.forEach(async item => { - await this.getVideoData(item.photo.photoUrl, 'binary', item.photo.id); - }); - } + this.principalId = pcursor; - async getVideoData(url, encoding, filename) { - return new Promise((resolve, reject) => { - let req = https.get(url, res => { - let result = '' - encoding && res.setEncoding(encoding) - res.on('data', d => result += d) - res.on('end', () => this.savefileToPath(result, filename)) - res.on('error', e => { - console.log("1 res.on 出错了:", e); - console.log("重新请求数据中:", url); - this.getVideoData(url, encoding, filename) - }) - }) - req.end() + console.log(""); + console.log(`1:本单次请求到:${feeds.length} 条视频`); + + // console.log(`下一页页码为:${principalId}`); + this.TotalFeeds = [...this.TotalFeeds, ...feeds]; + console.log("2:当前总视频数为:", this.TotalFeeds.length); + console.log(""); + console.log("-----------视频下载进度-------------"); + feeds.forEach(async item => { + await this.getVideoData(item.photo.photoUrl, 'binary', item.photo.id); + }); + } + + async getVideoData(url, encoding, filename) { + return new Promise((resolve, reject) => { + let req = https.get(url, res => { + let result = '' + encoding && res.setEncoding(encoding) + res.on('data', d => result += d) + res.on('end', () => this.savefileToPath(result, filename)) + res.on('error', e => { + console.log("1 res.on 出错了:", e); + console.log("重新请求数据中:", url); + this.getVideoData(url, encoding, filename) }) - } + }) + req.end() + }) + } - - async savefileToPath(fileData, fileName) { - let fileFullName = path.join(__dirname, `../../dist/${fileName}.mp4`) - return new Promise((resolve, reject) => { - fs.writeFile(fileFullName, fileData, 'binary', err => { - if (err) { - console.log('savefileToPath error:', err) - } else { - this.TotalCount += 1; - console.log(`第 ${this.TotalCount} 条视频:${fileName}.mp4 已保存到本地!`); - // Screenshot(fileName) - if (this.TotalCount >= this.TotalFeeds.length) { - console.log("-----------视频下载进度-------------"); - console.log(""); - console.log(`3:已下载:${this.TotalFeeds.length} 条视频,5秒 后将开始下载 下一页..`); - console.log("") - console.log("") - setTimeout(() => this.start(), 5000) - } - resolve('已下载') - } - }) - }) - } + async savefileToPath(fileData, fileName) { + let fileFullName = path.join(__dirname, `../../dist/${fileName}.mp4`) + return new Promise((resolve, reject) => { + fs.writeFile(fileFullName, fileData, 'binary', err => { + if (err) { + console.log('savefileToPath error:', err) + } else { + this.TotalCount += 1; + console.log(`第 ${this.TotalCount} 条视频:${fileName}.mp4 已保存到本地!`); + // Screenshot(fileName) + if (this.TotalCount >= this.TotalFeeds.length) { + console.log("-----------视频下载进度-------------"); + console.log(""); + console.log(`3:已下载:${this.TotalFeeds.length} 条视频,5秒 后将开始下载 下一页..`); + console.log("") + console.log("") + + setTimeout(() => this.start(), 5000) + } + resolve('已下载') + } + }) + }) + } } diff --git a/downKsVideo/src/visionProfileLikePhotoList.js b/downKsVideo/src/visionProfileLikePhotoList.js index 35cc16d..5ed9b41 100644 --- a/downKsVideo/src/visionProfileLikePhotoList.js +++ b/downKsVideo/src/visionProfileLikePhotoList.js @@ -3,4 +3,4 @@ const utils = require("./utils/utils"); let u = new utils() // u.init("visionProfileLikePhotoList") u.init("visionProfilePhotoList", "3xiem77vya42p4y") -u.start() \ No newline at end of file +u.start({ page: "profile", pcursor: this.principalId }) \ No newline at end of file diff --git a/downKsVideo/test.html b/downKsVideo/test.html new file mode 100644 index 0000000..d26d1fe --- /dev/null +++ b/downKsVideo/test.html @@ -0,0 +1,163 @@ + + + + + + Document + + + + + + + +