using System.IO; using System.IO.MemoryMappedFiles; using System.Threading.Tasks; using OfficeOpenXml; public void ConvertCsvToExcel(string csvPath, string excelPath, string sheetName) { ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // 使用内存映射文件读取CSV using (var mmf = MemoryMappedFile.CreateFromFile(csvPath, FileMode.Open, null, 0, MemoryMappedFileAccess.Read)) using (var stream = mmf.CreateViewStream(0, 0, MemoryMappedFileAccess.Read)) using (var reader = new StreamReader(stream)) { // 并行读取并处理数据 var lines = new System.Collections.Concurrent.ConcurrentBag<string[]>(); Parallel.ForEach(ReadLines(reader), line => { lines.Add(line.Split(',')); }); // 创建Excel包 using (var package = new ExcelPackage()) { // 工作表处理(保持原有逻辑) var existingSheet = package.Workbook.Worksheets[sheetName]; if (existingSheet != null) { package.Workbook.Worksheets.Delete(sheetName); } var worksheet = package.Workbook.Worksheets.Add(sheetName); // 按原始顺序写入数据 var orderedLines = lines.OrderBy(x => x[0]).ToArray(); // 需要根据实际调整排序依据 for (int row = 0; row < orderedLines.Length; row++) { var columns = orderedLines[row]; for (int col = 0; col < columns.Length; col++) { worksheet.Cells[row + 1, col + 1].Value = columns[col]; } } // 自动调整列宽 worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns(); // 条件删除行(保持原有逻辑) if (csvPath.Contains("Customer")) { worksheet.DeleteRow(10); } // 保存文件 FileInfo excelFile = new FileInfo(excelPath); package.SaveAs(excelFile); } } } // 流式逐行读取迭代器 private IEnumerable<string> ReadLines(StreamReader reader) { string line; while ((line = reader.ReadLine()) != null) { yield return line; } } 这个方法会导致读取数据丢失
时间: 2025-06-13 13:29:35 浏览: 27
<think>好的,我现在需要分析用户提供的C#代码,找出为什么ConvertCsvToExcel方法会导致数据丢失。用户提到使用这个方法时数据会丢失,我需要仔细检查代码的逻辑,特别是与文件读取、并行处理和排序相关的部分。
首先,代码的整体结构是这样的:使用内存映射文件读取CSV,然后并行处理每一行,分割成字符串数组并存入ConcurrentBag。接着创建Excel包,将数据按排序后写入工作表,最后保存文件。用户指出问题可能出在数据丢失,所以需要逐步检查每个环节。
首先看文件读取部分。代码使用MemoryMappedFile.CreateFromFile打开CSV文件,创建视图流并用StreamReader读取。这里可能的问题是内存映射文件的设置是否正确。注意到CreateFromFile的第三个参数是mapName,设置为null应该没问题。第四个参数是capacity,这里设置为0,根据文档,如果设置为0,容量会等于文件的实际大小。这可能没问题,但需要确认是否正确读取了整个文件。
接下来是并行处理部分。使用Parallel.ForEach遍历ReadLines(reader)返回的每一行,然后分割成数组存入ConcurrentBag。这里可能存在几个问题。首先,ReadLines方法使用StreamReader.ReadLine()逐行读取,而StreamReader本身不是线程安全的。当Parallel.ForEach并发读取时,多个线程可能同时调用ReadLine,导致数据错乱或丢失。这是非常关键的一点,因为StreamReader的实例在多个线程中同时使用会导致竞争条件,从而漏掉某些行或者读取错误的数据。
另外,ConcurrentBag是一个线程安全的集合,但它的特性是顺序不确定的。即使并行处理正确地将所有行加入集合,由于ConcurrentBag的存储顺序不保证添加顺序,后续的排序步骤可能会错误地重新排列数据。例如,代码中使用OrderBy(x => x[0])按第一列排序,如果原始数据没有按该列排序,这会导致行顺序改变,但用户可能期望保持原顺序。如果用户的数据本身没有按第一列排序,这样的排序会导致数据行在Excel中的顺序与CSV不同,看起来像是丢失,但实际上是被重新排列了。不过用户明确提到是数据丢失,而不仅仅是顺序问题,所以这可能不是主要原因,但需要检查。
然后,写入Excel的部分,代码将orderedLines数组按行和列写入单元格。这里可能的错误是,如果某些行在分割时出错,或者在并行处理时没有正确捕获异常,导致部分数据未被处理。例如,某些行可能在Split(',')时出现异常(比如空行或格式错误),但代码中没有异常处理,导致这些行被忽略。
另外,关于ReadLines方法,它返回的是一个IEnumerable<string>,通过yield return逐行返回。但是当Parallel.ForEach并发处理这个迭代器时,可能会出现线程安全问题,因为多个线程同时调用MoveNext方法,可能导致某些行被跳过或者重复读取。因为IEnumerable的枚举器默认不是线程安全的,这在并行环境中是不安全的,会导致数据丢失或重复。
此外,当使用Parallel.ForEach处理行时,如果某些行在分割时抛出异常(例如,空行导致Split结果为空),这些异常会被吞没,导致数据未被加入集合,从而丢失。但由于代码中没有异常处理,任何Split时的错误都会导致整个循环终止吗?不,Parallel.ForEach默认会聚合异常,但在此代码中并未处理,可能导致程序崩溃,但用户观察到的是数据丢失,所以可能部分行处理失败而未被注意到。
还有一个可能的问题是,在并行处理完成后,使用OrderedLines.OrderBy(x => x[0])对行进行排序。如果原始CSV文件的行顺序不是按照第一列排序的,那么写入Excel的顺序会改变,这可能让用户觉得某些行丢失了,但实际上是被重新排序了。但用户明确提到是数据丢失,所以这可能是次要问题,但需要确认用户的数据是否包含唯一标识,以便检查是否真的丢失还是顺序变化。
另一个可能的点是,在保存Excel时,如果文件路径已存在,是否会被覆盖?代码中使用package.SaveAs(excelFile),如果文件存在,会被覆盖,所以这应该不是问题。但如果在保存过程中发生错误,可能导致文件未正确写入,但用户的问题是关于数据丢失,不是文件保存失败。
总结可能的问题点:
1. 主要的线程安全问题:StreamReader在并行读取时不是线程安全的,导致部分行未被正确处理,造成数据丢失。
2. ConcurrentBag的顺序问题导致排序后的行顺序混乱,但可能被误认为丢失。
3. 异常未被处理,导致部分行未被处理,例如Split时出现错误。
其中,最可能的原因是问题1:使用Parallel.ForEach并发读取StreamReader的行,导致线程竞争,部分行未被读取,从而导致数据丢失。
验证这个问题的方法是,将Parallel.ForEach改为顺序处理,即使用普通的foreach循环,看是否仍然出现数据丢失。如果顺序处理时数据完整,那么问题就出在并行读取上。
此外,ReadLines方法返回的IEnumerable在并行处理时,枚举器可能被多个线程同时访问,导致行被遗漏或重复。例如,假设两个线程同时调用MoveNext,一个线程可能获取下一行,而另一个线程获取下下行,导致中间的行被跳过。
因此,正确的做法应该是先将所有行读取到内存中(例如List<string>),然后再进行并行处理,或者使用线程安全的方式读取所有行后再处理。或者,如果必须流式读取,应确保读取部分是线程安全的。
另外,代码中使用ConcurrentBag来收集分割后的行,但由于并行处理,行的顺序会被打乱,之后使用OrderBy(x => x[0])进行排序,如果原始数据的第一列不能保证唯一或顺序,这会导致最终Excel中的行顺序与CSV不一致,但用户可能误以为是数据丢失。例如,如果CSV中有多行具有相同的第一列值,排序后可能会覆盖或顺序错乱。
解决方案建议:
1. 避免在读取StreamReader时使用并行处理,因为StreamReader不是线程安全的。改为先顺序读取所有行到内存中,然后再进行并行处理(如果确实需要并行处理分割操作)。
2. 如果必须并行处理,确保读取行的部分是线程安全的。例如,先将所有行读取到List<string>中,然后使用Parallel处理这个列表。
3. 检查OrderBy的使用是否符合预期,是否需要保持原有行顺序。如果需要保持CSV中的原始顺序,应避免使用ConcurrentBag,因为它不保持顺序,改用其他并发集合或者先读取所有行并保留顺序。
修改后的代码可能的结构:
- 先顺序读取所有行到List<string>中。
- 然后使用Parallel.ForEach处理这些行,分割并保存到数组,但需要考虑如何保持顺序。或者,使用并行处理但保留原始顺序,例如使用Parallel.For循环,或者处理完后根据行号排序。
或者,完全放弃并行处理,因为IO操作可能已经是瓶颈,并行分割可能不会带来明显性能提升,反而引入复杂性。
例如,修改后的代码:
public void ConvertCsvToExcel(string csvPath, string excelPath, string sheetName)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
var lines = new List<string[]>();
// 顺序读取所有行
using (var reader = new StreamReader(csvPath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line.Split(','));
}
}
// 后续处理...
}
这样可以避免并行读取导致的数据丢失。如果用户确实需要并行处理以提高性能,可以考虑在读取所有行到内存后,再并行处理这些行,例如进行数据转换或其他计算密集型操作,但在此例中,分割CSV行可能并不是性能瓶颈,顺序读取可能更合适。
另一个需要注意的点是,在原始代码中,当使用OrderBy(x => x[0])时,如果CSV的某行的第一个字段为空或null,可能会导致排序异常,但代码中未处理这种情况,可能导致某些行被排除在orderedLines之外。但根据代码,所有行都被处理,只是排序可能有问题。
此外,条件删除行的逻辑(worksheet.DeleteRow(10))可能导致用户误以为第10行数据丢失,但这是在所有数据处理之后的操作,如果CSV行数不足10行,可能导致异常。但用户的问题是关于读取阶段的数据丢失,因此这可能不是主要原因。
综上所述,主要问题在于并行读取StreamReader导致的数据丢失,解决方案是改为顺序读取所有行后再进行处理。</think>这是一个使用C#将CSV转换为Excel的方法,主要存在**并行读取导致数据丢失**的问题。以下是对代码的逐层解析和改进建议:
---
### **问题分析:数据丢失原因**
1. **线程不安全的流读取**
$\texttt{StreamReader}$ 不是线程安全类,$\texttt{Parallel.ForEach}$ 多线程并发读取会导致:
- 行读取错位(部分行被跳过)
- 数据重复
- 流指针竞争
2. **无序集合导致排序失真**
$\texttt{ConcurrentBag}$ 是**无序集合**,使用 $\texttt{OrderBy(x => x[0])}$ 后:
- 若首列值不唯一,排序会打乱原始顺序
- 无法还原CSV原始行序
3. **异常处理缺失**
- $\texttt{Split(',')}$ 可能因格式错误抛出异常
- 并行循环中未捕获异常,导致部分数据静默丢失
---
### **改进方案**
#### **1. 改用顺序读取(修复核心问题)**
```csharp
var lines = new List<string[]>();
using (var reader = new StreamReader(csvPath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line.Split(','));
}
}
```
#### **2. 保留原始行序**
```csharp
// 直接按读取顺序写入Excel
for (int row = 0; row < lines.Count; row++)
{
var columns = lines[row];
// 写入逻辑...
}
```
#### **3. 可选:安全并行处理(仅用于计算密集型操作)**
```csharp
var lines = File.ReadAllLines(csvPath)
.AsParallel()
.AsOrdered() // 保持原始顺序
.Select(line => line.Split(','))
.ToList();
```
---
### **完整优化代码**
```csharp
public void ConvertCsvToExcel(string csvPath, string excelPath, string sheetName)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
// 顺序读取CSV
var lines = new List<string[]>();
using (var reader = new StreamReader(csvPath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
lines.Add(line.Split(','));
}
}
// 创建Excel
using (var package = new ExcelPackage())
{
// 工作表处理(略)
var worksheet = package.Workbook.Worksheets.Add(sheetName);
// 按原始顺序写入
for (int row = 0; row < lines.Count; row++)
{
var columns = lines[row];
for (int col = 0; col < columns.Length; col++)
{
worksheet.Cells[row + 1, col + 1].Value = columns[col];
}
}
// 后续操作(自动列宽、删行等)
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
if (csvPath.Contains("Customer"))
{
worksheet.DeleteRow(10);
}
package.SaveAs(new FileInfo(excelPath));
}
}
```
---
### **关键改进点**
1. **线程安全**
顺序读取确保100%数据完整性
2. **保留行序**
直接按CSV原始顺序写入Excel
3. **代码简化**
移除不必要的内存映射文件操作(普通流读取足够应对大多数CSV场景)
4. **可维护性提升**
减少并行带来的调试复杂度
---
### **性能建议**
- 如需处理超大规模数据(GB级),可改用内存映射文件+分段读取
- 推荐使用专用库(如CsvHelper)处理复杂CSV格式(含引号、转义符等)
阅读全文
相关推荐


















