Парсинг и применение косметических фильтров: полное руководство

Косметический фильтр вида example.com##.banner-ad не блокирует сетевой запрос — он скрывает уже загруженный элемент страницы через CSS, поэтому рекламный блок исчезает визуально, хотя сам запрос к серверу был выполнен. Именно поэтому парсинг таких правил принципиально отличается от обработки сетевых фильтров: здесь важны доменная привязка, синтаксис CSS-селекторов и специальные маркеры вроде ##, #@# и #?#.

В этой статье разберём, как устроены косметические фильтры, как их корректно парсить из списков EasyList и AdGuard, как применять их вручную в uBlock Origin и что учитывать при написании собственного парсера. Материал подойдёт и пользователям, которые хотят точечно скрыть назойливый элемент, и разработчикам, встраивающим фильтрацию в свой инструмент.

Что такое косметические фильтры и чем они отличаются от сетевых

Косметический фильтр — это правило, которое изменяет отображение страницы после её загрузки: скрывает элементы, сбрасывает стили или внедряет собственный CSS. В отличие от сетевого фильтра, который отменяет сам HTTP-запрос к рекламному серверу, косметический фильтр работает на уровне DOM-дерева.

Типичные задачи, которые решают косметические фильтры:

  • 🧹 скрытие рекламных баннеров, загруженных с того же домена, что и контент (сетевой фильтр их не отличит);
  • ✂️ удаление всплывающих окон о подписке, cookie-баннеров и плашек «установите наше приложение»;
  • 🔓 разблокировка прокрутки страницы, которую сайт отключает через overflow: hidden;
  • 🎨 исправление «дыр» в вёрстке после срабатывания сетевых фильтров.

Базовый синтаксис выглядит так: домен##селектор. Например, правило site.ru##.promo-banner скроет элемент с классом promo-banner только на сайте site.ru. Если домен не указан (##.promo-banner), правило считается общим и применяется на всех сайтах — такие правила блокировщики обрабатывают отдельно и осторожнее, чтобы не замедлять каждую страницу.

Синтаксис и типы косметических правил

Перед парсингом нужно понимать, какие маркеры встречаются в списках фильтров. Ошибка в определении типа правила — самая частая причина, почему самописный парсер «съедает» валидные фильтры или применяет исключения как блокирующие правила.

МаркерНазначениеПример
##Скрытие элемента по CSS-селекторуsite.ru##.ad
#@#Исключение: отменяет скрытиеsite.ru#@#.ad
#?#Расширенный селектор ( procedural, :has() и т.п.)site.ru#?#div:-abp-has(.ad)
#$#Внедрение произвольного CSSsite.ru#$#body { overflow: auto !important; }
#%#Внедрение скрипта (scriptlet, синтаксис AdGuard)site.ru#%#//scriptlet(...)

Обратите внимание на правила-исключения с маркером #@#: они существуют, потому что общие фильтры иногда скрывают полезный контент. При парсинге их нельзя отбрасывать — иначе ваш инструмент будет ломать сайты там, где авторы списков уже предусмотрели отмену.

⚠️ Внимание: правила вида #%# и #$# исполняют произвольный код и стили на странице. Применяйте такие фильтры только из доверенных списков (официальные EasyList, AdGuard, uAssets) — вредоносное правило из непроверенного источника способно воровать данные со страницы.

Как парсить списки косметических фильтров

Парсинг списка фильтров — это построчная обработка текстового файла с классификацией каждой строки. Список вроде EasyList содержит тысячи строк, и среди них перемешаны комментарии, сетевые правила и косметические фильтры всех типов.

