Как найти, заменить байты в двоичных данных или потоке в 1С:Предприятие и вернуть измененный буфер обратно в поток в памяти?

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

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

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

Почему нельзя работать с двоичными данными как с текстом?

Прежде всего, крайне важно понимать, почему нельзя просто преобразовать двоичные данные в текстовую строку и использовать стандартные строковые функции для поиска и замены. Основная причина, как уже упоминалось в обсуждении, заключается в потенциальном наличии байта 00(hex) (нулевого байта) в двоичных данных. При попытке преобразования двоичных данных, содержащих 00(hex), в текстовую строку, этот байт может быть интерпретирован как символ-терминатор строки (как, например, в языке C), что приведет к обрезанию данных, их некорректной интерпретации или даже к ошибкам. Поэтому для надежной и точной работы с такими данными необходим побайтовый подход, который не зависит от текстовых кодировок и не страдает от проблемы нулевого байта.

Шаг 1: Чтение исходного потока в БуферДвоичныхДанных

Для того чтобы получить возможность модифицировать двоичные данные, их необходимо сначала прочитать из исходного потока в объект БуферДвоичныхДанных. Этот объект представляет собой массив байтов, который хранится в оперативной памяти и предоставляет методы для доступа и изменения отдельных байтов или их последовательностей. Именно БуферДвоичныхДанных является нашим основным инструментом для манипуляций с "сырыми" байтами.

Мы используем объект ЧтениеДанных для выполнения этой операции. Важно правильно указать параметры КодировкаТекста и ПорядокБайтов, если двоичные данные содержат структурированные значения (например, числа, даты), интерпретация которых зависит от этих параметров. Для "сырых" двоичных данных, где нас интересует только последовательность байтов, эти параметры могут быть менее критичны, но их указание все равно является хорошей практикой, чтобы явно обозначить намерение.

Посмотрим на пример, как мы можем прочитать данные из исходного потока:


// Предположим, что ПотокИсходный уже инициализирован.
// Это может быть любой объект, производный от Поток, например,
// ПотокИзФайла, ПотокВПамяти, или поток, полученный из HTTP-запроса.
ПотокИсходный = ...; 

// Создаем объект ЧтениеДанных для работы с потоком.
// Здесь мы указываем КодировкуТекста.ANSI и ПорядокБайтов.BigEndian
// как пример. Выбор этих параметров критически зависит от структуры
// и ожидаемой интерпретации ваших конкретных двоичных данных.
ЧтениеДанных = Новый ЧтениеДанных(ПотокИсходный, КодировкаТекста.ANSI, ПорядокБайтов.BigEndian);

// Прочитываем весь поток в БуферДвоичныхДанных.
// Весь контент ПотокИсходный будет скопирован в оперативную память.
Буфер = ЧтениеДанных.ПрочитатьВБуферДвоичныхДанных();

// Важно: всегда закрываем объект ЧтениеДанных после использования,
// чтобы освободить ресурсы, связанные с потоком.
ЧтениеДанных.Закрыть();

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

Шаг 2: Поиск и изменение байтов в БуфереДвоичныхДанных

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

Поиск подпоследовательности байтов

Для поиска определенной подпоследовательности байтов (которую мы можем условно называть "строкой байтов" для удобства понимания) в БуфереДвоичныхДанных, мы будем итерировать по буферу, извлекая его части и сравнивая их с искомой подпоследовательностью. Это классический алгоритм поиска подстроки, адаптированный для двоичных данных. Рассмотрим, как мы можем это реализовать:


// Создаем БуферДвоичныхДанных, содержащий искомую подпоследовательность байтов.
// Например, ищем последовательность из 3 байтов, соответствующих "hel".
ИскомыеБайты = Новый БуферДвоичныхДанных(3); 
ИскомыеБайты.УстановитьБайт(0, 104); // ASCII для 'h'
ИскомыеБайты.УстановитьБайт(1, 101); // ASCII для 'e'
ИскомыеБайты.УстановитьБайт(2, 108); // ASCII для 'l'

// Переменная для хранения позиции первого найденного вхождения.
// Если последовательность не найдена, останется -1.
ПозицияНайдена = -1;

