Байты на диск: ДвоичныеДанные, буфер и потоки во встроенном языке 1С 8.3

· 9 мин

Содержание

Записать двоичные данные в файл в 1С проще всего одной строкой: ДД.Записать("C:\Temp\file.bin"), где ДД — значение типа ДвоичныеДанные. Файл создаётся на том компьютере, где выполняется код: на сервере 1С, если это серверная процедура, или у пользователя, если клиентская.

Сложности начинаются, когда байты нужно собрать по кусочкам, поправить заголовок, дописать в конец существующего файла или отдать файл пользователю из веб-клиента. Для этого в платформе 8.3 есть ДвоичныйБуфер, потоки (ПотокВПамяти, ФайловыйПоток) и объект ЗаписьДанных. Всё ниже относится к платформе 8.3 и не зависит от конфигурации: код одинаково работает в БП, ЗУП, УТ и в собственной разработке.

Что такое ДвоичныеДанные и почему их нельзя изменить

ДвоичныеДанные — это неизменяемый набор байтов. Его можно прочитать, записать в файл, поместить в хранилище, но нельзя поменять пятый байт или дописать что-то в конец. Любая «правка» — это создание нового значения.

Откуда обычно берутся двоичные данные:

  • 📄 из файла: Новый ДвоичныеДанные("C:\Temp\in.pdf");
  • 🖼️ из реквизита типа ХранилищеЗначения: Объект.Скан.Получить();
  • 🌐 из ответа HTTP-сервиса: Ответ.ПолучитьТелоКакДвоичныеДанные();
  • 🔤 из строки: ПолучитьДвоичныеДанныеИзСтроки(Текст, "UTF-8");
  • 🔐 из Base64: ПолучитьДвоичныеДанныеИзBase64Строки(Строка) или старая функция Base64Значение(Строка);
  • 🧩 из шестнадцатеричной строки: ПолучитьДвоичныеДанныеИзHexСтроки("FFD8FF").

У самого объекта немного методов, и нужны почти все:

МетодЧто делает
Размер()Возвращает число байтов
Записать(ИмяФайла)Сохраняет байты в файл, перезаписывая его
Записать(Поток)Пишет байты в открытый поток
ОткрытьПотокДляЧтения()Даёт поток для последовательного чтения без копии в памяти

Минимальный рабочий пример на сервере:

&НаСервереБезКонтекста
Процедура СохранитьФайлНаСервере(АдресВХранилище, ИмяФайла)
	ДД = ПолучитьИзВременногоХранилища(АдресВХранилище);
	Если ТипЗнч(ДД) <> Тип("ДвоичныеДанные") Тогда
		ВызватьИсключение "Во временном хранилище нет двоичных данных";
	КонецЕсли;
	ДД.Записать(ИмяФайла);
КонецПроцедуры

Метод Записать не создаёт недостающие каталоги. Если пути D:\Обмен\2025\ нет, будет ошибка. Создайте каталог заранее: СоздатьКаталог("D:\Обмен\2025").

Склеить несколько фрагментов можно без потоков: СоединитьДвоичныеДанные(МассивФрагментов). Обратная операция — РазделитьДвоичныеДанные(ДД, РазмерЧасти), удобно для отправки больших файлов частями.

Правка отдельных байтов: ДвоичныйБуфер

Если нужно поменять конкретные байты — записать сигнатуру, номер версии формата, контрольную сумму в заголовок, — используйте ДвоичныйБуфер. В отличие от двоичных данных, буфер изменяемый, но его размер фиксирован при создании.

Основные приёмы:

  1. Создать пустой буфер: Буфер = Новый ДвоичныйБуфер(16); — 16 байтов, заполненных нулями.
  2. Установить байт: Буфер.Установить(0, 255); — значение от 0 до 255.
  3. Прочитать байт: Буфер.Получить(0).
  4. Записать целое число в несколько байтов: Буфер.ЗаписатьЦелое32(4, 1024, ПорядокБайтов.LittleEndian);.
  5. Превратить буфер в двоичные данные: ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер).
  6. И обратно: ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ДД).

Пример: собираем файл с 8-байтовым заголовком и полезной нагрузкой.

Функция СобратьФайлСЗаголовком(Нагрузка)
	// Заголовок: 4 байта сигнатуры + 4 байта длины нагрузки
	Заголовок = Новый ДвоичныйБуфер(8);
	Заголовок.Установить(0, 77);  // M
	Заголовок.Установить(1, 89);  // Y
	Заголовок.Установить(2, 70);  // F
	Заголовок.Установить(3, 49);  // 1
	Заголовок.ЗаписатьЦелое32(4, Нагрузка.Размер(), ПорядокБайтов.LittleEndian);

	ДанныеЗаголовка = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Заголовок);

	Части = Новый Массив;
	Части.Добавить(ДанныеЗаголовка);
	Части.Добавить(Нагрузка);
	Возврат СоединитьДвоичныеДанные(Части);
КонецФункции

Порядок байтов — частая причина «битых» файлов. Большинство форматов под Windows и x86 используют LittleEndian: число 1024 ляжет как 00 04 00 00. Сетевые протоколы и часть форматов (например, PNG) хранят числа в BigEndian: 00 00 04 00. Сверяйтесь со спецификацией формата, а не с привычкой.

Буфер целиком живёт в оперативной памяти. Буфер на 500 МБ в рабочем процессе сервера — плохая идея: при нескольких одновременных пользователях процесс rphost раздуется. Для больших файлов используйте потоки.

Чтобы быстро посмотреть, что получилось, выведите байты в шестнадцатеричном виде: ПолучитьHexСтрокуИзБуфераДвоичныхДанных(Заголовок) вернёт строку вида 4D594631....

Как записать двоичные данные через ПотокВПамяти и ЗаписьДанных

Когда файл собирается из множества мелких кусков — байт, строка, ещё кусок, — склеивать массивы неудобно. Удобнее писать последовательно в поток. Объект ЗаписьДанных умеет писать в любой поток байты, числа, строки в нужной кодировке и готовые двоичные данные.

Поток в памяти подходит, когда результат нужен как значение — положить в хранилище, отправить по HTTP, вложить в письмо.

Функция СформироватьПакет(ТекстКомментария, Вложение)
	Поток = Новый ПотокВПамяти;
	Запись = Новый ЗаписьДанных(Поток);

	Запись.ЗаписатьБайт(1);                              // версия формата
	Запись.ЗаписатьСимволы(ТекстКомментария, "UTF-8");   // текст без перевода строки
	Запись.ЗаписатьБайт(0);                              // разделитель
	Запись.Записать(Вложение);                           // готовые ДвоичныеДанные

	Запись.Закрыть();
	Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
КонецФункции

Чем отличаются методы записи:

Метод ЗаписьДанныхЧто пишет
ЗаписатьБайт(Число)Один байт 0–255
ЗаписатьСимволы(Строка, Кодировка)Строку в заданной кодировке без разделителя
ЗаписатьСтроку(Строка, Кодировка)Строку и разделитель строк
Записать(ДвоичныеДанные)Готовый набор байтов
ЗаписатьБуферДвоичныхДанных(Буфер)Содержимое буфера

💡 ЗакрытьИПолучитьДвоичныеДанные() — ключевой метод потока в памяти. После него поток закрыт, писать в него больше нельзя. Если забыть закрыть ЗаписьДанных до этого вызова, часть данных может остаться в её внутреннем буфере и не попасть в результат.

Строка в файл без лишних байтов

Частая задача — записать текст в файл «как есть», без BOM в начале. ТекстовыйДокумент.Записать и ЗаписьТекста в UTF-8 по умолчанию ставят BOM, и некоторые банковские и государственные шлюзы такой файл не принимают. Решение через двоичные данные:

ДД = ПолучитьДвоичныеДанныеИзСтроки(Текст, "UTF-8", Ложь); // Ложь — без BOM
ДД.Записать(ИмяФайла);

Запись больших файлов и дописывание: ФайловыйПоток

Если файл большой или его нужно дописывать порциями, пишите сразу в файловый поток. В памяти тогда держится только текущий кусок.

Процедура ДописатьВЖурнал(ИмяФайла, Фрагмент)
	Поток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Дописать);
	Попытка
		Фрагмент.Записать(Поток);
		Поток.Закрыть();
	Исключение
		Поток.Закрыть();
		ВызватьИсключение;
	КонецПопытки;
КонецПроцедуры

Режим открытия определяет, что будет с существующим файлом:

РежимОткрытияФайлаФайла нетФайл есть
СоздатьСоздаётсяПерезаписывается
СоздатьНовыйСоздаётсяОшибка
ОткрытьОшибкаОткрывается с начала
ОткрытьИлиСоздатьСоздаётсяОткрывается с начала
ОбрезатьОшибкаОчищается до нуля байтов
ДописатьСоздаётсяЗапись идёт в конец

Чтобы скопировать большой файл с преобразованием, читайте его кусками через ОткрытьПотокДляЧтения() или ФайловыеПотоки.ОткрытьДляЧтения(Имя) и пишите в выходной поток. Порции по 1–4 МБ — разумный компромисс между скоростью и памятью.

Копирование большого файла порциями

Пример ниже переносит файл любого размера, держа в памяти только 1 МБ. Метод Прочитать потока возвращает число реально прочитанных байтов: на последней порции оно меньше размера буфера, а в конце файла равно нулю.

Процедура СкопироватьПорциями(ИмяИсточника, ИмяПриёмника)
	РазмерПорции = 1048576; // 1 МБ
	Буфер = Новый ДвоичныйБуфер(РазмерПорции);
	Чтение = ФайловыеПотоки.ОткрытьДляЧтения(ИмяИсточника);
	Запись = Новый ФайловыйПоток(ИмяПриёмника, РежимОткрытияФайла.Создать);
	Попытка
		Прочитано = Чтение.Прочитать(Буфер, 0, РазмерПорции);
		Пока Прочитано > 0 Цикл
			Запись.Записать(Буфер, 0, Прочитано);
			Прочитано = Чтение.Прочитать(Буфер, 0, РазмерПорции);
		КонецЦикла;
		Чтение.Закрыть();
		Запись.Закрыть();
	Исключение
		Чтение.Закрыть();
		Запись.Закрыть();
		ВызватьИсключение;
	КонецПопытки;
КонецПроцедуры

Между чтением и записью можно вставить свою обработку буфера: шифрование, замену байтов, подсчёт хеша по порциям через ХешированиеДанных.Добавить.

Всегда закрывайте файловый поток, в том числе при ошибке. Незакрытый поток держит файл открытым, пока рабочий процесс не освободит объект. Другой пользователь или следующий запуск обработки получит отказ в доступе к файлу.

Куда сохранить результат: диск, хранилище или пользователь

Записать байты — половина дела. Дальше решите, где они должны оказаться.

Временное хранилище

Чтобы передать данные между клиентом и сервером, используйте временное хранилище:

Адрес = ПоместитьВоВременноеХранилище(ДД, УникальныйИдентификатор);

УникальныйИдентификатор формы привязывает данные к её жизни: форму закрыли — хранилище очистилось. Без него данные живут до конца сеанса, а при вызове из длительной операции адрес нужно передать явно.

Реквизит базы

Для постоянного хранения скана, подписанного файла или вложения записывайте данные в реквизит типа ХранилищеЗначения:

Объект.Файл = Новый ХранилищеЗначения(ДД, Новый СжатиеДанных(9));

Сжатие уровня 9 заметно экономит место на текстовых форматах (XML, CSV) и почти ничего не даёт на PDF, JPEG и ZIP — они уже сжаты.

Файл на компьютере пользователя

Из серверного кода нельзя записать файл на диск пользователя: сервер не видит его компьютер. Правильная схема — сервер кладёт данные во временное хранилище, клиент забирает:

&НаКлиенте
Процедура Скачать(Команда)
	Адрес = ПодготовитьФайлНаСервере();
	НачатьПолучениеФайлаССервера(Адрес, "Выгрузка.bin");
КонецПроцедуры

В новых версиях платформы есть асинхронный вариант ПолучитьФайлССервераАсинх. В конфигурациях на БСП удобнее вызвать ФайловаяСистемаКлиент.СохранитьФайл: он сам учитывает тип клиента и наличие расширения для работы с файлами.

Проверка записи и обработка ошибок

Файл записался без исключения — ещё не значит, что он правильный. Проверяйте результат, особенно если файл уходит во внешнюю систему.

🔍 Что стоит проверять после записи:

  • 📏 размер: Новый Файл(ИмяФайла).Размер() должен совпасть с ДД.Размер();
  • 🧮 хеш: сравните контрольную сумму с ожидаемой;
  • 🔖 сигнатуру: первые байты формата (25 50 44 46 у PDF, 50 4B 03 04 у ZIP);
  • 🗂️ существование файла: Новый Файл(ИмяФайла).Существует().

Хеш считается объектом ХешированиеДанных:

Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);
Хеш.Добавить(ДД);
Контроль = ПолучитьHexСтрокуИзДвоичныхДанных(Хеш.ХешСумма);

Запись оборачивайте в Попытка ... Исключение и пишите причину в журнал регистрации — пользователь увидит короткое сообщение, а администратор найдёт подробности:

Попытка
	ДД.Записать(ИмяФайла);
Исключение
	Инфо = ИнформацияОбОшибке();
	ЗаписьЖурналаРегистрации("Выгрузка файла", УровеньЖурналаРегистрации.Ошибка,,,
		ПодробноеПредставлениеОшибки(Инфо));
	ВызватьИсключение "Не удалось записать файл " + ИмяФайла + ": " + КраткоеПредставлениеОшибки(Инфо);
КонецПопытки;
Диагностика: выберите симптом

Разные режимы запуска: где сработает код

Один и тот же код записи ведёт себя по-разному в зависимости от того, где он выполняется.

Где выполняетсяКуда пишется файлОграничения
Сервер, клиент-серверная базаДиск сервера 1СПрава учётной записи службы сервера
Сервер, файловая базаКомпьютер, где запущен клиентПрава пользователя Windows
Тонкий клиент, &НаКлиентеКомпьютер пользователяПрава пользователя Windows
Веб-клиентПрямой записи на диск нетТолько передача файла браузеру
1С:Фреш и другие облакаКаталог сервиса, доступ ограниченОбычно запрещено профилем безопасности
Фоновое заданиеДиск сервера 1СНет клиента и диалогов

Несколько практических выводов:

  • 🖥️ в файловой базе «серверный» код выполняется в процессе клиента, поэтому путь C:\Temp — это диск пользователя. При переносе базы на сервер тот же код начнёт писать на сервер;
  • 🌐 в веб-клиенте метод Записать на клиенте недоступен. Отдавайте файл через временное хранилище и НачатьПолучениеФайлаССервера — браузер сохранит его в папку загрузок;
  • ⏱️ в фоновом задании нельзя показать диалог выбора файла. Путь задаётся заранее, а результат удобно класть во временное хранилище, адрес которого передан в задание;
  • 🔒 если в конфигурации отключено использование модальности, синхронные диалоги выбора файла не работают — используйте асинхронные методы.

Для временных файлов на сервере используйте ПолучитьИмяВременногоФайла("bin") и удаляйте их после работы через УдалитьФайлы(ИмяФайла). Иначе каталог временных файлов сервера постепенно заполняется, а при нескольких рабочих серверах файл может оказаться не на той машине, где его ищет следующий вызов.

Частые вопросы

Можно ли изменить один байт в ДвоичныеДанные?
Нет, значение неизменяемое. Получите буфер через ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных, измените байт методом Установить и соберите новые двоичные данные.
Какой максимальный размер двоичных данных в 1С?
Жёсткий предел упирается в доступную память процесса. Файлы в сотни мегабайт обрабатывайте потоками порциями, а не целиком в памяти.
Чем Base64 отличается от двоичных данных?
Base64 — текстовое представление тех же байтов, оно примерно на треть длиннее. Используйте его для передачи в XML и JSON, а в файл пишите исходные двоичные данные.
Почему методы потоков не находятся в старой конфигурации?
Потоки, ДвоичныйБуфер и ЗаписьДанных появились в платформе 8.3.9. Если конфигурация работает в режиме совместимости с более ранней версией, эти объекты недоступны.
Как записать двоичные данные в файл на клиенте без вызова сервера?
В тонком клиенте вызовите ДД.Записать(ИмяФайла) прямо в клиентской процедуре. В веб-клиенте так не получится — нужна передача файла браузеру.