这里是佳奥!我们开始解析作者提供的表达矩阵吧!
本次的代码在这里:
https://github.com/jmzeng1314/scRNA_smart_seq2
下载.zip后解压,开始下游分析!
rm(list = ls())
Sys.setenv(R_MAX_NUM_DLLS=999) ##Sys.setenv修改环境设置,R的namespace是有上限的,如果导入包时超过这个上次就会报错,R_MAX_NUM_DLLS可以修改这个上限
options(stringsAsFactors = F) ##options:允许用户对工作空间进行全局设置,stringsAsFactors防止R自动把字符串string的列辨认成factor
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager") ##判断是否存在BiocManager包,不存在的话安装
library(BiocManager)
BiocManager::install(c( 'scran'),ask = F,update = F)
BiocManager::install("TxDb.Mmusculus.UCSC.mm10.knownGene",ask = F,update = F)
BiocManager::install("org.Mm.eg.db",ask = F,update = F)
BiocManager::install("genefu",ask = F,update = F)
BiocManager::install("org.Hs.eg.db",ask = F,update = F)
BiocManager::install("TxDb.Hsapiens.UCSC.hg38.knownGene",ask = F,update = F)
install.packages("ggfortify")
install.packages("FactoMineR")
install.packages("factoextra")
上面的是rawCounts表达矩阵,下面的是Normalized归一化后的表达矩阵。
rpkm:单纯比较基因reads数量是没有意义的(测序量不一样,基因长度不一样,测序文库大小不一样),所以需要归一化。

