使用不同的规则从Excel导入不同的文件

时间:2017-08-22 21:21:17

标签: c# excel rule-engine

我最近的任务是编写一个可以导入Excel文件的软件。

我试图解决的问题是我的公司有c100客户端,每个客户端都提供不同布局的文件,因为文件中的列在客户端之间会有所不同,但每个文件中都有相关的信息。

由于需要对不同的文件进行某些操作,因此这个过程很复杂。

例如,在1文件中,需要在特定列之后插入列,然后需要将计算结果放入该列。在同一张表中,地址提供9列,此地址需要移动到9列中的最后6列,然后删除前3列。

我不想做的是为每个文件(如上所述的c 100)编写处理逻辑,从而陷入必须维护此代码并负责添加新客户文件的困境。

我想要做的是创建一个规则或处理引擎,我可以有基本的规则,如“插入列”,“删除列”,“插入计算”,“格式a,b,c,d,e & f Columns To use d,e& f“ - 原因是为了配置任何新文件的读取和处理可以通过最终用户的前端软件完成(显然有一些关于什么的培训)做)。

是否有适合这种情况的模式或策略?我已经阅读过有关规则引擎的内容,但最好的例子是简单的布尔比较,例如“Age = 15”或“Surname ='Smith'”,但找不到像“在G列后插入列”这样的事情的好例子“将G - 125放入H列。”

非常感谢这里的任何帮助,或指向良好方法的指针。

3 个答案:

答案 0 :(得分:1)

让我看看我是否可以帮助你。

如果我错了,请纠正我,但似乎所有输入和输出文件仅包含列和列中的数据。

在这种情况下,您应该将您的问题想象为X输入列到Y输出列的转换。对于每个客户端,您将需要一个指定转换的配置。配置可能如下所示

Y1 = X1
Y2 = X1 + X2 
Y3 = X3 + " some string"

如您所见,您的配置行只是C#表达式。您可以使用LINQ Expression class从转换公式中构建表达式树。您可以了解Expressions here。然后可以编译这些表达式并用于执行实际转换。如果您考虑C#,您将构建一个静态转换方法,该方法将列表作为输入并返回列表作为每个客户端的输出。使用表达式时,您必须自己解析配置文件。

您还可以使用Roslyn Compiler Services,它可以支持正确的C#语法。这样,您可以逐字地使用可以进行转换的静态方法。这也减轻了解析任务的负担。

在任何一种情况下,您仍然需要处理以下事项:我应该期望列是一个字符串(这意味着您的支持需要知道明确指示配置GUI将所需的列解析为数字)或者我应该自动将数字字段转换为数字(现在支持不必进行额外的配置,但是在处理具有数字的列时可能遇到问题,例如ID,但应该被视为字符串以避免任何不正确的处理)等。

总结一下,我的方法是:

  • 为每个客户端创建配置文件。
  • 使用Expressions或Roslyn
  • 动态地将配置文件转换为C#方法
  • 提供用于生成此配置的GUI - 这样,支持人员可以在不知道您的特殊语法(表达式)或C#语法(Roslyn)的情况下轻松指定转换。保存配置时,您可以在单个程序集中为每个客户端生成一个方法(或每个客户端单独的程序集)并保留它。我们称之为客户端库。
  • 您的主应用程序可以执行从excel,验证等读取的所有标准内容,然后调用客户端库方法以标准格式生成输出,可以在主应用程序中进一步处理。

希望你得到主旨。

编辑:添加一些代码来演示。代码有点啰嗦,但是为了理解而评论。

// this data represents your excel data
var data = new string[][] {
    new string [] { "col_1_1", "10", "09:30" },
    new string [] { "col_2_1", "12", "09:40" }
};

// you should read this from your client specific config file/section
// Remember: you should provide a GUI tool to build this config
var config = @"
            output.Add(input[0]);

            int hours = int.Parse(input[1]);
            DateTime date = DateTime.Parse(input[2]);
            date = date.AddHours(hours);
            output.Add(""Custom Text: "" + date);
";

// this template code should be picked up from a 
// non client specific config file/section
var code = @"
using System;
using System.Collections.Generic;
using System.Linq;

namespace ClientLibrary {
    static class ClientLibrary {
        public static List<string> Client1(string[] input) {
            var output = new List<string>();

            <<code-from-config>>

            return output;
        }
    }
}
";

// Inject client configuration into template to form full code
code = code.Replace(@"<<code-from-config>>", config);

// Compile your dynamic method and get a reference to it
var references = new MetadataReference[] {
    MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
};

CSharpCompilation compilation = CSharpCompilation.Create(
    null,
    syntaxTrees: new[] { CSharpSyntaxTree.ParseText(code) },
    references: references,
    options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));


MethodInfo clientMethod = null;
using (var ms = new MemoryStream()) {
    EmitResult result = compilation.Emit(ms);

    if (!result.Success) {
        foreach (Diagnostic diagnostic in result.Diagnostics) {
            Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
        }
    } else {
        ms.Seek(0, SeekOrigin.Begin);
        Assembly assembly = Assembly.Load(ms.ToArray());
        clientMethod = assembly.GetType("ClientLibrary.ClientLibrary").GetMethod("Client1");
    }
}

if (clientMethod == null)
    return;

// Do transformation
foreach (string[] row in data) {
    var output = clientMethod.Invoke(null, new object[] { row }) as List<string>;
    Console.WriteLine(string.Join("|", output));
}

你需要一些nuget库来编译它,以及它们相应的using子句

nuget install Microsoft.Net.Compilers   # Install C# and VB compilers
nuget install Microsoft.CodeAnalysis    # Install Language APIs and Services

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

正如您所注意到的,唯一需要担心的是自动生成转换代码的GUI - 我在这里没有提供。如果你想要简单的变换,那应该很容易,但是对于复杂的变换,它将更加复杂

答案 1 :(得分:0)

听起来你期望你的最终用户具备足够的技术知识,能够理解你将要编写的这种配置机制。如果他们可以处理这种级别的技术细节,那么为他们提供Excel书籍和官方Excel模板可能会更简单,该模板包含导入应用程序所需的所有列,他们可以手动按摩数据到规范。

否则,我会建议一些基于策略设计的模式解决方案来构建已知格式的“数据按摩器”类库,并在遇到新格式时添加新类。例如

public interface IClientDataImporter
{ 
    List<MyCustomRowStructure> Import(string filename); 
}

// client 1 importer
public class ClientOneImporter : IClientDataImporter
{
    public List<MyCustomRowStructure> Import(string filename)
    {
       var result = new List<MyCustomRowStructure>();
       // ..... insert custom logic here
       return result;
    }
}

// client 2 importer
public class ClientTwoImporter : IClientDataImporter
{
    public List<MyCustomRowStructure> Import(string filename)
    {
       var result = new List<MyCustomRowStructure>();
       // ..... insert custom logic here
       return result;
    }
}

// repeat up to however many formats you need

// then.....

public class ExcelToDatabaseImporter
{
    public void ImportExcelFile(string filename, string clientName)
    {
         var myValidData = GetClientDataImporter(clientName).Import(filename);
         StickMyDataToMyDatabase(myValidData); // this is where you would load the structure into the db... won't need to touch every time a new format is encountered
    }
    public IClientDataImporter GetClientDataImporter(string clientName)
    {
         switch (clientName):
            case "ClientOne":
                return new ClientOneImporter(); 
                break;
            case "ClientTwo":
                return new ClientTwoImporter(); 
                break;
            default:
                throw new ArgumentException("No importer for client");
                break;

    }
}

答案 2 :(得分:-2)

我建议你为每个excel文件维护一个xml配置文件。 xml配置必须由工具读取,可以是控制台应用程序,并根据xml配置生成新的CSV文件。

由于XML配置文件可以通过任何文本编辑器轻松编辑,因此用户可以更新相同的文件。