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调试,可以看到粉丝名字的节点classsub-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并更新itemui显示,向下滑动同理,把超过显示区域底部的item复用到顶部。
为了方便大家理解,我画成图,如下:
在这里插入图片描述

注:循环列表的具体实现,我之前写过一篇教程,可以参见我之前写的这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》

循环列表的逻辑封装在RecyclingListViewRecyclingListViewItem类中,源码参见文章末尾。
在这里插入图片描述

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
在这里插入图片描述
更多的二进制文件头如下:

文件格式头部字节尾部字节
JPGFF D8FF D9
PNG89 50 4E 47 0D 0A 1A 0A
GIF 89a47 49 46 38 39 61
GIF 87a47 49 46 37 39 61
TGA未压缩00 00 02 00 00
TGA压缩00 00 10 00 00
BMP42 4D
PCX0A
TIFF4D 4D 或 49 49
ICO00 00 01 00 01 00 20 20
CUR00 00 02 00 01 00 20 20
IFF46 4F 52 4D
ANI52 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循环复用列表 | 头像加载与缓存)的主要内容,如果未能解决你的问题,请参考以下文章

520,解锁开发者的专属浪漫

520,解锁开发者的专属浪漫

我是如何俘虏学姐芳心~❤给她放一场浪漫的烟花3D相册❤~(520情人节/七夕情人节/生日快乐/烟花告白/程序员表白专属)

[Python] 字典操作近两万字大总结(超详细教程)

520表白C语言开发《浪漫流星雨》表白程序,源码来了!

520,一份给程序员的“硬核”脱单秘籍