Как в 1С прочитать и преобразовать двоичные данные (Integer, TDateTime, String) из файла?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с внешними системами или устаревшими форматами данных часто возникает необходимость выполнять импорт данных, читать двоичные файлы, структура которых известна, и преобразовывать их содержимое в стандартные типы данных 1С. Эта задача требует понимания внутреннего представления данных, таких как целые числа, даты и строки, а также умения работать с низкоуровневыми объектами платформы 1С или использовать библиотеки для работы с двоичными данными. В этой статье мы подробно разберем, как получить массив байтов из бинарного файла (что критично для таких задач, как визуализация технологического процесса) и затем преобразовать его в осмысленные значения, такие как целые числа, даты (TDateTime) и строки, используя примеры из реальной практики.

Мы будем рассматривать ситуацию, когда у нас есть бинарный файл известной структуры (что актуально и для работы с PDF файлами в 1С), например, LastResults x 1.krd, и нам требуется извлечь из него значения полей различных типов.

Начало работы: чтение двоичного файла

Прежде чем приступить к преобразованию, нам необходимо получить содержимое бинарного файла в виде массива байтов. В 1С для этого существуют различные подходы. Рассмотрим два наиболее распространенных.

  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. Использование объектов ФайловыйПоток и БуферДвоичныхДанных: Этот подход является более гибким и производительным, так как позволяет читать файл порциями (блоками), не загружая его целиком в оперативную память. Это особенно важно при работе с очень большими файлами.

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

Преобразование типов данных

После того как мы получили доступ к двоичным данным (будь то весь файл в массиве байтов или текущий блок в БуферДвоичныхДанных), нам необходимо преобразовать последовательности байтов в осмысленные значения.

  1. Чтение целочисленных значений (Integer)

    Чтение целых чисел является одной из самых простых задач. В зависимости от размера числа (2, 4 или 8 байт), мы используем соответствующие методы объекта БуферДвоичныхДанных или ЧтениеДанных. Например, для 4-байтового целого числа (как в случае поля DBNr), мы можем использовать метод ПрочитатьЦелое32.

    Предположим, что поле DBNr занимает 4 байта и находится со смещением 4 байта от начала блока. Мы можем прочитать его следующим образом:

    
    // Предполагаем, что ОсновнойБуфер содержит текущий блок данных
    БуферЧисло = ОсновнойБуфер.Прочитать(4,4); // Читаем 4 байта, начиная со смещения 4
    СтрТЧ.DBNr = БуферЧисло.ПрочитатьЦелое32(0); // Преобразуем прочитанные 4 байта в 32-битное целое число
    

    Здесь БуферЧисло.ПрочитатьЦелое32(0) читает 32-битное целое число, начиная с нулевого смещения внутри БуферЧисло, который сам является частью ОсновнойБуфер.

  2. Чтение значений типа TDateTime (Double)

    Это наиболее сложный, но часто встречающийся тип данных при интеграции с системами, разработанными, например, на Delphi или Pascal. Тип TDateTime представляет собой 64-битное число с плавающей точкой (аналогичное типу Double по стандарту IEEE 754). Целая часть этого числа обозначает количество дней, прошедших с 30 декабря 1899 года, а дробная часть — долю суток.

    Стандарт IEEE 754 для чисел двойной точности (64-бит) устроен следующим образом:

    • 1 бит отводится под знак числа (0 для положительного, 1 для отрицательного).
    • 11 бит — под показатель степени (экспоненту), смещенную на 1023.
    • 52 бита — под основание степени (мантиссу), представляющую дробную часть числа.

    Для нашего примера (байты 228 29 28 99 202 210 229 64 для rDateTime), последовательность байтов интерпретируется как Double. Мы можем преобразовать это значение, проанализировав его битовое представление.

    Рассмотрим пошаговое преобразование TDateTime:

    1. Получение 8 байт: Извлекаем 8 байт, которые представляют число типа Double.
    2. Переворот байтов (Endianness): В зависимости от порядка байтов (Little-endian или Big-endian) в исходном файле и ожидаемого в 1С, может потребоваться перевернуть последовательность байтов. Например, если исходные данные Little-endian, а мы читаем их в Big-endian или хотим анализировать в определенном порядке, то БуферДвоичныхДанных.Перевернуть() будет очень полезен.
    3. Преобразование в шестнадцатеричную строку: Для удобства анализа и извлечения отдельных частей (знак, экспонента, мантисса) можно преобразовать эти 8 байт в шестнадцатеричную строку.
    4. Извлечение знака, экспоненты и мантиссы: Используем битовые операции для выделения соответствующих частей из шестнадцатеричной строки.
    5. Расчет значения по формуле IEEE 754: Значение числа с плавающей точкой вычисляется по формуле: Значение = Знак * (1 + Мантисса / 2^52) * 2^(Экспонента - 1023).
    6. Преобразование полученного числа в дату: После получения численного значения 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Строка));
    

    Здесь БуферДата.Перевернуть() используется для корректировки порядка байтов, если это необходимо.

  3. Чтение строковых значений (String)

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

    • С префиксом длины (Length-prefixed): Перед самой строкой записывается число, указывающее ее длину. Это число может быть одним байтом (для коротких строк до 255 символов), двумя байтами и т.д.
    • Null-terminated (оканчивающиеся нулевым байтом): Строка читается до тех пор, пока не встретится нулевой байт.
    • Фиксированной длины (Fixed-length): Строка всегда занимает определенное количество байтов, даже если она короче (остаток заполняется пробелами или нулями).

    Кроме того, при чтении строк критически важно учитывать их кодировку (например, 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. Порядок байтов (Endianness): Это порядок, в котором байты многобайтовых данных (целых чисел, чисел с плавающей точкой) хранятся в памяти или файле.
    • Little-endian (младший байт в начале): Наименее значимый байт хранится по младшему адресу.
    • Big-endian (старший байт в начале): Наиболее значимый байт хранится по младшему адресу.

    Несоответствие порядка байтов при чтении может привести к абсолютно неверной интерпретации данных. В 1С БуферДвоичныхДанных позволяет явно перевернуть байты с помощью метода Перевернуть(), что помогает адаптироваться к разным порядкам байтов.

  2. Точная структура файла: Прежде чем приступать к чтению, нам необходимо иметь максимально точное описание структуры бинарного файла:
    • Смещения каждого поля относительно начала записи или файла.
    • Типы данных (Integer, Double, String, Word и т.д.) и их точные размеры в байтах.
    • Порядок байтов для многобайтовых полей.
    • Формат хранения строк (с префиксом длины, Null-terminated, фиксированной длины).
    Без этой информации чтение будет методом проб и ошибок.
  3. Кодировка строк: Всегда указывайте правильную кодировку при чтении символьных данных. Если исходный файл использовал кодировку ANSI, а вы попытаетесь прочитать его как UTF-8, то получите "кракозябры". Объект ЧтениеДанных позволяет явно указать КодировкаТекста.
  4. Обработка ошибок и производительность: При работе с файлами всегда следует предусматривать обработку возможных ошибок (файл не найден, поврежден и т.д.). Для больших файлов предпочтительнее использовать потоковый доступ (ФайловыйПоток, ЧтениеДанных), чтобы не загружать весь файл в оперативную память, что может вызвать проблемы с производительностью и нехваткой ресурсов.

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

← На главную