Байты на диск: ДвоичныеДанные, буфер и потоки во встроенном языке 1С 8.3
Содержание
Записать двоичные данные в файл в 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").
Склеить несколько фрагментов можно без потоков: СоединитьДвоичныеДанные(МассивФрагментов). Обратная операция — РазделитьДвоичныеДанные(ДД, РазмерЧасти), удобно для отправки больших файлов частями.
Правка отдельных байтов: ДвоичныйБуфер
Если нужно поменять конкретные байты — записать сигнатуру, номер версии формата, контрольную сумму в заголовок, — используйте ДвоичныйБуфер. В отличие от двоичных данных, буфер изменяемый, но его размер фиксирован при создании.
Основные приёмы:
- Создать пустой буфер:
Буфер = Новый ДвоичныйБуфер(16);— 16 байтов, заполненных нулями. - Установить байт:
Буфер.Установить(0, 255);— значение от 0 до 255. - Прочитать байт:
Буфер.Получить(0). - Записать целое число в несколько байтов:
Буфер.ЗаписатьЦелое32(4, 1024, ПорядокБайтов.LittleEndian);. - Превратить буфер в двоичные данные:
ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер). - И обратно:
ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ДД).
Пример: собираем файл с 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") и удаляйте их после работы через УдалитьФайлы(ИмяФайла). Иначе каталог временных файлов сервера постепенно заполняется, а при нескольких рабочих серверах файл может оказаться не на той машине, где его ищет следующий вызов.