Чтение текстового файла построчно в Collection на VB6: фильтр по словам и RegExp

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

Нужна не весь файл, а только строки с ошибками или без комментариев? Собираем функцию, которая читает файл в Collection и сразу отбирает строки по словам или регулярному выражению.

Главное. Отбор строк прямо при чтении экономит память и код: одна функция с параметрами Include или Exclude и выбором между InStr и RegExp закрывает большинство задач разбора текстовых файлов в VB6.

Зачем читать файл сразу с фильтром

Чтение файла в строку подходит, когда текст нужен целиком. Но часто программе интересна только часть строк: записи лога с уровнем ERROR, строки конфигурации без комментариев, позиции выгрузки с определённым артикулом. Загружать весь файл в память и потом отдельным циклом выбирать нужное неудобно и расточительно.

Функция Text2Col решает задачу за один проход. Она читает файл построчно, проверяет каждую строку условием и складывает подходящие в Collection. Вызывающий код получает готовый список и перебирает его через For Each. Если условие не задано, в коллекцию попадают все строки, и функция работает как обычное построчное чтение.

Параметры и перечисления

Чтобы вызов читался без комментариев, режимы оформлены перечислениями. GrabMode задаёт, что делать с найденными строками: оставлять (Include) или, наоборот, отбрасывать (Exclude). SearchMode выбирает механизм поиска: строковые функции VB или регулярные выражения.

Поисковая строка в строковом режиме может содержать несколько вариантов через вертикальную черту: «ERROR|FATAL» найдёт строки с любым из двух слов. В режиме RegExp та же черта уже часть синтаксиса шаблона и означает альтернативу, так что запись остаётся привычной.

Параметр CompareMode принимает стандартные vbBinaryCompare и vbTextCompare. Первый различает регистр, второй нет. Для регулярного выражения он переводится в свойство IgnoreCase.

modText.bas: объявленияVB6
Public Enum GrabMode
    Include = 0   ' оставить найденные строки
    Exclude = 1   ' оставить все, кроме найденных
End Enum

Public Enum SearchMode
    UseStringFunctions = 0
    UseRegex = 1
End Enum

Функция Text2Col

Для регулярных выражений в проект добавляется ссылка Microsoft VBScript Regular Expressions 5.5. Шаблон компилируется один раз до цикла, а не на каждой строке, это заметно ускоряет обработку больших файлов. Для проверки совпадения достаточно метода Test: он возвращает True или False и не создаёт коллекцию совпадений, как Execute.

Номер файла берётся через FreeFile, а при ошибке файл закрывается до того, как исключение уйдёт наверх.

modText.bas: Text2ColVB6
Public Function Text2Col(ByVal strPath As String, _
        Optional ByVal strSearch As String, _
        Optional ByVal Grab As GrabMode = Include, _
        Optional ByVal Compare As VbCompareMethod = vbTextCompare, _
        Optional ByVal SearchBy As SearchMode = UseStringFunctions) As Collection
    Dim col As New Collection, rx As New VBScript_RegExp_55.RegExp
    Dim arr() As String, s As String, i As Long, f As Integer, found As Boolean, hasFilter As Boolean
    On Error GoTo ErrTrap
    hasFilter = Len(Trim$(strSearch)) > 0
    If SearchBy = UseRegex Then
        rx.Pattern = strSearch
        rx.IgnoreCase = (Compare = vbTextCompare)
    Else
        arr = Split(strSearch, "|")
    End If
    f = FreeFile
    Open strPath For Input As #f
    Do Until EOF(f)
        Line Input #f, s
        If hasFilter Then
            found = False
            If SearchBy = UseRegex Then
                found = rx.Test(s)
            Else
                For i = 0 To UBound(arr)
                    If InStr(1, s, arr(i), Compare) > 0 Then found = True: Exit For
                Next
            End If
            If found = (Grab = Include) Then col.Add s
        Else
            col.Add s
        End If
    Loop
    Close #f
    Set Text2Col = col
    Exit Function
ErrTrap:
    If f > 0 Then Close #f
    Err.Raise Err.Number, "Text2Col", Err.Description
End Function

Как работает условие отбора

Ключевая строка функции: If found = (Grab = Include). Она объединяет четыре случая в одно сравнение. При режиме Include строка попадает в коллекцию, когда совпадение найдено. При режиме Exclude, когда совпадения нет. Такой приём короче вложенных If и не даёт ошибиться в одной из веток.

В строковом режиме цикл по вариантам прерывается на первом совпадении через Exit For. Если вариантов много, ставьте самые частые первыми.

  • Text2Col(path) вернёт все строки файла;
  • Text2Col(path, «ERROR|FATAL») оставит только аварийные записи лога;
  • Text2Col(path, «^\s*(;|#)», Exclude, , UseRegex) уберёт комментарии из ini-подобного файла;
  • Text2Col(path, «\d{2}\.\d{2}\.\d{4}», Include, , UseRegex) отберёт строки с датой.

Ограничения Line Input и Collection

Line Input считает концом строки пару CR LF и одиночный CR. Файлы из Unix с одним LF он прочитает как одну огромную строку. Если такие файлы возможны, надёжнее прочитать файл целиком, заменить vbLf на vbCrLf после нормализации и разбить через Split.

Open For Input читает файл в ANSI-кодировке системы. Текст в UTF-8 придёт с искажёнными русскими буквами, а UTF-16 вообще не прочитается построчно. Для UTF-8 используйте ADODB.Stream с Charset utf-8 и методом ReadText(adReadLine).

Collection удобна для перебора, но доступ по индексу в ней медленный: col(i) каждый раз проходит список. Перебирайте результат через For Each. Если нужен произвольный доступ к десяткам тысяч строк, лучше вернуть массив строк.

Пустой строке поиска в режиме Exclude соответствует «ничего не исключать», поэтому функция просто вернёт все строки. Проверка hasFilter делает это поведение явным.

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

В .NET это выражение в одну строку: File.ReadLines(path).Where(Function(l) rx.IsMatch(l)).ToList(). ReadLines читает файл лениво и не держит его в памяти целиком, корректно понимает LF и CRLF, а Regex с флагом RegexOptions.Compiled быстрее VBScript RegExp. Кодировку задают вторым аргументом, по умолчанию это UTF-8.

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

Почему файл из Linux читается одной строкой?

Line Input не считает одиночный LF концом строки. Прочитайте файл целиком, нормализуйте переводы строк и разбейте текст через Split по vbCrLf.

Чем RegExp.Test лучше Execute для фильтра?

Test только проверяет наличие совпадения и не создаёт объект MatchCollection. Для отбора строк этого достаточно, и работает он быстрее.

Как искать без учёта регистра?

Передайте vbTextCompare в параметр Compare. В строковом режиме он уйдёт в InStr, в режиме RegExp превратится в IgnoreCase = True.

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