2024年1月

torch.nn.Module

Module类是用户使用torch来自定义网络模型的基础,Module的设计要求包括低耦合性,高模块化等等。一般来说,计算图上所有的子图都可以是Module的子类,包括卷积,激活函数,损失函数节点以及相邻节点组成的集合等等,注意这里的关键词是“节点”,Module族类在计算图中主要起到搭建结构的作用,而不涉及运算逻辑的具体实现。

要注意的是,Module类对象的children所指向的其他Module类对象,并不等同于计算图中的子节点。如果我们展开Module网络,得到的一般是树形结构而非DAG,Module网络需要经过其他工作才能转化为计算图。

源代码分析

成员分析

首先直接从前端入手,找到torch/nn/module目录,可以看到这个目录下主要存放Module及其子类的定义,如。我们首先找到module.py内Module的定义

阅读__init__ 函数,可以看到Module基类的主要私有成员,其中包括

指向本Module内带梯度的可学习参数的parameter
指向本Module内不需要学习的模型状态参数的buffer
其他临时参数
前向与反向过程的hook函数,这些函数在运行backward与forward时允许自定义其它额外工作
state_dict相关函数,state_dict保存了模型的状态,是模型写入磁盘与加载的主要方式
modules指向该模块内部的所有子模块

方法分析

结构相关
  • 子模块生成

首先从我们日常使用pytorch搭建网络的用法可以想到,应该先去__setattr__函数寻找建立子节点的入口。

下图折叠了几个分支,可以看到当我们运行self.c1 = Conv2d(...)时,将会进入1202行的分支内,并且判断新成员是否是Module类型,如果是则将其放入本对象的子模块字典内。

__setattr__ 内主要对Parameter,Module,特定name的Tensor(也就是buffer)等参数做特判,其他情况则调用object的属性设置流程。事实上,其他的类似方法(如getattr等)也是同样的流程。

  • 内部参数访问

对于存储于私有成员_module内的子模块,一般使用children方法进行调用

我们在外部所使用的xx.modules()方法,就是通过调用children方法实现的。

另外,nn.Module实现了许多对参数转化的方法,比如CPU(将内部参数转移到内存中),CUDA(将内部参数转移到显存中)以及type(将参数转化为指定类型),而这些是通过调用内部的_apply方法实现的

可以看到,_apply接受一个函数指针参数,并对所有的子模块递归地调用自己。然后对本Module内所有的Parameter与buffer应用该函数。

问题来了,既然每个节点都进行函数应用,那么如何避免对同一参数重复应用fn?这个问题的关键在于内部的Parameter到底是如何存储的。

印象里,我们在外部使用xx.parameters()时,得到的是xx模块的所有参数,看起来和上述代码里的_parameters并非直接取用的关系,我们可以看一下parameters()的实现

注意到默认参数recurse=True,相信大部分人已经明白原因了,我们继续看到named_parameters()

对_named_member方法传入了获取子模块_parameters字典键值对的匿名函数,继续看到_named_members()

可以看到具体流程是先递归或者不递归地获取该模块下的所有用户希望获得的东西(具体定义在第一个函数参数中),然后返回迭代器

这里1489行体现递归调用,原因是named_modules方法本身就是一个递归函数

在这里插入图片描述

事实上,named_parameters, named_buffers均是通过named_members进而调用named_modules方法实现的,_module成员体现网络结构的特殊性在这里可以窥见一二。另外可以看到,上述方法内都存在memo集合进行去重,确保不会返回相同的指针对象。

  • 简要流程图

在这里插入图片描述

参考文章

https://zhuanlan.zhihu.com/p/340453841

聚类算法属于无监督学习,其中最常见的是
均值聚类

scikit-learn
中,有两种常用的均值聚类算法:
一种是有名的
K-means
(也就是K-均值)聚类算法,这个算法几乎是学习聚类必会提到的算法;
另一个是
均值偏移
聚类,它与
K-means
各有千秋,只是针对的应用场景不太一样,但是知名度远不如
K-Means

本篇介绍如何在
scikit-learn
中使用这两种算法。

1. 算法概述

1.1. K-Means

K-means
算法起源于1967年,由James MacQueen和J. B. Hartigan提出。
它的基本原理是是将
n个点
划分为
K个集群
,使得每个点都属于离其最近的均值(中心点)对应的集群。

