Dapper 分页,万能公共分页,使用sql高效率分页_橙-极纪元的博客-CSDN博客

mikel阅读(310)

来源: Dapper 分页,万能公共分页,使用sql高效率分页_橙-极纪元的博客-CSDN博客

用户表实体类 UserInfoModel
public class UserInfoModel
{
public int Id{set;get;}
public int ClassId{set;get;}
public string Name{set;get;}
public int sex{set;get;}

}
分页模型

/*分页计算
当前显示数据=每页行数x(当前页数-1)
skip()跳过多少条,take()查询多少条
list.Skip(page.pageSize * (page.pageIndex – 1)).Take(page.pageSize).AsQueryable().ToList();
*/

/// <summary>
/// 分页信息
/// </summary>
public class PageInfo
{

/// <summary>
/// 每页行数(每页数据量):默认每页10条
/// </summary>
public int pageSize { get; set; } = 10;

/// <summary>
/// 当前页:默认第1页
/// </summary>
public int pageIndex { get; set; } = 1;

/// <summary>
/// 总记录数:默认0条
/// </summary>
public int count { get; set; } = 0;

/// <summary>
/// 总页数
/// </summary>
public int pageCount
{
get
{
if (count > 0)
{
return count % this.pageSize == 0 ? count / this.pageSize : count / this.pageSize + 1;
}
else
{
return 0;
}
}
}
}
数据库基类
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace User.Dal
{
public class BaseDapper
{
/// <summary>
/// 数据库连接字符串
/// </summary>
protected static string connStr = System.Configuration.ConfigurationManager.ConnectionStrings[“DbContext”].ConnectionString;

/// <summary>
/// 公共分页
/// </summary>
/// <typeparam name=”T”>返回类型</typeparam>
/// <param name=”model”>SQL条件</param>
/// <param name=”total”>结果集总数</param>
/// <param name=”param”>参数</param>
/// <returns></returns>
public static IEnumerable<T> GetPageList<T>(SQLSelectPageModel model,out int total,object param = null)
{
#region 分页算法
int skip = 1;//从第几条开始
int take = model.pageIndex * model.pageSize;//到第几条结束
if (model.pageIndex > 0)
{
skip = ((model.pageIndex – 1) * model.pageSize)+1;
}
#endregion

StringBuilder sqlStr = new StringBuilder();
sqlStr.Append($”SELECT COUNT(1) FROM {model.tableName} where {model.where};”);
sqlStr.Append($@”SELECT {model.files}FROM
(SELECT ROW_NUMBER() OVER(ORDER BY {model.orderby}) AS RowNum,{model.files} FROM {model.tableName} WHERE {model.where}) AS result
WHERE RowNum >= {skip} AND RowNum <= {take} ORDER BY {model.orderby}”);

using (SqlConnection conn = new SqlConnection(connStr))
{
//获取多个结果集
Dapper.SqlMapper.GridReader res = conn.QueryMultiple(sqlStr.ToString(), param: param, commandType: CommandType.Text);

//注意:如果存储过程首先查出是Type,其次是Product,那么你在执行下面代码的时候顺序必须和存储过程查询顺序一致
//read方法获取Type和Product
total = res.ReadFirst<int>();
IEnumerable<T> list = res.Read<T>();
return list;

//total = reader.ReadFirst<int>();
//return reader.Read<T>();
}
}
}
/// <summary>
/// sql 分页模型
/// </summary>
public class SQLSelectPageModel
{
/// <summary>
/// 查询的“列”
/// </summary>
public string files { set; get; }
/// <summary>
/// 表名 (可以跟join)
/// </summary>
public string tableName { set; get; }
/// <summary>
/// 条件
/// </summary>
public string where { set; get; }
/// <summary>
/// 排序 条件
/// </summary>
public string orderby { set; get; }
/// <summary>
/// 当前页
/// </summary>
public int pageIndex { set; get; }
/// <summary>
/// 当前页显示条数
/// </summary>
public int pageSize { set; get; }
}
}
子类调用
方式一 【推荐】
public class UserInfoDAL
{
/// <summary>
/// 根据分类ID获取信息列表
/// </summary>
/// <param name=”ClassId”>分类ID</param>
/// <param name=”sex”>性别 0 全部,1男,2女</param>
/// <param name=”pageInfo”>分页信息</param>
/// <returns></returns>
public static List<UserInfoModel> GetBaseInfoList(UserInfoModel UModel, PageInfo pageInfo)
{
List<UserInfoModel> result = new List<UserInfoModel>();
try
{
//1.SQL参数
SQLSelectPageModel sQLSelectPage = new SQLSelectPageModel() {
files=”*”,
tableName= “UserInfo”,
where= “ClassId=@ClassId”,
orderby= “Id desc”,
pageIndex= pageInfo.pageIndex,
pageSize= pageInfo.pageSize
};

//2.拼装条件 和参数
StringBuilder whereSB = new StringBuilder();

if (UModel.sex == 1 || UModel.sex == 2)
{
whereSB.Append(” and sex = @sex “);
}

sQLSelectPage.where = sQLSelectPage.where + whereSB.ToString();

//3.调用 基类 公共分页
var res = GetPageList<UserInfoModel>(sQLSelectPage, out int totalCount, UModel).ToList();

pageInfo.count = totalCount;

return res;
}
catch (Exception e)
{
return result;
}

}
}
方式二
public class UserInfoDAL
{
/// <summary>
/// 根据分类ID获取信息列表
/// </summary>
/// <param name=”ClassId”>分类ID</param>
/// <param name=”sex”>性别 0 全部,1男,2女</param>
/// <param name=”pageInfo”>分页信息</param>
/// <returns></returns>
public static List<UserInfoModel> GetBaseInfoList(int ClassId, int sex, PageInfo pageInfo)
{
List<UserInfoModel> result = new List<UserInfoModel>();
try
{
//1.SQL参数
SQLSelectPageModel sQLSelectPage = new SQLSelectPageModel() {
files=”*”,
tableName= “UserInfo”,
where= “ClassId=@ClassId”,
orderby= “Id desc”,
pageIndex= pageInfo.pageIndex,
pageSize= pageInfo.pageSize
};

//2.拼装条件 和参数
var param = new object();
if (sex == 1 || sex == 2)
{
sQLSelectPage.where = sQLSelectPage.where + ” and sex = @sex”;
param = new { ClassId = ClassId, sex = sex };
}
else
{
param = new {ClassId = ClassId};
}

//3.调用 基类 公共分页
var res = GetPageList<UserInfoModel>(sQLSelectPage, out int totalCount, param).ToList();

pageInfo.count = totalCount;

return res;
}
catch (Exception e)
{

}

return result;
}

}

————————————————
版权声明:本文为CSDN博主「橙-极纪元」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cplvfx/article/details/120204281

基于Dapper的分页实现,支持筛选,排序,结果集总数,多表查询,非存储过程 - JIN Weijie - 博客园

mikel阅读(374)

来源: 基于Dapper的分页实现,支持筛选,排序,结果集总数,多表查询,非存储过程 – JIN Weijie – 博客园

简介

之前事先搜索了下博客园上关于Dapper分页的实现,有是有,但要么是基于存储过程,要么支持分页,而不支持排序,或者搜索条件不是那么容易维护。

代码

首先先上代码: https://github.com/jinweijie/Dapper.PagingSample

方法定义

以下是我的一个分页的实现,虽然不是泛型(因为考虑到where条件以及SQL语句的搭配),但是应该可以算是比较通用的了,方法定义如下:

public Tuple<IEnumerable<Log>, int> Find(LogSearchCriteria criteria
            , int pageIndex
            , int pageSize
            , string[] asc
            , string[] desc);

以上函数定义是一个查询Log的示例,返回结果中,Tuple的第一个值是结果集,第二个值是总行数(例如,总共有100条记录,每页10条,当前第一页,那么第一个值是10条记录,第二个值是100)

在示例项目中,我用两种方法实现了分页:

1. 第一种是基于2此查询,第一次得到总数,第二次查询得到结果集。

2. 第二种是基于1此查询,用了SQLServer 的Offest/Fetch,所以只支持SQL Server 2012+,所以大家根据自己用的Sql Server版本选择不同的实现,这里当然是第二种实现效率更高一点。

运行示例

1. 将Github的Repo下载或者Clone到本地以后,到Database目录下,解压缩Database.7z

2. Attach到Sql Server上。默认我使用Sql Server LocalDB,连接字符串是 Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DapperPagingSample;integrated security=True;   如果你用的不是LocalDB,请酌情修改App.Config的连接字符串。

3. Ctrl+F5运行程序,示例项目里,我用了一个简单的WinForm程序,但应该可以比较好的演示分页效果。

多表支持

增加了示例,支持多表查询,例如有两个Log表,Level表,Log的LevelId字段引用Level的Id字段,通过以下的查询,可以实现多表查询的分页,排序,过滤:

首先是通过两次查询的示例(基本支持所有版本Sql Server):

复制代码
 1 public Tuple<IEnumerable<Log>, int> Find(LogSearchCriteria criteria
 2             , int pageIndex
 3             , int pageSize
 4             , string[] asc
 5             , string[] desc)
 6         {
 7             using (IDbConnection connection = base.OpenConnection())
 8             {
 9                 const string countQuery = @"SELECT COUNT(1)
10                                             FROM      [Log] l
11                                             INNER JOIN [Level] lv ON l.LevelId = lv.Id
12                                             /**where**/";
13 
14                 const string selectQuery = @"  SELECT  *
15                             FROM    ( SELECT    ROW_NUMBER() OVER ( /**orderby**/ ) AS RowNum, l.*, lv.Name as [Level]
16                                       FROM      [Log] l
17                                       INNER JOIN [Level] lv ON l.LevelId = lv.Id
18                                       /**where**/
19                                     ) AS RowConstrainedResult
20                             WHERE   RowNum >= (@PageIndex * @PageSize + 1 )
21                                 AND RowNum <= (@PageIndex + 1) * @PageSize
22                             ORDER BY RowNum";
23 
24                 SqlBuilder builder = new SqlBuilder();
25 
26                 var count = builder.AddTemplate(countQuery);
27                 var selector = builder.AddTemplate(selectQuery, new { PageIndex = pageIndex, PageSize = pageSize });
28 
29                 if (!string.IsNullOrEmpty(criteria.Level))
30                     builder.Where("lv.Name= @Level", new { Level = criteria.Level });
31 
32                 if (!string.IsNullOrEmpty(criteria.Message))
33                 {
34                     var msg = "%" + criteria.Message + "%";
35                     builder.Where("l.Message Like @Message", new { Message = msg });
36                 }
37 
38                 foreach (var a in asc)
39                 {
40                     if(!string.IsNullOrWhiteSpace(a))
41                         builder.OrderBy(a);
42                 }
43 
44                 foreach (var d in desc)
45                 {
46                     if (!string.IsNullOrWhiteSpace(d))
47                         builder.OrderBy(d + " desc");
48                 }
49 
50                 var totalCount = connection.Query<int>(count.RawSql, count.Parameters).Single();
51                 var rows = connection.Query<Log>(selector.RawSql, selector.Parameters);
52 
53                 return new Tuple<IEnumerable<Log>, int>(rows, totalCount);
54             }
55         }
复制代码

