520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)相关的知识,希望对你有一定的参考价值。
文章目录
一、前言
点关注不迷路,持续输出Unity
干货文章。
嗨,大家好,我是新发。
今天是2021年5月19日
,明天就是5.20
了(可能粉丝们看到这篇文章时已经5.20
了),该表示表示了。我的粉丝数量马上突破两万了,我决定用Unity
做一个Demo
,给我这近两万粉丝比心。那么,开始吧~
二、最终效果
最终Unity
运行效果如下:
点击比心,访问对应的博客主页:
本文Demo
工程已上传到CodeChina
,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityCSDNFansList
注:我使用的Unity
版本:Unity 2020.1.14f1c1 (64-bit)
。
三、读取CSDN粉丝列表数据
1、分析粉丝列表页面结构
首先,分析一下CSDN
粉丝页面的页面结构。在浏览器中按F12
调试,可以看到粉丝名字的节点class
是sub-people-username
。
进一步跟进到节点中,还可以看到粉丝博客url
和头像的url
。
部分粉丝写了简介,也一起读取下来吧。
2、爬数据
开始爬数据…
注:爬虫我就不教大家啦,怕教坏小朋友。之前看新闻看到有程序员因为弄爬虫被抓的。
最后生成一个json
文件,如下:
四、Unity制作
1、文件读取
把上面生成的json
文件放到Unity
工程的Resources
目录中。
这样,我们就可以直接通过Resources.Load
来读取文件。
var fansJsonStr = Resources.Load<TextAsset>("fans_list").text;
注:关于
Unity
文件目录结构以及Resources.Load
读取文件的教程,可以参见我之前写的这篇文章:《学Unity的猫》——第五章:规范Unity的工程目录结构,
以及这篇文章:《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》
2、c#解析json
读取了json
文件内容后,我们需要对数据进行解析。Unity
中大家常用的json
解析库是LitJson
,可以从GitHub
找到源码。
GitHub
地址:https://github.com/LitJSON/litjson
不过我这边弄了一个迷你版的jsoin
解析库,可以参见我之前写的这篇文章:《用C#实现一个迷你json库,无需引入dll(可直接放到Unity中使用)》
解析json
的逻辑封装在JSONConvert
类中,源码参见文章末尾。
这样,我们就可以解析json
数据了。
var fansJsonStr = Resources.Load<TextAsset>("fans_list").text;
// 解析json数据
var fansJsonArray = JSONConvert.DeserializeArray(fansJsonStr);
foreach (JSONObject dataItem in fansJsonArray)
{
var fansName = (string)dataItem["name"]; // 昵称
var fansIntro = (string)dataItem["intro"]; // 简介
var fansBlogUrl = (string)dataItem["blog_url"]; // 博客地址
var fansImageUrl = (string)dataItem["img_url"]; // 头像地址
// ...
}
3、UGUI循环复用列表
由于粉丝数据达到近两万条,我们要在一个列表中显示这么多数据,如果创建近两万个ui item
的话性能肯定是很差的,所以必须循环复用ui item
。
循环复用列表的原理其实就是,列表向上滑动时,当item
超过显示区域的上边界时,把item
移动到列表底部,重复使用item
并更新item
的ui
显示,向下滑动同理,把超过显示区域底部的item复用到顶部。
为了方便大家理解,我画成图,如下:
注:循环列表的具体实现,我之前写过一篇教程,可以参见我之前写的这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》
循环列表的逻辑封装在RecyclingListView
和RecyclingListViewItem
类中,源码参见文章末尾。
4、头像的加载
我们拿到的头像数据是一个https
链接,我们可以通过UnityWebRequest
来请求下载头像。
例:
string url = "https://profile.csdnimg.cn/6/6/4/3_m0_57622304";
var request = new UnityWebRequest(url);
downloadHandlerTexture = new DownloadHandlerTexture(true);
request.downloadHandler = downloadHandlerTexture;
request.SendWebRequest();
while(!request.isDone)
{
// 等待
// 这里的while等待逻辑,可以改成协程yield return request.SendWebRequest();
}
if (string.IsNullOrEmpty(request.error))
{
Texture2D tex2D = downloadHandlerTexture.texture;
// TODO
}
注:关于
UnityWebRequest
的详细使用教程,可以参见我之前写的这篇文章:《长江后浪推前浪,UnityWebRequest替代WWW》
5、gif头像问题
运行过程中,我发现有不少头像加载出来是一个问号。
我手动下载对应头像,发现它们其实是gif
格式,比如这个:
虽然它看起来不会动,但它实际上是gif
格式的,我们可以使用二进制查看器查看它的头部,可以看到是47 49 46 38 39 61
,即GIF 89a
格式。
如果是JPG
格式,则头两个字节是FF D8
更多的二进制文件头如下:
文件格式 | 头部字节 | 尾部字节 |
---|---|---|
JPG | FF D8 | FF D9 |
PNG | 89 50 4E 47 0D 0A 1A 0A | |
GIF 89a | 47 49 46 38 39 61 | |
GIF 87a | 47 49 46 37 39 61 | |
TGA未压缩 | 00 00 02 00 00 | |
TGA压缩 | 00 00 10 00 00 | |
BMP | 42 4D | |
PCX | 0A | |
TIFF | 4D 4D 或 49 49 | |
ICO | 00 00 01 00 01 00 20 20 | |
CUR | 00 00 02 00 01 00 20 20 | |
IFF | 46 4F 52 4D | |
ANI | 52 49 4646 |
注:二进制查看器,推荐大家一个工具:
Hex Editor
,非常的轻巧,而且用它可以打开大型的文本文件。
HexEditor
下载地址:https://hexeditor.en.softonic.com/
关于HexEditor
可以参见我之前写的这篇文章:《超大文本文件怎么打开(使用Hex Editor)》
我们在工程中放一张默认的JPG
头像:
我们下载头像时,对头部二进制进行检测,判断是否是GIF
,如果是GIF
,则使用过默认头像显示。
var bytes = downloadHandlerTexture.data;
if(0x47 == bytes[0] && 0x49 == bytes[1] && 0x46 == bytes[2])
{
// 是gif,显示默认头像
}
注,
Unity
中如果想要播放GIF
也是可以的,感兴趣的同学可以参见我之前写的这篇文章:《Unity解析和显示/播放GIF图片,支持http url,支持本地file://,支持暂停、继续播放》
6、头像缓存
头像是动态加载的,加载过的头像,我们可以缓存起来,下次再显示时,可以不请求https
了,直接从内存中读取。
/// <summary>
/// 头像缓存
/// </summary>
public class HeadCache
{
static Dictionary<string, Texture2D> cache = new Dictionary<string, Texture2D>();
public static void CacheTexture(string url, Texture2D tex)
{
if (!cache.ContainsKey(url))
cache[url] = tex;
}
public static Texture2D GetFromCache(string url)
{
if (cache.ContainsKey(url))
return cache[url];
return null;
}
}
上面的缓存,我只是缓存在内存中,如果你想写到本地磁盘中,可以使用EncodeToJPG
转为二进制流再通过IO
写到可写目录中。
例:
// using using System.IO;
// Texture2D tex;
string savePath = Application.persistentDataPath + "/head_icons";
var bytes = tex.EncodeToJPG();
using (FileStream fs = new FileStream(savePath, FileMode.Create))
{
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(bytes);
}
}
7、界面制作
界面使用UGUI
来制作。
养成习惯,做好的界面保存成预设:
注:关于预设的相关教程,可以参见我之前写的这两篇文章:《学Unity的猫——第八章:Unity预设文件,无限纸团喷射机》,《Unity2018.3.0.b1 版本的预设新工作流方式的使用体验》
8、比心,拉起浏览器访问博客
点击比心按钮,拉起浏览器访问博客,用的是Application.OpenURL
接口。
例:
var blog_url = "https://blog.csdn.net/linxinfa";
UnityEngine.Application.OpenURL(blog_url);
点击比心按钮,效果如下:
可以看到,拉起的是系统默认的浏览器,事实上,我们也可以做内置浏览器。
注:关于
Unity
的浏览器插件,可以参见我之前写的这些文章:
《Unity内嵌浏览器插件(Android、iOS、Windows)》
《新发的无聊小发明——PC端自制迷你浏览器给Unity调用(Windows窗体应用/WebBrowser/EXE)》
《unity内置浏览器插件UniWebView的使用(支持Android,ios,Mac)》
《新发的日常小实验——使用c# winfrom窗体应用制作浏览器,实现c#与html js交互》
五、结束语
感谢所有粉丝的支持。
末了,喜欢Unity
的同学,不要忘记点击关注,如果有什么Unity
相关的技术难题,也欢迎留言或私信~
六、附录:工程源码
工程总共6个脚本。
1、CSDNFansPanel.cs
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine.UI;
public class CSDNFansPanel : MonoBehaviour
{
public RecyclingListView scrollList;
/// <summary>
/// 列表数据
/// </summary>
private List<FansData> list = new List<FansData>();
private void Start()
{
ReadJson();
// 列表item更新回调
scrollList.ItemCallback = PopulateItem;
// 设置数据,此时列表会执行更新
scrollList.RowCount = list.Count;
}
private void ReadJson()
{
var fansList = Resources.Load<TextAsset>("fans_list").text;
var jsonArray = JSONConvert.DeserializeArray(fansList);
int index = jsonArray.Count;
foreach (JSONObject dataItem in jsonArray)
{
FansData fans = new FansData();
fans.Name = (string)dataItem["name"];
fans.Intro = (string)dataItem["intro"];
fans.BlogUrl = (string)dataItem["blog_url"];
fans.ImageUrl = (string)dataItem["img_url"];
fans.Row = index;
list.Add(fans);
--index;
}
}
/// <summary>
/// item更新回调
/// </summary>
/// <param name="item">复用的item对象</param>
/// <param name="rowIndex">行号</param>
private void PopulateItem(RecyclingListViewItem item, int rowIndex)
{
var child = item as FansItem;
child.Data = list[rowIndex];
}
}
2、FansItem.cs
using UnityEngine.Networking;
using UnityEngine.UI;
public class FansItem : RecyclingListViewItem
{
// 昵称
public Text nameText;
// 简介
public Text introText;
// 比心按钮
public Button btn;
public HeadIconLoader iconLoader;
public Text rowText;
private FansData data;
public FansData Data
{
get { return data; }
set
{
data = value;
nameText.text = data.Name;
rowText.text = $"第{data.Row}位粉丝";
introText.text = data.Intro;
iconLoader.LoadIcon(data.ImageUrl);
}
}
private void Start()
{
// 比心按钮
btn.onClick.AddListener(() =>
{
UnityEngine.Application.OpenURL(data.BlogUrl);
});
}
}
public struct FansData
{
public string Name;
public string Intro;
public string BlogUrl;
public string ImageUrl;
public int Row;
}
3、HeadIconLoader.cs
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using System.Collections.Generic;
public class HeadIconLoader : MonoBehaviour
{
public RawImage headIcon;
public Texture2D defaultTexture;
private UnityWebRequest request;
private DownloadHandlerTexture downloadHandlerTexture;
private bool loading = false;
private string headUrl;
public void LoadIcon(string url)
{
headUrl = url;
var tex = HeadCache.GetFromCache(url);
if(null != tex)
{
headIcon.texture = tex;
loading = false;
return;
}
if (null != request)
{
request.Dispose();
}
headIcon.texture = null;
request = new UnityWebRequest(url);
downloadHandlerTexture = new DownloadHandlerTexture(true);
request.downloadHandler = downloadHandlerTexture;
loading = true;
request.SendWebRequest();
}
private void Update()
{
if (loading && request.isDone)
{
loading = false;
if (string.IsNullOrEmpty(request.error))
{
var bytes = downloadHandlerTexture.data;
if(0x47 == bytes[0] && 0x49 == bytes[1] && 0x46 == bytes[2])
{
// 是gif,显示默认头像
headIcon.texture = defaultTexture;
HeadCache.CacheTexture(headUrl, defaultTexture);
}
else
{
headIcon.texture = downloadHandlerTexture.texture;
HeadCache.CacheTexture(headUrl, downloadHandlerTexture.texture);
}
}
}
}
}
/// <summary>
/// 头像缓存
/// </summary>
public class HeadCache
{
static Dictionary<string, Texture2D> cache = new Dictionary<string, Texture2D>();
public static void CacheTexture(string url, Texture2D tex)
{
if (!cache.ContainsKey(url))
cache[url] = tex;
}
public static Texture2D GetFromCache(string url)
{
if (cache.ContainsKey(url))
return cache[url];
return null;
}
}
4、RecyclingListView.cs
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 循环复用列表
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class RecyclingListView : MonoBehaviour
{
[Tooltip("子节点物体")]
public RecyclingListViewItem ChildObj;
[Tooltip("行间隔")]
public float RowPadding = 15f;
[Tooltip("事先预留的最小列表高度")]
public float PreAllocHeight = 0;
public enum ScrollPosType
{
Top,
Center,
Bottom,
}
public float VerticalNormalizedPosition
{
get => scrollRect.verticalNormalizedPosition;
set => scrollRect.verticalNormalizedPosition = value;
}
/// <summary>
/// 列表行数
/// </summary>
protected int rowCount;
/// <summary>
/// 列表行数,赋值时,会执行列表重新计算
/// </summary>
public int RowCount
{
get => rowCount;
set
{
if (rowCount != value)
{
rowCount = value;
// 先禁用滚动变化
ignoreScrollChange = true;
// 更新高度
UpdateContentHeight();
// 重新启用滚动变化
ignoreScrollChange = false;
// 重新计算item
ReorganiseContent(true);
}
}
}
/// <summary>
/// item更新回调函数委托
/// </summary>
/// <param name="item">子节点对象</param>
/// <param name="rowIndex">行数</param>
public delegate void ItemDelegate(RecyclingListViewItem item, int rowIndex);
/// <summary>
/// item更新回调函数委托
/// </summary>
public ItemDelegate ItemCallback;
protected ScrollRect scrollRect;
/// <summary>
/// 复用的item数组
/// </summary>
protected RecyclingListViewItem[] childItems;
/// <summary>
/// 循环列表中,第一个item的索引,最开始每个item都有一个原始索引,最顶部的item的原始索引就是childBufferStart
/// 由于列表是循环复用的,所以往下滑动时以上是关于520程序员的浪漫,给CSDN近两万的粉丝比心心(python爬虫 | Unity循环复用列表 | 头像加载与缓存)的主要内容,如果未能解决你的问题,请参考以下文章
我是如何俘虏学姐芳心~❤给她放一场浪漫的烟花3D相册❤~(520情人节/七夕情人节/生日快乐/烟花告白/程序员表白专属)