言:
圣诞节快到了,是不是要给女朋友或者正在追求的妹子一点小惊喜呢,今天这篇博客就分享下前端代码如何实现3D立体动态相册。赶紧学会了,来制作属于我们程序员的浪漫吧!先上效果图,来引起下你们的兴趣。
正文:
一、新建一个index.html的文件,代码如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>纯CSS实现鼠标经过3D立体动态展示图片特效代码</title> <link type="text/css" href="css/style.css" rel="stylesheet" /> </head> <body> <div class="box"> <ul class="minbox"> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> <ol class="maxbox"> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ol> </div> </body> </html>
二、css样式的代码
@charset "utf-8"; *{ margin:0; padding:0; } body{ max-width: 100%; min-width: 100%; height: 100%; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-size:100% 100%; position: absolute; margin-left: auto; margin-right: auto; } li{ list-style: none; } .box{ width:200px; height:200px; background-size: cover; background-repeat: no-repeat; background-attachment: fixed; background-size:100% 100%; position: absolute; margin-left: 42%; margin-top: 22%; -webkit-transform-style:preserve-3d; -webkit-transform:rotateX(13deg); -webkit-animation:move 5s linear infinite; } .minbox{ width:100px; height:100px; position: absolute; left:50px; top:30px; -webkit-transform-style:preserve-3d; } .minbox li{ width:100px; height:100px; position: absolute; left:0; top:0; } .minbox li:nth-child(1){ background: url(../img/01.png) no-repeat 0 0; -webkit-transform:translateZ(50px); } .minbox li:nth-child(2){ background: url(../img/02.png) no-repeat 0 0; -webkit-transform:rotateX(180deg) translateZ(50px); } .minbox li:nth-child(3){ background: url(../img/03.png) no-repeat 0 0; -webkit-transform:rotateX(-90deg) translateZ(50px); } .minbox li:nth-child(4){ background: url(../img/04.png) no-repeat 0 0; -webkit-transform:rotateX(90deg) translateZ(50px); } .minbox li:nth-child(5){ background: url(../img/05.png) no-repeat 0 0; -webkit-transform:rotateY(-90deg) translateZ(50px); } .minbox li:nth-child(6){ background: url(../img/06.png) no-repeat 0 0; -webkit-transform:rotateY(90deg) translateZ(50px); } .maxbox li:nth-child(1){ background: url(../img/1.png) no-repeat 0 0; -webkit-transform:translateZ(50px); } .maxbox li:nth-child(2){ background: url(../img/2.png) no-repeat 0 0; -webkit-transform:translateZ(50px); } .maxbox li:nth-child(3){ background: url(../img/3.png) no-repeat 0 0; -webkit-transform:rotateX(-90deg) translateZ(50px); } .maxbox li:nth-child(4){ background: url(../img/4.png) no-repeat 0 0; -webkit-transform:rotateX(90deg) translateZ(50px); } .maxbox li:nth-child(5){ background: url(../img/5.png) no-repeat 0 0; -webkit-transform:rotateY(-90deg) translateZ(50px); } .maxbox li:nth-child(6){ background: url(../img/6.png) no-repeat 0 0; -webkit-transform:rotateY(90deg) translateZ(50px); } .maxbox{ width: 800px; height: 400px; position: absolute; left: 0; top: -20px; -webkit-transform-style: preserve-3d; } .maxbox li{ width: 200px; height: 200px; background: #fff; border:1px solid #ccc; position: absolute; left: 0; top: 0; opacity: 0.2; -webkit-transition:all 1s ease; } .maxbox li:nth-child(1){ -webkit-transform:translateZ(100px); } .maxbox li:nth-child(2){ -webkit-transform:rotateX(180deg) translateZ(100px); } .maxbox li:nth-child(3){ -webkit-transform:rotateX(-90deg) translateZ(100px); } .maxbox li:nth-child(4){ -webkit-transform:rotateX(90deg) translateZ(100px); } .maxbox li:nth-child(5){ -webkit-transform:rotateY(-90deg) translateZ(100px); } .maxbox li:nth-child(6){ -webkit-transform:rotateY(90deg) translateZ(100px); } .box:hover ol li:nth-child(1){ -webkit-transform:translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } .box:hover ol li:nth-child(2){ -webkit-transform:rotateX(180deg) translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } .box:hover ol li:nth-child(3){ -webkit-transform:rotateX(-90deg) translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } .box:hover ol li:nth-child(4){ -webkit-transform:rotateX(90deg) translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } .box:hover ol li:nth-child(5){ -webkit-transform:rotateY(-90deg) translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } .box:hover ol li:nth-child(6){ -webkit-transform:rotateY(90deg) translateZ(300px); width: 400px; height: 400px; opacity: 0.8; left: -100px; top: -100px; } @keyframes move{ 0%{ -webkit-transform: rotateX(13deg) rotateY(0deg); } 100%{ -webkit-transform:rotateX(13deg) rotateY(360deg); } }
三、文件的目录结构,把css文件放到css文件夹里,图片放到img文件夹里。
四、图片尺寸不会修改的,可以选择使用美图秀秀网页版很简单
五、容易出现的问题 ,图片展示不全或展示的方向不对
1.图片尺寸需要修改 (解决展示不全的情况)
上图是我的图片文件夹里放的图片 ,首先01-06编号命名的图片尺寸是100x100px的大小的,1-6编号是400x400px,如果效果想展示最佳,100x100px的图片是以头部特写的照片最好,因为01-06是立体照片内部小正方体的照片,1-6编号是外部正方体的照片。
2.图片的方向需要修改(解决头朝下的问题)
修改的方向如上图展示所示,比如第三张头就应该朝下,否则形成的效果图像会出现头是倒着的情况。
六、放一下动态效果图
总结:
生活虽然总有辛酸,但是我们不应该减少去创造生活的乐趣,不断寻找属于自己的那份快乐,才应该是我们生活的态度。
我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。
原文链接:https://blog.csdn.net/jdk_wangtaida/article/details/103253883
析QQ空间
登录QQ空间
爬取第一步,分析站点,首先需要知道如何登录QQ空间。最初想法是用requests库配置登录请求,模拟登录,但是不久便放弃了这一思路,请看下图↓
login
根据登录按钮绑定的监听事件可以追踪到该按钮的点击事件如下:
login function
账号加密是必然的,但这一堆堆的代码真心不好解析,有耐心的勇士尽情一试!
在排除这种登录方法后,选择selenium模拟用户登录不失为省时省力的方法,而且我们只是需要通过selenium完成登录,获取到Cookies和后面讲述的g_tk参数后,就可以停用了,所以效率并不太低。
分析空间相册
登录以后,页面会跳转至 [https://user.qzone.qq.com/{QQ_NUMBER}](javascript:;), 这时把鼠标移到导航栏你会发现,所有的导航栏链接都是javascript:; 。没错就是这么坑,一切都是暗箱操作。
当然这并不难处理,使用调试工具捕获点击后产生的请求,然后过滤出正确的请求包即可。因为网络包非常多,那么怎么过滤呢,猜想相册数据的API必然会返回个列表list,尝试过滤list然后逐个排除,最后定位到请求包。下面是通过fcg_list过滤后的数据包,列表信息以jsonp格式返回,稍作处理即可当做json格式来读取(后面有讲)。
album list
从Headers和Response可以分别获取到两组重要信息:
先看请求包:
# url https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3 # args g_tk: 477819917 callback: shine0_Callback t: 691481346 hostUin: 123456789 uin: 123456789 appid: 4 inCharset: utf-8 outCharset: utf-8 source: qzone plat: qzone format: jsonp notice: 0 filter: 1 handset: 4 pageNumModeSort: 40 pageNumModeClass: 15 needUserInfo: 1 idcNum: 4 callbackFun: shine0 _: 1551788226819
其中hostUin, uin都是QQ号,g_tk是必须的且每次重新登录都会更新(后面有讲如何获取),其它有些参数不是必须的,我尝试后整理出如下请求参数:
query = { 'g_tk': self.g_tk, 'hostUin': self.username, 'uin': self.username, 'appid': 4, 'inCharset': 'utf-8', 'outCharset': 'utf-8', 'source': 'qzone', 'plat': 'qzone', 'format': 'jsonp' }
接下来看jsonp格式的跨域响应包:
shine0_Callback({ "code":0, "subcode":0, "message":"", "default":0, "data": { "albumListModeSort" : [ { "allowAccess" : 1, "anonymity" : 0, "bitmap" : "10000000", "classid" : 106, "comment" : 11, "createtime" : 1402661881, "desc" : "", "handset" : 0, "id" : "V13LmPKk0JLNRY", "lastuploadtime" : 1402662103, "modifytime" : 1408271987, "name" : "毕业季", "order" : 0, "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/a\/dIY29GUbJgAA", "priv" : 1, "pypriv" : 1, "total" : 4, "viewtype" : 0 },
shine0_Callback是请求包的callbackFun参数决定的,如果没这个参数,响应包会以_Callback作为默认名,当然这都不重要。所有相册信息以json格式存入albumListModeSort中,上面仅截取了一个相册的信息。
相册信息中,name代表相册名称,id作为唯一标识可用于请求该相册内的照片信息,而pre仅仅是一个预览缩略图的链接,无关紧要。
分析单个相册
与获取相册信息类似,进入某一相册,使用cgi_list过滤数据包,找到该相册的照片信息
photo list
同样的道理,根据数据包可以获取照片列表信息的请求包和响应信息,先看请求:
# url https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo # args g_tk: 477819917 callback: shine0_Callback t: 952444063 mode: 0 idcNum: 4 hostUin: 123456789 topicId: V13LmPKk0JLNRY noTopic: 0 uin: 123456789 pageStart: 0 pageNum: 30 skipCmtCount: 0 singleurl: 1 batchId: notice: 0 appid: 4 inCharset: utf-8 outCharset: utf-8 source: qzone plat: qzone outstyle: json format: jsonp json_esc: 1 question: answer: callbackFun: shine0 _: 1551790719497
其中有几个关键参数:
为了一次性获取所有照片,可以将pageStart设为0,pageNum设为所有相册所含照片的最大值。
同样可以对上面的参数进行简化,在相册列表请求参数的基础上添加topicId,pageStart和pageNum三个参数即可。
下面来看返回的照片列表信息:
shine0_Callback({ "code":0, "subcode":0, "message":"", "default":0, "data": { "limit" : 0, "photoList" : [ { "batchId" : "1402662093402000", "browser" : 0, "cameratype" : " ", "cp_flag" : false, "cp_x" : 455, "cp_y" : 388, "desc" : "", "exif" : { "exposureCompensation" : "", "exposureMode" : "", "exposureProgram" : "", "exposureTime" : "", "flash" : "", "fnumber" : "", "focalLength" : "", "iso" : "", "lensModel" : "", "make" : "", "meteringMode" : "", "model" : "", "originalTime" : "" }, "forum" : 0, "frameno" : 0, "height" : 621, "id" : 0, "is_video" : false, "is_weixin_mode" : 0, "ismultiup" : 0, "lloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!", "modifytime" : 1402661792, "name" : "QQ图片20140612104616", "origin" : 0, "origin_upload" : 0, "origin_url" : "", "owner" : "123456789", "ownername" : "123456789", "photocubage" : 91602, "phototype" : 1, "picmark_flag" : 0, "picrefer" : 1, "platformId" : 0, "platformSubId" : 0, "poiName" : "", "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/a\/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!", "raw" : "http:\/\/r.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/r\/dIY29GUbJgAA", "raw_upload" : 1, "rawshoottime" : 0, "shoottime" : 0, "shorturl" : "", "sloc" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!", "tag" : "", "uploadtime" : "2014-06-13 20:21:33", "url" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfSk58K2rQY!\/b\/dIY29GUbJgAA&bo=pANtAgAAAAABCeY!", "width" : 932, "yurl" : 0 }, // ... ] "t" : "952444063", "topic" : { "bitmap" : "10000000", "browser" : 0, "classid" : 106, "comment" : 1, "cover_id" : "NDN0sggyKs3smlOg6eYghjb0ZRsmAAA!", "createtime" : 1402661881, "desc" : "", "handset" : 0, "id" : "V13LmPKk0JLNRY", "is_share_album" : 0, "lastuploadtime" : 1402662103, "modifytime" : 1408271987, "name" : "毕业季", "ownerName" : "707922098", "ownerUin" : "707922098", "pre" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/a\/dIY29GUbJgAA", "priv" : 1, "pypriv" : 1, "share_album_owner" : 0, "total" : 4, "url" : "http:\/\/b171.photo.store.qq.com\/psb?\/V13LmPKk0JLNRY\/eSAslg*mYWaytEtLysg*Q*5Km91gIWfGuwSk58K2rQY!\/b\/dIY29GUbJgAA", "viewtype" : 0 }, "totalInAlbum" : 4, "totalInPage" : 4 }
返回的照片信息都存于photoList, 上面同样只截取了一张照片的信息,后面一部分返回的是当前相册的一些基本信息。totalInAlbum, totalInPage存储了当前相册总共包含的照片数及本次返回的照片数。而我们需要下载的图片链接则是url!
OK, 到此,所有请求和响应数据都分析清楚了,接下来便是coding的时候了。
确定爬取方案
创建qqzone类
class qqzone(object): """QQ空间相册爬虫""" def __init__(self, user): self.username = user['username'] self.password = user['password']
模拟登录
from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import WebDriverExceptio # ... def _login_and_get_args(self): """登录QQ,获取Cookies和g_tk""" opt = webdriver.ChromeOptions() opt.set_headless() driver = webdriver.Chrome(chrome_options=opt) driver.get('https://i.qq.com/') # time.sleep(2) logging.info('User {} login...'.format(self.username)) driver.switch_to.frame('login_frame') driver.find_element_by_id('switcher_plogin').click() driver.find_element_by_id('u').clear() driver.find_element_by_id('u').send_keys(self.username) driver.find_element_by_id('p').clear() driver.find_element_by_id('p').send_keys(self.password) driver.find_element_by_id('login_button').click() time.sleep(1) driver.get('https://user.qzone.qq.com/{}'.format(self.username))
此处需要注意的是:
获取 Cookies
使用selenium获取Cookies非常方便
self.cookies = driver.get_cookies()
获取 g_tk
获取g_tk最开始可以说是本爬虫最大的难点,因为从网页中根本找不到直接写明的数值,只有各种函数调用。为此我全局搜索,发现好多地方都有其获取方式。
g_tk
最后选择了其中一处,通过selenium执行脚本的功能成功获取到了g_tk!
self.g_tk = driver.execute_script('return QZONE.FP.getACSRFToken()')
到此,selenium的使命就完成了,剩下的将通过requests来完成。
初始化 request.Session
接下来需要逐步生成请求然后获取数据。但是为方便起见,这里使用会话的方式请求数据,配置好cookie和headers,省的每次请求都设置一遍。
def _init_session(self): self.session = requests.Session() for cookie in self.cookies: self.session.cookies.set(cookie['name'], cookie['value']) self.session.headers = { 'Referer': 'https://qzs.qq.com/qzone/photo/v7/page/photo.html?init=photo.v7/module/albumList/index&navBar=1', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36' }
请求相册信息
获取相册信息,需要先封装好请求参数,然后通过session.get爬取数据,再通过正则匹配以json格式读取jsonp数据,最后解析所需的name和id。
def _get_ablum_list(self): """获取相册的列表信息""" album_url = '{}{}'.format( 'https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/fcg_list_album_v3?', self._get_query_for_request()) logging.info('Getting ablum list id...') resp = self.session.get(album_url) data = self._load_callback_data(resp) album_list = {} for item in data['data']['albumListModeSort']: album_list[item['name']] = item['id'] return album_list
其中的参数组合来自下面的函数_get_query_for_request函数。
def _get_query_for_request(self, topicId=None, pageStart=0, pageNum=100): """获取请求相册信息或照片信息所需的参数 Args: topicId: 每个相册对应的唯一标识符 pageStart: 请求某个相册的照片列表信息所需的起始页码 pageNum: 单次请求某个相册的照片数量 Returns: 一个组合好所有请求参数的字符串 """ query = { 'g_tk': self.g_tk, 'hostUin': self.username, 'uin': self.username, 'appid': 4, 'inCharset': 'utf-8', 'outCharset': 'utf-8', 'source': 'qzone', 'plat': 'qzone', 'format': 'jsonp' } if topicId: query['topicId'] = topicId query['pageStart'] = pageStart query['pageNum'] = pageNum return '&'.join('{}={}'.format(key, val) for key, val in query.items())
其中的jsonp解析函数如下,主体部分就是一个正则匹配,非常简单。
def _load_callback_data(self, resp): """以json格式解析返回的jsonp数据""" try: resp.encoding = 'utf-8' data = loads(re.search(r'.*?\(({.*}).*?\).*', resp.text, re.S)[1]) return data except ValueError: logging.error('Invalid input')
解析并下载照片
获取相册列表后,逐个请求照片列表信息,进而逐一下载
def _get_photo(self, album_name, album_id): """获取单个相册的照片列表信息,并下载该相册所有照片""" photo_list_url = '{}{}'.format( 'https://h5.qzone.qq.com/proxy/domain/photo.qzone.qq.com/fcgi-bin/cgi_list_photo?', self._get_query_for_request(topicId=album_id)) logging.info('Getting photo list for album {}...'.format(album_name)) resp = self.session.get(photo_list_url) data = self._load_callback_data(resp) if data['data']['totalInPage'] == 0: return None file_dir = self.get_path(album_name) for item in data['data']['photoList']: path = '{}/{}.jpg'.format(file_dir, item['name']) logging.info('Downloading {}-{}'.format(album_name, item['name'])) self._download_image(item['url'], path)
下载图片也是通过request,记得设置超时时间。
def _download_image(self, url, path): """下载单张照片""" try: resp = self.session.get(url, timeout=15) if resp.status_code == 200: open(path, 'wb').write(resp.content) except requests.exceptions.Timeout: logging.warning('get {} timeout'.format(url)) except requests.exceptions.ConnectionError as e: logging.error(e.__str__) finally: pass
爬取测试
capturing
downloaded photos
写在最后
助CSS所提供的animation动画属性及2D、3D变换属性,我们可以摆脱对JavaScript的依赖,设计开发各类效果优秀的前端动态效果,在之前文章和视频中我们也介绍了不少基于CSS与JavaScript技术实现的各类动画及页面元素设计效果。本文主要介绍使用CSS技术实现精美的3D旋转相册效果。主要实现效果动画展示如下:
纯CSS技术实现旋转相册效果展示
本例开发主要涉及使用技术包括animation动画属性、keyframes关键帧、transform变形等相关知识及方法、技术。部分核心技术说明如下:
1、CSS自定义属性(变量)
在使用CSS进行样式定义时,可以使用自定义属性,即变量。从变量而言考虑的话,会涉及到变量的定义、赋值与使用等。自定义属性的声明使用--表示。其赋值与style样式其他属性类似可通过:(冒号)进行赋值。在使用该自定义属性时需要用var()函数对属性名进行包裹。如在自定义变量需要进行数学运算时需要使用calc()方法。自定义属性实例如下:
:root{--fontSize:2em;}
p{font-size:var(--fontSize);}
CSS自定义属性使用实例如上所示,我们定义了一个fontSize变量,在P元素选择器中使用了该定义的变量,即设置P段落字体size为2em。
2、3D变换属性与方法
实现3D效果需要使用3D变换相关属性与方法,其主要属性方法包括transform-origin(旋转原点)、transform-style(旋转类型2D/3D)、perspective(透视点)等属性,主要方法包括translate位移、scale放缩、rotate旋转与skew扭曲等。使用实例代码如下:
position: absolute;//定位
transform-origin: center;//中心点
transform-style: preserve-3d;//类型
transform:
translateZ(400px) rotateY(60deg);
//沿Z轴移动400px,沿Y轴旋转60度
3、flex布局
本例需要使用flex布局用于实现将页面元素在页面中位置进行定位与布局设置,主要设置水平与垂直居中效果,实例代码如下:
display: flex;
justify-content: center;
align-items: center;
在明确以上基本技术点之后,我们就可以收集素材完整3D旋转相册的设计与制作。首先第一步就是素材的采集,本例所需图片如下所示:
3D旋转相册图片素材
在完成素材搜集基础上就可以使用CSS页面布局技术等,实现页面的布局,通过旋转变换,将10张图片进行不同角度的分布。其中页面布局代码如下:
页面布局代码
完成页面元素设置之后,就要考虑页面布局问题,本例需要实现旋转因此需要将每一个图片所对应元素进行旋转等变换设置。部分代码如下:
CSS样式设置
核心CSS样式设置如上图所示,其中我们定义了动画animate,因此需要使用keyframes对其关键帧进行定义,关键帧定义如下:
动画关键帧定义
通过关键帧定义之后,整个gallery层就会绕着y轴进行旋转最终实现动态旋转效果。本例静态展示如下图所示:
3D旋转相册静态效果展示
以上给出了3D旋转相册设计及实现过程核心知识点及实现思路过程、核心代码说明。如需完整代码请关注并私信。
本头条号长期关注编程资讯分享;编程课程、素材、代码分享及编程培训。如果您对以上方面有兴趣或代码错误、建议与意见,可在评论区回复。更多程序设计相关教程及实例分享,期待大家关注与阅读!
*请认真填写需求信息,我们会在24小时内与您取得联系。