文章目录
- 1 前言
- 2 项目地址
- 3 使用方法
- 3.1 写入 Excel
- 3.2 读取 Excel
- 3.3 读写 csv 文件
- 4 ExcelSheet 代码
1 前言
前几日,一直被如何在 Unity 中读取 Excel 的问题给困扰,网上搜索相关教程相对古老(4、5 年以前了)。之前想用 csv 文件格式替代 Excel 存储,用 WPS 也能打开 csv 文件。但一个明显的坏处是单元格内不能出现逗号(“,”),因为 csv 文件依据逗号分隔单元格。而网上相关 Excel 的插件教程也是摸不着头脑,从 B 站上找了 19 年的 EPPlus 插件视频,结果插件不好用,变量经常报空,而且项目导出也存在问题。也有教程说直接在 NuGet 包中下载 EPPlus 插件,但是 Unity 加载过后就无法识别程序集,直接将 dll 导入 Unity 中的 Plugins 文件夹下也提示无法导入,预测原因之一可能是 EPPlus 插件需要进行验证。
近日,偶然找到可以用的 EPPlus 插件,经过测试没有问题。因此分享到网上,并封装了 ExcelSheet 类,用于更加快速、方便地读写 Excel。由于之前写过 csv 的读写代码,因此也封装到 ExcelSheet 中,支持 xlsx 和 csv 两种文件格式的读写。
2 项目地址
Github 地址(项目内附带插件):https://github.com/zheliku/EPPlus-Learning。
3 使用方法
3.1 写入 Excel
public void Save(string filePath, string sheetName = null, FileFormat format = FileFormat.Xlsx);
一个 ExcelSheet 对象即存储了一个表中的内容(Dictionary 中),new 出对象后直接索引每个单元的内容,可读取也可更改:
var sheet = new ExcelSheet();
sheet[0, 0] = "1"; // 第一行第一列赋值为 1
sheet[1, 2] = "2"; // 第二行第三列赋值为 2
sheet.Save("test", "Sheet1", ExcelSheet.FileFormat.Xlsx); // 写入文件
上述 4 行代码即可完成 Excel 的写入。因此写入的大致流程为:
- 创建一个新对象 sheet;
- 通过索引器访问 sheet 的值并修改,没有修改的单元格不会写入,默认为空内容。
- 通过 Save() 方法保存到某个 Excel 文件中的某个表中。
注意:
- 通过索引器修改时,仅仅是缓存中(Dictionary)的值发生变化。要写入文件,必须调用 Save() 方法。
- Save() 方法:
- 第一个参数 filePath 是文件名,不需要带后缀(后缀名由第三个参数在方法内部决定)。
- 第二个参数 sheetName 是表名,如果 Excel 中没有该表,则会自动创建并写入内容。表名可不填,不填时默认与文件名称相同。
- 第三个参数 format 是保存的格式,有 xlsx 和 csv 两种。
- 文件默认保存在
ExcelSheet.SAVE_PATH
下,可在外部调用进行更改,或者手动更改 ExcelSheet 代码。
3.2 读取 Excel
public void Load(string filePath, string sheetName = null, FileFormat format = FileFormat.Xlsx);
可以创建 sheet 时就指定文件名进行读取:
var sheet = new ExcelSheet("test"); // 读取 test.xlsx 文件中 test 表的内容
也可以先创建空 sheet,再用 Load 方法读取:
var sheet = new ExcelSheet();
sheet.Load("test", "Sheet1") // 读取 test.xlsx 文件中 Sheet1 表的内容
读取后,内容存储在 sheet 的字典中,即可访问使用:
_sheet.Load("test", "Sheet1");
for (int i = 0; i < _sheet.RowCount; i++) { // 行遍历
for (int j = 0; j < _sheet.ColCount; j++) { // 列遍历
var value = _sheet[i, j];
if (string.IsNullOrEmpty(value)) continue;
Debug.Log($"Sheet[{i}, {j}]: {value}");
}
}
使用 Load 时,默认会先清空 sheet 中原有的内容,再读取表格,即覆盖读取。
3.3 读写 csv 文件
var sheet = new ExcelSheet();
sheet[0, 0] = "1"; // 第一行第一列赋值为 1
sheet[1, 2] = "2"; // 第二行第三列赋值为 2
sheet.Save("test", "Sheet1", ExcelSheet.FileFormat.Csv); // 写入 csv 文件
与 Excel 文件的读写类似,只需要更改 format 为 ExcelSheet.FileFormat.Csv 即可,便会保存为 test.csv 文件。需要注意,读写 csv 文件时,参数 sheetName 不起任何作用,因为 csv 文件中没有表。
4 ExcelSheet 代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using OfficeOpenXml;
/// <summary>
/// Excel 文件存储和读取器
/// </summary>
public partial class ExcelSheet
{
public static string SAVE_PATH = Application.streamingAssetsPath + "/Excel/";
private int _rowCount = 0; // 最大行数
private int _colCount = 0; // 最大列数
public int RowCount { get => _rowCount; }
public int ColCount { get => _colCount; }
private Dictionary<Index, string> _sheetDic = new Dictionary<Index, string>(); // 缓存当前数据的字典
public ExcelSheet() { }
public ExcelSheet(string filePath, string sheetName = null, FileFormat format = FileFormat.Xlsx) {
Load(filePath, sheetName, format);
}
public string this[int row, int col] {
get {
// 越界检查
if (row >= _rowCount || row < 0)
Debug.LogError($"ExcelSheet: Row {row} out of range!");
if (col >= _colCount || col < 0)
Debug.LogError($"ExcelSheet: Column {col} out of range!");
// 不存在结果,则返回空字符串
return _sheetDic.GetValueOrDefault(new Index(row, col), "");
}
set {
_sheetDic[new Index(row, col)] = value;
// 记录最大行数和列数
if (row >= _rowCount) _rowCount = row + 1;
if (col >= _colCount) _colCount = col + 1;
}
}
/// <summary>
/// 存储 Excel 文件
/// </summary>
/// <param name="filePath">文件路径,不需要写文件扩展名</param>
/// <param name="sheetName">表名,如果没有指定表名,则使用文件名。若使用 csv 格式,则忽略此参数</param>
/// <param name="format">保存的文件格式</param>
public void Save(string filePath, string sheetName = null, FileFormat format = FileFormat.Xlsx) {
string fullPath = SAVE_PATH + filePath + FileFormatToExtension(format); // 文件完整路径
var index = fullPath.LastIndexOf("/", StringComparison.Ordinal);
var directory = fullPath[..index];
if (!Directory.Exists(directory)) { // 如果文件所在的目录不存在,则先创建目录
Directory.CreateDirectory(directory);
}
switch (format) {
case FileFormat.Xlsx:
SaveAsXlsx(fullPath, sheetName);
break;
case FileFormat.Csv:
SaveAsCsv(fullPath);
break;
default: throw new ArgumentOutOfRangeException(nameof(format), format, null);
}
Debug.Log($"ExcelSheet: Save sheet \"{filePath}::{sheetName}\" successfully.");
}
/// <summary>
/// 读取 Excel 文件
/// </summary>
/// <param name="filePath">文件路径,不需要写文件扩展名</param>
/// <param name="sheetName">表名,如果没有指定表名,则使用文件名</param>
/// <param name="format">保存的文件格式</param>
public void Load(string filePath, string sheetName = null, FileFormat format = FileFormat.Xlsx) {
// 清空当前数据
Clear();
string fullPath = SAVE_PATH + filePath + FileFormatToExtension(format); // 文件完整路径
if (!File.Exists(fullPath)) { // 不存在文件,则报错
Debug.LogError($"ExcelSheet: Can't find path \"{fullPath}\".");
return;
}
switch (format) {
case FileFormat.Xlsx:
LoadFromXlsx(fullPath, sheetName);
break;
case FileFormat.Csv:
LoadFromCsv(fullPath);
break;
default: throw new ArgumentOutOfRangeException(nameof(format), format, null);
}
Debug.Log($"ExcelSheet: Load sheet \"{filePath}::{sheetName}\" successfully.");
}
public void Clear() {
_sheetDic.Clear();
_rowCount = 0;
_colCount = 0;
}
}
public partial class ExcelSheet
{
public struct Index
{
public int Row;
public int Col;
public Index(int row, int col) {
Row = row;
Col = col;
}
}
/// <summary>
/// 保存的文件格式
/// </summary>
public enum FileFormat
{
Xlsx,
Csv
}
private string FileFormatToExtension(FileFormat format) {
return $".{format.ToString().ToLower()}";
}
private void SaveAsXlsx(string fullPath, string sheetName) {
var index = fullPath.LastIndexOf("/", StringComparison.Ordinal);
var fileName = fullPath[(index + 1)..];
sheetName ??= fileName[..fileName.IndexOf(".", StringComparison.Ordinal)]; // 如果没有指定表名,则使用文件名
var fileInfo = new FileInfo(fullPath);
using var package = new ExcelPackage(fileInfo);
if (!File.Exists(fullPath) || // 不存在 Excel
package.Workbook.Worksheets[sheetName] == null) { // 或者没有表,则添加表
package.Workbook.Worksheets.Add(sheetName); // 创建表时,Excel 文件也会被创建
}
var sheet = package.Workbook.Worksheets[sheetName];
var cells = sheet.Cells;
cells.Clear(); // 先清空数据
foreach (var pair in _sheetDic) {
var i = pair.Key.Row;
var j = pair.Key.Col;
cells[i + 1, j + 1].Value = pair.Value;
}
package.Save(); // 保存文件
}
private void SaveAsCsv(string fullPath) {
using FileStream fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
Index idx = new Index(0, 0);
for (int i = 0; i < _rowCount; i++) {
idx.Row = i;
idx.Col = 0;
// 写入第一个 value
var value = _sheetDic.GetValueOrDefault(idx, "");
if (!string.IsNullOrEmpty(value))
fs.Write(Encoding.UTF8.GetBytes(value));
// 写入后续 value,需要添加 ","
for (int j = 1; j < _colCount; j++) {
idx.Col = j;
value = "," + _sheetDic.GetValueOrDefault(idx, "");
fs.Write(Encoding.UTF8.GetBytes(value));
}
// 写入 "\n"
fs.Write(Encoding.UTF8.GetBytes("\n"));
}
}
private void LoadFromXlsx(string fullPath, string sheetName) {
var index = fullPath.LastIndexOf("/", StringComparison.Ordinal);
var fileName = fullPath[(index + 1)..];
sheetName ??= fileName[..fileName.IndexOf(".", StringComparison.Ordinal)]; // 如果没有指定表名,则使用文件名
var fileInfo = new FileInfo(fullPath);
using var package = new ExcelPackage(fileInfo);
var sheet = package.Workbook.Worksheets[sheetName];
if (sheet == null) { // 不存在表,则报错
Debug.LogError($"ExcelSheet: Can't find sheet \"{sheetName}\" in file \"{fullPath}\"");
return;
}
_rowCount = sheet.Dimension.Rows;
_colCount = sheet.Dimension.Columns;
var cells = sheet.Cells;
for (int i = 0; i < _rowCount; i++) {
for (int j = 0; j < _colCount; j++) {
var value = cells[i + 1, j + 1].Text;
if (string.IsNullOrEmpty(value)) continue; // 有数据才记录
_sheetDic.Add(new Index(i, j), value);
}
}
}
private void LoadFromCsv(string fullPath) {
// 读取文件
string[] lines = File.ReadAllLines(fullPath); // 读取所有行
for (int i = 0; i < lines.Length; i++) {
string[] line = lines[i].Split(','); // 读取一行,逗号分割
for (int j = 0; j < line.Length; j++) {
if (line[j] != "") // 有数据才记录
_sheetDic.Add(new Index(i, j), line[j]);
}
// 更新最大行数和列数
_colCount = Mathf.Max(_colCount, line.Length);
_rowCount = i + 1;
}
}
}