C# 使用 Office 自动化读取、修改并写回 Excel 列数据

8 次阅读

C# 操作 Excel 的方式有很多,如果目标电脑已经安装 Microsoft Office,并且程序需要直接调用 Excel 打开工作簿、读取指定列、修改数据后保存原文件,那么 Office 自动化仍然是一种直观且实用的实现方案。

对于“读取、修改并写回 Excel 列数据”这一类需求,核心并不复杂:通过 Microsoft.Office.Interop.Excel 获取工作簿和工作表,再定位目标列的数据区域,逐行读取单元格内容,根据业务规则完成转换,最后将结果写回对应单元格并保存。

一、准备 Office Interop 环境

C# 使用 Office 自动化操作 Excel,通常需要引用 Excel Interop 程序集。

如果使用 Visual Studio,可以通过 NuGet 安装:

PowerShell
Install-Package Microsoft.Office.Interop.Excel

安装完成后,在代码中引入:

C#
using Excel = Microsoft.Office.Interop.Excel;

需要注意,Office 自动化依赖本机安装 Microsoft Excel。程序运行时实际上会启动 Excel COM 组件,因此它更适合运行在已经安装 Office 的 Windows 桌面环境中。

如果程序部署在服务器、Windows Service、ASP.NET 后端等无人值守环境,则不建议直接采用 Office Interop,因为 Excel COM 自动化存在进程残留、权限、桌面交互和并发等问题。

二、打开 Excel 工作簿

假设当前 Excel 文件路径为:

D:ExcelDataemployees.xlsx

最基本的打开方式如下:

C#
using Excel = Microsoft.Office.Interop.Excel;

Excel.Application excelApp = new Excel.Application();
Excel.Workbook workbook = excelApp.Workbooks.Open(@"D:ExcelDataemployees.xlsx");

Excel.Worksheet worksheet = workbook.Worksheets["Sheet1"];

这里涉及三个非常重要的对象:

  • Excel.Application:Excel 应用程序实例

  • Excel.Workbook:当前打开的 Excel 工作簿

  • Excel.Worksheet:具体的工作表

如果工作表名称不确定,也可以按照索引获取:

C#
Excel.Worksheet worksheet = workbook.Worksheets[1];

Excel 工作表索引从 1 开始,而不是从 0 开始。

三、读取 Excel 指定列数据

假设 Excel 中存在如下数据:

A列:编号
B列:姓名
C列:部门
D列:手机号

现在需要读取 D 列中的手机号。

可以直接通过行号访问单元格:

C#
Excel.Range cell = worksheet.Cells[2, 4];

string phone = Convert.ToString(cell.Value2);
Console.WriteLine(phone);

其中:

C#
worksheet.Cells[2, 4]

表示第 2 行、第 4 列,也就是 D2。

如果需要读取整个 D 列,可以循环处理:

C#
for (int row = 2; row <= 100; row++)
{
    Excel.Range cell = worksheet.Cells[row, 4];

    string phone = Convert.ToString(cell.Value2);

    Console.WriteLine(phone);
}

通常第一行是表头,因此从第 2 行开始处理。

四、动态获取 Excel 数据行数

实际项目中不应该把最后一行固定为 100。Excel 数据量经常会发生变化,更合理的方式是根据指定列自动寻找最后一行。

例如获取 D 列最后一个有数据的行:

C#
Excel.Range lastCell = worksheet.Cells[
    worksheet.Rows.Count,
    4
];

int lastRow = worksheet.Cells[
    worksheet.Rows.Count,
    4
].End[Excel.XlDirection.xlUp].Row;

如果 D 列数据如下:

D1:手机号
D2:13800138000
D3:13900139000
D4:13600136000

那么:

C#
lastRow

得到的结果就是:

4

完整读取代码可以写成:

C#
int lastRow = worksheet.Cells[
    worksheet.Rows.Count,
    4
].End[Excel.XlDirection.xlUp].Row;

for (int row = 2; row <= lastRow; row++)
{
    string value = Convert.ToString(
        worksheet.Cells[row, 4].Value2
    );

    Console.WriteLine(value);
}

这种方式比固定循环次数更加适合实际业务。

五、读取列数据并进行修改

假设 Excel 的 D 列存储手机号,现在希望将手机号中间四位替换成 ****。

例如:

13800138000
13900139000
13612345678

修改之后:

138****8000
139****9000
136****5678

可以先编写一个处理方法:

C#
private static string MaskPhone(string phone)
{
    if (string.IsNullOrWhiteSpace(phone))
        return phone;

    phone = phone.Trim();

    if (phone.Length != 11)
        return phone;

    return phone.Substring(0, 3)
         + "****"
         + phone.Substring(7);
}

然后读取 D 列并写回:

C#
for (int row = 2; row <= lastRow; row++)
{
    Excel.Range cell = worksheet.Cells[row, 4];

    string phone = Convert.ToString(cell.Value2);

    string newPhone = MaskPhone(phone);

    cell.Value2 = newPhone;
}

这样就完成了“读取 → 修改 → 写回”的完整流程。

六、C# 完整实现 Excel 列数据修改

将前面的代码组合起来,可以形成一个相对完整的示例:

C#
using System;
using Excel = Microsoft.Office.Interop.Excel;

class Program
{
    static void Main()
    {
        string filePath = @"D:ExcelDataemployees.xlsx";

        Excel.Application excelApp = null;
        Excel.Workbooks workbooks = null;
        Excel.Workbook workbook = null;
        Excel.Worksheet worksheet = null;

        try
        {
            excelApp = new Excel.Application();
            excelApp.DisplayAlerts = false;

            workbooks = excelApp.Workbooks;

            workbook = workbooks.Open(filePath);

            worksheet = workbook.Worksheets["Sheet1"];

            int lastRow = worksheet.Cells[
                worksheet.Rows.Count,
                4
            ].End[Excel.XlDirection.xlUp].Row;

            for (int row = 2; row <= lastRow; row++)
            {
                Excel.Range cell = worksheet.Cells[row, 4];

                string phone = Convert.ToString(cell.Value2);

                string newPhone = MaskPhone(phone);

                cell.Value2 = newPhone;
            }

            workbook.Save();

            Console.WriteLine("Excel 数据修改完成。");
        }
        catch (Exception ex)
        {
            Console.WriteLine("处理失败:" + ex.Message);
        }
        finally
        {
            if (workbook != null)
            {
                workbook.Close(false);
            }

            if (excelApp != null)
            {
                excelApp.Quit();
            }

            ReleaseComObject(worksheet);
            ReleaseComObject(workbook);
            ReleaseComObject(workbooks);
            ReleaseComObject(excelApp);
        }
    }

    private static string MaskPhone(string phone)
    {
        if (string.IsNullOrWhiteSpace(phone))
            return phone;

        phone = phone.Trim();

        if (phone.Length != 11)
            return phone;

        return phone.Substring(0, 3)
             + "****"
             + phone.Substring(7);
    }

    private static void ReleaseComObject(object obj)
    {
        if (obj != null &&
            System.Runtime.InteropServices.Marshal.IsComObject(obj))
        {
            System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
        }
    }
}

这里最重要的一点是:

C#
cell.Value2 = newPhone;

它负责将修改后的数据重新写入 Excel 单元格。

而:

C#
workbook.Save();

负责将修改后的工作簿保存到原文件。

七、读取一列并写入另一列

实际业务中,更常见的情况并不是直接覆盖原始数据,而是保留原数据,将处理结果写入新列。

例如:

D列:原手机号
E列:脱敏手机号

可以这样实现:

C#
for (int row = 2; row <= lastRow; row++)
{
    string phone = Convert.ToString(
        worksheet.Cells[row, 4].Value2
    );

    string maskedPhone = MaskPhone(phone);

    worksheet.Cells[row, 5].Value2 = maskedPhone;
}

如果 E1 还是空白,可以先设置表头:

C#
worksheet.Cells[1, 5].Value2 = "脱敏手机号";

完整效果就是:

D列             E列
手机号          脱敏手机号
13800138000     138****8000
13900139000     139****9000
13612345678     136****5678

这种方式更加安全,因为原始数据没有被覆盖。

八、一次性读取整列数据

如果 Excel 中有几万甚至几十万行数据,逐单元格访问:

C#
worksheet.Cells[row, 4].Value2

会产生大量 COM 调用,执行速度可能明显下降。

对于批量数据,可以一次性获取一个区域:

C#
Excel.Range range = worksheet.Range[
    worksheet.Cells[2, 4],
    worksheet.Cells[lastRow, 4]
];

object[,] values = range.Value2;

然后在内存中处理:

C#
int rowCount = values.GetLength(0);

for (int i = 1; i <= rowCount; i++)
{
    string phone = Convert.ToString(values[i, 1]);

    values[i, 1] = MaskPhone(phone);
}

处理完成后,再一次性写回:

C#
range.Value2 = values;

相比逐个单元格写入,这种批量方式通常效率更高。

九、批量读取和写回 Excel 列的完整示例

例如处理 D 列数据,可以采用下面的方式:

C#
Excel.Range range = worksheet.Range[
    worksheet.Cells[2, 4],
    worksheet.Cells[lastRow, 4]
];

object[,] values = range.Value2;

for (int i = 1; i <= values.GetLength(0); i++)
{
    string value = Convert.ToString(values[i, 1]);

    if (!string.IsNullOrWhiteSpace(value))
    {
        values[i, 1] = MaskPhone(value);
    }
}

range.Value2 = values;

这里有一个容易忽略的细节。

当读取的区域只有一个单元格时,Value2 可能返回普通对象,而不是二维数组。因此,如果程序需要兼容只有一条数据的情况,最好根据区域大小进行处理,或者统一采用更稳妥的封装方法。

十、修改 Excel 指定列中的数值

如果需要将某一列中的数值统一增加 10,例如 C 列:

100
200
300

修改后:

110
210
310

可以这样写:

C#
for (int row = 2; row <= lastRow; row++)
{
    Excel.Range cell = worksheet.Cells[row, 3];

    if (cell.Value2 != null)
    {
        double value = Convert.ToDouble(cell.Value2);

        cell.Value2 = value + 10;
    }
}

对于数字计算,建议先判断数据是否为空,并注意 Excel 中可能存在文本、错误值或者格式异常。

更稳妥的处理方式是:

C#
object value = cell.Value2;

if (value != null &&
    double.TryParse(value.ToString(), out double number))
{
    cell.Value2 = number + 10;
}

这样可以避免遇到非数字数据时直接抛出异常。

十一、修改日期列时需要注意格式

Excel 日期数据是 Office 自动化中比较容易出现问题的地方。

例如读取日期:

C#
object value = worksheet.Cells[row, 5].Value2;

Excel 可能返回日期序列值,而不是传统意义上的日期字符串。

可以尝试:

C#
if (value != null)
{
    DateTime date = DateTime.FromOADate(
        Convert.ToDouble(value)
    );

    Console.WriteLine(date.ToString("yyyy-MM-dd"));
}

如果要写入日期:

C#
worksheet.Cells[row, 5].Value2 = DateTime.Now;

还可以进一步设置显示格式:

C#
worksheet.Cells[row, 5].NumberFormat = "yyyy-mm-dd";

需要区分“单元格实际值”和“单元格显示格式”。NumberFormat 改变的是显示方式,并不会简单地把底层数据转换成字符串。

十二、读取 Excel 列时不要忽略空单元格

实际 Excel 数据经常存在空行或空单元格。

例如:

C#
string value = Convert.ToString(
    worksheet.Cells[row, 4].Value2
);

即使单元格为空,也不要直接假设一定存在字符串。

可以统一进行判断:

C#
object rawValue = worksheet.Cells[row, 4].Value2;

if (rawValue == null)
{
    continue;
}

string value = rawValue.ToString().Trim();

if (string.IsNullOrEmpty(value))
{
    continue;
}

这样可以减少数据转换过程中出现的异常。

十三、如何写回新的 Excel 文件

如果不希望覆盖原文件,可以使用:

C#
string outputPath = @"D:ExcelDataemployees_new.xlsx";

workbook.SaveAs(outputPath);

原始文件:

employees.xlsx

处理后的文件:

employees_new.xlsx

这种方式非常适合数据清洗、批量转换和报表处理。

如果需要保留原文件不变,推荐优先使用 SaveAs,而不是直接调用:

C#
workbook.Save();

十四、Excel 自动化中的 COM 对象释放

Office Interop 与普通 .NET 对象存在明显区别,它实际上涉及 COM 对象。

如果只写:

C#
workbook.Close();
excelApp.Quit();

有时 Windows 任务管理器中仍然可能看到 EXCEL.EXE 进程。

因此需要主动释放 COM 对象:

C#
System.Runtime.InteropServices.Marshal.ReleaseComObject(
    worksheet
);

