ТекстовыйДокумент или ЧтениеТекста: чтение файлов, кодировки и импорт CSV в 1С

· 9 мин

Содержание

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

Остальные проблемы при чтении текстового документа — это кодировка, файл на компьютере пользователя, который не видит сервер, и кривой CSV. Всё это ниже, с рабочим кодом. Примеры подходят для любой конфигурации: это возможности платформы, а не БП или УТ.

Чтение файла целиком через ТекстовыйДокумент

ТекстовыйДокумент (в английском синтаксисе TextDocument) — это тип встроенного языка. Его же использует макет типа «Текстовый документ» в конфигураторе, поэтому иногда его называют объектом метаданных. Для чтения файла с диска макет не нужен: объект создаётся в коде.

Базовый вариант:

Текст = Новый ТекстовыйДокумент;
Текст.Прочитать("C:\Обмен\price.txt", КодировкаТекста.UTF8);

Для НомерСтроки = 1 По Текст.КоличествоСтрок() Цикл
    Строка = Текст.ПолучитьСтроку(НомерСтроки);
    // обработка строки
КонецЦикла;

Что здесь важно:

  • 📌 Нумерация строк начинается с 1, а не с 0.
  • 📌 КоличествоСтрок() считает и пустые строки, включая последнюю, если файл заканчивается переводом строки. Пустые строки пропускайте явно.
  • 📌 Весь текст одной строкой отдаёт ПолучитьТекст(). Удобно, когда файл нужно передать дальше целиком, например в ЧтениеJSON.

Синхронный и асинхронный вызов

Метод Прочитать() синхронный. На сервере это нормально. В клиентском коде веб-клиента и в конфигурациях с запретом модальности и синхронных вызовов платформа его не пропустит. Там используйте НачатьЧтение() с описанием оповещения или, в актуальных версиях платформы 8.3, асинхронный ПрочитатьАсинх() с Ждать.

&НаКлиенте
Асинх Процедура ЗагрузитьТекст(Команда)
    Текст = Новый ТекстовыйДокумент;
    Ждать Текст.ПрочитатьАсинх(ПутьКФайлу, КодировкаТекста.UTF8);
    Сообщить("Строк в файле: " + Текст.КоличествоСтрок());
КонецПроцедуры

Если нужно показать файл пользователю, ТекстовыйДокумент открывается в окне просмотра методом Показать(). Для отладки выгрузок это быстрее, чем открывать файл в блокноте.

Построчное чтение большого файла классом ЧтениеТекста

ЧтениеТекста (TextReader) держит в памяти только текущую строку. Файл открывается в конструкторе, а не отдельным методом:

Чтение = Новый ЧтениеТекста("C:\Обмен\journal.log", КодировкаТекста.UTF8);

Строка = Чтение.ПрочитатьСтроку();
Пока Строка <> Неопределено Цикл
    // обработка строки
    Строка = Чтение.ПрочитатьСтроку();
КонецЦикла;

Чтение.Закрыть();

Конец файла определяется так: ПрочитатьСтроку() возвращает Неопределено. Пустая строка файла придёт как "", поэтому сравнивайте именно с Неопределено, а не проверяйте ПустаяСтрока(). Иначе цикл остановится на первой пустой строке в середине файла.

Другие методы, которые пригодятся:

МетодЧто делает
ПрочитатьСтроку()Следующая строка без символа перевода строки
Прочитать(Количество)Указанное число символов; без параметра — весь остаток файла
Закрыть()Освобождает файл
Открыть(...)Переоткрывает объект на другой файл

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

Чтение из потока

Начиная с платформы 8.3.9 конструктор ЧтениеТекста принимает не только путь, но и поток. Это удобно, когда файл пришёл двоичными данными из HTTP-запроса или временного хранилища:

Поток = ДвоичныеДанныеФайла.ОткрытьПотокДляЧтения();
Чтение = Новый ЧтениеТекста(Поток, КодировкаТекста.UTF8);

Временный файл на диск записывать не нужно.

Кодировка и разделители строк при чтении текстового файла

