Web Analytics

MiniExcel

⭐ 3234 stars Traditional Chinese by mini-software

NuGet Build status star GitHub stars version Ask DeepWiki


本專案是 .NET 基金會 的一部分,並遵守其 行為準則


English | 简体中文 | 繁體中文 | 日本語 | 한국어 | हिन्दी | ไทย | Français | Deutsch | Español | Italiano | Русский | Português | Nederlands | Polski | العربية | فارسی | Türkçe | Tiếng Việt | Bahasa Indonesia


您的 Stars捐款 可以讓 MiniExcel 更加完善


簡介

MiniExcel 是一款簡單且高效的 .NET Excel 處理工具,專為降低記憶體使用量而設計。

目前,大多數主流框架需要將 Excel 文件中的所有資料載入記憶體以便操作,但這可能會造成記憶體消耗問題。MiniExcel 採用不同的方法:以串流方式逐行處理資料,將原本可能高達數百 MB 的消耗降至僅需數 MB,有效防止記憶體不足(OOM)問題。

flowchart LR
    A1(["Excel analysis
process"]) --> A2{{"Unzipping
XLSX file"}} --> A3{{"Parsing
OpenXML"}} --> A4{{"Model
conversion"}} --> A5(["Output"])

B1(["Other Excel
Frameworks"]) --> B2{{"Memory"}} --> B3{{"Memory"}} --> B4{{"Workbooks &
Worksheets"}} --> B5(["All rows at
the same time"])

C1(["MiniExcel"]) --> C2{{"Stream"}} --> C3{{"Stream"}} --> C4{{"POCO or dynamic"}} --> C5(["Deferred execution
row by row"])

classDef analysis fill:#D0E8FF,stroke:#1E88E5,color:#0D47A1,font-weight:bold; classDef others fill:#FCE4EC,stroke:#EC407A,color:#880E4F,font-weight:bold; classDef miniexcel fill:#E8F5E9,stroke:#388E3C,color:#1B5E20,font-weight:bold;

class A1,A2,A3,A4,A5 analysis; class B1,B2,B3,B4,B5 others; class C1,C2,C3,C4,C5 miniexcel;

功能特色

2.0 版預覽

我們正在開發下一代 MiniExcel 版本,將會有全新模組化且聚焦的 API、 核心與 Csv 功能分開的 nuget 套件、完整支援 IAsyncEnumerable 非同步串流查詢, 以及更多即將推出的新功能!這些套件將以預覽版發佈,歡迎大家試用並給予回饋!

如果有興趣,請同時參閱新文件升級說明

開始使用

安裝方式

您可以從 NuGet 安裝套件

發行說明

請參閱發行說明

待辦事項

請檢查 TODO

效能

基準測試的程式碼可在 MiniExcel.Benchmarks 找到。

用於效能測試的檔案是 Test1,000,000x10.xlsx,這是一份 32MB 的文件,包含 1,000,000 行 * 10 列,所有儲存格皆填入字串 "HelloWorld"。

要執行所有基準測試,請使用:

dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join
您可以在這裡找到最新版本的基準測試結果。

Excel 查詢/匯入

#### 1. 執行查詢並將結果對應到強型別的 IEnumerable [[試試看]](https://dotnetfiddle.net/w5WD1J)

建議使用 Stream.Query,因為效率更佳。

public class UserAccount
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public DateTime BoD { get; set; }
    public int Age { get; set; }
    public bool VIP { get; set; }
    public decimal Points { get; set; }
}

var rows = MiniExcel.Query(path);

// or

using (var stream = File.OpenRead(path)) var rows = stream.Query();

image

#### 2. 執行查詢並將其對應到動態物件清單(不使用 head)[[試試看]](https://dotnetfiddle.net/w5WD1J)

| MiniExcel | 1 | |-----------|---| | Github | 2 |


var rows = MiniExcel.Query(path).ToList();

// or using (var stream = File.OpenRead(path)) { var rows = stream.Query().ToList();

Assert.Equal("MiniExcel", rows[0].A); Assert.Equal(1, rows[0].B); Assert.Equal("Github", rows[1].A); Assert.Equal(2, rows[1].B); }

#### 3. 執行查詢並以第一列作為標題 [[試試看]](https://dotnetfiddle.net/w5WD1J)

注意:相同欄位名稱會使用最右邊的那一個

輸入的 Excel:

| 欄位1 | 欄位2 | |-----------|---------| | MiniExcel | 1 | | Github | 2 |


var rows = MiniExcel.Query(useHeaderRow:true).ToList();

// or

using (var stream = File.OpenRead(path)) { var rows = stream.Query(useHeaderRow:true).ToList();

Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); Assert.Equal("Github", rows[1].Column1); Assert.Equal(2, rows[1].Column2); }

#### 4. 查詢支援 LINQ 擴充方法 First/Take/Skip ...等

查詢 First

var row = MiniExcel.Query(path).First();
Assert.Equal("HelloWorld", row.A);

// or

using (var stream = File.OpenRead(path)) { var row = stream.Query().First(); Assert.Equal("HelloWorld", row.A); }

MiniExcel/ExcelDataReader/ClosedXML/EPPlus 之間的效能比較 queryfirst

#### 5. 依工作表名稱查詢

MiniExcel.Query(path, sheetName: "SheetName");
//or
stream.Query(sheetName: "SheetName");
#### 6. 查詢所有工作表名稱及行數

var sheetNames = MiniExcel.GetSheetNames(path);
foreach (var sheetName in sheetNames)
{
    var rows = MiniExcel.Query(path, sheetName: sheetName);
}
#### 7. 取得欄位

var columns = MiniExcel.GetColumns(path); // e.g result : ["A","B"...]

var cnt = columns.Count; // get column count

#### 8. 動態查詢將資料列轉型為 IDictionary

foreach(IDictionary row in MiniExcel.Query(path))
{
    //..
}

// or var rows = MiniExcel.Query(path).Cast>(); // or Query specified ranges (capitalized) // A2 represents the second row of column A, C3 represents the third row of column C // If you don't want to restrict rows, just don't include numbers var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast>();

#### 9. 查詢 Excel 並返回 DataTable

不建議這麼做,因為 DataTable 會將所有資料載入記憶體,這樣會失去 MiniExcel 低記憶體消耗的特性。

``C# var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true);

image

#### 10. 指定開始讀取資料的儲存格

csharp MiniExcel.Query(path,useHeaderRow:true,startCell:"B3")
image

#### 11. 填充合併儲存格

注意:相較於「不使用合併填充」效率較慢

原因:OpenXml 標準將 mergeCells 放在檔案底部,導致需要對 sheetxml 進行兩次遍歷

csharp var config = new OpenXmlConfiguration() { FillMergedCells = true }; var rows = MiniExcel.Query(path, configuration: config);
image

支援變長與變寬的多列多欄填充

image

#### 12. 透過磁碟快取讀取大型檔案(磁碟快取 - SharedString)

如果 SharedStrings 的大小超過 5 MB,MiniExcel 預設會使用本地磁碟快取,例如,10x100000.xlsx(一百萬筆資料),當停用磁碟快取時,最大記憶體用量為 195MB,但啟用磁碟快取只需 65MB。注意,此優化會有一些效能成本,因此在這種情況下,讀取時間會從 7.4 秒增加到 27.2 秒。如果你不需要這個功能,可以用下列程式碼關閉磁碟快取:

csharp var config = new OpenXmlConfiguration { EnableSharedStringCache = false }; MiniExcel.Query(path,configuration: config)
您可以使用 SharedStringCacheSize 來更改 sharedString 檔案大小,超過指定大小時將進行磁碟快取
csharp var config = new OpenXmlConfiguration { SharedStringCacheSize=50010241024 }; MiniExcel.Query(path, configuration: config);
image

image

建立/匯出 Excel

  • 必須是具有公有無參數建構子的非抽象型別。
  • MiniExcel 支援 IEnumerable 參數的延遲執行,如果您想使用最少的記憶體,請不要呼叫如 ToList 等方法
例如:ToList 或不使用記憶體 image

#### 1. 匿名型別或強型別 [[試試看]](https://dotnetfiddle.net/w5WD1J)

csharp var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); MiniExcel.SaveAs(path, new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} });
#### 2. IEnumerable>

