csv/json/list/datatable匯出為excel的通用模組設計

俞正東發表於2022-03-05

匯出excel的場景我一般都是一個List直接匯出成一張sheet,用Npoi.Mapper庫很方便,最近我經常是需要將介面返回的jsonarray轉成一張excel表,比如從elasticsearch中或者從clickhouse中拿到的列是不固定的,比如從clickhouse中是根據select語句中的欄位集合變化而變化,無法提前定義一個未知class再反序列化!所以我想了另外一種辦法,也就是本文要分享的:動態生成class+模板引擎的方式來生成Excel/Word/Html/PDF等

程式碼我已放到github上

https://github.com/yuzd/Exporter

歡迎star!

整體思路是:

  • 1如果無法預先定義class那就根據input來動態生成class類T
  • 2再把資料裝載到List集合中
  • 3利用模板引擎+List生成目標檔案

第一步:先根據input來動態生成class類T

根據目前需要,input分成2大類

1. 無法確定class型別的
  • CSV格式逗號分隔的string集合
  • jsonarray字串
  • DataTable
  • DataSet
  • DataReader

針對這種場景,那麼我們需要按流程一步步來先動態生成class類

2. 已經知道class型別的
  • List集合(T即為我們想要的class型別)
  • key,value形式的Map集合(key集合作為列,value的型別即為我們想要的class型別)

針對這種場景,那麼在流程中我們只需要最後一步利用模板引擎即可

動態生成class類的文字

1. csv的場景

csv檔案本身雙擊可以開啟,csv檔案比如你發到qq或者微信,預覽不了,轉成excel的話可以直接預覽


var arrCSV = new List<string>();
arrCSV.Add("Name,Age,測試");
arrCSV.Add("1112,20,hello");
arrCSV.Add("1232,21,world");

先根據第一列"Name,Age,測試"採用Razor模板引擎生成一個class的文字


using System;
public class @Model.ClassName {
//constructor
public @Model.ClassName (
    @foreach(var prop in Model.Properties){
    <text>string @prop , </text>
    }
    //add a fake property
string fake=null)
{
    @foreach(var prop in Model.Properties){
    <text>this.@prop = @prop;</text>
    }
}//end constructor
//properties
@foreach(var prop in Model.Properties){
    <text>public string @prop{get;set;}</text>
    }
 
}//end class

生成的class文字是長這樣的:

image
image
  • 為了後續確保欄位相同的都共用一個class型別,class的名稱預設的生成規則是Data_${欄位拼接string}的hash

2. jsonarray的場景



string json = @"[
        { 'Name':'Andrei Ignat', 
            'WebSite':'http://xxxx/',
            'CV':'adada.xls'        
        },
    { 'Name':'Your Name', 
            'WebSite':'http://your website',
            'CV':'cv.doc'        
        }
    ]";
var data2 = ExportFactory.ExportDataJson(json, ExportToFormat.Excel);
File.WriteAllBytes("a.xlsx", data2);


採用Xamasoft.JsonClassGenerator庫生成class文字

public class Data1888056300
{
    public string Name { get; set; }
    public string WebSite { get; set; }
    public string CV { get; set; }
}

3. DataTable等其他的場景

比如DataTable,先從裡面取所有的列,然後按照和1同樣的方式即可生成class文字

動態編譯生成class類

按照上面的方式生成了class的類文字,接下來需要動態編譯成class類並載入到當前的Domain中。

採用natasha元件,用法如下


AssemblyCSharpBuilder builder = new("ExportCoreClass")
{
    Domain = DomainManagement.Default
};
//code = class文字
builder.Add(code);
var asm = builder.GetAssembly();
//這個type就是我們想要的class型別
var type = asm.DefinedTypes.First(t => t.Name == mrj.ClassName);

這裡要注意一點,因為className我們是特定規則生成的,所以在動態編譯生成class之前先檢查當前Domain 中是否已存在

/// <summary>
/// 檢測當前domain已經建立好了相同的class
/// </summary>
/// <param name="className"></param>
/// <returns></returns>
private static Type? GetExistedTypeInCurrentDomain(string className)
{
    try
    {
        // 檢測當前domain已經建立好了相同的class
        var typeExisting = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(a => a.GetTypes())
            .FirstOrDefault(t => t.FullName != null && t.FullName.Equals(className));

        if (typeExisting != null)
            return typeExisting;
    }
    catch (Exception)
    {
        //ignore
    }
    return null;
}

第二步:資料裝載到List集合中

這一步比較簡單,因為class型別已經生成好了,接下來就是採用反射的方式,建立一個List集合, 在把input資料的每一項根據反射生成T的例項裝載進去就好了

image
image

