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

· 9 мин

Содержание

Чтобы прочитать файл как двоичные данные в 1С, достаточно одной строки: Данные = Новый ДвоичныеДанные("C:\Обмен\счет.pdf");. Платформа загрузит файл целиком в память, и дальше его можно записать в реквизит, отправить по HTTP, перевести в Base64 или посчитать хеш.

Этого хватает для файлов до десятков мегабайт. Для гигабайтных выгрузок, разбора заголовков и побайтовой работы в 1С:Предприятии 8.3 есть объекты БуферДвоичныхДанных, ЧтениеДанных и файловые потоки. Ниже — как и когда применять каждый, с рабочим кодом на встроенном языке.

Всё описанное относится к платформе, а не к конкретной конфигурации: код одинаково работает в БП, ЗУП, УТ или своей разработке. Объекты буферов и потоков появились в платформе 8.3.9, поэтому база должна работать в режиме совместимости не ниже этой версии.

Что хранит объект ДвоичныеДанные и чем он ограничен

ДвоичныеДанные — неизменяемый набор байтов. Создали объект — поменять в нём отдельный байт уже нельзя. Можно только прочитать, скопировать или собрать новый.

Что умеет объект сам по себе:

  • 📏 Размер() — размер в байтах;
  • 💾 Записать(ИмяФайла) — сохранить на диск;
  • 🔁 ОткрытьПотокДляЧтения() — получить поток и читать его по частям;
  • 🧳 передаваться между клиентом и сервером как параметр и храниться во временном хранилище.

Главное ограничение — память. Конструктор Новый ДвоичныеДанные(ИмяФайла) читает файл полностью. Файл на 2 ГБ займёт 2 ГБ в рабочем процессе сервера, а в 32-битном клиенте такой объём просто не поместится.

Сравнение инструментов для чтения:

ИнструментКогда братьОграничение
Новый ДвоичныеДанные(Имя)Файл целиком: вложение, скан, выгрузка до ~50 МБВесь файл в памяти
БуферДвоичныхДанныхНужно читать и менять отдельные байты, числаТоже в памяти, зато изменяемый
ЧтениеДанныхРазбор формата: заголовок, записи фиксированной длины, строкиТолько последовательное чтение вперёд
ФайловыйПотокБольшой файл, чтение кусками, переход по смещениюКод длиннее, поток нужно закрывать

Если файл текстовый и нужен как строка, двоичные данные не всегда нужны. Для построчного чтения CSV или лога удобнее ЧтениеТекста — оно сразу работает со строками и кодировкой.

Как прочитать файл в двоичные данные: базовый код

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

&НаСервереБезКонтекста
Функция ПрочитатьФайл(ИмяФайла)

    Файл = Новый Файл(ИмяФайла);
    Если Не Файл.Существует() Тогда
        ВызватьИсключение СтрШаблон("Файл не найден: %1", ИмяФайла);
    КонецЕсли;

    Данные = Новый ДвоичныеДанные(ИмяФайла);
    Сообщить(СтрШаблон("Прочитано байт: %1", Формат(Данные.Размер(), "ЧГ=0")));

    Возврат Данные;

КонецФункции

Порядок действий такой:

  1. Проверить, что файл есть: Новый Файл(Имя).Существует().
  2. Создать объект ДвоичныеДанные, передав полный путь.
  3. Использовать результат: сохранить, отправить, преобразовать.

Где лежит файл: на сервере или у пользователя

Самая частая путаница. Код в процедуре &НаСервере ищет файл на компьютере сервера 1С, а не у пользователя. Путь C:\Users\Иванов\Desktop\счет.pdf на сервере, скорее всего, не существует.

Если файл выбирает пользователь, его сначала передают на сервер:

&НаКлиенте
Асинх Процедура ЗагрузитьФайл(Команда)

    Параметры = Новый ПараметрыДиалогаПомещенияФайлов;
    Параметры.Фильтр = "PDF (*.pdf)|*.pdf";

    Описание = Ждать ПоместитьФайлНаСерверАсинх(, , , Параметры, УникальныйИдентификатор);
    Если Описание = Неопределено Или Описание.ПомещениеФайлаОтменено Тогда
        Возврат;
    КонецЕсли;

    ОбработатьНаСервере(Описание.Адрес);

КонецПроцедуры

&НаСервере
Процедура ОбработатьНаСервере(Адрес)
    Данные = ПолучитьИзВременногоХранилища(Адрес);
    // Данные — объект ДвоичныеДанные
КонецПроцедуры

Асинхронные методы с Асинх/Ждать есть в платформе 8.3.18 и новее. В более ранних релизах ту же задачу решает НачатьПомещениеФайлаНаСервер с описанием оповещения. Этот способ работает и в веб-клиенте, где прямого доступа к диску нет.

Чтение по пути на сервере выполняется от имени пользователя ОС, под которым запущена служба 1С (часто USR1CV8). У него должны быть права на сетевую папку, иначе будет ошибка доступа, хотя у вас в проводнике файл открывается.

Чтение файла по байтам: буфер и ЧтениеДанных

Когда нужно не просто «взять файл», а разобрать его содержимое — проверить формат, достать число из заголовка, прочитать записи фиксированной длины, — подключают буфер и объект чтения.

Проверка сигнатуры файла

Расширение врёт: пользователь может переименовать .docx в .pdf. Надёжнее проверить первые байты. Читаем только 8 байт, а не весь файл:

Функция ТипФайлаПоСигнатуре(ИмяФайла)

    Чтение = Новый ЧтениеДанных(ИмяФайла);
    Результат = Чтение.Прочитать(8);
    Чтение.Закрыть();

    Буфер = Результат.ПолучитьБуферДвоичныхДанных();
    Если Буфер.Размер < 4 Тогда
        Возврат "Неизвестно";
    КонецЕсли;

    Б0 = Буфер.Получить(0);
    Б1 = Буфер.Получить(1);
    Б2 = Буфер.Получить(2);
    Б3 = Буфер.Получить(3);

    Если Б0 = 37 И Б1 = 80 И Б2 = 68 И Б3 = 70 Тогда       // %PDF
        Возврат "PDF";
    ИначеЕсли Б0 = 137 И Б1 = 80 И Б2 = 78 И Б3 = 71 Тогда  // 0x89 PNG
        Возврат "PNG";
    ИначеЕсли Б0 = 255 И Б1 = 216 И Б2 = 255 Тогда          // FF D8 FF
        Возврат "JPEG";
    ИначеЕсли Б0 = 80 И Б1 = 75 И Б2 = 3 И Б3 = 4 Тогда     // PK — ZIP, DOCX, XLSX
        Возврат "ZIP";
    КонецЕсли;

    Возврат "Неизвестно";

КонецФункции

Сигнатуры, которые пригодятся чаще всего:

ФорматПервые байты (hex)Десятичные
PDF25 50 44 4637 80 68 70
PNG89 50 4E 47137 80 78 71
JPEGFF D8 FF255 216 255
ZIP, DOCX, XLSX50 4B 03 0480 75 3 4

Чтение чисел из заголовка

У PNG ширина и высота картинки лежат по смещениям 16 и 20 как 4-байтные целые со старшим байтом впереди. Метод буфера ПрочитатьЦелое32 берёт позицию и порядок байтов:

Чтение = Новый ЧтениеДанных(ИмяФайла);
Буфер = Чтение.Прочитать(24).ПолучитьБуферДвоичныхДанных();
Чтение.Закрыть();

Ширина = Буфер.ПрочитатьЦелое32(16, ПорядокБайтов.BigEndian);
Высота = Буфер.ПрочитатьЦелое32(20, ПорядокБайтов.BigEndian);
Сообщить(СтрШаблон("%1 x %2 пикселей", Формат(Ширина, "ЧГ=0"), Формат(Высота, "ЧГ=0")));