K-Means算法
主要包含2个部分:

  1. 距离公式
    :通常采用
    欧几里得距离
    来计算数据点与质心之间的距离

\(d(X_i, C_j) = ||X_i - C_j||^2\)
其中,
\(X_i\)
是数据点,
\(C_j\)
是质心。

  1. 目标函数
    :目标是最小化所有数据点与所属簇的质心之间的距离平方和

\(J = \sum_{j=1}^k \sum_{i=1}^{N_j} ||X_i - C_j||^2\)
其中,
\(N_j\)
表示第
\(j\)
个簇中的样本数量。

1.2. 均值漂移

均值漂移
算法最早是由Fukunaga等人在1975年提出的。
它的基本原理是对每一个数据点,算法都会估算其周围点的
密度梯度
,然后沿着密度上升的方向移动该点,直至达到密度峰值。

均值漂移算法
主要有3个步骤:


  1. 核函数
    估计数据点的密度:常用的核函数比如高斯核,

\(K(x) = \exp(-||x||^2 / (2h^2))\)
其中,
\(h\)
为带宽参数,控制核的宽度。

  1. 均值漂移向量
    :也就是对于每个数据点,计算其周围点的密度梯度
  2. 迭代更新
    :根据均值漂移向量,每个数据点会沿着密度上升的方向移动,更新自己的位置

2. 创建样本数据

利用
scikit-learn
中的样本生成器,创建一些用于聚类的数据。

import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=1000, centers=5)
plt.scatter(X[:, 0], X[:, 1], marker="o", c=y, s=25)

plt.show()

image.png
生成了包含
5个
类别的
1000条
样本数据。

3. 模型训练

首先,划分训练集和测试集。

from sklearn.model_selection import train_test_split

# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

按照
8:2
的比例划分了
训练集

测试集

3.1. K-Means

对于
K-Means
算法来说,需要指定聚类的数目,通过观察数据,我们指定聚类的数目
5

这里的样本数据比较简单,能够一下看出来,实际情况下并不会如此容易的知道道聚类的数目是多少,
常常需要多次的尝试,才能得到一个比较好的聚类数目,也就是
K
的值。

基于上面的数据,我们设置5个簇,看看聚类之后的质心在训练集和测试集上的表现。

from sklearn.cluster import KMeans

# 定义
reg = KMeans(n_clusters=5, n_init="auto")

# 训练模型
reg.fit(X_train, y_train)

# 绘制质心
_, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
markers = ["x", "o", "^", "s", "*"]
centers = reg.cluster_centers_

axes[0].scatter(X_train[:, 0], X_train[:, 1], marker="o", c=y_train, s=25)
axes[0].set_title("【训练集】的质心位置")

axes[1].scatter(X_test[:, 0], X_test[:, 1], marker="o", c=y_test, s=25)
axes[1].set_title("【测试集】的质心位置")

for idx, c in enumerate(centers):
    axes[0].plot(c[0], c[1], markers[idx], markersize=10)
    axes[1].plot(c[0], c[1], markers[idx], markersize=10)

plt.show()

image.png

3.2. 均值漂移

均值漂移聚类,事先是不用指定聚类的数目的,通过调整它的
bandwidth
参数,
可以训练出拥有不同数目质心的模型。

下面,设置了
bandwidth=5
,训练之后得到了拥有
3个质心
的模型。

from sklearn.cluster import MeanShift

# 定义
reg = MeanShift(cluster_all=False, bandwidth=5)

# 训练模型
reg.fit(X, y)

# 绘制质心
_, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
markers = ["x", "o", "^", "s", "*"]
centers = reg.cluster_centers_
print(len(centers))

axes[0].scatter(X_train[:, 0], X_train[:, 1], marker="o", c=y_train, s=25)
axes[0].set_title("【训练集】的质心位置")

axes[1].scatter(X_test[:, 0], X_test[:, 1], marker="o", c=y_test, s=25)
axes[1].set_title("【测试集】的质心位置")

for idx, c in enumerate(centers):
    axes[0].plot(c[0], c[1], markers[idx], markersize=10)
    axes[1].plot(c[0], c[1], markers[idx], markersize=10)

plt.show()

image.png
它把左下角的
3类
比较接近的样本数据点算作一类。
通过调整
bandwidth
参数,也可以得到和 K-Means 一样的结果,
有兴趣的话可以试试,大概设置
bandwidth=2
左右的时候,可以得到5个质心,与上面的K-Means算法的结果类似。

