В прошлом материале — «Парсер 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.featured — div сразу с двумя классами.
span.price.current /* <span class="price current"> */
li#first-item /* <li id="first-item"> */
Группировка
Запятая объединяет несколько селекторов в один список — вернутся элементы, подходящие под любой из них:
h1, h2, h3 /* все заголовки этих трёх уровней */
.price-new, .price-old /* и новая, и старая цена */
Удобно, когда товар на одних карточках имеет класс price-new, а на других — price-old, и нужно собрать оба варианта одним запросом.
Комбинаторы: связи между элементами
Комбинаторы описывают, как элементы расположены друг относительно друга в дереве. Это самая важная часть для устойчивого парсинга.
Потомок (пробел)
A B — элемент B, который лежит где-то внутри A, на любой глубине.
.product-card .price /* .price в любом месте внутри карточки */
Прямой потомок (>)
A > B — B является непосредственным ребёнком A. Глубже не спускается.
ul.menu > li /* только пункты верхнего уровня меню, без вложенных */
Разница принципиальна: .menu li захватит и пункты подменю, а .menu > li — нет.
Соседний (+)
A + B — элемент B, который идёт сразу за A и имеет того же родителя.
h2 + p /* первый абзац сразу после заголовка */
.label + .value /* значение, стоящее вплотную после подписи */
Родственный (~)
A ~ B — все B, идущие после A у общего родителя (не обязательно вплотную).
.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 — сравнение без учёта регистра |
a[href^="/catalog/"] /* ссылки внутри каталога */
img[src$=".webp"] /* картинки в формате webp */
div[data-testid*="product"] /* контейнеры с признаком товара в data-атрибуте */
input[name="csrf"] /* скрытое поле токена */
Атрибутные селекторы особенно ценны в мониторинге цен интернет-магазинов: крупные площадки часто отдают данные в data-*-атрибутах (data-price, data-sku, data-currency), и зацепиться за них надёжнее, чем за классы, которые меняются при каждом релизе фронтенда.
Псевдоклассы: выбор по позиции и состоянию
Псевдоклассы фильтруют элементы по их месту среди соседей или по дополнительным условиям.
Позиционные
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 — только элементы того же тега:
p:first-of-type /* первый абзац среди соседей */
img:nth-of-type(2) /* вторая картинка по счёту */
span:last-of-type /* последний span */
Это спасает, когда нужный <p> идёт вторым абзацем, но перед ним вклинились теги другого типа.
Отрицание и состояние
input:not([type="hidden"]) /* все поля, кроме скрытых */
li:not(.sold-out) /* товары не в статусе «продано» */
a:not([rel="nofollow"]) /* ссылки без nofollow */
:not() принимает внутрь другой селектор и инвертирует его. Поддерживаются также :empty (пустые элементы), :checked, :disabled и другие — но в статическом парсинге чаще всего в ход идут именно позиционные псевдоклассы и :not().
Обратите внимание: псевдоэлементы (
::before,::after,::first-line) в парсинге не используются — они описывают сгенерированный стилями контент, которого нет в исходном HTML.
Собираем сложный селектор
Реальные выражения комбинируют всё вышеперемногое. Пример — достать актуальную цену из карточки товара в каталоге:
div.product-card a[href^="/product/"] span.price:not(.price--old)
Читается так: внутри карточки товара, у ссылки на страницу товара, взять span с классом price, который не является старой (зачёркнутой) ценой.
Инструменты: где это работает
CSS-селекторы поддерживает почти весь стек парсинга.
Python — BeautifulSoup (через библиотеку soupsieve):
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, быстрая):
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, тот же синтаксис:
const cards = document.querySelectorAll("div.product-card");
const price = document.querySelector("span.price:not(.price--old)")?.textContent.trim();
Playwright / Selenium — для динамических страниц, где данные подгружаются скриптами:
# Playwright
price = page.locator("div.product-card span.price").first.inner_text()
То есть один раз освоив селекторы, вы используете их и в лёгких HTTP-парсерах, и в headless-браузерах.
Применение в мониторинге цен
В типичной задаче мониторинга цен интернет-магазинов сценарий такой:
- Список товаров каталога. Селектор вида
div.catalog__itemнаходит все карточки на странице выдачи. - Ссылка на товар. Из каждой карточки
a[href^="/product/"]::attr(href)достаёт URL. - Цена и наличие. На странице товара
[data-price]::attr(data-price)илиspan.price--currentдаёт цену, а.availability:not(.out-of-stock)подтверждает, что товар в продаже. - Старая цена и скидка.
span.price--oldловит зачёркнутую цену, чтобы посчитать размер скидки.
Поскольку у конкурентов вёрстка разная, под каждую площадку пишется свой набор селекторов, а движок парсинга сайтов применяет их по расписанию и складывает результат в общую таблицу для сравнения.
Частые ошибки и советы
- Не цепляйтесь за «мусорные» классы. Классы вроде
.css-1ab2c3генерируются сборщиками и меняются при каждом деплое. Предпочитайтеdata-*-атрибуты, осмысленныеidили структурные комбинаторы. - Используйте
>там, где важна структура. Это отсекает случайные совпадения во вложенных блоках. - Проверяйте селектор в DevTools. Команда
$$("ваш селектор")в консоли браузера сразу показывает, сколько элементов нашлось. - Закладывайте варианты. Через запятую перечисляйте несколько возможных классов цены — это повышает устойчивость к A/B-тестам магазина.
- Помните о лимитах CSS. Селекторы не умеют «подняться к родителю» по содержимому или фильтровать по тексту узла. Где CSS не хватает, в дело идут XPath или регулярные выражения — о них в следующем материале.
Итог
CSS-селекторы — основной инструмент адресации в DOM. Базовые селекторы (тег, класс, id) задают «что», комбинаторы (, >, +, ~) — «где», атрибутные селекторы и псевдоклассы добавляют точные условия. Вместе они покрывают подавляющее большинство задач парсинга сайтов, а грамотно составленный селектор делает парсер устойчивым к мелким правкам вёрстки — что критично, когда речь идёт о регулярном мониторинге цен интернет-магазинов.
В следующей статье разберём, как доставать данные из «сырого» текста с помощью регулярных выражений — там, где структуры DOM уже недостаточно.