ЧтениеДанных и ЗаписьДанных. Работа со строками

Использование потоков и двоичных данных для работы со строками.

Начиная с релиза 8.3.9, в 1С есть инструментарий для работы с двоичными данными и потоками, широко уже освещённый и разобранный на примерах, в 8.3.10 добавлись ещё полезные глобальные функции. Но большинство примеров касаются именно т.н. BLOB, больших бинарных объектов, иная обработка которых, чем этими инструментами, невозможна.

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

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

Инструментарий

ДвоичныеДанные (далее ДД) — по сути, указатель на некую кучу (heap) в ОЗУ. Никак не упорядочены, но именно оперируя ими, наиболее экономим ресурсы. Размер можно только узнать. Доступа к внутренностям нет.

Буфер — по сути, тоже множество данных в ОЗУ, но с возможностью точечно, адресно, указав позицию в байтах, обратиться к данным, прочесть, изменить, вставить их; никакой структуры внутри себя не имеет — просто набор байтов. Буфер позволяет обработать все байты (отзеркалить, инвертировать итд). Размер можно узнать, изменить, назначить заранее (зависит от доступной ОЗУ, на диск не дампится). Доступ к внутренностям — произвольный.

Поток — по сути, данные с указателем текущей позиции, который двигается при операциях вперёд и который можно позиционировать. Поток может быть файловый, т.е. связанный с указателем на файл, наиболее близкий аналог связи и ограничений доступа, которые налагает такой объект в рамках файловой системы ОС это объект "СсылкаНаФайл" из 8.3.12 (в смысле блокировки при чтении/записи). Неудобен непредсказуемостью кэширования, выполняемого ОС, и неравномерной нагрузкой на жёсткий диск. Поток может быть в оперативке, созданный "на лету" из других объектов, обрабатываемый ими. Поток обрабатывается постепенно и может "съесть" весьма большие данные. Размер может расти произвольно, платформа предпринимает меры по поддержанию потока при любой нехватке ОЗУ. Доступ к внутренностям — последовательный.

ЧтениеДанных и ЗаписьДанных — по сути, насадки на потоки, расширяющие их возможности в части чтения фрагментов, в т.ч. по маркерам и разделителям, и более гибкой записи. Даже если их создавали по двоичным данным или файлу, всегда у чтения есть "исходный поток", у записи "целевой поток", причём с одним потоком могут поработать несколько чтений, несколько записей, равно те и другие. Они просто инструменты удобной обработки потоков, в т.ч. если явно с потоком и не работают, он всё равно — основной объект. Доступ к внутренностям — произвольный и последовательный, но и при произвольном указатель позиции двигается к концу данных.

РезультатЧтенияДанных — по сути, данные выборки в оптимизированном хранилище (если на клиенте, то могут дампиться во временные файлы, если на сервере, то могут помещаться в сеансовые файлы). Существуют отдельно от объекта чтения, породившего их, и от его исходного потока (даже если тот закрыт). По нагрузке на svhost наиболее схожи с ДД.

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

Для работы со строками, особенно больших объёмов, интерес представляют ДД, потоки, чтение и запись. Буфер бесполезен, т.к. строка может содержать любые символы, чей размер разнится (например, латиница 1 байт, кириллица 2 байта итд), однозначно позиционироваться и порционно читать такое нельзя. Но буфер можно использовать как единый и неделимый промежуточный носитель неких данных, если операция с его участием быстрее иных способов. Операции разделения и слияния для буфера кэшируются, этим он тоже полезен.

Объекты имеют и синхронные, и асинхронные методы; в примерах использованы синхронные. Кстати, на сервере, очевидно, применимы только синхронные методы.

 

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

ИтоговыеДД=ПолучитьДвоичныеДанныеИзСтроки(ИсходнаяСтрока,Кодировка,СимволBOM);
ИтоговаяСтрока=ПолучитьСтрокуИзДвоичныхДанных(ИсходныеДД);
ИтоговыеДД=СоединитьДвоичныеДанные(МассивИсходныхДД);

Буфер=ПолучитьБуферДвоичныхДанныхИзСтроки(ИсходнаяСтрока,Кодировка,СимволBOM);
Буфер=ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ИсходныеДД);
ИтоговыеДД=ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер);
ИтоговаяСтрока=ПолучитьСтрокуИзБуфераДвоичныхДанных(Буфер,Кодировка);
ИтоговыйБуфер=СоединитьБуферыДвоичныхДанных(МассивИсходныхБуферов);

 