##载入表达矩阵
a=read.table('../GSE111229_Mammary_Tumor_fibroblasts_768samples_rawCounts.txt.gz',
header = T ,sep = '\t') ##把表达矩阵文件载入R,header=T :保留文件头部信息,seq='\t'以tap为分隔符
##每次都要检测数据
a[1:6,1:4] #对于a矩阵取第1~6行,第1~4列
##读取RNA-seq的 counts 定量结果,表达矩阵需要进行简单的过滤
dat=a[apply(a,1, function(x) sum(x>1) > floor(ncol(a)/50)),]
#筛选表达量合格的行(基因), 列(细胞)数不变
##ncol()返回矩阵的列数值;floor()四舍五入取整数;
##function() 定义一个函数;sum()求和
#上面的apply()指令代表对矩阵a进行行计算,判断每行表达量>1的样本总个数,并筛选出细胞表达量合格的基因(行)
#第一个参数是指要参与计算的矩阵——a
#第二个参数是指按行计算还是按列计算,1——表示按行计算,2——按列计算;
#第三个参数是指具体的运算参数,定义一个函数x(即表达量为x)
#对每行中x>1的列(即样本数)求和,即得出的是每行中表达量大于1的样本数,
#然后再筛选出大于floor(ncol(a)/50)的行,这样的行(基因)的细胞表达量才算合格
#因为2%的细胞有表达量,所以对于768个细胞样本,每个行(基因)在细胞中的表达至少要有15.36(约等于15)个样本表达才算合格
## 2 % 的细胞有表达量
##这里是演示归一化如何计算的
dat[1:4,1:4]
sum(dat[,3])
# 0610007P14Rik in SS2_15_0048_A5
log2(18*1000000/sum(dat[,3])+1)
## 18 -- > 6.459884 ## SS2_15_0048_A5
##归一化的一种选择,这里是CPM(count-per-million,每百万碱基中每个转录本的count值)
##CPM只对read count相对总reads数做了数量的均一化,去除文库大小差异。
dat=log2(edgeR::cpm(dat)+1)
dat[1:4,1:4]
SS2_15_0048_A3 SS2_15_0048_A6 SS2_15_0048_A5 SS2_15_0048_A4
0610007P14Rik 0 0 6.459884 6.313884
0610009B22Rik 0 0 0.000000 0.000000
0610009L18Rik 0 0 0.000000 0.000000
0610009O20Rik 0 0 2.544699 3.025273
##下面介绍一下 dist 函数
x=1:10
y=2*x
z=rnorm(10)
tmp=data.frame(x,y,z)
dist(tmp)
head(tmp)
dist(t(tmp))
cor(tmp)
dist(t(scale(tmp)))
##可以看到dist函数计算样本直接距离和cor函数计算样本直接相关性,是完全不同的概念。虽然我都没有调它们两个函数的默认的参数。
# 总结:
# - dist函数计算行与行(样本)之间的距离
# - cor函数计算列与列(样本)之间的相关性
# - scale函数默认对每一列(样本)内部归一化
# - 计算dist之前,最好是对每一个样本(列)进行scale一下
##层次聚类,近800细胞。
##原始表达矩阵转置后,细胞在行,所以计算的是细胞与细胞之间的距离。
hc=hclust(dist(t(dat))) ##样本间层次聚类
## statquest 有详细讲解背后的统计学原理。
class(hc)
?plot.hclust
## 查看说明书。
plot(hc,labels = FALSE)
#t:矩阵转置,行转列,列转行
#分类时常常需要估算不同样本之间的相似性(Similarity Measurement)
#这时通常采用的方法就是计算样本间“距离”(Distance)。
#dist函数是R语言计算距离的主要函数。dist函数可以计算行与行两两间的距离。
#所以之前的矩阵里面行是基因,转置后行是样本,因为我们要计算样本与样本之间的距离。
#dist()函数计算变量间距离
#hclust函数用来层次聚类
clus = cutree(hc, 4)##对hclust()函数的聚类结果进行剪枝,即选择输出指定类别数的系谱聚类结果。
group_list= as.factor(clus)##转换为因子属性
table(group_list)##统计频数
group_list
1 2 3 4
312 300 121 35
##提取批次信息
colnames(dat) #取列名
library(stringr)
plate=str_split(colnames(dat),'_',simplify = T)[,3] #取列名,以'_'号分割,提取第三列。
#str_split()函数可以分割字符串
table(plate)
n_g = apply(a,2,function(x) sum(x>1)) #统计每个样本有表达的有多少行(基因)
# 这里我们定义, reads数量大于1的那些基因为有表达,一般来说单细胞转录组过半数的基因是不会表达的。
# 而且大部分单细胞转录组技术很烂,通常超过75%的基因都没办法检测到。
df=data.frame(g=group_list,plate=plate,n_g=n_g) #新建数据框(细胞的属性信息)
##样本为行名,列分别为:样本分类信息,样本分组,样本表达的基因数【注意:不是表达量的和,而是种类数或者说个数】
df$all='all' #添加列,列名为"all",没事意思,就是后面有需要
metadata=df
save(a,dat,df,file = '../input.Rdata')
##保存a,dat,df这变量到上级目录的input.Rdata
##因为另外一个项目也需要使用这个数据集,所以保存到了上级目录。
##查看一下n_g
hist(n_g)
hist(n_g,breaks = 30)

rm(list = ls())
options(stringsAsFactors = F)
a=read.table('../GSE111229_Mammary_Tumor_fibroblasts_768samples_rpkmNormalized.txt.gz',header = T ,sep = '\t')
##把表达矩阵文件载入R,header=T :保留文件头部信息,seq='\t'以tap为分隔符
##每次都要检测数据
a[1:6,1:4] #对于a矩阵取第1~6行,第1~4列
## 读取RNA-seq的 counts 定量结果,表达矩阵需要进行简单的过滤
dat=a[apply(a,1, function(x) sum(x>0) > floor(ncol(a)/50)),] #筛选表达量合格的行,列数不变
dat[1:4,1:4]
#层次聚类
hc=hclust(dist(t(log(dat+0.1)))) ##样本间层次聚类
##如果是基因聚类,可以选择 wgcna 等算法
## statquest
plot(hc,labels = F)
clus = cutree(hc, 4) #对hclust()函数的聚类结果进行剪枝,即选择输出指定类别数的系谱聚类结果。
group_list= as.factor(clus) ##转换为因子属性
table(group_list) ##统计频数
group_list
1 2 3 4
344 189 217 18

