数据是许多活动的核心资源。处理数据的一个重要挑战是以正确的方式存储数据。我们需要选择一种格式, 这样就可以很容易地解决手头的问题。当使用相同的数据解决多个问题时, 这可能意味着必须以不同的格式提供相同的数据。有可能不同的参与者可以使用相同的数据块;他们可能是人或项目。这些可能更喜欢或需要使用不同的格式, 在这种情况下, 我们需要转换它们之间的数据。

例如, 您可能有一个程序在 XML 文件中生成一些数据, 但您需要使用期望 JSON 的 API 处理相同的数据。或者你需要与其他部门分享你部门生产的一些数据。问题是, 您的部门的同事希望以某种格式获得数据, 而 B 部门则坚持以另一种格式提供数据。

因此, 您需要在不同格式之间进行转换。这是一个常见的问题, 通常是可解的, 但它是一个重复的任务, 有几个微妙的注意。

作为工程师, 我们知道所有枯燥、容易出错的任务只是乞求一个聪明的解决方案。让我们尝试提供一个。

以多种格式处理数据

在不同格式之间进行转换可能有以下几个原因:

  • 如果没有现成的库, 则分析新格式可能会很困难。
  • 如果您想支持几种格式, 可能的转换的数量必须迅速爆炸。例如, 如果支持四格式, 则可以有12个不同的转换来处理。如果再添加一个新格式, 现在就有20种不同的转换来支持。
  • 不同的格式可以以稍有不同的方式处理相同类型的数据。例如, 字符串始终 " 以 JSON 格式在双引号 () 之间, 但它可能在双引号之间, 也可能不是 CSV 格式。在不同的格式中, 转义字符的规则可能类似, 但稍有不同。完全正确地获取所有这些细节需要大量的工作。
  • 并非所有格式都兼容。例如, 可以将 CSV 数据转换为 JSON, 但反之亦然。这是因为 CSV 文件旨在表示齐次元素的列表。而 JSON 则可以包含其他数据结构, 如异构元素的集合。

这就是问题所在。现在, 假设您可以创建一个可以在不同格式之间转换的通用服务。通过这种方法, 您可以在每次进行即席转换时获得一些优势。

创建服务以在不同格式之间进行转换时, 您有一条装配线, 允许您从一种格式移动到另一种形式, 抽象出简单但复杂的细节。您还可以将您的装配线配置为执行各种有用的操作, 例如:

  • 在生成的输出中强制执行特定规则。例如, 您可能需要指定具有特定数字的十进制数字的所有数字。
  • 可以将以格式为 B 或反之亦然的单个文件中的多个文件中表示的数据组合在一起。
  • 可以对数据应用筛选规则 (例如, 仅考虑 CSV 文件中的某些行以转换为 JSON)。

在本文中, 我们将只创建此服务的一个简单版本, 但这可以让您了解什么是可能的。

的设计

我们将创建一个简单的 REST API, 用于接收文件并将它们转换为指定的格式。为了限制我们需要处理的转换数, 我们将依赖内部中间表示。这样, 我们就需要将转换只写入和从中间表示形式。因此, 例如, 我们不会直接从 CSV 转换为 JSON

这种方法允许更大的灵活性, 因为为了实现一种新的格式, 我们只需要与我们的中间表示而不是任何特定格式进行交互。

我们不打算在本教程中这样做, 但另一个好处是, 通过使用中间表示, 我们可以很容易地在一个输出文件中合并不同的数据源。

因此, 工作流如下所示:

  • 输入文件集 (通常只有一组) 被转换为通用数据结构。
  • 一般数据结构转换为请求的输出格式 (例如, JSON), 生成一个或多个输出文件 (通常是一个)。