4. 总结

K-Means

均值漂移
聚类都是强大的聚类工具,各有其优缺点。

K-Means
的优势是简单、快速且易于实现,当数据集是密集的,且类别之间有明显的分离时,效果非常好;
不过,它需要
预先设定簇
的数量
k
,且对初始质心的选择敏感,所以,对于不是凸形状或者大小差别很大的簇,效果并不好。


均值漂移聚类
的优势在于
不需要
预先知道
簇的数量
,可以自适应地找到数据的“模式”,对噪声和异常值也有很好的鲁棒性。
不过,与
K-Means
相比,它需要选择合适的带宽参数,对高维数据可能不太有效,且计算复杂度较高。

最后,对于这两种均值聚类算法来说,选择哪种取决于数据的性质和应用的需求。

一.正则表达式的入门

正则表达式是一些特定支付组成的,代表一个规则,简化代码,以字符的形式体现规则

正则表达式,又称规则表达式,(Regular Expression,在代码中常简写为regex、regexp或RE),是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符"),是计算机科学的一个概念。

正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。

正则表达式的概念始于20世纪50年代,当时美国数学家StephenColeKleene正式提出了正则语言的概念。随着Unix文本处理工具的出现,正则表达式被普遍使用。

自20世纪80年代以来,存在不同的编写正则表达式的语法,其中一种是POSIX标准,另一种是广泛使用的Perl语法

入门程序:

我们需要简单校验一下中国的电话号码的合法性,有以下简单规则:

  • 以数字1开头
  • 长度为11位
  • 中间不包括字符

使用自己写的代码程序校验:

public static voidmain(String[] args) {//手写程序
System.out.println(checkPhoneNumber("17388268020"));//true
System.out.println(checkPhoneNumber(null));//false
System.out.println(checkPhoneNumber("12"));//false
System.out.println(checkPhoneNumber("12as6987412"));//false
}//手动书写程序
public static booleancheckPhoneNumber(String number){if (number == null){return false;
}
if (number.length()!=11 || number.charAt(0)!='1'){return false;
}
for (int i = 1; i < number.length(); i++) {if (number.charAt(i) < '0' || number.charAt(i) >'9'){return false;
}
}
return true;
}

使用正则表达式:

public static voidmain(String[] args) {//正则表达式
System.out.println(checkNumberUseRegex("17388268020"));//true
System.out.println(checkNumberUseRegex(null));//false
System.out.println(checkNumberUseRegex("12"));//false
System.out.println(checkNumberUseRegex("12as6987412"));//false
}//正则表达式
public static booleancheckNumberUseRegex(String number){return (number!=null) && number.matches("[1]\\d{10}");
}

可以看到,代码很简单,很优雅,很简洁

就是有点看不懂,所以我们需要认识以下,一些正则表达式的符号

正则表达式的常用符号

1.字符类:只能匹配单个字符 ,[ ]

 //1.字符类(只能匹配单个字符)
 System.out.println("a".matches("[abc]")); //true 只能是a,b,c
 System.out.println("e".matches("[abc]"));//false
 System.out.println("d".matches("[^abc]")); //ture 不能是a,b,c
 System.out.println("a".matches("[^abc]"));//false
 System.out.println("b".matches("[a-zA-Z]"));//true 范围是a~z,A~Z
 System.out.println("2".matches("[a-zA-Z]"));//false
 System.out.println("k".matches("[a-z&&[^bc]]"));//ture 范围是a~z,但是不包括bc
 System.out.println("b".matches("[a-z&&[^bc]]"));//false
 System.out.println("ab".matches("[a-z]"));//false  []只能匹配一个字符,第二字符是不能匹配的

2.预定义字符:只能匹配单个字符

.   \d   \D   \s   \S   \w   \W

