Чтение текстового файла в строку на VB6: Open, Binary, FSO и UTF-8

РазделVisual Basic 6УровеньНовичок Чтение8 минОпубликовано ТегиVB6ФайлыFileSystemObject

Загрузить файл целиком в String нужно для разбора конфигов, шаблонов и логов. В VB6 для этого есть несколько способов, и они заметно различаются скоростью, кодировками и поведением на краях.

Главное. Для чтения файла целиком в VB6 надёжнее всего режим Binary с одним Get, а для UTF-8 подходит только ADODB.Stream. Цикл ReadLine со склейкой строк теряет переводы строк, вместо него используйте ReadAll.

Когда файл нужен целиком

Построчное чтение хорошо, когда файл огромный и обрабатывается потоком. Но во многих задачах удобнее получить весь текст одной строкой: подставить значения в HTML-шаблон, найти фрагмент регулярным выражением, сравнить содержимое с эталоном, показать файл в TextBox. Для файлов размером в единицы мегабайт это и проще, и быстрее.

В VB6 есть три инструмента для такой задачи: встроенные операторы Open, Input и Get, объектная библиотека Microsoft Scripting Runtime с классом FileSystemObject и компонент ADODB.Stream из ADO. Разберём каждый и посмотрим, когда что выбирать.

Open For Input и функция Input

Самый старый способ работает ещё со времён QuickBASIC. Функция FreeFile выдаёт свободный номер файла, Open открывает файл под этим номером, LOF возвращает длину, а Input читает нужное количество символов. Тот же номер потом передаётся в Close.

Обратите внимание на структуру обработки ошибок: метка обработчика стоит перед Close, поэтому файл закрывается и при успехе, и при сбое. Close с номером, который не был открыт, ошибку не вызывает, так что код безопасен, даже если сбой случился до Open.