第二个示例是通过Offset/Fetch查询(支持Sql Server 2012+)

复制代码
 1 public Tuple<IEnumerable<Log>, int> FindWithOffsetFetch(LogSearchCriteria criteria
 2                                                 , int pageIndex
 3                                                 , int pageSize
 4                                                 , string[] asc
 5                                                 , string[] desc)
 6         {
 7             using (IDbConnection connection = base.OpenConnection())
 8             {
 9                
10                 const string selectQuery = @" ;WITH _data AS (
11                                             SELECT l.*, lv.Name AS [Level]
12                                             FROM      [Log] l
13                                             INNER JOIN [Level] lv ON l.LevelId = lv.Id
14                                             /**where**/
15                                         ),
16                                             _count AS (
17                                                 SELECT COUNT(1) AS TotalCount FROM _data
18                                         )
19                                         SELECT * FROM _data CROSS APPLY _count /**orderby**/ OFFSET @PageIndex * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY";
20 
21                 SqlBuilder builder = new SqlBuilder();
22                 
23                 var selector = builder.AddTemplate(selectQuery, new { PageIndex = pageIndex, PageSize = pageSize });
24 
25                 if (!string.IsNullOrEmpty(criteria.Level))
26                     builder.Where("lv.Name = @Level", new { Level = criteria.Level });
27 
28                 if (!string.IsNullOrEmpty(criteria.Message))
29                 {
30                     var msg = "%" + criteria.Message + "%";
31                     builder.Where("l.Message Like @Message", new { Message = msg });
32                 }
33                 
34                 foreach (var a in asc)
35                 {
36                     if (!string.IsNullOrWhiteSpace(a))
37                         builder.OrderBy(a);
38                 }
39 
40                 foreach (var d in desc)
41                 {
42                     if (!string.IsNullOrWhiteSpace(d))
43                         builder.OrderBy(d + " desc");
44                 }
45                 
46                 var rows = connection.Query<Log>(selector.RawSql, selector.Parameters).ToList();
47 
48                 if(rows.Count == 0)
49                     return new Tuple<IEnumerable<Log>, int>(rows, 0);
50                 
51 
52                 return new Tuple<IEnumerable<Log>, int>(rows, rows[0].TotalCount);
53                 
54             }
55         }
复制代码

 

谢谢

希望对大家有帮助:)

最后,我更新了本篇随便,增加了内容,希望不要再被撤下了(上次撤下说是因为篇幅太短。。。),因为个人觉得这个对大家应该还是会有用的。

最大的 AI 工具目录库 - 知乎

mikel阅读(365)

来源: 最大的 AI 工具目录库 – 知乎

1.Futurepedia

几乎包括现有所有 AI 工具 ,都在这个目录中!每天还在不断更新……目前总共收录了482个工具(截止时间:20221222),总共分为:文本工具(172个),图片工具(135个),营销类工具(94个),助手工具(69个),代码工具(60个),设计工具(54个),写作工具(49个),音频工具(40个),视频工具(40个),搜索工具(34个),头像工具(30个),演讲工具(22个)…..;可以根据相关的标签类别进行搜索。

主页

2.Best AI Tools

1400 多种 Ai 工具按 semrush 排名排序。

3.Creaitives

定期抓取所有其他 AI 数据库并汇总数据,目前,已收录了800+AI工具,配合浏览器翻译插件使用会更好。

主页

4.Futuretools

FutureTool是一个AI工具导航网站,已收集了 173个各类 AI 工具,可根据聊天、金融、娱乐、视频、代码等多个筛选找到自己心仪的 AI 工具。

主页

5.Airadar.getinference

找到你需要的一切,主要用于创意和营销的AI工具 500+产品

主页

6.ai-lib.club

主页

7.wikiaitools

汇集1000+的Ai工具

php商城伪静态,友价商城nginx伪静态源码规则_weixin_39882394的博客-CSDN博客

mikel阅读(222)

来源: (2条消息) php商城伪静态,友价商城nginx伪静态源码规则_weixin_39882394的博客-CSDN博客

不少朋友搭建完友价 T5 源码商城后发现很多页面无法正常打开,其实是伪静态没做好。

在这里我推荐使用宝塔搭建,将下面伪静态规则复制到:网站-管理-伪静态-保存即可

一般网站有自带的apache规则,但是经过转换后无法使用,用下面的规则后便可正常了!如需源码可以联系客服!手上有更好用的源代码!

nginx 伪静态规则:rewrite ^/(.*)/search(.*).html /$1/index.php?str=$2 last;

rewrite ^/(.*)/view([0-9]*).html /$1/view.php?id=$2 last;

rewrite ^/(.*)/(.*)list(.*).html /$1/$2list.php?str=$3 last;

rewrite ^/(.*)/(.*)view(.*).html /$1/$2view.php?id=$3 last;

location ~* ^/((.*)upload|ad|gg|img|ckeditor\/attached|(.*)upload1|(.*)upload2|(.*)upload3)/.*\.(php|php5|asp)$

{

deny all;

}

rewrite ^(?i)/config/ueditor/php/upload/(.*).PHP /css/ last;

rewrite ^(?i)/config/ueditor/php/upload/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor/php/upload1/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor/php/upload1/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor/php/upload2/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor/php/upload2/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor/php/upload3/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor/php/upload3/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload1/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload1/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload2/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload2/(.*).asp /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload3/(.*).php /css/ last;

rewrite ^(?i)/config/ueditor_mini/php/upload3/(.*).asp /css/ last;

rewrite ^(?i)/ad/(.*).php /css/ last;

rewrite ^(?i)/ad/(.*).asp /css/ last;

rewrite ^(?i)/gg/(.*).php /css/ last;

rewrite ^(?i)/gg/(.*).asp /css/ last;

rewrite ^(?i)/img/(.*).php /css/ last;

rewrite ^(?i)/img/(.*).asp /css/ last;

rewrite ^(?i)/upload/(.*).php /css/ last;

rewrite ^(?i)/upload/(.*).asp /css/ last;

rewrite ^(?i)/ckeditor/attached/(.*).php /css/ last;

rewrite ^(?i)/ckeditor/attached/(.*).asp /css/ last;

LoadRunner快速入门!看一遍就会了。(内含实例) - 知乎

mikel阅读(492)

来源: LoadRunner快速入门!看一遍就会了。(内含实例) – 知乎

LoadRunner是软件测试利器。

本教程可以帮你完成一次软件测试实验。

题目:

使用LoadRunner自带的测试项目–航班订票管理系统WebTours,网站地址为:127.0.0.1:1080/WebTours(用户名为jojo,密码为bean),完成性能测试,要求:

1、 虚拟用户为10;

2、 每隔15s启动2个Vusers;

3、 运行时间为5min;

4、 每隔15s停止2个Vusers;

5、在分析工具中自定义一个图形。


下载

链接你不需要找了,我已经给你找好了。

《LoadRunner 12 链接》,可复制链接后用石墨文档 App 或小程序打开


安装

下载解压后应该是这个样子的

选好安装位置点击Install

正在安装

安装好之后还提示安装运行依赖库 直接点击确定

运行依赖库
提示

同样,直接点确定

这里需要选择第一个 从网上下载

运行依赖库下载中
运行依赖库安装中

运行依赖库安装好后就会出现安装向导

选择好路径 点击安装

我的安装目录

安装目录

等待安装

正在安装

取消勾选指定LoadRunner代理将要使用的证书 后点下一步

身份验证
安装完成

在桌面已经生成了快捷方式

快捷方式

安装完成!!!


必知必会

LoadRunner的组件

  • Virtual User Generator—虚拟用户生成器
  • LoadRunner Controller—创建、运行和监控场景
  • LoadRunner Analysis—分析测试结果

官方使用指南

我们可以结合官方的使用指南进行学习

使用指南

LoadRunner相关的术语:

LoadRunner Terminology
  • Scenario(场景) 定义测试阶段出现的事件,基于性能需求
  • Virtual Users or Vusers Vusers 模拟用户在你的系统上操作的动作。一个场景可以包含几十,上百,甚至上千的虚拟用户。
  • Vuser Script(脚本) 记录你的应用程序中执行业务流程的动作
  • Protocal 协议是客户端和服务器之间交流的方式
  • Transaction(事务) 你所定义的事务,用于测试你的系统性能。一个事务代表一个或多个终端用户的业务流程。一个事务允许你测试这些业务流程花费了多长时间。

LoadRunner负载测试的流程是什么?

  1. Plan the load test 计划负载测试
  2. Create Vuser scripts 创建脚本
  3. Define the scenario 定义场景
  4. Analyze the results 分析结果

第一个例子

启动HP WebTours服务器

  1. 快捷键Win+S 搜索 Start HP
搜索 Satrt HP
打开文件位置

你也可以根据路径找到该文件

C:\ProgramData\Microsoft\Windows\Start Menu\Programs\HP Software\HP LoadRunner\Samples\Web 

2.双击启动服务器

不出意外你会看到这样的命令行窗口

造成该问题的原因是服务器未配置ip地址,解决办法就是给它配置咯

所以接下来我要做一件事情

在LoadRunner的安装位置找到WebTours文件夹 并双击进入

找到WebTours
进入文件夹
打开配置文件
搜搜ServerName

再次重复 双击Start HP Web Tours Server

这样就表示已经成功启动啦

注意:需要保持这个命令行窗口状态,如果关闭就代表关闭服务器。

然后我们再双击 HP Web Tours Application

Web Tours

能够成功访问网页了。

登录测试

Username:jojo

Password:bean


录制脚本

1.启动Virtual User Generator

Virtual User Generator

2.新建脚本和解决方案

新建

3.选择Web-HTTP/HTML

