CSV — не «просто текст»: практика работы с CSV в бизнес-приложениях на C# (кодировки, совместимость с Excel, защита от инъекций)
· Го Комура · CSharp, .NET, CSV, Excel, Кодировки символов, Интеграция файлов, Разработка под Windows, Техническая консультация
CSV-интеграция данных до сих пор остаётся действующим главным героем в мире бизнес-приложений. И именно из-за того, что она выглядит как «просто соединение текста запятыми», в этой области постоянно повторяются одни и те же аварии: самописный парсер ломается на запятой внутри поля, файл искажается при открытии в Excel, у номера телефона пропадает начальный ноль.
В этой статье, применительно к бизнес-приложениям на C#, читающим и записывающим CSV, мы разберём спецификацию CSV (RFC 4180) и реальные диалекты, выбор кодировки символов, проблемы совместимости с Excel, защиту от CSV-инъекций и эксплуатационные ловушки — в том порядке, в котором на практике чаще всего возникают затруднения при принятии решений.
1. Сначала выводы (таблица решений)
Проектные решения по CSV нужно выстраивать от ответа на вопрос «кто (или что) будет открывать этот файл».
| Назначение | Кодировка по умолчанию | Ключевые моменты проектирования |
|---|---|---|
| Человек открывает двойным щелчком в Excel | UTF-8 (с BOM) | Без BOM в некоторых окружениях Excel неверно распознаёт кодировку и текст искажается. Проблемы потери начальных нулей и преобразования в даты кодировкой не решаются (см. раздел 5) |
| Новая интеграция между системами | UTF-8 (без BOM) | В спецификации явно указать кодировку, правила кавычек, символ переноса строки и наличие заголовка. Также явно указать наличие/отсутствие BOM |
| Интеграция с устаревшей системой | Следовать спецификации партнёра (часто Shift_JIS) | В .NET (Core) для Shift_JIS требуется регистрация CodePagesEncodingProvider (см. раздел 4). Заранее договориться об обработке зависящих от платформы символов |
| Пакетная интеграция больших объёмов данных | Следовать спецификации | Обрабатывать построчно потоково, не загружая все записи в память целиком (см. раздел 7). Также предусмотреть блокировки и обработку недописанных файлов |
| Основная цель — просмотр и печать человеком в Excel | — | Стоит вообще рассмотреть вывод в xlsx вместо CSV (см. FAQ) |
Сначала изложим выводы.
- Разбор CSV с помощью
String.Split(',')формально ломается, даже если кажется, что всё работает. Причина в том, что этот подход не умеет обрабатывать запятые, переносы строк и кавычки внутри полей. В рамках стандартных средств .NET используйтеTextFieldParser(см. раздел 3). - Проблемы с кодировкой возникают из-за того, как их интерпретирует Excel. Для CSV, которые люди открывают в Excel, безопасным выбором на сегодня является UTF-8 с BOM. При этом в .NET есть подводный камень: наличие BOM зависит от способа указания
Encoding(см. раздел 4). - В .NET (Core) Shift_JIS без дополнительного пакета не работает.
Encoding.GetEncoding("shift_jis")завершится успешно только после регистрацииCodePagesEncodingProviderиз пакетаSystem.Text.Encoding.CodePages. Это типичное место, где обработка CSV, перенесённая с .NET Framework, впервые выбрасывает исключение уже в продакшене. - Потерю начальных нулей и преобразование в дату средствами самого CSV не предотвратить. Важный факт: это не спасает даже заключение поля в двойные кавычки (см. раздел 5).
- Приложениям, которые выводят пользовательский ввод в CSV, необходима защита от CSV-инъекций. Поля, начинающиеся с
=или+, Excel интерпретирует как формулы (см. раздел 6).
2. «Спецификация» CSV: RFC 4180 и реальные диалекты
У CSV есть фактический стандарт — RFC 41801. На практике достаточно запомнить всего четыре правила.
- Записи разделяются переносом строки (CRLF), поля — запятой.
- Если поле содержит запятую, перенос строки или двойную кавычку, всё поле целиком заключается в двойные кавычки.
- Двойная кавычка внутри поля экранируется удвоением:
"". - Поля, которые не обязательно заключать в кавычки, можно заключать или не заключать по желанию.
То есть следующая строка — это запись из трёх полей.
1001,"株式会社サンプル 営業部","住所は""東京都○○区1-2-3""です,と入力された備考"
При этом RFC 4180 — это лишь «фактический» стандарт, и в реальности CSV существует множество диалектов: перенос строки только LF, отсутствие обработки кавычек, разделитель в виде табуляции или точки с запятой, наличие или отсутствие строки заголовка. Именно поэтому указать в спецификации интеграции фразу «соответствует RFC 4180», а также явно зафиксировать кодировку, наличие BOM, символ переноса строки и наличие заголовка — это самая экономически эффективная мера по предотвращению будущих проблем.
3. Не пишите собственный парсер на Split(‘,’): практика чтения CSV
С учётом правил из предыдущей главы становится очевидно, почему String.Split(',') ломается. Запятая внутри поля вызывает лишнее разбиение, а перенос строки внутри кавычек обрывает строку на середине. То, что сейчас всё «работает», объясняется лишь тем, что в текущих файлах нет данных с запятыми внутри полей — в любой момент, в зависимости от данных, это может сломаться.
Если нужно корректно читать CSV в рамках стандартных средств .NET, можно использовать Microsoft.VisualBasic.FileIO.TextFieldParser. Несмотря на то что пространство имён относится к Visual Basic, из C# он используется совершенно обычным образом.
using Microsoft.VisualBasic.FileIO; // Microsoft.VisualBasic.Core (входит в состав SDK для Windows-десктопа)
using var parser = new TextFieldParser(path, System.Text.Encoding.UTF8)
{
TextFieldType = FieldType.Delimited,
HasFieldsEnclosedInQuotes = true, // корректно обрабатывать поля, заключённые в кавычки
TrimWhiteSpace = false, // по умолчанию true (пробелы по краям молча удаляются). Отключите, если пробелы имеют значение для данных
};
parser.SetDelimiters(",");
while (!parser.EndOfData)
{
try
{
string[]? fields = parser.ReadFields();
if (fields is null) break;
// обработать fields[0], fields[1], ...
}
catch (MalformedLineException ex)
{
// повреждённая строка: содержимое и номер строки можно записать через ErrorLine / ErrorLineNumber
logger.LogWarning("CSVの{Line}行目が不正な形式です: {Content}",
ex.LineNumber, parser.ErrorLine);
}
}
Практическое преимущество TextFieldParser в том, что помимо обработки кавычек (HasFieldsEnclosedInQuotes), он умеет обнаруживать повреждённые строки в виде MalformedLineException с указанием номера строки. При импорте CSV в бизнес-приложениях ключевым вопросом спецификации становится: «отклонять весь файл, если повреждена одна строка, или пропустить только эту строку и сообщить о ней» — и эту логику обработки исключений можно напрямую заложить в спецификацию.
Если требуются сопоставление столбцов с объектами, преобразование типов и производительность на масштабах в миллионы строк, рассмотрите проверенную библиотеку для CSV (широко используемую через NuGet). Какую бы библиотеку вы ни выбрали, стоит проверить четыре аспекта: «обработка кавычек по RFC 4180», «указание кодировки», «обработка повреждённых строк» и «возможность потокового чтения».
Для записи спецификация проще, поэтому собственная реализация реалистична, но обязательно соберите логику экранирования в одну функцию.
// Вся логика экранирования при записи собрана в этой единственной функции
static string ToCsvField(string? value)
{
value ??= string.Empty;
bool needsQuoting = value.Contains(',') || value.Contains('"')
|| value.Contains('\r') || value.Contains('\n');
return needsQuoting ? $"\"{value.Replace("\"", "\"\"")}\"" : value;
}
Если хоть в одном месте остался код, где, например, столбец с суммой вручную собирается через string.Join в обход общей функции, именно это место в будущем станет источником проблем.
4. Кодировки символов: поведение Excel и подводные камни .NET
4.1 Если файл открывают в Excel — UTF-8 с BOM
Большинство случаев искажения текста в CSV происходит из-за того, что Excel неверно распознаёт кодировку файла. Если открыть двойным щелчком CSV в UTF-8 без BOM (BOM — это несколько байт-меток в начале файла), то в некоторых окружениях он будет интерпретирован как ANSI (в японском окружении — как Shift_JIS), и весь японский текст превратится в кракозябры. При наличии BOM UTF-8 распознаётся правильно как UTF-8, поэтому для CSV, которые люди открывают двойным щелчком в Excel, безопасным выбором на сегодня является UTF-8 с BOM.
Здесь добавляется ещё один подводный камень на стороне .NET: наличие BOM зависит от того, как передан Encoding.
// UTF-8 без BOM: значение по умолчанию, если кодировка не указана
File.WriteAllText(path, csv);
// UTF-8 с BOM: статическое свойство Encoding.UTF8 настроено на вывод BOM
File.WriteAllText(path, csv, Encoding.UTF8);
// Если хотите явно выразить намерение, однозначнее указать через конструктор
File.WriteAllText(path, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
То, что результирующий файл отличается в зависимости от того, передан ли Encoding.UTF8 или кодировка опущена, — особенность, о которой невозможно догадаться, не зная её заранее. Если вы зафиксировали в спецификации правило «без BOM для интеграции между системами» и «с BOM для Excel», рекомендуется и в коде явно выражать намерение через new UTF8Encoding(...).
4.2 Для использования Shift_JIS в .NET (Core) требуется регистрация
При интеграции с устаревшими системами CSV в кодировке Shift_JIS (кодовая страница 932) всё ещё широко используется. Здесь важно, что .NET (Core) / .NET 5 и новее по умолчанию поддерживают лишь небольшой набор кодировок — в основном Unicode-семейство и ASCII. Вызов Encoding.GetEncoding("shift_jis") будет выбрасывать исключение до тех пор, пока не будет добавлен пакет System.Text.Encoding.CodePages и зарегистрирован CodePagesEncodingProvider.
using System.Text;
// Регистрируется один раз при запуске приложения (например, в Program.cs)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Теперь можно получить Shift_JIS (кодовая страница 932)
Encoding sjis = Encoding.GetEncoding("shift_jis");
Это классическое место аварий: приложение, перенёсшее обработку CSV из эпохи .NET Framework на .NET, впервые в продакшене получает файл в Shift_JIS и выбрасывает исключение. Ключевые моменты проверки при миграции собраны в статье «Чек-лист перед миграцией с .NET Framework на .NET».
Кроме того, при использовании Shift_JIS неизбежны проблемы с зависящими от платформы символами (например, ① или ㈱), проблема волнистого дефиса (тильды в японских шрифтах) и потеря символов, существующих только в Unicode. Основы этих вопросов кодировки подробно рассмотрены в статьях «Кодировки символов Windows и механизм искажения текста» и «Практика работы с кодировками символов и символами переноса строки в Windows».
5. Данные, которые ломаются при открытии в Excel: потеря нулей, преобразование в дату, экспоненциальная запись
Даже если кодировка настроена правильно, при просмотре в Excel остаётся ещё одна группа проблем: при открытии CSV Excel «интерпретирует» значения.
| Исходные данные | Отображение в Excel | Что произошло |
|---|---|---|
090123456xx (номер телефона) |
90123456xx |
Интерпретируется как число, начальный ноль пропадает |
007 (код товара) |
7 |
То же самое |
1-1 (номер подпункта) |
1 января |
Интерпретируется как дата |
1234567890123456 (16-значный учётный номер) |
1.23457E+15 |
Преобразуется в экспоненциальную запись, младшие разряды теряются |
Важно, что эти проблемы не решаются даже заключением поля в двойные кавычки. Кавычки лишь обозначают синтаксическую границу поля в CSV, а Excel заново интерпретирует его содержимое.
На практике есть три варианта решения.
- Просить открывать файл правильным способом в Excel. Если использовать не двойной щелчок, а функцию Excel «Импорт данных (из текста/CSV)» и указать для столбца тип «текст», данные не будут повреждены. Это стоит зафиксировать в регламенте эксплуатации.
- Если основная цель — просмотр, выводить данные в формате xlsx. Поскольку форматирование ячеек можно контролировать на стороне вывода, вся эта группа проблем исчезает в принципе. О том, как реализовать вывод файлов Excel, см. «Как реализовать вывод отчётов Excel». Отметим, что автоматизация Excel через COM на сервере или в сервисе не рекомендуется — как описано в статье «Проблема остающихся процессов EXCEL.EXE», это создаёт почву для зависания процессов, поэтому выбирайте вывод на основе библиотек.
- Вывод в формате
="007"— крайняя мера. Это приём, при котором значение отображается в Excel как текст, но с точки зрения CSV файл становится «нестандартным файлом с формулой внутри», что ломает импорт в системах, отличных от Excel. Не используйте этот приём для файлов, которые совместно используются с другими системами.
6. CSV-инъекции: не превращайте пользовательский ввод в формулы
В приложениях, которые выводят введённые пользователем значения (имя, примечание, адрес и т. п.) в CSV, предполагая последующее открытие в Excel, существует вектор атаки под названием CSV-инъекция. Поля, начинающиеся с =, +, -, @ и т. п., Excel интерпретирует как формулы, поэтому вредоносный ввод (например, формула вида =cmd|'/C calc'!A0, нацеленная на запуск внешнего процесса) может быть выполнен в окружении другого пользователя, который откроет этот CSV.
Базовая мера защиты — обезвреживать опасные начальные символы при выводе.
// Добавляем одинарную кавычку перед полем, которое может быть интерпретировано как формула.
// В японском окружении широкие (полноширинные) символы =+-@ тоже могут восприниматься как начало формулы, поэтому включаем их в проверку
static string SanitizeForExcel(string value)
{
if (value.Length > 0 && value[0]
is '=' or '+' or '-' or '@' or '\t' or '\r' or '\n'
or '=' or '+' or '-' or '@')
{
return "'" + value;
}
return value;
}
Начальная одинарная кавычка служит для Excel признаком «обрабатывать как текст». При этом список опасных начальных символов может меняться в зависимости от реализации и версии программы для работы с электронными таблицами, поэтому такую санитизацию устойчивее проектировать в направлении «пропускать без изменений только если начальный символ входит в список разрешённых (буквенно-цифровые и т. п.)» — так меньше риск упустить непредвиденный случай. Однако нужно учитывать, что если этот CSV импортируется другой системой, одинарная кавычка останется в данных, поэтому возможно и решение разделить файлы на «для открытия в Excel» и «для импорта в систему». Что касается того, стоит ли сочетать это с валидацией входных значений (например, вообще не разрешать начальный символ =), а также общего проектирования безопасности приложения — см. «Минимальный чек-лист безопасности Windows-приложений».
7. Эксплуатационные ловушки: где возникают проблемы помимо формата
Наконец, соберём моменты, где на практике возникают проблемы помимо самого синтаксиса CSV.
- Не привязывайте формат чисел и дат к культуре ОС.
value.ToString()зависит от региональных настроек операционной системы. В регионах, где десятичным разделителем служит запятая, значение выведется как1,5и вступит в конфликт с разделителем CSV. Для преобразований, предназначенных для интеграции файлов, явно указывайтеCultureInfo.InvariantCulture, а даты выводите в фиксированном формате, напримерyyyy-MM-dd. Проектирование работы с датами подробно рассмотрено в статье «Проектирование даты/времени и часовых поясов в бизнес-приложениях». - Обрабатывайте большие объёмы данных потоково — но допустимость обработки «построчно» зависит от спецификации.
File.ReadAllLinesзагружает все строки в память в виде массива, поэтому не подходит для больших файлов. Заменять его наFile.ReadLines(отложенное перечисление) или на построчное чтение черезStreamReaderможно только для файлов, в спецификации которых явно указано, что переносы строк внутри полей не допускаются. В CSV, соответствующем RFC 4180, где переносы строк внутри кавычек разрешены, разбиение по строкам может разорвать одну запись посередине. В этом случае используйтеTextFieldParser, читающий напрямую из пути к файлу (он может обрабатывать данные, не загружая всё в память), либо потоковое чтение библиотеки для CSV. - Не давайте читать «недописанный файл» и сами его не читайте. Базовая схема интеграции файлов — сторона вывода пишет во временный файл и затем переименовывает его, а сторона импорта проверяет блокировку перед чтением — подробно описана в статье «Основы блокировок при интеграции файлов», а нюансы импорта через мониторинг папки собраны в «Практическом руководстве по FileSystemWatcher».
- Будьте готовы к изменению числа столбцов. Зафиксируйте в спецификации, будет ли сопоставление вестись «по именам столбцов в строке заголовка» или «по фиксированному порядку столбцов с проверкой их количества и отклонением при несовпадении». Неявные обращения по магическим числам вроде
fields[7]незаметно ломаются при малейшем добавлении столбца партнёром по интеграции. - Заранее определите обработку завершающего переноса строки и пустых строк. Наличие переноса строки после последней строки, а также решение — пропускать пустые строки или отклонять их — это места, где поведение расходится в зависимости от реализации парсера.
TextFieldParserпропускает пустые строки.
Итог
CSV выглядит как формат, где «данные просто разделены запятыми», но на деле это формат интеграции, полный диалектов. Практика сводится к четырём пунктам: для чтения используйте компонент, умеющий обрабатывать кавычки (не пишите парсер на Split самостоятельно); кодировку определяйте, отталкиваясь от того, кто будет открывать файл (особенно Excel), и фиксируйте в спецификации в том числе наличие BOM; учитывайте, что интерпретацию значений Excel (потерю нулей, преобразование в дату) невозможно предотвратить средствами самого CSV; если выводите пользовательский ввод — внедрите защиту от инъекций. Уже одно только это позволяет устранить большинство проблем интеграции через CSV ещё на этапе проектирования.
Расследование искажений кодировки и повреждения данных в существующей интеграции через CSV, систематизация спецификаций интеграции с устаревшими системами, а также новая реализация функций импорта и экспорта CSV — всё это часто требует принятия решений с учётом реальных файлов и спецификаций партнёра по интеграции. Если сомневаетесь — обращайтесь за консультацией.
Связанные статьи
- Кодировки символов Windows и механизм искажения текста
- Практика работы с кодировками символов и символами переноса строки в Windows
- Как реализовать вывод отчётов Excel
- Проблема остающихся процессов EXCEL.EXE при взаимодействии через Excel COM
- Основы блокировок при интеграции файлов
- Практическое руководство по FileSystemWatcher
- Проектирование даты/времени и часовых поясов в бизнес-приложениях
- Чек-лист перед миграцией с .NET Framework на .NET
- Минимальный чек-лист безопасности Windows-приложений
Похожие направления консультаций
ООО «Комура Софт» занимается технической консультацией по разработке бизнес-приложений, включая интеграцию через CSV и файлы, расследованием причин искажения кодировки и повреждения данных в существующей интеграции, а также систематизацией спецификаций интеграции с устаревшими системами.
Источники
-
IETF, RFC 4180 - Common Format and MIME Type for Comma-Separated Values (CSV) Files. Фактический стандарт формата CSV. Об обработке кавычек для запятых, переносов строк и двойных кавычек внутри полей, а также об экранировании через
"". ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Интернационализация приложений WinForms/WPF — resx, сателлитные сборки и переключение культуры на практике
Разбираем интернационализацию десктопных приложений Windows на практике: различие между CurrentCulture и CurrentUICulture, устройство рес...
Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)
HttpClient в C#, создаваемый через using при каждом запросе, приводит к исчерпанию сокетов, а статический HttpClient перестаёт учитывать ...
Не только appsettings.json — практическое руководство по управлению конфигурацией Windows-бизнес-приложений (настройки окружений, секреты и место записи)
Практический разбор управления конфигурацией Windows-бизнес-приложений: слои appsettings.json, выбор между IConfiguration и семейством IO...
Печать и вывод PDF в Windows-бизнес-приложениях — выбор между System.Drawing.Printing, WPF и библиотеками отчётов
В виде таблицы выбора по требованиям собраны печать WinForms через PrintDocument, печать WPF через FlowDocument/FixedDocument и варианты ...
Как точно найти причину «медленной работы» с PerfView и dotnet-trace — практическое введение в анализ производительности .NET
Когда бизнес-приложение «работает медленно», «загружает ЦП под завязку» или «иногда зависает», какой инструмент выбрать и на что смотреть...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Проектирование и реализация интеграции между системами через CSV и вывода отчётов относится к практической области консультаций по разработке Windows-приложений.
Технические консультации и ревью дизайна
Расследование причин искажения кодировки и повреждения данных в существующей интеграции через CSV, а также систематизация требований — это техническая консультация, сопровождающаяся ревью проектных решений.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Какую кодировку символов следует использовать для CSV?
- Главное правило: если сторона-получатель уже определена, ориентируйтесь на её спецификацию. Если вы можете задать формат с нуля, безопасной отправной точкой будет: для файлов, которые люди открывают в Excel, использовать UTF-8 с BOM, а для интеграции между системами — UTF-8 без BOM, и явно зафиксировать это в спецификации. Если при интеграции с устаревшей системой требуется Shift_JIS, необходимо заранее договориться об обработке зависящих от платформы символов и нестандартных (внешних) знаков — иначе впоследствии возникнут споры об ответственности за искажение кодировки.
- Стоит ли использовать библиотеку для работы с CSV, или можно обойтись собственной реализацией?
- Для чтения CSV собственная реализация на String.Split по спецификации не может корректно обработать некоторые случаи (запятые, переносы строк и кавычки внутри полей), поэтому стоит использовать какой-либо готовый компонент. В рамках стандартных средств .NET класс TextFieldParser поддерживает обработку кавычек в соответствии с RFC 4180. Если вам нужны сопоставление столбцов с объектами, преобразование типов и производительность на больших объёмах данных, рассмотрите проверенную библиотеку для работы с CSV. Для записи спецификация проще, поэтому собственная реализация вполне реалистична, но обязательно создайте одну общую функцию для экранирования кавычек и пропускайте через неё весь вывод.
- Стоит ли использовать разделитель табуляции (TSV) вместо запятой?
- Если в данных часто встречаются запятые (адреса, имена, суммы и т. п.), переход на TSV снижает частоту необходимости обработки кавычек и облегчает визуальную проверку. Однако табуляция и переносы строк точно так же могут встречаться в данных, поэтому необходимость в обработке кавычек не исчезает. Важно понимать, что «раз это TSV, значит, о разделителе можно не думать» — неверное рассуждение. Если с партнёром по интеграции удаётся договориться, для предотвращения проблем важнее не сам разделитель, а чёткая фиксация в спецификации кодировки символов, правил кавычек, символа переноса строки и наличия/отсутствия заголовка.
- Если цель — открытие файла в Excel, не лучше ли выводить файл Excel вместо CSV?
- Если главная цель — «просмотр и печать человеком в Excel», стоит рассмотреть вывод в формате xlsx. CSV — это формат, в котором при открытии в Excel происходит потеря начальных нулей и преобразование значений в даты, и полностью предотвратить это средствами самого CSV невозможно. В xlsx же форматирование ячеек можно контролировать на стороне вывода. С другой стороны, если основная цель — импорт в другую систему, а просмотр в Excel вторичен, более реалистичным решением будет оставить CSV и описать в регламенте эксплуатации процедуру импорта (с использованием функции импорта данных).
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки