При работе с очень большими файлами (например, в несколько гигабайт) часто возникает необходимость удалить часть данных из их начала или выполнить трансформацию данных (например, декодирование Base64) прямо «на месте», без создания нового файла или полной загрузки содержимого в оперативную память. Такая задача представляет собой вызов, поскольку встроенные средства 1С, как и большинство высокоуровневых языков программирования, не предоставляют прямой функции для мгновенного «обрезания» начала файла. Давайте подробно рассмотрим, как решить эту проблему, используя встроенный язык 1С.
Прежде чем перейти к решению, нам важно понять, почему «отрезать» начало файла не так просто, как «отрезать» его конец. Файловые системы (такие как NTFS или EXT4) хранят данные не побайтово, а блоками или кластерами. Начало файла обычно привязано к началу одного из таких блоков. Если мы хотим удалить первые несколько байт, это не означает простое смещение указателя на начало файла в таблице метаданных. Вместо этого, операционной системе пришлось бы физически перемещать все оставшиеся данные файла к его началу, чтобы заполнить освободившееся пространство. Эта операция эквивалентна перезаписи всего файла, начиная с определенной точки.
Именно поэтому стандартные средства 1С и других языков не предлагают метода типа УдалитьСНачала(КоличествоБайт). Для удаления данных из середины или начала файла единственный практический программный подход заключается в следующем:
Этот процесс требует и чтения, и записи в один и тот же файл, что накладывает определенные ограничения на производительность, но позволяет избежать создания дублирующего файла.
Рассмотрим по шагам, как реализовать вышеописанный подход с помощью встроенного языка 1С.
Для работы с файлом нам потребуется объект ФайловыйПоток. Мы откроем его в режиме РежимОткрытияФайла.Открыть с доступом на ДоступКФайлу.ЧтениеИЗапись. Нам понадобятся две позиции: одна для чтения данных из исходной части файла, вторая — для записи этих данных в новую, смещенную позицию.
Процедура СрезДвоичныхДанных(ФайлИсточник, УдалитьНач = 0, УдалитьКон = 0, РазмерБуфера = 512)
// Создаем файловый поток для работы с файлом
Поток = Новый ФайловыйПоток(ФайлИсточник, РежимОткрытияФайла.Открыть, ДоступКФайлу.ЧтениеИЗапись);
// Получаем текущий и рассчитываем целевой размер файла
ИсходныйРазмер = Поток.Размер;
ЦелевойРазмер = ИсходныйРазмер - УдалитьНач - УдалитьКон;
// Если требуется удалить байты с начала файла
Если УдалитьНач > 0 Тогда
// Позиция, с которой будем читать данные (пропускаем УдалитьНач байт)
ПозицияЧтения = УдалитьНач;
// Позиция, с которой будем записывать данные (в начало файла)
ПозицияЗаписи = 0;
// Создаем буфер для временного хранения порции данных
// Размер буфера может быть значительно больше, чем 512 байт, для повышения производительности
Буфер = Новый БуферДвоичныхДанных(РазмерБуфера);
// Цикл перезаписи данных
Пока Истина Цикл
// Переходим к позиции чтения и считываем блок данных
Поток.Перейти(ПозицияЧтения, ПозицияВПотоке.Начало);
Прочитано = Поток.Прочитать(Буфер, 0, РазмерБуфера);
// Если ничего не прочитано (достигнут конец файла или целевой размер), выходим из цикла
Если Прочитано = 0 Или ПозицияЗаписи >= ЦелевойРазмер Тогда
Прервать;
КонецЕсли;
// Переходим к позиции записи и записываем прочитанный блок
Поток.Перейти(ПозицияЗаписи, ПозицияВПотоке.Начало);
Поток.Записать(Буфер, 0, Прочитано); // Важно записывать Прочитано, а не РазмерБуфера, на последней итерации
// Обновляем позиции для следующей итерации
ПозицияЧтения = ПозицияЧтения + Прочитано;
ПозицияЗаписи = ПозицияЗаписи + Прочитано;
КонецЦикла;
КонецЕсли;
// После перемещения данных, устанавливаем новый, окончательный размер файла
Поток.УстановитьРазмер(ЦелевойРазмер);
// Закрываем поток
Поток.Закрыть();
КонецПроцедуры
В этом коде мы используем метод Поток.Перейти(), чтобы управлять указателями чтения и записи внутри одного файлового потока. Важно, что мы читаем и записываем данные блоками через БуферДвоичныхДанных, что позволяет не загружать весь файл в память.
Как мы уже выяснили, скорость выполнения операции по сдвигу данных в файле критически зависит от производительности дисковой подсистемы. Однако на уровне программного кода мы можем оптимизировать процесс, правильно выбрав РазмерБуфера. Рассмотрим, как это влияет на скорость:
Вывод: Увеличение размера буфера значительно сокращает время выполнения операции. Это происходит потому, что уменьшается количество итераций цикла чтения-записи, а каждая операция чтения/записи в файловую систему имеет свои накладные расходы. Чем меньше таких операций, тем быстрее работает программа. Однако эта зависимость не линейна. После определенного значения (например, 64 Кб или 16 Мб) дальнейшее увеличение буфера уже не дает такого драматического прироста скорости. Это связано с тем, что скорость выполнения кода 1С упирается в физическую производительность дисковых операций (чтение/запись), которые зависят от:
Таким образом, наша программа будет ждать, пока операционная система и железо выполнят команду записи большого блока данных, и дальнейшая оптимизация кода 1С уже не принесет заметного ускорения. Оптимальный размер буфера (например, 64 Мб) следует подбирать экспериментально для конкретной системы.
В отличие от удаления байтов в начале, операция по усечению файла с конца (например, методом Поток.УстановитьРазмер()) выполняется очень быстро. Это объясняется тем, что файловая система не перемещает физические данные по диску. Вместо этого она просто обновляет метаданные файла (например, в MFT для NTFS), указывая его новый, меньший размер. Кластеры, которые ранее принадлежали файлу, но теперь находятся за его новой границей, помечаются как свободные. Это значительно более легкая операция, чем физический сдвиг данных.
Представим, что у нас есть большой файл (например, 10 Гб), содержащий данные, закодированные в Base64. Наша задача — раскодировать его, также на месте, в тот же файл, не загоняя целиком в память.
Поскольку двоичные данные после декодирования Base64 занимают примерно 75% от размера исходных Base64-данных (3 байта кодируются 4 символами), то раскодированный файл гарантированно поместится на месте исходного. Это позволяет нам использовать ту же логику «сдвига» данных, что и при простом удалении байтов, но с дополнительным шагом декодирования.
Начальная идея заключается в следующем:
// ... (начало процедуры СрезДвоичныхДанных, как описано выше)
Если УдалитьНач > 0 Тогда // Или если нужно декодировать Base64
ПозицияЧтения = УдалитьНач; // Если есть начальный сдвиг, иначе 0
ПозицияЗаписи = 0;
БуферЧтения = Новый БуферДвоичныхДанных(РазмерБуфера);
Пока Истина Цикл
Поток.Перейти(ПозицияЧтения, ПозицияВПотоке.Начало);
Прочитано = Поток.Прочитать(БуферЧтения, 0, РазмерБуфера);
Если Прочитано = 0 Тогда // Достигнут конец файла
Прервать;
КонецЕсли;
// *Здесь происходит декодирование Base64*
// Это простой, но проблемный подход, как выясним ниже
Поток.Перейти(ПозицияЗаписи, ПозицияВПотоке.Начало);
// БуферЗаписи = Base64Значение(БуферЧтения); // Примерно так
// Поток.Записать(БуферЗаписи, 0, БуферЗаписи.Размер());
ПозицияЧтения = ПозицияЧтения + Прочитано;
// ПозицияЗаписи = ПозицияЗаписи + БуферЗаписи.Размер(); // Смещаем на размер декодированных данных
КонецЦикла;
КонецЕсли;
// ... (обрезание файла)
Однако при первой попытке реализовать декодирование Base64-данных на месте мы можем столкнуться с проблемой: результирующий файл не будет идентичен эталону, а данные «расползутся». Причина кроется в форматировании Base64-данных.
Часто Base64-данные в файлах (особенно полученные из веб-сервисов) порезаны на строки фиксированной длины (например, по 64 символа) и содержат символы переноса строк (Символы.ПС, т.е. CRLF или LF). Декодирование Base64 требует, чтобы входные данные были последовательностью Base64-символов, длина которой кратна 4 (каждые 4 символа Base64 кодируют 3 байта двоичных данных). Включения переносов строк нарушают это выравнивание. Если буфер для чтения захватывает неполный блок Base64 или обрывает его посреди переноса строки, декодирование пойдет некорректно.
Для корректного декодирования Base64-данных в потоковом режиме, особенно с учетом возможных переносов строк, нам необходимо:
Читать данные блоками. Используем БуферДвоичныхДанных, как и ранее.
Преобразовать прочитанный буфер в строку. Поскольку операции с Base64 (и поиск переносов строк) удобнее выполнять со строковыми данными, нам потребуется преобразовать часть БуферДвоичныхДанных в строку. Важно помнить о потенциальных ограничениях на размер строковых переменных в 1С (хотя 64 Мб, скорее всего, будут работать, 64 Кб более безопасны для избежания лишнего расхода памяти).
СтрокаBase64 = КодировкиТекста.UTF8.ПолучитьСтроку(БуферЧтения);
Найти последний полный блок Base64. В прочитанной строке нам нужно найти последний перенос строки (Символы.ПС) или, если их нет, убедиться, что длина части строки, предназначенной для декодирования, кратна 4. Мы будем декодировать только ту часть буфера, которая составляет полный и корректный Base64-блок, заканчивающийся перед переносом строки или в точке, кратной 4 символам.
// Сначала удаляем все переносы строк из буфера для декодирования
СтрокаBase64Чистая = СтрЗаменить(СтрокаBase64, Символы.ПС, "");
// Определяем реальную длину декодируемой части, кратную 4
// Base64Значение работает с любым количеством символов,
// но для корректного потокового декодирования без ошибок
// нужно обрабатывать полные блоки.
// Если символы ПС удалены, то длина должна быть кратна 4.
ДлинаДекодируемойЧасти = СтрокаBase64Чистая.Длина() - (СтрокаBase64Чистая.Длина() % 4);
ДекодируемаяСтрока = Лев(СтрокаBase64Чистая, ДлинаДекодируемойЧасти);
Декодировать найденный блок. Используем функцию Base64Значение().
БуферДекодированный = Base64Значение(ДекодируемаяСтрока);
Записать декодированные данные. Записываем полученный БуферДвоичныхДанных в файл по позиции записи.
Поток.Записать(БуферДекодированный, 0, БуферДекодированный.Размер());
Корректировка позиций чтения и записи.
// ... внутри цикла ...
// Преобразуем буфер чтения в строку для удобства работы с Base64 и переносами строк
СтрокаBase64 = КодировкиТекста.UTF8.ПолучитьСтроку(БуферЧтения);
// Находим позицию последнего переноса строки в буфере
// или определяем максимально возможную длину для корректного Base64-блока
// Сначала очищаем от переносов строк, чтобы работать с чистым Base64-потоком
СтрокаBase64Чистая = СтрЗаменить(СтрокаBase64, Символы.ПС, "");
// Длина строки для декодирования должна быть кратна 4
ДлинаДекодируемойСтроки = СтрокаBase64Чистая.Длина() - (СтрокаBase64Чистая.Длина() % 4);
ДекодируемаяЧасть = Лев(СтрокаBase64Чистая, ДлинаДекодируемойСтроки);
// Декодируем Base64
БуферДекодированный = Base64Значение(ДекодируемаяЧасть);
// Переходим к позиции записи и записываем декодированный буфер
Поток.Перейти(ПозицияЗаписи, ПозицияВПотоке.Начало);
Поток.Записать(БуферДекодированный, 0, БуферДекодированный.Размер());
// Обновляем позицию записи
ПозицияЗаписи = ПозицияЗаписи + БуферДекодированный.Размер();
// Теперь самое важное: корректировка позиции чтения для следующей итерации.
// Мы должны учесть, сколько символов из СтрокаBase64 было реально использовано
// и сколько, возможно, осталось "лишних" символов, которые не вошли в текущий декодируемый блок.
// Это требует более точного подсчета символов в оригинальной СтрокаBase64
// до и после удаления переносов строк и формирования ДекодируемаяЧасть.
// Предположим, что мы нашли, сколько байт было "использовано" из оригинального буфера
// (с учетом переносов строк, которые были удалены).
// Простой способ: найти последнюю позицию, с которой можно начать следующую итерацию
// без потери данных.
// Количество "сырых" байт (включая переносы строк), соответствующих ДекодируемаяЧасть
КоличествоИспользованныхБайт = ДлинаДекодируемойСтроки; // Это если бы не было переносов строк
// Более сложная логика:
// Необходимо отследить, какая часть исходной `СтрокаBase64` соответствует `ДекодируемаяЧасть`.
// Найдем, сколько символов `ДекодируемаяЧасть` занимает в `СтрокаBase64`, включая переносы строк.
// Для этого нужно найти `ДекодируемаяЧасть` в `СтрокаBase64Чистая`, затем определить соответствующую
// ей часть в оригинальной `СтрокаBase64` с учетом всех Символы.ПС.
// Для упрощения, предположим, что мы читаем буфер, очищаем его, декодируем, записываем.
// А затем для следующей итерации ПозицияЧтения корректируется на количество реально обработанных символов.
// Например:
КоличествоСимволовОбработано = СтрокаBase64.Длина() - (СтрокаBase64Чистая.Длина() - ДлинаДекодируемойСтроки);
ПозицияЧтения = ПозицияЧтения + КоличествоСимволовОбработано;
// Если есть остаток строки (хвост, который не был декодирован из-за некратности 4 или из-за обрыва переноса строки),
// нам нужно отступить ПозицияЧтения назад на размер этого хвоста, чтобы в следующей итерации он был прочитан.
// Например, если Прочитано было 100 байт, а декодировано только 90, то ПозицияЧтения должна сместиться не на 100, а на 90.
// А оставшиеся 10 байт будут прочитаны снова в начале следующей итерации в новом буфере.
// Это потребует более тонкого управления ПозицияЧтения и Прочитано.
// Более надежный подход:
// 1. Прочитать БуферЧтения
// 2. Преобразовать в СтрокаBase64
// 3. Найти последнюю позицию корректного Base64 блока (например, кратного 4 символам после удаления ПС)
// 4. Декодировать эту часть (ДекодируемаяСтрока)
// 5. Записать БуферДекодированный
// 6. Сместить ПозицияЗаписи на БуферДекодированный.Размер()
// 7. Сместить ПозицияЧтения на количество *оригинальных* байт из БуферЧтения,
// которые соответствуют ДекодируемаяСтрока + все Символы.ПС, которые были перед ней.
// Остальные байты из БуферЧтения (не вошедшие в ДекодируемаяСтрока) будут перехвачены
// следующей операцией чтения.
// Примерная корректировка для ПозицияЧтения:
КоличествоНеобработанныхСимволов = СтрокаBase64Чистая.Длина() - ДлинаДекодируемойСтроки;
ПозицияЧтения = ПозицияЧтения + СтрокаBase64.Длина() - КоличествоНеобработанныхСимволов; // Смещаемся на "полностью обработанную" часть
// Также необходимо учитывать, что Base64 может иметь символы-заполнители '=' в конце.
// Функция Base64Значение() их обрабатывает корректно.
// Конец цикла, проверка ПозицияЗаписи и ЦелевойРазмер
Если ПозицияЗаписи >= ЦелевойРазмер Тогда
Прервать;
КонецЕсли;
// ... (конец цикла и процедуры)
Важно: При работе со строками большого размера (например, 64 Мб) в 1С следует быть осторожными. Операции типа СтрЗаменить() или Разделить() могут создавать временные копии строк в памяти, что может привести к значительному потреблению оперативной памяти. Для поиска переносов строк, возможно, более эффективно будет работать со строкой, а затем, вычислив смещение, работать с буфером двоичных данных, чтобы точно определить количество байт для чтения следующего блока. Использование метода СтрНайти() с конца строки для поиска последнего переноса строки может быть более эффективным, чем Разделить(), так как он не создает массив строк.
Решение задачи по удалению байтов из начала файла или декодированию Base64-данных на месте в 1С является сложной, но выполнимой задачей, которая требует тщательного подхода к управлению файловыми потоками и буферами. Мы рассмотрели, что:
Base64Значение().Этот подход позволяет эффективно работать с очень большими файлами, минимизируя потребление оперативной памяти и избегая создания дополнительных копий файлов на диске, что крайне важно в условиях ограниченных ресурсов.