В части работы с файлами есть нюанс в режиме записи, системное перечисление "РежимОткрытияФайла". При использовании менеджера файловых потоков или одного файлового потока в конструкторе режим указывается явно; а если задействована ЗаписьДанных, то повлиять на режим нельзя, и работает она как "ОткрытьИлиСоздать". При этом, если файл уже был, и перезаписывается меньшим количеством данных, то старые данные будут "торчать" из-под новых, как некий недозатёртый "хвост" наподобие новой аудио/видео записи на кассетах, из-под которой в конце видна предыдущая запись на этом же месте. Рекомендуется для существующих файлов ставить режим "Обрезать", или удалять файлы средствами ОС, или очищать иначе.

Примеры

В качестве иллюстрации возможностей и разнообразия способов применения — общеизвестная задача разбивки строки на массив подстрок по разделителю. Любой из ниже перечисленных способов в разы и десятки раз медленнее, чем СтрРазделить и прочие варианты, но при больших объёмах все, кроме буферов, с гораздо большей вероятностью работоспособны.

// Все приведённые варианты ведут себя как СтрЗаменить(рСтрока,рРазделитель,Истина), т.е. включая пустые строки в результат
// Указаны времязатраты для 100'000 вызовов в цикле, СтрЗаменить показала время 1-2 сек.

// Только буферы данных, разовое разделение методом буфера; 22-26 сек.
Функция РазделитьСтрокуНаМассивПодстрок1(рСтрока,рРазделитель=" ")
буфСтроки=ПолучитьБуферДвоичныхДанныхИзСтроки(рСтрока);
буфРазделителя=ПолучитьБуферДвоичныхДанныхИзСтроки(рРазделитель);
//
мБуферов=буфСтроки.Разделить(буфРазделителя);
//
мРезультатов=Новый Массив;
Для каждого рБуфер Из мБуферов Цикл
мРезультатов.Добавить(ПолучитьСтрокуИзБуфераДвоичныхДанных(рБуфер));
КонецЦикла;
Возврат мРезультатов;
КонецФункции

// ДвоичныеДанные и ЧтениеДанных, разовое разделение методом ЧтениеДанных; 28-32 сек.
Функция РазделитьСтрокуНаМассивПодстрок2(рСтрока,рРазделитель=" ")
рЧтение=Новый ЧтениеДанных(ПолучитьДвоичныеДанныеИзСтроки(рСтрока));
мРезЧтения=рЧтение.Разделить(рРазделитель);
рЧтение.Закрыть();
//
мРезультатов=Новый Массив;
Для каждого рРезультат Из мРезЧтения Цикл
мРезультатов.Добавить(ПолучитьСтрокуИзДвоичныхДанных(рРезультат.ПолучитьДвоичныеДанные()));
КонецЦикла;
Возврат мРезультатов;
КонецФункции

// ДвоичныеДанные и ЧтениеДанных, последовательное чтение строк по разделителю; 27-31 сек.
Функция РазделитьСтрокуНаМассивПодстрок3(рСтрока,рРазделитель=" ")
рЧтение=Новый ЧтениеДанных(ПолучитьДвоичныеДанныеИзСтроки(рСтрока));
//
мРезультатов=Новый Массив;
Пока Истина Цикл
#Если Клиент Тогда
ОбработкаПрерыванияПользователя();
#КонецЕсли
мРезультатов.Добавить(рЧтение.ПрочитатьСтроку(,рРазделитель));
// или так (но это дольше и более ресурсоёмко, 46-52 сек.), даже если без промежуточных переменных:
//рез=рЧтение.ПрочитатьДо(рРазделитель);
//рДД=рез.ПолучитьДвоичныеДанные();
//мРезультатов.Добавить(ПолучитьСтрокуИзДвоичныхДанных(рДД));
//
Если рЧтение.ЧтениеЗавершено Тогда Прервать КонецЕсли;
КонецЦикла;
рЧтение.Закрыть();
Возврат мРезультатов;
КонецФункции

Эти примеры иллюстрируют разнообразие приёмов в рассматриваемом инструментарии по части чтения. Замечу, что "ПрочитатьСтроку" и вообще все методы, возвращающие символы и строки, ведут себя как буферы, а не как потоки, поэтому следует осмотрительно относиться к возможному объёму прочитанного, дабы не превысить выделенные объёмы ОЗУ.

 

С помощью этих инструментов можно решать вопрос конкатенации, особенно это эффективно для одинаковых строк (мы знаем, что  строковые операции весьма времяёмки), пример: 

