Як парсити сайти на 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 браузера:

  1. Відкрийте сайт новин у браузері
  2. Натисніть F12 → вкладка Elements
  3. Знайдіть контейнери з новинами
  4. Визначте 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_spider

4. 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.