C# с возможностью передачи данных в csv
Может кто-нибудь, пожалуйста, скажите мне, почему следующий код не работает. Данные сохраняются в файле CSV, однако данные не разделяются. Все это существует в первой ячейке каждой строки.
StringBuilder sb = new StringBuilder();
foreach (DataColumn col in dt.Columns)
{
sb.Append(col.ColumnName + ',');
}
sb.Remove(sb.Length - 1, 1);
sb.Append(Environment.NewLine);
foreach (DataRow row in dt.Rows)
{
for (int i = 0; i < dt.Columns.Count; i++)
{
sb.Append(row[i].ToString() + ",");
}
sb.Append(Environment.NewLine);
}
File.WriteAllText("test.csv", sb.ToString());
Благодарю.
23 ответа
Следующая короткая версия прекрасно открывается в Excel, возможно, ваша проблема заключалась в запятой
.net = 3,5
StringBuilder sb = new StringBuilder();
string[] columnNames = dt.Columns.Cast<DataColumn>().
Select(column => column.ColumnName).
ToArray();
sb.AppendLine(string.Join(",", columnNames));
foreach (DataRow row in dt.Rows)
{
string[] fields = row.ItemArray.Select(field => field.ToString()).
ToArray();
sb.AppendLine(string.Join(",", fields));
}
File.WriteAllText("test.csv", sb.ToString());
.net> = 4.0
И, как отметил Тим, если вы используете.net>=4, вы можете сделать его еще короче:
StringBuilder sb = new StringBuilder();
IEnumerable<string> columnNames = dt.Columns.Cast<DataColumn>().
Select(column => column.ColumnName);
sb.AppendLine(string.Join(",", columnNames));
foreach (DataRow row in dt.Rows)
{
IEnumerable<string> fields = row.ItemArray.Select(field => field.ToString());
sb.AppendLine(string.Join(",", fields));
}
File.WriteAllText("test.csv", sb.ToString());
Как предполагает Кристиан, если вы хотите обрабатывать специальные символы, экранирующие поля, замените блок цикла на:
foreach (DataRow row in dt.Rows)
{
IEnumerable<string> fields = row.ItemArray.Select(field =>
string.Concat("\"", field.ToString().Replace("\"", "\"\""), "\""));
sb.AppendLine(string.Join(",", fields));
}
И последнее предложение, вы могли бы написать контент CSV построчно, а не как целый документ, чтобы избежать большого документа в памяти.
Я обернул это в класс расширения, который позволяет вам вызывать:
myDataTable.WriteToCsvFile("C:\\MyDataTable.csv");
на любой DataTable.
public static class DataTableExtensions
{
public static void WriteToCsvFile(this DataTable dataTable, string filePath)
{
StringBuilder fileContent = new StringBuilder();
foreach (var col in dataTable.Columns)
{
fileContent.Append(col.ToString() + ",");
}
fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);
foreach (DataRow dr in dataTable.Rows)
{
foreach (var column in dr.ItemArray)
{
fileContent.Append("\"" + column.ToString() + "\",");
}
fileContent.Replace(",", System.Environment.NewLine, fileContent.Length - 1, 1);
}
System.IO.File.WriteAllText(filePath, fileContent.ToString());
}
}
Новая функция расширения, основанная на ответе Пола Гримшоу. Я убрал его и добавил возможность обрабатывать неожиданные данные. (Пустые данные, встроенные кавычки и запятые в заголовках...)
Он также возвращает строку, которая является более гибкой. Он возвращает Null, если объект таблицы не содержит никакой структуры.
public static string ToCsv(this DataTable dataTable) {
StringBuilder sbData = new StringBuilder();
// Only return Null if there is no structure.
if (dataTable.Columns.Count == 0)
return null;
foreach (var col in dataTable.Columns) {
if (col == null)
sbData.Append(",");
else
sbData.Append("\"" + col.ToString().Replace("\"", "\"\"") + "\",");
}
sbData.Replace(",", System.Environment.NewLine, sbData.Length - 1, 1);
foreach (DataRow dr in dataTable.Rows) {
foreach (var column in dr.ItemArray) {
if (column == null)
sbData.Append(",");
else
sbData.Append("\"" + column.ToString().Replace("\"", "\"\"") + "\",");
}
sbData.Replace(",", System.Environment.NewLine, sbData.Length - 1, 1);
}
return sbData.ToString();
}
Вы называете это следующим образом:
var csvData = dataTableOject.ToCsv();
Если ваш код вызова ссылается на System.Windows.Forms
Сборка, вы можете рассмотреть принципиально другой подход. Моя стратегия состоит в том, чтобы использовать функции, уже предоставленные платформой, для достижения этой цели в очень небольшом количестве строк кода без необходимости циклически перебирать столбцы и строки. Что код ниже делает программно создать DataGridView
на лету и установить DataGridView.DataSource
к DataTable
, Затем я программно выбираю все ячейки (включая заголовок) в DataGridView
и позвонить DataGridView.GetClipboardContent()
, поместив результаты в Windows Clipboard
, Затем я вставляю содержимое буфера обмена в вызов File.WriteAllText()
, убедившись, что форматирование вставки указано как TextDataFormat.CommaSeparatedValue
,
Вот код:
public static void DataTableToCSV(DataTable Table, string Filename)
{
using(DataGridView dataGrid = new DataGridView())
{
// Save the current state of the clipboard so we can restore it after we are done
IDataObject objectSave = Clipboard.GetDataObject();
// Set the DataSource
dataGrid.DataSource = Table;
// Choose whether to write header. Use EnableWithoutHeaderText instead to omit header.
dataGrid.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText;
// Select all the cells
dataGrid.SelectAll();
// Copy (set clipboard)
Clipboard.SetDataObject(dataGrid.GetClipboardContent());
// Paste (get the clipboard and serialize it to a file)
File.WriteAllText(Filename,Clipboard.GetText(TextDataFormat.CommaSeparatedValue));
// Restore the current state of the clipboard so the effect is seamless
if(objectSave != null) // If we try to set the Clipboard to an object that is null, it will throw...
{
Clipboard.SetDataObject(objectSave);
}
}
}
Обратите внимание, что я также должен сохранить содержимое буфера обмена перед тем, как я начну, и восстановить его, как только я закончу, чтобы пользователь не получил кучу неожиданного мусора в следующий раз, когда пользователь попытается вставить. Основные предостережения к этому подходу: 1) Ваш класс должен ссылаться System.Windows.Forms
, что может быть не так на уровне абстракции данных, 2) Ваша сборка должна быть предназначена для платформы.NET 4.5, так как DataGridView не существует в 4.0, и 3) Метод не будет работать, если буфер обмена используется другой процесс.
В любом случае, этот подход может не подходить для вашей ситуации, но, тем не менее, он интересен и может стать еще одним инструментом в вашем наборе инструментов.
4 строки кода:
public static string ToCSV(DataTable tbl)
{
StringBuilder strb = new StringBuilder();
//column headers
strb.AppendLine(string.Join(",", tbl.Columns.Cast<DataColumn>()
.Select(s => "\"" + s.ColumnName + "\"")));
//rows
tbl.AsEnumerable().Select(s => strb.AppendLine(
string.Join(",", s.ItemArray.Select(
i => "\"" + i.ToString() + "\"")))).ToList();
return strb.ToString();
}
Обратите внимание, что ToList()
в конце важно; Мне нужно что-то, чтобы заставить выражение выражения. Если бы я играл в гольф, я мог бы использовать Min()
вместо.
Также обратите внимание, что результат будет иметь новую строку в конце из-за последнего вызова AppendLine()
, Вы можете не хотеть этого. Вы можете просто позвонить TrimEnd()
удалить это.
Я сделал это недавно, но включил двойные кавычки вокруг моих значений.
Например, измените эти две строки:
sb.Append("\"" + col.ColumnName + "\",");
...
sb.Append("\"" + row[i].ToString() + "\",");
Попробуйте изменить sb.Append(Environment.NewLine);
в sb.AppendLine();
,
StringBuilder sb = new StringBuilder();
foreach (DataColumn col in dt.Columns)
{
sb.Append(col.ColumnName + ',');
}
sb.Remove(sb.Length - 1, 1);
sb.AppendLine();
foreach (DataRow row in dt.Rows)
{
for (int i = 0; i < dt.Columns.Count; i++)
{
sb.Append(row[i].ToString() + ",");
}
sb.AppendLine();
}
File.WriteAllText("test.csv", sb.ToString());
Лучшая реализация будет
var result = new StringBuilder();
for (int i = 0; i < table.Columns.Count; i++)
{
result.Append(table.Columns[i].ColumnName);
result.Append(i == table.Columns.Count - 1 ? "\n" : ",");
}
foreach (DataRow row in table.Rows)
{
for (int i = 0; i < table.Columns.Count; i++)
{
result.Append(row[i].ToString());
result.Append(i == table.Columns.Count - 1 ? "\n" : ",");
}
}
File.WriteAllText("test.csv", result.ToString());
Для записи в файл, я думаю, что следующий метод является наиболее эффективным и простым: (Вы можете добавить кавычки, если хотите)
public static void WriteCsv(DataTable dt, string path)
{
using (var writer = new StreamWriter(path)) {
writer.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(dc => dc.ColumnName)));
foreach (DataRow row in dt.Rows) {
writer.WriteLine(string.Join(",", row.ItemArray));
}
}
}
Ошибка - разделитель списка.
Вместо того чтобы писать sb.Append(something... + ',')
Вы должны положить что-то вроде sb.Append(something... + System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator);
Вы должны поместить символ разделителя списка, сконфигурированный в вашей операционной системе (как в примере выше), или разделитель списка на клиентском компьютере, где будет просматриваться файл. Другой вариант - настроить его в app.config или web.config в качестве параметра вашего приложения.
Вот усовершенствование сообщения vc-74, которое обрабатывает запятые так же, как Excel. Excel помещает в кавычки данные, если в данных есть запятая, но не в кавычках, если в данных нет запятой.
public static string ToCsv(this DataTable inDataTable, bool inIncludeHeaders = true)
{
var builder = new StringBuilder();
var columnNames = inDataTable.Columns.Cast<DataColumn>().Select(column => column.ColumnName);
if (inIncludeHeaders)
builder.AppendLine(string.Join(",", columnNames));
foreach (DataRow row in inDataTable.Rows)
{
var fields = row.ItemArray.Select(field => field.ToString().WrapInQuotesIfContains(","));
builder.AppendLine(string.Join(",", fields));
}
return builder.ToString();
}
public static string WrapInQuotesIfContains(this string inString, string inSearchString)
{
if (inString.Contains(inSearchString))
return "\"" + inString+ "\"";
return inString;
}
Чтобы имитировать Excel CSV:
public static string Convert(DataTable dt)
{
StringBuilder sb = new StringBuilder();
IEnumerable<string> columnNames = dt.Columns.Cast<DataColumn>().
Select(column => column.ColumnName);
sb.AppendLine(string.Join(",", columnNames));
foreach (DataRow row in dt.Rows)
{
IEnumerable<string> fields = row.ItemArray.Select(field =>
{
string s = field.ToString().Replace("\"", "\"\"");
if(s.Contains(','))
s = string.Concat("\"", s, "\"");
return s;
});
sb.AppendLine(string.Join(",", fields));
}
return sb.ToString().Trim();
}
Вот мое решение, основанное на предыдущих ответах Пола Гримшоу и Энтони В.О. Я представил код в проекте C# на Github.
Мой основной вклад заключается в устранении явного создания и манипулирования StringBuilder
и вместо этого работает только с IEnumerable
, Это позволяет избежать выделения большого буфера в памяти.
public static class Util
{
public static string EscapeQuotes(this string self) {
return self?.Replace("\"", "\"\"") ?? "";
}
public static string Surround(this string self, string before, string after) {
return $"{before}{self}{after}";
}
public static string Quoted(this string self, string quotes = "\"") {
return self.Surround(quotes, quotes);
}
public static string QuotedCSVFieldIfNecessary(this string self) {
return (self == null) ? "" : self.Contains('"') ? self.Quoted() : self;
}
public static string ToCsvField(this string self) {
return self.EscapeQuotes().QuotedCSVFieldIfNecessary();
}
public static string ToCsvRow(this IEnumerable<string> self){
return string.Join(",", self.Select(ToCsvField));
}
public static IEnumerable<string> ToCsvRows(this DataTable self) {
yield return self.Columns.OfType<object>().Select(c => c.ToString()).ToCsvRow();
foreach (var dr in self.Rows.OfType<DataRow>())
yield return dr.ItemArray.Select(item => item.ToString()).ToCsvRow();
}
public static void ToCsvFile(this DataTable self, string path) {
File.WriteAllLines(path, self.ToCsvRows());
}
}
Этот подход прекрасно сочетается с преобразованием IEnumerable
в DataTable, как и просили здесь.
StringBuilder sb = new StringBuilder();
SaveFileDialog fileSave = new SaveFileDialog();
IEnumerable<string> columnNames = tbCifSil.Columns.Cast<DataColumn>().
Select(column => column.ColumnName);
sb.AppendLine(string.Join(",", columnNames));
foreach (DataRow row in tbCifSil.Rows)
{
IEnumerable<string> fields = row.ItemArray.Select(field =>string.Concat("\"", field.ToString().Replace("\"", "\"\""), "\""));
sb.AppendLine(string.Join(",", fields));
}
fileSave.ShowDialog();
File.WriteAllText(fileSave.FileName, sb.ToString());
Возможно, наиболее простым способом будет использовать:
https://github.com/ukushu/DataExporter
особенно в случае ваших данных, содержащих данные, содержащие /r/n
символы или символ разделителя внутри ваших ячеек dataTable.
Вам нужно только написать следующий код:
Csv csv = new Csv("\t");//Needed delimiter
var columnNames = dt.Columns.Cast<DataColumn>().
Select(column => column.ColumnName).ToArray();
csv.AddRow(columnNames);
foreach (DataRow row in dt.Rows)
{
var fields = row.ItemArray.Select(field => field.ToString()).ToArray;
csv.AddRow(fields);
}
csv.Save();
public void ExpoetToCSV(DataTable dtDataTable, string strFilePath)
{
StreamWriter sw = new StreamWriter(strFilePath, false);
//headers
for (int i = 0; i < dtDataTable.Columns.Count; i++)
{
sw.Write(dtDataTable.Columns[i].ToString().Trim());
if (i < dtDataTable.Columns.Count - 1)
{
sw.Write(",");
}
}
sw.Write(sw.NewLine);
foreach (DataRow dr in dtDataTable.Rows)
{
for (int i = 0; i < dtDataTable.Columns.Count; i++)
{
if (!Convert.IsDBNull(dr[i]))
{
string value = dr[i].ToString().Trim();
if (value.Contains(','))
{
value = String.Format("\"{0}\"", value);
sw.Write(value);
}
else
{
sw.Write(dr[i].ToString().Trim());
}
}
if (i < dtDataTable.Columns.Count - 1)
{
sw.Write(",");
}
}
sw.Write(sw.NewLine);
}
sw.Close();
}
Большинство существующих ответов могут легко вызвать
OutOfMemoryException
, поэтому я решил написать свой ответ .
НЕ ДЕЛАЙТЕ ЭТОГО:
использование DataSet + StringBuilder приводит к тому, что данные занимают память сразу в 3 раза:
- Загрузить все данные в
DataSet
- Скопируйте все данные в
StringBuilder
- Скопируйте данные в строку, используя
StringBuilder.ToString()
;
Вместо этого вы должны записать каждую строку в
FileStream
отдельно . Нет необходимости создавать весь CSV в памяти.
Еще лучше использовать DataReader вместо DataSet. Таким образом, вы можете читать из базы данных миллиарды записей одну за другой и записывать их в файл одну за другой.
Если вы не против использования внешней библиотеки для CSV, я могу порекомендовать самый популярный CsvHelper, у которого нет зависимостей.
using (var writer = new FileWriter("test.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
foreach (DataColumn dc in dt.Columns)
{
csv.WriteField(dc.ColumnName);
}
csv.NextRecord();
foreach (DataRow dr in dt.Rows)
{
foreach (DataColumn dc in dt.Columns)
{
csv.WriteField(dr[dc]);
}
csv.NextRecord();
}
writer.ToString().Dump();
}
В случае, если кто-то еще наткнется на это, я использовал File.ReadAllText для получения данных CSV, а затем изменил их и записал обратно с помощью File.WriteAllText. CRLF \ r \ n были в порядке, но вкладки \ t игнорировались, когда Excel открывал их. (Все решения в этой теме до сих пор используют запятую, но это не имеет значения.) Блокнот показал тот же формат в результирующем файле, что и в исходном коде. Diff даже показал файлы как идентичные. Но я понял, когда открыл файл в Visual Studio с помощью бинарного редактора. Исходный файл был Unicode, но целью был ASCII. Чтобы исправить это, я модифицировал ReadAllText и WriteAllText с третьим аргументом, установленным как System.Text.Encoding.Unicode, и оттуда Excel смог открыть обновленный файл.
private string ExportDatatableToCSV(DataTable dtTable)
{
StringBuilder sbldr = new StringBuilder();
if (dtTable.Columns.Count != 0)
{
foreach (DataColumn col in dtTable.Columns)
{
sbldr.Append(col.ColumnName + ',');
}
sbldr.Append("\r\n");
foreach (DataRow row in dtTable.Rows)
{
foreach (DataColumn column in dtTable.Columns)
{
sbldr.Append(row[column].ToString() + ',');
}
sbldr.Append("\r\n");
}
}
return sbldr.ToString();
}
StringBuilder sb = new StringBuilder();
foreach (DataColumn col in table.Columns)
{
sb.Append(col.ColumnName + ";");
}
foreach (DataRow row in table.Rows)
{
sb.AppendLine();
foreach (DataColumn col in table.Columns)
{
sb.Append($@"{Convert.ToString(row[col])}" + ";");
}
}
File.WriteAllText(path, sb.ToString());
DataTable dt = yourData();
StringBuilder csv = new StringBuilder();
int dcCounter = 0;
foreach (DataColumn dc in dt.Columns)
{
csv.Append(dc);
if (dcCounter != dt.Columns.Count - 1)
{
csv.Append(",");
}
dcCounter++;
}
csv.AppendLine();
int numOfDc = dt.Columns.Count;
foreach (DataRow dr in dt.Rows)
{
int colIndex = 0;
while (colIndex <= numOfDc - 1)
{
var colVal = dr[colIndex].ToString();
if (colVal != null && colVal != "")
{
DateTime isDateTime;
if (DateTime.TryParse(colVal, out isDateTime))
{
csv.Append(Convert.ToDateTime(colVal).ToShortDateString());
}
else
{
csv.Append(dr[colIndex]);
}
}
else
{
csv.Append("N/A");
}
if (colIndex != numOfDc - 1)
{
csv.Append(",");
}
colIndex++;
}
csv.AppendLine();
Мне также нужно было переопределить данные, поэтому есть несколько операторов "если еще". Мне нужно было убедиться, что если поле было пустым, чтобы ввести вместо него "N/A", или если поле Date было отформатировано как "01.01.1900:00", оно будет сохранено как "01.01.1900" вместо.
Если все данные все еще находятся в первой ячейке, это означает, что приложение, с которым вы открыли файл, ожидает другой разделитель. MSExcel может обрабатывать запятую как разделитель, если не указано иное.