Парсинг сайтов — это автоматический сбор данных с веб-страниц. Вместо того чтобы копировать информацию руками, вы пишете скрипт, который делает это за секунды. В этой статье — пошаговое руководство по парсингу на Python с использованием httpx и BeautifulSoup4: от первого запроса до экспорта в CSV.
Что понадобится
- Python 3.10+
httpx— асинхронный HTTP-клиент (быстрее requests, поддерживает HTTP/2)beautifulsoup4— парсер HTMLlxml— быстрый бэкенд для 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. Об этом — в следующей статье.
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

