Парсинг сайтов на Python: как устроен веб и HTML для скрапера
Первый урок раздела: раз разбираем страницы — сначала поймём, из чего они состоят. Теги, атрибуты, DOM-дерево, селекторы и DevTools за один вечер.
Редакция Питоники
Вы копируете цены конкурентов из каталога в таблицу. Четыреста карточек: Ctrl+C, Ctrl+V, скролл, снова Ctrl+C. Через час глаза болят, в таблице дважды промахнулись по строке, а каталог тем временем обновился. Программа-парсер делает ту же работу за двадцать секунд и не устаёт никогда. Но прежде чем её писать, нужно понять, как устроена страница, из которой мы будем забирать данные. Этим и займёмся.
Что такое парсинг сайтов простыми словами?
Парсинг (по-английски web scraping, он же скрейпинг) — это когда программа открывает веб-страницу и извлекает из неё данные: цены, заголовки, контакты, таблицы. Парсером называют и саму такую программу. Не путайте с краулингом: краулер (его ещё называют пауком или spider) ходит по ссылкам и обходит страницы одну за другой — так поисковые системы индексируют интернет. Парсер чаще приходит на конкретную страницу за конкретными данными. Краулинг в чистом виде нам понадобится позже, когда будем обходить каталог с пагинацией.
Что происходит, когда вы открываете сайт
Вы вводите адрес, и через полсекунды браузер показывает страницу. Между этими событиями случилось вот что: браузер отправил на сервер HTTP-запрос — обычно типа GET, что значит «дай мне документ по такому адресу». Сервер ответил: статус-код 200 (всё хорошо) и текст документа — HTML.
HTML — это обычный текст с разметкой: угловые скобки, теги, атрибуты. Никакой магии. Браузер читает этот текст, строит из него дерево элементов и рисует страницу. Парсер делает почти то же самое, только вместо рисования — вытаскивает нужные куски. Поэтому первый навык парсера — читать HTML глазами, а второй — находить в нём нужный элемент по признакам.
HTML за пятнадцать минут: теги и вложенность
Вот мини-документ, с которым мы будем работать весь урок, — страница кофейни с меню. Запустите код и посмотрите, что вытащил из неё парсер:
from bs4 import BeautifulSoup
# HTML страницы кофейни, встроенный прямо в код.
# В браузере мы учимся на таких строках: Pyodide не может ходить в интернет.
html = """
<html>
<head><title>Кофейня Зерно - меню</title></head>
<body>
<h1>Меню на сегодня</h1>
<div class="drink">
<h3>Капучино</h3>
<p class="price">180 руб.</p>
</div>
<div class="drink">
<h3>Латте</h3>
<p class="price">200 руб.</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.title.string) # текст из тега title
print(soup.h1.string) # текст из первого тега h1
Кофейня Зерно - меню Меню на сегодня
Каждая конструкция в угловых скобках — тег. Большинство тегов парные: <div> открывает, </div> закрывает, а всё внутри — содержимое. Бывают и одиночные теги вроде <img> (картинка) или <br> (перенос строки) — им закрывать нечего. Заголовок страницы лежит в <title>, всё видимое содержимое — в <body>, <h1> — главный заголовок, <div> — контейнер-блок, <p> — абзац.
Теги вкладываются друг в друга, как матрёшки: внутри <body> живут <h1> и два блока <div>, а внутри каждого блока — свой <h3> и своя цена. Эта вложенность — не украшение, а структура, на которой держится весь парсинг.
Атрибуты: ярлыки элементов
У тегов есть атрибуты — свойства, записанные внутри открывающего тега. В примере выше <div class="drink"> читается как «блок с классом drink». Именно по атрибутам парсер отличает нужный элемент от сотен соседних. Самые ходовые:
hrefу тега<a>— адрес ссылки, за ним и охотятся чаще всегоclass— класс элемента, главный ориентир для поискаid— уникальный идентификатор, встречается на странице один разsrcу<img>— адрес файла картинкиdata-...— пользовательские атрибуты, в них сайты нередко прячут цены и артикулы
Класс — это «почтовый индекс» элемента. Сайты почти всегда дают нужным блокам осмысленные классы: product, price, title, rating. Увидели в коде класс — знаете, за что хвататься. Проверим на навигационном меню:
from bs4 import BeautifulSoup
html = """
<html>
<body>
<nav class="menu">
<a href="/menu/">Меню</a>
<a href="/about/">О нас</a>
<a href="/cart/" class="cart-link">Корзина</a>
</nav>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
for a in soup.select("nav.menu a"): # все ссылки внутри меню
print(a.text, "-", a["href"]) # текст ссылки и её адрес
Меню - /menu/ О нас - /about/ Корзина - /cart/
Одна строка a["href"] — и у нас есть адрес ссылки, как будто мы по ней кликнули. Запись nav.menu a называется CSS-селектором, о нём чуть ниже. Подробный разбор всех способов поиска ждёт вас в уроке про find, find_all и селекторы.
Дерево документа: родители, дети и соседи
Вложенность превращает HTML в дерево. У каждого элемента есть родитель (parent), дети (children) и соседи (siblings). В нашем меню: каждый тег <a> — ребёнок тега <nav>, а три ссылки — соседи относительно друг друга. Программисты называют эту модель DOM (Document Object Model) — модель документа. Когда говорят «парсим DOM», имеют в виду ровно это дерево.
Зачем парсеру видеть дерево? Потому что BeautifulSoup умеет не только искать по всему документу, но и ходить по дереву: взять родителя найденного тега, перебрать детей, перескочить к соседнему элементу. Многие задачи решаются в два движения, если понимаешь, кто кому родственник. Посмотрим, как парсер представляет разметку:
from bs4 import BeautifulSoup
html = "<div class='drink'><h3>Капучино</h3><p class='price'>180 руб.</p></div>"
soup = BeautifulSoup(html, "html.parser")
print(soup.prettify()) # тот же HTML, но с отступами
<div class="drink"> <h3> Капучино </h3> <p class="price"> 180 руб. </p> </div>
Метод prettify() печатает каждый тег с новой строки, а вложенные — с отступом: сразу видно, кто в кого вложен. Для маленьких фрагментов это удобный способ убедиться, что парсер понял разметку так же, как вы.
CSS-селекторы: точный адрес элемента
CSS-селектор — это правило, по которому находят элементы. Язык родился в CSS для оформления стилей, но им же пользуются парсеры, браузерный JavaScript и DevTools. Учитесь один раз — работает везде. Базовые шаблоны:
div— все элементы div на странице.price— все элементы с классом price (точка означает класс)div.card— элементы div, у которых есть класс card.card .title— элемент с классом title внутри любого элемента с классом card.card > .title— то же, но title обязан быть прямым ребёнком carda[href]— все ссылки, у которых задан атрибут href
from bs4 import BeautifulSoup
html = """
<html>
<body>
<h1>Меню на сегодня</h1>
<div class="drink">
<h3>Капучино</h3>
<p class="price">180 руб.</p>
</div>
<div class="drink">
<h3>Латте</h3>
<p class="price">200 руб.</p>
</div>
<div class="food">
<h3>Круассан</h3>
<p class="price">120 руб.</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
for tag in soup.select(".price"): # все цены: напитки и еда
print(tag.text)
print("Всего цен:", len(soup.select(".price")))
for h3 in soup.select("div.drink > h3"): # только названия напитков
print(h3.text)
180 руб. 200 руб. 120 руб. Всего цен: 3 Капучино Латте
Смотрите, какая разница: селектор .price нашёл три цены, а div.drink > h3 — только два названия напитков, потому что мы потребовали, чтобы h3 был прямым ребёнком блока с классом drink. Круассан из блока food в выборку не попал. Управляя одним символом >, вы меняете глубину поиска — это и есть сила селекторов.
Класс, id и осмысленные имена
Классов у элемента может быть несколько: class="product out-of-stock" — карточка товара, которой нет на складе. Поиск по id пишут через решётку: #header. Личный совет из практики: я почти всегда ищу по классам, а не по id. id часто генерируются автоматически (product-8347, root-1x9f) и меняются от релиза к релизу, а семантические классы вроде price живут годами.
from bs4 import BeautifulSoup
html = """
<div class="product out-of-stock">
<h3>Мышь Logitech MX Master 3S</h3>
<p class="price">9 990 руб.</p>
</div>
<div class="product">
<h3>Клавиатура Keychron K2</h3>
<p class="price">7 490 руб.</p>
</div>
"""
soup = BeautifulSoup(html, "html.parser")
# классы в селекторе склеиваются точкой: нужен и product, и out-of-stock
for card in soup.select("div.product.out-of-stock"):
print("Нет на складе:", card.h3.text)
print("Всего карточек:", len(soup.select("div.product")))
Нет на складе: Мышь Logitech MX Master 3S Всего карточек: 2
DevTools: как подсмотреть разметку настоящего сайта
В уроках HTML встроен в код, чтобы всё запускалось прямо здесь. На настоящем сайте структуру подсматривают в инструментах разработчика — DevTools. Порядок действий такой:
- Откройте любой сайт и нажмите F12 (или правая кнопка мыши — «Посмотреть код»)
- Вкладка Elements — там живёт HTML текущей страницы в виде дерева
- Включите значок курсора и кликните по нужному элементу на странице — DevTools прыгнет к его разметке
- Правый клик по подсвеченному тегу — Copy — Copy selector: браузер сам составит селектор
- Проверьте селектор в консоли: document.querySelectorAll('.price') покажет, сколько элементов нашлось
Один нюанс, о который спотыкаются все: DevTools показывает DOM после работы JavaScript, а библиотека requests (её возьмём в дело в следующем уроке) отдаёт исходный HTML, каким его прислал сервер. На статических сайтах это одно и то же. На динамических — нет, и эту ловушку мы разберём отдельно, когда вы уже будете уверенно владеть поиском.
Мини-словарь парсера
Соберём термины урока в одну таблицу — она останется в голове быстрее, если пробежать её глазами перед практикой:
| Термин | Что означает |
|---|---|
| Парсер | программа, которая извлекает данные из документа |
| Скрейпинг (web scraping) | автоматический сбор данных с сайтов |
| Краулинг | автоматический обход страниц по ссылкам |
| HTML | текстовый язык разметки веб-страниц |
| Тег | элемент разметки в угловых скобках, например div или p |
| Атрибут | свойство тега: class, id, href, src |
| DOM | дерево документа: у каждого элемента есть родитель и дети |
| CSS-селектор | правило поиска элементов: .price, div.card > h3 |
| GET-запрос | запрос «отдай документ» по протоколу HTTP |
Практика: собираем меню кофейни
Складываем всё изученное в первый рабочий скрипт. Задача звучит как реальная: вывести каждое название напитка вместе с ценой — именно это делают парсеры, следящие за ценами кофеен и магазинов. Запустите и посмотрите:
from bs4 import BeautifulSoup
html = """
<html>
<body>
<h1>Меню на сегодня</h1>
<div class="drink">
<h3>Капучино</h3>
<p class="price">180 руб.</p>
</div>
<div class="drink">
<h3>Латте</h3>
<p class="price">200 руб.</p>
</div>
<div class="food">
<h3>Круассан</h3>
<p class="price">120 руб.</p>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "html.parser")
cards = soup.select(".drink")
print("Напитков в меню:", len(cards))
for card in cards:
name = card.select_one("h3").text # первый h3 внутри карточки
price = card.select_one(".price").text # первый элемент с классом price
print(name, "-", price)
Напитков в меню: 2 Капучино - 180 руб. Латте - 200 руб.
Обратите внимание на порядок: сначала len(cards) — проверяем, что карточек столько, сколько ждём, и только потом цикл. В реальном парсере эта привычка спасает от тихих ошибок: селектор с опечаткой найдёт ноль элементов, цикл молча не выполнится, и вы будете гадать, куда пропали данные. select_one внутри карточки ищет уже не по всей странице, а только внутри неё — это ключевая идея парсинга каталогов: нашли карточку, а потом добираем детали внутри неё.
Пока мы работали со строкой HTML, которую сами и написали. А как страница попадает в программу на настоящем сайте? Для этого есть библиотека requests — вот как выглядит скачивание в локальном Python:
import requests
response = requests.get("https://example.com")
print(response.status_code) # 200 - сервер ответил успешно
print(response.text[:64]) # начало HTML-кода страницы
200
<!doctype html>
<html>
<head>
<title>Example Domain</title>
Что дальше
Проверьте себя: тег, атрибут, дерево DOM, селектор, DevTools — можете объяснить каждый термин одним предложением? Если да, фундамент готов. Ориентироваться в остальных уроках помогает самоучитель BeautifulSoup — десять уроков от первого HTML до парсера каталога. Мы намеренно не скачивали страницы из интернета: браузерная песочница не может обращаться к чужим сайтам, зато техника разбора разметки здесь та же, что и на живых страницах.
В следующем уроке напишем первый настоящий парсер: скачаем страницу библиотекой requests, передадим её BeautifulSoup и достанем заголовок со ссылками — а заодно выясним, почему код скачивания из браузера не запускается и как с этим жить. После него откроем главный инструментарий: find, find_all и все виды поиска.
Сначала предскажи ответ в голове — это главный навык программиста.
from bs4 import BeautifulSoup
html = '<div><p class="price">100</p><p class="price">200</p></div>'
soup = BeautifulSoup(html, "html.parser")
print(len(soup.select(".price")))
from bs4 import BeautifulSoup
html = '<a href="/shop/">Каталог</a>'
soup = BeautifulSoup(html, "html.parser")
a = soup.find("a")
print(a["href"])
1. Что получает программа в ответ на успешный GET-запрос к сайту?
2. Какой атрибут чаще всего служит главным ориентиром при поиске элемента?
3. Что найдёт селектор div.card > h3?
4. Что делает метод soup.prettify()?
5. Чем парсинг отличается от краулинга?
Перед вами фрагмент раздела «Чай» интернет-магазина. Выведите каждый товар отдельной строкой вида «Пуэр - 640 руб.» — название, пробел, дефис, цена. Работайте селекторами: пройдите по карточкам div.item и возьмите из каждой название и цену.
Что такое парсинг сайтов и законно ли это?
Парсинг — автоматическое извлечение данных со страниц: программа скачивает HTML и достаёт из него цены, заголовки, таблицы. Сбор публичных данных сам по себе законен, но есть правила: уважать robots.txt сайта, не создавать чрезмерную нагрузку, не собирать персональные данные и не обходить авторизацию. Этике и границам посвящён отдельный урок раздела.
Нужно ли знать HTML и CSS для парсинга на Python?
Нужна база: теги, атрибуты, классы, вложенность и CSS-селекторы — это примерно один вечер. Именно этот минимум даёт первый урок раздела, дальше всё закрепляется на практике: без понимания разметки невозможно писать селекторы, которые находят нужные элементы.
Чем парсинг отличается от краулинга?
Парсинг — извлечение данных с конкретной страницы: пришли, забрали цены, ушли. Краулинг — обход множества страниц по ссылкам, как это делают поисковые роботы. В реальных задачах они ходят в паре: краулер находит страницы каталога, парсер вытаскивает из каждой данные.
Как посмотреть HTML-код интересующего элемента на сайте?
Нажмите F12, чтобы открыть DevTools, включите инструмент выбора элемента и кликните по нужному месту страницы — браузер подсветит тег во вкладке Elements. Правый клик по тегу, затем Copy — Copy selector даст готовый CSS-селектор, который можно сразу проверять в консоли.
Понравился урок? Сошлитесь на него
«Эта вложенность — не украшение, а структура, на которой держится весь парсинг.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
BeautifulSoup / Scrapy · Урок 2
Первый парсер: requests и BeautifulSoup — скачиваем и разбираем страницу
Собираем первого рабочего парсера по схеме «скачать — разобрать — достать»: requests скачивает страницу, BeautifulSoup превращает её в объект, из которого данные достаются в одну строку.
BeautifulSoup / Scrapy · Урок 3
Поиск элементов: find, find_all и CSS-селекторы в BeautifulSoup
Два метода find и find_all плюс семейство select закрывают большинство задач поиска. Разбираем их отличия, ловушки с class_ и attrs и применяем к каталогу товаров.
BeautifulSoup / Scrapy · Урок 6
Многостраничный парсинг: пагинация и обход каталога
Данные редко лежат на одной странице. Учим парсер ходить по пагинации, заходить в карточки товаров, выдерживать паузы и переживать обрывы связи.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 1
Что такое JSON и зачем он Python: первый json.dumps
Первое превращение словаря в json-строку одной командой: import json, json.dumps и честный взгляд на кракозябры в выводе — всё исполняется прямо на странице.
что такое jsonjson для начинающих
Flask · Урок 1
Что такое Flask: знакомство с микрофреймворком
Первый урок самоучителя Flask: как устроен веб, зачем нужен WSGI, чем микрофреймворк отличается от Django — и как за семь строк получить работающий сайт.
что такое flaskflask для начинающих
BeautifulSoup / Scrapy · Урок 9
Scrapy для начинающих: пауки, проекты и почему он быстрее
BeautifulSoup разобрал страницу — но не умеет ходить по сайту, ставить паузы и писать CSV. Scrapy делает это сам: собираем проект, разбираем паука, yield и pipeline.
scrapy для начинающихscrapy vs beautifulsoup
Проверьте знания по BeautifulSoup / Scrapy
В челлендже — 20 задач по BeautifulSoup / Scrapy, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по BeautifulSoup / Scrapy: 20 задач