第22课:读写Excel文件
在数据驱动的应用中交换数据,Excel一种常用的格式,比如,将服务器端导出的数据转换为Excel文件,下载后可以直接加工使用,数据加工完成后可以上传并批量更新,为用户带来了极大的便利性。本课将讨论Excel数据的读取与写入。
本节操作使用了第三方资源NPOI,可以从https://github.com/nissl-lab/npoi获取相关信息。
在Visual Studio 2019集成开发环境中,可以通过菜单项“工具”>>“NuGet包管理器”>>“管理解决方案的NuGet程序包”选择安装相关资源,主要包括NPOI.dll、NPOI.OOXML.dll、NPOI.OpenXml4Net.dll和NPOI.OpenXmlFormats.dll等文件,这些文件会自动安装到网站根目录下的bin目录中。
将数据写入Excel文件时,.xls和.xlsx格式的操作是不同的,需要分别编码实现;而读取Excel文件时,则可以使用相同的代码完成。代码中使用tExcel类完成Excel文件的读取和写入操作。
写入Excel文件
首先来看Excel文件的写入操作,如下面的代码(/app_code/office/tExcel.cs)。
C# |
using System;
using System.IO;
using System.Data;
using System.Collections.Generic;
using NPOI.SS.UserModel;
using NPOI.HSSF.UserModel;
using NPOI.XSSF.UserModel;
public static class tExcel
{
// 将DataTable对象写入Excel文件的第一个工作表
public static bool WriteExcel(DataTable tbl, string path,
bool writeColName = true)
{
string ext = Path.GetExtension(path).ToLower();
if (ext == ".xls")
return WriteXls(tbl, path, writeColName);
else
return WriteXlsx(tbl, path, writeColName);
}
// 将DataTable对象写入.xls文件第一个工作表
private static bool WriteXls(DataTable tbl, string xlsFile,
bool writeColName = true)
{
try
{
if (tbl == null || tbl.Columns.Count < 1)
return false;
//
HSSFWorkbook wb = new HSSFWorkbook();
ISheet sheet = wb.CreateSheet("Sheet1");
int curRow = 0;
// 写入列名
if (writeColName)
{
IRow sheetRow = sheet.CreateRow(0);
for (int col = 0; col < tbl.Columns.Count; col++)
{
ICell cell = sheetRow.CreateCell(col);
cell.SetCellType(CellType.String);
cell.SetCellValue(tbl.Columns[col].ColumnName);
}
curRow++;
}
// 日期和时间格式
IDataFormat dataFormat = wb.CreateDataFormat();
ICellStyle datetimeStyle = wb.CreateCellStyle();
datetimeStyle.DataFormat =
dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
// 写入数据行
for (int row = 0; row < tbl.Rows.Count; row++)
{
IRow sheetRow = sheet.CreateRow(curRow++);
for (int col = 0; col < tbl.Columns.Count; col++)
{
ICell cell = sheetRow.CreateCell(col);
Type dataType = tbl.Columns[col].DataType;
if (tType.IsNumeric(dataType))
{
cell.SetCellType(CellType.Numeric);
cell.SetCellValue(tDbl.GetValue(tbl.Rows[row][col]));
}
else if (dataType.Name == "DateTime")
{
cell.CellStyle = datetimeStyle;
cell.SetCellValue(tDate.GetValue(tbl.Rows[row][col]));
}
else if (dataType.Name == "Boolean")
{
cell.SetCellType(CellType.Boolean);
cell.SetCellValue(tBool.GetValue(tbl.Rows[row][col]));
}
else
{
cell.SetCellType(CellType.String);
cell.SetCellValue(tStr.GetValue(tbl.Rows[row][col]));
}
}
}
//
FileStream fs = new FileStream(xlsFile, FileMode.Create);
wb.Write(fs);
fs.Close();
//
sheet = null;
wb = null;
return true;
}
catch (Exception ex)
{
tLog.E(ex, -1000L, "tExcel.WriteXls");
return false;
}
}
// 将DataTable对象写入.xlsx文件第一个工作表
private static bool WriteXlsx(DataTable tbl, string xlsxFile,
bool writeColName = true)
{
try
{
if (tbl == null || tbl.Columns.Count < 1)
return false;
//
XSSFWorkbook wb = new XSSFWorkbook();
ISheet sheet = wb.CreateSheet("Sheet1");
int curRow = 0;
// 写入列名
if (writeColName)
{
IRow sheetRow = sheet.CreateRow(0);
for (int col = 0; col < tbl.Columns.Count; col++)
{
ICell cell = sheetRow.CreateCell(col);
cell.SetCellValue(tbl.Columns[col].ColumnName);
}
curRow++;
}
// 日期和时间格式
IDataFormat dataFormat = wb.CreateDataFormat();
ICellStyle datetimeStyle = wb.CreateCellStyle();
datetimeStyle.DataFormat =
dataFormat.GetFormat("yyyy/MM/dd HH:mm:ss");
// 写入数据行
for (int row = 0; row < tbl.Rows.Count; row++)
{
IRow sheetRow = sheet.CreateRow(curRow++);
for (int col = 0; col < tbl.Columns.Count; col++)
{
ICell cell = sheetRow.CreateCell(col);
Type dataType = tbl.Columns[col].DataType;
if (tType.IsNumeric(dataType))
{
cell.SetCellType(CellType.Numeric);
cell.SetCellValue(tDbl.GetValue(tbl.Rows[row][col]));
}
else if (dataType.Name == "DateTime")
{
cell.CellStyle = datetimeStyle;
cell.SetCellValue(tDate.GetValue(tbl.Rows[row][col]));
}
else if (dataType.Name == "Boolean")
{
cell.SetCellType(CellType.Boolean);
cell.SetCellValue(tBool.GetValue(tbl.Rows[row][col]));
}
else
{
cell.SetCellType(CellType.String);
cell.SetCellValue(tStr.GetValue(tbl.Rows[row][col]));
}
}
}
//
FileStream fs = new FileStream(xlsxFile, FileMode.Create);
wb.Write(fs);
fs.Close();
//
sheet = null;
wb = null;
//
return true;
}
catch (Exception ex)
{
tLog.E(ex, -1000L, "tExcel.WriteXlsx()");
return false;
}
}
// 其它代码...
}
|
代码中,WriteExcel()方法是Excel文件写入的通用方法,其中包括三个参数,分别是:
- tbl,定义为DataTable对象,指定需要写入的数据。
- path,指定写入Excel文件的路径。
- writeColName,指定是否在表的第一行写入列名,默认值为true。
WriteExcel()方法中,会根据指定Excel文件名判断是.xls或.xlsx文件,然后分别调用WriteXls()或WriteXlsx()方法,两个方法的参数相同,分别是需要写入的数据(DateTable对象)、Excel文件路径,以及是否将DataTable对象中的列名作为第一行数据,即作为标题行(默认为true);方法中的大部分代码是相同的,只是在处理Excel文档(Workbook对象)时使用的类型不同,处理.xls文件时使用了HSSFWorkbook类型,处理.xlsx文件时使用了XSSFWorkbook类型。
数据写入单元格时会调用单元格对象的SetCellValue()方法,此方法有四个重载版本,分别用于写入double、DateTime、bool和string类型数据。代码中,首先会判断DataTable对象中对应列的数据类型,当数据为数值类型时,调用将数据转换为double类型后写入;数据类型为DateTime类型时转换为DateTime类型后写入,并设置日期和时间格式为“yyyy/MM/dd HH:mm:ss”,即4位年份、2位月份、2位日期、2位24小时制时间、2位分名和2位秒数;数据类型为Boolean时转换为bool类型后写入;其它类型的数据则转换为字符串后写入单元格。
下面的代码,我们将user_main表中的username、email和locked字段数据分别导出到D:盘下的user.xls和user.xlsx文件。
C# |
using System;
using System.Data;
public partial class Test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
tSelect sel = tApp.Db.GetSelect("user_main");
DataTable tbl = sel.GetTable("username,email,locked", "");
tWeb.WriteLine(tExcel.WriteExcel(tbl, @"d:\user.xls"));
tWeb.WriteLine(tExcel.WriteExcel(tbl, @"d:\user.xlsx"));
}
}
|
操作成功时,页面会显示两个True,此时,可以在D:盘下查看导出的Excel文件,两个文件包含了相同的数据。
读取Excel文件
读取Excel文件中的数据时,需要注意一个问题,即每行中包含有效数据的单元格数量有可能不同,NPIO代码库处理可能会有读取数据不完整的问题;针对这一问题,在读取数据前可以先检查一定行数的数据,如检查前100行数据,不足100行检查全部行的列数,以判断最大列数。
实际应用中,也可以在读取方法中设置一个参数指定检查的行数;也可以在应用中对Excel文件格式作出约定,即第一行作为标题行,也是数据最完整的一行。
下面的代码(/app_code/office/tExcel.cs)就是tExcel.ReadExcel()方法的实现,其功能是读取Excel文件中指定表单(Sheet)的数据。
C# |
public static DataTable ReadAll(string path,
int sheetIndex = 0,
bool readColName = true)
{
try
{
IWorkbook wb = WorkbookFactory.Create(path);
ISheet ws = wb.GetSheetAt(sheetIndex);
// 处理标题,创建数据表的列
DataTable tbl = new DataTable();
// 通过前100行(或全部行)判断最大列
int cols = ws.GetRow(0).LastCellNum;
int rows = ws.LastRowNum + 1;
int chkRows = (rows >= 100) ? 100 : rows;
int tmp;
for (int row = 1; row < chkRows; row++)
{
tmp = ws.GetRow(row).LastCellNum;
if (tmp > cols) cols = tmp;
}
// 添加DataTable对象的列
for (int col = 0; col < cols; col++)
tbl.Columns.Add();
// 读取数据
IRow readRow;
for (int row = 0; row < rows; row++)
{
readRow = ws.GetRow(row);
DataRow dataRow = tbl.NewRow();
for (int col = 0; col < cols; col++)
{
try
{
ICell cell = readRow.GetCell(col);
if (cell == null)
{
dataRow[col] = DBNull.Value;
}
else if (cell.CellType == CellType.Numeric)
{
// Excel中的日期时间值是Double类型
if (DateUtil.IsCellDateFormatted(cell))
dataRow[col] = cell.DateCellValue;
else
dataRow[col] = cell.NumericCellValue;
}
else if (cell.CellType == CellType.Boolean)
{
dataRow[col] = cell.BooleanCellValue ? 1 : 0;
}
else
{
string val = cell.StringCellValue.Trim();
if (val == "") dataRow[col] = DBNull.Value;
else dataRow[col] = val;
}
}
catch
{
dataRow[col] = DBNull.Value;
}
}
tbl.Rows.Add(dataRow);
}
// 处理标题
if (readColName)
{
string sName;
for (int col = 0; col < cols; col++)
{
sName = tStr.GetValue(tbl.Rows[0][col]).Trim();
if (sName != "") tbl.Columns[col].ColumnName = sName;
}
tbl.Rows.RemoveAt(0);
}
//
return tbl;
}
catch (Exception ex)
{
tLog.E(ex, -1000L, "tExcel.ReadExcel()");
return null;
}
}
|
代码中,ReadAll()方法需要三个参数,分别是:
- path,指定读取Excel文件的路径。
- sheetIndex,指定读取工作表(Sheet)的索引值。请注意,与使用官方API操作不同,这里,第一个工作表的的索引值为0,这也是默认读取的工作表。
- readColName,指定是否读取第一行数据作为列名,默认值为true。方法中,在读取全部数据后,如果指定第一行作为标题行,会其数据设置为DataTable对象的列名(ColumnName),然后从DataTable对象中删除第一行。
读取单元格数据时,首先会判断是否为空值,如果不是空值,则分别按数字类型、布尔类型或文本类型读取数据。需要注意的是,当单元格数据是数字时,也可能保存了日期和时间数据,即使用OLE自动化标准时间格式的日期和时间数据,此时使用单元格是否定义了日期和时间格式来判断。最终,从Excel文件中读取的数据会以DataTable对象返回。
下面的代码,会读取d:\user.xlsx文件的数据,并绑定到GridView控件。
C# |
using System;
using System.Data;
public partial class Test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
DataTable tbl= tExcel.ReadAll(@"d:\user.xlsx");
grd1.DataSource = tbl;
grd1.DataBind();
}
}
|