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

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

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

Первый парсер: requests и BeautifulSoup — скачиваем и разбираем страницу

Собираем первого рабочего парсера по схеме «скачать — разобрать — достать»: requests скачивает страницу, BeautifulSoup превращает её в объект, из которого данные достаются в одну строку.

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

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

Схема любого парсера: четыре шага

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

  1. Скачать: отправить GET-запрос и получить HTML в виде текста — этим занимается requests
  2. Разобрать: превратить текст в дерево элементов — этим занимается BeautifulSoup
  3. Достать: найти нужные теги и вытащить из них текст и атрибуты
  4. Сохранить: сложить данные в список, CSV или JSON

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

Устанавливаем библиотеки

requests и BeautifulSoup не входят в стандартную поставку Python, поэтому на своей машине их ставят одной командой. В терминале (не в Python!) это выглядит так:

Терминал — установка библиотек парсинга
pip install requests beautifulsoup4
Вывод
Successfully installed requests-2.32.5 beautifulsoup4-4.14.3
Это команда терминала, а не код на Python. В браузерной песочнице beautifulsoup4 уже установлен и доступен сразу - устанавливать ничего не нужно.

Обратите внимание на разницу в написании: пакет называется beautifulsoup4 (с четвёркой), а импортируется как bs4. Это одна и та же библиотека — просто имя пакета в pip и имя модуля в Python исторически не совпадают.

requests: скачиваем страницу

Библиотека requests делает ровно то, что делает ваш браузер при открытии сайта, только вместо отрисовки отдаёт вам сырой HTML. Вот минимальный запрос:

download.py — GET-запрос к сайту
import requests

response = requests.get("https://example.com")

print(response.status_code)   # 200 - сервер ответил успешно
print(response.text[:64])     # первые 64 символа HTML
Вывод
200
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
В браузере страница не может обращаться к чужим сайтам (CORS). Локально этот код работает так же - только HTML скачивается с сервера. В песочнице мы передаём BeautifulSoup готовую строку: техника разбора при этом один в один.

Разберём три главные детали. requests.get(url) отправляет GET-запрос и возвращает объект ответа. response.status_code — число-статус: 200 значит успех, 404 — страницы нет, 403 — доступ запрещён. response.text — сам HTML в виде строки; именно эту строку мы дальше скормим BeautifulSoup. Длина строки у настоящих страниц — сотни килобайт, поэтому в примере мы отрезали первые 64 символа срезом [:64].

У ответа есть ещё пара полезных свойств, которые пригодятся позже. response.url — итоговый адрес: если сайт перебросил вас с http на https или добавил слэш, вы это увидите. response.content — те же данные, но в байтах, без декодирования: к байтам прибегают, когда надо сохранить картинку или PDF. А для обычного HTML строка .text — то, что нужно. Проверяйте статус до разбора: if response.status_code == 200: — и только внутри условия запускайте парсер. Иначе на каждую ошибку сервера вы будете разбирать HTML-страницу с текстом «страница не найдена» и удивляться, почему в ней нет товаров.

Заголовки запроса: User-Agent

Каждый HTTP-запрос несёт заголовки — служебные строчки с метаданными. Самый важный для парсера — User-Agent: имя программы-клиента. У requests он по умолчанию выглядит как python-requests/2.32, и некоторые сайты сразу режут такие запросы ответом 403, потому что так ходят боты. Вежливый парсер представляется честно:

download_ua.py — запрос с заголовками
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
response = requests.get("https://example.com", headers=headers, timeout=5)

print(response.status_code)
print(response.encoding)
Вывод
200
UTF-8
Снова CORS: из песочницы в интернет не выходим. Локально этот запрос вернёт то же самое. Заголовок User-Agent мы ещё вспомним в уроке про этику парсинга - там разберём, зачем он нужен сайту и почему задержки важнее подмены подписи.

Параметр timeout=5 не даёт программе зависнуть навечно, если сервер молчит: через пять секунд requests бросит исключение, и вы сможете его обработать. Привычка ставить таймаут в каждом запросе отличает парсер от скрипта, который однажды запустили и забыли.

BeautifulSoup: превращаем HTML в объект soup

HTML-строка — это просто текст, из него ещё надо достать данные. Конструктор BeautifulSoup(html, "html.parser") строит из текста дерево элементов, а переменную с этим деревом по традиции называют soup — «суп из тегов»:

first_soup.py — создаём объект BeautifulSoup
from bs4 import BeautifulSoup

html = """
<article>
  <h1>Как выбрать кофемолку для дома</h1>
  <p class="meta">Автор: Анна Смирнова</p>
  <p>Жерновая или ножевая? Отвечаем на главный вопрос.</p>
  <a href="/blog/kofemolki-jernovye/">Жерновые кофемолки</a>
  <a href="https://partner.example.com/kofe">Купить кофе</a>
</article>
"""

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

print(type(soup))          # что за объект получился
print(soup.h1.text)        # текст первого тега h1
print(soup.p.text)         # текст первого тега p
Вывод
<class 'bs4.BeautifulSoup'>
Как выбрать кофемолку для дома
Автор: Анна Смирнова

Смотрите, что произошло: soup.h1 — это обращение к первому тегу h1 в документе, как к обычному свойству. А .text достаёт из тега весь текст внутри, без разметки. Второй аргумент конструктора — "html.parser" — имя движка разбора. Он встроен в Python и работает везде; есть ещё быстрый сторонний lxml, но он требует отдельной установки, поэтому в уроках мы всегда пишем html.parser — гарантированно рабочий вариант.

prettify: приводим HTML в читаемый вид

Сайты часто отдают HTML одной длинной строкой без переносов — глаза сломаешь. У объекта soup есть метод prettify(), который печатает дерево с отступами. Это первый инструмент отладки: не понимаете, что нашёл парсер — выведите prettify:

pretty.py — prettify выравнивает дерево
from bs4 import BeautifulSoup

html = "<h1>Заголовок</h1><p>Первый абзац</p><p>Второй абзац</p>"
soup = BeautifulSoup(html, "html.parser")
print(soup.prettify())
Вывод
<h1>
 Заголовок
</h1>
<p>
 Первый абзац
</p>
<p>
 Второй абзац
</p>

Мой рабочий порядок такой: сперва prettify всего документа или его фрагмента, глазами — есть ли там вообще то, за чем пришёл, и только потом пишу селекторы. Когда prettify печатает страницу на три экрана, помогает вывести фрагмент: print(soup.find("div", class_="content").prettify()) — дерево конкретного блока вместо всей страницы.

Достаём все ссылки со страницы

Ссылки — хлеб парсинга: по ним парсер ходит между страницами, из них собирает пагинацию и адреса товаров. Метод find_all("a") возвращает список всех тегов <a>, а у каждого есть текст и атрибут href. Достанем навигацию сайта:

links.py — find_all и атрибут href
from bs4 import BeautifulSoup

html = """
<nav>
  <a href="/catalog/">Каталог</a>
  <a href="/delivery/">Доставка</a>
  <a href="/contacts/">Контакты</a>
  <a href="tel:+74951234567">Позвонить</a>
</nav>
"""
soup = BeautifulSoup(html, "html.parser")

links = soup.find_all("a")
print(len(links))
for a in links:
    print(a.text, "->", a.get("href"))
Вывод
4
Каталог -> /catalog/
Доставка -> /delivery/
Контакты -> /contacts/
Позвонить -> tel:+74951234567

Тут две новые детали. len(links) — простая проверка, что нашлось столько элементов, сколько вы ожидали. И a.get("href") вместо a["href"]: метод .get() возвращает None, если атрибута нет, а квадратные скобки в этом случае упадут с KeyError. Последняя ссылка в примере — телефонная, у неё href начинается с tel:; на настоящих сайтах встречаются и mailto:, и javascript: — такие адреса стоит фильтровать по началу строки.

Мини-парсер целиком

Соберём всю цепочку: HTML → soup → структурированные данные. Скрипт забирает с карточки новости заголовок, дату, абзацы текста и первую ссылку — и складывает в словарь. Такой словарь потом удобно отдавать в CSV или pandas, о сохранении поговорим отдельно:

parser.py — полный мини-парсер новости
from bs4 import BeautifulSoup

html = """
<div class="post">
  <h1>5 ошибок новичка в парсинге</h1>
  <time datetime="2025-11-03">3 ноября 2025</time>
  <div class="content">
    <p>Первая ошибка - торопиться.</p>
    <p>Вторая ошибка - игнорировать robots.txt.</p>
  </div>
  <a href="/blog/oshibka-3/">Про третью ошибку</a>
</div>
"""

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

title = soup.find("h1").text              # заголовок
date = soup.find("time").get("datetime")  # атрибут, а не текст
paragraphs = [p.text for p in soup.find_all("p")]
link = soup.find("a").get("href")         # первая ссылка

print(title)
print(date)
print(paragraphs)
print(link)
Вывод
5 ошибок новичка в парсинге
2025-11-03
['Первая ошибка - торопиться.', 'Вторая ошибка - игнорировать robots.txt.']
/blog/oshibka-3/

Хорошая деталь в примере: у тега <time> мы взяли не видимый текст «3 ноября 2025», а атрибут datetime="2025-11-03" — машиночитаемую дату в формате ISO. Это общий принцип: сайты дублируют данные в атрибутах, и для программы атрибут часто удобнее человеческого текста. Все способы их чтения — в уроке про извлечение текста и атрибутов.

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

А вот как тот же скрипт выглядит в локальном Python, когда страница скачивается с сервера, — сравните с песочницей, отличается только источник HTML:

parser_local.py — полный цикл с requests
import requests
from bs4 import BeautifulSoup

url = "https://example.com/blog/"
response = requests.get(url, timeout=5)

if response.status_code == 200:
    soup = BeautifulSoup(response.text, "html.parser")
    print(soup.h1.text)
else:
    print("Ошибка запроса:", response.status_code)
Вывод
Example Domain
В браузере страница не может обращаться к чужим сайтам (CORS). Локально этот код работает так же - только HTML скачивается с сервера. Проверка status_code перед разбором обязательна: разбирать HTML ошибки 404 бессмысленно.

Ошибки, которые ждут вас на этом этапе

У каждого, кто пишет первый парсер, случается один и тот же набор сюрпризов. Разберём их заранее — узнаете на месте.

Почему BeautifulSoup не видит данные со страницы?

  • AttributeError: 'NoneType' object has no attribute 'text' — find ничего не нашёл и вернул None, а вы у None просите .text. Проверяйте результат find перед обращением
  • 403 на каждом запросе — сайт не пускает без User-Agent или вы стучитесь слишком часто; добавьте заголовки и паузы
  • Кракозябры вместо русских букв — проблема кодировки; помогает response.encoding = response.apparent_encoding перед чтением .text
  • Пустой список find_all при живой странице — почти всегда контент рисует JavaScript (см. выше) или вы ищете не в том фрагменте

Что дальше

Сегодня мы прошли схему «скачать — разобрать — достать»: умеем ставить запрос с заголовками, строить soup, вынимать первый попавшийся тег, список ссылок и даже складывать данные в словарь. Эта схема одна на весь самоучитель BeautifulSoup: впереди таблицы, сохранение в CSV и даже Scrapy. Но soup.h1 и find_all("a") — это верхушка айсберга поиска. Реальные страницы требуют искать по классам и атрибутам, ограничивать глубину и число совпадений, а иногда — объединять всё это в сложные CSS-селекторы.

В следующем уроке разберём поиск по-настоящему: find против find_all (и почему один возвращает тег, а другой список), поиск по классу с подчёркиванием class_, словарь attrs и CSS-селекторы через select — с кейсом разбора каталога товаров.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from bs4 import BeautifulSoup

html = "<div><p>Первый</p><p>Второй</p></div>"
soup = BeautifulSoup(html, "html.parser")
print(soup.p.text)
from bs4 import BeautifulSoup

html = '<nav><a href="/shop/">Магазин</a></nav>'
soup = BeautifulSoup(html, "html.parser")
a = soup.find("a")
print(a.get("target", "_self"))
Проверь себя
0 / 5

1. Какая команда ставит библиотеки для парсинга?

2. Что нужно передать в BeautifulSoup для разбора страницы?

3. Зачем парсеру заголовок User-Agent?

4. Чем a.get("href") отличается от a["href"]?

5. Страница в браузере показывает цены, но soup.find('p', class_='price') вернул None. Самая вероятная причина?

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

Перед вами страница статьи из блога. Напишите парсер, который выведет заголовок h1, а затем каждую ссылку строкой вида «Жерновые -> /blog/jernovye/» — текст, пробел, стрелка, адрес. Заголовок и ссылки берите через find и find_all.

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

Как установить requests и beautifulsoup4?

В терминале выполните pip install requests beautifulsoup4. Импортируются они по-разному: import requests, но from bs4 import BeautifulSoup — имя модуля bs4, а не beautifulsoup4. В браузерной песочнице Питоники bs4 уже предустановлен.

Чем html.parser отличается от lxml?

html.parser встроен в Python и работает везде, lxml — отдельный C-парсер: заметно быстрее на больших документах, но требует установки и может отсутствовать в вашей среде. Для обучения и средних объёмов html.parser достаточно; если страницы крупные и lxml доступен — ставьте его вторым аргументом.

Почему BeautifulSoup не видит данные, которые видны на странице?

Почти всегда виноват JavaScript: сервер отдал каркас страницы, а данные (цены, списки) браузер дорисовал скриптом уже после. requests получает только то, что отдал сервер, — проверьте вкладку View Source: если данных там нет, их не будет и в response.text. Выход — искать внутренний API сайта во вкладке Network или использовать Selenium.

Что делать, если сайт возвращает 403 при парсинге?

Сначала добавить заголовок User-Agent с честной подписью и паузы между запросами. Если не помогло — прочитать robots.txt: возможно, этот раздел закрыт от роботов, и его лучше не парсить. Систематический разбор статусов, задержек и правил — в уроке про этичный парсинг.

Чем requests отличается от BeautifulSoup?

requests скачивает: отправляет GET-запрос и возвращает HTML в виде строки (.text). BeautifulSoup разбирает: превращает эту строку в дерево элементов и ищет по нему. Они работают в паре и не заменяют друг друга — отсюда схема «скачать — разобрать — достать» из начала урока: requests.get(url) → BeautifulSoup(response.text, "html.parser") → поиск данных.

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

«Привычка ставить таймаут в каждом запросе отличает парсер от скрипта, который однажды запустили и забыли.»

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

TelegramVK

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

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

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

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

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