// этот код выполняется примерно 30 сек.
а="Это";
б=" круто";
Для й=1 По 100000 Цикл
а=а+б;
КонецЦикла;

// этот код выполняется 1 сек.
мДД=Новый Массив;
мДД.Добавить(ПолучитьДвоичныеДанныеИзСтроки("Это"));
рДДДобавка=ПолучитьДвоичныеДанныеИзСтроки(" круто"));
Для й=1 По 100000 Цикл
мДД.Добавить(рДДДобавка);
КонецЦикла;
а=ПолучитьСтрокуИзДвоичныхДанных(СоединитьДвоичныеДанные(мДД));

 

Более сложный пример иллюстрирует чтение и запись с применением потоков. Задача: разделение xml-файла большого размера, с большим количеством однотипно повторяющихся узлов. Решение не вполне универсальное, но работоспособное:

 // разбивка хмл-файла на примере файла Import CML 2.X в нотации 1С-Битрикс

// входные параметры
имяф="C:НекийПутьКФайлуimport___f3b35232-98a0-4b75-b7c5-6aa8c7199ff2.xml";
квоБлоковВФайле=100; // последний файл может быть меньшего размера
рТег="<Товар>";
рСтаршийТег="<Товары>";
рКодировка=КодировкаТекста.UTF8;

// собственно действия
рЗакрытиеСтаршегоТега=СтрЗаменить(рСтаршийТег,"<","</");

рФайл=Новый Файл(имяф);
рПоток=Новый ФайловыйПоток(имяф,РежимОткрытияФайла.Открыть);

рЧтение=Новый ЧтениеДанных(рПоток,рКодировка);
ддНачало=рЧтение.ПрочитатьДо(рТег).ПолучитьДвоичныеДанные();
рЧтение.ПропуститьДо(рЗакрытиеСтаршегоТега);
ддКонец=рЧтение.Прочитать().ПолучитьДвоичныеДанные();
рЧтение.Закрыть();

рПоток.Перейти(0,ПозицияВПотоке.Начало); // т.к. чтение упёрлось в конец потока

рДДТега=ПолучитьДвоичныеДанныеИзСтроки(рТег);
рДДЗакрытиеСтаршегоТега=ПолучитьДвоичныеДанныеИзСтроки(рЗакрытиеСтаршегоТега);

// разные способы манипуляции кусками данных
рПодвариант=3;
Если рПодвариант=1 Тогда
рЧтение=Новый ЧтениеДанных(рПоток,рКодировка);
мДоИПослеСтаршего=рЧтение.Разделить(рЗакрытиеСтаршегоТега); // самое ресурсоёмкое
резДоСтаршего=мДоИПослеСтаршего.Получить(0);
подПоток=резДоСтаршего.ОткрытьПотокДляЧтения();
рЧтениеБлоков=Новый ЧтениеДанных(подПоток);
рЧтение.Закрыть();
рЧтениеБлоков.ПропуститьДо(рСтаршийТег);
//
ИначеЕсли рПодвариант=2 Тогда
рЧтение=Новый ЧтениеДанных(рПоток,рКодировка);
рЧтение.ПропуститьДо(рСтаршийТег);
рез1=рЧтение.ПрочитатьДо(рЗакрытиеСтаршегоТега); // самое ресурсоёмкое
подПоток=рез1.ОткрытьПотокДляЧтения();
рЧтениеБлоков=Новый ЧтениеДанных(подПоток);
рЧтение.Закрыть();
//
ИначеЕсли рПодвариант=3 Тогда // наиболее быстрый способ, оперирует значительно меньшими объёмами
рЧтениеБлоков=Новый ЧтениеДанных(рПоток,рКодировка);
//
КонецЕсли;

мБлоков=рЧтениеБлоков.Разделить(рТег); // саму рТег не включает в результат
рЧтениеБлоков.Закрыть();
квоБлоковВсего=мБлоков.Количество();

рПоток.Закрыть();

рНомерБлока=999999;
рСчётчикФайлов=1;
рИмяРезФайла="";
рЗапись=Неопределено;

