Технологии и защита 8 мин чтения

CSS-селекторы для парсинга: обзор конструкций

Шпаргалка по CSS-селекторам для извлечения данных: классы, атрибуты, псевдоклассы, комбинаторы и типичные ошибки при выборе элементов.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 2 февраля 2025

В прошлом материале — «Парсер DOM» — мы разобрали, как браузер и библиотеки превращают HTML в дерево узлов и как по этому дереву можно ходить программно. Но обходить DOM «руками», переходя от родителя к потомку, утомительно и хрупко. На практике в парсинге сайтов нужный узел почти всегда адресуют одним коротким выражением — CSS-селектором.

CSS-селекторы изначально придуманы для вёрстки: ими описывают, к каким элементам применить стиль. Но тот же синтаксис отлично подходит, чтобы находить элементы. Поэтому почти все парсеры — от BeautifulSoup до Scrapy и Playwright — поддерживают выборку по CSS. Разберём конструкции по порядку.

Зачем CSS-селекторы в парсинге

Селектор — это декларативный способ сказать «дай мне все элементы, которые выглядят вот так». Вместо десятка строк обхода дерева вы пишете одну строку, а движок сам спускается по DOM и возвращает совпадения. Это:

  • компактно — один селектор вместо цикла;
  • читаемо — выражение похоже на описание места элемента на странице;
  • переносимо — тот же селектор работает в Python, JS, в консоли браузера (document.querySelectorAll).

Главный навык парсера — посмотреть на структуру страницы в DevTools и составить селектор, который попадёт точно в цель и при этом переживёт мелкие изменения вёрстки.

Базовые селекторы

Это атомы, из которых строится всё остальное.

Селектор Что выбирает Пример
* любой элемент *
div элементы по имени тега a, span, tr
.price элементы с классом price .product-card
#main элемент с id="main" #catalog

Класс и тег можно склеивать без пробела — это логическое «И»: a.button означает «ссылка, у которой есть класс button». А div.card.featureddiv сразу с двумя классами.

css
span.price.current     /* <span class="price current"> */
li#first-item          /* <li id="first-item"> */

Группировка

Запятая объединяет несколько селекторов в один список — вернутся элементы, подходящие под любой из них:

css
h1, h2, h3             /* все заголовки этих трёх уровней */
.price-new, .price-old /* и новая, и старая цена */

Удобно, когда товар на одних карточках имеет класс price-new, а на других — price-old, и нужно собрать оба варианта одним запросом.

Комбинаторы: связи между элементами

Комбинаторы описывают, как элементы расположены друг относительно друга в дереве. Это самая важная часть для устойчивого парсинга.

Потомок (пробел)

A B — элемент B, который лежит где-то внутри A, на любой глубине.

css
.product-card .price   /* .price в любом месте внутри карточки */

Прямой потомок (>)

A > BB является непосредственным ребёнком A. Глубже не спускается.

css
ul.menu > li           /* только пункты верхнего уровня меню, без вложенных */

Разница принципиальна: .menu li захватит и пункты подменю, а .menu > li — нет.

Соседний (+)

A + B — элемент B, который идёт сразу за A и имеет того же родителя.

css
h2 + p                 /* первый абзац сразу после заголовка */
.label + .value        /* значение, стоящее вплотную после подписи */

Родственный (~)

A ~ B — все B, идущие после A у общего родителя (не обязательно вплотную).

css
.in-stock ~ .price     /* все цены, идущие после метки «в наличии» */

Селекторы атрибутов

Часто у элемента нет удобного класса, зато есть характерный атрибут. Селекторы атрибутов выручают, когда классы динамические или зашумлённые.

Конструкция Значение
[data-id] есть атрибут data-id (с любым значением)
[type="submit"] точное совпадение значения
[class~="price"] значение содержит слово price среди пробельно-разделённых
[href^="https"] значение начинается с https
[href$=".pdf"] значение заканчивается на .pdf
[href*="product"] значение содержит подстроку product
[lang|="en"] значение равно en или начинается с en-
[type="text" i] флаг i — сравнение без учёта регистра
css
a[href^="/catalog/"]           /* ссылки внутри каталога */
img[src$=".webp"]              /* картинки в формате webp */
div[data-testid*="product"]    /* контейнеры с признаком товара в data-атрибуте */
input[name="csrf"]             /* скрытое поле токена */

Атрибутные селекторы особенно ценны в мониторинге цен интернет-магазинов: крупные площадки часто отдают данные в data-*-атрибутах (data-price, data-sku, data-currency), и зацепиться за них надёжнее, чем за классы, которые меняются при каждом релизе фронтенда.

Псевдоклассы: выбор по позиции и состоянию

Псевдоклассы фильтруют элементы по их месту среди соседей или по дополнительным условиям.

Позиционные