csharp var values = new List>() { new Dictionary{{ "Column1", "MiniExcel" }, { "Column2", 1 } }, new Dictionary{{ "Column1", "Github" }, { "Column2", 2 } } }; MiniExcel.SaveAs(path, values);
建立檔案結果:

| 欄位1 | 欄位2 | |-----------|---------| | MiniExcel | 1 | | Github | 2 |

#### 3. IDataReader

  • 建議使用,可以避免將所有資料載入記憶體
csharp MiniExcel.SaveAs(path, reader);
image

DataReader 匯出多個工作表(建議使用 Dapper ExecuteReader)

csharp using (var cnn = Connection) { cnn.Open(); var sheets = new Dictionary(); sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); MiniExcel.SaveAs("Demo.xlsx", sheets); }

#### 4. 資料表格

  • 不建議使用,因為它會將所有資料載入記憶體
  • DataTable 會優先使用 Caption 作為欄位名稱,然後才使用欄位名稱
csharp var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); var table = new DataTable(); { table.Columns.Add("Column1", typeof(string)); table.Columns.Add("Column2", typeof(decimal)); table.Rows.Add("MiniExcel", 1); table.Rows.Add("Github", 2); }

MiniExcel.SaveAs(path, table);

####  5. Dapper 查詢

感謝 @shaofing #552,請使用 CommandDefinition + CommandFlags.NoCache

csharp using (var connection = GetConnection(connectionString)) { var rows = connection.Query( new CommandDefinition( @"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2" , flags: CommandFlags.NoCache) ); // Note: QueryAsync will throw close connection exception MiniExcel.SaveAs(path, rows); }
以下程式碼將把所有資料載入記憶體

csharp using (var connection = GetConnection(connectionString)) { var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); MiniExcel.SaveAs(path, rows); }
#### 6. SaveAs 到 MemoryStream  [[試試看]](https://dotnetfiddle.net/JOen0e)

csharp using (var stream = new MemoryStream()) //support FileStream,MemoryStream ect. { stream.SaveAs(values); }
例如:導出 Excel 的 API

csharp public IActionResult DownloadExcel() { var values = new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} };

var memoryStream = new MemoryStream(); memoryStream.SaveAs(values); memoryStream.Seek(0, SeekOrigin.Begin); return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "demo.xlsx" }; }

#### 7. 建立多個工作表

