模拟测试数据的生成方法
【摘要】
许多程序员都头疼测试数据的模拟,一个是要逼真,另一个需要数据量,不逼真往往导致一些 bug 测不出来,数据量不够则无法发现性能问题,这篇文章给出了很好的解决办法。
1)、应用系统或软件产品一般都需要进行不同阶段的验证工作,包括原型功能论证、功能测试、性能测试等,这些测试、论证场景都可能涉及到测试数据的准备。
2)、根据用户的业务需求、数据预置约束条件、数据间层级关联等条件,生成对应的模拟验证数据。一般来说,按照用户要求模拟数据应该做到:数据量可控、充分的随机性、保持数据间特定的逻辑关联关系(包括直接关联或隐含关联)。
3)、本文我们将介绍一个方便、灵活的模拟数据生成工具——集算器。
去乾学院看个究竟吧! 模拟测试数据的生成方法
模拟测试数据的生成方法
应用系统或软件产品一般都需要进行不同阶段的验证工作,包括原型功能论证、功能测试、性能测试等,这些测试、论证场景都可能涉及到测试数据的准备。测试数据有时可以直接复用历史数据,但很多情况下,基于历史数据建立的测试数据可能会出现内容缺失不全、数据量级不够、数据涉密不能导出、数据已加密无法参与计算等情况,这时就需要根据用户的业务需求、数据预置约束条件、数据间层级关联等条件,生成对应的模拟验证数据。一般来说,按照用户要求模拟数据应该做到:数据量可控、充分的随机性、保持数据间特定的逻辑关联关系(包括直接关联或隐含关联)。
单数据表是最常见的数据模拟情况,这种表由主键和普通字段组成,也可以由无主键的普通字段组成。在单数据表的基础上,可以进一步进行多数据表的关联模拟,关联关系可以归类分为:外键关联、同维关联、主子关联。对于关联数据生成的原则具体可参见《怎样生成有关联的测试数据》一文。对于已经生成的数据,则可以参考《优化 Join运算的系列方法》对模拟生成的数据进行更进一步地深入查询和分析处理。
一般来说,单数据表模拟数据的难点是对数据表字段内容的灵活生成。而多数据表模拟则是在此基础上考虑关联关系(一层或多层)的生成,尤其是需要保证多数据表在进行关联过滤之后,还能够有充足、有效的结果数据,从而满足数据的关联运算或是其它展现需求。实际情况中,通过编程生成这些模拟验证数据的难点通常是数据间复杂的逻辑关联关系。
本文中,我们将介绍一个方便、灵活的模拟数据生成工具——集算器。集算器是一款面向应用程序员和数据分析员,专注于结构化数据分析与处理的快速开发工具,是一套基于 Java 解释执行的动态语言,采用了先进的计算模型和设计思想,让开发更易于实现、性能更好。同时,集算器还具备完备的类库和轻量级架构,足以让数据模拟生成更加灵活和高效。
一、 引言
集算器具有跨平台、无框架、易部署的特点,仅需要安装有JAVA虚拟机的操作系统即可,特别是对于数据模拟生成的过程,可以随时分步调试查看中间过程数据。
集算器使用SPL程序语言。SPL(Structured Process Language)是一种面向(半)结构化数据计算的程序设计语言,能够满足复杂处理和过程计算的数据处理需求。
同时,集算器还可以直接将模拟生成的数据落地在本地磁盘的二进制“集文件”(集算器自定义的一种文件格式)中,免去繁琐的安装数据库的操作,这也符合模拟测试相对临时性的工作特点。集文件本身具有使用简单、可追加、支持大数据、可分段并行等特点,而且很容易转成其它格式,如数据库、文本等。当然,集算器自身也支持直接或通过集文件将模拟数据导入到任意目标数据库中,所需的具体外部库的使用可以参见《外部库》函数参考章节。
为了方便叙述和验证,本文默认模拟生成的数据均落地在本地集文件中。
1、 为什么使用集文件?
在用集算器生成模拟数据时,常用两种文件格式:文本文件和集文件。
文本是各种数据平台 / 数据库都支持的文件格式,具有良好的通用性。但文本文件的查询性能较差,占用磁盘空间也较大,而且缺少字段的数据类型定义,有时可能会出现“类型歧义”的错误。
针对这些问题,集算器设计了一种二进制格式文件,称为集文件(文件后缀 btx)。集文件中使用了低 CPU 消耗的压缩编码,数据存储时较文本文件占用磁盘空间更小,具有较高的查询性能,并且字段的数据类型也被存储,避免出现类型歧义。同时,集文件继承了文本文件支持大数据量、可追加和易于分段并行的特点。因此,在需要使用数据文件时,集文件是更好的选择。
2、 如何使用集文件?
利用集文件存储模拟生成的数据,因为其具有与其它数据格式广泛的互通性,后续就可以灵活进行与目标数据源的双向转化,包括 Oracle、DB2、MS SQL、MySQL、PG 等关系型数据库和 TXT/CSV、JSON/XML、EXCEL 等文件类型。
下面首先针对常用的文本文件、MySQL进行双向互转的说明。
1) 集文件与文本文件互转
集文件可以与文本文件进行互相转换,相应的SPL实现脚本示例如下:
A |
B |
|
1 |
=file("文本.txt").cursor@t() |
/导入文本数据 |
2 |
>file("文本转集文件.btx").export@ab(A1) |
/导出为集文件 |
3 |
=file("集文件.btx").cursor@b() |
/导入集文件 |
4 |
>file("集文件转文本数据.txt").export@at(A3) |
/导出为文本 |
A1:导入文本文件数据。使用cursor@t()游标方式读入“文本.txt”文件数据,其中 @t 指定将第一行记录作为字段名,如果不使用 @t 选项就会以_1,_2,…作为字段名。
A2:使用循环函数,将文本数据循环追加到集文件中。使用export@ab()将文本数据导出到集文件中,其中,@a 表示追加写数据到集文件, 如果不用 @a 就表示重建集文件,@b 表示导出为集文件。
在集算器中可以直接查看生成的“文本转集文件.btx”文件数据结果,如下图所示:
A3:导入集文件数据。使用cursor@b()游标方式读入“集文件.btx”,其中 @b 表示将导入的是集文件。
A4:使用循环函数,将集文件数据循环追加到文本文件中。export@t()将指定单元格的数据导出到文本文件中,其中,@a 表示追加写数据到文本文件中, 不用 @a 则表示重建文本文件,@t表示将集文件的字段名作为第一条记录写入文本文件。
查看生成的“集文件转文本数据.txt”文本文件数据结果,如下图所示:
2) 集文件与 MySQL 互转
集文件也可以与MySQL之间进行数据互转,相应的SPL脚本也很简单。下例将MySQL数据库employeeinfo表中的数据导出到集文件中:
A |
B |
|
1 |
=connect("MySQL") |
/连接 MySQL 数据源 |
2 |
=A1.cursor("select * from employeeinfo") |
/游标读取 MySQL 中 employeeinfo 表数据 |
3 |
>file("MySQL集文件.btx").export@az(A2) |
/循环游标将 MySQL 数据导入到集文件中 |
4 |
>A1.close() |
/关闭数据库连接 |
A1:连接 MySQL 数据源,使用connect()进行 MySQL 数据库的连接。如果用鼠标点击 A1 单元格,可以直接查看 MySQL 数据库的连接信息。具体查看数据库配置教程相关章节文档的配置说明。
A2:游标读取 MySQL 中 employeeinfo 表数据。
A3:使用循环函数,将 A2读出的数据循环追加到集文件中。使用export@az()将文本数据导出到集文件中,其中,@a 表示追加写数据到集文件中, 如果不用 @a 则表示重建集文件,@z是将数据强制导出集文件,注意,这里用了 @z 而不是前面的 @b,事实上 @z 会强制导出集文件(和 @b 效果一样),同时还可以通过表达式 export@z(A2;s) 增加 s 选项作为分组表达式,s对于文本文件为自选分隔符, 缺省默认分隔符是 tab,有 s 参数时认为 A2 文本数据对 s 有序,仅在 s 变化时才分段,这种设置分段的集文件用于并行数据量大时的分段导出, 导出时同一段的记录不会被拆开。缺省情况下不分段导出“文本转集文件.btx”集文件。
同样,在集算器中可以直接查看生成的“MySQL集文件.btx”文件数据结果,如下图所示:
A4:使用close()函数关闭 A1 建立起的 MySQL 数据源连接。
下例则是将集文件中的数据插入到MySQL数据库employeeinfo表中:
A |
B |
|
1 |
=connect("MySQL") |
/连接MySQL数据源 |
2 |
=file("集文件.btx").cursor@b() |
/游标导入集文件数据 |
3 |
=A1.update(A2,employeeinfo,empid,other;empid) |
/执行update更新 |
4 |
>A1.close() |
/关闭数据库连接 |
A1:连接 MySQL 数据源。使用connect()进行 MySQL 数据库的连接。如果用鼠标点击 A1 单元格,可以直接查看 MySQL 数据库的连接信息。具体查看数据库配置教程相关章节文档的配置说明。
A2:游标方式导入集文件的数据。集文件数据使用cursor@b()将以游标方式读取,其中包含数据表字段:empid 和 other。
A3:更新 MySQL 数据库“employeeinfo”库表中的数据。使用update()将单元格 A2 通过游标读取的集文件数据更新到 MySQL 数据库“employeeinfo”库表中。
A4:使用close()函数关闭 A1 建立起的 MySQL 数据源连接。
使用第三方工具查看 MySQL 数据库的“employeeinfo”库表,插入的数据结果如下截图所示:
二、 模拟生成字段数据
在开始单数据表模拟之前,我们先了解一下通过SPL脚本处理主键字段和普通字段的方法。
1、 主键字段数据的模拟生成
主键字段最典型的要求就是模拟生成的数值不能重复,另外可能会要求一定的顺序、范围、格式要求。
1) 数值自增
对于主键字段,常见的要求就是字段值唯一、不重复,且自增:
A |
B |
C |
|
1 |
=file("数据自增表.btx") |
/模拟数据生成并导出为集文件 |
|
2 |
100 |
/定义生成的数据量 |
|
3 |
=A2.new(#:empid,rand(100000):other) |
/批量模拟生成数据 |
|
4 |
>A1.export@b(A3) |
/将生成数据导出为集文件 |
A1:新建导出的集文件。
A2:定义模拟生成的数据量。
A3:批量模拟生成数据。其中empid是模拟数据表的主键数值自增字段。使用“#”序号值直接设置主键empid的自增数值,使用rand()函数生成 0-100000 内的随机数模拟 other 字段。
A4:将单元格 A3 生成的数据导出到 A1 的新建集文件中。使用export@b()将数据追加写入到 btx 集文件数据中,其中, @b 表示导出集文件格式。
点击 A1 单元格生成的“数据自增表.btx”集文件,查看文件数据结果如下:
2) 在规定数据范围内取值且不重复
主键字段也可以是某个规定范围内不重复的数值,比如从1~10000中批量取1000个不重复的数值作为主键字段,SPL脚本如下:
A |
B |
C |
|
1 |
=n=10000 |
/规定的数据范围 |
|
2 |
=m=1000 |
/定义需要获取的数据数量 |
|
3 |
=to(n).sort(rand()).to(m) |
/这是数据范围n不大时,获取 m 个不重复数值的方法 |
|
4 |
=(m*1.1).(rand(n)).id() |
=A4.to(m) |
/这是数据范围n很大时,获取 m 个不重复数值的方法 |
A1:模拟数据范围 n
A2:模拟需要获取的数据数量 m。
A3:数据范围 n 不大时获取 m 个不重复数值的方法。如果n不大,那么对于包含 n 个成员的集合排序很快,这时可以先用 to() 函数生成一个从 1 到 n 的序列,然后使用sort()函数基于rand()函数生成的随机数直接将这个序列的成员顺序随机打乱,然后再通过to()获取 m 个不重复数值。
查看单元格 A3 的结果,如下图所示:
A4:如果数据范围n很大,那么对个成员的集合排序就会比较慢,因此就不能使用 A3 的方法了。我们的做法是在数据范围 n 内随机获取 m*1.1 个数值,然后使用 id() 函数将这 m*1.1 个随机获取的数据去重。由于随机取数可能会有重复,如果只取 m 个,去重后就会少于 m 个,所以这里定义了一个系数 1.1,适当多取几个,确保在去重后,剩余数量多于 m 个。然后在 B4 中通过to()获取其中的 m 个。
查看单元格 A4 的结果,如下图所示,一共取得了 1044 个不重复的数(有 56 个重复被去重去掉了):
再查看单元格 B4 的结果,如下图所示,正好是 1000 个:
3) 字符串主键
主键字段还有一种情况是由“特定编号 + 序号 / 随机数” (日期的情况暂不在此讨论)组成,也就是常见的编码形式,相应的SPL模拟脚本如下:
A |
B |
C |
|
1 |
=file("字符串编码表.btx") |
/模拟数据生成并导出为集文件 |
|
2 |
100 |
/定义生成的数据量 |
|
3 |
=A2.new("RAQ"+string(#,"#000")+10.(rand(10)).concat():empid,rand(100000):other) |
/批量模拟生成数据 |
|
4 |
>A1.export@ab(A3) |
/将生成数据导出为集文件 |
A1:新建导出的集文件。其中将使用empid作为模拟数据表的字符串主键字段。
A2:定义模拟生成的数据量。
A3:批量模拟生成数据。使用字符串拼接出主键字段 empid。empid 由固定字符串“RAQ”标识、三位数序号,以及 10 位随机数拼接而成。其中,三位数序号字符串使用string()对获取的序号进行格式化处理,用“0”补足三位;用函数concat()将序列10.(rand(10))中的 10 个数字成员用无分隔符连接,拼接成 10 位随机数字组成的字符串。另外,依旧使用rand()函数生成 0-100000 内的随机数填入 other 字段。
A4:将生成数据导出为集文件格式,与上例相同。
点击 A1 单元格生成的“字符串编码表.btx”集文件查看文件数据结果,如下图所示:
特别地,红框中标示的就是生成的三位序号字符串。
4) 大数据量的流式追加生成
上面的主键生成都不涉及复杂的主键字段生成,而且当小数据量时基本只要一行循环函数代码,全内存执行就可以了。但是当要求生成的数据量巨大,例如千万、亿级以上,或是生成的数据在内存中放不下的时候,我们就需要考虑分次生成追加记录了。
这时候就需要用 for 循环进行显式控制,分批生成数据。每次流式写入多行,而不是一行一行写。下面我们看一下使用 SPL 编写脚本如何实现大数量的生成与处理:
A |
B |
C |
D |
|
1 |
=file("大数据量的流式追加生成.btx") |
/模拟数据生成并导出为集文件 |
||
2 |
10000000 |
/定义生成的大数据量 |
100000 |
/定义每次分批生成的数据量 |
3 |
for A2\C2 |
/保证结果是整数 |
||
4 |
=C2.new((A3-1)*C2+~:empid,rand(10000):other) |
/每次分批生成的字段数据 |
||
5 |
>A1.export@ab(B4) |
/流式写入 btx 集文件数据 |
||
6 |
>B4=null |
/清空临时数据表的流式分批数据 |
A1:新建导出的集文件。其中将用empid作为模拟数据表的字符串主键字段,用 other 作为普通字段。
A2:定义生模拟生成的数据量和每次分批生成的数据。
A3:循环生成数据,计算出分批生成数据的循环次数并进行循环。
B4:按照单元格 A3 设定的循环分批生成具体数据,其中使用“(A3-1)*C2+~”生成主键序号。
B5-B6:将大据量分批流式写入集文件格式,并清空分批的临时数据。使用export@ab()将生成的数据流式追加写入到集文件,@a 表示追加数据,@b 表示导出集文件格式。最后,在单元格 B6 中使用 null 清空单元格 B4 中的数据。
例子中单元格 A3-B6 四行代码,实现了大数据的分批流式追加写入,能够有效防止内存溢出。
查看生成的 1 千万数据量的"大数据量的流式追加生成.btx" 集文件的数据内容,如下图所示:
2、 普通字段数据的模拟生成
1) 常规日期
SPL对日期时间的处理提供了丰富的函数支持,详见日期时间函数参考,下面是两个例子。
A |
B |
|
1 |
=birthday=date(year(now()),1,1) |
|
2 |
=birthday=elapse(birthday,rand(days@y(birthday))) |
|
3 |
A1:获取当前年份的第一天日期。使用 now() 函数获取当前系统时间,使用 year 获取年份,使用date(year,month,day)函数获取年份的第 1 天日期。
A2:随机生成指定年份的日期。使用elapse()函数计算相差某个时间的新日期数据,即与新年第一天相差 n 天的日期。其中,days@y()是获得指定日期所在年的天数。
点击 A2 单元格,可以看到某次运行的数据结果是:
2) 规定数据范围可重复的值
模拟的字段在规定数据范围内随机取值,由于是非主键字段,值可以重复,如下例所示:
A |
B |
|
1 |
=10.(rand(10)).concat() |
/第一种生成方法 |
2 |
=rands("0123456789",10) |
/第二种生成方法 |
3 |
=file("行政区划代码.txt").import@t() |
/导入txt行政区划代码数据表 |
4 |
=rand(A3.len())+1 |
/随机生成数据位置序号 |
5 |
=A3(A4) |
|
6 |
=A3(A4).id |
/第三种生成方法 |
A1:随机生成一个 10 位数字的字符串。查看某次运行结果为:
A2:在字符 0-9 之间随机生成 10 位数字的字符串。这里使用rands()函数取得一个随机字符串。从结果看 A2 与 A1 的结果都是生成 10 位显示数字的字符串,可以根据实际需求对生成的结果值进行数据类型转换处理,如:字符串转数值等。
A3~A6:在行政区划中随机取值,其中:
A3:导入本地包含“行政区划代码”数据的文本文件。
查看该表数据可以看到包含 id 编号和区域名称。
A4:使用len()函数获取行政区划代码数据表长度,然后在此范围内随机获取一个序号。
A5:根据 A4 的序号返回 A3 的“行政区划代码”数据表的指定位置的 id 数据。
A6:获取指定位置数据的 id 字段值。
3) 字段拆分与组合
某些字段的构成需要遵循一定的编码规则,例如身份证号码,我们通过公民身份证号码可以很直接的获取一个人所在的地区、出生日期、性别,并计算出年龄。这里就以此为例对字段的拆分和组合做详细的说明,其它类似的情况可以以此为参考。:
公民身份证号码是特征组合码,由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码。具体数据特征和约束如下:
l 地址码:编码对象常住户口所在县级(市、旗、区)行政区划代码,按 GB/T2260 标准执行。
l 出生日期码:编码对象出生的年、月、日,按 GB/T7408 标准执行,年、月、日之间不用分隔符。
l 顺序码:在同一地址码所标识的区域范围内,对同年、同月、同日出生的人编定的顺序号,其中奇数分配给男性,偶数分配给女性。
l 校验码:根据前面十七位数字码,按照 ISO 7064:1983.MOD 11-2 校验码计算出来的检验码。
模拟生成身份证编码的SPL脚本如下:
A |
B |
C |
D |
|
1 |
=file("行政区划代码.txt").import@t() |
/导入前六位的行政区划代码 |
||
2 |
=rand(A1.len())+1 |
/随机生成行政区划代码位置序号 |
||
3 |
=A1(A2) |
|||
4 |
=area=A3.area |
/获取对应的【所在区域】 |
||
5 |
=rands("0123456789",3) |
/随机取得三位数字组成的顺序码字符串,偶数代表女,奇数代表男 |
||
6 |
/******身份证号码规则&计算公式****** |
|||
7 |
="7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2" |
/对应的17个系数 |
="1-0-X -9-8-7-6-5-4-3-2" |
/对应的 11 个校验码 |
8 |
=string(A1(A2).id)+string(now(),"yyyyMMdd")+A5 |
|||
9 |
=A8.split@p() |
|||
10 |
=A7.split@p("-").(~*A9(#)).sum()%11 |
/求与系数相乘的和的余数 |
||
11 |
=C7.split("-")(A10+1) |
/根据余数找出对应校验码值 |
||
12 |
=idcard=A8+string(A11) |
/返回身份证号,【身份证号码】=行政区域代码+出生日期+三位顺序码+一位校验码(已包含X的情况)。 |
A1:导入本地 txt 文本文件,其中包含“行政区划代码”数据。
A2-A4:随机生成“行政区划代码”数据表范围内的数据位置序号。并按照位置序号获取对应的所在区域。
A5:随机取得三位数字组成的顺序码字符串,偶数代表女,奇数代表男。
A6-A7:按照标准编码规则和计算公式,设置计算系数和校验码。
A8:字符串拼接组合身份证号码的前三组的字符串,即:地址码+出生日期码+顺序码。
A9-A11:按照身份证号码的标准计算公式,拆分字符串系数和校验码,计算出对应的一组校验码。
A12:返回身份证号,行政区域代码 + 出生日期 + 三位顺序码 + 一位校验码,特别地,校验码已包含 X 的情况,这个是根据校验码生成规则自动生成的一个身份证编码:
这个例子中利用了对字符串的拆分和组合,轻松实现了自动生成合法身份证号码的程序。
4) 序列与字符串的转换
这里补充一个小知识点,在处理字符串时,一方面可能需要将一个序列的值组合成一个字符串,另一方面又可能把一个字符串按照分隔符拆分成序列。SPL 中可以通过split()和concat()两个函数,很方便地实现序列和字串的相互转化,以便进一步计算。
其中,函数 s.split(d) 用来将字串 s 以分隔符 d 拆分成序列。函数 A.concat(d) 用来将序列 A 用分隔符 d 连接,拼接成字串,自动处理数据类型。两个函数中都可以通过不同的选项进行控制,例如 @c 选项,表示以逗号为分隔符,下面是几个例子:
A |
B |
|
1 |
a,1,c,2018-8-28,false,d |
|
2 |
=A1.split@c() |
|
3 |
=A2.concat@c() |
A1:输入字符串。
A2:将 A1 的字符串以逗号为分隔符转化为序列。查看运行结果为:
A3:将 A2 的序列以逗号为分隔符转化为字符串。查看运行结果为:
这个技巧在后面常规单数据表模拟的例子中有具体的应用。
5) 普通字段列自动扩展
在进行数据表模拟时,还可能需要模拟一些非必要的普通字段,这些字段可能记录了一些属性数值,也可能是空值,但在模拟数据查询时又是不可缺少的。SPL 提供了非常简便的方法,可以自动批量扩展出一些字段,快速填充:
A |
B |
|
1 |
=create(empID,other1,other2,other3,other4,other5,other6) |
|
2 |
=6.("rand(100000)").string() |
/批量模拟追加 other 字段 |
3 |
>A1.insert(0,1,${A2}) |
/将生成的字段插入到临时数据表中 |
A1:创建一个数据表,其中 empID 是主键,other1-other6 字段就是需要批量模拟生成的字段。
A2:循环扩展出 6 个随机取数函数,并转换为字符串。查看运行结果为:
A3:将模拟的数据插入到 A1 数据表中。其中,使用${A2}动态进行宏运算,也就是将 A2 的字符串表达式作为宏进行动态计算,得到的随机数作为 insert() 函数中的参数。当然,这一步也可以手工直接一个一个写出来。查看运行结果为:
三、 模拟生成单数据表
1、 常规单数据表模拟
通常,我们做模拟测试或是功能验证,都是模拟一个单一的大宽数据表,需要的数据字段全部都在一个单数据表中,既方便数据过滤查询,也相对容易生成。现在我们就利用前面介绍的技巧,使用SPL生成一个大数据量的单数据表模拟数据,一个“员工登记表”的测试用数据。数据表结构如下:
表 A:人员登记表 |
||
员工编号 |
EmpID |
/*主键 */ |
姓名 |
Name |
|
身份证号码 |
IDCard |
|
性别 |
Sex |
|
出生日期 |
Birthday |
|
所在区域 |
Area |
|
其它 1 |
Other1 |
|
其它 2 |
Other2 |
|
… … |
… … |
|
其它 12 |
Other12 |
模拟数据的生成规则:
l 按照数据表生成对应信息,包括:员工编号、姓名、身份证号码、性别、出生日期、所在区域、其它1…其它12等信息。
l 各字段的生成规则:
n 【员工编号】是依次产生(主键);
n 【姓名】、【身份证号码】、【性别】、【出生日期】随机生成;
n 【身份证号码】的隐藏条件是与【出生日期】和【性别】对应,即身份证号中的日期与【出生日期】相同,最右一位的顺序码与【性别】对应(奇数是男性,偶数是女性),
n 同时还需要保证【出生日期】模拟的员工年龄应在18~65岁之间;
n 【所在区域】从固定范围数据表中随机获取;
n 【其它1】…【其它12】模拟非必要条件的常规备用字段。
l 生成数据量:要求至少生成1000万条模拟数据。
以下是相应的SPL脚本:
A |
B |
C |
D |
E |
F |
||||
1 |
=file("姓氏.txt").import@t() |
=file("男名.txt").import@t() |
=file("女名.txt").import@t() |
/引入需要的字典表,用于随机组合生成新字段 |
|||||
2 |
=file("行政区划代码.txt").import@t() |
/导入前六位的行政区划代码 |
|||||||
3 |
=file("人员登记表.btx") |
/用于导出模拟数据的集文件 |
|||||||
4 |
=create(empID,name,sex,birthday,area,age,idcard,other1,other2,other3,other4,other5,other6,other7,other8,other9,other10,other11,other12) |
/创建人员登记表 |
|||||||
5 |
10000000 |
100000 |
/A5设置模拟生成的总数据量,B5 设置每次流入写入 btx 集文件的数据量,控制内存占用,避免内存溢出 |
||||||
6 |
for A5 |
>A1(rand(A1.len())+1) |
/获取随机姓氏 |
=rand(A2.len())+1 |
/随机生成行政区划代码位置序号 |
||||
7 |
=rands("0123456789",3) |
/随机取得三位数字组成的顺序码字符串,偶数代表女,奇数代表男 |
|||||||
8 |
if int(B7)%2==0 |
>sex="女 " |
>name=B6.姓氏 +C1(rand(C1.len())+1). 女名 |
/生成【性别】和据性别拼接【姓名】 |
|||||
9 |
else |
>sex="男 " |
>name=B6.姓氏 +B1(rand(B1.len())+1). 男名 |
/根据性别拼接姓名 |
|||||
10 |
>age=18+rand(48) |
/控制【年龄】在 18-65 岁之间 |
|||||||
11 |
>birthday=date(year(now())-age,1,1) |
>birthday=elapse(birthday,rand(days@y(birthday))) |
/随机生成出生日期 |
||||||
12 |
=A2(D6) |
>area=B12.area |
/获取对应的【所在区域】中文名称 |
||||||
13 |
/******身份证号码规则 & 计算公式 ****** |
||||||||
14 |
="7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2" |
/对应的 17 个系数 |
="1-0-X -9-8-7-6-5-4-3-2" |
/对应的 11 个校验码 |
|||||
15 |
=string(B12.id)+string(birthday,"yyyyMMdd")+B7 |
||||||||
16 |
>B15.split@p() |
>B14.split@p("-").(~*B16(#)).sum()%11 |
/求与系数相乘的和的余数 |
>D14.split("-")(C16+1) |
/根据余数找出对应校验码值 |
||||
17 |
>idcard=B15+string(E16) |
/返回身份证号,【身份证号码】= 行政区域代码 + 出生日期 + 三位顺序码 + 一位校验码 |
|||||||
18 |
=12.("rand(100000)").string() |
/批量模拟追加【其它】字段 |
|||||||
19 |
>A4.insert(0,A6,name,sex,birthday,area,age,idcard,${B18}) |
/将生成的字段插入到临时数据表中 |
|||||||
20 |
if A4.len()>=B5||A6==A5 |
>A3.export@ab(A4) |
/流式写入追加 btx 集文件数据 |
||||||
21 |
>A4.reset() |
/每 10 万数据清空一次临时数据表 |
|||||||
A1-A2:从本地文件中导入人员登记表必要的字典表。包括:自动生成人员姓名需要的“姓氏.txt”、“男名.txt”、“女名.txt”和生成身份证需要的“行政区划代码.txt”数据。
A3:创建集文件“人员登记表.btx”,用于导出生成的模拟数据。
A4:创建临时人员登记表。
A5:设置模拟生成大数据的总量,这里按照要求设置 1 千万,B5 设置每次流式追加写入 btx 集文件的数据量,控制内存占用,避免内存溢出。
A6-A21:按照生成数据量的要求循环生成 1 千万的数据,并存入集文件“人员登记表.btx”中。其中使用的一些函数在上面都有介绍,这里就不再赘述。特别地,需要说一下单元格 B20,用 if 判断单元格 A4 临时表的数据长度是否大于等于 B5 设置的 10 万或是当前 A6 单元格的循环数值等于 A5 设置的阈值,如果条件符合为 true,在单元格 C20 使用export@ab()流式写入追加 btx 集文件数据。最后,在单元格 C21 中使用reset()清空 A4 序表成员数据并保留创建的数据结构。
使用类似分页的流式追加数据,可以有效应对大数据量模拟数据生成的场景,可以避免在数据生成过程中占用大量的 JVM 内存,有效避免 JVM 内存溢出。
【注意事项:】
如果要查看生成的人员登记表的集文件数据,在集算器安装包中,提供了集文件浏览器,可以在集算器安装目录的 esProc\bin 路径下,运行,查看集文件。
使用 BTX 集文件浏览器打开有 1 千万条数据量的“人员登记表.btx”集文件,可以看到数据内容如下:
2、 机构树型数据表模拟
上面介绍的生成大数据量常规单数据表模拟数据的例子,是数据从无到有的情况,还有一种情况是已有基础数据,需要基于基础数据按照规则生成新的模拟数据。
事实上:上述的场景中,已经利用了已有的“行政区划代码”数据字典表(包括:id 区域编码、区域名称),表中的内容如下图所示:
表 B:行政区划代码表 |
||
序号 |
ID编号 |
区域名称 |
1 |
130000 |
河北 |
2 |
130100 |
石家庄 |
3 |
130102 |
长安区 |
4 |
130103 |
桥东区 |
5 |
130104 |
桥西区 |
6 |
130105 |
新华区 |
7 |
130107 |
井陉矿区 |
8 |
130108 |
裕华区 |
…… |
…… |
…… |
现在更进一步,假设需要基于“行政区划代码”数据表生成省、市、区县的三层组织机构的模拟数据。
这种结构的数据可以按照组织机构关系呈树型表示,如下图所示。数据表的其它字段与常规数据表字段生成原理相同。
对数据进行分析,可以发现行政区划代码的数据规律是:代码是定长的六位数,其中省的格式是 xx0000,市的格式是 xxxx00,区县的格式是 xxxxxx。基于这个规则,我们生成用于查询的模拟数据,并且包含对应层级的人口统计信息字段。
下面是相应的 SPL 脚本代码:
A |
B |
C |
D |
|
1 |
=file("行政区划代码.txt").import@t(id:string,area).keys(id) |
/导入行政区划代码表 |
=file("区域统计表.btx") |
/保存生成的结果集文件 |
2 |
=A1.new(left(string(id),2)+ "0000":level1,省,ifn(A1.find(left(string(id),4)+"00"), left(string(id),2)+"0000"):level2, 市,id:level3,area: 区县,int(rand()*100000):count) |
/按照机构编号拆分三层机构树 |
||
3 |
=A2.run(省 =A1.find(level1).area, 市 =ifn(A1.find(level2).area, 省 )) |
/修改设置显示对应层级机构的区域名称 |
||
4 |
>C1.export@b(A2) |
A1:导入“行政区划代码.txt”数据字典表并设置键。“行政区划代码.txt”中的 id 字段是唯一且不重复的主键,使用keys()对读取得到的序表 id 进行键设置,方便单元格 A2 和 A3 根据 id 主键查找数据。
C1:创建保存模拟数据的集文件。
A2:按照行政区域的机构编号规则,拆分出三层机构树,并增加相应的机构名称信息和随机的人口统计数,最终生成需要的包含了三层机构树型结构的模拟数据。具体做法是:根据上面分析行政区划代码数据的规律,使用new()将 id 字段拆分格式为 xx0000、xxxx00、xxxxxx 的三层机构数据,分别对应为:level1、level2、level3 字段。由于机构编码表的数据规则,这里的 level2 需要特别处理一下,当机构是直辖市时,省与市的机构编码是一样的,即 level1 与 level2 是相同的,因此要先使用find()查找 level2 值是否存在于“行政区划代码”表中,再使用ifn()判断返回非空成员的值,从而实现返回正确市 level2 的机构编号。其中,需要特别注意的地方是keys()与find()是需要一起配合使用。
返回的层级数据结果,如下图所示:
A3:将单元格 A2 中省、市的编码转为中文名称。使用run()函数可以针对每条记录计算表达式,然后返回记录本身。特别地,需要注意run()是直接改变原数据值。同样的,这里对第二层级“市”字段也进行了查找返回非空成员值的处理方法,最终运行的结果如下:
A4:将模拟生成的数据导出为“区域统计表.btx”集文件。
同样地,可以使用集文件浏览器打开该二进制数据文件查看数据:
四、 补齐 / 补全固定数据
除了直接生成模拟数据,或者基于一些基础数据生成补充模拟数据,在业务中还可能需要在已有历史数据的基础上,针对存在的数据缺失情况,生成模拟数据进行补充。下面就介绍两种常见的固定数据补齐 / 补全的场景,从而应对数据缺失情况。
1、 补齐 / 补全固定分组数据
产品完工记录存储在“building” 数据表中,其中 YEAR 为完工时间(数据类型:字符型),格式为“年份 上半年 \ 下半年”。现在要指定起止年份,统计出各类产品每半年的完工数量。固定数据信息如下:
表 C:Building 表 |
||
ID |
TYPE |
YEAR |
1 |
33 |
2014 last half |
2 |
33 |
2014 last half |
3 |
33 |
2013 first half |
4 |
34 |
2013 first half |
5 |
33 |
2013 first half |
…… |
…… |
…… |
“building” 数据表中的年份不连续,统计时就需要进行额外的判断和处理。下面的 SPL 脚本在处理此类数据时就相对简单很多:
A |
B |
C |
D |
|
1 |
=file("building.txt").import@t(TYPE,YEAR) |
/导入 building 表 |
=file("building.btx") |
|
2 |
=create(Type,Year,SubTotal) |
|||
3 |
=to(2012,2014).([string(~)+"first half",string(~)+"last half"]).conj() |
/实现全部的完工时间,用于后面的数据补齐 |
||
4 |
for A1.group(TYPE) |
>A2.insert(0:A3,A4.TYPE,A3.~,A4.count(YEAR==A3.~)) |
/对齐数据进行补充 |
|
5 |
>C1.export@b(A2) |
A1-D1:导入“building.txt”数据表,创建保存数据结果的building.btx集文件
A2:创建临时二维表。
A3:按照起、止时间段创建新的完整的年份统计表。
A4-B4:将 building 表按照 TYPE 分组,并循环处理每组数据。在单元格 B4 中使用insert()实现 A3 与 A4 的数据补齐,其中参数“0:A3”表示在 A2 二维表末尾的位置追加 A3 长度的多条记录数据,并将补齐 / 补全数据插入到单元格 A2 的二维表中。
A5:将 A2 数据结果导出到 building.btx 集文件中。查看集文件补齐数据结果如下:
2、 补齐 / 补全固定分组并转置
员工的出差记录存储在“OnBusiness”数据表中,包含:Date 和 ID_user 字段。而用户信息数据存储在“User”表中,包含:ID 和 Name 字段。需要实现在指定的时间段,按顺序列出每周每个员工是否出差状态信息。
这个场景的特殊之处是需要每个 User 员工对应占一列。OnBusiness 和 User 的部分数据,如下:
表 D:OnBusiness表 |
|||
Date |
ID_User |
Name |
|
2015-06-22 |
2 |
User2 |
|
2015-06-01 |
1 |
User1 |
|
2015-06-03 |
5 |
User5 |
|
2015-06-19 |
3 |
User3 |
|
2015-06-02 |
4 |
User4 |
|
…… |
…… |
…… |
|
表 E:User 表 |
||
ID |
Name |
|
1 |
User1 |
|
2 |
User2 |
|
3 |
User3 |
|
4 |
User4 |
|
5 |
User5 |
|
…… |
…… |
|
如果当起止日期是 2015-05-31、2015-06-28,则期望数据的结果,如下:
表 F:NewOnBusiness表 |
|||||
Week |
User1 |
User2 |
User3 |
User4 |
User5 |
1 |
Yes |
No |
No |
Yes |
Yes |
2 |
No |
No |
No |
No |
No |
3 |
No |
No |
Yes |
No |
No |
4 |
No |
Yes |
No |
No |
No |
…… |
…… |
…… |
…… |
SPL脚本如下所示:
A |
B |
C |
D |
|
1 |
=file("OnBusiness.txt").import@t().select(Date>=date("2015-05-31")&&Date<=date("2015-06-28")) |
/导入并过滤在日期范围内的数据 |
=file("NewOnBusiness.btx") |
/保存生成的结果集文件 |
2 |
=A1.group(ID_User) |
|||
3 |
=(interval(date("2015-05-31"),date("2015-06-28"))\7).new(~:Week,${A2.("\"No\":"+Name).string()}) |
/构建区间二维表 |
||
4 |
=A2.run(~.run(A3(interval(date("2015-05-31"),Date)\7+1).field(A2.#+1,"Yes"))) |
|||
5 |
>C1.export@b(A3) |
A1:简单关联过滤查询区间范围内的数据。
C1:创建保存生成结果数据的 NewOnBusiness.btx 集文件。
A2:对单元格 A1 中关联过滤后的数据按 ID_User 分组。
A3:按照区间构造二维表的。每周 Week 占 1 行(间断时自动补齐数据),User1-User5 每个员工各占一列,默认初值都是“No”标识状态。
A4:通过多次调用 run() 函数循环 A2 数据并确定行记录后,再使用 A.field() 函数实现确定需要修改的列位置,最终将单元格 A3 构造的二维表对应的位置数据修改为员工出差状态为“Yes”状态值。
查看集文件补齐并转置的数据结果,如下:
上述运算的结果与预期数据结果一致,实现了对数据的补齐和转置。
对润乾产品感兴趣的小伙伴,一定要知道软件还能这样卖哟性价比还不过瘾? 欢迎加入好多乾计划。
这里可以低价购买软件产品,让已经亲民的价格更加便宜!
这里可以销售产品获取佣金,赚满钱包成为土豪不再是梦!
这里还可以推荐分享抢红包,每次都是好几块钱的巨款哟!
来吧,现在就加入,拿起手机扫码,开始乾包之旅
嗯,还不太了解好多乾?