css
li:first-child         /* первый ребёнок */
li:last-child          /* последний ребёнок */
tr:nth-child(2)        /* вторая строка */
tr:nth-child(odd)      /* нечётные строки */
tr:nth-child(even)     /* чётные строки */
tr:nth-child(3n)       /* каждая третья */
li:nth-last-child(1)   /* первый с конца */

Формула an+b в :nth-child() гибкая: nth-child(2n+1) — нечётные, nth-child(n+3) — все начиная с третьего.

По типу тега

:nth-child считает всех соседей подряд, а :nth-of-type — только элементы того же тега:

css
p:first-of-type        /* первый абзац среди соседей */
img:nth-of-type(2)     /* вторая картинка по счёту */
span:last-of-type      /* последний span */

Это спасает, когда нужный <p> идёт вторым абзацем, но перед ним вклинились теги другого типа.

Отрицание и состояние

css
input:not([type="hidden"])   /* все поля, кроме скрытых */
li:not(.sold-out)            /* товары не в статусе «продано» */
a:not([rel="nofollow"])      /* ссылки без nofollow */

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

Обратите внимание: псевдоэлементы (::before, ::after, ::first-line) в парсинге не используются — они описывают сгенерированный стилями контент, которого нет в исходном HTML.

Собираем сложный селектор

Реальные выражения комбинируют всё вышеперемногое. Пример — достать актуальную цену из карточки товара в каталоге:

css
div.product-card a[href^="/product/"] span.price:not(.price--old)

Читается так: внутри карточки товара, у ссылки на страницу товара, взять span с классом price, который не является старой (зачёркнутой) ценой.

Инструменты: где это работает

CSS-селекторы поддерживает почти весь стек парсинга.

Python — BeautifulSoup (через библиотеку soupsieve):

python
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")

# .select() возвращает список, .select_one() — первый элемент
cards = soup.select("div.product-card")
for card in cards:
    title = card.select_one("h3.title").get_text(strip=True)
    price = card.select_one("span.price:not(.price--old)").get_text(strip=True)
    print(title, price)

Python — parsel / Scrapy (обёртка над lxml, быстрая):

python
from parsel import Selector

sel = Selector(text=html)
prices = sel.css("span.price::text").getall()   # ::text — особое расширение parsel
links = sel.css("a.product-link::attr(href)").getall()

Здесь ::text и ::attr(name) — нестандартные псевдоэлементы Scrapy/parsel: они сразу достают текст или атрибут, минуя отдельный вызов.

JavaScript / браузер — нативный API, тот же синтаксис:

javascript
const cards = document.querySelectorAll("div.product-card");
const price = document.querySelector("span.price:not(.price--old)")?.textContent.trim();

Playwright / Selenium — для динамических страниц, где данные подгружаются скриптами:

python
# Playwright
price = page.locator("div.product-card span.price").first.inner_text()

То есть один раз освоив селекторы, вы используете их и в лёгких HTTP-парсерах, и в headless-браузерах.

Применение в мониторинге цен

В типичной задаче мониторинга цен интернет-магазинов сценарий такой:

  1. Список товаров каталога. Селектор вида div.catalog__item находит все карточки на странице выдачи.
  2. Ссылка на товар. Из каждой карточки a[href^="/product/"]::attr(href) достаёт URL.
  3. Цена и наличие. На странице товара [data-price]::attr(data-price) или span.price--current даёт цену, а .availability:not(.out-of-stock) подтверждает, что товар в продаже.
  4. Старая цена и скидка. span.price--old ловит зачёркнутую цену, чтобы посчитать размер скидки.

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

Частые ошибки и советы

  • Не цепляйтесь за «мусорные» классы. Классы вроде .css-1ab2c3 генерируются сборщиками и меняются при каждом деплое. Предпочитайте data-*-атрибуты, осмысленные id или структурные комбинаторы.
  • Используйте > там, где важна структура. Это отсекает случайные совпадения во вложенных блоках.
  • Проверяйте селектор в DevTools. Команда $$("ваш селектор") в консоли браузера сразу показывает, сколько элементов нашлось.
  • Закладывайте варианты. Через запятую перечисляйте несколько возможных классов цены — это повышает устойчивость к A/B-тестам магазина.
  • Помните о лимитах CSS. Селекторы не умеют «подняться к родителю» по содержимому или фильтровать по тексту узла. Где CSS не хватает, в дело идут XPath или регулярные выражения — о них в следующем материале.

Итог

CSS-селекторы — основной инструмент адресации в DOM. Базовые селекторы (тег, класс, id) задают «что», комбинаторы (, >, +, ~) — «где», атрибутные селекторы и псевдоклассы добавляют точные условия. Вместе они покрывают подавляющее большинство задач парсинга сайтов, а грамотно составленный селектор делает парсер устойчивым к мелким правкам вёрстки — что критично, когда речь идёт о регулярном мониторинге цен интернет-магазинов.

В следующей статье разберём, как доставать данные из «сырого» текста с помощью регулярных выражений — там, где структуры DOM уже недостаточно.