Порядок байтов — частая ловушка. Форматы Windows (BMP, WAV) пишут числа младшим байтом вперёд (ПорядокБайтов.LittleEndian), сетевые протоколы и PNG — старшим (ПорядокБайтов.BigEndian). Перепутаете — вместо 800 получите 537 067 520.

Последовательное чтение ЧтениеДанных

У объекта ЧтениеДанных есть методы, которые сдвигают позицию сами: ПрочитатьБайт(), ПрочитатьЦелое16(), ПрочитатьЦелое32(), ПрочитатьСимволы(Количество), ПрочитатьСтроку(), Пропустить(Количество). Это удобно для файлов из записей одинаковой длины: прочитали поле, перешли к следующему, без ручного подсчёта смещений.

Кодировку и порядок байтов задают в конструкторе: Новый ЧтениеДанных(ИмяФайла, КодировкаТекста.UTF8, ПорядокБайтов.LittleEndian).

Большие файлы: потоковое чтение по частям

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

Пример: считаем SHA-256 большой выгрузки, читая по 1 МБ.

Функция ХешБольшогоФайла(ИмяФайла)

    РазмерКуска = 1024 * 1024;
    Буфер = Новый БуферДвоичныхДанных(РазмерКуска);
    Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);

    Поток = ФайловыеПотоки.ОткрытьДляЧтения(ИмяФайла);
    Попытка
        Пока Истина Цикл
            Прочитано = Поток.Прочитать(Буфер, 0, РазмерКуска);
            Если Прочитано = 0 Тогда
                Прервать;
            КонецЕсли;
            Если Прочитано = РазмерКуска Тогда
                Кусок = Буфер;
            Иначе
                Кусок = Буфер.Прочитать(0, Прочитано);
            КонецЕсли;
            Хеш.Добавить(ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Кусок));
        КонецЦикла;
        Поток.Закрыть();
    Исключение
        Поток.Закрыть();
        ВызватьИсключение;
    КонецПопытки;

    Возврат НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеш.ХешСумма));

КонецФункции

Что в этом коде принципиально:

  • 🔒 поток закрывается и при успехе, и при ошибке — иначе файл останется заблокированным до завершения рабочего процесса;
  • ✂️ последний кусок обычно короче буфера, поэтому берём из буфера ровно Прочитано байт, а не весь буфер с хвостом от прошлой итерации;
  • 🧮 размер куска 1 МБ — разумный компромисс: меньше — много вызовов, больше — лишняя память.

Для одной задачи «хеш файла» есть короткий путь: Хеш.ДобавитьФайл(ИмяФайла) — платформа сама прочитает файл по частям. Ручной цикл нужен, когда с каждым куском делается что-то своё: отправка частями, поиск подстроки, копирование с прогрессом.

Переход по смещению

Поток умеет прыгать в нужную позицию без чтения всего, что до неё:

Поток = ФайловыеПотоки.ОткрытьДляЧтения(ИмяФайла);
Поток.Перейти(Поток.Размер() - 1024, ПозицияВПотоке.Начало);  // последние 1024 байта
Буфер = Новый БуферДвоичныхДанных(1024);
Прочитано = Поток.Прочитать(Буфер, 0, 1024);
Поток.Закрыть();

Так читают «хвост» лога или служебную запись в конце архива. Для файла меньше 1024 байт сначала проверьте Поток.Размер(), иначе смещение уйдёт в минус.

Преобразование двоичных данных в строку, Base64 и другие типы 1С

Прочитанные байты почти всегда нужно во что-то превратить. Готовые функции глобального контекста:

ЗадачаФункция
Байты → строка в кодировкеПолучитьСтрокуИзДвоичныхДанных(Данные, "UTF-8")
Строка → байтыПолучитьДвоичныеДанныеИзСтроки(Строка, "UTF-8")
Байты → Base64ПолучитьBase64СтрокуИзДвоичныхДанных(Данные)
Base64 → байтыПолучитьДвоичныеДанныеИзBase64Строки(Строка)
Байты → hexПолучитьHexСтрокуИзДвоичныхДанных(Данные)
Байты → буфер (для правки)ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(Данные)
Буфер → байтыПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер)