// Итерируем по основному буферу.
// Мы не доходим до самого конца буфера на длину искомой подпоследовательности,
// так как для сравнения всегда требуется буфер размером ИскомыеБайты.Размер().
Для Индекс = 0 По Буфер.Размер() - ИскомыеБайты.Размер() Цикл
    // Получаем часть основного буфера, начиная с текущего индекса
    // и имеющую длину, равную длине искомых байтов.
    ЧастьБуфера = Буфер.ПолучитьЧасть(Индекс, ИскомыеБайты.Размер());

    // Сравниваем полученную часть с искомыми байтами.
    // Метод Сравнить() возвращает 0, если буферы идентичны.
    Если ЧастьБуфера.Сравнить(ИскомыеБайты) = 0 Тогда 
        ПозицияНайдена = Индекс; // Запоминаем позицию первого вхождения
        Прервать; // Нашли первое вхождение, дальнейший поиск не требуется, выходим из цикла.
    КонецЕсли;
КонецЦикла;

Если ПозицияНайдена <> -1 Тогда
    Сообщить("Искомая последовательность найдена на позиции: " + ПозицияНайдена);
Иначе
    Сообщить("Искомая последовательность не найдена в буфере.");
КонецЕсли;

Такой ручной поиск обеспечивает нам полный контроль над процессом и позволяет работать с любыми последовательностями байтов, включая те, что содержат 00(hex).

Модификация байтов

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

  1. Прямая запись по индексу: Если нам нужно изменить один или несколько байтов на конкретных позициях, и при этом размер буфера не должен меняться (т.е. замена происходит "на месте" с таким же количеством байтов), мы можем использовать метод УстановитьБайт().
    
    // Предположим, мы хотим изменить байт на позиции 5 на новое значение 0xFF (255 в десятичной системе).
    // Это может быть полезно для изменения флагов или отдельных параметров.
    Буфер.УстановитьБайт(5, 255);
    

    Этот метод наиболее прост, когда известно точное местоположение и количество заменяемых байтов совпадает с количеством заменяющих.

  2. Удаление и вставка: Этот подход является более гибким и применяется, когда размер заменяемой подпоследовательности отличается от размера новой подпоследовательности, или когда нам нужно вставить целый блок данных в определенную позицию, сдвинув остальные данные. Используем методы Удалить() и Вставить().
    
    // Предположим, мы ранее нашли последовательность "hello" (5 байтов) на позиции ПозицияНайдена
    // и теперь хотим заменить ее на "world" (также 5 байтов).
    // Или, например, заменить на "Привет" (12 байтов в UTF-8), что изменит размер.
            
    // Создаем БуферДвоичныхДанных с новыми байтами для вставки.
    // Пусть это будут байты, соответствующие "WORLD!" (6 байтов).
    НовыеБайтыДляВставки = Новый БуферДвоичныхДанных(6); 
    НовыеБайтыДляВставки.УстановитьБайт(0, 87); // W
    НовыеБайтыДляВставки.УстановитьБайт(1, 79); // O
    НовыеБайтыДляВставки.УстановитьБайт(2, 82); // R
    НовыеБайтыДляВставки.УстановитьБайт(3, 76); // L
    НовыеБайтыДляВставки.УстановитьБайт(4, 68); // D
    НовыеБайтыДляВставки.УстановитьБайт(5, 33); // !
    
    // Определяем размер удаляемой последовательности.
    // Например, если мы искали "hello", ее размер был 5.
    РазмерУдаляемойПоследовательности = 5; // Предположим, что мы удаляем 5 байтов.
    
    // Сначала удаляем старые байты из буфера.
    // Метод Удалить() принимает начальную позицию и количество удаляемых байтов.
    Буфер.Удалить(ПозицияНайдена, РазмерУдаляемойПоследовательности); 
    
    // Затем вставляем новые байты на ту же позицию.
    // Метод Вставить() принимает позицию для вставки и БуферДвоичныхДанных для вставки.
    Буфер.Вставить(ПозицияНайдена, НовыеБайтыДляВставки);
    
    Сообщить("Данные в буфере успешно заменены.");
    

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

