При интеграции 1С со сторонними веб-сервисами часто возникает задача выгрузки табличных данных в формате CSV через протокол HTTP методом POST с составным содержимым multipart/form-data. Нередко при такой отправке сервер возвращает ошибку валидации данных, например: Отсутствует колонка "ID", хотя визуально файл сформирован корректно и первая колонка действительно называется «ID».
Проанализируем причину этой проблемы, выясним, откуда берется невидимый символ перед заголовком первой колонки, и подробно разберем правильные способы подготовки и отправки данных средствами платформы 1С:Предприятие.
Если открыть результирующий файл в шестнадцатеричном редакторе или специализированном текстовом редакторе в кодировке UTF-8, можно заметить, что перед первым символом заголовка вставляется неразрывный пробел нулевой ширины — ZWNBSP (символ Юникода U+FEFF). Это метка порядка байтов — BOM (Byte Order Mark), представляющая собой последовательность трех байт: 0xEF, 0xBB, 0xBF.
Платформа 1С при стандартной записи файлов через ЗаписьТекста или ТекстовыйДокумент с указанием кодировки КодировкаТекста.UTF8 по умолчанию всегда добавляет эти три байта в начало потока. В то же время большинство внешних веб-серверов и библиотек парсинга CSV не ожидают сигнатуру BOM и считывают её как часть имени первой колонки. В итоге сервер видит заголовок [BOM]ID вместо ожидаемого ID и отклоняет запрос.
Рассмотрим самый компактный и надежный способ сформировать CSV-файл в UTF-8 без добавления сигнатуры BOM. Для этого воспользуемся встроенной функцией ПолучитьДвоичныеДанныеИзСтроки, в которой предусмотрен специальный параметр управления записью BOM:
// Формируем текст CSV в переменной
ТекстCSV = "ID;Наименование;Количество" + Символы.ВК + Символы.ПС;
ТекстCSV = ТекстCSV + "1;Товар 1;10" + Символы.ВК + Символы.ПС;
// Третий параметр (Ложь) отключает добавление метки BOM
ДвоичныеДанныеCSV = ПолучитьДвоичныеДанныеИзСтроки(ТекстCSV, "UTF-8", Ложь);
// Сохраняем чистый UTF-8 во временный файл
ИмяФайлаДляЗагрузки = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанныеCSV.Записать(ИмяФайлаДляЗагрузки);
Этот способ позволяет гарантированно избавиться от символа ZWNBSP без необходимости ручной обработки байтов.
Если объем данных велик и хранить весь текст выгрузки в оперативной памяти нерационально, разберем по шагам потоковую запись. При последовательной записи данных через объект ЗаписьДанных в файл метод ЗаписатьСтроку не добавляет BOM-маркер:
ИмяФайлаCSV = ПолучитьИмяВременногоФайла("csv");
// Открываем файловый поток для записи
ФайловыйПоток = Новый ФайловыйПоток(ИмяФайлаCSV, РежимРаскрытияФайла.Создать, ДоступКФайлу.Запись);
ЗаписьДанных = Новый ЗаписьДанных(ФайловыйПоток);
// Записываем заголовки и строки с явным указанием кодировки UTF-8
ЗаписьДанных.ЗаписатьСтроку("ID;Наименование;Количество", "UTF-8", Символы.ВК + Символы.ПС);
ЗаписьДанных.ЗаписатьСтроку("1;Товар 1;10", "UTF-8", Символы.ВК + Символы.ПС);
// Закрываем запись и поток
ЗаписьДанных.Закрыть();
ФайловыйПоток.Закрыть();
Такой подход обеспечивает высокую скорость работы и предотвращает появление нежелательных байтов в начале файла.
Если CSV-файл уже сформирован сторонней подсистемой или выгружен типовым механизмом с BOM, разберем, как удалить первые 3 байта перед отправкой:
ДвоичныеДанныеСBOM = Новый ДвоичныеДанные(ИмяИсходногоФайла);
Буфер = ДвоичныеДанныеСBOM.ОткрытьПотокДляЧтения().ПрочитатьВБуферДвоичныхДанных();
// Проверяем наличие сигнатуры UTF-8 BOM (0xEF, 0xBB, 0xBF)
Если Буфер.Размер >= 3 И Буфер[0] = 239 И Буфер[1] = 187 И Буфер[2] = 191 Тогда
// Создаем буфер нужного размера без первых трех байт
НовыйБуфер = Новый БуферДвоичныхДанных(Буфер.Размер - 3);
Буфер.Скопировать(3, НовыйБуфер, 0, Буфер.Размер - 3);
// Перезаписываем файл
ПотокДляЗаписи = Новый ФайловыйПоток(ИмяИсходногоФайла, РежимРаскрытияФайла.Создать, ДоступКФайлу.Запись);
ЗаписьДанных = Новый ЗаписьДанных(ПотокДляЗаписи);
ЗаписьДанных.ЗаписатьБуферДвоичныхДанных(НовыйБуфер);
ЗаписьДанных.Закрыть();
ПотокДляЗаписи.Закрыть();
КонецЕсли;
Кроме кодировки самого файла, при сборке составного POST-запроса часто допускаются критические ошибки, приводящие к сбоям разбора данных на стороне веб-сервера:
CRLF (Символы.ВК + Символы.ПС). Использование только Символы.ПС нередко приводит к тому, что веб-сервер не может найти границы блоков multipart."--" + Boundary), а финальный разделитель закрывается двумя дефисами и спереди, и сзади: "--" + Boundary + "--". Дублирование разделителей в конце файла ломает структуру запроса.CRLF, закрывающий boundary пристыкуется непосредственно к последней строке CSV. Это приведет к порче последней записи данных.Authorization (например, Authorization: Bearer <токен>), а не Authenticate.Соберем все рекомендации воедино и посмотрим на пример корректного формирования и выполнения HTTP-запроса:
РазделительСтрок = Символы.ВК + Символы.ПС;
Boundary = "----Boundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");
// 1. Формируем тело CSV без BOM
ТекстCSV = "ID;Наименование;Количество" + РазделительСтрок;
ТекстCSV = ТекстCSV + "1001;Тестовая позиция;5" + РазделительСтрок;
ДД_CSV = ПолучитьДвоичныеДанныеИзСтроки(ТекстCSV, "UTF-8", Ложь);
ИмяФайлаCSV = ПолучитьИмяВременногоФайла("csv");
ДД_CSV.Записать(ИмяФайлаCSV);
// 2. Формируем начальную часть multipart
ИмяФайлаНачало = ПолучитьИмяВременногоФайла("txt");
ФайлНачало = Новый ЗаписьТекста(ИмяФайлаНачало, КодировкаТекста.ANSI, РазделительСтрок, Ложь);
// Текстовое поле формы
ФайлНачало.ЗаписатьСтроку("--" + Boundary);
ФайлНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""format""");
ФайлНачало.ЗаписатьСтроку("");
ФайлНачало.ЗаписатьСтроку("smsPersons");
// Заголовок передаваемого файла
ФайлНачало.ЗаписатьСтроку("--" + Boundary);
ФайлНачало.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""upload.csv""");
ФайлНачало.ЗаписатьСтроку("Content-Type: text/csv; charset=utf-8");
ФайлНачало.ЗаписатьСтроку("");
ФайлНачало.Закрыть();
// 3. Формируем завершающую часть multipart
ИмяФайлаКонец = ПолучитьИмяВременногоФайла("txt");
ФайлКонец = Новый ЗаписьТекста(ИмяФайлаКонец, КодировкаТекста.ANSI, РазделительСтрок, Ложь);
ФайлКонец.ЗаписатьСтроку("");
ФайлКонец.ЗаписатьСтроку("--" + Boundary + "--");
ФайлКонец.Закрыть();
// 4. Объединяем части в один файл отправки
ИмяФайлаОтправки = ПолучитьИмяВременногоФайла("bin");
МассивФайлов = Новый Массив;
МассивФайлов.Добавить(ИмяФайлаНачало);
МассивФайлов.Добавить(ИмяФайлаCSV);
МассивФайлов.Добавить(ИмяФайлаКонец);
ОбъединитьФайлы(МассивФайлов, ИмяФайлаОтправки);
// 5. Выполняем HTTP-запрос
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Boundary);
Заголовки.Вставить("Authorization", "Bearer " + Токен);
HTTPСоединение = Новый HTTPСоединение(Сервер, 443,,,, 30, Новый ЗащищенноеСоединениеOpenSSL);
HTTPЗапрос = Новый HTTPЗапрос(Ресурс, Заголовки);
HTTPЗапрос.УстановитьИмяФайлаТела(ИмяФайлаОтправки);
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
КодСостояния = HTTPОтвет.КодСостояния;
ТекстОтвета = HTTPОтвет.ПолучитьТелоКакСтроку();
// 6. Удаляем временные файлы
УдалитьФайлы(ИмяФайлаНачало);
УдалитьФайлы(ИмяФайлаCSV);
УдалитьФайлы(ИмяФайлаКонец);
УдалитьФайлы(ИмяФайлаОтправки);
Применение функции ПолучитьДвоичныеДанныеИзСтроки с флагом отключения BOM и использование стандартного разделителя CRLF обеспечивают безошибочную передачу данных и корректное распознавание структуры колонок CSV внешним сервисом.