При работе с внешними системами или устаревшими форматами данных часто возникает необходимость выполнять импорт данных, читать двоичные файлы, структура которых известна, и преобразовывать их содержимое в стандартные типы данных 1С. Эта задача требует понимания внутреннего представления данных, таких как целые числа, даты и строки, а также умения работать с низкоуровневыми объектами платформы 1С или использовать библиотеки для работы с двоичными данными. В этой статье мы подробно разберем, как получить массив байтов из бинарного файла (что критично для таких задач, как визуализация технологического процесса) и затем преобразовать его в осмысленные значения, такие как целые числа, даты (TDateTime) и строки, используя примеры из реальной практики.
Мы будем рассматривать ситуацию, когда у нас есть бинарный файл известной структуры (что актуально и для работы с PDF файлами в 1С), например, LastResults x 1.krd, и нам требуется извлечь из него значения полей различных типов.
Прежде чем приступить к преобразованию, нам необходимо получить содержимое бинарного файла в виде массива байтов. В 1С для этого существуют различные подходы. Рассмотрим два наиболее распространенных.
ADODB.Stream: Этот метод подходит для небольших файлов и предполагает загрузку всего содержимого файла в оперативную память. Он требует наличия установленной библиотеки ADO (ActiveX Data Objects).
Для работы с ADODB.Stream нам потребуется создать COM-объект и настроить его на работу с бинарными данными. Затем мы загружаем файл и считываем его содержимое.
ИмяФайла="\\SERVER\Work\Стажеры\Документы сотрудников\Новопашин\Павлик\RTables\LastResults x 1.krd";
// Перечисления из библиотеки ADO
StreamTypeEnum = Новый Структура("adTypeBinary, adTypeText", 1, 2);
ConnectModeEnum = Новый Структура(
"adModeRead, adModeReadWrite, adModeRecursive, adModeShareDenyNone, |adModeShareDenyRead, adModeShareDenyWrite, adModeShareExclusive, |adModeUnknown, adModeWrite",
1, 3, 4194304, 16, 4, 8, 12, 0, 2
);
SaveOptionsEnum = Новый Структура("adSaveCreateNotExist, adSaveCreateOverWrite", 1, 2);
StreamIn = Новый COMОбъект("ADODB.Stream");
// Для создания этого объекта требуется MDAC 2.5 и выше
StreamIn.Type = StreamTypeEnum.adTypeBinary;
StreamIn.Open; // По умолчанию открывается на чтение
StreamIn.LoadFromFile(ИмяФайла);
// Очевидно, что для больших файлов так поступать нехорошо.
Data = StreamIn.Read; // У переменной Data тип COMSafeArray, это массив байтов
StreamIn.Close; // Больше нам этот поток не нужен - закрываем.
// Конечно, манипуляция в памяти приемлима только для небольших файлов!
СодержимоеМассив = Data.Выгрузить;
Сообщить("Готово");
В результате выполнения этого кода, переменная СодержимоеМассив будет содержать массив байтов, представляющих содержимое файла. Однако, как отмечено в комментарии, этот подход неэффективен для больших файлов.
ФайловыйПоток и БуферДвоичныхДанных: Этот подход является более гибким и производительным, так как позволяет читать файл порциями (блоками), не загружая его целиком в оперативную память. Это особенно важно при работе с очень большими файлами.
Объект ФайловыйПоток предоставляет возможность последовательного доступа к файлу, а БуферДвоичныхДанных позволяет работать с прочитанным блоком данных, обращаясь к байтам по смещению. Мы будем использовать этот подход в дальнейшем для демонстрации обработки различных типов данных.
После того как мы получили доступ к двоичным данным (будь то весь файл в массиве байтов или текущий блок в БуферДвоичныхДанных), нам необходимо преобразовать последовательности байтов в осмысленные значения.
Чтение целых чисел является одной из самых простых задач. В зависимости от размера числа (2, 4 или 8 байт), мы используем соответствующие методы объекта БуферДвоичныхДанных или ЧтениеДанных. Например, для 4-байтового целого числа (как в случае поля DBNr), мы можем использовать метод ПрочитатьЦелое32.
Предположим, что поле DBNr занимает 4 байта и находится со смещением 4 байта от начала блока. Мы можем прочитать его следующим образом:
// Предполагаем, что ОсновнойБуфер содержит текущий блок данных
БуферЧисло = ОсновнойБуфер.Прочитать(4,4); // Читаем 4 байта, начиная со смещения 4
СтрТЧ.DBNr = БуферЧисло.ПрочитатьЦелое32(0); // Преобразуем прочитанные 4 байта в 32-битное целое число
Здесь БуферЧисло.ПрочитатьЦелое32(0) читает 32-битное целое число, начиная с нулевого смещения внутри БуферЧисло, который сам является частью ОсновнойБуфер.
Это наиболее сложный, но часто встречающийся тип данных при интеграции с системами, разработанными, например, на Delphi или Pascal. Тип TDateTime представляет собой 64-битное число с плавающей точкой (аналогичное типу Double по стандарту IEEE 754). Целая часть этого числа обозначает количество дней, прошедших с 30 декабря 1899 года, а дробная часть — долю суток.
Стандарт IEEE 754 для чисел двойной точности (64-бит) устроен следующим образом:
Для нашего примера (байты 228 29 28 99 202 210 229 64 для rDateTime), последовательность байтов интерпретируется как Double. Мы можем преобразовать это значение, проанализировав его битовое представление.
Рассмотрим пошаговое преобразование TDateTime:
Double.БуферДвоичныхДанных.Перевернуть() будет очень полезен.Значение = Знак * (1 + Мантисса / 2^52) * 2^(Экспонента - 1023).Double, мы используем его, чтобы рассчитать дату и время, прибавляя его к базовой дате 30.12.1899.
Рассмотрим функции, которые помогут нам в этом, взятые из предложенного решения. Функция ПолучитьДробноеЧислоИзHexСтроки выполняет преобразование шестнадцатеричной строки в число типа Double, а ПолучитьДатуИзДробногоЧисла конвертирует это число в дату 1С.
&НаКлиенте
Функция ПолучитьHexСтрокуИзБуфераДвоичныхДанных(ВхБуфер)
// Функция для преобразования БуфераДвоичныхДанных в шестнадцатеричную строку
// В 1С нет прямого метода, поэтому можем использовать промежуточное преобразование через ДвоичныеДанные.Base64
ДД = Новый ДвоичныеДанные(ВхБуфер);
Base64Строка = Base64Кодировать(ДД);
// Ручное преобразование Base64 в Hex (для 8 байт)
// Это упрощенный пример, который может быть не универсальным для всех Base64 строк
// Более надежно - побайтово конвертировать БуферДвоичныхДанных в Hex
Результат = "";
Для Сч = 0 По ВхБуфер.Размер() - 1 Цикл
Байт = ВхБуфер.ПрочитатьБайт(Сч);
HexБайт = Формат(Байт, "ЧП=0; ЧВТ=16"); // Преобразуем байт в 2-х значное HEX
Если СтрДлина(HexБайт) = 1 Тогда
HexБайт = "0" + HexБайт;
КонецЕсли;
Результат = Результат + HexБайт;
КонецЦикла;
Возврат Результат;
КонецФункции
&НаКлиенте
Функция ПолучитьДробноеЧислоИзHexСтроки(ВхСтрока)
// Пример для 64-битного числа Double (IEEE 754)
// ВхСтрока должна быть 16 символов (8 байт в HEX)
// Битовая структура Double:
// 1 бит - знак (Sign)
// 11 бит - экспонента (Exponent)
// 52 бита - мантисса (Fraction/Mantissa)
// Разделяем Hex строку на части, соответствующие битовым полям
// Например, для 8 байт (16 HEX символов): S(1) E(11) M(52)
// 1-й HEX символ (4 бита) содержит бит знака и 3 бита экспоненты
// Следующие 2 HEX символа (8 бит) содержат оставшиеся 8 бит экспоненты
// Следующие 13 HEX символов (52 бита) - мантисса
// Получаем значение всех 64 бит как одно большое целое число
// (поскольку ЧислоИзШестнадцатеричнойСтроки может работать с большими числами)
ВсеБитыЧисла = ЧислоИзШестнадцатеричнойСтроки("0x"+ВхСтрока);
// Извлекаем знак (1 бит, самый старший)
ЗнакЧисла = 1;
Если ПроверитьБит(ВсеБитыЧисла, 63) Тогда // 63-й бит (счет с 0)
ЗнакЧисла = -1;
КонецЕсли;
// Извлекаем экспоненту (11 бит, с 52 по 62)
// Сдвигаем вправо на 52, чтобы экспонента оказалась в младших битах
ЭкспБиты = Цел(ВсеБитыЧисла / Pow(2, 52));
// Применяем маску, чтобы получить только 11 бит экспоненты
Эксп = ЭкспБиты % Pow(2, 11);
// Вычитаем смещение
Эксп = Эксп - 1023;
// Извлекаем мантиссу (52 бита, с 0 по 51)
// Применяем маску, чтобы получить только 52 бита мантиссы
Мантисса = ВсеБитыЧисла % Pow(2, 52);
// Вычисляем дробное число по формуле IEEE 754:
// (-1)^Sign * (1 + Mantissa / 2^52) * 2^Exponent
ДробноеЧисло = ЗнакЧисла * Pow(2, Эксп) * (1 + Мантисса / Pow(2, 52));
Возврат ДробноеЧисло;
КонецФункции
&НаКлиенте
Функция ПолучитьДатуИзДробногоЧисла(ВхЧисло)
// TDateTime: количество дней с 30 декабря 1899 года
// Дробная часть числа - доля суток
КоличествоДней = Цел(ВхЧисло);
ДробнаяЧасть = ВхЧисло - КоличествоДней;
// Базовая дата: 30.12.1899
БазоваяДата = Дата(1899,12,30);
// Добавляем дни и время
Результат = БазоваяДата + (КоличествоДней + ДробнаяЧасть) * 3600 * 24;
Возврат Результат;
КонецФункции
Функция ПолучитьHexСтрокуИзБуфераДвоичныхДанных конвертирует содержимое БуферДвоичныхДанных в шестнадцатеричную строку.
Функция ПолучитьДробноеЧислоИзHexСтроки принимает эту шестнадцатеричную строку, извлекает из нее биты знака, экспоненты и мантиссы, а затем рассчитывает итоговое значение Double.
Функция ПолучитьДатуИзДробногоЧисла берет полученное Double-значение, разделяет его на целую и дробную части, и, используя базовую дату "30.12.1899", преобразует его в объект "Дата" 1С.
Пример использования в основном коде:
// получаем rDateTime
БуферДата = ОсновнойБуфер.Прочитать(8,8); // Читаем 8 байт для Double, начиная со смещения 8
rDateTimeСтрока = ПолучитьHexСтрокуИзБуфераДвоичныхДанных(БуферДата.Перевернуть); // Преобразуем в HEX, возможно с переворотом байтов
СтрТЧ.rDateTime = ПолучитьДатуИзДробногоЧисла(ПолучитьДробноеЧислоИзHexСтроки(rDateTimeСтрока));
Здесь БуферДата.Перевернуть() используется для корректировки порядка байтов, если это необходимо.
Чтение строк из двоичных данных может быть несколько сложнее, поскольку не существует единого универсального способа их хранения. Строки могут быть:
Кроме того, при чтении строк критически важно учитывать их кодировку (например, ANSI, UTF-8, UTF-16), чтобы символы были интерпретированы правильно.
Рассмотрим случай из примера, когда строка (поле LabNr) хранится с префиксом длины: первый байт указывает длину строки, а затем следуют байты самой строки.
&НаКлиенте
Функция ПолучитьLabNrИзДвоичныхДанных(ВхДД)
// ВхДД - это БуферДвоичныхДанных, содержащий информацию о строке (длина + сама строка)
ЧтениеДанных = Новый ЧтениеДанных(ВхДД); // Создаем ЧтениеДанных для удобного чтения из буфера
// Первый байт содержит длину строки
ДлинаНомера = ЧтениеДанных.ПрочитатьБайт;
// Читаем указанное количество символов в кодировке ANSI
// Важно указать правильную кодировку, иначе символы могут быть прочитаны неверно
Номер = ЧтениеДанных.ПрочитатьСимволы(ДлинаНомера, КодировкаТекста.ANSI);
ЧтениеДанных.Закрыть; // Закрываем поток чтения
Возврат Номер;
КонецФункции
В этой функции ЧтениеДанных.ПрочитатьБайт считывает первый байт, который интерпретируется как длина строки. Затем ЧтениеДанных.ПрочитатьСимволы(ДлинаНомера, КодировкаТекста.ANSI) читает указанное количество символов, используя кодировку ANSI. Если бы файл использовал другую кодировку (например, UTF-8), мы бы указали КодировкаТекста.UTF8.
Пример использования в основном коде:
// Получаем LabNr
БуферСтрока = ОсновнойБуфер.Прочитать(16,16); // Читаем блок из 16 байт, начиная со смещения 16, где предположительно находится LabNr
LabNrДД = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(БуферСтрока); // Создаем ДвоичныеДанные из буфера
СтрТЧ.LabNr = ПолучитьLabNrИзДвоичныхДанных(LabNrДД);
Здесь ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных (это внутренняя функция, которая создает ДвоичныеДанные из части буфера для передачи в ЧтениеДанных) используется для подготовки данных перед передачей их в функцию ПолучитьLabNrИзДвоичныхДанных.
Для поля Massa, которое в примере описания формата указано как Word (2 байта), мы можем использовать следующий подход:
// Получаем массу
БуферМасса = ОсновнойБуфер.Прочитать(198,2); // Читаем 2 байта со смещением 198
МассаСтрока = ПолучитьHexСтрокуИзБуфераДвоичныхДанных(БуферМасса.Перевернуть); // Преобразуем в HEX, возможно с переворотом байтов
СтрТЧ.Massa = ЧислоИзШестнадцатеричнойСтроки("0x"+МассаСтрока); // Конвертируем HEX в число
Здесь Word интерпретируется как 2-байтовое беззнаковое целое число.
Теперь, когда мы понимаем, как работать с отдельными типами данных, мы можем собрать все это в единое решение, которое читает файл поблочно. Этот подход является наиболее оптимальным для обработки файлов произвольного размера, так как позволяет эффективно управлять использованием памяти.
Мы будем использовать объекты ФайловыйПоток и БуферДвоичныхДанных для чтения файла блоками по 256 байт (это размер блока, указанный в примере, соответствующий структуре данных файла). В каждом блоке мы будем извлекать необходимые поля: DBNr (Integer 4 байта), rDateTime (TDateTime 8 байт), LabNr (строка с префиксом длины) и Massa (Word 2 байта).
&НаКлиенте
Процедура ОткрытьФайл(Команда)
Диалог = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
Диалог.Заголовок = "Выберите файл";
Диалог.Фильтр = "Файл (*.krd)|*.krd|";
Если Диалог.Выбрать Тогда
ПолноеИмяФайла = Диалог.ПолноеИмяФайла;
ПолучитьДанныеИзФайла(ПолноеИмяФайла);
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ПолучитьДанныеИзФайла(ИмяФайла)
// Объект.ЗагруженныеДанные - это табличная часть, куда будут записываться прочитанные значения
Объект.ЗагруженныеДанные.Очистить();
// Создаем ФайловыйПоток для чтения файла
// Размер буфера 256 байт соответствует размеру записи в файле
ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла,РежимОткрытияФайла.Открыть,ДоступКФайлу.Чтение, 256);
// Создаем БуферДвоичныхДанных для чтения каждого блока
ОсновнойБуфер = Новый БуферДвоичныхДанных(256);
// Читаем файл блоками, пока Прочитать() возвращает количество прочитанных байтов > 0
Пока ФайловыйПоток.Прочитать(ОсновнойБуфер,0,256) > 0 Цикл
СтрТЧ = Объект.ЗагруженныеДанные.Добавить(); // Добавляем новую строку в табличную часть
// Получаем DBNr (4 байта, Integer)
// Смещение 4 означает, что мы пропускаем первые 4 байта блока,
// а затем читаем 4 байта, которые представляют DBNr.
БуферЧисло = ОсновнойБуфер.Прочитать(4,4);
СтрТЧ.DBNr = БуферЧисло.ПрочитатьЦелое32(0); // Преобразуем 4 байта в 32-битное целое число
// Получаем rDateTime (8 байт, TDateTime / Double)
// Смещение 8 означает, что мы пропускаем следующие 8 байт (после DBNr, если бы мы отсчитывали от 0),
// а затем читаем 8 байт, которые представляют rDateTime.
// Здесь важно учитывать, что смещения отсчитываются от начала текущего блока (ОсновнойБуфер).
БуферДата = ОсновнойБуфер.Прочитать(8,8);
// Переворачиваем байты, так как формат Double может быть Little-endian
rDateTimeСтрока = ПолучитьHexСтрокуИзБуфераДвоичныхДанных(БуферДата.Перевернуть);
СтрТЧ.rDateTime = ПолучитьДатуИзДробногоЧисла(ПолучитьДробноеЧислоИзHexСтроки(rDateTimeСтрока));
// Получаем LabNr (строка, 1 байт длины + N байт данных)
// Смещение 16 означает, что мы начинаем чтение LabNr со смещения 16.
// Предположим, что 16 байт - это максимальный размер для LabNr
БуферСтрока = ОсновнойБуфер.Прочитать(16,16);
// Преобразуем часть буфера в ДвоичныеДанные для передачи в функцию
// (возможно, здесь стоит улучшить, передавая напрямую БуферСтрока в новую функцию
// с использованием ЧтениеДанных(БуферДвоичныхДанных))
LabNrДД = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(БуферСтрока);
СтрТЧ.LabNr = ПолучитьLabNrИзДвоичныхДанных(LabNrДД);
// Получаем массу (2 байта, Word)
// Смещение 198 означает, что мы читаем 2 байта со смещения 198.
БуферМасса = ОсновнойБуфер.Прочитать(198,2);
// Переворачиваем байты, так как Word может быть Little-endian
МассаСтрока = ПолучитьHexСтрокуИзБуфераДвоичныхДанных(БуферМасса.Перевернуть);
СтрТЧ.Massa = ЧислоИзШестнадцатеричнойСтроки("0x"+МассаСтрока); // Преобразуем HEX в число
КонецЦикла;
ФайловыйПоток.Закрыть(); // Не забываем закрыть файловый поток
КонецПроцедуры
// Вспомогательная функция, которая создает ДвоичныеДанные из БуферДвоичныхДанных
// (может быть встроена в более универсальную функцию чтения строк,
// если ЧтениеДанных позволяет напрямую работать с БуферДвоичныхДанных)
&НаКлиенте
Функция ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(ВхБуфер)
Возврат Новый ДвоичныеДанные(ВхБуфер);
КонецФункции
// Остальные вспомогательные функции (ПолучитьHexСтрокуИзБуфераДвоичныхДанных,
// ПолучитьДробноеЧислоИзHexСтроки, ПолучитьДатуИзДробногоЧисла, ПолучитьLabNrИзДвоичныхДанных)
// должны быть определены, как показано выше.
Этот код демонстрирует полный цикл работы: от выбора файла до поблочного чтения и преобразования данных. Каждая запись файла (блок размером 256 байт) обрабатывается в цикле, и из нее извлекаются необходимые поля.
Чтобы успешно работать с двоичными данными и избежать распространенных ошибок, мы должны учитывать несколько ключевых моментов:
Несоответствие порядка байтов при чтении может привести к абсолютно неверной интерпретации данных. В 1С БуферДвоичныхДанных позволяет явно перевернуть байты с помощью метода Перевернуть(), что помогает адаптироваться к разным порядкам байтов.
ЧтениеДанных позволяет явно указать КодировкаТекста.
ФайловыйПоток, ЧтениеДанных), чтобы не загружать весь файл в оперативную память, что может вызвать проблемы с производительностью и нехваткой ресурсов.
Мы рассмотрели основные подходы и методы для чтения и преобразования двоичных данных в 1С. Используя объекты ФайловыйПоток, БуферДвоичныхДанных и ЧтениеДанных, а также понимая форматы данных, мы можем эффективно работать с бинарными файлами, извлекая из них необходимую информацию и приводя ее к стандартным типам 1С. Эта методика позволит вам интегрироваться с различными внешними системами и обрабатывать данные, хранящиеся в нестандартных форматах.