C#中将xls文件复制到新CSV文件,如果文件存在则追加内容
时间: 2025-08-28 21:57:06 AIGC 浏览: 23
<think>我们被要求将xls文件复制到新的CSV文件,如果新文件存在则追加内容。注意,这里的数据源是xls文件(可能是旧版Excel格式),目标文件是CSV(逗号分隔值)文件。
步骤:
1. 读取xls文件内容(这里我们可能需要使用第三方库,因为.NET Framework本身对xls的支持有限,建议使用NPOI库)。
2. 将读取的数据转换为适合CSV的格式(通常是字符串数组或列表)。
3. 检查目标CSV文件是否存在,如果存在则追加,否则创建新文件并写入数据(包括标题行,如果文件不存在则写入标题,追加时不需要重复标题)。
但是,由于引用中提到了C#的CSV读写示例(引用[5])和Path.ChangeExtension方法(引用[4]),我们可以参考这些思路。
由于引用[5]中展示了一个MarkDataModel类,并且有Description属性,我们可以借鉴其将对象写入CSV的方法。但是,我们这里需要读取的是xls文件,而不是直接写入对象。
因此,我们需要:
- 使用NPOI读取xls文件(因为Microsoft.Office.Interop.Excel需要安装Excel,且不适用于服务器环境)。
- 将读取的每一行数据转换为CSV格式(即用逗号分隔,并且处理特殊字符如逗号、引号等)。
- 然后根据目标文件是否存在,选择追加或创建。
注意:在追加时,我们通常不写入列标题(如果原xls文件有标题,那么在第一次写入时已经写入标题,追加时只需要追加数据行)。但是,这取决于需求。这里假设xls文件的第一行是标题,并且我们只在创建新文件时写入标题,追加时不写入标题。
步骤详细:
1. 安装NPOI库(如果项目中没有,需要安装):
- 使用NuGet:Install-Package NPOI
2. 编写代码:
- 读取xls文件(.xls格式,使用HSSFWorkbook)。
- 获取第一个工作表。
- 遍历每一行,将每个单元格的值转换为字符串,并用逗号连接。
- 处理字符串中的特殊字符(例如,如果字符串包含逗号、换行或双引号,则需要用双引号括起来,并且内部的双引号要转义为两个双引号)。
3. 写入CSV文件时,如果文件存在,则追加数据(不写标题行);如果不存在,则先写入标题行(第一行),再写入数据。
但是,注意:xls文件可能有多个工作表,我们只处理第一个工作表。
代码结构:
使用NPOI读取xls文件:
引用[5]中的类可以作为我们数据模型的参考,但这里我们不需要定义模型,因为xls文件的结构是未知的,我们按行读取并转换为字符串数组即可。
具体步骤:
1. 读取xls文件:
using (FileStream file = new FileStream(sourceXlsPath, FileMode.Open, FileAccess.Read))
{
HSSFWorkbook workbook = new HSSFWorkbook(file);
var sheet = workbook.GetSheetAt(0); // 第一个工作表
// 遍历行
for (int i = 0; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (row == null) continue;
// 遍历单元格
List<string> cells = new List<string>();
for (int j = 0; j < row.LastCellNum; j++)
{
var cell = row.GetCell(j);
cells.Add(FormatCellValue(cell));
}
// 将这一行的单元格连接成CSV行
string csvLine = string.Join(",", cells);
// 这里需要处理特殊字符,但为了简单,我们可以使用一个通用的格式化函数(如将整个字段用双引号包围,并转义内部的双引号)
// 但是,上面的简单连接可能不够,我们需要一个方法将字符串格式化为CSV字段
// 所以,我们修改:对每个单元格字符串进行CSV格式化
// 我们可以写一个方法:EscapeCsvField
}
}
2. 格式化CSV字段的函数:
private static string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field))
return "";
// 如果字段包含逗号、双引号、换行符,则需要用双引号括起来,并且内部的双引号要替换为两个双引号
if (field.Contains(",") || field.Contains("\"") || field.Contains("\r") || field.Contains("\n"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
3. 在读取单元格时,我们需要将单元格的值转换为字符串,然后应用EscapeCsvField。
4. 写入CSV文件:
- 判断目标文件是否存在。
- 如果不存在,则写入标题行(第一行)和所有数据行(包括第一行数据?注意:我们读取的第一行是标题,然后从第一行开始读数据行?)
但是,在读取xls时,第一行通常就是标题行,所以我们在写入新文件时,第一行就是标题行,然后后面是数据行。
- 如果存在,则只追加数据行(跳过标题行?)。这里我们需要注意:在读取xls文件时,我们是从第0行开始读的。在写入新文件时,我们写入所有行(包括标题行)。在追加时,我们跳过标题行(即从第1行开始,索引0是标题行,索引1开始是数据行)。
但是,这里有一个问题:xls文件可能没有标题行?所以,我们最好让用户指定是否有标题行,或者根据需求来。这里按照常规处理:假设第一行是标题行,在追加时跳过标题行。
因此,我们可以这样:
bool fileExists = File.Exists(targetCsvPath);
using (StreamWriter writer = new StreamWriter(targetCsvPath, true)) // true表示追加
{
// 如果是新文件,写入标题行(即第一行)和所有数据行(包括第一行数据?不,我们通常将第一行作为标题,然后后面是数据行,但这里我们读取的xls第一行就是标题,第二行开始是数据)
// 但是,在读取xls时,我们是一行一行读的,所以我们可以:
// 当文件不存在时,从第0行开始写(即包括标题行和所有数据行)
// 当文件存在时,从第1行开始写(即跳过标题行,只写数据行)
// 但是,我们读取整个xls文件时,已经将整个sheet的内容读出来了,我们需要记录行索引。
// 我们可以这样:在循环读取行的时候,记录行索引,然后根据情况决定写入哪些行。
// 更好的方法:先读取整个xls文件的内容到一个字符串列表(List<string>),每一行是一个CSV格式的字符串。
// 然后,在写入时:
// if (fileExists)
// 写入从第1行开始到最后(即跳过第一行)
// else
// 写入所有行
// 但是,如果xls文件很大,这样可能会占用大量内存。我们可以改为在读取xls时,根据情况写入。
// 这里我们采用:先读取整个xls内容到内存,因为通常xls文件不会太大。如果文件很大,我们可以采用流式读取并逐行写入(但需要知道当前是否是第一行,且目标文件是否存在)。
// 为了简单,我们先将所有行读入一个列表,然后根据文件是否存在决定写入哪些行。
}
考虑到大文件,我们可以这样优化:
- 如果目标文件不存在,则写入所有行(包括标题行)。
- 如果目标文件存在,则跳过标题行(即第一行),从第二行开始写入。
但是,在读取xls时,我们可以先判断目标文件是否存在,然后决定是否跳过第一行?但是这样我们仍然需要读取整个xls文件,只是写入时跳过第一行。
因此,我们可以:
List<string> csvLines = new List<string>();
// 读取整个xls文件,转换为CSV行(包括标题行和数据行)
// 然后:
if (File.Exists(targetCsvPath))
{
// 追加模式,跳过第一行(标题行)
if (csvLines.Count > 0)
{
// 跳过第一行
csvLines = csvLines.Skip(1).ToList();
}
}
// 然后将csvLines写入文件(追加模式,但这里我们使用File.AppendAllLines?或者我们使用StreamWriter,并且追加模式打开,但注意如果文件不存在,StreamWriter会创建文件,但不会写入标题行?所以我们需要在追加模式下,如果文件不存在,写入标题行?这样不行,因为我们在追加模式下,如果文件不存在,我们写入所有行(包括标题行)?但是我们在上面已经跳过了标题行,所以追加模式下写入的数据就没有标题行了?不对,我们的逻辑是:如果文件存在,则跳过标题行;如果不存在,则写入所有行(包括标题行)。所以我们在追加模式下,应该只写入数据行(跳过标题行),而新文件则写入标题行和数据行。
但是,我们在写入时,如果文件不存在,我们写入整个csvLines(包括标题行和所有数据行);如果存在,我们写入csvLines中除了标题行以外的所有行(即从第一行开始)。
因此,我们可以这样写:
if (!File.Exists(targetCsvPath))
{
File.WriteAllLines(targetCsvPath, csvLines, Encoding.UTF8);
}
else
{
// 追加数据行(跳过标题行)
if (csvLines.Count > 1)
{
// 跳过第一行(标题行)
var dataLines = csvLines.Skip(1);
File.AppendAllLines(targetCsvPath, dataLines, Encoding.UTF8);
}
}
但是,这样如果文件很大,一次性读取整个xls文件到内存可能会占用较多内存。我们可以改为流式处理:
using (var fs = new FileStream(sourceXlsPath, FileMode.Open, FileAccess.Read))
using (var workbook = new HSSFWorkbook(fs))
{
var sheet = workbook.GetSheetAt(0);
bool fileExists = File.Exists(targetCsvPath);
using (var writer = new StreamWriter(targetCsvPath, true, Encoding.UTF8)) // 追加模式
{
for (int i = 0; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (row == null) continue;
List<string> cells = new List<string>();
for (int j = 0; j < row.LastCellNum; j++)
{
var cell = row.GetCell(j);
cells.Add(EscapeCsvField(GetCellValueAsString(cell)));
}
string csvLine = string.Join(",", cells);
// 如果是新文件,则写入所有行
// 如果是已存在的文件,则跳过第一行(即标题行)
if (!fileExists)
{
writer.WriteLine(csvLine);
}
else
{
if (i > 0) // 跳过第一行(索引0)
writer.WriteLine(csvLine);
}
}
}
}
注意:这里我们使用流式写入,逐行处理,内存占用较小。但是,我们在一开始就判断了文件是否存在(fileExists),这个判断在循环外部,这样在整个写入过程中,如果文件在写入前不存在,那么写入过程中即使文件被创建,也不会改变我们跳过第一行的逻辑。但是,如果文件在开始写入后由其他进程创建,我们这里不考虑。
另外,我们还需要一个将单元格值转换为字符串的函数GetCellValueAsString。
5. 单元格值转换函数:
private static string GetCellValueAsString(ICell cell)
{
if (cell == null)
return string.Empty;
switch (cell.CellType)
{
case CellType.String:
return cell.StringCellValue;
case CellType.Numeric:
if (DateUtil.IsCellDateFormatted(cell))
{
return cell.DateCellValue.ToString("yyyy-MM-dd HH:mm:ss");
}
else
{
return cell.NumericCellValue.ToString();
}
case CellType.Boolean:
return cell.BooleanCellValue ? "TRUE" : "FALSE";
case CellType.Formula:
// 对于公式,我们可以选择计算后的值,也可以选择公式字符串。这里我们取计算后的值
try
{
if (cell.CachedFormulaResultType == CellType.String)
return cell.StringCellValue;
else if (cell.CachedFormulaResultType == CellType.Numeric)
return cell.NumericCellValue.ToString();
else if (cell.CachedFormulaResultType == CellType.Boolean)
return cell.BooleanCellValue ? "TRUE" : "FALSE";
else
return cell.ToString();
}
catch
{
return cell.ToString();
}
default:
return cell.ToString();
}
}
6. 注意:如果单元格是空,我们返回空字符串。
7. 最后,完整的代码示例:
注意:需要添加引用
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
代码:
public void ConvertXlsToCsv(string sourceXlsPath, string targetCsvPath)
{
// 检查源文件是否存在
if (!File.Exists(sourceXlsPath))
throw new FileNotFoundException("Source xls file not found", sourceXlsPath);
// 判断目标文件是否存在(在写入前判断)
bool fileExists = File.Exists(targetCsvPath);
using (FileStream fs = new FileStream(sourceXlsPath, FileMode.Open, FileAccess.Read))
{
HSSFWorkbook workbook = new HSSFWorkbook(fs);
ISheet sheet = workbook.GetSheetAt(0);
using (StreamWriter writer = new StreamWriter(targetCsvPath, true, Encoding.UTF8)) // 追加模式
{
for (int i = 0; i <= sheet.LastRowNum; i++)
{
IRow row = sheet.GetRow(i);
if (row == null) continue;
List<string> cells = new List<string>();
for (int j = 0; j < row.LastCellNum; j++)
{
ICell cell = row.GetCell(j);
string cellValue = GetCellValueAsString(cell);
cells.Add(EscapeCsvField(cellValue));
}
string csvLine = string.Join(",", cells);
// 如果是新文件,则写入所有行;如果是已存在的文件,则跳过第一行(标题行)
if (!fileExists)
{
writer.WriteLine(csvLine);
}
else
{
if (i > 0) // 第一行是标题行,跳过
{
writer.WriteLine(csvLine);
}
}
}
}
}
}
private static string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field))
return "";
if (field.Contains(",") || field.Contains("\"") || field.Contains("\r") || field.Contains("\n"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
private static string GetCellValueAsString(ICell cell)
{
if (cell == null)
return string.Empty;
switch (cell.CellType)
{
case CellType.String:
return cell.StringCellValue;
case CellType.Numeric:
if (DateUtil.IsCellDateFormatted(cell))
{
return cell.DateCellValue.ToString("yyyy-MM-dd HH:mm:ss");
}
else
{
return cell.NumericCellValue.ToString();
}
case CellType.Boolean:
return cell.BooleanCellValue ? "TRUE" : "FALSE";
case CellType.Formula:
try
{
// 取公式计算后的值
if (cell.CachedFormulaResultType == CellType.String)
return cell.StringCellValue;
else if (cell.CachedFormulaResultType == CellType.Numeric)
return cell.NumericCellValue.ToString();
else if (cell.CachedFormulaResultType == CellType.Boolean)
return cell.BooleanCellValue ? "TRUE" : "FALSE";
else
return cell.ToString();
}
catch
{
return cell.ToString();
}
default:
return cell.ToString();
}
}
注意:这个代码处理的是.xls格式(HSSFWorkbook),如果是.xlsx格式,需要使用XSSFWorkbook。但问题要求是xls,所以我们用HSSFWorkbook。
另外,如果目标文件不存在,则创建并写入所有行(包括标题行);如果存在,则追加数据行(跳过标题行)。
但是,这里假设xls文件的第一行是标题行。如果实际情况不是,可以调整。
8. 异常处理:在实际应用中,应添加异常处理。
9. 引用:我们使用了NPOI库,所以需要安装NPOI。
10. 关于引用[1][2][3]:它们提到了Python和JavaScript的方法,但这里我们使用C#,所以不直接相关。引用[4]是Path.ChangeExtension方法,但这里我们没有改变扩展名,所以没有使用。引用[5]是一个CSV数据模型,我们这里没有使用特定模型,而是动态读取。
因此,我们提供上述代码作为解决方案。
注意:如果xls文件很大,可能性能有影响,但通常xls文件不会太大(因为xls格式最多65536行)。
最后,我们生成相关问题。</think>### 在C#中实现将XLS文件复制到CSV文件(存在则追加内容)
以下是完整的实现方案,使用 **NPOI库** 读取XLS文件,并处理CSV文件的追加逻辑:
```csharp
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using NPOI.HSSF.UserModel; // 处理.xls格式
using NPOI.SS.UserModel;
public class XlsToCsvConverter
{
public static void ConvertXlsToCsv(string sourceXlsPath, string targetCsvPath)
{
// 1. 读取XLS文件内容
List<string[]> dataRows = ReadXlsFile(sourceXlsPath);
// 2. 检查目标CSV文件是否存在
bool isNewFile = !File.Exists(targetCsvPath);
// 3. 写入或追加到CSV文件
using (StreamWriter writer = new StreamWriter(targetCsvPath, true, Encoding.UTF8)) // true表示追加模式
{
// 新文件写入列头,已存在文件跳过列头
for (int i = isNewFile ? 0 : 1; i < dataRows.Count; i++)
{
writer.WriteLine(string.Join(",", EscapeCsvFields(dataRows[i])));
}
}
}
private static List<string[]> ReadXlsFile(string filePath)
{
List<string[]> rows = new List<string[]>();
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
HSSFWorkbook workbook = new HSSFWorkbook(fs); // 处理.xls格式
ISheet sheet = workbook.GetSheetAt(0); // 第一个工作表
for (int i = 0; i <= sheet.LastRowNum; i++)
{
IRow row = sheet.GetRow(i);
if (row == null) continue;
List<string> cells = new List<string>();
for (int j = 0; j < row.LastCellNum; j++)
{
ICell cell = row.GetCell(j);
cells.Add(GetCellValueAsString(cell));
}
rows.Add(cells.ToArray());
}
}
return rows;
}
private static string GetCellValueAsString(ICell cell)
{
if (cell == null) return "";
return cell.CellType switch
{
CellType.String => cell.StringCellValue,
CellType.Numeric => DateUtil.IsCellDateFormatted(cell)
? cell.DateCellValue.ToString("yyyy-MM-dd HH:mm:ss")
: cell.NumericCellValue.ToString(),
CellType.Boolean => cell.BooleanCellValue.ToString(),
CellType.Formula => GetCellValueAsString(workbook.GetCreationHelper().CreateFormulaEvaluator().EvaluateInCell(cell)),
_ => cell.ToString()
};
}
private static string[] EscapeCsvFields(string[] fields)
{
for (int i = 0; i < fields.Length; i++)
{
if (fields[i].Contains(",") || fields[i].Contains("\"") || fields[i].Contains("\n"))
{
fields[i] = "\"" + fields[i].Replace("\"", "\"\"") + "\"";
}
}
return fields;
}
}
// 调用示例
XlsToCsvConverter.ConvertXlsToCsv("input.xls", "output.csv");
```
### 关键步骤说明:
1. **读取XLS文件**
- 使用 `NPOI.HSSF` 解析 `.xls` 格式文件
- 逐行读取数据并转换为字符串列表
2. **CSV追加逻辑**
- 通过 `StreamWriter(targetCsvPath, true)` 启用追加模式
- 新文件写入**所有行**(含列头)
- 已存在文件**跳过列头**仅追加数据行
3. **特殊字符处理**
- 字段包含逗号/引号时自动添加双引号包围
- 转义内部双引号(`"` → `""`)
4. **数据类型转换**
- 日期类型转为标准格式 `yyyy-MM-dd HH:mm:ss`
- 公式单元格计算后取值
### 依赖安装
通过NuGet安装NPOI库:
```bash
Install-Package NPOI -Version 2.6.0
```
### 注意事项
1. 文件路径需使用**完整物理路径**
2. 大文件建议分块读取避免内存溢出
3. 扩展名处理可使用 `Path.ChangeExtension("input.xls", ".csv")`[^4]
4. 追加时默认UTF-8编码,中文场景需统一编码
> 此方案通过直接解析Excel二进制数据,无需安装Office组件,适用于服务器环境[^1][^2]。
---
### 相关问题
1. **如何批量转换文件夹内所有XLS文件为CSV?**
2. **转换时如何跳过隐藏行或空单元格?**
3. **如何处理XLSX格式(新版Excel)的文件?**
4. **追加数据时如何避免重复标题行?**
[^1]: 引用自CSV与Excel格式转换的Python实现思路
[^2]: 参考数组转Excel文件的通用逻辑
[^4]: 来自C#的Path.ChangeExtension方法文档
[^5]: 借鉴CSV数据模型定义和文件追加写入模式
阅读全文
相关推荐

