第二步:利用模板引擎+List生成目標檔案

其實上面已經用了Razor模板引擎來幫我們生成class類文字了,Razor模板引擎非常強大,擴充套件性也非常好

這裡我們採用不同的型別對應不同的Razor模板,目前已經實現了的有:

  • Excel2003
  • Excel2007及以上
  • Word2003
  • Word2007及以上
  • Html(Table)

如下圖:

image
image

採用工廠模式暴露對外使用,不同的output採用不同的類進行處理,也方便日後新增其他型別的匯出(比如PDF)

以Excel為例子

非POI庫的方式來生成excel,先介紹下excel的模板是什麼樣子

<= 2003版本之前的excel是這樣的結構:

image
image

= 2007版本的excel是這樣的結構:

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<worksheet xmlns='http://schemas.openxmlformats.org/spreadsheetml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships'>
    <sheetData>

@Include(Model.NameOfT+"Excel2007Header")

@foreach(var item in Model.Data){
  @Include(Model.NameOfT+"Excel2007Item",item)
} 

    </sheetData>
</worksheet>

根據上面的xml結構還需要用DocumentFormat.OpenXml庫來生成excel


/// <summary>
/// 生成excel位元組陣列
/// </summary>
/// <param name="worksheetName"></param>
/// <param name="textSheet"></param>
/// <returns></returns>
private byte[] CreateExcel2007(string[] worksheetName, string[] textSheet)
{
    using var ms = new MemoryStream();
    using var sd = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook);
    var workbook = sd.AddWorkbookPart();
    var strSheets = "<sheets>";
    for (var i = 0; i < worksheetName.Length; i++)
    {
        var sheet = workbook.AddNewPart<WorksheetPart>();
        WriteToPart(sheet, textSheet[i]);
        strSheets += string.Format("<sheet name=\"{1}\" sheetId=\"{2}\" r:id=\"{0}\" />",
            workbook.GetIdOfPart(sheet), worksheetName[i], (i + 1));
    }
    strSheets += "</sheets>";
    WriteToPart(workbook, string.Format(
        "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">{0}</workbook>",
        strSheets
    ));

    sd.Close();
    return ms.ToArray();
}

其他型別的output也是類似套路,制定模板,然後List資料+模板+加工=最終檔案

nuget地址以及常用的使用方法

Install-Package ExporterCore

csv(逗號分隔)匯出為excel


var arrCSV = new List<string>();
arrCSV.Add("Name,WebSite,連線");
arrCSV.Add("111,http://msprogrammer.serviciipeweb.ro/,http://serviciipeweb.ro/iafblog/content/binary/cv.doc");
arrCSV.Add("123,http://msprogrammer.serviciipeweb.ro/,http://serviciipeweb.ro/iafblog/content/binary/cv.doc");

var data = ExportFactory.ExportDataCsv(arrCSV.ToArray(), ExportToFormat.Excel2007);
File.WriteAllBytes("a.xlsx", data);

json匯出為excel

string json = @"[
        { 'Name':'Andrei Ignat', 
            'WebSite':'http://xxx/',
            'CV':'http://aaaaa/binary/cv.doc'        
        },
    { 'Name':'Your Name', 
            'WebSite':'http://your website',
            'CV':'cv.doc'        
        }
    ]";
var data2 = ExportFactory.ExportDataJson(json, ExportToFormat.Excel);
File.WriteAllBytes("a.xlsx", data2);

list匯出為excel


List<Person> listWithPerson = new List<Person>
{
    new Person
    {
        Name = "aa",
        Aget = 12
    },
    new Person
    {
        Name = "dasda",
        Aget = 1222
    }
};
var data = ExportFactory.ExportData(listWithPerson, ExportToFormat.Excel);
File.WriteAllBytes("a.xlsx", data);

多個list匯出同個excel的多張Sheet


var p = new Person { Name = "andrei", WebSite = "http://xxx.ro/", CV = "http://daary/cv.doc" };
var p1 = new Person { Name = "you", WebSite = "http://yourwebsite.com/" };
var list = new List<Person>() { p, p1 };

var kvp = new List<Tuple<string, string>>();
for (int i = 0; i < 10; i++)
{
    var q = new Tuple<string, string>("This is key " + i, "Value " + i);
    kvp.Add(q);
}

var export = new ExportExcel2007<Person>();
var data = export.ExportMultipleSheets(new IList[] { list, kvp });
File.WriteAllBytes("multiple.xlsx", data);

未完待續

後續可能會完善一下內建的模板可以讓使用者定製化,這樣就完整了

關注公眾號一起學習

csv/json/list/datatable匯出為excel的通用模組設計

 

相關文章