System.Runtime.InteropServices.Marshal.ReleaseComObject(
    workbook
);

System.Runtime.InteropServices.Marshal.ReleaseComObject(
    excelApp
);

开发过程中还应该尽量避免大量使用这种连续链式调用:

C#
excelApp.Workbooks.Open(filePath)
    .Worksheets[1]
    .Cells[2, 4]
    .Value2;

因为中间会产生多个 COM 对象,不容易控制释放。

更推荐拆开:

C#
Excel.Workbooks workbooks = excelApp.Workbooks;

Excel.Workbook workbook = workbooks.Open(filePath);

Excel.Worksheet worksheet = workbook.Worksheets[1];

Excel.Range cell = worksheet.Cells[2, 4];

然后按照创建和使用关系进行释放。

十五、使用 try-finally 保证 Excel 正常退出

Excel 文件操作过程中可能出现很多异常,例如文件不存在、文件被占用、工作表名称错误、权限不足等。

因此应该把关闭和释放工作放到 finally:

C#
try
{
    // 打开 Excel
    // 读取数据
    // 修改数据
    // 保存文件
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    if (workbook != null)
        workbook.Close(false);

    if (excelApp != null)
        excelApp.Quit();

    ReleaseComObject(worksheet);
    ReleaseComObject(workbook);
    ReleaseComObject(workbooks);
    ReleaseComObject(excelApp);
}

这样即使中途发生异常,也能够尽可能保证 Excel 进程被关闭。

十六、Excel 文件被占用时如何处理

如果 Excel 文件已经被其他用户打开,调用:

C#
workbooks.Open(filePath);

可能出现文件锁定、只读打开或者保存失败等情况。

可以根据业务需求设置打开方式。例如只读模式:

C#
workbook = workbooks.Open(
    filePath,
    ReadOnly: true
);

如果程序需要修改并保存文件,就必须确认当前用户对文件具有写入权限,同时确保文件没有被其他程序锁定。

在批处理程序中,可以先检查文件:

C#
if (!File.Exists(filePath))
{
    Console.WriteLine("Excel 文件不存在。");
    return;
}

需要引用:

C#
using System.IO;

十七、处理大量 Excel 数据时的性能优化

如果只是几十行数据,逐单元格读写基本没有问题。

但当数据达到几万行时:

C#
for (...)
{
    worksheet.Cells[row, column].Value2 = ...;
}

可能明显变慢。

原因在于 C# 与 Excel 之间不断发生 COM 跨边界调用。

更高效的思路是:

Excel区域
   ↓
一次性读取
   ↓
C# 内存中的二维数组
   ↓
批量计算和修改
   ↓
一次性写回Excel

示例:

C#
Excel.Range range = worksheet.Range[
    worksheet.Cells[2, 4],
    worksheet.Cells[lastRow, 4]
];

object[,] values = range.Value2;

for (int i = 1; i <= values.GetLength(0); i++)
{
    string value = Convert.ToString(values[i, 1]);

    values[i, 1] = MaskPhone(value);
}

range.Value2 = values;

这种方式对于批量修改列数据尤其值得采用。

十八、关闭 Excel 界面提高自动化体验

如果不希望用户看到 Excel 窗口,可以设置:

C#
excelApp.Visible = false;

同时可以关闭提示:

C#
excelApp.DisplayAlerts = false;

例如:

C#
Excel.Application excelApp = new Excel.Application();

excelApp.Visible = false;
excelApp.DisplayAlerts = false;

处理完成之后:

C#
workbook.Close(true);
excelApp.Quit();

不过需要注意,关闭界面并不意味着可以在服务器环境中安全运行 Office 自动化。Interop.Excel 本质上仍然依赖本机 Excel 环境。

十九、按列名查找目标列

如果程序直接写死:

C#
worksheet.Cells[row, 4]

那么当 Excel 用户调整列顺序后,程序可能操作错误的字段。

更可靠的方法是先根据第一行表头查找列号。

例如:

C#
int targetColumn = -1;

int columnCount = worksheet.UsedRange.Columns.Count;

for (int column = 1; column <= columnCount; column++)
{
    string header = Convert.ToString(
        worksheet.Cells[1, column].Value2
    );

    if (header == "手机号")
    {
        targetColumn = column;
        break;
    }
}

找到以后:

C#
if (targetColumn == -1)
{
    throw new Exception("未找到手机号列。");
}

