Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 1 из 10 Начальный 35 мин 100 XP

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

Первый урок раздела: раз разбираем страницы — сначала поймём, из чего они состоят. Теги, атрибуты, DOM-дерево, селекторы и DevTools за один вечер.

Редакция Питоники

Вы копируете цены конкурентов из каталога в таблицу. Четыреста карточек: Ctrl+C, Ctrl+V, скролл, снова Ctrl+C. Через час глаза болят, в таблице дважды промахнулись по строке, а каталог тем временем обновился. Программа-парсер делает ту же работу за двадцать секунд и не устаёт никогда. Но прежде чем её писать, нужно понять, как устроена страница, из которой мы будем забирать данные. Этим и займёмся.

Что такое парсинг сайтов простыми словами?

Парсинг (по-английски web scraping, он же скрейпинг) — это когда программа открывает веб-страницу и извлекает из неё данные: цены, заголовки, контакты, таблицы. Парсером называют и саму такую программу. Не путайте с краулингом: краулер (его ещё называют пауком или spider) ходит по ссылкам и обходит страницы одну за другой — так поисковые системы индексируют интернет. Парсер чаще приходит на конкретную страницу за конкретными данными. Краулинг в чистом виде нам понадобится позже, когда будем обходить каталог с пагинацией.

Что происходит, когда вы открываете сайт

Вы вводите адрес, и через полсекунды браузер показывает страницу. Между этими событиями случилось вот что: браузер отправил на сервер HTTP-запрос — обычно типа GET, что значит «дай мне документ по такому адресу». Сервер ответил: статус-код 200 (всё хорошо) и текст документа — HTML.

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

HTML за пятнадцать минут: теги и вложенность

Вот мини-документ, с которым мы будем работать весь урок, — страница кофейни с меню. Запустите код и посмотрите, что вытащил из неё парсер:

first_soup.py — первый взгляд на BeautifulSoup
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. Увидели в коде класс — знаете, за что хвататься. Проверим на навигационном меню:

menu_links.py — читаем атрибут href
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 умеет не только искать по всему документу, но и ходить по дереву: взять родителя найденного тега, перебрать детей, перескочить к соседнему элементу. Многие задачи решаются в два движения, если понимаешь, кто кому родственник. Посмотрим, как парсер представляет разметку:

tree_view.py — prettify показывает дерево
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 обязан быть прямым ребёнком card
  • a[href] — все ссылки, у которых задан атрибут href
selectors.py — select возвращает список совпадений
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 живут годами.

two_classes.py — селектор по нескольким классам
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. Порядок действий такой:

  1. Откройте любой сайт и нажмите F12 (или правая кнопка мыши — «Посмотреть код»)
  2. Вкладка Elements — там живёт HTML текущей страницы в виде дерева
  3. Включите значок курсора и кликните по нужному элементу на странице — DevTools прыгнет к его разметке
  4. Правый клик по подсвеченному тегу — Copy — Copy selector: браузер сам составит селектор
  5. Проверьте селектор в консоли: 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

Практика: собираем меню кофейни

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

collect_menu.py — первый мини-парсер
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:

download.py — как страницу скачивают локально
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>
В браузере страница не может обращаться к чужим сайтам (CORS). Локально этот код работает так же - только HTML скачивается с сервера. В песочнице мы передаём BeautifulSoup готовую строку - вся техника разбора при этом идентична.

Что дальше

Проверьте себя: тег, атрибут, дерево 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"])
Проверь себя
0 / 5

1. Что получает программа в ответ на успешный GET-запрос к сайту?

2. Какой атрибут чаще всего служит главным ориентиром при поиске элемента?

3. Что найдёт селектор div.card > h3?

4. Что делает метод soup.prettify()?

5. Чем парсинг отличается от краулинга?

Карточки терминов
Запомнено: 0 / 6
Практика

Перед вами фрагмент раздела «Чай» интернет-магазина. Выведите каждый товар отдельной строкой вида «Пуэр - 640 руб.» — название, пробел, дефис, цена. Работайте селекторами: пройдите по карточкам div.item и возьмите из каждой название и цену.

practice.py
Вопросы и ответы по уроку

Что такое парсинг сайтов и законно ли это?

Парсинг — автоматическое извлечение данных со страниц: программа скачивает HTML и достаёт из него цены, заголовки, таблицы. Сбор публичных данных сам по себе законен, но есть правила: уважать robots.txt сайта, не создавать чрезмерную нагрузку, не собирать персональные данные и не обходить авторизацию. Этике и границам посвящён отдельный урок раздела.

Нужно ли знать HTML и CSS для парсинга на Python?

Нужна база: теги, атрибуты, классы, вложенность и CSS-селекторы — это примерно один вечер. Именно этот минимум даёт первый урок раздела, дальше всё закрепляется на практике: без понимания разметки невозможно писать селекторы, которые находят нужные элементы.

Чем парсинг отличается от краулинга?

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

Как посмотреть HTML-код интересующего элемента на сайте?

Нажмите F12, чтобы открыть DevTools, включите инструмент выбора элемента и кликните по нужному месту страницы — браузер подсветит тег во вкладке Elements. Правый клик по тегу, затем Copy — Copy selector даст готовый CSS-селектор, который можно сразу проверять в консоли.

Понравился урок? Сошлитесь на него

«Эта вложенность — не украшение, а структура, на которой держится весь парсинг.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по BeautifulSoup / Scrapy

В челлендже — 20 задач по BeautifulSoup / Scrapy, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по BeautifulSoup / Scrapy: 20 задач