Шаг 3: Возвращение измененного БуфераДвоичныхДанных в поток в памяти

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

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


// Пример записи БуфераДвоичныхДанных в файл на диске.
// Создаем путь к новому файлу.
НовыйФайл = "C:\Temp\МойИзмененныйФайл.bin";

// Открываем файловый поток для записи.
ПотокКонечныйФайл = ФайловыеПотоки.ОткрытьДляЗаписи(НовыйФайл); 

// Создаем объект ЗаписьДанных, связывая его с файловым потоком.
ЗаписьДанных = Новый ЗаписьДанных(ПотокКонечныйФайл);

// Записываем весь измененный БуферДвоичныхДанных в файл.
ЗаписьДанных.ЗаписатьБуферДвоичныхДанных(Буфер); // Предполагаем, что Буфер уже изменен

// Важно: закрываем ЗаписьДанных, чтобы убедиться, что все данные записаны
// и ресурсы освобождены.
ЗаписьДанных.Закрыть();

// Закрываем файловый поток.
ПотокКонечныйФайл.Закрыть();

Сообщить("Измененные данные успешно записаны в файл: " + НовыйФайл);

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


// Создаем новый ПотокВПамяти из нашего измененного БуфераДвоичныхДанных.
// Теперь ПотокКонечныйВПамяти содержит все модифицированные байты
// и готов к использованию как обычный поток.
ПотокКонечныйВПамяти = Новый ПотокВПамяти(Буфер); // Буфер - это наш измененный БуферДвоичныхДанных

// Теперь ПотокКонечныйВПамяти можно использовать для дальнейшей обработки
// другими объектами, которые ожидают на вход объект типа Поток.
// Например, для чтения PDF-документа из этого потока в памяти:
PDF = Новый ДокументPDF;    
PDF.Прочитать(ПотокКонечныйВПамяти);
Сообщить("PDF-документ успешно прочитан из потока в памяти после модификации.");

// После использования поток следует закрыть, чтобы освободить системные ресурсы.
// Это особенно важно для ПотокВПамяти, чтобы сборщик мусора мог очистить память.
ПотокКонечныйВПамяти.Закрыть();

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

Дополнительные рекомендации и важные аспекты

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

  1. Производительность и потребление памяти: При работе с очень большими объемами данных (например, файлами размером в несколько гигабайт), чтение всего потока в БуферДвоичныхДанных может привести к значительному потреблению оперативной памяти и даже к исчерпанию доступной памяти. В таких случаях, возможно, стоит рассмотреть более сложный потоковый подход, обрабатывая данные небольшими "чанками" (блоками) непосредственно из исходного потока и записывая их в целевой поток, модифицируя только необходимые чанки. Однако для большинства типовых задач с файлами разумного размера (до нескольких десятков или сотен мегабайт) подход с полным чтением в БуферДвоичныхДанных будет эффективным и достаточным.
  2. Порядок байтов (Endianness): Если вы работаете не с "сырыми" последовательностями байтов, а с бинарными представлениями структурированных данных, таких как числа, даты или другие типы данных, крайне важно правильно определить и использовать ПорядокБайтов (BigEndian или LittleEndian) при чтении и записи. Неправильный порядок байтов приведет к некорректной интерпретации этих данных, что проявится как неверные значения чисел или дат. Всегда уточняйте формат данных, с которыми вы работаете.
  3. Документация: Всегда рекомендуется обращаться к официальной документации 1С:Предприятие для объектов БуферДвоичныхДанных, Поток, ПотокВПамяти, ЧтениеДанных и ЗаписьДанных. Они содержат полный список методов и свойств, а также подробные описания их использования, что может быть полезно в различных, нестандартных сценариях.
  4. Кодировки текста: Если двоичные данные могут содержать текстовые фрагменты (например, метаданные в каком-либо формате), критически важно правильно выбрать КодировкаТекста при работе с ЧтениеДанных и ЗаписьДанных. Неверная кодировка приведет к "кракозябрам" и нечитаемым символам при попытке интерпретации текста.

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

← На главную