##提取批次信息
colnames(dat) #取列名
library(stringr)
plate=str_split(colnames(dat),'_',simplify = T)[,3] #取列名,以'_'号分割,提取第三列。
#str_split()函数可以分割字符串
table(plate)
n_g = apply(a,2,function(x) sum(x>0)) #统计每个样本有表达的有多少行(基因)
# 这里我们定义, reads数量大于1的那些基因为有表达,一般来说单细胞转录组过半数的基因是不会表达的。
# 而且大部分单细胞转录组技术很烂,通常超过75%的基因都没办法检测到。
df=data.frame(g=group_list,plate=plate,n_g=n_g) #新建数据框(细胞的属性信息)
##(样本为行名,列分别为:样本分类信息,样本分组,样本表达的基因数【注意:不是表达量的和,而是种类数或者说个数】)
df$all='all' #添加列,列名为"all",后面有需要
metadata=df
save(dat,metadata,file = '../input_rpkm.Rdata') #保存dat,df这变量到上级目录的input_rpkm.Rdata
##因为另外一个项目也需要使用这个数据集,所以保存到了上级目录。
初步处理了表达矩阵,我们开始正式的分析探索。
文章的图都是基于rpkm值来画的。
load(file = '../input.Rdata')
a[1:4,1:4]
head(df) #head()函数显示操作前面的信息,默认前6行
## 载入第0步准备好的表达矩阵,及细胞的一些属性(hclust分群,plate批次,检测到的基因数量)
##注意:变量a是原始的counts矩阵,变量dat是log2CPM后的表达量矩阵。
group_list=df$g #'$'符,取列,取metadata矩阵的g列,取出层级聚类信息
table(group_list) ##这是全部基因集的聚类分组信息
cg=names(tail(sort(apply(dat,1,sd)),100)) ##取表达量标准差最大的100行的行名
#这个前面演示过一次,dat=a[apply(a,1, function(x) sum(x>1) > floor(ncol(a)/50)),] #筛选表达量合格的行,列数不变
#对dat矩阵每行求标准差,排序,取最后的100行,并获得后100行的行名(探针名)
#sd()求标准差,对dat矩阵每一行的counts求标准差
#sort()函数,排序;
#tail()函数,显示操作对象后面的信息,默认后6行,这里设定取后100行
#names()函数,获取或设置对象的名称
library(pheatmap)
##画热图,针对top100的sd的基因集的表达矩阵,没有聚类分组
pheatmap(dat[cg,],show_colnames =F,show_rownames = F)
pheatmap(dat[cg,],show_colnames =F,show_rownames = F,
filename = 'all_cells_top_100_sd.png')

##继续针对top100的sd的基因集的表达矩阵,归一化,分组画图
##这部分是示例,归一化不改变数据性质,但是改变数据分布(值域)
x=1:10;plot((x))
scale(x);plot(scale(x))
#scale()函数去中心化和标准化
n=t(scale(t(dat[cg,])))
##对每个探针的表达量进行去中心化和标准化
n[n>2]=2 #矩阵n中归一化后,大于2的项,赋值使之等于2(相当于设置了一个上限)
n[n<-2]= -2 #小于-2的项,赋值使之等于-2(相当于设置了一个下限)
n[1:4,1:4]
SS2_15_0048_A3 SS2_15_0048_A6 SS2_15_0048_A5 SS2_15_0048_A4
Kitl 0.9165638 1.2684450 1.4703938 0.6289595
Mmp11 -2.0000000 0.3807272 0.5857658 0.1026709
Atad1 0.4275134 -1.0657421 0.8755671 -1.0657421
Btg2 0.7810682 0.9129418 0.7773478 1.4196704
#pheatmap(n,show_colnames =F,show_rownames = F)
ac=data.frame(g=group_list) #制作细胞(样本)分组矩阵
rownames(ac)=colnames(n) ##ac的行名(样本名)等于n的列名(样本名)
##判断分组矩阵的行(样本数)和表达矩阵的列(样本数)是否相等
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac)
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac,
filename = 'all_cells_top_100_sd_cutree1.png')

