
2.5 数据操作入门
在实际工作中,往往需要对数据(在R中数据通常以数据框形式保存,即data.frame)进行复杂的清洗转化操作,包括创建、添加、删除、插入、排序、过滤、分组、汇总、连接、长宽转换等。这些操作使用频次很高,因此初学者需要在刚入门的时候就掌握这些操作。本节将主要使用tidyfst包来对数据进行操作,这个包语法结构与dplyr类似,而底层则利用data.table来构筑,运行速度快、语法简洁优雅,非常适合初学者使用。同时,本部分还会根据要解决的问题,进行讲解和探讨,并在必要的时候介绍更多的工具包来满足多元化的需求。
2.5.1 文件读写
在数据分析中,第一步就是要把数据导入到软件环境中,通常把这个步骤叫作文件读写(File Input/Output)。在文件读写的过程中,通常会考虑的因素包括:
● 读入和写出是否正确,即读写不能改变原有数据。
● 读写速度是否足够快,高性能读写工具能够减少读写的时间。
● 写出的数据是否足够小,也就是说保存的文件大小应该限制在一定范围内。
● 导出数据格式通用性是否够用,是否能够在其他系统或软件环境中打开。
R语言的文件读写系统非常完备,而且在拓展包的支持下几乎能够读写任意格式的数据。本节将会对主流的数据读写方法进行介绍,并对其性能特点进行解析。
1.csv文件的读写
csv(Comma-Separated Values)的中文名称为逗号分隔值,是一种常见的文件格式,它能够保存R中的数据框。这种文件格式,能够在Excel、Python等其他软件工具中导入导出,是非常流行的通用数据保存格式。在R语言中,可以直接利用基本包内置的read.csv和write.csv函数来读写csv文件,但是它们的功能不够强大,因此这里直接给出相对安全而更快捷的方法:
● 如果文件比较大,使用data.table包的fread函数来读取csv文件。
● 如果文件比较大,使用data.table包的fwrite函数来写出csv文件。
● 如果读取文件出现乱码问题,尝试给fread函数加设参数encoding="UTF-8"。
● 如果写出函数用Excel打开出现乱码问题,尝试使用readr包的write_excel_csv函数来写出。
接下来做一个演示,把内置的iris数据集读出到D盘根目录中。请确保D盘根目录下没有同名文件(文件名称为“iris.csv”)。首先把R的内置数据框iris写出。

D盘根目录下就有了一个名为“iris.csv”的文件,读者可以自行打开查看。然后,使用fread函数把它读回来。

需要注意的是,fread函数返回的是data.table类型的数据,它的本质依然是一个数据框。如果需要处理编码格式问题,可以加设参数:

如果需要在写出的时候让Excel能够自动识别编码格式,可以使用readr包的write_excel_csv函数。

最后,我们直接在R中把这个写出的文件删除掉。

