Як парсити сайти на Python: BeautifulSoup, requests і реальний приклад
Уявіть ситуацію: потрібно щодня відстежувати ціни конкурентів на 500+ товарів, збирати новини з десятків джерел або аналізувати відгуки клієнтів з різних платформ. Робити це вручну — марнування часу, а API не завжди доступні. Саме тут на допомогу приходить веб-скрапінг — автоматизований збір даних з веб-сайтів за допомогою Python.
У цій статті розглянемо, як використовувати бібліотеки requests і BeautifulSoup для парсингу сайтів, побачимо практичний приклад збору новин та дізнаємося про типові помилки, яких варто уникати.
Що таке веб-скрапінг і навіщо він потрібен аналітику даних
Різниця між парсингом сайтів та API
API (Application Programming Interface) — це офіційний спосіб отримання даних, який надає сам сайт. Парсинг сайтів — це «неофіційний» спосіб: скрипт імітує поведінку звичайного браузера та витягує дані з HTML-сторінок.
Переваги API:
- Стабільна структура даних
- Офіційна підтримка
- Зазвичай швидше і надійніше
Переваги парсингу:
- Доступ до будь-якої публічної інформації
- Не потребує реєстрації або ключів
- Можливість збирати дані з сайтів без API
Коли парсинг краще за ручний збір даних
Веб-скрапінг стає незамінним у таких випадках:
- Моніторинг цін — автоматичне відстеження цін конкурентів
- Збір новин — агрегація контенту з різних джерел
- Дослідження ринку — аналіз відгуків, рейтингів, трендів
- Lead generation — збір контактної інформації потенційних клієнтів
- SEO-аналіз — моніторинг позицій та контенту конкурентів
Етичні аспекти веб-скрапінгу
Перед початком парсингу завжди варто перевірити:
- robots.txt — файл з правилами для ботів (наприклад, site.com/robots.txt)
- Умови використання сайту
- Навантаження на сервер — робіть паузи між запитами
- Авторські права на контент
Дотримання цих правил допомагає зменшити юридичні та технічні ризики, а також забезпечує більш стабільну й етичну роботу скриптів.
Налаштування середовища: встановлення requests і BeautifulSoup
Встановлення необхідних бібліотек
Для парсингу сайтів знадобляться чотири основні бібліотеки:
pip install requests beautifulsoup4 lxml pandas- requests — для HTTP-запитів
- beautifulsoup4 — для парсингу HTML
- lxml — швидкий XML/HTML парсер
- pandas — для роботи з даними
Перший запит до сайту з requests
import requests
from bs4 import BeautifulSoup
# Базовий запит до сайту
url = 'https://httpbin.org/html'
response = requests.get(url)
print(f'Статус код: {response.status_code}')
print(f'Тип контенту: {response.headers["content-type"]}')
print(f'Розмір відповіді: {len(response.text)} символів')Основи HTML для парсингу
Для ефективного парсингу важливо розуміти структуру HTML:
<div class="article">
<h2 id="title">Заголовок статті</h2>
<span class="date">2024-01-15</span>
<p class="content">Текст статті...</p>
<a href="/article/123">Читати далі</a>
</div>Основні елементи для пошуку:
- Теги: <div>, <h2>, <p>, <a>
- Класи: class=”article”
- ID: id=”title”
- Атрибути: href=”/article/123″
Налаштування headers для імітації браузера
Багато сайтів блокують запити від ботів, тому важливо налаштувати правильні заголовки:
headers = {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'uk-UA,uk;q=0.8,en-US;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
}Основи роботи з requests: отримання та обробка веб-сторінок
GET та POST запити в Python
Більшість парсингу використовує GET-запити, але іноді потрібні POST — наприклад, для форм пошуку:
import requests
import time
# GET запит з параметрами
params = {
'page': 1,
'category': 'tech',
'sort': 'date'
}
response = requests.get('https://example.com/news', params=params)
# POST запит (для пошукових форм)
data = {
'query': 'Python парсинг',
'filter': 'recent'
}
response = requests.post('https://example.com/search', data=data, headers=headers)Робота з сесіями та cookies
Деякі сайти вимагають cookies або спеціальні заголовки для коректної роботи:
# Створення сесії для збереження cookies між запитами
session = requests.Session()
session.headers.update(headers)
# Перший запит — отримуємо cookies
response1 = session.get('https://example.com/login')
# Другий запит — cookies передаються автоматично
response2 = session.get('https://example.com/protected-page')
# Ручне додавання cookies
cookies = {'session_id': 'abc123', 'user_pref': 'uk'}
response = requests.get(url, headers=headers, cookies=cookies)Обробка помилок та статус-кодів
Завжди перевіряйте статус відповіді та обробляйте помилки — це критично для стабільної роботи скрипта:
import requests
from requests.exceptions import RequestException
import time
def safe_request(url, headers, max_retries=3, delay=1):
"""Безпечний запит з повторними спробами"""
for attempt in range(max_retries):
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response
elif response.status_code == 429: # Too Many Requests
wait = delay * (2 ** attempt) # exponential backoff
print(f'Занадто багато запитів. Очікування {wait} секунд...')
time.sleep(wait)
elif response.status_code == 403:
print('Доступ заборонено. Перевірте headers.')
return None
else:
print(f'Статус код: {response.status_code}')
except RequestException as e:
print(f'Помилка запиту (спроба {attempt + 1}): {e}')
if attempt < max_retries - 1:
time.sleep(delay)
return None
# Використання
response = safe_request('https://example.com', headers=headers)
if response:
print('Запит успішний!')
else:
print('Не вдається отримати дані')BeautifulSoup: пошук та витягування даних з HTML
Створення soup-об’єкта та навігація по DOM
from bs4 import BeautifulSoup
html = """
<html>
<body>
<div class="container">
<h1>Головна сторінка</h1>
<div class="news-item">
<h2 class="title">Перша новина</h2>
<span class="date">2024-01-15</span>
<p class="content">Текст першої новини...</p>
</div>
<div class="news-item">
<h2 class="title">Друга новина</h2>
<span class="date">2024-01-14</span>
<p class="content">Текст другої новини...</p>
</div>
</div>
</body>
</html>
"""
# Створення soup-об'єкта
soup = BeautifulSoup(html, 'lxml')
# Базова навігація
print(soup.h1.text) # Перший h1
print(soup.find('div', class_='container')) # Пошук за класомМетоди find() та find_all()
# find() — знаходить ПЕРШИЙ відповідний елемент
first_news = soup.find('div', class_='news-item')
title = first_news.find('h2', class_='title').text
date = first_news.find('span', class_='date').text
content = first_news.find('p', class_='content').text
print(f'Заголовок: {title}')
print(f'Дата: {date}')
print(f'Контент: {content}')
# find_all() — знаходить ВСІ відповідні елементи
all_news = soup.find_all('div', class_='news-item')
print(f'Знайдено {len(all_news)} новин')
# Перебір всіх новин
for news in all_news:
title = news.find('h2').text
date = news.find('span', class_='date').text
print(f'• {title} ({date})')CSS-селектори та регулярні вирази
CSS-селектори дають більше гнучкості при пошуку елементів:
import re
# CSS-селектори
titles = soup.select('h2.title') # Всі h2 з класом title
dates = soup.select('.news-item .date') # Вкладений пошук
first_title = soup.select_one('h2.title') # Перший елемент
# Пошук за атрибутами
links = soup.find_all('a', href=re.compile(r'/article/\d+'))
# Пошук за текстом
tech_news = soup.find_all(string=re.compile(r'технологі', re.IGNORECASE))
# Примітка: підтримка :has() залежить від версії BeautifulSoup / Soup Sieve,
# тому для максимальної сумісності краще використовувати Python-фільтрацію
news_with_date = [
item for item in soup.find_all('div', class_='news-item')
if item.find('span', class_='date')
]Примітка: Підтримка селектора :has() залежить від версії BeautifulSoup / Soup Sieve та конкретного середовища. Для більшої сумісності краще використовувати Python-фільтрацію, як показано вище.
Практичний приклад: парсинг новинного сайту з збереженням у CSV
Аналіз структури цільового сайту
Перед написанням скрипта потрібно проаналізувати HTML-структуру сайту через DevTools браузера:
- Відкрийте сайт новин у браузері
- Натисніть F12 → вкладка Elements
- Знайдіть контейнери з новинами
- Визначте CSS-селектори для заголовків, дат, посилань
Написання скрипта для збору статей
import requests
from bs4 import BeautifulSoup
import time
from datetime import datetime
from urllib.parse import urljoin
class NewsParser:
def __init__(self, base_url, headers=None):
self.base_url = base_url
self.headers = headers or {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get_page(self, url):
"""Отримання сторінки з обробкою помилок"""
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response
except requests.RequestException as e:
print(f"Помилка при отриманні {url}: {e}")
return None
def parse_article_list(self, soup):
"""Парсинг списку статей зі сторінки"""
articles = []
# Адаптуйте селектор під конкретний сайт
news_items = soup.find_all("div", class_="news-item")
for item in news_items:
try:
title_elem = item.find("h2") or item.find("h3")
title = title_elem.text.strip() if title_elem else "Без заголовка"
link_elem = item.find("a")
link = link_elem.get("href", "") if link_elem else ""
if link:
link = urljoin(self.base_url, link)
date_elem = item.find("time") or item.find("span", class_="date")
date = date_elem.text.strip() if date_elem else ""
summary_elem = item.find("p")
summary = summary_elem.text.strip() if summary_elem else ""
if len(summary) > 200:
summary = summary[:200] + "..."
articles.append(
{
"title": title,
"link": link,
"date": date,
"summary": summary,
"parsed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
)
except Exception as e:
print(f"Помилка парсингу елемента: {e}")
continue
return articles
def parse_multiple_pages(self, max_pages=3):
"""Парсинг декількох сторінок пагінації"""
all_articles = []
for page in range(1, max_pages + 1):
print(f"Парсинг сторінки {page}...")
page_url = f"{self.base_url}/news?page={page}"
response = self.get_page(page_url)
if not response:
continue
soup = BeautifulSoup(response.content, "lxml")
articles = self.parse_article_list(soup)
if not articles:
print("Статті не знайдені, завершуємо парсинг")
break
all_articles.extend(articles)
print(f"Знайдено {len(articles)} статей на сторінці {page}")
time.sleep(1) # пауза між запитами
return all_articles
# Використання
if __name__ == "__main__":
parser = NewsParser("https://example-news-site.com")
articles = parser.parse_multiple_pages(max_pages=3)
print(f"Всього зібрано {len(articles)} статей")Збереження даних у структурованому вигляді
import json
import pandas as pd
def save_to_csv(articles, filename='news_articles.csv'):
"""Збереження статей у CSV файл"""
if not articles:
print('Немає даних для збереження')
return
df = pd.DataFrame(articles)
df.to_csv(filename, index=False, encoding='utf-8-sig')
print(f'Дані збережено у файл {filename}')
def save_to_json(articles, filename='news_articles.json'):
"""Збереження у JSON для подальшої обробки"""
with open(filename, 'w', encoding='utf-8') as file:
json.dump(articles, file, ensure_ascii=False, indent=2)
print(f'Дані збережено у файл {filename}')
# Збереження зібраних даних
save_to_csv(articles)
save_to_json(articles)
# Швидкий аналіз
df = pd.DataFrame(articles)
print(f'\nСтатистика:')
print(f'Всього статей: {len(df)}')
print(f'Унікальних заголовків: {df["title"].nunique()}')Типові помилки та як їх уникнути
Технічні помилки при парсингу
1. Ігнорування статус-кодів
# Неправильно — код виконається навіть при помилці сервера
response = requests.get(url)
soup = BeautifulSoup(response.content, 'lxml')
# Правильно
response = requests.get(url)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'lxml')
else:
print(f'Помилка: {response.status_code}')2. Проблеми з кодуванням
# Автовизначення кодування
response = requests.get(url)
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.content, 'lxml')
# Або явно вказати UTF-8
soup = BeautifulSoup(response.content.decode('utf-8', errors='replace'), 'lxml')3. Динамічний контент (JavaScript)
BeautifulSoup не виконує JavaScript. Для сайтів на базі SPA (React, Vue, Angular) потрібен Selenium або Playwright:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Починаючи з Selenium 4.6, Selenium Manager у багатьох випадках автоматично керує драйверами, тому окреме ручне встановлення ChromeDriver часто вже не потрібне
options = webdriver.ChromeOptions()
options.add_argument('--headless') # Без графічного інтерфейсу
driver = webdriver.Chrome(options=options)
try:
driver.get(url)
# Чекаємо завантаження потрібних елементів
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'news-item')))
# Отримуємо HTML після виконання JavaScript
html = driver.page_source
soup = BeautifulSoup(html, 'lxml')
# далі — звичайний парсинг через BeautifulSoup
finally:
driver.quit()Порада: presence_of_element_located чекає появи першого елемента. Якщо потрібно дочекатися завантаження всіх елементів списку, використовуйте presence_of_all_elements_located.
Правові та етичні аспекти
1. Перевірка robots.txt
import urllib.robotparser
def check_robots_txt(base_url, target_url, user_agent='*'):
"""Перевірка дозволів у robots.txt"""
try:
rp = urllib.robotparser.RobotFileParser()
rp.set_url(f'{base_url}/robots.txt')
rp.read()
return rp.can_fetch(user_agent, target_url)
except Exception as e:
print(f'Не вдалося перевірити robots.txt: {e}')
return True # За замовчуванням дозволяємо
# Використання
target = 'https://example.com/news'
if check_robots_txt('https://example.com', target):
print('Парсинг дозволено')
else:
print('Парсинг заборонено robots.txt')2. Обмеження швидкості запитів
import time
from datetime import datetime, timedelta
class RateLimiter:
def __init__(self, max_requests=10, time_window=60):
self.max_requests = max_requests
self.time_window = time_window
self.requests = []
def wait_if_needed(self):
now = datetime.now()
# Видаляємо запити, що вийшли за межі вікна
self.requests = [
t for t in self.requests
if now - t < timedelta(seconds=self.time_window)
]
if len(self.requests) >= self.max_requests:
sleep_time = self.time_window - (now - self.requests[0]).total_seconds()
if sleep_time > 0:
print(f'Очікування {sleep_time:.1f} секунд...')
time.sleep(sleep_time)
self.requests.append(datetime.now())
# Використання: 5 запитів на хвилину
limiter = RateLimiter(max_requests=5, time_window=60)
urls = ['https://example.com/page1', 'https://example.com/page2']
for url in urls:
limiter.wait_if_needed()
response = requests.get(url, headers=headers)
# обробка відповіді...Оптимізація швидкості: асинхронний парсинг
Для великих проєктів, де потрібно обробити сотні URL одночасно, варто розглянути асинхронний підхід з asyncio та aiohttp:
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def fetch_page(session, url):
"""Асинхронне отримання HTML сторінки"""
try:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
except Exception as e:
print(f'Помилка {url}: {e}')
return None
def parse_articles(soup):
"""Витягування статей з soup-об'єкта"""
articles = []
for item in soup.find_all('div', class_='news-item'):
title_elem = item.find('h2')
if title_elem:
articles.append({'title': title_elem.text.strip()})
return articles
async def parse_multiple_urls(urls):
"""Паралельний парсинг декількох URL"""
async with aiohttp.ClientSession() as session:
tasks = [fetch_page(session, url) for url in urls]
results = await asyncio.gather(*tasks)
all_articles = []
for html in results:
if html:
soup = BeautifulSoup(html, 'lxml')
all_articles.extend(parse_articles(soup))
return all_articles
# Використання
urls = ['https://example.com/page1', 'https://example.com/page2']
articles = asyncio.run(parse_multiple_urls(urls))Підсумки та що вивчати далі
Резюме ключових концепцій
У цій статті розглянуто:
- Основи веб-скрапінгу та його переваги над ручним збором даних
- Налаштування середовища з requests і BeautifulSoup
- Практичні методи пошуку та витягування даних з HTML
- Практичний приклад парсингу новинного сайту зі збереженням у CSV та JSON
- Типові помилки та способи їх уникнення
Веб-скрапінг з Python надає аналітикам даних потужний інструмент для автоматизації збору інформації, що особливо цінно в епоху Big Data.
Напрямки для поглиблення знань
1. Selenium для складних сайтів
Коли BeautifulSoup недостатньо — JavaScript, SPA-застосунки, взаємодія з формами:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://spa-website.com')
driver.find_element(By.ID, 'load-more').click() # Натискання кнопки
html = driver.page_source # HTML після рендерингу JS
driver.quit()2. Playwright — сучасна альтернатива Selenium
Playwright від Microsoft — сучасний інструмент для роботи з динамічними сайтами, який підтримує Chromium, Firefox і WebKit:
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://spa-website.com')
page.wait_for_selector('.news-item') # Чекаємо появи елементів
html = page.content() # HTML після рендерингу JS
browser.close()
soup = BeautifulSoup(html, 'lxml')3. Scrapy для масштабних проєктів
Scrapy — професійний фреймворк для великих парсингових проєктів з вбудованою підтримкою черг, middleware та експорту даних:
pip install scrapy
scrapy startproject news_spider4. pandas.read_html() для таблиць
Якщо потрібно витягнути саме HTML-таблиці, pandas часто дозволяє розв’язати задачу одним рядком без окремого парсингу через BeautifulSoup:
import pandas as pd
# Автоматично знаходить і парсить усі таблиці на сторінці
tables = pd.read_html('https://example.com/stats-page')
# tables — список DataFrame, по одному на кожну таблицю
print(f'Знайдено {len(tables)} таблиць')
print(tables[0].head())Інтеграція в аналітичні пайплайни
Зібрані дані можна інтегрувати в:
- ETL-пайплайни для регулярного оновлення баз даних
- BI-системи для візуалізації трендів
- ML-моделі для прогнозування та класифікації
- Дашборди для моніторингу ключових метрик
Парсинг сайтів — це лише перший крок у роботі з даними. Наступні етапи включають очищення, аналіз та візуалізацію зібраної інформації. Для тих, хто лише починає шлях у Python і хоче освоїти всі ці етапи на практиці, корисним стартом може стати курс «Програмування на Python» від Data Lab.