Кракозябры вместо кириллицы почти всегда означают неверную кодировку. Платформа умеет определить UTF-8 и UTF-16 по метке BOM в начале файла. Файл без BOM в Windows-1251 она угадывает не всегда, поэтому кодировку надо указывать явно.

Источник файлаЧто указать
Выгрузка из Excel «CSV (разделители — запятые)»КодировкаТекста.ANSI или "windows-1251"
Excel «CSV UTF-8», большинство веб-сервисовКодировкаТекста.UTF8
Старые DOS-программы, банковские файлы старого форматаКодировкаТекста.OEM (cp866)
Файлы, сохранённые как «Юникод» в блокнотеКодировкаТекста.UTF16

Кодировку можно передать и строкой: "windows-1251", "utf-8", "cp866". Строковый вариант надёжнее ANSI: значение ANSI зависит от настроек операционной системы, на которой выполняется код. На сервере под Linux оно может оказаться не 1251.

Если в первой строке UTF-8 файла видите лишний символ перед текстом, значит, файл с BOM прочитан без указания UTF-8. Первое поле CSV в этом случае не совпадёт с ожидаемым заголовком, и поиск колонки по имени сломается.

Разделители строк

Файлы из Windows разделены парой символов ВК+ПС, из Linux и macOS — одним ПС. Оба объекта по умолчанию корректно разбирают оба варианта. Если источник использует нестандартный разделитель, например ; в конце каждой записи, передайте его третьим параметром:

Чтение = Новый ЧтениеТекста(ИмяФайла, "utf-8", ";");

Файл на компьютере пользователя: клиент, сервер и временное хранилище

Самая частая ошибка новичка — вызвать чтение на сервере с путём C:\Users\... с компьютера пользователя. В клиент-серверной базе сервер 1С работает на другой машине и такого файла не видит. В итоге ошибка «файл не найден», хотя файл точно лежит на месте.

Правильная схема:

  1. На клиенте пользователь выбирает файл в диалоге.
  2. Файл передаётся на сервер через временное хранилище.
  3. На сервере двоичные данные читаются через поток или временный файл.
&НаКлиенте
Асинх Процедура ВыбратьИЗагрузить(Команда)
    Параметры = Новый ПараметрыДиалогаПомещенияФайлов;
    Параметры.Фильтр = "CSV (*.csv)|*.csv|Текст (*.txt)|*.txt";
    Описание = Ждать ПоместитьФайлНаСерверАсинх(, , , Параметры, УникальныйИдентификатор);
    Если Описание = Неопределено Или Описание.ПомещениеФайлаОтменено Тогда
        Возврат;
    КонецЕсли;
    ЗагрузитьНаСервере(Описание.Адрес);
КонецПроцедуры

&НаСервере
Процедура ЗагрузитьНаСервере(Адрес)
    Данные = ПолучитьИзВременногоХранилища(Адрес);
    Чтение = Новый ЧтениеТекста(Данные.ОткрытьПотокДляЧтения(), "windows-1251");
    // разбор строк
    Чтение.Закрыть();
    УдалитьИзВременногоХранилища(Адрес);
КонецПроцедуры

ПоместитьФайлНаСерверАсинх есть в актуальных версиях платформы 8.3. На более старых используйте НачатьПомещениеФайла с описанием оповещения — смысл тот же.

Если файл лежит в общей сетевой папке, сервер может прочитать его напрямую по UNC-пути \\server\obmen\file.txt. Учтите, что читать будет учётная запись службы сервера 1С, а не пользователь. У неё должны быть права на эту папку.

Ошибки чтения и проверка, что файл существует

Перед чтением проверьте файл. Это даёт пользователю понятное сообщение вместо системного исключения:

Файл = Новый Файл(ИмяФайла);
Если Не Файл.Существует() Тогда
    ВызватьИсключение "Не найден файл загрузки: " + ИмяФайла;
КонецЕсли;

На клиенте синхронный Существует() тоже может быть запрещён. Используйте СуществуетАсинх() с Ждать.

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