//2.预定义字符(只匹配单个字符)//
//. \d \D \s \S \w \W
System.out.println("马".matches("."));//ture " . " 可以匹配任意字符
System.out.println("马马".matches("."));//false 只能匹配单个字符//使用\d等字符定义的时候,一定要注意\代表转义,所以使用\d等,需要在加一个\,将\d的\转换掉,即\\d
System.out.println("3".matches("\\d"));//ture \d => 0~9
System.out.println("a".matches("\\d"));//false a不是数字
System.out.println(" ".matches("\\s"));//ture \s:代表一个空白字符
System.out.println("a".matches("\\s"));//false
System.out.println(" ".matches("\\S"));//false \S代表非空格
System.out.println("a".matches("\\S"));//ture
System.out.println("a".matches("\\w"));//ture 字符数字下划线:a~z,A~Z,0~9,_
System.out.println("_".matches("\\w"));//ture
System.out.println("9".matches("\\w"));//ture
System.out.println("马".matches("\\w"));//false
System.out.println("马".matches("\\W"));//ture 不能是 字符数字下划线
System.out.println("a".matches("\\W"));//false

3.数量词

?  *  +  {n}  {n, }  {n,m}

//3.数量词//?  *  +  {n}  {n, }  {n,m}
System.out.println("a".matches("\\w?")); //ture ? 0次或1次
System.out.println("".matches("\\w?"));//ture 没出现
System.out.println("abc".matches("\\?"));//false
System.out.println("abc12".matches("\\w*"));//* 代表 0次或多次
System.out.println("".matches("\\w*"));//ture
System.out.println("acb马".matches("\\w*"));//false
System.out.println("abc12".matches("\\w+"));//ture  + 代表 1次或多次
System.out.println("".matches("\\w+"));//false
System.out.println("acb马".matches("\\w+"));//false 出现:非字符数字下划线
System.out.println("acb".matches("\\w{3}"));//ture 出现3 次
System.out.println("abc".matches("\\w{3,}"));//ture 大于等于3次
System.out.println("abc".matches("\\w{3,6}"));//ture 大于等于 3次 ,小于等于 6次

4.其它几个常用的符号

(?i):忽略大小写    | :或       ():分组
//4.其它几个常用的符号//(?i)忽略大小写  | 或 ():分组
System.out.println("abc".matches("(?i)abc"));//ture a 被忽略大小写
System.out.println("Abc".matches("(?i)abc"));//ture
System.out.println("aBc".matches("ab(?i)c"));//false B没有被限制
System.out.println("abc".matches("123{3}|\\w{3}"));//ture 要么是123三个字符 或者 要么是字符数字下划线三个
System.out.println("123".matches("123{3}|\\w{3}"));//ture
System.out.println("马ac".matches("123{3}|\\w{3}"));//false
System.out.println("ababc".matches("(ab)+c"));//ture ab必须出现1次或多次
System.out.println("(acacb)".matches("(ab)+c"));//false
System.out.println("abab".matches("(ab)?"));//false ab出现0次或1次

二.正则表达式的应用

使用正则表达式来校验电话号码

//检测电话号码是否合规//可能是座机,可能是手机
public  static  booleancheckPhone(String num){//手机电话以1开始,第二位是3-7,长度一般是 11 位//座机号码一般是0开始,前三位是区号,中间可有可无 -符号,后面长度为5-20//号码全是数字,无字符
    return (num !=null) && (num.matches("(1[3-7]\\d{9})|(0\\d{2}-?[1-9]\\d{3,20})"));
}

正则表达式的写法其实就是在找字符串的规律:
(1[3-7]\\d{9})|(0\\d{2}-?[1-9]\\d{3,20})

验证手机号:

首先在校验手机号码的时候,我们可以看到必须是以1开头的,所以正则表达式就需要在第一个位置占位1

而第二位我们也知道了只能在3~9中选择,因为我们并没有发现10,11,12开头的电话号码,所以第二位的取值区间也被限定了[3-9]

除开前两位数字以后,后面的就没什么要求了,除了全部都是数字没有其它的要求所以可以使用  \ \d 直接概括完

(1[3-7]\\d{9})

验证座机号:

中间是有一个 | 符号的,它的作用是 :或的意思,也就是前面不匹配的时候可以试着匹配后面部分,在思想上就是如果不是手机号就是座机号

对于座机号我们也需要按照,字符串的规则来制定正则表达式

首先座机号码一般是0开始的,所以正则表达式直接使用0先占位

前三位只要是数字就行,所以可以直接使用 \\d{2},在匹配随机的任意两位数字就可以了

中间区号有些是需要使用 - 符号分割的,但是有些座机号又没有,所以按需使用就可以了,也就是 - 可以出现,也可以不出现,使用?就可以匹配 0 次或 1次