选择协议并创建

4.创建好之后是这个样子的

创建完成

此时,解决方案是一个空的脚本。

*如果你的左侧没有Solution Explorer(解决方案资源管理器)。

不要慌,很简单,一下就能解决。

解决方案资源管理器

开始录制

1.点击 Record>Record

2.填入URL地址

http://localhost:1080/WebTours

其他选项保持默认,但需要与我保持一致。

填写URL
高级设置

开始录制

此时已经开始录制了。

你需要进行以下动作:

用户名登录

进入航班详情

选择航班

进入订单详情

查看日程

停止录制

查看脚本

步骤导航

Step Navigator

重播脚本

Runtime Settings

设置迭代次数

设置步调

选择无代理

运行

运行截图

Nice!

脚本重播完成

怎么办?不用怕。

解决常见的重播问题

打开Web Tours主页

回到主页

回到主页

使用Controller打开解决方案

启动Controller

新建场景

开启场景

出现错误

警告信息

不用害怕

接下来我们要做一些事情

查询本机IP

再次开启场景

开始运行

运行截图

分析总结


Nice!Nice!Nice!

你已经入门啦!

AI建模工具速查表 (2) | TensorFlow 使用指南

mikel阅读(298)

来源: AI建模工具速查表 (2) | TensorFlow 使用指南

TensorFlow速查手册

TensorFlow是一个由谷歌开发和维护的开源机器学习平台,用于科学计算、神经网络、图像分类、聚类、回归、强化学习、自然语言处理等。

TensorFlow2建模速查

TensorFlow2应用速查

下载

🔔 GitHub提供了对应代码支持一键查看与运行!能科学上网的小伙伴可以点击 Notebook 头部按钮一键直达 Google Colab 运行,也可以使用下方在线编程环境运行代码与学习。

公众号 GitHub 博客
ShowMeAI研究中心 ShowMeAI-Hub blog.showmeai.tech
下载高清PDF 一键运行代码 在线编程环境

ShowMeAI 速查表一览

速查表是『ShowMeAI硬核资料库』的重要组成部分!Awesome AI Cheatsheets 系列包含『编程语言』『AI技能知识』『数据科学工具库』『AI垂直领域工具库』四个板块。ShowMeAI会陆续把最新、最优质的工具/知识整理成速查表!系列持续更新中,想第1时间获取更新通知,请关注我们吧!

▣ 数据科学工具库速查表

Numpy SciPy Pandas Matplotlib Seaborn Bokeh Spark RDD SparkSQL More…
More…
速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 More…

▣ AI垂直领域工具库速查表

SKLearn Keras TensorFlow2 PyTorch OpenCV 4.x More…
More…
速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 More…

▣ AI知识技能速查表

Jupyter 正则表达式 CS229 CS230 线代与微积分 概率与统计 More…
More…
速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 速查表详情 More…

▣ 编程语言速查表

SQL Python 3 More…
More…
速查表详情 速查表详情 More…

ShowMeAI 系列教程推荐

从0搭建基于神经网络的手语识别系统

mikel阅读(562)

来源: 从0搭建基于神经网络的手语识别系统

据北京听力协会预估数据,我国听障人群数量已过千万。而在全球范围内有4.66亿人患有残疾性听力损失,约占全世界人口的5%。聋哑人士很特殊,他们需要使用手语进行交流,其他与常人无异,我国存在特殊教育水平在各城市中发展力度具有较大差异,国家通用手语推广程度浅,但不懂手语,与听力障碍者交流会非常困难。

在本篇内容中,ShowMeAI 借助深度学习与神经网络技术,针对这个问题从 0 构建 1 个应用程序,检测手语并将其翻译给其他人进而打破手语隔阂。

搭建和部署完成后,你可以通过摄像头,轻松测试模型,如下图所示,快来一起试试吧。这个动图中的手势代表的单词,见文末哦!

💡 手语介绍

我们先来简单了解一下手语,它由 3 个主要部分组成:

  • 手指拼写:这是一种手动的交流方式,用双手和手指拼写单词。每个字母都用指定的手位置表示。
  • 单词级符号词汇:这是一个大型视频数据集,用于识别单词或字母的整个手势。
  • 非手部特征:包括任何面部表情、嘴巴、舌头或身体姿势。

在本文中,我们先解决第①个部分的问题。我们准备使用的解决方案是基于视觉数据的神经网络

💡 深度学习与计算机视觉

人工智能和计算机视觉的最典型的模型是卷积神经网络(CNN),它在典型的计算机视觉应用中(如图像识别、目标检测等)应用广泛。我们在本次应用的核心技术也将采用 CNN。

CNN 网络有着如上图所示的网络结构,典型的结构包括卷积层、池化层、激活层、全连接层等,对于输入图像,可以有效抽取图像内容表征,并进行分类或其他处理。卷积层等特殊结构,可以在控制参数量的前提下,保证良好的图像特征提取能力。

关于卷积神经网络的详细知识可以参考ShowMeAI下述教程:
深度学习教程 | 吴恩达专项课程 · 全套笔记解读 中的文章 卷积神经网络解读
深度学习与计算机视觉教程 中的文章 卷积神经网络详解

💡 小试牛刀,打通流程

我们来构建一个 CNN 识别的流程,会分成以下基础步骤:

  • 数据读取与切分
  • 数据可视化及预处理
  • CNN网络构建与训练

① 导入相关库

我们在这里主要使用 TensorFlow 构建网络与训练,会使用 Numpy 做数据计算与处理,以及使用 Matplotlib 进行简单可视化。

对于这些工具库,ShowMeAI都制作了快捷即查即用的速查表手册,大家可以在下述位置获得:
Tensorflow 速查手册
Numpy 速查手册
Matplotlib 速查手册

我们先把这些工具库导入。

  1. # 导入工具库
  2. import string
  3. import pandas as pd
  4. import numpy as np
  5. import tensorflow as tf
  6. import matplotlib.pyplot as plt
  7. from tensorflow import keras
  8. from functools import partial
  9. from tensorflow.keras.preprocessing.image import ImageDataGenerator, array_to_img

② 读取数据集

本数据集为手语字母对应的数据集,图片 size 不大,所以也叫做 sign_mnist 数据集(类比手写数字数据集 mnist),部分示例图片如下

数据集大家可以在 Kaggle平台对应数据集页面 下载,也可以通过ShowMeAI的百度网盘地址下载。

🏆 实战数据集下载(百度网盘):公众号『ShowMeAI研究中心』回复『实战』,或者点击 这里 获取本文 [5] 从0搭建基于神经网络的手语识别系统 『sign_mnist 数据集

⭐ ShowMeAI官方GitHubhttps://github.com/ShowMeAI-Hub

下面我们加载训练集与测试集并切分特征与标签:

  1. # 读取数据
  2. test = pd.read_csv("sign_mnist_test.csv")
  3. train = pd.read_csv("sign_mnist_train.csv")
  4. # 输出基本信息
  5. print("训练集维度", train.shape)
  6. print("测试集维度", train.shape)
  1. # 输出标签信息
  2. labels = train["label"].value_counts().sort_index(ascending=True)
  3. labels
  1. # 切分特征与标签
  2. train_x = train.drop(labels = "label", axis = 1)
  3. train_y = train["label"]
  4. test_x = test.drop(labels = "label", axis = 1)
  5. test_y = test["label"]
  6. train_x.head()
  1. # 数据预处理与可视化
  2. # 存储标签数据
  3. test_classes= test_y
  4. train_clasees = train_y
  5. # 特征转为numpy格式
  6. train_x = train_x.to_numpy()
  7. test_x = test_x.to_numpy()
  8. # 把数据转为3维图像数据(图片数量*宽*高,这里如果是灰度图,颜色通道为1,省略)
  9. train_x = train_x.reshape(-1,28,28)
  10. test_x = test_x.reshape(-1,28,28)
  1. # 在训练集中取样30张图片,做可视化查看
  2. def plot_categories(training_images, training_labels):
  3. fig, axes = plt.subplots(3, 10, figsize=(16, 15))
  4. axes = axes.flatten()
  5. letters = list(string.ascii_lowercase)
  6. for k in range(30):
  7. img = training_images[k]
  8. img = np.expand_dims(img, axis=-1)
  9. img = array_to_img(img)
  10. ax = axes[k]
  11. ax.imshow(img, cmap="Greys_r")
  12. ax.set_title(f"{letters[int(training_labels[k])]}")
  13. ax.set_axis_off()
  14. plt.tight_layout()
  15. plt.show()
  16. plot_categories(train_x, train_y)

③ 卷积神经网络CNN搭建

我们使用 TensorFlow 的 high level API(即keras)搭建一个简易CNN神经网络,并拟合一下数据

  1. def create_model():
  2. model = tf.keras.models.Sequential([
  3. # 卷积层
  4. tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(28, 28, 1)),
  5. # 池化层
  6. tf.keras.layers.MaxPooling2D(2,2),
  7. # 卷积层
  8. tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
  9. # 池化层
  10. tf.keras.layers.MaxPooling2D(2,2),
  11. # 展平
  12. tf.keras.layers.Flatten(),
  13. # 全连接层
  14. tf.keras.layers.Dense(512, activation='relu'),
  15. # softmax分类
  16. tf.keras.layers.Dense(26, activation='softmax')])
  17. model.compile(
  18. optimizer='adam', #优化器
  19. loss='sparse_categorical_crossentropy', #损失函数
  20. metrics=['accuracy']) #评估准则
  21. return model
  1. # 初始化模型
  2. model = create_model()
  3. # 拟合数据
  4. history = model.fit(train_x, train_y, epochs=20, validation_data=(test_x, test_y))

我们这里在全量数据集上迭代20个轮次,结果如下:

我们可以看到,这里的数据并不特别复杂,在自己从头搭建的 CNN 模型上,经过训练可以达到训练集 100% 验证集 92% 的准确率。

我们再对训练过程中的「准确率」及「损失函数」变化值进行绘制,以了解模型状态。

  1. # 获取准确率与损失函数情况
  2. acc = history.history['accuracy']
  3. val_acc = history.history['val_accuracy']
  4. loss = history.history['loss']
  5. val_loss = history.history['val_loss']
  6. # matplotlib绘制训练过程中指标的变化状况
  7. epochs = range(len(acc))
  8. plt.plot(epochs, acc, 'r', label='Training accuracy')
  9. plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
  10. plt.title('Training and validation accuracy')
  11. plt.legend()
  12. plt.figure()
  13. plt.plot(epochs, loss, 'r', label='Training Loss')
  14. plt.plot(epochs, val_loss, 'b', label='Validation Loss')
  15. plt.title('Training and validation loss')
  16. plt.legend()
  17. plt.show()