我们的内部数据格式很简单, 基本上, 在基础上, DataItem 它代表了我们数据的一个通用元素:

  • DataArray表示值列表 (例如, ["one", "two"] 但也[ {"name": "john"}, {"name": "mike"} ]
  • DataObject表示一系列具有值的字段名称对 (例如, {"name": "john"} 但也{"names": ["mike", "pike", "kite"]}
  • DataValue用于包含单个值 (例如, 5 , "john" )。这将是布尔值、数字和字符串。

因此, 复杂类, 像 DataArrayDataObject 可以包含其他元素, 并基本上允许一个树状组织。

关于物理中间格式的说明

现在, 您可能会问自己, 在内存中创建自定义表示形式是一回事, 但我们是否也应该创建自定义格式?嗯, 我们不需要它在我们的教程。但是, 如果我们正在建立一个生产系统, 根据需求, 我们可能需要创建一个自定义格式, 即, 一个专门设计的方式, 以有效地存储数据, 为我们的目的。

如果由于某种原因, 无法在单个进程中实现转换过程 (共享内存), 则此物理中间格式可能很有用。例如, 如果我们希望不同的可执行文件或不同的 web 服务执行解析以及所需输出的序列化, 则需要使这些组件进行通信。在这种情况下, 它们可能需要物理中间格式。

您可能会问: 对于代表任意数据结构, XML 是否完全正常?当然, 对于像这样的简单教程中考虑的案例, JSON 或 XML 将作为自定义中间格式工作正常。但是, 这可能不是真的或最佳的代表我们的假设服务的所有格式和功能。

不同的格式是为不同的东西设计的: 相同的图像可能以不同的格式表示, 但结果文件将具有不同的特征 (例如, JPEG 将小于 PNG)。通过设计我们的自定义表示, 我们可以更好地控制过程, 避免特定现有格式的任何怪癖, 并以最佳的方式为我们的服务保存任何数据。

例如, 我们可以采用一种格式来方便地处理数据上的转换 (例如, 通过存储对数据所做的不同操作)。设计自定义格式并不一定意味着要打乱字节: OpenDocument是一堆压缩文件, 其中的数据存储在具有特定属性和值的 XML 文件中。

这就是设计考虑的原因。让我们看看代码。

设置项目

我们将使用命令行程序创建一个新的 ASP.NET Web API 项目 dotnet

dotnew new webapi

然后, 我们将添加必要的包来处理 JSON 和分析的事情。

dotnet add package Newtonsoft

运行时. 标准 dotnet 添加包 Microsoft.CodeAnalysis.CSharp.Scripting

当然, 我们将使用 ANTLR 来分析这些文件。由于我们使用 visual Studio 代码, 我们还设置了令人敬畏的可视代码扩展, 以便在每次保存语法时自动生成一个 ANTLR 分析器。只需将此值放在项目的设置中即可。

{
    "antlr4.generation": {        
        "language": "CSharp",
        "listeners": false,
        "visitors": false,
        "outputDir": "../",
        "package": "ParsingServices.Parsers"
    }
}

如果使用其他编辑器, 则需要为 ANTLR 工具提供正确的选项:

  • 我们在命名空间内创建分析器 (Java 术语包)ParsingServices.Parsers
  • 我们生成一个 c# 项目。
  • 我们既不制造听众也不创造访客。
  • 我们在语法上面的目录中生成解析器。

如果你不知道如何使用 ANTLR 我们已经编写了大量的教程, 你可以阅读入门与 ANTLR 在 c# 中, 如果你想要一个简短的介绍, 或我们的ANTLR 超级教程, 如果你想知道很多。

这些选项意味着我们的语法将位于一个 antlr/grammars 文件夹中, 而解析器将在文件夹内生成 antlr 。这使得一个干净的结构, 从生成的代码分离文法。

语法

说到 ANTLR 文法: 我们有两个, 一个用于 JSON, 另一个用于 CSV。两者都是从ANTLR 文法的存储库中取出的, 但我们已经对它们进行了修改, 以便清晰和一致。

grammar CSV;

csv     : hdr row+ ;
hdr     : row ;

row     : field (',' field)* '\r'? '\n' ;

field   : TEXT
        | STRING
        |
        ;

TEXT    : ~[,\n\r"]+ ;
STRING  : '"' ('""'|~'"')* '"' ; // quote-quote is an escaped quote

我们还有 JSON 语法, 以方便我们的工作在其余的程序。我们创建了一个独特的案例来区分简单的值 (例如 "number" : 5 ) 与复杂的值 (例如, "numbers" : [5, 3]"number" : { "value": 1, "text": "one" } )。

grammar JSON;

json                    : complex_value ;

obj                     : '{' pair (',' pair)* '}'
                        | '{' '}'
                        ;

pair                    : STRING ':' (value | complex_value) ;

array                   : '[' composite_value (',' composite_value)* ']'
                        | '[' ']'
                        ;

composite_value         : value
                        | complex_value
                        ;

value                   : TRUE
                        | FALSE
                        | NULL
                        | STRING
                        | NUMBER
                        ;

complex_value           : obj
                        | array
                        ;

TRUE                    : 'true' ;
FALSE                   : 'false' ;
NULL                    : 'null' ;

STRING                  : '"' (ESC | SAFECODEPOINT)* '"' ;

fragment ESC            : '\\' (["\\/bfnrt] | UNICODE) ;

fragment UNICODE        : 'u' HEX HEX HEX HEX ;

fragment HEX            : [0-9a-fA-F] ;

fragment SAFECODEPOINT  : ~ ["\\\u0000-\u001F] ;

NUMBER                  : '-'? INT ('

..]

WS: [\n \r] +-> 跳过;

的数据类

在查看如何实现转换之前, 让我们来看看 Data* 构成我们的数据格式结构的类。我们已经解释了他们的一般设计之前, 所以在这里, 我们主要是看代码。

public class DataItem { }

public class DataValue : DataItem
{        
    public string Text { get; set; } = "";   
    public ValueFormat Format { get; set; } = ValueFormat.NoValue;     
}

public class DataField : DataItem
{
    public string Text { get; set; } = ""; 
    public DataItem Value { get; set; } = null;
}

public class DataObject : DataItem
{     
    public IList<DataField> Fields { get; set; } = new List<DataField>();        

    [..]
}

public class DataArray : DataItem
{
    public IList<DataItem> Values { get; set; } = new List<DataItem>();

    [..]
}

我们删除了这些方法, 因为它们对于了解类的连接方式是多余的。正如您所看到的, 它们是非常直观的, 可能会看你期望他们如何。

public enum ValueFormat
{
    Bool,
    Integer,
    Numeric,
    String,
    NoValue
}

ValueFormat是应表示数据的实际类型的枚举。这是因为我们将每个值视为字符串来简化输入和输出阶段, 因为字符串可以接受任何类型的输入。但我们知道, 实际上, 有不同类型的数据。因此, 我们尝试了解不同的格式在这里。

public class DataValue : DataItem
{        
    private string _text = "";
    public string Text
    {
        get {
            return _text;
        }
        set {
            Format = DataValue.DetermineFormat(value);

            _text = DataValue.PrepareValue(Format, value);                
        }
    }
    public ValueFormat Format { get; private set; } = ValueFormat.String;     

    private static ValueFormat DetermineFormat(string text)
    {
        ValueFormat format = ValueFormat.String;

        text = text.Trim().Trim('"');

        int intNum;

        bool isInt = int.TryParse(text, out intNum);            

        if(isInt)
            return ValueFormat.Integer;

        double realNum;

        bool isNum = double.TryParse(text, out realNum);

        if(isNum)
            return ValueFormat.Numeric;

        bool boolean;

        bool isBool = bool.TryParse(text, out boolean);

        if(isBool)
            return ValueFormat.Bool;

        return format;
    }
}

为了了解我们所管理的数据的真实类型, 我们尝试分析每个值, 直到找到匹配项。如果没有匹配的任何类型, 这意味着我们有一个字符串。我们需要找出真正的类型, 因为每种类型都可以用特定的格式不同地表示。例如, 在 JSON 中, 一个数字可以在不带括双引号的情况下编写, 而字符串总是需要它们。因此, 当我们以特定格式输出数据时, 将使用此信息。

我们解决的问题, 不同的格式可能代表相同的数据在输入阶段, 当我们转换原来的格式在我们自己的中间一个。例如, 字符串始终 " 以 JSON 格式在双引号 () 之间, 但它可能在双引号之间, 也可能不是 CSV 格式。我们必须清除输入中的所有数据, 以便在我们的自定义数据格式中具有标准的表示形式。

这部分将是对数据进行任何标准编辑的理想位置, 如确保所有数字都使用一个特定的十进制分隔符。因为我们想保持简单的东西, 我们只是修剪字符串的任何空白。

private static String PrepareValue(string text)
{
    text = text.Trim();

    return text;
}

这是所有的1部分

Comments are closed.