##针对top100的sd的基因集的表达矩阵 重新进行 聚类并且分组
n=t(scale(t(dat[cg,])))
n[n>2]=2
n[n<-2]=-2
n[1:4,1:4]
##这个聚类分组只是对top100的sd的基因集,从这里开始
hc=hclust(dist(t(n)))
clus = cutree(hc, 4)
group_list=as.factor(clus)
table(group_list) ##这个聚类分组信息是针对top100的sd的基因集的,和针对全部基因集的分组结果不一样
table(group_list,df$g) ## 其中 df$g 是前面步骤针对全部表达矩阵的层次聚类结果
##下面针对本次挑选100个基因的表达矩阵的层次聚类结果进行热图展示
ac=data.frame(g=group_list)
rownames(ac)=colnames(n)
pheatmap(n,show_colnames = F,show_rownames = F,
annotation_col=ac)
pheatmap(n,show_colnames = F,show_rownames = F,
annotation_col=ac,
filename = 'all_cells_top_100_sd_cutree_2.png')
dev.off() ##关闭画板
##先对整个基因集聚类分组,再对top100的sd的基因集画图
##和选取top100的sd的基因集,聚类分组再画图,结果聚类分组信息不同
##两次的聚类分组信息不同,画出的图不同

##防止下面操作把数值搞坏的一个备份
dat_back=dat
dat=dat_back ##表达矩阵数据
dat[1:4,1:4]
dat=t(dat)
dat=as.data.frame(dat) ##转换为数据框
dat=cbind(dat,group_list ) ##cbind()合并列(横向追加);添加分组信息
dat[1:4,1:4]
## 表达矩阵可以随心所欲的取行列,基础知识需要打牢。
dat[1:4,12197:12199]
dat[,ncol(dat)] #ncol()列,返回列长值
table(dat$group_list)
library("FactoMineR")
library("factoextra")
# The variable group_list (index = ) is removed
# before PCA analysis
## 这里的PCA分析,被该R包包装成一个简单的函数,复杂的原理后面讲解。
dat.pca <- PCA(dat[,-ncol(dat)], graph = FALSE) #'-'表示“非”
fviz_pca_ind(dat.pca,repel =T,
geom.ind = "point", # show points only (nbut not "text")只显示点不显示文本
col.ind = dat$group_list, # color by groups 颜色组
# palette = c("#00AFBB", "#E7B800"),
addEllipses = TRUE, # Concentration ellipses 集中成椭圆
legend.title = "Groups")
## 事实上还是有很多基因dropout非常严重。
ggsave('all_cells_PCA.png')

rm(list = ls())
options(stringsAsFactors = F)
load(file = '../input_rpkm.Rdata')
# a[1:4,1:4]
dat[1:4,1:4]
head(metadata) #head()函数显示操作前面的信息,默认前6行
##载入第0步准备好的表达矩阵,及细胞的一些属性(hclust分群,plate批次,检测到的基因数量)
##注意 变量a是原始的counts矩阵,变量 dat是log2CPM后的表达量矩阵。
group_list=metadata$g #'$'符,取列,取metadata矩阵的g列,取出层级聚类信息
table(group_list) ##这是全部基因集的聚类分组信息
cg=names(tail(sort(apply(dat,1,sd)),100)) ##取表达量标准差最大的100行的行名
# 这个前面演示过一次,dat=a[apply(a,1, function(x) sum(x>1) > floor(ncol(a)/50)),] #筛选表达量合格的行,列数不变
#对dat矩阵每行求标准差,排序,取最后的100行,并获得后100行的行名(探针名)
#sd()求标准差,对dat矩阵每一行的counts求标准差
#sort()函数,排序;
#tail()函数,显示操作对象后面的信息,默认后6行,这里设定取后100行
#names()函数,获取或设置对象的名称
library(pheatmap)
##后面要画热图,需要取对数,不然会被最大值覆盖掉
mat=log2(dat[cg,]+0.01)
##画热图,针对top100的sd的基因集的表达矩阵,没有聚类分组
pheatmap(mat,show_colnames =F,show_rownames = F)
pheatmap(mat,show_colnames =F,show_rownames = F,
filename = 'all_cells_top_100_sd.png')