💡 问题与优化

① 深度网络与梯度消失

一般来说,随着 CNN 网络层数变深,模型的学习能力会变强,也能学到更多的信息。但训练深度CNN存在梯度消失的问题。

梯度消失和梯度爆炸部分内容也可以参考ShowMeAI的对吴恩达老师课程的总结文章 深度学习教程 | 深度学习的实用层面

非常深的神经网络的梯度会很快变为零(反向传播的梯度连乘带来的问题),这最终会使整个梯度下降变慢。有一些特殊结构的神经网络,可以大程度缓解这个问题,比如最著名的 ResNet,当然,大家可以借助 ResNet 预训练模型快速迁移学习应用在我们当前的手语识别问题上,为了让大家对ResNet 细节更清晰,我们在这里手动搭建 ResNet-50(即50层的ResNet网络)来训练和做效果对比。

ResNet的详细讲解也可以参考ShowMeAI的 深度学习教程 | 吴恩达专项课程 · 全套笔记解读中的文章 深度学习教程 | 经典CNN网络实例详解

② ResNet 模型简介

ResNet 是 Residual Networks 的简称,是迄今为止我们看到的最流行和最成功的深度学习模型之一。ResNets 由残差块组成,残差块的核心组件是『跳跃连接/skip-connection』。跳跃连接,也称为快捷连接,让神经网络跳过某些层并将一层的输出馈送到神经网络中另一层的输入。它能帮助模型避免乘以中间跳过的那些层的权重,从而有助于解决梯度消失的问题。

然而,使用 ResNet 和跳跃连接,由于中间有卷积层和池化层,一层输出的维度可能与另一层的输出维度不同。为了解决这个问题,可以使用两种方法:

  • 快捷连接填充多个零实体以增加其维度
  • 添加 1X1 卷积层来匹配维度。

但是,对于第二种方法,我们需要在输出中添加一个额外的参数,而第一种方法不需要。

③ ResNet为何有效

ResNet的效果核心有2点:

  • ① 它使用我们上面提到的跳跃连接,它跳过层来解决梯度消失的问题。
  • ② 它通过让模型学习恒等函数来确保最高层的性能至少与最低层一样好。

④ 构建ResNet-50

下面我们参考 keras 官方 ResNet 构建方式,构建一个 ResNet-50,如下所示,我们先构建基本模块,再组装成最终的网络。

  1. # Defining the identity block of the Resnet-50 Model.
  2. def identity_block(X, f, filters, training=True):
  3. # filter of the three convs
  4. f1,f2,f3 = filters
  5. X_shortcut = X
  6. # First Component
  7. X = tf.keras.layers.Conv2D(filters = f1, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
  8. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  9. X = tf.keras.layers.Activation('relu')(X)
  10. # Second Component
  11. X = tf.keras.layers.Conv2D(filters = f2, kernel_size = f, strides = (1,1), padding = 'same')(X)
  12. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  13. X = tf.keras.layers.Activation('relu')(X)
  14. # Third Component
  15. X = tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
  16. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  17. # Adding the two tensors
  18. X = tf.keras.layers.Add()([X_shortcut,X])
  19. X = tf.keras.layers.Activation('relu')(X)
  20. # Returning the last output
  21. return X
  1. # Defining the Convolution Block of the Resnet-50 Model.
  2. def convolutional_block(X, f, filters, s=2,training=True):
  3. # filter of the three convs
  4. f1,f2,f3 = filters
  5. X_shortcut = X
  6. # First Component
  7. X = tf.keras.layers.Conv2D(filters = f1, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
  8. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  9. X = tf.keras.layers.Activation('relu')(X)
  10. # Second Component
  11. X = tf.keras.layers.Conv2D(filters = f2, kernel_size = f, strides = (s,s), padding = 'same')(X)
  12. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  13. X = tf.keras.layers.Activation('relu')(X)
  14. # Third Component
  15. X = tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (1,1), padding = 'valid')(X)
  16. X = tf.keras.layers.BatchNormalization(axis = 3)(X, training = training) # Default axis
  17. # Converting the Input Volume to the match the last output for addition.
  18. X_shortcut =tf.keras.layers.Conv2D(filters = f3, kernel_size = 1, strides = (s,s), padding = 'valid')(X_shortcut)
  19. X_shortcut = tf.keras.layers.BatchNormalization(axis = 3)(X_shortcut, training = training)
  20. X = tf.keras.layers.Add()([X_shortcut,X])
  21. X = tf.keras.layers.Activation('relu')(X)
  22. # Adding the last two tensors
  23. X = tf.keras.layers.Add()([X, X_shortcut])
  24. X = tf.keras.layers.Activation('relu')(X)
  25. # Returning the output tensor
  26. return X
  1. # Defining a modified Resnet-50 Model using the Identity and Convolution Blocks.
  2. def ResNet50(input_shape = (28, 28, 1), classes = 26):
  3. # Defining the input as a tensor with shape input_shape
  4. X_input = tf.keras.Input(input_shape)
  5. # Zero-Padding
  6. X = tf.keras.layers.ZeroPadding2D((3, 3))(X_input)
  7. # Stage 1
  8. X = tf.keras.layers.Conv2D(64, (5, 5), strides = (1, 1))(X)
  9. X = tf.keras.layers.BatchNormalization(axis = 3)(X)
  10. X = tf.keras.layers.Activation('relu')(X)
  11. X = tf.keras.layers.MaxPooling2D((3, 3), strides=(2, 2))(X)
  12. # Stage 2
  13. X = convolutional_block(X, f = 3, filters = [64, 64, 256], s = 1)
  14. X = identity_block(X, 3, [64, 64, 256])
  15. X = identity_block(X, 3, [64, 64, 256])
  16. # Add an Average Pool Layer
  17. X = tf.keras.layers.AveragePooling2D((2,2))(X)
  18. # Output Layer
  19. X = tf.keras.layers.Flatten()(X)
  20. X = tf.keras.layers.Dense(classes, activation='softmax')(X)
  21. # Create Model
  22. model = tf.keras.Model(inputs = X_input, outputs = X)
  23. return model

⑤ 训练ResNet-50

下面我们在数据集上,使用 ResNet-50 网络进行训练

  1. # 初始化模型
  2. model = ResNet50()
  3. # 编译
  4. model.compile(optimizer="adam",metrics=["accuracy"],loss = "sparse_categorical_crossentropy")
  5. # 训练
  6. history = model.fit(train_x, train_y, validation_data = (test_x, test_y), epochs =10)

得到如下结果

💡 优化效果对比

我们对ResNet-50也绘制训练过程中准确率和损失函数的变化,如下

  1. # 获取准确率与损失函数情况
  2. acc = history.history['accuracy']
  3. val_acc = history.history['val_accuracy']
  4. loss = history.history['loss']
  5. val_loss = history.history['val_loss']
  6. # matplotlib绘制训练过程中指标的变化状况
  7. epochs = range(len(acc))
  8. plt.plot(epochs, acc, 'r', label='Training accuracy')
  9. plt.plot(epochs, val_acc, 'b', label='Validation accuracy')
  10. plt.title('Training and validation accuracy')
  11. plt.legend()
  12. plt.figure()
  13. plt.plot(epochs, loss, 'r', label='Training Loss')
  14. plt.plot(epochs, val_loss, 'b', label='Validation Loss')
  15. plt.title('Training and validation loss')
  16. plt.legend()
  17. plt.show()

对比图如下:

我们观察到,从简单的 CNN 模型换到 ResNet 模型时,测试集的准确率从92% 到 97% 。也说明了,ResNet 的结构确实能够带来效果上的提升。

💡 部署与实时测试

在这里我们做一个简单的测试,使用 OpenCV 的视频录制功能,通过 python 收集我们的摄像头的镜头采集的图像并进行实时预测。

ShowMeAI给OpenCV工具库制作了快捷即查即用的 OpenCV 速查表手册,大家可以点击查看和下载。

具体的过程是,我们解析捕获的每一帧图像,将其处理为灰度图(类似于我们模型的训练集),在图像中心抓取一个 400×400 像素的正方形区域(参见 x0,x1,y0,y1),将正方形调整为我们最初的 28×28 大小并使用我们的模型进行测试(之前保存到 .h5 文件)。

  1. # 导入工具库
  2. import keras
  3. import numpy as np
  4. from PIL import Image
  5. import string
  6. import pandas as pd
  7. import tensorflow as tf
  1. # 导入OpenCV
  2. import cv2
  3. from matplotlib import pyplot
  4. ## 设定维度
  5. dim = (28, 28) # 图像维度
  6. letters = list(string.ascii_lowercase) # 识别的字母
  7. x0 = 1920 // 2 - 400 # 400px left of center
  8. x1 = 1920 // 2 + 400 # 400px right of center
  9. y0 = 1080 // 2 - 400 # 400px right of center
  10. y1 = 1080 // 2 + 400 # 400px right of center
  11. # 初始化视频捕获
  12. video=cv2.VideoCapture(0)
  13. cv2.namedWindow('Webcam') # 构建1个窗口
  14. cv2.moveWindow('Webcam',40,30) # 放置窗口
  15. while video.isOpened(): # 只要没有关掉实时摄像头
  16. ret,capture = video.read() # 抓取每个视频帧
  17. cropped = capture[y0:y1, x0:x1] # 截取
  18. img = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY) # 转成灰度图
  19. img = cv2.GaussianBlur(img, (5, 5), 0) # 图像平滑
  20. img = cv2.resize(img, dim) # 图像大小缩放
  21. pyplot.imshow(img, cmap='gray') # 可视化展示图片
  22. pyplot.show() # 展示
  23. img = np.reshape(img, (1,img.shape[0],img.shape[1],1))
  24. img = tf.cast(img, tf.float32)
  25. pred=model.predict(img)
  26. # 可视化实时效果
  27. cv2.rectangle(capture, (x0,y0),(x1,y1),(255,0,0),2) # 为图片添加矩形框
  28. cv2.putText(capture,'{} res50'.format(letters[np.argmax(pred[0])]),(x0+25,y0+50),cv2.FONT_HERSHEY_SIMPLEX,0.9,(0,255,0),1) # 预测字母
  29. cv2.imshow('Webcam', capture) # 展示视频
  30. # 结果输出
  31. print(pred)
  32. print(letters[np.argmax(pred[0])])
  33. # 退出视频输入
  34. key = cv2.waitKey(1)
  35. if key == ord('q'):
  36. break
  37. video.release()
  38. cv2.destroyAllWindows()