modFileText.basVB6
Public Function ReadAllText(ByVal strPath As String) As String
    Dim intFile As Integer
    On Error GoTo ErrTrap
    If Len(Dir$(strPath)) = 0 Then Exit Function  ' файла нет
    intFile = FreeFile
    Open strPath For Input As #intFile
    ReadAllText = Input$(LOF(intFile), #intFile)
ErrTrap:
    Close #intFile                                ' закрываем в любом случае
    If Err.Number <> 0 Then
        Err.Raise Err.Number, "ReadAllText", Err.Description
    End If
End Function
У режима Input есть подвох: встретив символ Chr(26) (Ctrl+Z), он считает файл закончившимся и падает с ошибкой 62 «Input past end of file». Для произвольных файлов надёжнее режим Binary.

Режим Binary: быстро и без сюрпризов

В режиме Binary VB6 не интерпретирует содержимое. Мы заранее создаём строку нужной длины через Space$ и одним оператором Get заполняем её байтами файла. Это самый быстрый способ в чистом VB6: одна операция чтения вместо разбора по символам.

Байты при этом переводятся в Unicode-строку по текущей кодовой странице системы (для русской Windows это windows-1251). Для ANSI-файлов результат правильный, для UTF-8 кириллица превратится в набор знаков, о чём ниже.

modFileText.basVB6
Public Function ReadAllBinary(ByVal strPath As String) As String
    Dim intFile As Integer, strBuf As String
    On Error GoTo ErrTrap
    intFile = FreeFile
    Open strPath For Binary Access Read As #intFile
    If LOF(intFile) > 0 Then
        strBuf = Space$(LOF(intFile))   ' буфер на весь файл
        Get #intFile, , strBuf          ' одно чтение
    End If
    ReadAllBinary = strBuf
ErrTrap:
    Close #intFile
    If Err.Number <> 0 Then Err.Raise Err.Number, "ReadAllBinary", Err.Description
End Function

FileSystemObject: ReadAll вместо цикла

Для FileSystemObject нужна ссылка Project, References, Microsoft Scripting Runtime (scrrun.dll). Метод OpenTextFile возвращает объект TextStream, а у него есть ReadAll, который отдаёт всё содержимое сразу.

Частая ошибка в чужом коде: чтение циклом Do Until ts.AtEndOfStream с ReadLine и склейкой строк. ReadLine отрезает перевод строки, поэтому на выходе весь файл слипается в одну линию. Если цикл действительно нужен, добавляйте vbCrLf вручную, но проще сразу вызвать ReadAll. Единственная ловушка ReadAll: на пустом файле он выбрасывает ошибку, поэтому сначала проверяем размер.

modFileText.basVB6
' Требуется ссылка: Microsoft Scripting Runtime
Public Function ReadAllFso(ByVal strPath As String, _
                           Optional ByVal blnUnicode As Boolean = False) As String
    Dim fso As New FileSystemObject
    Dim ts As TextStream
    If Not fso.FileExists(strPath) Then Exit Function
    If fso.GetFile(strPath).Size = 0 Then Exit Function  ' ReadAll упадёт
    ' TristateTrue читает UTF-16, TristateFalse ANSI
    Set ts = fso.OpenTextFile(strPath, ForReading, False, _
                              IIf(blnUnicode, TristateTrue, TristateFalse))
    ReadAllFso = ts.ReadAll
    ts.Close
End Function

Файлы в UTF-8: ADODB.Stream

Ни Open, ни FileSystemObject не умеют читать UTF-8: первый понимает только ANSI, второй ANSI и UTF-16. А современные конфиги, выгрузки из веба и файлы из Git почти всегда в UTF-8. Выручает ADODB.Stream, который есть на любой Windows с установленным MDAC. Ему можно указать кодировку через свойство Charset, и он сам пропустит метку BOM в начале файла.

Подключать ADO ссылкой не обязательно, достаточно позднего связывания через CreateObject. Константа adTypeText равна 2.

modFileText.basVB6
Public Function ReadAllUtf8(ByVal strPath As String) As String
    Dim stm As Object
    Set stm = CreateObject("ADODB.Stream")
    stm.Type = 2              ' adTypeText
    stm.Charset = "utf-8"     ' можно и "windows-1251", "koi8-r"
    stm.Open
    stm.LoadFromFile strPath
    ReadAllUtf8 = stm.ReadText
    stm.Close
End Function

Поиск по содержимому через RegExp

Раз файл уже в строке, искать в нём удобно регулярными выражениями. В VB6 их даёт библиотека Microsoft VBScript Regular Expressions 5.5. Ниже функция, которая возвращает все совпадения шаблона, каждое с новой строки.

Шаблон стоит проверять до вызова: некорректное выражение вызовет ошибку 5017, а неудачный шаблон с вложенными квантификаторами на большом файле может работать очень долго.

modFileText.basVB6
' Требуется ссылка: Microsoft VBScript Regular Expressions 5.5
Public Function FindInText(ByVal strText As String, ByVal strPattern As String, _
                           Optional ByVal blnIgnoreCase As Boolean = True) As String
    Dim rx As New RegExp, m As Match, strOut As String
    rx.Pattern = strPattern
    rx.Global = True          ' все совпадения, а не первое
    rx.MultiLine = True       ' ^ и $ работают по строкам
    rx.IgnoreCase = blnIgnoreCase
    For Each m In rx.Execute(strText)
        strOut = strOut & m.Value & vbCrLf
    Next
    FindInText = strOut
End Function

' Пример: MsgBox FindInText(ReadAllBinary("C:\app\settings.ini"), "^timeout=.*$")

Что выбрать

Короткая шпаргалка по способам из статьи:

  • ANSI-файл, нужна скорость и отсутствие зависимостей: Open For Binary и Get;
  • проект уже использует FileSystemObject или нужен UTF-16: TextStream.ReadAll с проверкой на пустой файл;
  • файл в UTF-8 или другой кодировке, отличной от системной: ADODB.Stream с Charset;
  • Open For Input оставьте для заведомо чистых текстовых файлов, в которых не встретится Chr(26).
Строка в VB6 хранится в UTF-16, поэтому файл на 50 МБ займёт в памяти около 100 МБ. Для гигантских логов читайте файл кусками или построчно.

Как это делают сегодня

В .NET вся статья сводится к одной строке: File.ReadAllText(path, Encoding.UTF8) или асинхронный File.ReadAllTextAsync. Кодировки, BOM и закрытие файла фреймворк берёт на себя. При миграции legacy-кода на VB.NET полезно сразу заменить вызовы Open и FileSystemObject на System.IO и явно указывать кодировку, чтобы не зависеть от региональных настроек сервера.

Вопросы и ответы

Какую ссылку подключить для FileSystemObject?

Project, References, Microsoft Scripting Runtime (файл scrrun.dll). Без неё типы FileSystemObject и TextStream не будут известны компилятору. Альтернатива: позднее связывание через CreateObject("Scripting.FileSystemObject").

Почему вместо кириллицы получаются кракозябры?

Файл, скорее всего, в UTF-8, а Open и FileSystemObject читают его в системной кодовой странице. Используйте ADODB.Stream с Charset = "utf-8".

Почему ReadAll падает с ошибкой 62?

Так TextStream реагирует на пустой файл. Перед чтением проверьте размер через fso.GetFile(path).Size и возвращайте пустую строку, если он равен нулю.

Читайте также