Чтение = Неопределено;
Попытка
    Чтение = Новый ЧтениеТекста(ИмяФайла, "utf-8");
    Строка = Чтение.ПрочитатьСтроку();
    Пока Строка <> Неопределено Цикл
        ОбработатьСтроку(Строка);
        Строка = Чтение.ПрочитатьСтроку();
    КонецЦикла;
    Чтение.Закрыть();
Исключение
    Если Чтение <> Неопределено Тогда
        Чтение.Закрыть();
    КонецЕсли;
    ЗаписьЖурналаРегистрации("Загрузка из файла", УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    ВызватьИсключение;
КонецПопытки;

ПодробноеПредставлениеОшибки даёт стек вызовов для журнала регистрации. Пользователю достаточно КраткоеПредставлениеОшибки.

Диагностика: выберите симптом

Что быстрее: сравнение способов чтения текста

Оба объекта читают файл быстро. Разница появляется на больших объёмах и в том, что вы делаете со строками дальше.

КритерийТекстовыйДокументЧтениеТекста
ПамятьВесь файл в памяти, с запасом на служебные структурыТолько текущая строка
Доступ к строкеЛюбая строка по номеруТолько последовательно
Подходит дляФайлов до нескольких мегабайт, справочников, настроекЛогов, выгрузок, файлов в сотни мегабайт
Нужно ли закрыватьНетДа, Закрыть()
Чтение из потокаНетДа, с 8.3.9

Практические выводы:

  • 🚀 Файл в 200 МБ через ТекстовыйДокумент на 32-битном клиенте может упасть с нехваткой памяти. ЧтениеТекста обработает его без проблем.
  • 🚀 Узкое место почти никогда не само чтение, а запись в базу. Тысяча вызовов Записать() по одному элементу займёт заметно больше, чем чтение файла.
  • 🚀 Если нужен весь текст строкой, ЧтениеТекста.Прочитать() без параметра делает то же, что ТекстовыйДокумент.ПолучитьТекст(), без промежуточного объекта.
  • 🚀 Для JSON и XML не разбирайте текст вручную: есть ЧтениеJSON и ЧтениеXML. Для двоичных форматов с полями фиксированной длины — ЧтениеДанных.

Пример: загрузка справочника из CSV-файла

Задача: загрузить номенклатуру из файла, который бухгалтер сохранил из Excel. Формат: первая строка — заголовки, разделитель ;, кодировка Windows-1251.

Артикул;Наименование;Цена
А-001;Бумага А4 80 г/м2;450,00
А-002;"Скрепки 28 мм; 100 шт";95,50

Во второй строке разделитель стоит внутри кавычек. Простой СтрРазделить(Строка, ";") разобьёт её на четыре части вместо трёх. Поэтому нужна функция разбора с учётом кавычек:

Функция РазобратьСтрокуCSV(Строка, Разделитель = ";")
    Поля = Новый Массив;
    Текущее = "";
    ВКавычках = Ложь;
    Номер = 1;
    Длина = СтрДлина(Строка);
    Пока Номер <= Длина Цикл
        Символ = Сред(Строка, Номер, 1);
        Если Символ = """" Тогда
            Если ВКавычках И Сред(Строка, Номер + 1, 1) = """" Тогда
                Текущее = Текущее + """";
                Номер = Номер + 1;
            Иначе
                ВКавычках = Не ВКавычках;
            КонецЕсли;
        ИначеЕсли Символ = Разделитель И Не ВКавычках Тогда
            Поля.Добавить(СокрЛП(Текущее));
            Текущее = "";
        Иначе
            Текущее = Текущее + Символ;
        КонецЕсли;
        Номер = Номер + 1;
    КонецЦикла;
    Поля.Добавить(СокрЛП(Текущее));
    Возврат Поля;
КонецФункции

Цены в файле с запятой и иногда с неразрывным пробелом между разрядами («1 250,00»). Приводим к числу отдельно:

Функция СтрокаВЧисло(Знач Текст)
    Текст = СтрЗаменить(Текст, Символы.НПП, "");
    Текст = СтрЗаменить(Текст, " ", "");
    Текст = СтрЗаменить(Текст, ",", ".");
    Попытка
        Возврат Число(Текст);
    Исключение
        Возврат 0;
    КонецПопытки;
КонецФункции

Основная процедура загрузки на сервере. Справочник и реквизиты приведены для примера: в вашей конфигурации артикул может храниться в другом реквизите, а цена — в регистре сведений, а не в справочнике.

&НаСервере
Процедура ЗагрузитьНоменклатуру(ДвоичныеДанные)
    Чтение = Новый ЧтениеТекста(ДвоичныеДанные.ОткрытьПотокДляЧтения(), "windows-1251");
    Заголовок = Чтение.ПрочитатьСтроку(); // пропускаем заголовки
    Загружено = 0;
    НомерСтроки = 1;

    Строка = Чтение.ПрочитатьСтроку();
    Пока Строка <> Неопределено Цикл
        НомерСтроки = НомерСтроки + 1;
        Если Не ПустаяСтрока(Строка) Тогда
            Поля = РазобратьСтрокуCSV(Строка);
            Если Поля.Количество() < 3 Тогда
                Сообщить("Строка " + НомерСтроки + ": мало полей, пропущена");
            Иначе
                Ссылка = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", Поля[0]);
                Если Ссылка.Пустая() Тогда
                    Объект = Справочники.Номенклатура.СоздатьЭлемент();
                    Объект.Артикул = Поля[0];
                Иначе
                    Объект = Ссылка.ПолучитьОбъект();
                КонецЕсли;
                Объект.Наименование = Поля[1];
                Объект.Записать();
                Загружено = Загружено + 1;
            КонецЕсли;
        КонецЕсли;
        Строка = Чтение.ПрочитатьСтроку();
    КонецЦикла;

    Чтение.Закрыть();
    Сообщить("Загружено элементов: " + Загружено);
КонецПроцедуры

Что стоит добавить в боевой вариант:

  1. Сверять заголовки в первой строке с ожидаемыми. Если бухгалтер переставил колонки, загрузка должна остановиться, а не записать цены в наименования.
  2. Не обрывать загрузку на одной плохой строке. Запись элемента оберните в Попытка и копите ошибки с номерами строк, чтобы показать их списком в конце.
  3. Для загрузки нескольких тысяч элементов группировать запись в транзакции порциями, например по 500 строк. Одна огромная транзакция надолго держит блокировки, а запись без транзакций идёт медленнее.

Первый прогон делайте на копии базы. Ошибка в сопоставлении колонок перезапишет наименования у существующей номенклатуры, и откатить это без резервной копии сложно.

Частые вопросы

Как прочитать текстовый макет конфигурации, а не файл с диска?
Получите его методом ПолучитьМакет("ИмяМакета") в модуле объекта или менеджера. Если макет типа «Текстовый документ», вернётся готовый ТекстовыйДокумент, и дальше работают КоличествоСтрок() и ПолучитьСтроку().
Можно ли прочитать файл, который в этот момент дописывает другая программа?
Да, если открыть его без монопольного режима. Последний параметр конструктора ЧтениеТекста и метода Прочитать() отвечает за монопольный доступ; передайте Ложь. Строки, дописанные после открытия, могут не попасть в чтение.
Как узнать кодировку файла, если её никто не сообщил?
Откройте файл в Notepad++ или аналогичном редакторе: кодировка видна в строке состояния. В коде надёжный признак только один — метка BOM для UTF-8 и UTF-16. Для файлов без метки договоритесь с источником о формате.
Как прочитать файл из интернета без сохранения на диск?
Получите ответ HTTP-запроса методом ПолучитьТелоКакДвоичныеДанные(), затем откройте поток и передайте его в ЧтениеТекста. Если нужен просто текст, хватит ПолучитьТелоКакСтроку() с указанием кодировки.
Чем ЧтениеТекста отличается от ЧтениеДанных?
ЧтениеТекста работает со строками и кодировками. ЧтениеДанных читает байты и числа заданной разрядности, им разбирают двоичные форматы. Для CSV и TXT нужен первый.