为了更轻松地对预估结果查看,我们把将预测的字母显示在实时画面上(请参阅下面的 gif 以测试单词 hello)。

💡 参考资料

ChatGPT指令大全(建议收藏) - 知乎

mikel阅读(747)

来源: ChatGPT指令大全(建议收藏) – 知乎

在使用 ChatGPT 时,当你给的指令越,它的回答会越到位,举例来说,假如你要请它帮忙写文案,如果没给予指定情境与对象,它会不知道该如何回答的更加准确。

一、写报告

1、我现在正在 [报告的情境与目的]。我的简报主题是 [主题],请提供 [数字] 种开头方式,要简单到 [目标族群] 能听懂,同时要足够能吸引人,让他们愿意专心听下去

我现在正在修台大的简报课,其中一项作业是要做一份让小学生能听懂的简报。我的简报主题是机会成本,请提供三种开头方式,要简单到小学生能听懂,同时要足够能吸引人,让他们愿意专心听下去

2、写出一篇有关 [知识] 的 [数字] 字研究报告,报告中需引述刷新的研究,并引用专家观点

写出一篇有关自动驾驶的 300 字研究报告,报告中需引述刷新的研究,并引用专家观点

3、你是 [某个主题] 的专家,请针对以下论述 [附上论述],提出 [数字] 个反驳的论点,每个论点都要有佐证

你是大数据分析的专家,请针对以下论述「在数据分析中,越多数据越好」,提出 3 个反驳的论点,每个论点都要有佐证

4、你是 [某个主题] 的专家,请总结以下内容,并针对以下内容提出未来能进一步研究的方向 [附上内容]

你是金融科技专家,请总结以下内容,并针对以下内容提出未来能进一步研究的方向 [附上内容]

二、资料整理

1、给我 [数字] 篇,有关 [领域] 的文章。

给我 5 篇,有关 SEO 的文章。

2、用列点的方式总结出这篇文章的 [数字] 个重点:[附上文章内容/附上文章网址]。

用列点的方式总结出这篇文章的 5 个重点:[附上文章内容/附上文章网址]。

3、用列点的方式总结出 [数字] 个 [领域] 知识重点

用列点的方式总结出 10 个量子力学知识重点。

三、简历与自传

1、这份 [职位] 的简历,有哪边可以写更好? 请以专业面试官的角度,提出具体改进建议。接着以你提出的建议来改写这段经历,改写时请维持列点的形式。[附上简历]

这份 UIUX 设计师的简历,有哪边可以写更好? 请以专业面试官的角度,提出具体改进建议。接着以你提出的建议来改写这段经历,改写时请维持列点的形式。

2、改写以下简历,为每一点加上量化的数据,改写时请维持列点的形式。[附上简历]

3、把这段经历写得更精简一点,让别人可以马上看到重点,同时维持生动的描述。[附上经历]

4、为不同公司定制化撰写简历

我今天要申请[公司]的[职位],改写以下经历,让我能更符合[公司]的企业文化。[附上经历]

我今天要申请 Google 的前端工程师,改写以下经历,让我能更符合 Google 的企业文化。[附上经历]

四、准备面试

1、你现在是[公司]的[职位]面试官,请分享在[职位]面试时常会问的[数字]个问题。

你现在是 Google 的产品经理面试官,请分享在 Google 产品经理面试时常会问的 5 个问题。

2、我针对 [问题] 的回答,有哪些可以改进的地方? [附上回答]

我针对「你会如何排定不同产品功能优先顺序?」的回答,有哪些可以改进的地方? [附上回答]

3、针对 [问题] 这个面试问题,请提供一些常见的追问面试题。

针对「你会如何排定不同产品功能优先顺序?」这个面试问题,请提供一些常见的追问面试题。

4、我在准备 [问题] 这个面试问题,请用 STAR 原则帮我回答这个问题。针对这个问题,我有的经历如下 [附上经历]

我在准备「请分享一个你在急迫的期限中完成专案的经验」这个面试问题,请用 STAR 原则帮我回答这个问题。针对这个问题,我有的经历如下 [附上经历]。

五、程式

1、你现在是一个 [程式语言] 专家,请帮我用 [程式语言] 写一个函式,它需要做到 [某个功能]

你现在是一个 JavaScript 专家,请帮我用 JavaScript 写一个函式,它需要做到 输入一个一维阵列,把这个一维阵列转换成二维阵列。同时我要能够自由地决定二维阵列中的子阵列长度是多少

2、你现在是一个 [程式语言] 专家,请告诉我以下的程式码在做什么。[附上程式码]

3、你现在是一个 Clean Code 专家,我有以下的程式码,请用更干净简洁的方式改写,让我的同事们可以更容易维护程式码。另外,也解释为什么你要这样重构,让我能把重构的方式的说明加到 Pull Request 当中。[附上程式码]

4、你现在是一个 [程式语言] 专家,我有一段程式码,我预期这段程式码可以 [做到某个功能],只是它通过不了 [测试案例] 这个测试案例。请帮我找出我哪里写错了,以及用正确的方式改写。[附上程式码]

你现在是一个 python 专家,我有一段程式码,我预期这段程式码可以判断一个字串是不是镜像回文,只是它通过不了 aacdeedcc 这个测试案例。请帮我找出我哪里写错了,以及用正确的方式改写。[附上程式码]

5、你现在是一个 [程式语言] 专家,我有一段程式码 [附上程式码],请帮我写一个测试,请至少提供五个测试案例,同时要包含到极端的状况,让我能够确定这段程式码的输出是正确的。

6、你现在是一个 Regex 专家,请帮我写一个 Regex ,它能够把 [需求]

你现在是一个 Regex 专家,请帮我写一个 Regex ,它能够把输入一个字串,把这个字串中的所有数字都取出来

六、知识学习

1、详细的说明 [填入想了解的知识]

详细的说明如何制造一台电脑

2、你扮演 [科目老师] 的角色, 我需要理解 [理论]。请用 [方式] 方式描述。

你扮演数学老师的角色, 我需要理解一元二次方程式。请用浅显易懂方式描述。

3、你是一个 [SEO] 专家,你要教我深度的 [SEO] 知识

你是一个 SEO 专家,你要教我深度的 SEO 知识。

4、教我 [二次方程式],给我一个测验

教我 一元二次方程式,给我一个测验

七、英语学习

1、用 [中文/英文] 解释以下英文单字:[填入一个或多个单字]。请用表格的方式呈现,并且表格内须包含单字、词性、解释与例句。

用中文解释以下英文单字:apple, orange, doctor, car, run。请用表格的方式呈现,并且表格内须包含单字、词性、解释与例句。

2、解释英文单字 [英文单字],并且给我 [数字] 个常用句子

解释英文单字 divest,并且给我 5 个常用句子。

3、英语对话

Can we have a conversation about 话题?

Can we have a conversation about machine learning?

4、校阅英文文法

Can you check the spelling and grammar in the following text? [附上英文文字]

5、英文作文修改与解释

校阅以下英文文章,并用表格的方式呈现,要有三个栏位,分别是原文、修正后的版本,以及用中文详解为什么要这样修改:附上英文文章

6、纠正文法和拼字错误

Please correct my grammar and spelling mistakes in the text above: 附上英文文字

Please correct my grammar and spelling mistakes in the text above: I love eat fooded

八、工作生产力

1、回覆 Email

你是一名 [职业],我会给你一封电子邮件,你要回覆这封电子邮件。电子邮件:[附上内容]

你是一名产品经理,我会给你一封电子邮件,你要回覆这封电子邮件。电子邮件:[附上内容]

九、写作帮手

1、撰写标题

写出 数字 个有关 主题 的 社群平台 风格标题,要遵守以下规则:规则 1、规则 2、其他规则。

写出 5 个有关日本迪士尼旅游心得的 Instagram 风格标题,要遵守以下规则:标题不超过 20 字、标题要加上适当表情符号。

2、撰写文章大纲

提供 [某主题] 主题的文章大纲

提供美国留学主题的文章大纲

3、文章撰写

针对 [主题] 这个主题生成一篇文章

针对使用 ChatGPT 小诀窍这个主题生成一篇文章

4、产品文案

将以下产品关键字生成 数字 句的产品文案。产品关键字:附上关键字…

将以下产品关键字生成 10 句的产品文案。产品关键字:球鞋、春季款、多种颜色、适合慢跑

十、日常生活

1、食谱生成

提供给我一个食谱,食材包含 食材 1、食材 2、食材…。

提供给我一个食谱,食材包含鸡腿肉、鸡蛋、起司。

2、提供食谱

请列出这份食谱的采买清单和步骤:数字 人份的 食谱。

请列出这份食谱的采买清单和步骤:1 人份的蕃茄炒蛋。

3、活动计划清单

你扮演一位专业的活动企划,请生成 活动 活动计划清单,包括重要任务和截止日期。

你扮演一位专业的活动企划,请生成运动会活动计划清单,包括重要任务和截止日期。

4、提供点子

提供 [数字] 个 [想法] 的点子

提供 5 个情人节的点子

5、旅游计划

生成一份 数字 天的 地点 旅游计划,交通工具是 交通工具…。要遵守以下规则:填入规则

生成一份 5 天的东京旅游计画,交通工具是地铁和火车。要遵守以下规则:1. 地点要包含东京铁塔、富士山、迪士尼乐园 2. 需要包含交通如何乘坐 3. 一天不超过 3 个地点。

十一、有趣好玩

1、写歌词

大家都说我写的歌词像 人名,但我有点没灵感,请帮我用 人名 的风格写一首歌。歌中包含的元素要 关键字…。

大家都说我写的歌词像方文山,但我有点没灵感,请帮我用方文山的风格写一首歌。歌中包含的元素要有别离、思念、峰回路转。

2、写故事

写出一篇有关 故事想法,拥有 风格 风格的短篇故事

写出一篇有关工程师拯救这个世界的短篇故事

3、写 rap

你是现在红的饶舌歌手,请创作一首 Rap,主题是 附上主题。

你是现在红的饶舌歌手,请创作一首 Rap,主题是孤勇者。

十二、角色扮演

1、综合情境

你现在是一名 角色,你要针对我提出的问题提供建议。我的问题是:附上问题。

你现在是一名生涯教练,你要针对我提出的问题提供建议。我的问题是:我是否要出国念书?

2、面试官

你现在是一个 职位 面试官,而我是要应征 职位 的面试者。你需要遵守以下规则:1. 你只能问我有关 职位 的面试问题。2. 不需要写解释。3. 你需要向面试官一样等我回答问题,再提问下一个问题。你好。

你现在是一个产品经理面试官,而我是要应征产品经理的面试者。你需要遵守以下规则:

1. 你只能问我有关产品经理的面试问题。

2. 不需要写解释。

3. 你需要向面试官一样等我回答问题,再提问下一个问题。,你好。

3、担任导游

你是一位导游,我会把我旅游的位置给你,你要找一个靠近我位置的地方。在某些情况下,我还会告诉您我想旅游地点的类型。你还会向我找靠近我位置的类似类型的地方。我个需求是填入需求

你是一位导游,我会把我旅游的位置给你,你要找一个靠近我位置的地方。在某些情况下,我还会告诉您我想旅游地点的类型。你还会向我找靠近我位置的类似类型的地方。我的首要需求是我在阿里山,我想参观博物馆。

以上指令请大家灵活应用,后面有更好的,我会继续更新在后面

免费1年服务器,部署个ChatGPT专属网页版! - 掘金

mikel阅读(368)

来源: 免费1年服务器,部署个ChatGPT专属网页版! – 掘金

免费1年服务器,部署个ChatGPT专属网页版!

作者:小傅哥
博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

白皮袄个免费1年服务器,部署个ChatGPT专属网页版!

api.openai.com port 443: Connection timed out 你是不在调用 ChatGPT 接口的时候报错 443 了,哈哈哈,我的也是。当小傅哥发现自己开发的《ChatGPT AI 问答助手》已经不工作了,搜索了下为啥会 443 呢,原来是 openAI 的 API 域名已经 DNS 污染了,所以没法使用。

所以如果你仍需要学习测试使用,要不是找其他人做好的代理接口,要不就是买一台能调用 openAI 的海外服务器。但其实对于学习来说能白皮袄还花费这个钱干嘛!所以小傅哥今天给粉丝伙伴分享个,能免费使用1年稳定的云服务器。第二年再重新申请就可以。

这里小傅哥先部署了一个 ChatGPT Webchatgpt.itedus.cn/ – 以下文章有专门的部署教程。

一、这是什么服务器?

国内我们知道有阿里云、腾讯云、华为云、百度云、京东云,而国外有亚马逊旗下的 AWS 云,并且重点来了,AWS 是有免费的云服务的,任何人都可以获得一台。其实以前就听说有 AWS 免费云,但一直没当回事,现在才感觉真香!

在 AWS 活动页,有一个免费套餐,这里有很多产品都可以免费使用,包括;云服务、数据库、函数计算、机器学习等。地址:aws.amazon.com/cn/free

这里小傅哥感兴趣的就是 Amazon EC2 云服务器,每个月免费750个小时。这一个月31天✖️24小时不也才744小时,所以这和免费有啥区别?🤔 难道一天能加出来25个小时班?

另外这里有一些注意点

  1. EC2 云服务器,只有1核1G,在安装 Docker、Portainer、以及一个 Java SpringBoot 应用程序后,运行正常&稳定。
  2. 申请时需要用到银行卡,我自己有一个 VISA 的信用卡,直接注册的时候使用就可以。其他银行卡也OK。有些营销文,为了卖服务器说乱扣费,为此我专门去搜索了下关于 AWS 免费服务器会有坑不,以及自己验证了一段时间,检查账单以及服务器的运行程度,发现使用的很顺畅,不会有恶意扣费。
  3. 使用到快到1年的时候,可以把服务停掉,并重新申请个账号就可以继续用1年了。

如果以上这些点你觉得没问题,那么就可以按照接下来的教程进行申请和使用了。因为 Amazon EC2 和国内的一些服务器使用风格略有差异,所以需要按照教程创建并使用实例。

二、怎么免费白皮袄?

1. 创建账号

地址:portal.aws.amazon.com/billing/sig…

2. 登录账号

地址:console.aws.amazon.com/console/hom…

3. 控制面板

地址:us-east-1.console.aws.amazon.com/iamv2/home?… – 搜索 IAM 并进入,就是控制面板了。

在创建 Amazon EC2 云服务器前,需要在控制面板中创建;用户组、用户、角色,这样在后面创建服务器的时候才能把用户信息给关联上。

3.1 创建用户

3.2 创建用户组

4. 部署实例

4.1 创建密钥对

这里需要先创建一个密钥对,之后再创建应用实例的时候把密钥对关联上去。同时密钥对也是用作本地 SSH 登录的一个重要凭证,否则不能登录。另外这里创建完密钥对,会自动下载到本地,你需要保存好。

地址:us-east-2.console.aws.amazon.com/ec2/home?re… – EC2 管理控制台。

  • 创建完成后,会自动下载一个名为 admin_key.pem 的文件,保存好它,后面用。

4.2 创建安装组

用于访问服务器的端口都需要在安全组中开通,比如 SSH 22、Portainer 9000、Docker 2375 所以需要自己创建一个安全组。

地址:us-east-2.console.aws.amazon.com/ec2/home?re…

  • 以后你想控制各个端口的访问,就在安全组中操作就可以了。

4.4 启动新实例

如果你比较粗鲁,刚一进来 AWS 就创建了实例,但发现用户也不对,权限也不对用不了。没关系可以把它终止掉,之后重新启动新实例。

地址:us-east-2.console.aws.amazon.com/ec2/home?re…

如果在使用中把自己的应用实例搞坏了,没关系。直接干掉它,重新启动新实例就可以了。

5. 连接实例

Amazon EC2 云服务器提供了 EC2 在线连接、会话管理器、SSH 客户端、EC2 串行管理器。我们这里主要用在线的和 SSH 客户端。

EC2 Instance Connect SSH 客户端
  • EC2 在线连接,直接点上连接就可以了。SSH 客户端需要按照引导进行操作。

通过此方式就可以本地连接了,还是非常方便的。如果你有连接工具也可以配置到工具中操作。

三、部署应用耍起来!

1. 安装 Docker

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

yum list docker --showduplicates | sort -r

sudo yum install docker

sudo systemctl start docker

systemctl enable docker

docker --version
复制代码

2. 配置远程链接

vim /usr/lib/systemd/system/docker.service
在ExecStart=/usr/bin/dockerd-current 后面加上 -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
# 重新加载配置文件
systemctl daemon-reload
# 重启启动
systemctl restart docker
复制代码

3. 部署网页版 ChatGPT

方式 免费? 可靠性 质量
ChatGPTAPI(gpt-3.5-turbo-0301) 可靠 相对较笨
ChatGPTUnofficialProxyAPI(网页 accessToken) 相对不可靠 聪明

通过以下方式,你可以自己部署一个网页版的 ChatGPT,让自己使用。 注意:OPENAI_API_KEY 需要申请

  • 海外服务器;docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=sk-6cPOiRgKmIcMA**** chenzhaoyu94/chatgpt-web
  • 海内服务器;docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=sk-6cPOiRgKmIcMA**** --env OPENAI_API_BASE_URL=https://open2.aiproxy.xyz chenzhaoyu94/chatgpt-web – 需要添加代理

也可以不使用Key方式部署,这个方式的好处是免费,都免费!

  • docker run –name chatgpt-web -d -p 3002:3002 –env OPENAI_ACCESS_TOKEN=【从这里获取 chat.openai.com/api/auth/se… chenzhaoyu94/chatgpt-web

4. 部署 ChatGPT 问答助手

TradingView--最专业的走势图表,收下吧,也许你会用到 - 知乎

mikel阅读(429)

来源: TradingView–最专业的走势图表,收下吧,也许你会用到 – 知乎

前言

最近在做交易所项目里的K线图,得些经验,与大家分享。
代码居多,流量预警!!!!
点赞 收藏 不迷路。

技术选型

  • echrats
    • 基于canvas绘制,种类齐全的可视化图表库。
    • 官方地址:
  • highcharts
    • 基于svg绘制,可定制化内容很多,操纵dom更方便。
    • 官方地址:
  • tradingview
    • 基于canvas的专业全球化走势图表。
    • 官方地址:
  • 优缺点
    • hightcharts: 前些日子有仔细研究过 hightcharts

发现svg中的dom操作,以及定制化内容更好实现,但几乎都需要手动实现的这个特性在开发周期短的压迫下屈服了。上面的这个项目在慢慢摸索下也做了小三个月的样子,但还是很有成就感的。

    • echrats: echarts的官方案例很多,经常在做一些后台管理系统,展现数据时候会用到,方便,易用,使用者也足够多,搜索引擎鸡本能够解决你的任何问题。但对一些在图上划线,等操作,就显得略微疲软。不够能满足需求。
    • tradingview: 只要进入官网,就可见其专业性,他完全就是为了专业交易儿打造的,您只需要想里面填充数据就可以了,甚至在一些常用的交易内容上,可以使用tradingview自己的数据推送。
  • 小记
    • 所以,专业的交易图表,就交给专业的库来做吧
    • 手动狗头~~~~(∩_∩)

准备工作

  • 申请账号(key)
    • 在官网注册后会有邮件提示的,一步一步跟着做就可以了,这里就不做赘述了。
  • 环境搭建
    • 我使用的是自己搭建的React+webpack4脚手架,你也可以使用原生JS,或者你喜欢的任何框架(后面贴出来的代码都是在React环境下的)。
    • 从官方下载代码库
  • 了解websocket通讯协议
    • 发送请求
    • 接收数据
  • 大纲
    • 这里附上tradingview中文开发文档
    • 以及api示例

(此处需要自备梯子)

  • 一位大神的Demo

准备开始吧