Алгоритм разбора одной строки выглядит так:

  • 📄 пропустить пустые строки и комментарии — они начинаются с ! или маркера заголовка [Adblock Plus;
  • 🔍 проверить наличие маркеров ##, #@#, #?#, #$# — если маркер найден, строка является косметическим правилом;
  • 🌐 отделить часть до маркера (список доменов через запятую) от части после него (селектор или CSS-код);
  • ↩️ учесть инверсию доменов: запись ~foo.com##.ad означает «применять везде, кроме foo.com»;
  • ✅ сохранить результат в структуру вида «домены → селекторы» для быстрого поиска при применении.

Пример простейшего разбора на Python:

COSMETIC_MARKERS = ["#@#", "#?#", "#$#", "#%#", "##"]

def parse_line(line):

line = line.strip()

if not line or line.startswith("!") or line.startswith("["):

return None

for marker in COSMETIC_MARKERS:

if marker in line:

domains, selector = line.split(marker, 1)

return {

"type": marker,

"domains": [d for d in domains.split(",") if d],

"selector": selector,

"is_exception": marker == "#@#",

}

return None # сетевое правило или мусор

Важная деталь: маркер #@# нужно проверять раньше, чем ##, иначе простой поиск подстроки ## не сработает на исключениях некорректно — хотя формально #@# не содержит ##, при посимвольном разборе легко ошибиться. Надёжнее искать маркеры по точному совпадению в порядке убывания длины.

📊 Для чего вы в основном используете косметические фильтры?
Скрытие рекламы, которую не берут сетевые фильтры
Удаление cookie-баннеров и попапов
Разработка собственного парсера/инструмента
Просто изучаю тему

Применение косметических фильтров в uBlock Origin и AdGuard

Если вы не пишете собственный инструмент, а хотите применять готовые или собственные правила, проще всего использовать вкладку пользовательских фильтров. В uBlock Origin откройте панель управления (иконка расширения → значок шестерёнок) и перейдите на вкладку Мои фильтры. В AdGuard аналогичный раздел называется Пользовательские правила.

Порядок добавления собственного правила:

  1. Откройте целевой сайт и через инспектор браузера (F12) определите CSS-селектор нежелательного элемента.
  2. Добавьте в пользовательские фильтры строку вида site.ru##.selector.
  3. Сохраните изменения и обновите страницу — элемент должен исчезнуть.
  4. Если элемент появляется динамически, проверьте, не требуется ли расширенный селектор.

☑️ Проверка собственного косметического фильтра

Выполнено: 0 / 5

У uBlock Origin есть и визуальный инструмент — режим зоркого глаза (element zapper) и пипетка (element picker) в выпадающей панели расширения. Пипетка позволяет кликнуть по элементу на странице, автоматически сгенерировать селектор и сохранить его как косметический фильтр без ручного написания кода. Это безопасный способ: сгенерированное правило можно отредактировать перед сохранением.

Применение фильтров в собственном инструменте

Разработчику, который парсит списки для своего браузерного расширения, скрапера или headless-браузера, нужно решить две задачи: выбрать правила, релевантные текущему домену, и внедрить их на страницу. Первое делается через индекс «домен → правила», второе — через внедрение CSS.

Классический способ применения — сгенерировать таблицу стилей, где каждому селектору назначается display: none !important, и вставить её в документ. В Puppeteer или Playwright это делается через добавление тега style на страницу:

const css = selectors

.map(s => `${s} { display: none !important; }`)

.join("\n");

await page.addStyleTag({ content: css });

Здесь есть тонкость: многие современные сайты генерируют контент динамически, и элемент появляется уже после внедрения стилей. CSS-способ хорош тем, что правило display: none продолжает действовать на новые узлы автоматически — в отличие от одноразового удаления элементов через JavaScript. Поэтому предпочтительнее внедрять стили, а не удалять узлы из DOM вручную.

Для расширенных селекторов (:has(), :-abp-has и прочих procedural-форм) простого CSS недостаточно: часть из них поддерживается современными браузерами нативно (например, :has()), а проприетарные конструкции придётся транслировать или обрабатывать скриптом через MutationObserver. Если ваша задача — базовая фильтрация, разумно ограничиться стандартными селекторами и честно пропускать неподдерживаемые правила, а не пытаться исполнить их некорректно.

Почему глобальные правила (без домена) обрабатывают отдельно

Правила вида ##.ad без указания домена потенциально применяются на каждой странице. Блокировщики выделяют их в отдельный реестр и внедряют выборочно: uBlock Origin, например, опрашивает страницу на наличие классов и id из глобального списка и применяет только совпавшие. Это снижает нагрузку — внедрять тысячи глобальных селекторов на каждый сайт было бы слишком дорого.

Типичные ошибки при парсинге и применении

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

Вторая проблема — неверная обработка доменных привязок. Правило sub.site.ru##.ad не должно срабатывать на site.ru, а вот правило для site.ru обычно действует и на поддомены — точное поведение зависит от реализации блокировщика, поэтому сверяйтесь с документацией целевого движка (Adblock Plus, uBlock Origin и AdGuard имеют различия в деталях синтаксиса).

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

Третья ошибка — попытка парсить списки регулярными выражениями «на коленке» без учёта экранирования. В селекторах встречаются символы # внутри атрибутов и escaped-последовательности, и наивный split по маркеру может разрезать селектор в неправильном месте. Ищите первое вхождение маркера и помните, что слева от него может быть пустая строка (глобальное правило).

Производительность и оптимизация

Полные списки содержат десятки тысяч правил, и применять их «в лоб» — внедряя все селекторы на каждую страницу — нельзя: это замедлит рендеринг и увеличит потребление памяти. Рабочий подход — индексация по доменам.

Оптимальная схема выглядит так: при загрузке списка строится словарь, где ключ — домен (или его регистрируемая часть), а значение — массив селекторов. При посещении страницы извлекаются только правила для текущего домена и его родительских зон, плюс выборочно — глобальные правила. Именно так устроены зрелые блокировщики, и для собственного инструмента эта модель тоже подходит.

Дополнительно стоит кешировать скомпилированный CSS для часто посещаемых доменов и дедуплицировать селекторы — в разных списках одни и те же правила встречаются многократно. Дедупликация по строке селектора перед генерацией CSS заметно сокращает итоговую таблицу стилей.

Часто задаваемые вопросы

Чем косметический фильтр отличается от сетевого?

Сетевой фильтр блокирует сам HTTP-запрос к рекламному серверу — ресурс не загружается вообще. Косметический фильтр работает после загрузки страницы: он скрывает или стилизует элементы через CSS. Сетевые правила эффективнее по ресурсам, но не справляются с рекламой, встроенной в основной контент сайта, — там нужны косметические.

Почему моё правило ##.selector не работает на конкретном сайте?

Возможные причины: селектор не совпадает с реальной разметкой (проверьте его в DevTools), элемент создаётся динамически со случайными классами, либо существует исключение #@# в одном из подключённых списков. Также убедитесь, что домен в правиле указан без опечаток и соответствует фактическому хосту страницы.

Можно ли применять косметические фильтры без расширений?

Да. Варианты: пользовательские стили через расширения типа Stylus, внедрение CSS в автоматизированном браузере (Puppeteer, Playwright) через addStyleTag, либо собственный парсер, генерирующий таблицу стилей из списков фильтров. Все три подхода используют один принцип — селекторам назначается display: none.

Безопасно ли подключать сторонние списки фильтров?

Списки из проверенных источников (EasyList, официальные списки AdGuard и uBlock Origin) поддерживаются сообществом и проверяются. Неизвестные списки опасны правилами вида #%# и #$#, которые исполняют код на странице. Перед подключением незнакомого списка просмотрите его на наличие таких маркеров.

Как обрабатывать правила с procedural-селекторами в своём парсере?

Самое надёжное решение для базового инструмента — распознавать маркер #?# и пропускать такие правила с записью в лог. Часть расширенных конструкций (например, :has()) поддерживается современными браузерами нативно и может применяться как обычный CSS, а проприетарные операторы конкретных движков требуют отдельной трансляции либо обработки через MutationObserver.