В старом коде встречаются Base64Строка() и Base64Значение(). Они работают, но в новых модулях лучше брать функции из таблицы: у них симметричные имена и есть варианты для буферов.

Кодировка при переводе в строку

Файл из старой учётной системы часто сохранён в Windows-1251. Если прочитать его как UTF-8, вместо текста получатся «кракозябры». Кодировку указывают явно: ПолучитьСтрокуИзДвоичныхДанных(Данные, "windows-1251"). Без параметра платформа считает текст UTF-8.

Хранение в базе

Двоичные данные нельзя положить в реквизит напрямую. Для этого есть тип ХранилищеЗначения, и при создании можно включить сжатие:

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

Обратно: Данные = Объект.ФайлСкана.Получить();. Сжатие 9 — максимальное. Для PDF и JPEG, которые уже сжаты, выигрыш мал, для XML и TXT — в разы.

Обработка ошибок при чтении файла

Любое чтение с диска оборачивают в Попытка … Исключение. Причины сбоя внешние: файл удалили, сеть отвалилась, антивирус держит блокировку.

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

Подробности идут в журнал регистрации для администратора, пользователю — короткий текст без стека.

Диагностика: выберите симптом

Практические примеры: файлы в обменах и интеграциях

Отправка файла в HTTP-запросе

Данные = Новый ДвоичныеДанные(ИмяФайла);
Запрос = Новый HTTPЗапрос("/api/upload");
Запрос.Заголовки.Вставить("Content-Type", "application/pdf");
Запрос.УстановитьТелоИзДвоичныхДанных(Данные);

Соединение = Новый HTTPСоединение("example.ru", , , , , 60, Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
Если Ответ.КодСостояния <> 200 Тогда
    ВызватьИсключение СтрШаблон("Сервер вернул код %1", Формат(Ответ.КодСостояния, "ЧГ=0"));
КонецЕсли;

Обратное направление — Ответ.ПолучитьТелоКакДвоичныеДанные(). Для больших скачиваний у метода Получить есть параметр с именем выходного файла: ответ пишется сразу на диск и в память не грузится.

Файл внутри JSON или XML

Многие API принимают файл полем JSON. Байты переводят в Base64 и вставляют строкой:

Тело = Новый Структура;
Тело.Вставить("fileName", "act.pdf");
Тело.Вставить("content", ПолучитьBase64СтрокуИзДвоичныхДанных(Данные));

Base64 увеличивает объём примерно на треть: файл на 3 МБ превратится в строку около 4 МБ. Учтите это в лимитах принимающей стороны.

Сохранение у пользователя

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

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

Дальше на клиенте — ПолучитьФайлССервераАсинх (8.3.18 и новее) или НачатьПолучениеФайлаССервера в более ранних релизах.

Присоединённые файлы в типовых конфигурациях

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

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

Сколько весит файл, который безопасно читать целиком?
Жёсткого предела нет, всё решает свободная память процесса. На практике до 50–100 МБ читают через ДвоичныеДанные, крупнее — потоком по частям.
Можно ли изменить один байт в ДвоичныеДанные?
Нет, объект неизменяемый. Получите буфер функцией ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных, поменяйте байт методом Установить и соберите новые двоичные данные из буфера.
Как склеить несколько файлов в один?
Функция СоединитьДвоичныеДанные принимает массив объектов ДвоичныеДанные и возвращает один общий. Для больших файлов надёжнее писать их по очереди в один ФайловыйПоток.
Работает ли чтение файла в веб-клиенте?
Прямого доступа к диску у веб-клиента нет. Файл выбирает пользователь, он передаётся на сервер через ПоместитьФайлНаСерверАсинх, а дальше вы получаете ДвоичныеДанные из временного хранилища.
Как сравнить два файла, не загружая их целиком?
Сначала сравните размеры, потом хеш-суммы, посчитанные через ХешированиеДанных.ДобавитьФайл. Совпали хеш SHA-256 и размер — файлы одинаковые.