后面就可以使用:

C#
worksheet.Cells[row, targetColumn]

这种设计比直接固定 D 列更加健壮。

二十、读取、修改、写回 Excel 列的推荐流程

一个比较完整的业务处理流程可以归纳为:

检查 Excel 文件
      ↓
创建 Excel Application
      ↓
打开 Workbook
      ↓
获取 Worksheet
      ↓
查找目标列
      ↓
确定最后一行
      ↓
批量读取目标区域
      ↓
C# 内存中处理数据
      ↓
批量写回 Excel
      ↓
保存或另存为新文件
      ↓
关闭 Workbook
      ↓
退出 Excel
      ↓
释放 COM 对象

如果数据量较小,可以直接逐行操作;如果数据量较大,则优先使用 Range.Value2 批量读取和写回。

二十一、Office 自动化并不适合所有场景

C# 使用 Office Interop 最大的优势是能够直接调用 Excel 的功能,例如:

  • 读取和修改单元格

  • 操作工作表

  • 设置单元格格式

  • 操作公式

  • 使用 Excel 原生功能

  • 打开、保存和另存为 Excel 文件

但它也有明显限制。

首先,本机通常需要安装 Microsoft Excel。其次,COM 对象管理相对复杂。如果程序长时间运行或者并发处理大量文件,Excel 进程管理会变得更加麻烦。

如果应用程序是 ASP.NET 网站、Web API、Windows Server 后台任务或者 Linux 服务,通常应该考虑使用专门的 Excel 文件处理库,而不是让后台进程启动桌面版 Excel。

例如,仅仅需要读取和修改 .xlsx 文件时,可以根据项目需求考虑 Open XML、NPOI、ClosedXML 等方案。它们不依赖 Excel 客户端,更适合服务端批量处理。

二十二、常见问题排查

1. 找不到 Microsoft.Office.Interop.Excel

首先确认项目已经安装对应的 NuGet 包,并检查项目目标框架与 Office 环境是否兼容。

2. Excel 文件无法保存

重点检查文件是否被其他 Excel 进程打开,以及当前程序是否拥有目标目录的写入权限。

3. 修改完成后 EXCEL.EXE 没有退出

通常与 COM 对象没有正确释放有关。不要只释放 Application,还需要关注 Workbook、Worksheet、Range 等对象。

4. Excel 日期读取结果异常

优先检查 Value2 返回的数据类型,并根据 Excel 日期序列值使用 DateTime.FromOADate() 转换。

5. 数据量大时运行速度很慢

避免逐个单元格进行 COM 调用。应该尽量通过 Range.Value2 一次性读取二维数组,在 C# 内存中处理完成后再批量写回。

6. 第一列或目标列存在空数据

不要直接强制转换,应该先判断:

C#
object value = cell.Value2;

if (value == null)
{
    continue;
}

7. 修改数据后格式发生变化

修改值和修改格式是两个不同的操作。必要时单独设置:

C#
cell.NumberFormat = "yyyy-mm-dd";

或者保留原单元格格式,仅修改 Value2。

二十三、实际项目中的设计建议

如果 Excel 数据只是一次性处理几十、几百行,直接使用 Interop 逐行读取和写回,代码简单、调试方便。

如果需要处理数万行数据,应优先采用批量读取:

C#
object[,] values = range.Value2;

处理完后:

C#
range.Value2 = values;

如果需要根据业务字段操作数据,则不要把列号硬编码为:

C#
4

而应该根据表头定位:

手机号 → D列
姓名 → B列
部门 → C列

这样即使用户调整 Excel 列顺序,程序仍然能够正常工作。

另外,原始 Excel 文件最好不要直接覆盖。对于重要业务数据,可以先通过 SaveAs 保存到新文件,验证结果正确后再替换正式文件,从而降低数据损坏的风险。

C# 通过 Office 自动化读取、修改并写回 Excel 列数据,核心操作就是 Workbook、Worksheet、Range 和 Value2。小规模数据可以采用逐单元格方式实现,数据量较大时则应该使用二维数组批量读写。同时必须重视 Excel 进程退出和 COM 对象释放,否则很容易出现 EXCEL.EXE 残留。对于桌面工具而言,Office Interop 仍然具有较好的实用性;对于服务端和高并发场景,则更适合选择不依赖 Excel 客户端的文件处理方案。