csharp // 1. Dictionary var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; var sheets = new Dictionary { ["users"] = users, ["department"] = department }; MiniExcel.SaveAs(path, sheets);

// 2. DataSet var sheets = new DataSet(); sheets.Add(UsersDataTable); sheets.Add(DepartmentDataTable); //.. MiniExcel.SaveAs(path, sheets);

image

#### 8. 表格樣式選項

預設樣式

image

無樣式設定

csharp var config = new OpenXmlConfiguration() { TableStyles = TableStyles.None }; MiniExcel.SaveAs(path, value,configuration:config);
image

#### 9. 自動篩選

從 v0.19.0 開始,OpenXmlConfiguration.AutoFilter 可以啟用或停用自動篩選,預設值為 true,設定自動篩選的方法如下:

csharp MiniExcel.SaveAs(path, value, configuration: new OpenXmlConfiguration() { AutoFilter = false });

#### 10. 建立映像檔

csharp var value = new[] { new { Name="github",Image=File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png"))}, new { Name="google",Image=File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png"))}, new { Name="microsoft",Image=File.ReadAllBytes(PathHelper.GetFile("images/microsoft_logo.png"))}, new { Name="reddit",Image=File.ReadAllBytes(PathHelper.GetFile("images/reddit_logo.png"))}, new { Name="statck_overflow",Image=File.ReadAllBytes(PathHelper.GetFile("images/statck_overflow_logo.png"))}, }; MiniExcel.SaveAs(path, value);
image

#### 11. 位元組陣列檔案導出

自 1.22.0 起,當值類型為 byte[] 時,系統會預設在儲存格中保存檔案路徑,匯入時系統可自動轉換為 byte[]。若不想啟用此功能,可將 OpenXmlConfiguration.EnableConvertByteArray 設為 false,這樣可以提升系統效能。

image

自 1.22.0 起,當值類型為 byte[] 時,系統會預設在儲存格中保存檔案路徑,匯入時系統可自動轉換為 byte[]。若不想啟用此功能,可將 OpenXmlConfiguration.EnableConvertByteArray 設為 false,這樣可以提升系統效能。

image

#### 12. 垂直合併相同儲存格

此功能僅支援 xlsx 格式,並會在 @merge 和 @endmerge 標記之間將儲存格垂直合併。 你可以使用 @mergelimit 來限制垂直合併儲存格的範圍。

csharp var mergedFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx");

var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx";

MiniExcel.MergeSameCells(mergedFilePath, path);

csharp var memoryStream = new MemoryStream();

var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx";

memoryStream.MergeSameCells(path);

合併前後的檔案內容:

無合併限制:

Screenshot 2023-08-07 at 11 59 24

Screenshot 2023-08-07 at 11 59 57

有合併限制:

Screenshot 2023-08-08 at 18 21 00

Screenshot 2023-08-08 at 18 21 40

#### 13. 跳過 null 值

新增明確選項以針對 null 值寫入空白儲存格:

csharp DataTable dt = new DataTable();

/ ... /

DataRow dr = dt.NewRow();

dr["Name1"] = "Somebody once"; dr["Name2"] = null; dr["Name3"] = "told me.";

dt.Rows.Add(dr);

OpenXmlConfiguration configuration = new OpenXmlConfiguration() { EnableWriteNullValueCell = true // Default value. };

MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration);

image

xml Somebody once told me.

先前行為:

csharp / ... /

OpenXmlConfiguration configuration = new OpenXmlConfiguration() { EnableWriteNullValueCell = false // Default value is true. };

MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration);

image

xml Somebody once told me.
適用於 null 和 DBNull 值。

#### 14. 凍結窗格

csharp / ... /

OpenXmlConfiguration configuration = new OpenXmlConfiguration() { FreezeRowCount = 1, // default is 1 FreezeColumnCount = 2 // default is 0 };

MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration);

image

填充資料到 Excel 範本

  • 宣告方式類似於 Vue 模板的 {{變數名稱}},或集合渲染 {{集合名稱.欄位名稱}}
  • 集合渲染支援 IEnumerable/DataTable/DapperRow
#### 1. 基本填充

範本: image

結果: image

程式碼:

csharp // 1. By POCO var value = new { Name = "Jack", CreateDate = new DateTime(2021, 01, 01), VIP = true, Points = 123 }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

// 2. By Dictionary var value = new Dictionary() { ["Name"] = "Jack", ["CreateDate"] = new DateTime(2021, 01, 01), ["VIP"] = true, ["Points"] = 123 }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

#### 2. IEnumerable 資料填充

注意1:使用相同欄位的第一個 IEnumerable 作為填充列表的依據

範本: image

結果: image

程式碼:

csharp //1. By POCO var value = new { employees = new[] { new {name="Jack",department="HR"}, new {name="Lisa",department="HR"}, new {name="John",department="HR"}, new {name="Mike",department="IT"}, new {name="Neo",department="IT"}, new {name="Loan",department="IT"} } }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

//2. By Dictionary var value = new Dictionary() { ["employees"] = new[] { new {name="Jack",department="HR"}, new {name="Lisa",department="HR"}, new {name="John",department="HR"}, new {name="Mike",department="IT"}, new {name="Neo",department="IT"}, new {name="Loan",department="IT"} } }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

#### 3. 複雜資料填充

