Поиск, фильтрация и обработка данных
Четыре операции, из которых состоит 90% офисной рутины: найти значение, отфильтровать по условию, вычистить дубли и подсветить проблемные строки.
Range.Find — программный «Ctrl+F»: ищет значение в диапазоне и возвращает ссылку на ячейку, где нашёл, или
Nothing, если не нашёл ничего.
Зачем это в коде, если всё есть в интерфейсе
Найти, отфильтровать, удалить дубли — всё это Excel умеет и без единой строчки VBA. Но руками это делается каждый раз заново. А в макросе — один раз, кнопкой, всегда в одном и том же порядке, без «ой, я забыл снять фильтр». Именно поэтому четыре приёма из этого урока встречаются почти в любом рабочем макросе, и именно на них новички ломают себе файлы. Разберём каждый на боевом примере.
Дальше везде работаем с листом «Данные»: шапка в строке 1, колонки A–F — Дата, Менеджер, Товар, Количество, Сумма, Источник.
Поиск: Find и FindNext
Простейший сценарий — спросить у пользователя товар и прыгнуть на первое совпадение.
Option Explicit
Sub FindProduct()
Dim ws As Worksheet
Dim hit As Range
Dim query As String
Set ws = ThisWorkbook.Worksheets("Данные")
query = Trim$(InputBox("Какой товар ищем?", "Поиск"))
If Len(query) = 0 Then Exit Sub ' нажали «Отмена» или ничего не ввели
Set hit = ws.Columns("C").Find( _
What:=query, _
LookIn:=xlValues, _
LookAt:=xlWhole, _
MatchCase:=False)
If hit Is Nothing Then
MsgBox "«" & query & "» в колонке C не найден.", vbExclamation
Else
ws.Activate
hit.Select
MsgBox "Нашёл в ячейке " & hit.Address(False, False) & _
", строка " & hit.Row, vbInformation
End If
End Sub
Ключевое здесь — Set hit = ... и проверка If hit Is Nothing. Find возвращает объект (ячейку), поэтому присваивание идёт через Set. А если совпадения нет, вернётся Nothing — и любая попытка спросить у него .Row уронит макрос с «Object variable not set». Проверка на Nothing — не перестраховка, а обязательная часть любого Find.
Ловушка, на которой обжигаются все
Параметры LookIn, LookAt и MatchCase — «липкие». Excel запоминает их последнее значение — причём общее для окна поиска Ctrl+F и для VBA. Если ты не указал LookAt явно, макрос возьмёт то, что пользователь выбрал в диалоге час назад. Вчера искал целиком — сегодня по части. Отсюда мистические баги «на моём компьютере работает». Лечение простое: указывай LookIn и LookAt явно всегда, при каждом вызове Find. xlWhole — совпадение целиком, xlPart — вхождение подстроки.
Найти все совпадения
Find находит первое, FindNext — следующее, по кругу. Значит, нужен якорь: запоминаем адрес первой находки и крутим цикл, пока не вернулись к нему.
Sub HighlightAllMatches()
Dim ws As Worksheet
Dim searchArea As Range
Dim hit As Range
Dim firstAddress As String
Dim found As Long
Set ws = ThisWorkbook.Worksheets("Данные")
Set searchArea = ws.Range("C2:C" & ws.Cells(ws.Rows.Count, "A").End(xlUp).Row)
Set hit = searchArea.Find(What:="Клавиатура", LookIn:=xlValues, LookAt:=xlWhole)
If hit Is Nothing Then
MsgBox "Совпадений нет.", vbInformation
Exit Sub
End If
firstAddress = hit.Address ' запомнили, откуда начали круг
Do
found = found + 1
hit.Interior.Color = RGB(255, 235, 156) ' жёлтая заливка
Set hit = searchArea.FindNext(hit)
If hit Is Nothing Then Exit Do ' страховка
Loop While hit.Address <> firstAddress
MsgBox "Подсвечено совпадений: " & found, vbInformation
End Sub
Почему проверка If hit Is Nothing Then Exit Do вынесена отдельной строкой, а не приписана в условие Loop While? Потому что VBA не умеет короткого замыкания. В выражении Not hit Is Nothing And hit.Address <> firstAddress он вычислит обе части, даже если первая уже дала «ложь», — и на hit.Address упадёт. В большинстве языков так нельзя, в VBA — можно и больно. Разделяй проверки на отдельные строки.
Без якоря firstAddress цикл будет крутиться вечно: дойдя до низа, FindNext начинает круг заново.
Фильтр по условию и выгрузка видимых строк
Классическая задача: «выгрузи мне сделки от 100 000 на отдельный лист».
Sub ExportBigDeals()
Dim ws As Worksheet
Dim wsOut As Worksheet
Dim lastRow As Long
Dim visibleRows As Range
Set ws = ThisWorkbook.Worksheets("Данные")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
If ws.AutoFilterMode Then ws.AutoFilterMode = False ' снимаем старый фильтр
' Field:=5 — пятая колонка ДИАПАЗОНА (это E, «Сумма»), а не пятая колонка листа
ws.Range("A1:F" & lastRow).AutoFilter Field:=5, Criteria1:=">=100000"
On Error Resume Next
Set visibleRows = ws.Range("A2:F" & lastRow).SpecialCells(xlCellTypeVisible)
On Error GoTo 0
If visibleRows Is Nothing Then
MsgBox "Под условие не попала ни одна строка.", vbInformation
Else
Set wsOut = ThisWorkbook.Worksheets.Add
wsOut.Name = "Крупные сделки " & Format(Now, "hh-mm")
ws.Range("A1:F1").Copy wsOut.Range("A1") ' шапка
visibleRows.Copy wsOut.Range("A2") ' только видимые строки
wsOut.Columns("A:F").AutoFit
End If
ws.AutoFilterMode = False
Application.CutCopyMode = False
End Sub
Три тонкости, которые надо запомнить:
Field:=5считается от левого края диапазона фильтра, а не от колонки A листа. Если бы фильтр стоял наC1:H100, пятой колонкой была бы G. Самая частая ошибка новичка — отфильтровать не то, что хотел.- Критерий передаётся строкой:
">=100000","<>"(непустые),"Иванов". Для дат надёжнее собирать строку черезFormat— иначе месяц и день могут поменяться местами из-за локали. SpecialCells(xlCellTypeVisible)выбрасывает ошибку 1004, если видимых ячеек нет вообще. Не «возвращает пусто», а именно падает. Поэтому вокруг него —On Error Resume Nextи проверка наNothing.
Плюс: visibleRows.Copy копирует только видимые строки, скрытые фильтром не поедут. Именно это нам и нужно.
Удаление дублей
У Excel есть встроенный метод, и он мгновенный. Но у него есть свойство, которое стоит людям нервов: после макроса не работает Ctrl+Z. Запуск любого макроса очищает стек отмены Excel. Удалил лишнее — вернуть штатно уже нечем. Поэтому сначала копия, потом удаление.
Sub DropDuplicates()
Dim ws As Worksheet
Dim backup As Worksheet
Dim lastRow As Long
Dim rowsBefore As Long
Dim rowsAfter As Long
Set ws = ThisWorkbook.Worksheets("Данные")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
rowsBefore = lastRow - 1
' СНАЧАЛА резервная копия: отменить удаление через Ctrl+Z будет нельзя
ws.Copy After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)
Set backup = ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)
backup.Name = "Копия " & Format(Now, "dd-mm hh-mm")
' дубль = совпали Дата (1), Менеджер (2) и Товар (3)
ws.Range("A1:F" & lastRow).RemoveDuplicates _
Columns:=Array(1, 2, 3), Header:=xlYes
rowsAfter = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row - 1
MsgBox "Было строк: " & rowsBefore & vbCrLf & _
"Стало строк: " & rowsAfter & vbCrLf & _
"Удалено дублей: " & (rowsBefore - rowsAfter), vbInformation
End Sub
Номера в Columns:=Array(1, 2, 3) — снова относительные, от левого края диапазона. Header:=xlYes говорит, что первая строка — шапка, и трогать её не надо. Метод оставляет первое вхождение и убивает все последующие; если тебе нужна, наоборот, самая свежая запись — сначала отсортируй по дате по убыванию.
Подсветка по правилу: два разных способа
Тут важно понимать разницу. Условное форматирование (FormatConditions) — это живое правило: поправил сумму в ячейке — цвет пересчитался сам. Прямая заливка (Interior.Color) — это разовый мазок краской: данные поменяются, а цвет останется врать.
Sub MarkLowSales()
Dim ws As Worksheet
Dim rng As Range
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Данные")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
Set rng = ws.Range("A2:F" & lastRow)
rng.FormatConditions.Delete ' иначе правила копятся при каждом запуске
' правило по формуле: смотрим на колонку E текущей строки
With rng.FormatConditions.Add(Type:=xlExpression, Formula1:="=$E2<10000")
.Interior.Color = RGB(255, 199, 206) ' бледно-красная заливка
.Font.Color = RGB(156, 0, 6)
.StopIfTrue = False
End With
End Sub
Знак доллара в $E2 — не украшение. Колонка закреплена ($E), строка свободна (2), и формула пишется относительно левой верхней ячейки диапазона, то есть A2. Для строки 3 Excel сам подставит $E3 — так подсвечивается вся строка целиком, а не одна ячейка.
Когда правило не нужно и достаточно один раз покрасить — крась, но делай это быстро. Читать ячейки по одной в цикле — самый верный способ подвесить Excel на большом файле. Вычитывай всю колонку в массив за одно обращение:
Sub HighlightBigRowsFast()
Dim ws As Worksheet
Dim values As Variant
Dim i As Long
Dim lastRow As Long
Set ws = ThisWorkbook.Worksheets("Данные")
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
' одно обращение к листу вместо тысяч: вся колонка E — в массив
values = ws.Range("E2:E" & lastRow).Value
Application.ScreenUpdating = False
For i = 1 To UBound(values, 1)
If IsNumeric(values(i, 1)) Then
If values(i, 1) >= 100000 Then
ws.Range("A" & (i + 1) & ":F" & (i + 1)).Interior.Color = RGB(226, 239, 218)
End If
End If
Next i
Application.ScreenUpdating = True
End Sub
Массив values — двумерный, даже если колонка одна: обращение всегда values(i, 1). Индексация начинается с 1, а не с 0. И заметь смещение i + 1: первая строка массива соответствует строке 2 листа, потому что строку 1 занимает шапка. Это классический источник ошибки «подсветилось на строку выше».
Как это работает
Все четыре операции объединяет одна идея: каждое обращение к ячейке — это разговор VBA с движком Excel через границу процессов, и он стоит дорого. Тысяча обращений в цикле — тысяча пересечений границы. Один Range.Value на весь диапазон — одно. Отсюда все приёмы этого урока: массивы вместо поячеечных циклов, AutoFilter вместо ручного перебора с проверкой условия, RemoveDuplicates вместо самописного словаря. Пусть тяжёлую работу делает движок Excel — он написан на C++ и быстрее любого твоего цикла.
Второй общий момент — состояние. Фильтр, режим копирования, условное форматирование, отключённая перерисовка — всё это макрос меняет глобально и обязан вернуть на место. Поэтому в коде столько парных строк: включили фильтр — сняли, скопировали — сбросили CutCopyMode, отключили ScreenUpdating — вернули.
Частые ошибки
| Симптом | Причина и лечение |
Ошибка «Object variable or With block variable not set» после Find | Не проверили результат на Nothing. Find без совпадений возвращает Nothing, а не пустую строку. |
| Find сегодня находит не то, что вчера | Не задали LookIn и LookAt явно — Excel подставил значения из последнего диалога Ctrl+F. Указывай их всегда. |
Цикл FindNext крутится вечно | Забыли запомнить firstAddress: поиск идёт по кругу и никогда не заканчивается сам. |
Ошибка 1004 на SpecialCells | Под фильтр не попало ни одной строки. Оберни в On Error Resume Next и проверь на Nothing. |
| Отфильтровалась не та колонка | Field считается от левого края диапазона фильтра, а не от колонки A листа. |
Удалились нужные строки, Ctrl+Z не помогает | Макрос очищает стек отмены Excel. Делай резервную копию листа до RemoveDuplicates. |
| Цвета в файле «врут» после правки данных | Использовали Interior.Color там, где нужно было живое правило FormatConditions. |
| Файл раздулся, Excel тормозит | Правила условного форматирования накопились: FormatConditions.Add без предварительного .Delete при каждом запуске плодит копии. |
Итоги
Findвозвращает объектRangeилиNothing— присваивай черезSetи всегда проверяй наNothing.- Параметры
LookInиLookAt«липкие» — задавай их явно при каждом вызове. - Обход всех совпадений —
FindNextплюс якорьfirstAddress, иначе вечный цикл. VBA не делает короткого замыкания: проверки наNothingвыноси отдельной строкой. AutoFilter+SpecialCells(xlCellTypeVisible)— штатный способ выгрузить строки по условию;Fieldсчитается от края диапазона.RemoveDuplicatesнеобратим:Ctrl+Zпосле макроса не работает. Сначала копия листа.FormatConditions— живое правило, пересчитывается само;Interior.Color— статичная заливка, устаревает вместе с данными.- Читай данные в массив (
Variant) одним обращением — это главный ускоритель любого макроса.