JSON (JavaScript Object Notation) 的序列化和反序列化在工作中偶尔会用到,特别是在处理网络请求的时候。通常的网站或者API接口会提供JSON数据格式,要将JSON字符串转换为C#中的对象,就要用到序列化和反序列化。
恰好最近有一个需求,需要从某个网站上获取一下Shibor数据,这里涉及到了自定义的JSON序列化,简单记录一下。
需求
程序需要从这个网站上获取SHIBOR数据,如下图:
分析
要获取网页信息,首先要分析网页信息的来源方式,如果是静态网页内容,可以使用HttpClient来获取内容,然后通过 HTML 解析库(如HtmlAgilityPack)提取所需的数据。如果网页内容是动态生成的,即网站的内容是在网页加载完成之后,通过另外的API调用填充,那么就不能直接获取网站内容字符串然后进行解析了。这种情况下有两种策略:如果能够模拟网站发起API请求直接获取数据,那么可以通过HttpClient发起API调用来获取需要的数据,然后进行解析,这种方式简单便捷依赖少。如果不行,通常情况下可能需要校验信息比如需要登录然后获得Token等,那么就要使用一些客户端比如Selenium WebDriver 来模拟浏览器行为,等待页面动态内容加载完成后再提取数据。这种方法比较笨重,需要首先安装相关的NuGet包,万不得已不会用。
要分析网页的数据来源类型,可以通过浏览器里面的“开发者工具”(快捷键F12)来协助分析,首先打开”Element",用鼠标选取页面上的内容,下面会显示相关的HTML标签,比如数据部分的内容如下:
可以看到,核心的数据是在ID为“shiborData”的标签里。接下来切换到"Network"标签,然后刷新页面,可以看到页面的所有数据请求:
一个一个查看,可以看到shibor.html这个页面在加载完成之后,在JavaScript里面,通过AJAX发起了一个Post请求,请求的数据格式为json,请求完成之后,解析返回的数据,然后将数据append到上述id为“shiborData”的标签里。
从上述的分析来看,这个Post请求没有做校验,所以是可以直接在C#里面发起一个HttpClient请求来获取数据。
实现
实现分为两部分,获取数据和序列化。
获取数据
从上述分析可知,只需要发起一个Http Post请求即可获取相应的数据,我们可以对返回的JSON数据进行查看和分析:
static ShiborResponse GetShiborData()
{
var url = "https://www.shibor.org/r/cms/www/chinamoney/data/shibor/shibor.json";
var httpClient = new HttpClient();
try
{
// 创建一个空的请求内容(如果 POST 请求需要传递参数,这里可以设置)
var content = new StringContent("{}", Encoding.UTF8, "application/json");
var response = httpClient.PostAsync(url, content).Result;
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content.ReadAsStringAsync().Result;
// 假设返回的数据是 JSON 格式,尝试解析
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<ShiborResponse>(responseContent, options);
}
}
catch (Exception ex)
{
Console.WriteLine($"发生错误: {ex.Message}");
}
return null;
}
上述代码使用了HttpClient发起了一个异步的Post请求,如果请求成功,它是一个JSON字符串,请求的结果存放在了responseContent里了,内容如下:
{
"head": {
"version": "2.0",
"provider": "CWAP",
"req_code": "",
"rep_code": "200",
"rep_message": "",
"ts": 1744859415143,
"producer": ""
},
"data": {
"showDateEN": "17/04/2025 11:00",
"showDateCN": "2025-04-17 11:00"
},
"records": [
{
"termCode": "O/N",
"shibor": "1.6340",
"shibIdUpDown": "7.40",
"shibIdUpDownNum": -7.4,
"termCodePath": "O/N"
},
{
"termCode": "1W",
"shibor": "1.6520",
"shibIdUpDown": "4.40",
"shibIdUpDownNum": -4.4,
"termCodePath": "1W"
},
{
"termCode": "2W",
"shibor": "1.7350",
"shibIdUpDown": "2.20",
"shibIdUpDownNum": -2.2,
"termCodePath": "2W"
},
{
"termCode": "1M",
"shibor": "1.7660",
"shibIdUpDown": "0.60",
"shibIdUpDownNum": -0.6,
"termCodePath": "1M"
},
{
"termCode": "3M",
"shibor": "1.7670",
"shibIdUpDown": "0.50",
"shibIdUpDownNum": -0.5,
"termCodePath": "3M"
},
{
"termCode": "6M",
"shibor": "1.7760",
"shibIdUpDown": "0.60",
"shibIdUpDownNum": -0.6,
"termCodePath": "6M"
},
{
"termCode": "9M",
"shibor": "1.7780",
"shibIdUpDown": "0.60",
"shibIdUpDownNum": -0.6,
"termCodePath": "9M"
},
{
"termCode": "1Y",
"shibor": "1.7770",
"shibIdUpDown": "0.60",
"shibIdUpDownNum": -0.6,
"termCodePath": "1Y"
}
]
}
现在正常情况下,只需要创建一个类似的C#对象来接收以上数据即可。
序列化
获得了原始的JSON数据后,可以直接按照JSON的格式来准备一个对应的C#数据结构即可。但是有时候,这些JSON的格式结构,与C#类中定义的标准规范可能不同。这时就需要对序列化进行控制。
比如,假设只需要获取返回数据里面的时间showDateCN,以及records里的每条记录,在每条记录里面,只需要termCode和shibor。shibor是一个double类型,并且因为是百分比,所以要在获取的数据上除以100。
上面的JSON字符串的字段名称是JSON里面的规范,而我们希望按照C#的标准来对字段的名称和类型进行重命名,比如 showDateCN 类型应该是日期,名称应该为ShowDate,termCode 应该是一个枚举,名称应该改为TermCode。C#代码的定义如下:
// 定义完整响应类
class ShiborResponse
{
public ShiborDataInfo Data { get; set; }
public List<ShiborRecord> Records { get; set; }
}
// 定义期限代码的枚举
enum TermCode
{
Overnight,
OneWeek,
TwoWeeks,
OneMonth,
ThreeMonths,
SixMonths,
NineMonths,
OneYear
}
// 定义数据信息类
class ShiborDataInfo
{
public DateTime ShowDate { get; set; }
}
// 定义记录信息类
class ShiborRecord
{
public TermCode TermCode { get; set; }
public double Shibor { get; set; }
}
直接将JSON字符反串序列化为上述的ShiborResponse对象肯定会报错,因为这里面C#定义的字段及类型与返回的JSON不同,并且枚举类型,日期类型,double类型的转换并除以100的这些逻辑都没有指定。这里以使用 System.Text.Json 为例来说明:
首先需要指定JsonProperyName,来将JSON里面的字段映射到C#的字段,然后定义一些转换类:
class ShiborResponse
{
[JsonPropertyName("data")]
public ShiborDateInfo Data { get; set; }
[JsonPropertyName("records")]
public List<ShiborRecord> Records { get; set; }
}
// 定义期限代码的枚举
enum TermCode
{
[JsonPropertyName("O/N")]
Overnight,
[JsonPropertyName("1W")]
OneWeek,
[JsonPropertyName("2W")]
TwoWeeks,
[JsonPropertyName("1M")]
OneMonth,
[JsonPropertyName("3M")]
ThreeMonths,
[JsonPropertyName("6M")]
SixMonths,
[JsonPropertyName("9M")]
NineMonths,
[JsonPropertyName("1Y")]
OneYear
}
// 定义数据信息类
class ShiborDateInfo
{
[JsonPropertyName("showDateCN")]
[JsonConverter(typeof(CustomDateTimeConverter))]
public DateTime ShowDate { get; set; }
}
// 定义记录信息类
class ShiborRecord
{
[JsonConverter(typeof(CustomTermCodeConverter))]
[JsonPropertyName("termCode")]
public TermCode TermCode { get; set; }
[JsonConverter(typeof(CustomShiborConverter))]
public double Shibor { get; set; }
}
类型转换相关的类如下:
// 自定义日期时间转换器
class CustomDateTimeConverter : JsonConverter<DateTime>
{
private readonly string[] _formats = { "yyyy-MM-dd HH:mm" };
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (DateTime.TryParseExact(reader.GetString(), _formats, null, System.Globalization.DateTimeStyles.None, out DateTime result))
{
return result;
}
throw new JsonException($"无法将 {reader.GetString()} 转换为 DateTime 类型。");
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(_formats[0]));
}
}
// 自定义枚举转换器
class CustomTermCodeConverter : JsonConverter<TermCode>
{
public override TermCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = reader.GetString();
switch (value)
{
case "O/N":
return TermCode.Overnight;
case "1W":
return TermCode.OneWeek;
case "2W":
return TermCode.TwoWeeks;
case "1M":
return TermCode.OneMonth;
case "3M":
return TermCode.ThreeMonths;
case "6M":
return TermCode.SixMonths;
case "9M":
return TermCode.NineMonths;
case "1Y":
return TermCode.OneYear;
default:
throw new JsonException($"无法将 {value} 转换为 TermCode 枚举类型。");
}
}
public override void Write(Utf8JsonWriter writer, TermCode value, JsonSerializerOptions options)
{
string termCodeString;
switch (value)
{
case TermCode.Overnight:
termCodeString = "O/N";
break;
case TermCode.OneWeek:
termCodeString = "1W";
break;
case TermCode.TwoWeeks:
termCodeString = "2W";
break;
case TermCode.OneMonth:
termCodeString = "1M";
break;
case TermCode.ThreeMonths:
termCodeString = "3M";
break;
case TermCode.SixMonths:
termCodeString = "6M";
break;
case TermCode.NineMonths:
termCodeString = "9M";
break;
case TermCode.OneYear:
termCodeString = "1Y";
break;
default:
throw new JsonException($"无法将 {value} 转换为字符串。");
}
writer.WriteStringValue(termCodeString);
}
}
// 自定义 Shibor 字段转换器
class CustomShiborConverter : JsonConverter<double>
{
public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = reader.GetString();
if (double.TryParse(value, out double shiborValue))
{
return shiborValue / 100;
}
throw new JsonException($"无法将 {value} 转换为 double 类型。");
}
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value * 100);
}
}
很好理解,只需要继承自JsonConverter类,并重写它的Read和Write方法即可。
上面的JSON里面还有一些字段比如head不需要就不用定义。现在以上代码能够正常运行,但是仍有瑕疵。
在ShiborResponse类里面有一个ShiborDateInfo类型的字段,这个类里面只有一个类型为DateTime的ShowDate字段 (这个类如果对照JSON应该有两个字段,showDateCN和showDateEN,因为不需要showDateEN),这里最好的办法是移除ShbiorDateInfo类,将该类的ShowDate字段放到ShiborResponse类里来。
// 定义完整响应类
class ShiborResponse
{
[JsonPropertyName("showDateCN")]
[JsonConverter(typeof(CustomDateTimeConverter))]
public DateTime ShowDate { get; set; }
[JsonPropertyName("records")]
public List<ShiborRecord> Records { get; set; }
}
现在如果直接运行代码,是肯定会报错的,因为C#类的结构和JSON的结构不对应,JSON文件中的showDateCN从data类移到了上一级里面,所以需要再添加一个手动的解析:
//自定义 ShiborResponse 转换器
class ShiborResponseConverter : JsonConverter<ShiborResponse>
{
public override ShiborResponse Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
var response = new ShiborResponse();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "records":
response.Records = JsonSerializer.Deserialize<List<ShiborRecord>>(ref reader, options);
break;
case "data":
if (reader.TokenType == JsonTokenType.StartObject)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType == JsonTokenType.PropertyName)
{
string dataPropertyName = reader.GetString();
reader.Read();
if (dataPropertyName == "showDateCN")
{
response.ShowDate = new CustomDateTimeConverter().Read(ref reader, typeof(DateTime), options);
}
}
}
}
break;
}
}
}
return response;
}
public override void Write(Utf8JsonWriter writer, ShiborResponse value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName("Records");
JsonSerializer.Serialize(writer, value.Records, options);
writer.WritePropertyName("Data");
writer.WriteStartObject();
new CustomDateTimeConverter().Write(writer, value.ShowDate, options);
writer.WriteEndObject();
writer.WriteEndObject();
}
}
是这个ShiborResponseConverter基本上就是对字符按照JSON的结构进行手动解析了。
现在运行测试代码:
void Main()
{
var shiborData = GetShiborData();
if (shiborData != null)
{
Console.WriteLine($"显示日期(中文): {shiborData.ShowDate}");
Console.WriteLine("Shibor 记录信息:");
foreach (var record in shiborData.Records)
{
Console.WriteLine($"期限代码: {record.TermCode}, Shibor 值: {record.Shibor}");
}
}
}
可以看到输出结果如下:
显示日期(中文): 2025/4/18 11:00:00
Shibor 记录信息:
期限代码: Overnight, Shibor 值: 0.0166
期限代码: OneWeek, Shibor 值: 0.01654
期限代码: TwoWeeks, Shibor 值: 0.01764
期限代码: OneMonth, Shibor 值: 0.01763
期限代码: ThreeMonths, Shibor 值: 0.01761
期限代码: SixMonths, Shibor 值: 0.01771
期限代码: NineMonths, Shibor 值: 0.01775
期限代码: OneYear, Shibor 值: 0.01772
相当完美。
这里也贴出用Newtonsoft.Json 库解析的类定义和转换代码,它与 System.Text.Json 有些不同,但大同小异:
数据获取,这里因为是采用Https请求,所以要首先定义SecurityProtocol,否则在一些操作系统上会报错:
/// <summary>
/// 获取最新的 Shibor 数据,如果未获取到数据,返回null
/// </summary>
/// <returns></returns>
public static ShiborResult GetShibors()
{
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
var url = "https://www.shibor.org/r/cms/www/chinamoney/data/shibor/shibor.json";
var httpClient = new HttpClient();
try
{
// 创建一个空的请求内容(如果 POST 请求需要传递参数,这里可以设置)
var content = new StringContent("{}", Encoding.UTF8, "application/json");
var response = httpClient.PostAsync(url, content).Result;
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content.ReadAsStringAsync().Result;
return JsonConvert.DeserializeObject<ShiborResult>(responseContent);
}
}
catch (Exception ex)
{
//Console.WriteLine($"发生错误: {ex.Message}");
}
return null;
}
实体定义,以及对应的转换方法:
[JsonConverter(typeof(ShiborResultConverter))]
public class ShiborResult
{
public DateTime ShowDate { get; set; }
public List<ShiborRecord> Records { get; set; }
}
public enum TermCode
{
[JsonProperty("O/N")]
Overnight,
[JsonProperty("1W")]
OneWeek,
[JsonProperty("2W")]
TwoWeeks,
[JsonProperty("1M")]
OneMonth,
[JsonProperty("3M")]
ThreeMonths,
[JsonProperty("6M")]
SixMonths,
[JsonProperty("9M")]
NineMonths,
[JsonProperty("1Y")]
OneYear
}
// 定义记录信息类
public class ShiborRecord
{
[JsonProperty("termCode")]
[JsonConverter(typeof(CustomTermCodeConverter))]
public TermCode TermCode { get; set; }
[JsonProperty("shibor")]
[JsonConverter(typeof(CustomShiborConverter))]
public double Shibor { get; set; }
}
// 自定义日期时间转换器
public class CustomDateTimeConverter : IsoDateTimeConverter
{
public CustomDateTimeConverter()
{
DateTimeFormat = "yyyy-MM-dd HH:mm";
}
}
// 自定义枚举转换器
public class CustomTermCodeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(TermCode);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string value = reader.Value.ToString();
switch (value)
{
case "O/N":
return TermCode.Overnight;
case "1W":
return TermCode.OneWeek;
case "2W":
return TermCode.TwoWeeks;
case "1M":
return TermCode.OneMonth;
case "3M":
return TermCode.ThreeMonths;
case "6M":
return TermCode.SixMonths;
case "9M":
return TermCode.NineMonths;
case "1Y":
return TermCode.OneYear;
default:
throw new JsonException($"无法将 {value} 转换为 TermCode 枚举类型。");
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
string termCodeString;
TermCode termCode = (TermCode)value;
switch (termCode)
{
case TermCode.Overnight:
termCodeString = "O/N";
break;
case TermCode.OneWeek:
termCodeString = "1W";
break;
case TermCode.TwoWeeks:
termCodeString = "2W";
break;
case TermCode.OneMonth:
termCodeString = "1M";
break;
case TermCode.ThreeMonths:
termCodeString = "3M";
break;
case TermCode.SixMonths:
termCodeString = "6M";
break;
case TermCode.NineMonths:
termCodeString = "9M";
break;
case TermCode.OneYear:
termCodeString = "1Y";
break;
default:
throw new JsonException($"无法将 {termCode} 转换为字符串。");
}
writer.WriteValue(termCodeString);
}
}
// 自定义 Shibor 字段转换器
public class CustomShiborConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(double);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string value = reader.Value.ToString();
if (double.TryParse(value, out double shiborValue))
{
return shiborValue / 100;
}
throw new JsonException($"无法将 {value} 转换为 double 类型。");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue((double)value * 100);
}
}
// 自定义 ShiborResponse 转换器
public class ShiborResultConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ShiborResult);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var response = new ShiborResult();
JObject jObject = JObject.Load(reader);
if (jObject["records"] != null)
{
response.Records = jObject["records"].ToObject<List<ShiborRecord>>();
}
if (jObject["data"] != null && jObject["data"]["showDateCN"] != null)
{
response.ShowDate = jObject["data"]["showDateCN"].ToObject<DateTime>();
}
return response;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var shiborResponse = (ShiborResult)value;
JObject jObject = new JObject();
JArray recordsArray = JArray.FromObject(shiborResponse.Records);
jObject["records"] = recordsArray;
JObject dataObject = new JObject();
dataObject["showDateCN"] = shiborResponse.ShowDate;
jObject["data"] = dataObject;
jObject.WriteTo(writer);
}
}