注意:支援多工作表並使用相同變數

範本:

image

結果:

image

csharp // 1. By POCO var value = new { title = "FooCompany", managers = new[] { new {name="Jack",department="HR"}, new {name="Loan",department="IT"} }, employees = new[] { new {name="Wade",department="HR"}, new {name="Felix",department="HR"}, new {name="Eric",department="IT"}, new {name="Keaton",department="IT"} } }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

// 2. By Dictionary var value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = new[] { new {name="Jack",department="HR"}, new {name="Loan",department="IT"} }, ["employees"] = new[] { new {name="Wade",department="HR"}, new {name="Felix",department="HR"}, new {name="Eric",department="IT"}, new {name="Keaton",department="IT"} } }; MiniExcel.SaveAsByTemplate(path, templatePath, value);

#### 4. 填充大數據效能

注意:在 MiniExcel 中,使用 IEnumerable 延遲執行而非 ToList,可以節省最大記憶體用量

image

#### 5. 儲存格值自動對應型別

範本

image

結果

image

類別

csharp public class Poco { public string @string { get; set; } public int? @int { get; set; } public decimal? @decimal { get; set; } public double? @double { get; set; } public DateTime? datetime { get; set; } public bool? @bool { get; set; } public Guid? Guid { get; set; } }

程式碼

csharp var poco = new TestIEnumerableTypePoco { @string = "string", @int = 123, @decimal = decimal.Parse("123.45"), @double = (double)123.33, @datetime = new DateTime(2021, 4, 1), @bool = true, @Guid = Guid.NewGuid() }; var value = new { Ts = new[] { poco, new TestIEnumerableTypePoco{}, null, poco } }; MiniExcel.SaveAsByTemplate(path, templatePath, value);
#### 6. 範例:列出 Github 專案

範本

image

結果

image

程式碼

csharp var projects = new[] { new {Name = "MiniExcel",Link="https://github.com/mini-software/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, new {Name = "HtmlTableHelper",Link="https://github.com/mini-software/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, new {Name = "PocoClassGenerator",Link="https://github.com/mini-software/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)} }; var value = new { User = "ITWeiHan", Projects = projects, TotalStar = projects.Sum(s => s.Star) }; MiniExcel.SaveAsByTemplate(path, templatePath, value);
#### 7. 分組資料填充

csharp var value = new Dictionary() { ["employees"] = new[] { new {name="Jack",department="HR"}, new {name="Jack",department="HR"}, new {name="John",department="HR"}, new {name="John",department="IT"}, new {name="Neo",department="IT"}, new {name="Loan",department="IT"} } }; await MiniExcel.SaveAsByTemplateAsync(path, templatePath, value);
##### 1. 同時使用 @group 標籤與 @header 標籤

before_with_header

after_with_header

##### 2. 僅使用 @group 標籤,未使用 @header 標籤

before_without_header

after_without_header

##### 3. 未使用 @group 標籤

without_group

without_group_after

#### 8. 在儲存格中使用 If/ElseIf/Else 條件語句

規則:

  • 支援 DateTime、Double、Int 型別與 ==、!=、>、>=、<、<= 運算子。
  • 支援 String 型別與 ==、!= 運算子。
  • 每個語句應該位於新的一行。
  • 運算子前後應加一個空格。
  • 語句內不應有換行。
  • 儲存格必須遵循如下的精確格式。
csharp @if(name == Jack) {{employees.name}} @elseif(name == Neo) Test {{employees.name}} @else {{employees.department}} @endif
在此之前

if_before

之後

if_after

#### 9. 以 DataTable 作為參數

csharp var managers = new DataTable(); { managers.Columns.Add("name"); managers.Columns.Add("department"); managers.Rows.Add("Jack", "HR"); managers.Rows.Add("Loan", "IT"); } var value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = managers, }; MiniExcel.SaveAsByTemplate(path, templatePath, value);
#### 10. 公式

##### 1. 範例 在你的公式前加上 $,並使用 $enumrowstart$enumrowend 來標記可列舉的開始與結束行:

image

當模板渲染時,$ 前綴會被移除,$enumrowstart$enumrowend 會被替換為可列舉的開始與結束行號:

image

##### 2. 其他範例公式:

| | | |--------------|-------------------------------------------------------------------------------------------| | 總和 | $=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) | | 另類平均 | $=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}}) | | 範圍 | $=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}}) |

#### 11. 其他

##### 1. 檢查模板參數鍵

自 V1.24.0 起,預設會忽略模板中缺失的參數鍵並以空字串取代,IgnoreTemplateParameterMissing 可控制是否拋出異常。

csharp var config = new OpenXmlConfiguration() { IgnoreTemplateParameterMissing = false, }; MiniExcel.SaveAsByTemplate(path, templatePath, value, config)
image

Excel 欄位名稱/索引/忽略屬性

#### 1. 指定欄位名稱、欄位索引、欄位忽略

Excel 範例

image

程式碼

csharp public class ExcelAttributeDemo { [ExcelColumnName("Column1")] public string Test1 { get; set; } [ExcelColumnName("Column2")] public string Test2 { get; set; } [ExcelIgnore] public string Test3 { get; set; } [ExcelColumnIndex("I")] // system will convert "I" to 8 index public string Test4 { get; set; } public string Test5 { get; } //wihout set will ignore public string Test6 { get; private set; } //un-public set will ignore [ExcelColumnIndex(3)] // start with 0 public string Test7 { get; set; } }

var rows = MiniExcel.Query(path).ToList(); Assert.Equal("Column1", rows[0].Test1); Assert.Equal("Column2", rows[0].Test2); Assert.Null(rows[0].Test3); Assert.Equal("Test7", rows[0].Test4); Assert.Null(rows[0].Test5); Assert.Null(rows[0].Test6); Assert.Equal("Test4", rows[0].Test7);

#### 2. 自訂格式(ExcelFormatAttribute)

自 V0.21.0 起支援包含 ToString(string content) 方法格式的類別

類別

csharp public class Dto { public string Name { get; set; }

[ExcelFormat("MMMM dd, yyyy")] public DateTime InDate { get; set; } }

程式碼

csharp var value = new Dto[] { new Issue241Dto{ Name="Jack",InDate=new DateTime(2021,01,04)}, new Issue241Dto{ Name="Henry",InDate=new DateTime(2020,04,05)}, }; MiniExcel.SaveAs(path, value);
結果

image

查詢支援自訂格式轉換

image

#### 3. 設定欄寬(ExcelColumnWidthAttribute)

csharp public class Dto { [ExcelColumnWidth(20)] public int ID { get; set; } [ExcelColumnWidth(15.50)] public string Name { get; set; } }
#### 4. 多個欄位名稱對應到同一個屬性。

csharp public class Dto { [ExcelColumnName(excelColumnName:"EmployeeNo",aliases:new[] { "EmpNo","No" })] public string Empno { get; set; } public string Name { get; set; } }

#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute

自 1.24.0 版本起,系統支援 System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute

C# public class TestIssueI4TXGTDto { public int ID { get; set; } public string Name { get; set; } [DisplayName("Specification")] public string Spc { get; set; } [DisplayName("Unit Price")] public decimal Up { get; set; } }

#### 6. ExcelColumnAttribute

自 V1.26.0 版本起,多個屬性可以簡化如下:

csharp public class TestIssueI4ZYUUDto { [ExcelColumn(Name = "ID",Index =0)] public string MyProperty { get; set; } [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] public DateTime MyProperty2 { get; set; } }
#### 7. DynamicColumnAttribute

自從 V1.26.0 起,我們可以動態設定 Column 的屬性

csharp var config = new OpenXmlConfiguration { DynamicColumns = new DynamicExcelColumn[] { new DynamicExcelColumn("id"){Ignore=true}, new DynamicExcelColumn("name"){Index=1,Width=10}, new DynamicExcelColumn("createdate"){Index=0,Format="yyyy-MM-dd",Width=15}, new DynamicExcelColumn("point"){Index=2,Name="Account Point"}, } }; var path = PathHelper.GetTempPath(); var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12) ,point = 123.456} }; MiniExcel.SaveAs(path, value, configuration: config);
image

#### 8. 動態工作表屬性

自從 V1.31.4 版本開始,我們可以動態設定工作表的屬性。我們可以設定工作表名稱和狀態(可見性)。

csharp var configuration = new OpenXmlConfiguration { DynamicSheets = new DynamicExcelSheet[] { new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } } };

var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; var sheets = new Dictionary { ["usersSheet"] = users, ["departmentSheet"] = department };

var path = PathHelper.GetTempPath(); MiniExcel.SaveAs(path, sheets, configuration: configuration);

我們也可以使用新的屬性 ExcelSheetAttribute:

C# [ExcelSheet(Name = "Departments", State = SheetState.Hidden)] private class DepartmentDto { [ExcelColumn(Name = "ID",Index = 0)] public string ID { get; set; } [ExcelColumn(Name = "Name",Index = 1)] public string Name { get; set; } }
### 新增、刪除、更新

#### 新增

v1.28.0 支援在最後一行後插入 N 行 CSV 資料

csharp // Origin { var value = new[] { new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, }; MiniExcel.SaveAs(path, value); } // Insert 1 rows after last { var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; MiniExcel.Insert(path, value); } // Insert N rows after last { var value = new[] { new { ID=4,Name ="Frank",InDate=new DateTime(2021,06,07)}, new { ID=5,Name ="Gloria",InDate=new DateTime(2022,05,03)}, }; MiniExcel.Insert(path, value); }
image

v1.37.0 支援在現有工作簿中插入新的工作表

csharp // Origin excel { var value = new[] { new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, }; MiniExcel.SaveAs(path, value, sheetName: "Sheet1"); } // Insert a new sheet { var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; MiniExcel.Insert(path, table, sheetName: "Sheet2"); }
#### 刪除(等待中)

#### 更新(等待中)

Excel 類型自動檢查

  • MiniExcel 會根據 檔案副檔名 自動判斷是 xlsx 或 csv,但這種方式可能不準確,請手動指定。
  • Stream 無法判斷來源為哪種 excel,請手動指定。
csharp stream.SaveAs(excelType:ExcelType.CSV); //or stream.SaveAs(excelType:ExcelType.XLSX); //or stream.Query(excelType:ExcelType.CSV); //or stream.Query(excelType:ExcelType.XLSX);

CSV

#### 注意

  • 預設回傳為 string 類型,且數值不會自動轉換為數字或日期時間,除非透過強型別泛型定義類型。
#### 自訂分隔符

