requests не видит контент, загружаемый через JavaScript. Решение — Playwright для рендеринга страницы + BeautifulSoup для удобного парсинга.

Пару месяцев назад я пытался собрать список новостей с сайта на Vue.js.
Сначала написал простой скрипт на requests + BeautifulSoup — получил пустую страницу с одним <div id="app"></div>.
Потом подумал: «Ладно, добавлю time.sleep() и Selenium» — но это оказалось медленным и нестабильным.
А потом я попробовал Playwright — и всё заработало с первого раза, без костылей.
Если вы парсите современные сайты (React, Vue, Angular, SPA), ваш код должен уметь «ждать» JavaScript. В этой статье я покажу, как сделать это без боли, с живыми примерами и готовыми решениями.
Оглавление
Почему requests и BeautifulSoup не работают с динамическими сайтами?
Когда вы открываете сайт в браузере, происходит следующее:
- Сервер отдаёт базовый HTML (часто почти пустой)
- Браузер загружает JavaScript
- JS делает API-запросы и динамически вставляет контент в DOM
Пример:
<!-- То, что получает requests -->
<div id="root"></div>
<!-- То, что видит пользователь (и Playwright) -->
<div id="root">
<article>Новость 1</article>
<article>Новость 2</article>
</div>
Модуль requests не выполняет JavaScript, поэтому вы получаете «скелет».
Решение — использовать настоящий браузер, который прогонит JS и даст финальный HTML.
Почему Playwright + BeautifulSoup — идеальная связка?
- Playwright — быстрый, современный, с встроенными ожиданиями (
wait_for_selector) - BeautifulSoup — удобный API для навигации по DOM, CSS-селекторы, обработка текста
- Вместе они дают мощность браузера + гибкость парсинга
📚 Документация:
Установка и настройка
pip install playwright beautifulsoup4
playwright install chromium
💡 Playwright сам скачивает легковесную версию Chromium — не нужно устанавливать браузер вручную.
Пример 1. Парсим цитаты с JS-сайта
Выбор демо-сайта
Используем официальный тестовый сайт:
👉 https://quotes.toscrape.com/js/
Он полностью построен на JavaScript — requests вернёт пустой список.
Полный рабочий скрипт
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import time
url = "https://quotes.toscrape.com/js/"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("Переходим на страницу...")
page.goto(url)
# Ждём появления хотя бы одной цитаты
print("Ждём загрузки контента...")
page.wait_for_selector(".quote", timeout=10000)
# Получаем финальный HTML после выполнения JS
html = page.content()
browser.close()
# Парсим через BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
quotes = soup.select(".quote")
print(f"\nНайдено цитат: {len(quotes)}\n")
for i, quote in enumerate(quotes[:5], 1): # первые 5
text = quote.select_one(".text").get_text(strip=True)
author = quote.select_one(".author").get_text(strip=True)
tags = [tag.get_text() for tag in quote.select(".tag")]
print(f"{i}. «{text}» — {author}")
print(f" Теги: {', '.join(tags)}\n")
Вывод:
1. «The world as we have created it is a process of our thinking.» — Albert Einstein
Теги: change, deep-thoughts, thinking, world
2. «It is our choices, Harry, that show what we truly are, far more than our abilities.» — J.K. Rowling
Теги: abilities, choices
...
✅ Контент получен! Даже теги — всё на месте.
Пример 2. Парсим таблицу с динамической подгрузкой
Допустим, нужно спарсить таблицу, которая появляется после клика.
Сайт: https://demo.scrapingbee.com/dynamic-table
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://demo.scrapingbee.com/dynamic-table")
# Кликаем по кнопке "Load Table"
page.click("button:has-text('Load Table')")
# Ждём появления таблицы
page.wait_for_selector("table tbody tr", timeout=10000)
html = page.content()
browser.close()
soup = BeautifulSoup(html, "html.parser")
rows = soup.select("table tbody tr")
for row in rows[:3]:
cells = [td.get_text(strip=True) for td in row.find_all("td")]
print(cells)
Этот подход работает даже если контент подгружается по AJAX после клика.
Как правильно ждать? Советы по надёжности
Используйте wait_for_selector вместо time.sleep()
❌ Плохо:
time.sleep(3) # Может не хватить или ждать лишнего
✅ Хорошо:
page.wait_for_selector(".result-item", timeout=15000)
Ожидание «тишины» в сети
page.goto(url, wait_until="networkidle") # Ждать, пока не прекратятся запросы
Обработка ошибок
try:
page.wait_for_selector(".quote", timeout=10000)
except TimeoutError:
print("❌ Контент не загрузился — возможно, сайт изменился")
return
Сохранение результата в CSV
import csv
with open("quotes.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Цитата", "Автор", "Теги"])
for quote in quotes:
text = quote.select_one(".text").get_text(strip=True)
author = quote.select_one(".author").get_text(strip=True)
tags = ", ".join(tag.get_text() for tag in quote.select(".tag"))
writer.writerow([text, author, tags])
print("✅ Результат сохранён в quotes.csv")
Заключение
Теперь вы умеете:
✅ Парсить сайты, где контент грузится через JavaScript
✅ Дожидаться нужных элементов без time.sleep()
✅ Объединять мощь Playwright и удобство BeautifulSoup
✅ Сохранять результат в структурированном виде
Эта связка — стандарт де-факто для современного парсинга на Python.
Даже если сайт использует сложную логику на React или Vue — вы получите нужные данные.
🐍 Попробуйте запустить примеры выше — и вы убедитесь, насколько это надёжно и просто.
📢 Подписывайтесь на Telegram-канал PythonAuto, чтобы не пропустить новые гайды по автоматизации, парсингу и Python.
👉 Ваш интерес — лучшая мотивация для новых статей!