后面的就全部是数字 \\ d就可以搞定,然后限制一下,后面最多,最少可以拥有多少位数字就完成了

(0\\d{2}-?[1-9]\\d{3,20})

使用正则表达式来校验邮箱账户

//验证邮箱账户是否合理//1425ms@163.com//2021373@qq.com//235676ll@msf.com.cn  二级域名
public  static  booleancheckEmail(String email){return (email!=null) && (email.matches("\\w{2,}@\\w{2,10}(\\.\\w{2,10})+"));
}

主要来来看看正则表达式部分:

\\w{2,}@\\w{2,10}(\\.\\w{2,10})+

在验证邮箱账户的时候,每个邮箱账户有一个很明显的特点,它是使用@符号进行分割的

@前面部分都是由数字字符下划线组成的自定义用户名

@后面部分就是高级域名,他一般指代的是公司和区域位置,如腾讯的qq.com,网易的163.com等

所以在写正则表达式的时候也需要利用@将邮箱账户直接划分成两部分

前部分可以直接利用\\w解决,因为都是字符数组之类的,只用划定一下长度区间就好了,但是一般自定义的账户都是没有做长度限制的,都是在2个字符以上就可以了

@后面部分是高级域名区和公司称谓,都是\\w直接解决,限定长度就可以了,但是注意要加 . 符号,但是在正则表达式中 .是通配的意思,所以需要转义 \\ .

由于高级域名可能有两级,如:.com.cn,所以限定 \\. \\w{2,10} + ,出现 1次,或多次就可以了

使用正则表达式来查找信息

Pattern类:

在Java中,
Pattern
类是用于表示正则表达式的一个类,它属于
java.util.regex
包。
Pattern
类提供了一种将正则表达式编译为可重用的模式的方法,并且可以通过这些模式执行各种匹配操作。

使用
Pattern
类,可以将正则表达式编译为
Pattern
对象,然后使用该对象创建
Matcher
对象,以便在特定的输入字符串上执行匹配操作。可以使用
Matcher
对象的各种方法来执行匹配、查找和替换等操作。

简单来讲:使用pattern类利用Pattern.compile()方法,需要将一个正则表达式模板传入,而pattern.compile()方法就会根据正则表达式模板生成一个pattern对象,在进行文本匹配的时候,就会反复调用这个文本模板

matcher方法:

在Java中,
Pattern
类的
matcher()
方法用于创建一个
Matcher
对象。
Matcher
对象用于在给定的输入字符串上执行匹配操作。

当你有一个编译好的
Pattern
对象时,你可以使用
matcher()
方法来创建一个
Matcher
对象,然后使用该对象的各种方法来执行匹配、查找和替换等操作。

使用matcher方法就可以反复的查找给定的一段文本中的类容,如果文本类容中有一段符合正则表达式,则就返回ture,我们还可以matcher.group()方法将这段匹配的文本内容拿出来

代码测试:

public static voidmain(String[] args) {
String data
="来黑马程序员学习Java\n"+ "电话:1866668888,18699997777\n"+ "或者联系邮箱: boniu@itcast.cn\n" + "座机电话:01036517895,010-98951256\n" + "邮箱: bozai@itcast.cn\n" + "邮箱2:dleieo09@163.com\n"+ "热线电话:400-618-9090 , 400-618-4008,4006184000,4006189090";//1.定义查询时数据规则,正则表达式//电话号码,座机号码,邮箱 String regex ="(1[3-9]\\d{9})|(0\\d{2}-?[1-9]\\d{3,20})|(\\w{2,}@\\w{2,10}(\\.\\w{2,10}){1,2})|(400-?618-?[1-9]\\d{3,20})";//2.利用pattern.compile方法构造处一个pattern对象,利用正则表达式的模板 Pattern pattern =Pattern.compile(regex);//3.利用pattern.matcher传入数据后,构造一个matcher对象,它会反复利用正则表达式遍历数据文本 Matcher matcher =pattern.matcher(data);//匹配上数据后就可以输出了 while(matcher.find()){
System.out.println(matcher.group());
}
}

正则表达式用来搜索替换内容,分割

替换内容使用replaceAll()方法:

public String replaceAll(String regex,String newStr)

其中regex参数需要传入的是一个正则表达式,而newStr则是需要替换的新字符串

按照正则表达式匹配上的文本,替换成新文本