預設分隔符為 ,,你可以修改 Seperator 屬性來自訂分隔符

csharp var config = new MiniExcelLibs.Csv.CsvConfiguration() { Seperator=';' }; MiniExcel.SaveAs(path, values,configuration: config);
自 V1.30.1 起支援自訂分隔符功能(感謝 @hyzx86)

csharp var config = new CsvConfiguration() { SplitFn = (row) => Regex.Split(row, $"\"" target="_blank" rel="noopener noreferrer">\t,$)") .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray() }; var rows = MiniExcel.Query(path, configuration: config).ToList();

#### 自訂換行符

預設為 \r\n 作為換行字元,您可以修改 NewLine 屬性來自訂

csharp var config = new MiniExcelLibs.Csv.CsvConfiguration() { NewLine='\n' }; MiniExcel.SaveAs(path, values,configuration: config);
#### 自訂編碼

  • 預設編碼為「從位元組順序標記偵測編碼」(detectEncodingFromByteOrderMarks: true)
  • 如果您有自訂編碼需求,請修改 StreamReaderFunc / StreamWriterFunc 屬性
csharp // Read var config = new MiniExcelLibs.Csv.CsvConfiguration() { StreamReaderFunc = (stream) => new StreamReader(stream,Encoding.GetEncoding("gb2312")) }; var rows = MiniExcel.Query(path, true,excelType:ExcelType.CSV,configuration: config);

// Write var config = new MiniExcelLibs.Csv.CsvConfiguration() { StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) }; MiniExcel.SaveAs(path, value,excelType:ExcelType.CSV, configuration: config);

#### 將空字串讀取為 null

預設情況下,空值會對應為 string.Empty。你可以修改這個行為

csharp var config = new MiniExcelLibs.Csv.CsvConfiguration() { ReadEmptyStringAsNull = true };
### DataReader

#### 1. GetReader 自 1.23.0 起,您可以使用 GetDataReader

csharp using (var reader = MiniExcel.GetReader(path,true)) { while (reader.Read()) { for (int i = 0; i < reader.FieldCount; i++) { var value = reader.GetValue(i); } } }

非同步

  • v0.17.0 支援非同步(感謝 isdaniel ( SHIH,BING-SIOU)](https://github.com/isdaniel))
csharp public static Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null) public static Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null) public static Task> QueryAsync(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) public static Task> QueryAsync(this Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() public static Task> QueryAsync(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() public static Task>> QueryAsync(this Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) public static Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value) public static Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value) public static Task SaveAsByTemplateAsync(string path, string templatePath, object value) public static Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value) public static Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null)
-  v1.25.0 支援 cancellationToken

其他

#### 1. 列舉型別

請確保 excel 與屬性名稱相同,系統會自動對應(不區分大小寫)

image

自 V0.18.0 起支援列舉型別描述(Enum Description)

csharp public class Dto { public string Name { get; set; } public I49RYZUserType UserType { get; set; } }

public enum Type { [Description("General User")] V1, [Description("General Administrator")] V2, [Description("Super Administrator")] V3 }

image

自 1.30.0 版本起支援將 Excel Description 轉換為 Enum,感謝 @KaneLeung

#### 2. 將 CSV 轉換為 XLSX 或將 XLSX 轉換為 CSV

