Парсинг сайтов на Python: httpx + BeautifulSoup за 20 минут

Парсинг сайтов — это автоматический сбор данных с веб-страниц. Вместо того чтобы копировать информацию руками, вы пишете скрипт, который делает это за секунды. В этой статье — пошаговое руководство по парсингу на Python с использованием httpx и BeautifulSoup4: от первого запроса до экспорта в CSV.

Что понадобится

  • Python 3.10+
  • httpx — асинхронный HTTP-клиент (быстрее requests, поддерживает HTTP/2)
  • beautifulsoup4 — парсер HTML
  • lxml — быстрый бэкенд для BeautifulSoup
pip install httpx beautifulsoup4 lxml

Первый запрос

Начнём с простого: загрузим HTML-страницу и выведем заголовок.

import httpx
from bs4 import BeautifulSoup

url = "https://quotes.toscrape.com/"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/131.0.0.0 Safari/537.36"
}

response = httpx.get(url, headers=headers)
soup = BeautifulSoup(response.text, "lxml")

title = soup.select_one("title").text
print(f"Заголовок страницы: {title}")

Заголовок User-Agent обязателен — без него многие сайты возвращают 403 или капчу.

CSS-селекторы: основы

BeautifulSoup поддерживает CSS-селекторы через метод select() — это удобнее, чем find_all().

Селектор Что выбирает Пример
.class Элементы с классом soup.select(".quote")
#id Элемент по id soup.select_one("#main")
tag.class Тег с классом soup.select("span.text")
parent > child Прямой дочерний soup.select("div.quote > span")
[attr=val] Атрибут со значением soup.select("a[href='/page/2/']")
tag:nth-child(n) N‑й дочерний soup.select("li:nth-child(1)")

Парсим список элементов

Соберём все цитаты со страницы: текст, автор, теги.

quotes = []
for block in soup.select("div.quote"):
    text = block.select_one("span.text").text.strip("«»\u201c\u201d")
    author = block.select_one("small.author").text
    tags = [tag.text for tag in block.select("a.tag")]

    quotes.append({
        "text": text,
        "author": author,
        "tags": tags
    })

for q in quotes[:3]:
    print(f'{q["author"]}: {q["text"][:60]}...')
    print(f'  Теги: {", ".join(q["tags"])}')
    print()

Пагинация: обход всех страниц

Большинство сайтов разбивают данные на страницы. Обходим их последовательно, пока есть кнопка «Далее».

import httpx
from bs4 import BeautifulSoup
import time

BASE_URL = "https://quotes.toscrape.com"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
}

all_quotes = []
url = BASE_URL

while url:
    response = httpx.get(url, headers=headers)
    soup = BeautifulSoup(response.text, "lxml")

    for block in soup.select("div.quote"):
        all_quotes.append({
            "text": block.select_one("span.text").text,
            "author": block.select_one("small.author").text,
            "tags": [t.text for t in block.select("a.tag")]
        })

    # Ищем ссылку на следующую страницу
    next_btn = soup.select_one("li.next > a")
    url = BASE_URL + next_btn["href"] if next_btn else None

    time.sleep(1)  # пауза между запросами

print(f"Собрано цитат: {len(all_quotes)}")

Пауза time.sleep(1) между запросами — проявление уважения к серверу и защита от блокировки.

Асинхронный парсинг для скорости

Когда страниц много, асинхронные запросы ускоряют работу в 5 – 10 раз. Используем asyncio.Semaphore для ограничения параллельности.

import asyncio
import httpx
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
}

async def fetch_page(client, url, semaphore):
    async with semaphore:
        response = await client.get(url, headers=HEADERS)
        await asyncio.sleep(0.5)  # rate limiting
        return response.text

async def main():
    base = "https://quotes.toscrape.com/page/{}/"
    urls = [base.format(i) for i in range(1, 11)]
    semaphore = asyncio.Semaphore(3)  # максимум 3 запроса одновременно

    async with httpx.AsyncClient() as client:
        tasks = [fetch_page(client, url, semaphore) for url in urls]
        pages = await asyncio.gather(*tasks)

    all_quotes = []
    for html in pages:
        soup = BeautifulSoup(html, "lxml")
        for block in soup.select("div.quote"):
            all_quotes.append({
                "text": block.select_one("span.text").text,
                "author": block.select_one("small.author").text,
            })

    print(f"Собрано: {len(all_quotes)} цитат")

asyncio.run(main())

Экспорт в CSV

import csv

def save_to_csv(data: list[dict], filename: str):
    if not data:
        return

    with open(filename, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)

    print(f"Сохранено {len(data)} записей в {filename}")

# Для CSV теги нужно преобразовать в строку
csv_data = [
    {**q, "tags": ", ".join(q["tags"])} for q in all_quotes
]
save_to_csv(csv_data, "quotes.csv")

Кодировка utf-8-sig добавляет BOM-маркер — без него Excel некорректно отображает кириллицу.

Обработка ошибок

В реальных проектах запросы падают: таймауты, 403, изменение вёрстки. Добавьте ретраи и обработку исключений.

async def fetch_with_retry(client, url, semaphore, retries=3):
    for attempt in range(retries):
        try:
            async with semaphore:
                response = await client.get(
                    url, headers=HEADERS, timeout=10.0
                )
                response.raise_for_status()
                await asyncio.sleep(0.5)
                return response.text
        except (httpx.HTTPStatusError, httpx.TimeoutException) as e:
            if attempt == retries - 1:
                print(f"Не удалось загрузить {url}: {e}")
                return None
            await asyncio.sleep(2 ** attempt)  # экспоненциальный бэкофф

Полезные приёмы

  • Проверяйте robots.txt перед парсингом — это правила сайта для ботов. Не нарушайте их без необходимости
  • Сохраняйте сырой HTML в файл при первом запуске — так при ошибке парсинга не нужно повторно загружать страницы
  • Используйте response.encoding — httpx автоматически определяет кодировку, но иногда ошибается на русскоязычных сайтах с windows-1251
  • Прокси — при большом объёме запросов используйте список прокси с ротацией
  • Кэширование — для отладки парсера сохраняйте HTML локально, чтобы не делать лишних запросов

Когда httpx + BS4 не хватает

Этот подход работает для статических сайтов, где контент отдаётся в HTML. Если сайт рендерит данные через JavaScript (SPA на React/Vue) — вам нужен Playwright. Об этом — в следующей статье.

Есть идея? Реализуем

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

Написать в Telegram

29.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять