经过前面【Grasshopper基础1~4】的介绍,相信读者已经了解了如何在Visual Studio里创建电池、如何获取数据、如何传出数据。
那么在了解这些原理之后,就让我们来一起实现一个小的项目,来看看一个电池从头到尾到底如何制作吧。
正如标题中所述的,我们今天就来做一个 “在GH里看基金净值” 的小电池,大致就是实现一个“给定基金代码,通过网络API调用获取基金净值信息,并输出该基金的所有净值信息”。要实现这个功能,首先我们需要首先进行需求分析,然后确定实现的逻辑,最后进行代码编写和简单的测试。
需求分析
“给定基金代码”
一般而言,基金代码都是整型数值,但是考虑到日常使用的时候,基金代码前可能会包含数字0,故认为它会是整型或者是字符串。
不过,我们在前文(【Grasshopper基础3】)中已经了解到了,由于GH里字符串(GH_String
)和整型(GH_Integer
)会自动进行隐式转换,其实不需要担心到底会是整形还是字符串,只需确定我们代码最终接受的类型,GH会在输入参数中自动帮我们进行类型转换。
“输出该基金的所有净值信息”
基金净值是浮点型,但是由于需要获取所有净值信息,包含历史上基金存在时所有的值,所以对应每一个基金代码应该会附带一个列表的信息。净值是浮点型。
经过上述分析,
- 电池的输入端需要一个参数,可以是整型或字符串,这个项目我们就直接采用整型;
- 输出端需要一个参数,是浮点型;
- 输入和输出直接的对应关系是一个整型输入对应一个浮点型列表,确定输入端的参数的
Access
属性为item,输出端的参数的Access
属性为list。
此时可以开一个Visual Studio,新建一个Grasshopper电池项目,然后将我们的输入、输出端的参数相关代码放入了。
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddIntegerParameter("Code", "C", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddNumber("DayValues", "v", "", GH_ParamAccess.list);
}
电池内部实现逻辑
这部分是整个电池项目中最耗时的部分,也就是在电池类的 SolveInstance
中处理输入的基金代码信息,并给出最终的净值浮点数列表。
首先获取基金代码:
protected override void SolveInstance(IGH_DataAccess DA)
{
// 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
int code = int.MinValue;
bool Success = DA.GetData(0, ref code);
if(!Success) return;
}
然后就是对基金净值API的调用,这里调用的是 “小熊同学”网站的基金API。这个API的返回值是一个json字符串,里面包含基金的名称、代码、净值等信息,详细的API文档可在网站中找到。
现在的问题就聚焦到两个部分了,一个是如何发送网络请求,另一个是如何在返回的json字符串中提取我们需要的信息。
网络请求的发送
由于我们是在Grasshopper中实现该逻辑,所以运行环境是 .NET Framework 4.5。在网络上找资料时也需要时刻注意运行环境。目前.NET生态已经布局到全平台,很多相关网络请求的资料找到的都是 .NET Core 或者 .NET 的,而 .NET Framework 与它们略有不同,依赖项可能会不一样。
(具体 .NET / .NET Core / .NET Framework 三者有什么区别和联系读者们也可以自行探索一下,相关历史介绍还是挺多的)
在 .NET Framework 框架下,网络请求的发送可以使用 System.Net
命名空间里的 WebRequest
类实现。
protected override void SolveInstance(IGH_DataAccess DA)
{
// 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
// .... ... .. . 略
// 建立请求
var req = WebRequest.Create(@"https://api.doctorxiong.club/v1/fund/detail?code=" + code.ToString());
req.Method = "GET";
req.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8");
// 发起请求,将请求返回保存至res中
var res = req.GetResponse();
}
返回的Json字符串解析
接下来,就需要对返回的json字符串进行解析了。考虑到现在网络编程的便利性,json字符串几乎已经成为网络编程中数据往来的标准格式,所以json字符串的解析就不需要手写了,.NET 框架已经自带了json字符串解析的功能(System.Text.Json
),可以自动将json字符串封装成一个 C# 的object
提供原生访问,十分方便。
但是,这个 System.Text.Json
在 .NET Framework 中是没有的!所以我们需要用到它的前生 Newtonsoft.Json
。不过这个dll需要手动添加,具体添加方法为
- 在Visual Studio的项目管理器中找到引用(References)并右键单击,选择添加引用
- 弹出窗口中,左上角选择 Assemblies -> Extensions
- 找到并选择 Json.NET,单击确定添加,此时引用中出现
Newtonsoft.Json
引用添加成功后我们就可以在代码中加入命名空间
using Newtonsoft.Json;
此时我们离解析成功仅差一步之遥了。
前面提到用使用Newtonsoft.Json
可以自动把json字符串转换为一个可以操作的对象实例,其前提是,这个对象实例我们需要有这个对象的数据模型。
想象json是一堆货物,Newtonsoft.Json
可以帮忙装货,但是它需要知道每种货物需要被装在什么位置,我们需要指明数据会被装在什么对象的什么属性中。
数据模型其实就是一个类,我们需要依据json的格式去构建这个类的属性,下面就直接给出笔者建立的数据模型。笔者在这里使用的是struct
结构体,读者也可以自行换成class
关键字。
// 数据模型中每一个属性都对应着json字符串中的标签
// 数据模型可以不包含所有的json字符串标签,不包含的数据会被忽略掉
public struct ResponseData
{
public HttpStatusCode Code { get; set; }
public string Message { get; set; }
public Fund Data { get; set; }
public string Meta { get; set; }
public struct Fund
{
public string Code { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public float NetWorth { get; set; }
public float ExpectWorth { get; set; }
public float TotalWorth { get; set; }
public string ExpectGrowth { get; set; }
public string DayGrowth { get; set; }
public string LastWeekGrowth { get; set; }
public string LastMonthGrowth { get; set; }
public string LastThreeMonthsGrowth { get; set; }
public string LastSixMonthsGrowth { get; set; }
public string LastYearGrowth { get; set; }
public string BuyMin { get; set; }
public string BuySourceRate { get; set; }
public string BuyRate { get; set; }
public string Manager { get; set; }
public string FundScale { get; set; }
public string NetWorthDate { get; set; }
public string ExpectWorthDate { get; set; }
public string[][] NetWorthData { get; set; }
public string[][] TotalNetWorthData { get; set; }
}
}
有了这个数据模型,我们就可以直接对返回的json字符串进行解析,代码如下。MemoryStream
类需要用到命名空间System.IO
,Encoding
类需要用命名空间System.Text
。
protected override void SolveInstance(IGH_DataAccess DA)
{
// 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
// 建立请求
// 发起请求,将请求返回保存至res中
// 准备解析
string jsonString;
using (var responseStream = res.GetResponseStream()) // 获取返回流
{
// 在内存中创建临时存储用来存储返回流数据
using (var ms = new MemoryStream())
{
// 返回流数据复制至内存中
responseStream.CopyTo(ms);
// 用UTF8解码返回流数据
jsonString = Encoding.UTF8.GetString(ms.ToArray());
// 解析json字符串
var resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
}
}
}
最终我们获得了变量 resData 就是最终我们解析得到的数据,它是一个 ResponseData
结构体的实例,我们可以在后续传出数据时方便地通过该结构体获得数据。
电池数据传出
下面的事情就又变得简单了起来,由于我们的输出是对应一个数据列表,我们仅需从 resData 中提取到我们想要的基金净值数据,存储在一个 List<double>
中,然后借由 DA.SetDataList()
方法传出即可。
通过基金API可知,基金净值信息包含在 NetWorthData
中,每个NetWorthData
包含4个数据,分别是 “日期、净值、日涨跌、额外信息”。我们所需要的净值在该列表的第二项。
又因为所有的数据在数据模型中是以字符串形式给出,我们需要使用double.Parse来进行格式转 …… 等等……GH可以自动隐式转换,我们不如直接输出字符串试试看?
protected override void SolveInstance(IGH_DataAccess DA)
{
// 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
// 建立请求
// 发起请求,将请求返回保存至res中
// 准备解析
string jsonString;
using (var responseStream = res.GetResponseStream()) // 获取返回流
{
// 在内存中创建临时存储用来存储返回流数据
using (var ms = new MemoryStream())
{
// 返回流数据复制至内存中
// 用UTF8解码返回流数据
// 解析json字符串
// 提取每天的净值信息,放入列表中
List<string> dayValues = new List<string>();
foreach (var item in resData.Data.NetWorthData)
dayValues.Add(item[1]);
// 传出数据
DA.SetDataList(0, dayValues);
}
}
}
这样我们的电池就完工了!下面赶紧让我们来试试看把。(文章最后有完整cs文件的代码可供参考)
代码测试
首先要做的是在Rhino中使用 GrasshopperDevelopSetting 命令加入我们电池的输出目录。具体细节可见【Grasshopper基础1】,本文在此不再复述。设置完成后关闭Rhino。
点击运行,让我们测试一下自己的电池吧!
但是作为测试内容,我们不能总是期待会给出正确的基金代码,换一个思路,当我们瞎输入一个实际不存在的基金代码时……
报错了!
Error converting value {null} to type ******
解读一下发现,原来是我们在做解析的时候并没有做空值判断。因为如果基金代码不存在的话,API返回的值肯定是带有错误信息的,其中的Data
值肯定是空,我们忽略了这一点,所以自然就报错啦。
简单处理一下,把这段放在try-catch
中,并添加一个错误信息,直接返回
protected override void SolveInstance(IGH_DataAccess DA)
{
// 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
// 建立请求
// 发起请求,将请求返回保存至res中
// 准备解析
string jsonString;
using (var responseStream = res.GetResponseStream()) // 获取返回流
{
// 在内存中创建临时存储用来存储返回流数据
using (var ms = new MemoryStream())
{
// 返回流数据复制至内存中
// 用UTF8解码返回流数据
// 解析json字符串
ResponseData resData = default(ResponseData);
try
{
resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
}
catch (Exception e)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.Message);
return;
}
// 提取每天的净值信息,放入列表中
// 传出数据
}
}
}
这回就对错误有了一些简单的错误处理逻辑了。当然,通过查阅API,还可以在返回的数据结构中针对不同种类的错误给出不同的提示,这里限于篇幅就不展开了。
总结
通过这个小电池,我们将前面1~4的内容进行了一个串联,从如何收集数据、实现自己的逻辑,到最后传出数据,进行简单的测试和Debug。涉及到的点大致有下面几个内容
- 输入/输出参数的确定,以及它们的类型,相关的GH特有的隐式类型转换
- 对 WebAPI 发起一个网络请求
- 对返回的json字符串进行解析
- 简单的测试
其中,json字符串解析部分本文并没有详细展开,因为它本身就能做一个超级大的内容了,这里只给出了一个具体的应用。想要详细了解的读者可以在CSDN上直接搜索(点击右侧) Newtonsoft.Json 就可以出来更多的资料了。
另外,网络请求相关的内容,读者在搜索额外资料时请注意适用环境,.NET / .NET Core 是一类环境,.NET Framework 与它们是有一些差别的。
最后,部分代码涉及到了 流(Stream) 的概念,这是有关于I/O相关的内容。初步理解的话就是可以认为“流”是一系列函数,最后的数据会在所有的中间的流中过一遍,也就是实现了对整个数据流的依次处理。读者感兴趣的话可以自行继续深入学习。
电池cs文件的全部代码
using Grasshopper.Kernel;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace DigitalCrab.Grasshopper
{
public struct ResponseData
{
public HttpStatusCode Code { get; set; }
public string Message { get; set; }
public Fund Data { get; set; }
public string Meta { get; set; }
public struct Fund
{
public string Code { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public float NetWorth { get; set; }
public float ExpectWorth { get; set; }
public float TotalWorth { get; set; }
public string ExpectGrowth { get; set; }
public string DayGrowth { get; set; }
public string LastWeekGrowth { get; set; }
public string LastMonthGrowth { get; set; }
public string LastThreeMonthsGrowth { get; set; }
public string LastSixMonthsGrowth { get; set; }
public string LastYearGrowth { get; set; }
public string BuyMin { get; set; }
public string BuySourceRate { get; set; }
public string BuyRate { get; set; }
public string Manager { get; set; }
public string FundScale { get; set; }
public string NetWorthDate { get; set; }
public string ExpectWorthDate { get; set; }
public string[][] NetWorthData { get; set; }
public string[][] TotalNetWorthData { get; set; }
}
}
public class GetStock : GH_Component
{
public GetStock()
: base("GetStock", "GS",
"Description",
"Params", "DigitalCrab")
{}
protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
pManager.AddIntegerParameter("Code", "C", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
pManager.AddNumberParameter("DayValues", "v", "", GH_ParamAccess.list);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
int code = int.MinValue;
bool Success = DA.GetData(0, ref code);
if(!Success) return;
var req = WebRequest.Create(@"https://api.doctorxiong.club/v1/fund/detail?code=" + code.ToString());
req.Method = "GET";
req.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8");
var res = req.GetResponse();
string jsonString;
using (var responseStream = res.GetResponseStream())
{
using (var ms = new MemoryStream())
{
responseStream.CopyTo(ms);
jsonString = Encoding.UTF8.GetString(ms.ToArray());
ResponseData resData = default(ResponseData);
try {
resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
}
catch (Exception e) {
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.Message);
return;
}
List<string> dayValues = new List<string>();
foreach (var item in resData.Data.NetWorthData) dayValues.Add((item[1]));
DA.SetDataList(0, dayValues);
}
}
}
protected override System.Drawing.Bitmap Icon => null;
public override Guid ComponentGuid => throw new NotImplementedException("请自行申请GUID并替换");
}
}