2.二进制文件的读写
如果想要获得更好的读写性能,就需要把数据文件存为二进制格式。在R中,使用readRDS函数和wirteRDS函数可以对任意R对象进行二进制文件的读写,如果要保存和读取多个变量,则可以使用load/save组合。使用二进制文件进行存取,会比其他方法更快,而且保存的文件占用内存也会更加少。在这个基础上,R的扩展包还能够让二进制文件读写的速度再提高一个水平,这里,对其中fst、feather、qs、ao 3个包进行简单介绍和比较。这3个包的各自特点如下。
● fst:保存速度快,而且对文件具有压缩作用,但是只能对数据框进行保存;
● feather:与fst类似,而且输出的文件格式能够被Python进行读写,能够很好地与Python数据科学工作流进行对接;
● qs:与fst类似,但是可以对任意R对象(不仅限于数据框)进行序列化压缩存取。
下面,尝试比较上述3个包的读写速度和保存文件大小,以评估三者的读写效果。如果读者目前还无法了解这些代码的细节,可以跳过过程直接看结论,来指导自己在不同的情况下使用不同的工具。
首先,加载 3个包和其他必要的包。需要说明的是,目前,feather项目已经转移到Apache Arrow中,因此需要在arrow包中对其调用,才能够获得最新也是最佳的性能(https://github.com/wesm/feather)。同时,会调用tidyfst包的import_fst函数和export_fst函数,它们的本质就是fst包的read_fst函数和write_fst函数,但是会把压缩因子调到最大,也就是尽可能地让输出文件变小。

另外,需要构造一个足够大的数据框来测试性能。把iris数据集的第一行复制一千万次,形成一个一千万行的数据框,并保存在a变量中。

因为不同平台具有不同的情况,在此不显示运行结果。在这个例子中,我们获取的数据框大小为1GB,接下来,演示如何构造一个临时的路径来保存它。

观察tf,可以发现它是一个临时文件路径。接下来构建一个测试函数test,把上面构造临时文件路径的方法融入其中。


以下对上面的代码稍作解析。其中system.time函数可以求得运行时间,取出其中的“user.self”属性是用户实际的等待时间(单位为秒);而fileSize函数来自于MODIS包,可以根据文件路径求得文件的实际大小,并设置其大小单位(这里设置为KB)。这个函数会测试read_fun和write_fun这两个函数的运行时间和输出文件大小,最后输出。通过更换读写函数,能够测试不同包的读写效果。我们为函数设置一个class参数,用来进行标注。下面进行测试:

在测试中,最后获得结果如下所示:

可见,文件压缩效果最好的是tidyfst包,在R环境中占用内存1GB的数据,导出后只有约692KB。从读写时间上来看,fst包的效果最好,读取文件只用了约0.19秒,写出文件只用了0.06秒。因此就数据框的读写而言,当前fst包是最佳的选择。在默认条件下,fst包的write_fst函数会把压缩因子(compress参数)设置为 50,这是对读写速度和文件大小的一个权衡。但是如果文件比较大,那么文件本身的传输速度就会减慢(例如我们写出的文件,要存到U盘或者上传到网络)。因此,在tidyfst中直接把compress参数设置为100,以确保输出文件尽可能小。在实际应用中,用户需要根据自己的需求进行设置使用。综合上述试验,可以得到如下的结论:
● 基本包的函数最慢,因此条件允许的情况下应该避免使用。
● 保存数据框的时候尽量使用fst包,它速度快,而且压缩效果最好。
● 如果需要让文件尽可能小,使用tidyfst包的export_fst函数,或把fst包中的write_fst函数的compress参数设为100。
● 如果需要保存数据框以外的R对象,使用qs包的qread和qsave函数。
● 如果需要与Python工作流对接,使用arrow包的read_feather和write_feather函数。
3.其他文件格式的读写
通过各式各样的拓展包,R可以对各种格式的文件进行读写,这些文件包括表格、文本、图像、音频、视频等。这些扩展包中的一些工具不仅仅具备读写各种文件的功能,还能够对读入的数据进行预处理和进一步的分析。接下来,将介绍不同格式文件可以用什么包读写,应该用其中的什么函数读写。
(1)表格数据读写
● readr:能够读写二维表形式的数据,主要支持csv格式的文件读写。经典的读写函数组合为read_csv/write_csv,而根据读写细节的变化,又衍生出read_tsv、read_csv2、write_excel_csv等函数,相关内容可参考官方网址https://github.com/tidyverse/readr。
● rio:可以根据文件扩展名对文件形式进行识别,然后进行读写。主要函数为import/export,支持包括csv、json、xlsx、fst、feather、mat(Matlab文件格式)在内的多种文件格式的读写。在导入多表格文件(如xlsx中的多个工作簿)时,可以使用import_list/export_list函数。更多细节信息参考https://github.com/leeper/rio。
(2)文本数据读写
● readtext:可以读取包括pdf、docx、xml、json在内的各种文本文件,核心函数为readtext。读取后获得的R对象为数据框,一列为文件名,一列为文本内容。获得的数据格式能够很好地利用quanteda包进行后续操作,详细信息见https://github.com/quanteda/readtext。
● textreadr:可以读取包括rtf、html、docx在内的多种文本数据类型,还可以读取路径下的所有文本文件,并展示其文本存储结构。更多内容请参考https://github.com/trinker/textreadr。
(3)图像数据读写
● magick:R中的高级图像处理软件包,可以通过image_read/image_write来对图像文件进行读写,具体信息可参考https://github.com/ropensci/magick。
● imager:基于CImg的R图像处理库,可以快速处理多达4个维度的图像信息(两个空间维度、一个时间或深度维度,一个颜色维度)。在该体系中,可以使用load.image/save.image来进行图像文件的读写,更多信息可参考https://github.com/dahtah/imager。
(4)音频/视频数据读写
● av:可以在R中对音频和视频进行分析的工具,其中read_audio_bin和read_audio_fft两个函数可以对音频数据进行读取,详细信息见https://github.com/ropensci/av。
● seewave:能够实现分析、操作、显示、编辑和合成时间波的功能(特别是音频文件),可以用export和savewav函数进行文件导出。关于音频文件的输入输出,可以参考https://cran.r-project.org/web/packages/seewave/vignettes/seewave_IO.pdf获得进一步的了解。
上面介绍的一些工具包,是R中文件读写拓展包的一部分,事实上R中还有很多功能强大的包能够进行文件信息的提取和写出。例如docxtractr包能够自动提取Word文件中的表格信息(https://github.com/hrbrmstr/docxtractr),openxlsx包能够批量读取Excel文件的多工作簿,进行各种编辑后再批量导出。因此,如果有更多特定需求的时候,可以在网络中进行深度的查询来找到合适的包,从而对特定格式的文件进行读写和编辑。
4.并行读取多个文件
在日常的工作任务中,经常可能遇到需要读取多个数据文件,然后将其合并成一个统一文件的情况。如果文件数量比较多,这个步骤往往会非常耗时,利用并行计算,就能够有效地解决这个问题。在介绍并行操作之前,先对并行计算的概念进行简单的介绍。能够并行化的任务,要符合“Split-Apply-Combine”准则,也就是任务是可分割的,然后再局部分别运行,最后把结果合并在一起。举个例子,任务A是做鸡蛋料理,分为打鸡蛋和煮鸡蛋两个部分;任务B是折一百只千纸鹤。那么,任务A是串行的,如果不先打鸡蛋,就没有鸡蛋可以煮;任务B是并行的,因为可以让5个人每人折20只千纸鹤。要进行并行运算,还对计算机的硬件有一定要求,必须能够支持多线程的计算。目前市面一般的计算机都能够支持,如果核心数量越多,那么并行的效果就越好。设置并行运算具有一定的时间成本,但是如果数据量特别大,这些时间成本基本可以忽略不计。
接下来会讲解如何在R中进行多个文件的并行读取,将会使用future.apply包来进行实现。future.apply包能够对R基本包中的apply族函数进行并行化的部署,而且语法结构与基本包维持一致,非常便捷。
在演示如何进行并行读取之前,先来构造一个文件夹,里面有500个一模一样的csv文件。
首先,在D盘根目录下创建一个名为“test_parallel”的文件夹,读者实际操作时要需要注意D盘中不能有同名的文件夹。

然后,会用iris作为操作数据在该文件夹中写出。一共写出500个csv文件,文件名称为1到500的正整数。写出的时候需要给出文件的绝对路径,因此利用paste0函数来构造文件名称,它能够对字符串进行拼接。

如下所示,在D盘的test_parallel文件夹中就写出了500个csv文件。接下来对其进行并行读取,然后合并成一个数据框。

至此,df中就保存了这500个文件读入后合并的结果。
以上操作的实质是,先利用dir函数获取所有目标文件的路径(注意:需要把full.names参数设置为TRUE,这样才能够获得绝对路径的名称);然后使用plan(multiprocess)这段代码一键设置并行环境;最后,利用future_lapply函数对路径下所有的文件(所有文件路径保存在fn中)进行并行读取(使用fread作为读取函数)。这一步会得到一个列表,列表中包含多个数据框,然后我们使用rbindlist函数将列表中所有的数据框按行合并到一起。
需要注意的是,不是所有的情况都适合使用并行读取,并行读取方式本身也具有一定的风险。例如在并行读取多个文件的过程中,如果其中一个文件的读取出了错误,或者格式不一致,就会导致最后合并的时候出错或无法得到正确结果。但是如果使用串行方式读取,那么就可以把前面正确合并的结果保存下来,并且能够知道出错的文件是哪一个。
实现串行合并也非常简单,操作如下。

all就是合并后获得的数据框。如果中间出错,则可以检查i合并到哪里,而之前合并的结果也会保存在all中,可以对当前出错的文件进行排错或跳过,然后继续进行读取合并。最后,把创建的文件夹删掉。

2.5.2 数据框的检视
对于一个读入的数据框,往往希望尽可能地了解它的结构,包括:这个数据表有几行几列,每一列的数据类型是什么,每一列中是否有缺失值,缺失值的比例是多少,连续型随机变量的分布大概是什么样子的,离散型随机变量不同的变量出现频次是多少,在R的基本包中,有一系列的函数来进行这些基础的数据探索。以R自带的iris数据集为例,如果需要知道它由几行几列构成,则可以用dim函数实现。

如果想要深挖其数据类型,可以使用str函数。

可以发现,iris数据框的前4列为数值型变量,而最后1列为因子型变量。
此外,我们还可以用head和tail函数来观察数据框的前6行和后6行。

如果想要了解每一列的数据分布,可以使用summary函数。对于数值型变量而言,可以显示其四分位数、极值和均值;对于因子变量而言,则会对其分类进行计数。


如果需要快速地从整体层面了解一个数据框,这里推荐使用skimr包的skim函数,能够用最少的代码来获知数据框的方方面面,如下所示。

通过使用skim函数,可以迅速获知数据框的行列数量,并能够了解不同类型的属性分别有多少列,对于不同的变量还会进行更加深入的探索展示。关于skimr包的更多用法,可以参考https://docs.ropensci.org/skimr/articles/skimr.html。
2.5.3 单表操作
所谓单表操作,就是基于单个表格进行的数据操作,包括检索、筛选、排序、汇总等。下面,将会以tidyfst包作为主要工具来说明如何在R中完成这些单表操作。
1.检索
检索就针对用户的需求来提取总数据集一部分进行查阅,一般可以分为行检索与列检索。在行检索中,一般是根据数据条目所在位置进行检索。例如想要查看iris数据表的第 3行,可以使用slice_dt函数进行行检索,具体操作如下。

如果想要查看多列,可以使用向量作为检索内容。

对列进行检索则具有更多灵活的选择。首先,与行检索类似,可以根据列所在的位置,通过select函数来对列进行检索。


其次,还可以通过变量的名称,使用select函数直接对其中的一个或多个变量进行检索。


如果要按照名称选择多列,还可以使用正则表达式的方法。例如要选择列名称中包含“Pe”的列,可以进行如下操作。

与此同时,还可以根据数据类型来选择列,例如如果需要选择所有的因子变量,可以进行如下操作。

如果要进行反向选择,在要去除的内容(可以是变量名、所在列数或正则表达式)前面加负号(“-”)即可。



2.筛选
筛选操作就是要把数据框中符合条件的行筛选出来,在tidyfst包中可以使用filter_dt函数实现。例如要筛选iris数据框中Sepal.Length列大于7的条目,可以操作如下。

在筛选条件中,可以使用与(&)、或(|)和非(!)3种逻辑运算符,来表达复杂的条件关系。例如,想要筛选Sepal.Length大于7且Sepal.Width大于3的条目,可以操作如下。

3.排序
在数据框的操作中,可以根据一个或多个变量对行进行排序。在tidyfst包中,可以利用arrange_dt函数对排序进行实现。例如,如果想要根据Sepal.Length进行排序,可以操作如下。

从结果中可以获知,默认的排序是升序排列。如果需要降序排列,那么要在变量前面加上负号。

同时,可以加入多个变量,从而在第一个变量相同的情况下,根据第二个变量进行排列。


可以看到,当Sepal.Length都等于4.4的时候,条目是根据Sepal.Width进行升序排列的。
4.更新
此处提到的更新是指对某一列进行数据的更新,或者通过计算获得一个新的数据列。在tidyfst包中,可以使用mutate_dt函数对列进行更新。例如,想要让iris数据框中的Sepal.Length列全部加1,具体操作如下。

也可以新增一列,例如要新增一个名称为one的列,这一列的数据为常数1。


如果我们在更新之后,只想保留更新的那些列,可以使用transmute_dt函数。

上面的例子中,我们就仅保留了更新后的两列。如果需要分组更新,可以使用by参数定义分组信息。例如,我们想要把iris数据框中,根据物种进行分组,然后把Sepal.Length的平均值求出来,附在名为“sp_avg_sl”列中,操作方法如下。

5.汇总
汇总,即对一系列数据进行概括的数据操作。求和、求均值、最大值、最小值,均可以视为汇总操作。可以使用tidyfst包的summarise_dt函数实现各类汇总。例如,我们想求iris数据框中Sepal.Length的均值,可以进行如下操作。

在上面的操作中,把最终输出的列名称设定为“avg”。
在实际应用中,往往需要进行分组汇总操作,这可以通过设定summarise_dt函数的by参数进行实现。例如,想知道每个物种Sepal.Length的均值,可以这样操作。

2.5.4 多表操作
在实际工作中,很多时候不仅是要对一个表格进行操作,而是要进行多个表格数据的整合归并。在tidyfst的工作流中,有3种处理多表操作的模式,包括更新型连接、过滤型连接和集合运算操作,下面一一进行介绍。
1.更新型连接
更新型连接(Mutating joins)是根据两个表格中的共有列进行匹配,然后完成合并的过程,可以分为内连接、外连接、左连接和右连接4种。下面,构造一个数据集来对4种连接进行说明。


在上面的代码中,构造了两个数据框(df1和df2)。其中,df1中有消费者的ID号和他们买了什么产品;df2中则包含了消费者ID号和他们所在的地点(省份)。
(1)内连接操作
内连接又称为自然连接,是根据两个表格某一列或多列共有部分进行连接的过程。以下对之前构造的两个表格进行内连接,可以使用inner_join_dt函数完成。

通过上面的结果,我们可以看到,如果没有设定连接的列,inner_join_dt函数会自动识别两个数据框中的同名列进行匹配。在内连接中,会找到df1和df2同名列CustomerId中完全匹配的条目进行连接。如果希望直接设定合并的列,可以使用by参数来特殊指定。

(2)全连接操作
在进行内连接的时候,不匹配的条目会全部消失。如果想要保留这些条目,可以使用全连接函数full_join_dt,它会保留两个表格中所有的条目,而没有数值的地方,并自动填充缺失值,如下所示。


在上面的结果中,可以看到,在df2中没有消费者1、3、5的地区数据,因此填充了缺失值NA。
(3)左连接和右连接
左连接和右连接是互为逆运算的两个操作(左连接函数:left_join_dt,右连接函数:right_join_dt),左连接会保留左边数据框的所有信息,但是对于右边的数据框,则只有匹配的数据得以保留,不匹配的部分会填入缺失值。下面代码是左连接和右连接的操作演示。

(4)指定连接表格的共有列
有的时候,需要将两个或以上的列进行连接,可以通过设置by参数来完成。下面举例说明。


在上面的代码中,获得了workers和position2两个数据框。其中,workers数据框中的name为工人名称,而position2数据框中的worker列为数据名称,因此两者合并的时候需要根据名称不同的列进行匹配。

得到的结果会保留第一个出现的数据框的名称,即workers数据框中的name,而第二个数据框position2的worker列则会消失。
2.过滤型连接
过滤型连接(Filtering joins)是根据两个表格中是否有匹配内容来决定一个表格中的观测是否得以保留的操作,在tidyfst包中可以使用anti_join_dt函数和semi_join_dt函数实现。其中,anti_join_dt函数会保留第一个表格有而第二个表格中没有匹配的内容,而semi_join_dt函数则会保留第一个表格有且第二个表格也有的内容。但是,第二个表格中非匹配列的其他数据不会并入生成表格中。

3.集合运算操作
在R中,每个向量都可以视为一个集合,基本包提供了intersect/union/setdiff函数来求集合的交集、并集和补集,并可以使用setequal函数来查看两个向量是否全等。以下对集合运算做一个简单的操作演示。

在tidyfst中,通过data.table包中的集合运算函数,包括union_dt、intersect_dt、setdiff_dt和setequal_dt,可以直接对数据框进行对应的集合运算操作。需要注意的是,所求数据框需要有相同的列名称。下面利用iris的前3列来做一个简单的演示。


这些函数都有all参数,可以调节其对重复值的处理。例如,如果取并集的时候不需要去重,那么可以设置“all=TRUE”。

2.5.5 缺失值处理
在数据处理的时候,难免会遇到数据集包含缺失值的情况。这有可能是因为人工失误引起的,也可能是系统故障导致的。根据缺失值分布的特征,通常可以把缺失情况分为 3类:完全随机缺失(Missing Completely At Random,MCAR)、随机缺失(Missing At Random,MAR)、非随机缺失(Missing Not At Random,MNAR)。常常需要根据数据缺失的分布特征,来推断数据缺失的真实原因,从而考虑如何处理这些缺失值。一般而言,缺失值的处理有3种手段:删除、替换、插值。下面,将会介绍如何利用tidyfst包在R中实现这3种缺失值处理。
1.缺失值删除
删除缺失值可能是缺失值处理中最为简单粗暴的方法,在样本量非常大的时候,直接删除缺失值往往对结果影响不大,而实现的成本又较低。在tidyfst包中,有3个函数能够对包含缺失值的数据进行直接删除。
●drop_na_dt:行删除操作,如果一列或多列中包含任意缺失值,对整行进行删除。
●delete_na_cols:列删除操作,如果数据框中任意列的缺失值比例或数量超过一个阈值,则将整个列删除掉。
●delete_na_rows:行删除操作,如果数据框中任意行的缺失值比例或数量超过一个阈值,则将整个列删除掉。
下面举例演示缺失值删除的操作。首先要构建一个缺失值数据框。

所构造的数据框中,第一、二、三、四列分别有1、2、0和3个缺失值。如果想要删除col2中包含缺失值的条目,可以使用drop_na_dt函数。

如果想要删除缺失值大于等于2个或缺失比例大于等于50%的列,则可以操作如下。


如果想要删除缺失值大于等于2个或缺失比例大于等于50%的行,则可以操作如下。

2.缺失值替换
缺失值替换就是把缺失的部分用指定数据进行替代的过程,在tidyfst中可以使用replace_na_dt函数进行实现。例如,要将上面所构造数据框的col1列缺失值替换为-99,可以操作如下。

也可以同时对col1和col4同时进行这项操作,如下所示。

如果不设定替换列,则默认替换所有的列。但需要注意的是,每一个列的类型都不一样,因此在替换的时候需要保证替换列数据类型的一致性。

3.缺失值插值
与替换不同,缺失值的插值需要根据列中数据的关系来对要插入的值进行一定的计算,然后再填入到缺失的部分。在tidyfst包中,可以完成插值的函数包括。
●fill_na_dt:把缺失值替换为其最临近的上一个或下一个观测值。
●impute_dt:根据列汇总数据(平均值、中位数、众数或其他)来对缺失值进行插补。
以下是fill_na_dt的操作演示,它的原理非常简单。


有的时候,会希望用均值来对数值型的变量进行插值,可以操作如下。

如果把.func设置为“mode”和“median”,就可以分别利用其众数和中位数进行插值。在此不再专门说明,读者可以自行操作。
2.5.6 长宽数据转换
表格的长宽转换是一个经典的数据操作,它可以自由地改变二维表的结构。例如对于iris数据框,可以将其转化为任意的其他结构。下面我们给每一朵花都进行编号,然后直接转为长表格式,如下所示。


在输出的结果中,可以看到3列,分别为id、name和value。以第一行为例,它表示id为1的花朵的Sepal.Length属性值为5.1。这种长宽变换能够让我们自如地对数据框结构进行重塑,从而获得满足分析要求的数据结构。
1.宽表转长表
在tidyfst包中,可以使用longer_dt函数来把宽表转化为长表,需要定义的核心参数是数据框和分组列。分组列就是不参与长宽变换的列,而其他列名称将会统统聚合起来成为一列。下面我们进行一个简单的演示。首先进行数据准备。如下所示,这是一个10行4列的数据框,一列为时间time,其余3列为数值列。

接下来,我们要使用longer_dt函数把它转化为长表。


可以发现,列名称X、Y、Z都在name列中,其值则在value列中。这些列名称可以通过更改name和value参数重新被定义,如下所示。


2.长表转宽表
实现长表转为宽表的函数为wider_dt。长表转为宽表是宽表转长表的逆运算,因此需要定义的核心参数也有相仿之处,需要知道数据框、分组列的信息,同时需要知道名称列(name)和数值列(value)分别来自哪里。以上面生成的longer_table为例,尝试把它进行还原,具体操作如下。


在上面的代码中,把time定义为分组列,然后以字符形式来定义哪一列是名称列,哪一列是数值列。在宽表转长表的时候,name和value如果不自定义,就会自动给名称列命名为“name”,给数值列命名为“value”;但是在长表转为宽表的时候,则必须手动进行定义,否则计算机无法自动识别。