//public String replaceAll(String regex,String newStr)//按照正则表达式匹配上的文本,替换成新文本
String txt1 = "正则表达式是1254真的好用12563,并且7852简洁,就是99864有点666666难";//将数字全部去掉
String s1 = txt1.replaceAll("\\w+", "");
System.out.println(s1);
//正则表达式是真的好用,并且简洁,就是有点难

进阶版:

 String txt2 = "我我我喜喜喜欢欢编编编编编编程程程程程程";//去掉多余的字符,每个字符只留下一个
 String s2 = txt2.replaceAll("(.)\\1+", "$1");//这里的\\1是分组的意思,表示1组,后面被替换的字符就是重出现的字符,也就是1组内的字符,使用$符 组号将其取出来
 System.out.println(s2); //我喜欢编程

分割内容使用的是spilt()方法:

public String[] spilt(String regex)
其中regex参数也是正则表达式
按照正则表达式匹配的内容进行分割,并且返回一个分割后的数组
//public String[] spilt(String regex)//按照正则表达式匹配的内容进行分割,并且返回一个分割后的数组
String txt3 = "原来你也玩原神3564a,不对,原神56688ooihb启动!";//将汉字用非汉字分割成字符串数组
String[] s3 = txt3.split("\\w+");
System.out.println(Arrays.toString(s3));
//[原来你也玩原神, ,不对,原神, 启动!]

在Spring框架中,事务管理是一个核心功能,然而有时候会遇到事务失效的情况,这可能导致数据一致性问题。本文将深入探讨一些Spring事务失效的常见场景,并提供详细的例子以及解决方案。

1.
跨方法调用问题

场景:
当一个事务方法内部调用另一个方法,而被调用的方法没有声明为
@Transactional
时,事务可能会失效。

示例:

@Transactional
public class TransactionalService {

    public void outerMethod() {
        innerMethod(); // 这里的调用会绕过事务
    }

    public void innerMethod() {
        // some logic
    }
}

解决方案:
确保被调用的方法也被事务管理,可以在
innerMethod
上添加
@Transactional
注解。

2.
自调用问题

场景:
当在同一个类的方法内部自调用,事务也可能失效。

示例:

@Transactional
public class TransactionalService {

    public void selfInvoke() {
        this.selfInvoke(); // 自调用,可能绕过事务
    }
}

解决方案:
使用AOP来实现方法自调用时的事务生效,或者将自调用提取到另一个Bean中。

3.
RuntimeException问题

场景:
默认情况下,Spring事务只在遇到
RuntimeException
时回滚,对于其他异常可能无法生效。

示例:

@Transactional
public class TransactionalService {

    public void methodWithCheckedException() throws SomeCheckedException {
        // some logic
        throw new SomeCheckedException(); // 事务可能不会回滚
    }
}

解决方案:

@Transactional
注解上添加
rollbackFor
属性,明确指定需要回滚的异常类型。

4.
并发问题

场景:
多线程并发执行时,Spring事务可能由于隔离级别不当而失效。

示例:

@Transactional(isolation = Isolation.READ_COMMITTED)
public class TransactionalService {

    public void concurrentMethod() {
        // some logic
        // 数据读取和写入操作可能发生不一致性
    }
}

解决方案:
选择适当的隔离级别,根据业务需求调整,或者使用乐观锁机制。

结论

Spring事务失效可能发生在多种场景下,但通过仔细分析每个场景并采取相应的解决方案,我们可以有效地确保事务的一致性和稳定性。在实际项目中,及时发现并解决事务失效问题对于保障数据完整性至关重要。

本系列旨在介绍Json Schema的常见用法,以及.net实现库Lateapexearlyspeed.Json.Schema的使用


这篇文章将介绍Json Schema中的type关键字,和string类型的常见验证功能。用例基于.net的LateApexEarlySpeed.Json.Schema nuget package。这是新创建的一个 Json Schema在.net下的高性能实现库。


最简单的Json Schema

就像其他各种Schema一样,Json Schema的一个基本且核心的目的是对Json数据进行描述,以便进行验证。Json Schema其实是一个由各种keywords组合而成的“容器”,每个keyword有不同的作用范围和验证功能。一个最简单的Json Schema是空Json object,它代表所有的Json 数据都是有效的 (因为它没有带着任何keyword):

{}

让我们用 .net下的Lateapexearlyspeed.Json.Schema library试一下:

var jsonValidator = new JsonValidator("{}");
ValidationResult validationResult = jsonValidator.Validate("123");

Assert.True(validationResult.IsValid);

除了空Json object, 还可以用true和false分别表示“任何数据都符合”和“任何数据都不符合”:

ValidationResult result = new JsonValidator("true").Validate("123");
Assert.True(result.IsValid);
ValidationResult result = new JsonValidator("false").Validate("123");
Assert.False(result.IsValid);

type 关键字

一般来说,大家用的最多的关键字(keyword)应该是type, 它用来描述数据应该是哪种类型的。比如下面的例子,只允许json数据是string而不能是其他类型:

string schema = """
{ "type": "string" }
""";
var jsonValidator = new JsonValidator(schema);
Assert.True(jsonValidator.Validate("\"abc\"").IsValid);
Assert.False(jsonValidator.Validate("123").IsValid);

type关键字支持如下内容:string,number,integer,object,array,boolean,null。

String

String type用于表示数据是json string type。

"This is string json token."
string schema = """
{ "type": "string" }
""";
var jsonValidator = new JsonValidator(schema);
Assert.True(jsonValidator.Validate("\"abc\"").IsValid);
Assert.False(jsonValidator.Validate("123").IsValid);

长度

对于String json token来说,可以用minLength和maxLength关键字来表示string长度:

string schema = """
{ 
  "type": "string",
  "minLength": 3,
  "maxLength": 5
}
""";
var jsonValidator = new JsonValidator(schema);
Assert.True(jsonValidator.Validate("\"abc\"").IsValid);

ValidationResult result = jsonValidator.Validate("\"ab\"");
Assert.False(result.IsValid);
Assert.Equal("minLength", result.Keyword);
Assert.Equal(ResultCode.StringLengthOutOfRange, result.ResultCode);
Assert.Equal("String instance's length is 2 which is less than '3'", result.ErrorMessage);

正则表达式

正则表达式的关键字是pattern,它用来验证string数据是否匹配要求的pattern.

string schema = """
{ 
  "type": "string",
  "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
}
""";
var jsonValidator = new JsonValidator(schema);
Assert.True(jsonValidator.Validate("\"(888)666666-1212\"").IsValid);

ValidationResult result = jsonValidator.Validate("\"(800)FLOWERS\"");
Assert.False(result.IsValid);
Assert.Equal("pattern", result.Keyword);
Assert.Equal(ResultCode.RegexNotMatch, result.ResultCode);
Assert.Equal("Regex: '^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$' cannot find match in instance: '(800)FLOWERS'", result.ErrorMessage);

字符串格式

有时人们需要表示数据是一些常用的格式,比如是邮箱地址,uri, ip地址,日期时间,GUID 等。虽然可以用正则表达式pattern来手动解决,但json schema还是规定了format关键字来描述一些常用格式,以方便使用。LateApexEarlySpeed.Json.Schema默认支持如下format:

  • uri
  • uri-reference
  • date
  • time
  • date-time
  • email
  • uuid
  • hostname
  • ipv4
  • ipv6
  • json-pointer
  • regex

它们各自的具体含义可参考
官方说明

这里仅用email format来举例子吧:

string schema = """
{ 
  "type": "string",
  "format": "email"
}
""";
var jsonValidator = new JsonValidator(schema);
Assert.True(jsonValidator.Validate("\"hello@world.com\"", new JsonSchemaOptions{ValidateFormat = true}).IsValid);

ValidationResult result = jsonValidator.Validate("\"@world.com\"", new JsonSchemaOptions { ValidateFormat = true });
Assert.False(result.IsValid);
Assert.Equal("format", result.Keyword);
Assert.Equal(ResultCode.InvalidFormat, result.ResultCode);
Assert.Equal("Invalid string value for format:'email'", result.ErrorMessage);

更完整的字符串相关关键字请参考官方json schema specification。


之后的文章会继续介绍Json Schema的其他功能和LateApexEarlySpeed.Json.Schema的使用。


LateApexEarlySpeed.Json.Schema是新的Json Schema的.net library, nuget package下载:
https://www.nuget.org/packages/Lateapexearlyspeed.Json.Schema

github doc repo:
https://github.com/lateapexearlyspeed/Lateapexearlyspeed.JsonSchema.Doc
, 使用中遇到的问题,欢迎发到repo issue这里。