读取 SQL 文件并写入 CSV 时数据列错位
结论
数据列错位通常与 SQL 查询或 OleDbDataAdapter 读取无关,问题往往出在 CSV 字段没有按规则转义。
字段中只要有逗号,直接拼接到一行里就会被当成多个字段。双引号、回车和换行也可能破坏 CSV 结构。处理时需要注意:
- 所有字段都用双引号包围;
- 字段内部的
"写成""; - 表头和数据使用相同的转义方法;
- 表头的数量和顺序必须与 SQL 查询结果一致。
为什么会发生错位
假设数据库中的地址是:
Flat 2, 18 High Street
如果直接输出:
1001,John,Flat 2, 18 High Street,London
CSV 解析器会把它识别成 5 个字段,而预期只有 4 个。从这里开始,后面的数据就会向右错一列。
正确格式是:
"1001","John","Flat 2, 18 High Street","London"
再看下面这个值:
John "Jack" Smith
应该写成:
"John ""Jack"" Smith"
用双引号包围字段后,字段中的换行符也能得到正确处理。
使用统一的 CSV 转义方法
可以先为所有字段编写一个转义函数:
using System;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
private static string EscapeCsvField(object value)
{
if (value == null || value == DBNull.Value)
return "\"\"";
string text;
if (value is DateTime dateTime)
{
text = dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
else if (value is IFormattable formattable)
{
text = formattable.ToString(null, CultureInfo.InvariantCulture);
}
else
{
text = value.ToString();
}
return "\"" + text.Replace("\"", "\"\"") + "\"";
}
然后根据 DataTable 的列生成表头和数据:
DataTable employeeTable = accessDataSet.Tables["employee"];
using (StreamWriter writer = new StreamWriter(
csvFileName,
false,
new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)))
{
string headerLine = string.Join(
",",
employeeTable.Columns
.Cast<DataColumn>()
.Select(column => EscapeCsvField(column.ColumnName)));
writer.WriteLine(headerLine);
foreach (DataRow row in employeeTable.Rows)
{
string csvLine = string.Join(
",",
row.ItemArray.Select(EscapeCsvField));
writer.WriteLine(csvLine);
}
}
这样处理后,数据中的逗号、双引号、回车和换行都不会改变字段数量。
如果 CSV 必须使用自定义表头,建议把表头放进数组,不要手写一整段以逗号分隔的字符串:
string[] headers =
{
"Reference",
"Gender",
"Title",
"Forename",
"Surname",
"Address1",
"Address2",
"Address3",
"Address4",
"Postcode",
"Country",
"P46 Statement A,B or C",
"P46(Expat) Statement A, B or C"
};
if (headers.Length != employeeTable.Columns.Count)
{
throw new InvalidOperationException(
$"表头有 {headers.Length} 列,但查询结果有 {employeeTable.Columns.Count} 列。");
}
writer.WriteLine(string.Join(",", headers.Select(EscapeCsvField)));
自定义表头也可能包含逗号,例如:
P46 Statement A,B or C
如果没有双引号,这段文字会被拆成两个表头字段。因此,表头同样需要经过 EscapeCsvField 处理。
改进数据库读取代码
连接、命令和适配器使用后都应及时释放。填充数据前还要清理同名数据表,避免多次点击按钮后残留旧数据:
string sql = File.ReadAllText(
@"X:\ZoddsandEnds\Emer\SQL\Employees.txt");
string connectionString =
"Provider=Microsoft.ACE.OLEDB.12.0;" +
"Data Source=" + dataAddress + ";" +
"Persist Security Info=False;";
try
{
if (accessDataSet.Tables.Contains("employee"))
accessDataSet.Tables.Remove("employee");
using (OleDbConnection connection =
new OleDbConnection(connectionString))
using (OleDbCommand command =
new OleDbCommand(sql, connection))
using (OleDbDataAdapter adapter =
new OleDbDataAdapter(command))
{
connection.Open();
adapter.Fill(accessDataSet, "employee");
}
}
catch (Exception ex)
{
MessageBox.Show(
"An error has occurred, failed to retrieve data from the database.\r\n" +
ex.Message);
return;
}
SQL 文件只是查询文本的来源,不负责 CSV 字段转义。如果查询结果已经正确写入 DataTable,就应该重点检查输出过程。
还需要检查的几个问题
表头字符串存在拼接错误
原代码中,下面两段之间缺少逗号:
"P46(Pen) Is Recently Bereaved, P46(Expat) Statement A, B or C" +
"P46 (Expat Student Loan Indicator, ..."
拼接后会变成:
P46(Expat) Statement A, B or CP46 (Expat Student Loan Indicator
结果是两个表头粘在一起。即使补上逗号,表头原本包含的逗号仍然需要转义,所以使用字符串数组生成表头更合适。
表头必须与查询列严格对应
还要逐项检查:
- 表头数量是否等于
DataTable.Columns.Count; - 表头顺序是否与 SQL
SELECT的列顺序一致; - SQL 是否使用了
SELECT *; - 查询中是否有重名列或计算列。
建议在 SQL 中明确列出字段:
SELECT
EmployeeReference,
Gender,
Title,
Forename,
Surname,
Address1,
Address2
FROM Employees;
不要依赖 SELECT * 的隐含列顺序。
不要通过删除逗号修复
删除地址、公司名称或备注中的逗号,可能暂时让列不再错位,却会损坏原始数据。正确的方法是转义字段,而不是修改字段内容。
Excel 的显示不一定代表文件结构错误
在某些地区设置下,Excel 默认使用分号而不是逗号作为列表分隔符。如果文件在文本编辑器或标准 CSV 解析器中结构正确,但用 Excel 打开后仍然显示异常,需要检查系统区域设置,或者使用 Excel 的”从文本/CSV”功能导入。
代码层面更稳妥的做法是使用成熟的 CSV 库,例如 CsvHelper,由它统一处理引号、分隔符、换行、编码和类型格式。如果不想引入第三方依赖,上面的 EscapeCsvField 配合 string.Join,也足以解决常见的列错位问题。
备注:内容仅供参考。