上图还是难以解释某些基因在细胞中的表达量是高还是低,难以区分(横向看颜色都差不多)。所以我们并不想知道原始表达量的高低,我们想知道该基因在不同样本表达的高低。
##针对top100的sd的基因集的表达矩阵,归一化,分组画图
x=1:10;plot((x))
scale(x);plot(scale(x))
n=t(scale(t( mat )))
#scale()函数去中心化和标准化
##对每个探针的表达量进行去中心化和标准化
n[n>2]=2 #矩阵n中归一化后,大于2的项,赋值使之等于2(相当于设置了一个上限)
n[n<-2]=-2 #小于-2的项,赋值使之等于-2(相当于设置了一个下限)
n[1:4,1:4]
# pheatmap(n,show_colnames =F,show_rownames = F)
ac=data.frame(g=group_list) #制作细胞(样本)分组矩阵
rownames(ac)=colnames(n) ##ac的行名(样本名)等于n的列名(样本名)
##判断分组矩阵的行(样本数)和表达矩阵的列(样本数)是否相等
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac)
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac,
filename = 'all_cells_top_100_sd_cutree1.png')
dev.off() ##关闭画板

重新进行分组
## 针对top100的sd的基因集的表达矩阵 进行重新 聚类并且分组。
n=t(scale(t( mat )))
n[n>2]=2
n[n<-2]= -2
n[1:4,1:4]
##这个聚类分组只是对top100的sd的基因集
hc=hclust(dist(t(n)))
clus = cutree(hc, 4)
group_list=as.factor(clus)
table(group_list) ##这个聚类分组信息是针对top100的sd的基因集的,和针对全部基因集的分组结果不一样
table(group_list,metadata$g) ## 其中 metadata$g 是前面步骤针对全部表达矩阵的层次聚类结果。
## 下面针对本次挑选100个基因的表达矩阵的层次聚类结果进行热图展示。
ac=data.frame(g=group_list)
rownames(ac)=colnames(n)
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac)
pheatmap(n,show_colnames =F,show_rownames = F,
annotation_col=ac,
filename = 'all_cells_top_100_sd_cutree_2.png')
dev.off() ##关闭画板

dat_back=dat ##防止下面操作把数值搞坏的一个备份
dat=dat_back ##表达矩阵数据
dat[1:4,1:4]
dat=t(dat)
dat=as.data.frame(dat) ##转换为数据框
dat=cbind(dat,group_list ) ##cbind()合并列(横向追加);添加分组信息
dat[1:4,1:4]
dat[1:4,12197:12199]
dat[,ncol(dat)] #ncol()列,返回列长值
table(dat$group_list)
library("FactoMineR")
library("factoextra")
# The variable group_list (index = ) is removed
# before PCA analysis
## 这里的PCA分析,被该R包包装成一个简单的函数,复杂的原理后面讲解。
dat.pca <- PCA(dat[,-ncol(dat)], graph = FALSE) #'-'表示“非”
fviz_pca_ind(dat.pca,repel =T,
geom.ind = "point", # show points only (nbut not "text")只显示点不显示文本
col.ind = dat$group_list, # color by groups 颜色组
# palette = c("#00AFBB", "#E7B800"),
addEllipses = TRUE, # Concentration ellipses 集中成椭圆
legend.title = "Groups")
## 事实上还是有很多基因dropout非常严重。
ggsave('all_cells_PCA.png')