Для й=0 По квоБлоковВсего-1 Цикл
рБлок=мБлоков.Получить(й);
ОбработкаПрерыванияПользователя();
//
Если рНомерБлока>квоБлоковВФайле Тогда
// заканчиваем предыдущий
Если рЗапись<>Неопределено Тогда
рЗапись.Записать(рДДЗакрытиеСтаршегоТега);
рЗапись.Записать(ддКонец);
рЗапись.Закрыть();
Сообщить("Обработан файл "+рИмяРезФайла);
КонецЕсли;
// начинаем новый
рИмяРезФайла=рФайл.Путь+рФайл.ИмяБезРасширения+"_part"+Формат(рСчётчикФайлов,"ЧГ=0")+рФайл.Расширение;
рЗапись=Новый ЗаписьДанных(рИмяРезФайла,рКодировка);
рЗапись.Записать(ддНачало);
рНомерБлока=0;
рСчётчикФайлов=рСчётчикФайлов+1;
КонецЕсли;
//
// вписываем текущий блок
рДДБлока=рБлок.ПолучитьДвоичныеДанные();
Если рПодвариант<>3 и й<>0 Тогда
рЗапись.Записать(рДДТега);
ИначеЕсли рПодвариант=3 Тогда
Если й=0 Тогда // чтение взяло блок от начала до разделителя целиком, вырезаем заголовочную часть
рЧтениеПервогоБлока=Новый ЧтениеДанных(рДДБлока);
рЧтениеПервогоБлока.ПропуститьДо(рТег);
рДДБлока=рЧтениеПервогоБлока.Прочитать().ПолучитьДвоичныеДанные();
рЧтениеПервогоБлока.Закрыть();
ИначеЕсли й=квоБлоковВсего-1 Тогда // чтение взяло блок от разделителя до конца, вырезаем хвостовую часть
рЧтениеПоследнегоБлока=Новый ЧтениеДанных(рДДБлока);
рДДБлока=рЧтениеПоследнегоБлока.ПрочитатьДо(рЗакрытиеСтаршегоТега).ПолучитьДвоичныеДанные();
рЧтениеПоследнегоБлока.Закрыть();
рЗапись.Записать(рДДТега);
Иначе
рЗапись.Записать(рДДТега);
КонецЕсли;
КонецЕсли;
рЗапись.Записать(рДДБлока);
рНомерБлока=рНомерБлока+1;
КонецЦикла;

// заканчиваем последний
Если рЗапись<>Неопределено Тогда
рЗапись.Записать(рДДЗакрытиеСтаршегоТега);
рЗапись.Записать(ддКонец);
рЗапись.Закрыть();
Сообщить("Обработан файл "+рИмяРезФайла);
КонецЕсли;

Сообщить("Всё!");

 

Если работа с потоком идёт через объекты-"насадки", то есть основная, доступная для чтения позиция, и неявные системные позиции по мнению 1С, недоступные из языка. Поэтому перепозиционирование указателя на потоке-владельце Чтения или Записи в процессе работы с ними выполнять не следует — сначала их надо закрыть, потом изменить позицию и лишь потом сделать новые Чтение/Запись. Так, например, команда 

рЧтение.ИсходныйПоток().Перейти(0,ПозицияВПотоке.Начало);

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

Итого

Сочетая избирательное чтение и запись вышеописанными способами с такими инструментами, как DOM, XDTO, XML+XPath+Xslt, RegExp и прочим, можно создать успешное решение для строковых big data промышленных масштабов — например, для обменов или библиографических систем.

Тестирование выполнялось на релизах 8.3.10.2561, 8.3.15.1534 и 8.3.15.1565.

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

 

p.s. Долго думал, как правильно во множественном числе — "буферы" или "буфера". Не взыщите)))

7 Comments

  1. fr13

    Я бы еще добавил, что Строка есть не что иное как массив символом (chars). Также Строка не мутабельна (то есть «а» + «б» -> всегда новая строка в памяти «аб»), отсюда требовательность к ресурсам.

    Reply
  2. A_Max

    тоже добавлю:

    мДД=Новый Массив;
    мДД.Добавить(«Это»);
    Для й=1 По 100000 Цикл
    мДД.Добавить(» круто»);
    КонецЦикла;
    а=СтрСоединить(мДД, «»);

    Тоже выполниться за секунду

    Reply
  3. cosmo2004

    (2) Для данного случая хорошо, но массив полностью находится в оперативке и можно получить нехватку памяти, поток универсальнее.

    Reply
  4. Gossluzh

    что это вообще такое????

    Reply
  5. Yashazz

    (3) Именно, поэтому я разные варианты и показываю. Опять же, между Поток.Записать(Буфер,0,Буфер.Размер) и Запись.Записать(Буфер) тоже есть разница.

    Reply
  6. Yashazz

    (2) Потому как есть у меня подозрение, что оное действие в потрохах платформы чем-то вроде двоичных данных и реализовано)

    Reply
  7. Cyberhawk
    ведут себя как СтрЗаменить

    СтрРазделить

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *