Первый парсер: requests и BeautifulSoup — скачиваем и разбираем страницу
Собираем первого рабочего парсера по схеме «скачать — разобрать — достать»: requests скачивает страницу, BeautifulSoup превращает её в объект, из которого данные достаются в одну строку.
Редакция Питоники
Задача на сегодня: забрать с блога заголовок статьи и список ссылок. Вручную — минута, но страницу придётся обновлять каждый день. Программа сделает это за доли секунды и не забудет ни одной ссылки. В первом уроке мы разобрали, как устроен HTML; теперь подключим два инструмента, без которых не обходится ни один парсер: requests — для скачивания страницы и BeautifulSoup — для её разбора.
Схема любого парсера: четыре шага
Каким бы сложным ни был сайт, парсер всегда повторяет одну и ту же цепочку. Запомните её — весь раздел мы просто будем углублять каждый шаг:
- Скачать: отправить GET-запрос и получить HTML в виде текста — этим занимается requests
- Разобрать: превратить текст в дерево элементов — этим занимается BeautifulSoup
- Достать: найти нужные теги и вытащить из них текст и атрибуты
- Сохранить: сложить данные в список, CSV или JSON
Сегодня пройдём первые три шага до конца: получим рабочий скрипт, который скачивает страницу (точнее — работает со встроенной копией), разбирает её и печатает заголовок со ссылками. Четвёртый шаг — сохранение — получит целый урок ближе к концу раздела.
Устанавливаем библиотеки
requests и BeautifulSoup не входят в стандартную поставку Python, поэтому на своей машине их ставят одной командой. В терминале (не в Python!) это выглядит так:
pip install requests beautifulsoup4
Successfully installed requests-2.32.5 beautifulsoup4-4.14.3
Обратите внимание на разницу в написании: пакет называется beautifulsoup4 (с четвёркой), а импортируется как bs4. Это одна и та же библиотека — просто имя пакета в pip и имя модуля в Python исторически не совпадают.
requests: скачиваем страницу
Библиотека requests делает ровно то, что делает ваш браузер при открытии сайта, только вместо отрисовки отдаёт вам сырой HTML. Вот минимальный запрос:
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>
Разберём три главные детали. 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, потому что так ходят боты. Вежливый парсер представляется честно:
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
Параметр timeout=5 не даёт программе зависнуть навечно, если сервер молчит: через пять секунд requests бросит исключение, и вы сможете его обработать. Привычка ставить таймаут в каждом запросе отличает парсер от скрипта, который однажды запустили и забыли.
BeautifulSoup: превращаем HTML в объект soup
HTML-строка — это просто текст, из него ещё надо достать данные. Конструктор BeautifulSoup(html, "html.parser") строит из текста дерево элементов, а переменную с этим деревом по традиции называют soup — «суп из тегов»:
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:
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. Достанем навигацию сайта:
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, о сохранении поговорим отдельно:
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:
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
Ошибки, которые ждут вас на этом этапе
У каждого, кто пишет первый парсер, случается один и тот же набор сюрпризов. Разберём их заранее — узнаете на месте.
Почему 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"))
1. Какая команда ставит библиотеки для парсинга?
2. Что нужно передать в BeautifulSoup для разбора страницы?
3. Зачем парсеру заголовок User-Agent?
4. Чем a.get("href") отличается от a["href"]?
5. Страница в браузере показывает цены, но soup.find('p', class_='price') вернул None. Самая вероятная причина?
Перед вами страница статьи из блога. Напишите парсер, который выведет заголовок h1, а затем каждую ссылку строкой вида «Жерновые -> /blog/jernovye/» — текст, пробел, стрелка, адрес. Заголовок и ссылки берите через find и find_all.
Как установить 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
BeautifulSoup / Scrapy · Урок 1
Парсинг сайтов на Python: как устроен веб и HTML для скрапера
Первый урок раздела: раз разбираем страницы — сначала поймём, из чего они состоят. Теги, атрибуты, DOM-дерево, селекторы и DevTools за один вечер.
BeautifulSoup / Scrapy · Урок 3
Поиск элементов: find, find_all и CSS-селекторы в BeautifulSoup
Два метода find и find_all плюс семейство select закрывают большинство задач поиска. Разбираем их отличия, ловушки с class_ и attrs и применяем к каталогу товаров.
BeautifulSoup / Scrapy · Урок 7
Сохранение результатов: CSV, JSON и базы данных
Список словарей собран — теперь доведём его до файла: CSV для таблиц, JSON для вложенных структур, sqlite3 для больших объёмов. Плюс две классические ловушки: пустые строки и кракозябры в Excel.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
requests · Урок 1
Библиотека requests Python с нуля: первый GET-запрос
Первый GET-запрос на requests: что улетает по сети, что возвращается и как разобрать ответ на статус, заголовки и тело — механику проверяем прямо в браузере.
requests pythonбиблиотека requests
json · Урок 1
Что такое JSON и зачем он Python: первый json.dumps
Первое превращение словаря в json-строку одной командой: import json, json.dumps и честный взгляд на кракозябры в выводе — всё исполняется прямо на странице.
json для начинающих
BeautifulSoup / Scrapy · Урок 5
Парсинг таблиц и списков: собираем данные в структуру
Таблица — самая частая структура в вебе: курсы валют, расписания, прайсы. Собираем thead и tbody в список словарей, чистим цены и разбираем colspan.
парсинг таблиц pythonbeautifulsoup таблица
Проверьте знания по BeautifulSoup / Scrapy
В челлендже — 20 задач по BeautifulSoup / Scrapy, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по BeautifulSoup / Scrapy: 20 задач