检查两个表达矩阵就到这里。
下一篇我们开始复现第一张图——平均表达量以及变异系数相关散点图。
我们下一篇再见!
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
我想从then子句中访问case语句表达式,即food="cheese"casefoodwhen"dip"then"carrotsticks"when"cheese"then"#{expr}crackers"else"mayo"end在这种情况下,expr是食物的当前值(value)。在这种情况下,我知道,我可以简单地访问变量food,但是在某些情况下,该值可能无法再访问(array.shift等)。除了将expr移出到局部变量然后访问它之外,是否有直接访问caseexpr值的方法?罗亚附注我知道这个具体示例很简单,只是一个示例场景。 最佳答案
我在Ruby中遇到了一个有趣的表达式:a||="new"表示如果没有定义a,则将"new"值赋给a;否则,a将保持原样。在进行一些数据库查询时很有用。如果设置了该值,我不想触发另一个数据库查询。所以我在Python中尝试了类似的思路:a=aifaisnotNoneelse"new"失败了。我认为这是因为如果未定义a,则无法在Python中执行“a=a”。所以我能得出的解决方案是检查locals()和globals(),或者使用try...except表达式:myVar=myVarif'myVar'inlocals()and'myVar'inglobals()else"new"或try:
我希望Ruby的解析器会进行这种微不足道的优化,但似乎并没有(谈到YARV实现,Ruby1.9.x、2.0.0):require'benchmark'deffib1a,b=0,1whileb由于这两种方法除了在第二种方法中使用预定义常量而不是常量表达式外是相同的,因此Ruby解释器似乎在每个循环中一次又一次地计算幂常数。是否有一些Material说明为什么Ruby根本不进行这种基本优化或只在某些特定情况下进行? 最佳答案 很抱歉给出了另一个答案,但我不想删除或编辑我之前的答案,因为它下面有有趣的讨论。正如JörgWMittag所说,
今天我在我的Rails控制台中尝试了一些东西,这发生了,2.0.0p247:009>Date.today-29.days=>Fri,07Feb20142.0.0p247:010>Date.today-29.days=>Thu,09Jan2014我很困惑。我可以看到我缺少一些基本的东西。但这让我印象深刻!谁能解释为什么会这样? 最佳答案 实际发生的是这样的:Date.today(-29.days)#=>Fri,07Feb2014today有一个名为start的可选参数,默认为Date::ITALY。Anoptionalargument
我知道在Ruby中,几乎所有东西都是表达式。即使是其他语言中的if-else语句、case语句、赋值语句、loop语句在Ruby中也是表达式。所以我想从Ruby的角度了解,statement和expression有什么区别? 最佳答案 Ruby中表达式和语句没有区别。一切都计算为一个值,所以一切都是表达式。 关于ruby-Ruby中的语句和表达式有什么区别?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.c
最近在工作中,看到一些新手测试同学,对接口测试存在很多疑问,甚至包括一些从事软件测试3,5年的同学,在聊到接口时,也是一知半解;今天借着这个机会,对接口测试做个实战教学,顺便总结一下经验,分享给大家。计划拆分成4个模块跟大家做一个分享,(接口测试、接口基础知识、接口自动化、接口进阶)感兴趣的小伙伴记得关注,希望对你的日常工作和求职面试,带来一些帮助。注:文章较长有5000多字,希望小伙伴们认真看完,当然有些内容对小白同学不是太友好,如果你需要详细了解其中的一些概念或者名词,请在文章之后留言,后续我将针对大家的疑问,整理输出一些大家感兴趣的文章。随着开发模式的迭代更新,前后端分离已不是新的概念,
(irb)a,b=5a=>5b=>nil不应该反过来吗?这里到底发生了什么? 最佳答案 在我写这篇文章时,我的同事发现了原因:Ruby将a,b=5视为a,b=5,nil在Python3中,抛出一个TypeError。 关于ruby-on-rails-为什么表达式"a,b=5"在Ruby中将a设置为5,而将b设置为nil?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/314621
根据AWSDocs:Anupdateexpressionconsistsofoneormoreclauses.EachclausebeginswithaSET,REMOVE,ADDorDELETEkeyword.Youcanincludeanyoftheseclausesinanupdateexpression,inanyorder.However,eachactionkeywordcanappearonlyonce.我无法在一个update_expression中获得正确的SET和REMOVE语法:params={key:{'id'=>{s:'123'}},table_name:'c
我有两个变量a和b。我想将a和b都与一个值进行比较,例如10。我可以这样做:10==a&&10==b但是,我想知道是否有任何方法可以将它写成一个表达式?(例如像a==b==10) 最佳答案 [a,b,3].all?{|x|x==10}但在这种情况下[].all?{|x|x==10}也会返回true 关于ruby-将多个变量与单个表达式中的值进行比较,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/qu