创建

    page
    |--kLine // k线内容文件夹
    |--|--api // 需要使用的方法
    |--|--|--datafees.js // 定义了一些公用方法
    |--|--|--dataUpdater.js // 更新时调用的内容
    |--|--|--socket.js // websocket方法
    |--|--index.js // 自己代码开发
    |--|--index.scss // 样式开发
  • datafees.js加入如下代码
       /**
         * JS API
         */
        import React from 'react'
        import DataUpdater from './dataUpdater'
        
        class datafeeds extends React.Component {
        /**
         * JS API
         * @param {*Object} react react实例
         */
            constructor(self) {
                super(self)
                this.self = self
                this.barsUpdater = new DataUpdater(this)
                this.defaultConfiguration = this.defaultConfiguration.bind(this)
            }
            /**
             * @param {*Function} callback  回调函数
             * `onReady` should return result asynchronously.
             */
            onReady(callback) {
                // console.log('=============onReady running')
                return new Promise((resolve) => {
                    let configuration = this.defaultConfiguration()
                    if (this.self.getConfig) {
                        configuration = Object.assign(this.defaultConfiguration(), this.self.getConfig())
                    }
                    resolve(configuration)
                }).then(data => callback(data))
            }
            /**
             * @param {*Object} symbolInfo  商品信息对象
             * @param {*String} resolution  分辨率
             * @param {*Number} rangeStartDate  时间戳、最左边请求的K线时间
             * @param {*Number} rangeEndDate  时间戳、最右边请求的K线时间
             * @param {*Function} onDataCallback  回调函数
             * @param {*Function} onErrorCallback  回调函数
             */
            getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback) {
                const onLoadedCallback = (data) => {
                    data && data.length ? onDataCallback(data, { noData: false }) : onDataCallback([], { noData: true })
                }
                this.self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                /* eslint-enable */
            }
            /**
             * @param {*String} symbolName  商品名称或ticker
             * @param {*Function} onSymbolResolvedCallback 成功回调
             * @param {*Function} onResolveErrorCallback   失败回调
             * `resolveSymbol` should return result asynchronously.
             */
            resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback) {
                return new Promise((resolve) => {
                    // reject
                    let symbolInfoName
                    if (this.self.symbolName) {
                        symbolInfoName = this.self.symbolName
                    }
                    let symbolInfo = {
                        name: symbolInfoName,
                        ticker: symbolInfoName,
                        pricescale: 10000,
                    }
                    const { points } = this.props.props
                    const array = points.filter(item => item.name === symbolInfoName)
                    if (array) {
                        symbolInfo.pricescale = 10 ** array[0].pricePrecision
                    }
                    symbolInfo = Object.assign(this.defaultConfiguration(), symbolInfo)
                    resolve(symbolInfo)
                }).then(data => onSymbolResolvedCallback(data)).catch(err => onResolveErrorCallback(err))
            }
            /**
             * 订阅K线数据。图表库将调用onRealtimeCallback方法以更新实时数据
             * @param {*Object} symbolInfo 商品信息
             * @param {*String} resolution 分辨率
             * @param {*Function} onRealtimeCallback 回调函数
             * @param {*String} subscriberUID 监听的唯一标识符
             * @param {*Function} onResetCacheNeededCallback (从1.7开始): 将在bars数据发生变化时执行
             */
            subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
                this.barsUpdater.subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback)
            }
            /**
             * 取消订阅K线数据
             * @param {*String} subscriberUID 监听的唯一标识符
             */
            unsubscribeBars(subscriberUID) {
                this.barsUpdater.unsubscribeBars(subscriberUID)
            }
            /**
             * 默认配置
             */
            defaultConfiguration = () => {
                const object = {
                    session: '24x7',
                    timezone: 'Asia/Shanghai',
                    minmov: 1,
                    minmov2: 0,
                    description: 'www.coinoak.com',
                    pointvalue: 1,
                    volume_precision: 4,
                    hide_side_toolbar: false,
                    fractional: false,
                    supports_search: false,
                    supports_group_request: false,
                    supported_resolutions: ['1', '15', '60', '1D'],
                    supports_marks: false,
                    supports_timescale_marks: false,
                    supports_time: true,
                    has_intraday: true,
                    intraday_multipliers: ['1', '15', '60', '1D'],
                }
                return object
            }
        }
        
        export default datafeeds 
  • dataUpdater加入如下代码
    class dataUpdater {
        constructor(datafeeds) {
            this.subscribers = {}
            this.requestsPending = 0
            this.historyProvider = datafeeds
        }
        subscribeBars(symbolInfonwq, resolutionInfo, newDataCallback, listenerGuid) {
            this.subscribers[listenerGuid] = {
                lastBarTime: null,
                listener: newDataCallback,
                resolution: resolutionInfo,
                symbolInfo: symbolInfonwq
            }
        }
        unsubscribeBars(listenerGuid) {
            delete this.subscribers[listenerGuid]
        }
        updateData() {
            if (this.requestsPending) return
            this.requestsPending = 0
            for (let listenerGuid in this.subscribers) {
                this.requestsPending++
                this.updateDataForSubscriber(listenerGuid).then(() => this.requestsPending--).catch(() => this.requestsPending--)
            }
        }
        updateDataForSubscriber(listenerGuid) {
            return new Promise(function (resolve, reject) {
              var subscriptionRecord = this.subscribers[listenerGuid];
              var rangeEndTime = parseInt((Date.now() / 1000).toString());
              var rangeStartTime = rangeEndTime - this.periodLengthSeconds(subscriptionRecord.resolution, 10);
              this.historyProvider.getBars(subscriptionRecord.symbolInfo, subscriptionRecord.resolution, rangeStartTime, rangeEndTime, function (bars) {
                this.onSubscriberDataReceived(listenerGuid, bars);
                resolve();
              }, function () {
                reject();
              });
            });
        }
        onSubscriberDataReceived(listenerGuid, bars) {
            if (!this.subscribers.hasOwnProperty(listenerGuid)) return
            if (!bars.length) return
            const lastBar = bars[bars.length - 1]
            const subscriptionRecord = this.subscribers[listenerGuid]
            if (subscriptionRecord.lastBarTime !== null && lastBar.time < subscriptionRecord.lastBarTime) return
            const isNewBar = subscriptionRecord.lastBarTime !== null && lastBar.time > subscriptionRecord.lastBarTime
            if (isNewBar) {
                if (bars.length < 2) {
                    throw new Error('Not enough bars in history for proper pulse update. Need at least 2.');
                }
                const previousBar = bars[bars.length - 2]
                subscriptionRecord.listener(previousBar)
            }
            subscriptionRecord.lastBarTime = lastBar.time
            console.log(lastBar)
            subscriptionRecord.listener(lastBar)
        }
        periodLengthSeconds =(resolution, requiredPeriodsCount) => {
            let daysCount = 0
            if (resolution === 'D' || resolution === '1D') {
                daysCount = requiredPeriodsCount
            } else if (resolution === 'M' || resolution === '1M') {
                daysCount = 31 * requiredPeriodsCount
            } else if (resolution === 'W' || resolution === '1W') {
                daysCount = 7 * requiredPeriodsCount
            } else {
                daysCount = requiredPeriodsCount * parseInt(resolution) / (24 * 60)
            }
            return daysCount * 24 * 60 * 60
        }
    }
    export default dataUpdater

  • socket.js加入如下代码(也可以使用自己的websocket模块)
     class socket {
            constructor(options) {
                this.heartBeatTimer = null
                this.options = options
                this.messageMap = {}
                this.connState = 0
                this.socket = null
            }
            doOpen() {
                if (this.connState) return
                this.connState = 1
                this.afterOpenEmit = []
                const BrowserWebSocket = window.WebSocket || window.MozWebSocket
                const socketArg = new BrowserWebSocket(this.url)
                socketArg.binaryType = 'arraybuffer'
                socketArg.onopen = evt => this.onOpen(evt)
                socketArg.onclose = evt => this.onClose(evt)
                socketArg.onmessage = evt => this.onMessage(evt.data)
                // socketArg.onerror = err => this.onError(err)
                this.socket = socketArg
            }
            onOpen() {
                this.connState = 2
                this.heartBeatTimer = setInterval(this.checkHeartbeat.bind(this), 20000)
                this.onReceiver({ Event: 'open' })
            }
            checkOpen() {
                return this.connState === 2
            }
            onClose() {
                this.connState = 0
                if (this.connState) {
                    this.onReceiver({ Event: 'close' })
                }
            }
            send(data) {
                this.socket.send(JSON.stringify(data))
            }
            emit(data) {
                return new Promise((resolve) => {
                    this.socket.send(JSON.stringify(data))
                    this.on('message', (dataArray) => {
                        resolve(dataArray)
                    })
                })
            }
            onMessage(message) {
                try {
                    const data = JSON.parse(message)
                    this.onReceiver({ Event: 'message', Data: data })
                } catch (err) {
                    // console.error(' >> Data parsing error:', err)
                }
            }
            checkHeartbeat() {
                const data = {
                    cmd: 'ping',
                    args: [Date.parse(new Date())]
                }
                this.send(data)
            }
            onReceiver(data) {
                const callback = this.messageMap[data.Event]
                if (callback) callback(data.Data)
            }
            on(name, handler) {
                this.messageMap[name] = handler
            }
            doClose() {
                this.socket.close()
            }
            destroy() {
                if (this.heartBeatTimer) {
                    clearInterval(this.heartBeatTimer)
                    this.heartBeatTimer = null
                }
                this.doClose()
                this.messageMap = {}
                this.connState = 0
                this.socket = null
            }
        }
        export default socket 

