演示数据





5.00/5 (11投票s)
“我的儿子,当心乌鸦!它的爪子会咬人,会抓人!当心朱朱鸟,躲避那凶猛的班达斯纳奇!” - 路易斯·卡罗尔
引言
当您准备向潜在客户或未来用户展示应用程序/功能时,您使用的数据非常重要。缺少细节、记录太少、信息重复或胡言乱语都会给对方留下糟糕的印象。错误的展示会毁掉您为代码所付出的所有努力,毕竟人们看到的是表面……
在本文中,您将看到一个我用来随机生成可用数据的工具……
背景
我的大部分工作都与数据有关。有些客户已经拥有数据,或者对如何创建数据有非常具体的想法(他们通常也能做到),这两种情况都使准备演示变得容易。然而,大多数客户只有想法,但没有足够具体的细节来开始,或者他们会将真实数据保密,所以您必须即兴发挥。在所有这些情况下,我都需要定义必要的数据模式并提供一些演示值,所有这些都是为了点燃客户的想象力并展示我的能力……
多年来,我使用了不同的方法来创建此类演示数据,并随着时间的推移创建了我自己的工具来做到这一点。在本文中,我将向您展示核心是如何构建的,以及如何使用和扩展它。
最终结果实际上是一个专业的工具,您可以用来创建任何您需要处理的任何语言的演示数据……
代码
完整代码仅在附件中提供。文章中的代码段仅用于展示思路,并不一定能独立运行……
核心
随机与随机的区别
首先,您需要了解您需要能够随机化的不同类型的数据。我说的不是您从 SQL 等地方知道的所有数据类型,例如整数和字符串。我的观点是如何人类定义数据,如何应用规则以及这些规则有多严格。
例如,想想数字。数字由计算机可以轻松重新创建的严格规则明确定义。科学计数法、千位分隔符或货币符号的所有选项仅在显示这些数字时才相关,而对数据存储方式没有影响,而这在谈论随机化时才是唯一相关的因素。
另一方面,想想名称。地名或人名。这些东西不能像数字那样通过相同的方法随机生成。在计算机达到能够制造出人类可接受的名称所需的创造力水平之前,唯一的方法就是从列表中选择。列表越长,结果越丰富。
介于两者之间……地址是两者的混合。它对不同部分的顺序有相当严格的规则,但这些部分有不同的随机化规则。但是,如果您检查每个部分,它要么有严格的规则,比如数字,要么必须被选中,比如名称。
总结以上所有内容,您可以看到,演示数据可以是以下几种之一:
- 真随机
- 基于列表
- 复杂,是以上两者的混合,并添加了一些文字
注意:可以肯定的是,计算机本身无法创建真正的随机数,就像我们掷骰子或转动轮盘赌一样。.NET Framework 使用伪随机数生成器,如文档中所述。
伪随机数是从有限的数字集中以相等的概率选择的。选择的数字不是完全随机的,因为使用数学算法来选择它们,但它们对于实际目的来说足够随机。Random 类的当前实现基于唐纳德·E·克努特的减法随机数生成器算法的修改版本。有关更多信息,请参阅 D. E. Knuth。《计算机程序设计艺术》,第二卷:半数值算法。Addison-Wesley,Reading,MA,第三版,1997。
真随机
这是最简单的部分。您只需要一个随机数生成器,.NET 就有(大多数其他现代/常用语言也一样)。使用该随机数生成器,我们可以为所有具有固定规则的数据类型创建方法。让我们看一些示例……
指定长度的任何正数
public static string Number ( int MinLength = 0, int MaxLength = 0 )
{
if ( MaxLength == 0 )
{
MaxLength = MinLength;
MinLength = 0;
}
return (
Convert.ToString(
_Random.Next(
Convert.ToInt32( "1".PadRight( MinLength, '0' ) ),
Convert.ToInt32( "1".PadRight( MaxLength + 1, '0' ) )
)
)
);
}
指定长度的字母数字值
public static string Alpha ( int MinLength = 0, int MaxLength = 0 )
{
if ( MaxLength == 0 )
{
MaxLength = MinLength;
MinLength = 1;
}
int nLength = _Random.Next( MinLength, MaxLength + 1 );
string szChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
string szResult = new string(
Enumerable.Repeat( szChars, nLength )
.Select( szArray => szArray[_Random.Next( szArray.Length )] )
.ToArray( ) );
return ( szResult );
}
我的解决方案的核心库包含这些数据类型的方法
- 数字(有符号/无符号/范围内)
- IP 地址(v4/v6)
- 经度/纬度
- 日期/时间/日期时间
- GUID(作为字符串/作为十六进制)
- 字母数字(长度受限)
- 顺序 ID
我根据需要随时间选择它们,可能无法满足您的所有需求,但是当我们将到扩展/使用部分时,您会明白它们无论如何都能满足 99.99999% 的需求……
基于列表的随机
为简单起见,我将所有核心可以处理的列表命名为“资源”。如果您想从您的列表中获取一个随机字符串,您所要做的就是调用一个函数,如下所示
Data.Resource("first");
每次调用都会从参数中引用的列表返回一个字符串——“资源”名称。
public static string Resource ( string Name )
{
if ( LoadResource( Name ) )
{
return ( _Resources[_CultureInfo.Name][Name][_Random.Next( _Resources[_CultureInfo.Name][Name].Length )] );
}
return ( string.Format( _MissingResource, Name, _CultureInfo.Name ) );
}
看看代码,您可能会好奇我使用的结构以及它所有的含义。请耐心等待,我将在存储结构部分很快介绍这个主题……
复杂随机
正如我之前所说,这些是最有趣的随机数。混合了以上两者的混合,并可能添加了文字。为了轻松定义(并稍后访问)这些复杂随机数,我创建了一个基于 JSON 的结构(一种封装在 JSON 中的压缩语言),它可以保存定义新方法来创建该复杂随机数。
{
"inherit": "",
"local": [
{
"name": "phone",
"func": "<number(3)>-<number(7)>"
},
{
"name": "firstname",
"func": "[first]"
},
{
"name": "mail",
"func": "[first]@dummy.com"
},
{
"name": "money",
"func": "<sign()><number(2,5)>.<number(2)>"
}
]
}
确切的解释将在代码部分之后给出,但即使没有它,您也可以看到一个名称-定义的模式,其中定义包含用不同括号括起来的部分和未括起来的部分……括起来的部分是函数调用或资源调用,未括起来的部分是我之前提到的文字……
例如,在函数“mail”中,[first] 将从标记为“name”的资源(列表)中选择一个值,并附加文字值“@dummy.com”……
存储结构
您在尖括号中定义的内容将是函数,类似于我在“真随机”部分中预定义的那些(关于如何操作,稍后会看到)。然而,方括号内的部分应该加载从列表中选择的值(如上所示);存储是所有这些列表 - 资源 - 存储的地方。为了支持多语言演示数据,我将结构分解为文件夹,由区域代码标识,例如 *en, en-US*, *fr, fr-CA*, *hu* 或 *he*。这遵循 ISO 命名约定(仅语言代码或与国家/地区代码组合)。语言代码 / 国家/地区代码。
在上面的示例中,您可以看到相同的文件名重复出现。例如,“first.json”包含一个名字列表,一次是英语,一次是法语,一次是希伯来语。
虽然命名列表完全取决于您,但最好在不同区域使用相同的名称,这样相同的命令文件可以在不同区域运行而无需更改(当然,关于这些命令文件稍后会有更多介绍)。这些文件中的每一个都包含一个 JSON 数组,代码会在请求时从该数组中选择一个值。
[ // content for first.json in English culture
"Peter",
"Noam",
"Paul",
"Wiliam",
"Susan",
"Topol"
]
它非常简单且可扩展,无需任何代码更改 - 这就是它的强大之处……
Func.json
除了所有代表基于列表的随机数据的 JSON 文件之外,还有一个特殊的“func.json”……它用于定义区域特定的方法来创建复杂的随机数(如上面的示例)。
代码
这是整个过程中最有趣的部分,原因是我不再处理随机数了。所有有趣的随机化部分及其理论都已经过去了,没有别的话可说。我将处理的是“func.json”和前面提到的命令文件中这些定义是如何实际使用的……
在这两种情况下(函数和命令),我都使用 JSON 文件创建一些 C# 代码并进行编译——一次编译到磁盘,一次编译到内存。所以我们这里的大部分代码经验都是如何解释定义“语言”,创建一些 C# 代码并将其编译成可执行文件(DLL),最终从您的代码中执行它……
编译函数
由该结构表示的函数文件
internal struct Culture
{
public struct Function
{
public string Name;
public string Func;
}
public string Inherit;
public Function[ ] Local;
}
让我们看看解释
继承 - 可以定义另一个库来扩展。继承由其他库的区域标识符标识。如果它现在是胡言乱语,请不要惊慌,在解释完存储结构后就会清楚。此属性可以重用通用定义,例如在阿拉伯语或英语的变体之间。
本地 - 此部分定义了此区域引入的新函数……
名称 - 新方法的名称。如果方法存在于继承的库中,则新方法将覆盖它,因此请小心您的选择。
Func - 方法体
< ... > - 用经典的 *method()* 形式包围方法调用,其中在括号内您可以根据方法定义添加参数。方法可以是任何预创建的方法或新创建的方法。注意递归!
[ ... ] - 表示一个资源。在任何地方放置它,它将被一个随机选择的值替换,该值从方括号内指示的名称的列表中选择。
不在任何这些括号对内的内容将被解释为文字。
这里是样本数据
{
"inherit": "",
"local": [
{
"name": "name",
"func": "[first] [last]"
},
{
"name": "address",
"func": "<number(3)> [street]"
},
{
"name": "phone",
"func": "<number(3)>-<number(7)>"
},
{
"name": "email",
"func": "[first]@dummy.com"
},
{
"name": "ballance",
"func": "<sign()><number(2,5)>.<number(2)>"
}
]
}
以及编译前的代码
using System.Linq;
namespace DemoData
{
public class Dataen : Data
{
public static dynamic Name ( )
{
return ( string.Format( "{0} {1}", Resource( "first" ), Resource( "last" ) ) );
}
public static dynamic Address ( )
{
return ( string.Format( "{1} {0}", Resource( "street" ), Number( 3 ) ) );
}
public static dynamic Phone ( )
{
return ( string.Format( "{0}-{1}", Number( 3 ), Number( 7 ) ) );
}
public static dynamic Email ( )
{
return ( string.Format( "{0}@dummy.com", Resource( "first" ) ) );
}
public static dynamic Ballance ( )
{
return ( string.Format( "{0}{1}.{2}", Sign( ), Number( 2, 5 ), Number( 2 ) ) );
}
}
}
以及生成上述代码的代码
string szFile = string.Format( @"{0}\{1}\func.json", Helpers.CultureRoot, Culture );
Culture oCustom = JsonConvert.DeserializeObject<Culture>( File.ReadAllText( szFile ).ToLower( ) );
StringBuilder oCode = new StringBuilder( );
oCode.AppendLine( "using System.Linq;" );
oCode.AppendLine( "namespace DemoData {" );
oCode.AppendLine( string.Format( "public class Data{0} : Data{1} {{", Culture, oCustom.Inherit ) );
foreach ( Function oCustomFunction in oCustom.Local )
{
List<string> oFunc = new List<string>( );
int nIndex = 0;
string szFinalFormat = oCustomFunction.Func;
Match oMatch = Helpers.Resource.Match( oCustomFunction.Func );
while ( oMatch.Success )
{
oFunc.Add( string.Format( "Resource({0})", Helpers.Replace( Helpers.SquareToQuoteRegex, Helpers.SquareToQuote, oMatch.Value ) ) );
szFinalFormat = szFinalFormat.Replace( oMatch.Value, string.Format( "{{{0}}}", nIndex++ ) );
oMatch = oMatch.NextMatch( );
}
oMatch = Helpers.Function.Match( szFinalFormat );
while ( oMatch.Success )
{
oFunc.Add( Helpers.TextInfo.ToTitleCase( string.Format( "{0}", Helpers.Replace( Helpers.AngleToNothingRegex, Helpers.AngleToNothing, oMatch.Value ) ) ) );
szFinalFormat = szFinalFormat.Replace( oMatch.Value, string.Format( "{{{0}}}", nIndex++ ) );
oMatch = oMatch.NextMatch( );
}
string szResult = string.Format( "return( \"{0}\" );", szFinalFormat );
if ( nIndex > 0 )
{
szResult = string.Format( "return( string.Format( \"{0}\", {1} ) );", szFinalFormat, string.Join( ", ", oFunc.ToArray( ) ) );
}
oCode.AppendLine( string.Format( "public static dynamic {0} () {{", Helpers.TextInfo.ToTitleCase( oCustomFunction.Name ) ) );
oCode.AppendLine( szResult );
oCode.AppendLine( "}" );
}
oCode.AppendLine( "}" );
oCode.AppendLine( "}" );
如您所见,它相当简单。我的步骤是
- 将 JSON 文件加载到内部结构中
- 循环处理那里定义的函数
- 识别函数调用、资源调用以及剩余的内容(文字)
- 使用 String.Format 语句构建单行方法,将随机值插入到文字之间
所以我们有一些 - 希望没有错误 - 从 JSON 文件生成的 C# 代码,我们只需要编译并存储到 DLL……
CSharpCodeProvider oCodeProvider = new CSharpCodeProvider( );
CompilerParameters oParameters = new CompilerParameters( );
oParameters.ReferencedAssemblies.Add( "System.Core.dll" );
oParameters.ReferencedAssemblies.Add( string.Format( "Data{0}.dll", oCustom.Inherit ) );
oParameters.GenerateInMemory = false;
oParameters.GenerateExecutable = false;
oParameters.OutputAssembly = string.Format( "Data{0}.dll", Culture );
oParameters.MainClass = string.Format( "Data{0}", Culture );
CompilerResults oResults = oCodeProvider.CompileAssemblyFromSource( oParameters, szCode );
这段代码将从上述代码创建“Dataen.dll”(英语区域),并将其存储在当前工作文件夹(工具启动的那个文件夹)中以便以后使用……(有关编译器选项的更多详细信息,请参阅此处:CompilerParameters)
在最低级别,所有这些 DLL 都继承自“真随机”和“基于列表的随机”中提到的硬编码随机数,因此在实际渲染数据时,您可以使用所有这些……
当然可能会出现错误,在这种情况下,它们将与代码一起转储到控制台……
现在您拥有了所有标准和特殊函数,可以根据所需的区域(包括语言)创建格式化数据,然后您可以继续使用它们创建实际的表数据……
运行命令
命令文件(它们的名称不固定,它们的位置也不固定,因为它们是或可以是区域无关的)用于实际创建数据,使用可用于特定区域的函数(内置和自定义函数);这就是我谈论跨不同区域进行类似命名的原因。如果您为不同区域使用了相同的资源名称和函数名称,您就可以使用同一个命令文件创建多语言演示数据……
命令过程的流程与函数编译的流程非常相似。只有一个主要区别 - 结果被编译到内存 DLL 并立即执行(然后丢弃)……
当然,定义命令结构的 JSON 根据其目标而有所不同。新的 JSON 有点复杂,看起来像这样
internal struct CommandList
{
public struct Column
{
public string Name;
public string Func;
}
public struct Relation
{
public string Parent;
public string Child;
}
public struct Table
{
public string Name;
public int Rows;
public Relation[ ] Relations;
public Table[ ] ChildTables;
public Column[ ] Columns;
}
public bool Compile;
public Format Output;
public Table[ ] Tables;
}
以及解释
Compile - 指示应用程序在编译并运行此命令之前,为指定的区域(在命令行上指定)编译(或重新编译)函数。
Output - 输出文件的格式 - 可以是 JSON 或 CSV。
Table - 保存要生成的表的定义,并具有以下部分
Name - 表的唯一名称,将用作输出值的文件名。
Rows - 要生成的行数。
Relations - 列到列关系的列表,用于在父表和子表之间复制数据以启用结果数据之间的外键(例如,如果父表以 ID 作为键,则可以使用这些定义声明 - 并复制 - 到任何子表中)。
Parent - 来自父表的列名。
Child - 来自子表(此表)的列名。
ChildTables - 此表子项的递归定义(理论上您创建的级别没有最大数量,但肯定会使事情变慢)。
Columns - 定义此表的列列表,形式为名称和函数。
Name - 有效的列名。
Func - 以我们在函数定义文件中使用的方式定义的函数。
现在是一个示例命令文件,以了解其工作原理
{
"compile": false,
"output": "csv",
"tables": [
{
"name": "person",
"rows": 1000,
"childTables": [
{
"name": "email",
"rows": 1,
"childTables": [
{
"name": "messages",
"rows": 7,
"relations": [
{
"parent": "person",
"child": "person"
},
{
"parent": "id",
"child": "email"
}
],
"columns": [
{
"name": "person",
"func": ""
},
{
"name": "email",
"func": ""
},
{
"name": "id",
"func": "<sid()>"
},
{
"name": "content",
"func": "<alpha(4,12)>"
}
]
}
],
"relations": [
{
"parent": "id",
"child": "person"
}
],
"columns": [
{
"name": "person",
"func": ""
},
{
"name": "id",
"func": "<sid()>"
},
{
"name": "email",
"func": "<alpha(7,15)>@lazy.com"
}
]
},
{
"name": "action",
"rows": 3,
"relations": [
{
"parent": "id",
"child": "person"
}
],
"columns": [
{
"name": "person",
"func": ""
},
{
"name": "id",
"func": "<sid()>"
},
{
"name": "value",
"func": "<ballance()>"
}
]
}
],
"columns": [
{
"name": "id",
"func": "<sid()>"
},
{
"name": "first_name",
"func": "[first]"
},
{
"name": "last_name",
"func": "[last]"
},
{
"name": "age",
"func": "<number(2,2)>"
}
]
}
]
}
您可能注意到的第一件事是空的 **func** 定义。这些是为了符合 C# 代码中的类,并表示通过关系从父表中获取值的字段……
此定义文件将创建如下代码
using System;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace DemoData
{
public class person
{
private static dynamic First;
private static dynamic Last;
private static void Next ( )
{
First = Datahe.Resource( "first" );
Last = Datahe.Resource( "last" );
}
private static JObject Record ( JObject Parent = null )
{
Next( );
return ( new JObject {
{ "Id", string.Format("{0}", Datahe.Sid())},
{"First_Name", string.Format("{0}", First)},
{"Last_Name", string.Format("{0}", Last)},
{"Age", string.Format("{0}", Datahe.Number(2,2))},
} );
}
public static void LoadData ( Dictionary<string, Stack> Storage, JObject Parent = null )
{
Data.PushSID( );
for ( int i = 0; i < 1000; i++ )
{
Storage["person"].Push( Record( Parent ) );
email.LoadData( Storage, ( JObject )Storage["person"].Peek( ) );
action.LoadData( Storage, ( JObject )Storage["person"].Peek( ) );
}
Data.PopSID( );
}
}
public class email
{
private static JObject Record ( JObject Parent = null )
{
return ( new JObject {
{ "Person", Parent["Id"].Value<string>()},
{"Id", string.Format("{0}", Datahe.Sid())},
{"Email", string.Format("{0}@lazy.com", Datahe.Alpha(7,15))},
} );
}
public static void LoadData ( Dictionary<string, Stack> Storage, JObject Parent = null )
{
Data.PushSID( );
for ( int i = 0; i < 1; i++ )
{
Storage["email"].Push( Record( Parent ) );
messages.LoadData( Storage, ( JObject )Storage["email"].Peek( ) );
}
Data.PopSID( );
}
}
public class messages
{
private static JObject Record ( JObject Parent = null )
{
return ( new JObject {
{ "Person", Parent["Person"].Value<string>()},
{"Email", Parent["Id"].Value<string>()},
{"Id", string.Format("{0}", Datahe.Sid())},
{"Content", string.Format("{0}", Datahe.Alpha(4,12))},
} );
}
public static void LoadData ( Dictionary<string, Stack> Storage, JObject Parent = null )
{
Data.PushSID( );
for ( int i = 0; i < 7; i++ )
{
Storage["messages"].Push( Record( Parent ) );
}
Data.PopSID( );
}
}
public class action
{
private static JObject Record ( JObject Parent = null )
{
return ( new JObject {
{ "Person", Parent["Id"].Value<string>()},
{"Id", string.Format("{0}", Datahe.Sid())},
{"Value", string.Format("{0}", Datahe.Ballance())},
} );
}
public static void LoadData ( Dictionary<string, Stack> Storage, JObject Parent = null )
{
Data.PushSID( );
for ( int i = 0; i < 3; i++ )
{
Storage["action"].Push( Record( Parent ) );
}
Data.PopSID( );
}
}
public class Execute
{
public static void Run ( )
{
Dictionary<string, Stack> oStorage = new Dictionary<string, Stack>( );
Data.Reset( "he" );
oStorage.Add( "person", new Stack( ) );
oStorage.Add( "email", new Stack( ) );
oStorage.Add( "messages", new Stack( ) );
oStorage.Add( "action", new Stack( ) );
person.LoadData( oStorage );
foreach ( KeyValuePair<string, Stack> oTable in oStorage )
{
Export.SetOutput( string.Format( @"C:\Users\peter\Source\Repos\demodata\DemoData\bin\Debug\results\{0}.CSV", oTable.Key ) );
Export.ToCsv( Array.ConvertAll( oTable.Value.ToArray( ), oItem => ( JObject )oItem ) );
Export.RestoreOutput( );
}
}
}
}
您可以看到类的重复模式 - 每个类代表一个表。该类有两个部分,一个用于创建单行,一个用于将其推送到存储。存储在顶层初始化,为定义文件中包含的每个表提供空间,并在数据生成后将每个表存储到其文件中。
有趣的一点是资源的使用 - 在我们示例中的 person 类中。创建它的方式确保在同一行中使用相同的资源多次(例如,使用名字创建电子邮件地址)将返回相同的值。
最后一部分是执行此代码以实际创建数据。相关的代码如下
// ...
oParameters.GenerateInMemory = true;
oParameters.GenerateExecutable = false;
oParameters.MainClass = "Execute";
// ...
var oType = oResults.CompiledAssembly.GetType( "DemoData.Execute" );
oType.GetMethod( "Run" ).Invoke ( null, null );
与之前编译的第一个区别是,我现在创建了一个内存 DLL,第二个是立即执行它。
如果到目前为止一切顺利,您将在可执行文件文件夹下的“results”文件夹中看到预期的输出文件。这些文件几乎可以导入到任何数据库(CSV)中,或者按原样使用(JSON)……这取决于您……
这里看不到的代码
有处理命令行选项的代码,即 - 列出区域、为区域编译函数定义以及运行命令文件。留给您去发现……
摘要
在学习(甚至可能调整)了这个工具之后,您将再也不会在网上搜索演示数据了……