csharp MiniExcel.ConvertXlsxToCsv(xlsxPath, csvPath); MiniExcel.ConvertXlsxToCsv(xlsxStream, csvStream); MiniExcel.ConvertCsvToXlsx(csvPath, xlsxPath); MiniExcel.ConvertCsvToXlsx(csvStream, xlsxStream);
`csharp
using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read))
using (var csvStream = new MemoryStream())
{
   MiniExcel.ConvertXlsxToCsv(excelStream, csvStream);
}
#### 3. 自訂 CultureInfo

自從 1.22.0 版本起,您可以像下面這樣自訂 CultureInfo,系統預設為 CultureInfo.InvariantCulture

var config = new CsvConfiguration()
{
    Culture = new CultureInfo("fr-FR"),
};
MiniExcel.SaveAs(path, value, configuration: config);

// or MiniExcel.Query(path, configuration: config);

#### 4. 自訂緩衝區大小

    public abstract class Configuration : IConfiguration
    {
        public int BufferSize { get; set; } = 1024 * 512;
    }
#### 5. 快速模式

系統不會控制記憶體,但你可以獲得更快的儲存速度。

var config = new OpenXmlConfiguration() { FastMode = true };
MiniExcel.SaveAs(path, reader,configuration:config);
#### 6. 批次新增圖片 (MiniExcel.AddPicture)

請在批次產生資料列之前新增圖片,否則在呼叫 AddPicture 時系統將會佔用大量記憶體。

var images = new[]
{
    new MiniExcelPicture
    {
        ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")),
        SheetName = null, // default null is first sheet
        CellAddress = "C3", // required
    },
    new MiniExcelPicture
    {
        ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")),
        PictureType = "image/png", // default PictureType = image/png
        SheetName = "Demo",
        CellAddress = "C9", // required
        WidthPx = 100,
        HeightPx = 100,
    },
};
MiniExcel.AddPicture(path, images);
Image

#### 7. 取得工作表尺寸

var dim = MiniExcel.GetSheetDimensions(path);

範例:

#### 1. SQLite & Dapper 大檔案 SQL 插入避免 OOM

注意:請不要在 Query 之後呼叫 ToList/ToArray 方法,這會將所有資料載入記憶體

using (var connection = new SQLiteConnection(connectionString))
{
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    using (var stream = File.OpenRead(path))
    {
       var rows = stream.Query();
       foreach (var row in rows)
             connection.Execute("insert into T (A,B) values (@A,@B)", new { row.A, row.B }, transaction: transaction);
       transaction.Commit();
    }
}
效能表現: image

#### 2. ASP.NET Core 3.1 或 MVC 5 下載/上傳 Excel Xlsx API 範例 試用

public class ApiController : Controller
{
    public IActionResult Index()
    {
        return new ContentResult
        {
            ContentType = "text/html",
            StatusCode = (int)HttpStatusCode.OK,
            Content = @"
DownloadExcel
DownloadExcelFromTemplatePath
DownloadExcelFromTemplateBytes

Upload Excel


public IActionResult DownloadExcel() { var values = new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} }; var memoryStream = new MemoryStream(); memoryStream.SaveAs(values); memoryStream.Seek(0, SeekOrigin.Begin); return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "demo.xlsx" }; }

public IActionResult DownloadExcelFromTemplatePath() { string templatePath = "TestTemplateComplex.xlsx";

Dictionary value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = new[] { new {name="Jack",department="HR"}, new {name="Loan",department="IT"} }, ["employees"] = new[] { new {name="Wade",department="HR"}, new {name="Felix",department="HR"}, new {name="Eric",department="IT"}, new {name="Keaton",department="IT"} } };

MemoryStream memoryStream = new MemoryStream(); memoryStream.SaveAsByTemplate(templatePath, value); memoryStream.Seek(0, SeekOrigin.Begin); return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "demo.xlsx" }; }

private static Dictionary TemplateBytesCache = new Dictionary();

static ApiController() { string templatePath = "TestTemplateComplex.xlsx"; byte[] bytes = System.IO.File.ReadAllBytes(templatePath); TemplateBytesCache.Add(templatePath, bytes); }

public IActionResult DownloadExcelFromTemplateBytes() { byte[] bytes = TemplateBytesCache["TestTemplateComplex.xlsx"];

Dictionary value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = new[] { new {name="Jack",department="HR"}, new {name="Loan",department="IT"} }, ["employees"] = new[] { new {name="Wade",department="HR"}, new {name="Felix",department="HR"}, new {name="Eric",department="IT"}, new {name="Keaton",department="IT"} } };

MemoryStream memoryStream = new MemoryStream(); memoryStream.SaveAsByTemplate(bytes, value); memoryStream.Seek(0, SeekOrigin.Begin); return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = "demo.xlsx" }; }

public IActionResult UploadExcel(IFormFile excel) { var stream = new MemoryStream(); excel.CopyTo(stream);

foreach (var item in stream.Query(true)) { // do your logic etc. }

return Ok("File uploaded successfully"); } }

#### 3. 分頁查詢

void Main()
{
    var rows = MiniExcel.Query(path);

Console.WriteLine("==== No.1 Page ===="); Console.WriteLine(Page(rows,pageSize:3,page:1)); Console.WriteLine("==== No.50 Page ===="); Console.WriteLine(Page(rows,pageSize:3,page:50)); Console.WriteLine("==== No.5000 Page ===="); Console.WriteLine(Page(rows,pageSize:3,page:5000)); }

public static IEnumerable Page(IEnumerable en, int pageSize, int page) { return en.Skip(page * pageSize).Take(pageSize); }

20210419

#### 4. WebForm 透過 Memorystream 匯出 Excel

var fileName = "Demo.xlsx";
var sheetName = "Sheet1";
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
response.AddHeader("Content-Disposition", $"attachment;filename=\"{fileName}\"");
var values = new[] {
    new { Column1 = "MiniExcel", Column2 = 1 },
    new { Column1 = "Github", Column2 = 2}
};
var memoryStream = new MemoryStream();
memoryStream.SaveAs(values, sheetName: sheetName);
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(Response.OutputStream);
response.End();
#### 5. 動態 i18n 多語言與角色權限管理

如範例所示,建立一個方法來處理 i18n 和權限管理,並使用 yield return 返回 IEnumerable>,以實現動態且低記憶體的處理效果

void Main()
{
    var value = new Order[] {
        new Order(){OrderNo = "SO01",CustomerID="C001",ProductID="P001",Qty=100,Amt=500},
        new Order(){OrderNo = "SO02",CustomerID="C002",ProductID="P002",Qty=300,Amt=400},
    };

Console.WriteLine("en-Us and Sales role"); { var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; var lang = "en-US"; var role = "Sales"; MiniExcel.SaveAs(path, GetOrders(lang, role, value)); MiniExcel.Query(path, true).Dump(); }

Console.WriteLine("zh-CN and PMC role"); { var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; var lang = "zh-CN"; var role = "PMC"; MiniExcel.SaveAs(path, GetOrders(lang, role, value)); MiniExcel.Query(path, true).Dump(); } }

private IEnumerable> GetOrders(string lang, string role, Order[] orders) { foreach (var order in orders) { var newOrder = new Dictionary();

if (lang == "zh-CN") { newOrder.Add("客户编号", order.CustomerID); newOrder.Add("订单编号", order.OrderNo); newOrder.Add("产品编号", order.ProductID); newOrder.Add("数量", order.Qty); if (role == "Sales") newOrder.Add("价格", order.Amt); yield return newOrder; } else if (lang == "en-US") { newOrder.Add("Customer ID", order.CustomerID); newOrder.Add("Order No", order.OrderNo); newOrder.Add("Product ID", order.ProductID); newOrder.Add("Quantity", order.Qty); if (role == "Sales") newOrder.Add("Amount", order.Amt); yield return newOrder; } else { throw new InvalidDataException($"lang {lang} wrong"); } } }

public class Order { public string OrderNo { get; set; } public string CustomerID { get; set; } public decimal Qty { get; set; } public string ProductID { get; set; } public decimal Amt { get; set; } }

image

常見問答

#### Q: Excel 標題欄名稱與類別屬性名稱不一致時,如何對應?

A. 請使用 ExcelColumnName 屬性

image

#### Q. 如何查詢或匯出多個工作表?

A. 使用 GetSheetNames 方法,並搭配 Query 的 sheetName 參數。

var sheets = MiniExcel.GetSheetNames(path);
foreach (var sheet in sheets)
{
    Console.WriteLine($"sheet name : {sheet} ");
    var rows = MiniExcel.Query(path,useHeaderRow:true,sheetName:sheet);
    Console.WriteLine(rows);
}
image

#### 問:如何查詢或匯出有關工作表可見性的資訊?

答:使用 GetSheetInformations 方法。

var sheets = MiniExcel.GetSheetInformations(path);
foreach (var sheetInfo in sheets)
{
    Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0
    Console.WriteLine($"sheet name : {sheetInfo.Name} ");   // sheet name
    Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden
}
#### Q. 使用 Count 會將所有資料載入記憶體嗎?

不會,影像測試有 1 百萬行*10 列的資料,最大記憶體用量小於 60MB,耗時 13.65 秒

image

#### Q. Query 如何使用整數索引?

Query 的預設索引是字串 Key: A,B,C.... 如果要改為數字索引,請建立以下方法進行轉換

void Main()
{
    var path = @"D:\git\MiniExcel\samples\xlsx\TestTypeMapping.xlsx";
    var rows = MiniExcel.Query(path,true);
    foreach (var r in ConvertToIntIndexRows(rows))
    {
        Console.Write($"column 0 : {r[0]} ,column 1 : {r[1]}");
        Console.WriteLine();
    }
}

private IEnumerable> ConvertToIntIndexRows(IEnumerable rows) { ICollection keys = null; var isFirst = true; foreach (IDictionary r in rows) { if(isFirst) { keys = r.Keys; isFirst = false; }

var dic = new Dictionary(); var index = 0; foreach (var key in keys) dic[index++] = r[key]; yield return dic; } } #### 問:為什麼當匯出 Excel 且值為空時,會產生沒有標題的空白 Excel?

因為 MiniExcel 採用類似於 JSON.NET 的邏輯,從值動態取得型別以簡化 API 操作,沒有資料時無法得知型別。你可以參考 issue #133 來了解。

image

強型別 & DataTable 會產生標題,但 Dictionary 仍然會產生空白的 Excel

#### 問:如何在遇到空白列時停止 foreach?

MiniExcel 可以搭配 LINQ TakeWhile 用來停止 foreach 的迭代器。

Image

#### 問:如何移除空白列?

image

IEnumerable :

public static IEnumerable QueryWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration)
{
    var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration);
    foreach (IDictionary row in rows)
    {
        if(row.Keys.Any(key=>row[key]!=null))
            yield return row;
    }
}

資料表:

public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration)
{
    if (sheetName == null && excelType != ExcelType.CSV) /Issue #279/
        sheetName = stream.GetSheetNames().First();

var dt = new DataTable(sheetName); var first = true; var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); foreach (IDictionary row in rows) { if (first) {

foreach (var key in row.Keys) { var column = new DataColumn(key, typeof(object)) { Caption = key }; dt.Columns.Add(column); }

dt.BeginLoadData(); first = false; }

var newRow = dt.NewRow(); var isNull=true; foreach (var key in row.Keys) { var _v = row[key]; if(_v!=null) isNull = false; newRow[key] = _v; }

if(!isNull) dt.Rows.Add(newRow); }

dt.EndLoadData(); return dt; }

#### 問:如何使用 SaveAs(path,value) 來取代已存在的檔案並且不拋出 "The file ...xlsx already exists" 的錯誤

請使用 Stream 類別來自訂檔案建立邏輯,例如:

`C# using (var stream = File.Create("Demo.xlsx")) MiniExcel.SaveAs(stream,value);

或者,從 V1.25.0 起,SaveAs 支援 overwriteFile 參數以啟用/禁用覆蓋現有檔案

csharp MiniExcel.SaveAs(path, value, overwriteFile: true); ``

限制與注意事項

  • 目前不支援 xls 及加密檔案
  • xlsm 只支援查詢

參考資料

ExcelDataReader / ClosedXML / Dapper / ExcelNumberFormat

感謝

#### Jetbrains

jetbrains-variant-2

感謝為本專案免費提供 All product IDE (授權)

貢獻分享捐贈

連結 https://github.com/orgs/mini-software/discussions/754

貢獻者

--- Tranlated By Open Ai Tx | Last indexed: 2025-10-09 ---