初始化图表

  • 可以同时请求websocket数据。
  • 新建init函数,并在onready/mounted/mounted等时候去调用(代码的含义在注释里,我尽量写的详细一点)
  init = () => {
        var resolution = this.interval; // interval/resolution 当前时间维度
        var chartType = (localStorage.getItem('tradingview.chartType') || '1')*1;
        var locale = this.props.lang; // 当前语言
        var skin = this.props.theme; // 当前皮肤(黑/白)
        if (!this.widgets) {
            this.widgets = new TradingView.widget({ // 创建图表
                autosize: true, // 自动大小(适配,宽高百分百)
                symbol:this.symbolName, // 商品名称
                interval: resolution,
                container_id: 'tv_chart_container', // 容器ID
                datafeed: this.datafeeds, // 配置,即api文件夹下的datafees.js文件
                library_path: '/static/TradingView/charting_library/', // 图表库的位置,我这边放在了static,因为已经压缩过
                enabled_features: ['left_toolbar'],
                timezone: 'Asia/Shanghai', // 图表的内置时区(常用UTC+8)
                // timezone: 'Etc/UTC', // 时区为(UTC+0)
                custom_css_url: './css/tradingview_'+skin+'.css', //样式位置
                locale, // 语言
                debug: false,
                disabled_features: [ // 在默认情况下禁用的功能
                    'edit_buttons_in_legend',
                    'timeframes_toolbar',
                    'go_to_date',
                    'volume_force_overlay',
                    'header_symbol_search',
                    'header_undo_redo',
                    'caption_button_text_if_possible',
                    'header_resolutions',
                    'header_interval_dialog_button',
                    'show_interval_dialog_on_key_press',
                    'header_compare',
                    'header_screenshot',
                    'header_saveload'
                ],
                overrides: this.getOverrides(skin), // 定制皮肤,默认无盖默认皮肤
                studies_overrides: this.getStudiesOverrides(skin) // 定制皮肤,默认无盖默认皮肤
            })
            var thats = this.widgets;
            // 当图表内容准备就绪时触发
            thats.onChartReady(function() {
                createButton(buttons);
            })
            var buttons = [
                {title:'1m',resolution:'1',chartType:1},
                {title:'15m',resolution:'15',chartType:1},
                {title:'1h',resolution:'60',chartType:1},
                {title:'1D',resolution:'1D',chartType:1},
            ];
            // 创建按钮(这里是时间维度),并对选中的按钮加上样式
            function createButton(buttons){
                for(var i = 0; i < buttons.length; i++){
                    (function(button){
                        let defaultClass =
                        thats.createButton()
                        .attr('title', button.title).addClass(`mydate ${button.resolution === '15' ? 'active' : ''}`)
                        .text(button.title)
                        .on('click', function(e) {
                            if (this.className.indexOf('active')> -1){// 已经选中
                                return false
                            }
                            let curent =e.currentTarget.parentNode.parentElement.childNodes
                            for(let index of curent) {
                                if (index.className.indexOf('my-group')> -1 && index.childNodes[0].className.indexOf('active')> -1) {
                                    index.childNodes[0].className = index.childNodes[0].className.replace('active', '')
                                }
                            }
                            this.className = `${this.className} active`
                            thats.chart().setResolution(button.resolution, function onReadyCallback() {})
                        }).parent().addClass('my-group'+(button.resolution == paramary.resolution ? ' active':''))
                    })(buttons[i])
                }
            }
        }
    }

请求数据

  • 新建initMessage函数—在需要去获取数据的时候,调取initMessage。
     initMessage = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => {
            let that = this
            //保留当前回调
            that.cacheData['onLoadedCallback'] = onLoadedCallback;
            //获取需要请求的数据数目
            let limit = that.initLimit(resolution, rangeStartDate, rangeEndDate)
            //如果当前时间节点已经改变,停止上一个时间节点的订阅,修改时间节点值
            if(that.interval !== resolution){
                that.interval = resolution
                paramary.endTime = parseInt((Date.now() / 1000), 10)
            } else {
                paramary.endTime = rangeEndDate
            }
            //获取当前时间段的数据,在onMessage中执行回调onLoadedCallback
            paramary.limit = limit
            paramary.resolution = resolution
            let param
            // 分批次获取历史(这边区分了历史记录分批加载的请求)
            if (isHistory.isRequestHistory) {
                param = {
                    // 获取历史记录时的参数(与全部主要区别是时间戳)
                }
            } else {
                param = {
                    // 获取全部记录时的参数
                }
            }
            this.getklinelist(param)
        } 
  • 在请求历史数据时,由于条件不满足,会一直请求后台接口,所以需要加上 函数节流
    • 在lodash这个库里面是有节流的方法的
    • 首先引入节流函数—-import throttle from 'lodash/throttle'
    • 使用非常简单,只要在函数前面套一层—–this.initMessage = throttle(this.initMessage, 1000);
      • throttle()函数里面,第一个参数是需要截留的函数,第二个为节流时间。

收到数据,渲染图表

  • 可以在接收数据的地方调用socket.on('message', this.onMessage(res.data))
  • onMessage函数,是为渲染数据进入图表内容
// 渲染数据
    onMessage = (data) => { // 通过参数将数据传递进来
        let thats = this
        if (data === []) {
            return
        }
        // 引入新数据的原因,是我想要加入缓存,这样在大数据量的时候,切换时间维度可以大大的优化请求时间
        let newdata = []
        if(data && data.data) {
            newdata = data.data
        }
        const ticker = `${thats.symbolName}-${thats.interval}`
        // 第一次全部更新(增量数据是一条一条推送,等待全部数据拿到后再请求)
        if (newdata && newdata.length >= 1 && !thats.cacheData[ticker] && data.firstHisFlag === 'true') {
            // websocket返回的值,数组代表时间段历史数据,不是增量
            var tickerstate = `${ticker}state`
            // 如果没有缓存数据,则直接填充,发起订阅
            if(!thats.cacheData[ticker]){
                thats.cacheData[ticker] = newdata
                thats.subscribe()   // 这里去订阅增量数据!!!!!!!
            }
            // 新数据即当前时间段需要的数据,直接喂给图表插件
            // 如果出现历史数据不见的时候,就说明 onLoadedCallback 是undefined
            if(thats.cacheData['onLoadedCallback']){ // ToDo
                thats.cacheData['onLoadedCallback'](newdata)
            }
            //请求完成,设置状态为false
            thats.cacheData[tickerstate] = false
            //记录当前缓存时间,即数组最后一位的时间
            thats.lastTime = thats.cacheData[ticker][thats.cacheData[ticker].length - 1].time
        }
        // 更新历史数据 (这边是添加了滑动按需加载,后面我会说明)
        if(newdata && newdata.length > 1 && data.firstHisFlag === 'true' && paramary.klineId === data.klineId && paramary.resolution === data.resolution && thats.cacheData[ticker] && isHistory.isRequestHistory) {
            thats.cacheData[ticker] = newdata.concat(thats.cacheData[ticker])
            isHistory.isRequestHistory = false
        }
        // 单条数据()
        if (newdata && newdata.length === 1 && data.hasOwnProperty('firstHisFlag') === false && data.klineId === paramary.klineId  && paramary.resolution === data.resolution) {
            //构造增量更新数据
            let barsData = newdata[0]
            //如果增量更新数据的时间大于缓存时间,而且缓存有数据,数据长度大于0
            if (barsData.time > thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) {
                //增量更新的数据直接加入缓存数组
                thats.cacheData[ticker].push(barsData)
                //修改缓存时间
                thats.lastTime = barsData.time
            } else if(barsData.time == thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length){
                //如果增量更新的时间等于缓存时间,即在当前时间颗粒内产生了新数据,更新当前数据
                thats.cacheData[ticker][thats.cacheData[ticker].length - 1] = barsData
            }
            // 通知图表插件,可以开始增量更新的渲染了
            thats.datafeeds.barsUpdater.updateData()
        }
    } 

逻辑中心===>getbars

  • 新建getbars函数(该函数会在图表有变化时自动调用)
     getBars = (symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) => {
            const timeInterval = resolution // 当前时间维度
            this.interval = resolution
            let ticker = `${this.symbolName}-${resolution}`
            let tickerload = `${ticker}load`
            var tickerstate = `${ticker}state`
            this.cacheData[tickerload] = rangeStartDate
            //如果缓存没有数据,而且未发出请求,记录当前节点开始时间
            // 切换时间或币种
            if(!this.cacheData[ticker] && !this.cacheData[tickerstate]){
                this.cacheData[tickerload] = rangeStartDate
                //发起请求,从websocket获取当前时间段的数据
                this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                //设置状态为true
                this.cacheData[tickerstate] = true
            }
            if(!this.cacheData[tickerload] || this.cacheData[tickerload] > rangeStartDate){
                //如果缓存有数据,但是没有当前时间段的数据,更新当前节点时间
                this.cacheData[tickerload] = rangeStartDate;
                //发起请求,从websocket获取当前时间段的数据
                this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback);
                //设置状态为true
                this.cacheData[tickerstate] = !0;
            }
            //正在从websocket获取数据,禁止一切操作
            if(this.cacheData[tickerstate]){
                return false
            }
            // 拿到历史数据,更新图表
            if (this.cacheData[ticker] && this.cacheData[ticker].length > 1) {
                this.isLoading = false
                onLoadedCallback(this.cacheData[ticker])
            } else {
                let self = this
                this.getBarTimer = setTimeout(function() {
                    self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                }, 10)
            }
            // 这里很重要,画圈圈----实现了往前滑动,分次请求历史数据,减小压力
            // 根据可视窗口区域最左侧的时间节点与历史数据第一个点的时间比较判断,是否需要请求历史数据
            if (this.cacheData[ticker] && this.cacheData[ticker].length > 1 && this.widgets && this.widgets._ready && !isHistory.isRequestHistory && timeInterval !== '1D') {
                const rangeTime = this.widgets.chart().getVisibleRange()  // 可视区域时间值(秒) {from, to}
                const dataTime = this.cacheData[ticker][0].time // 返回数据第一条时间
                if (rangeTime.from * 1000 <= dataTime + 28800000) { // true 不用请求 false 需要请求后续
                    isHistory.endTime = dataTime / 1000
                    isHistory.isRequestHistory = true
                    // 发起历史数据的请求
                    this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
                }
            }
        }   

小记

  • tradingview主要就是这几个函数之间的搭配。
  • 使用onLoadedCallback(this.cacheData[ticker])或者this.datafeeds.barsUpdater.updateData()去更新数据。
  • 滑动加载时,可以先加载200条,后面每次150条,这样大大缩小了数据量,增加了渲染时间。
  • 滑动加载时的节流会经常用到。

进阶websocket

  • 二进制传输数据
  • websocket在传输数据的时候是明文传输,而且像K线上的历史数据,一般数据量比较大。为了安全性以及更快的加载出图表,我们决定使用二进制的方式传输数据。
    • 可以通过使用pako.js解压二进制数据
    • 引入pako.jsyarn add pako -S
    • 使用方法
      if (res.data instanceof Blob) { // 看下收到的数据是不是Blob对象
            const blob = res.data
            // 读取二进制文件
            const reader = new FileReader()
            reader.readAsBinaryString(blob)
            reader.onload = () => {
                // 首先对结果进行pako解压缩,类型是string,再转换成对象
                data = JSON.parse(pako.inflate(reader.result, { to: 'string' }))
            }
        }
    • 转换后,数据大小大概减少了20%。

差不多了


写在最后

  • 这里只分享些简单的内容,细节可以参照原生js版本的Demo github.com/tenggouwa/tr
  • 关于滚动加载,以及二进制的内容有问题的可以评论留言。
  • 如果这篇文章对你有帮助,或者是让您对tradingview有些了解,欢迎留言或点赞,我会一一回复。
  • 笔者最大的希望就是您能从我的文章里获得点什么,我就很开心啦。。。
  • 后面,至少每个月更新一篇文章。点赞关注不迷路啊,老铁。