Парсинг (web scraping) — это автоматический сбор данных с веб-страниц и их преобразование в структурированный вид: таблицы, JSON, базу данных. Python стал стандартом де-факто для этой задачи: у него простой синтаксис, огромная экосистема библиотек и развитое сообщество.
Этот материал — обзорный «хаб». Здесь мы разберём весь процесс от запроса страницы до хранения очередей URL, дадим карту всех ключевых библиотек и решений, а по узким темам будем ссылаться на отдельные подробные статьи.
Оглавление
- Что такое парсинг и из чего он состоит
- Обзор библиотек и решений
- Как забираем страницу
- Библиотеки для парсинга содержимого
- Решение проблем с парсингом кириллицы
- Использование многопоточности
- Использование прокси
- Парсинг через TOR
- Работа с HTTPS/SSL
- Работа с cookie
- Статус ответа и заголовки
- Хранение URL и очередей
- Плюсы и минусы реализации
- Юридические и этические аспекты
1. Что такое парсинг и из чего он состоит
Любой парсер, независимо от масштаба, состоит из четырёх стадий:
- Получение страницы — HTTP-запрос к серверу и получение ответа (HTML, XML, JSON).
- Разбор содержимого — извлечение нужных данных из разметки по селекторам (CSS, XPath) или регулярным выражениям.
- Нормализация и хранение — приведение данных к единому формату и запись в файл/БД.
- Управление обходом — очередь URL, дедупликация, ограничение скорости, повторные попытки.
Простой скрипт может уместить всё это в 10 строк. Промышленный краулер выносит каждую стадию в отдельный слой с очередями, прокси и распределёнными воркерами.
2. Обзор библиотек и решений
Чтобы не запутаться, разделим инструменты по их роли.
Загрузка страниц (HTTP-клиенты)
| Библиотека | Тип | Когда использовать |
|---|---|---|
| requests | синхронный | стандарт для большинства задач, удобный API |
| urllib | синхронный | встроен в стандартную библиотеку, без зависимостей |
| httpx | синхр./асинхр. | современная замена requests с поддержкой async и HTTP/2 |
| aiohttp | асинхронный | высокая параллельность, тысячи запросов |
| pycurl | синхронный | тонкий контроль над запросом, максимальная скорость |
Разбор HTML/XML
| Библиотека | Движок | Особенности |
|---|---|---|
| BeautifulSoup (bs4) | html.parser / lxml | самый дружелюбный API, прощает «грязный» HTML |
| lxml | libxml2 (C) | максимальная скорость, полноценный XPath |
| parsel | lxml | CSS + XPath, основа Scrapy |
| selectolax | Modest/Lexbor (C) | очень быстрый CSS-парсер для больших объёмов |
| pyquery | lxml | синтаксис в стиле jQuery |
Динамические сайты (JavaScript)
| Инструмент | Назначение |
|---|---|
| Selenium | управление реальным браузером, классика |
| Playwright | современная альтернатива, быстрее и стабильнее |
| Pyppeteer | порт Puppeteer на Python |
Фреймворки и платформы
| Решение | Назначение |
|---|---|
| Scrapy | полноценный фреймворк для краулеров: очереди, пайплайны, middleware |
| Scrapy + Splash/Playwright | Scrapy с рендерингом JS |
| Django + Celery | парсинг как часть веб-приложения с фоновыми задачами |
Как выбрать
- Простая страница без JS, разовая задача → requests + BeautifulSoup.
- Нужна скорость на больших объёмах → httpx/aiohttp + lxml/selectolax.
- Сотни тысяч страниц, обход всего сайта → Scrapy. Подробно в статье «Парсинг сайтов на Scrapy».
- Контент рендерится через JavaScript → Playwright/Selenium.
- Парсинг встроен в веб-сервис → Django, см. «Парсинг на Django».
- Нужна максимальная параллельность → асинхронный подход.
3. Как забираем страницу
Базовый запрос на requests:
import requests
url = "https://example.com"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"Accept-Language": "ru-RU,ru;q=0.9",
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # выбросит исключение при 4xx/5xx
html = response.text
Ключевые моменты:
- User-Agent обязательно задавайте — многие сайты блокируют запросы с дефолтным
python-requests. - timeout всегда указывайте, иначе скрипт может зависнуть навсегда.
- raise_for_status() избавляет от ручной проверки кода ответа.
Если страница рендерится через JavaScript, requests вернёт пустой каркас. Тогда нужен браузерный движок:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector(".content") # ждём появления данных
html = page.content()
browser.close()
Если на выходе ожидается не HTML, а структурированные данные API, см. отдельный материал «Парсинг JSON на Python».
4. Библиотеки для парсинга содержимого
BeautifulSoup — точка входа для новичков
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml") # парсер lxml быстрее html.parser
title = soup.find("h1").get_text(strip=True)
links = [a["href"] for a in soup.select("a.product-link")]
price = soup.select_one(".price").text
BeautifulSoup прощает сломанную разметку и читается почти как естественный язык. Для извлечения таблиц есть отдельный разбор: «Парсинг таблиц с сайта на BeautifulSoup».
lxml — скорость и XPath
from lxml import html as lxml_html
tree = lxml_html.fromstring(html)
titles = tree.xpath('//h2[@class="title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')
lxml написан на C и работает в разы быстрее на больших объёмах. Полноценный XPath даёт гибкость, недоступную CSS-селекторам. Глубокий разбор — в статье «Парсинг на Python с lxml».
parsel и selectolax
parsel (ядро Scrapy) объединяет CSS и XPath:
from parsel import Selector
sel = Selector(text=html)
sel.css("h1::text").get()
sel.xpath("//a/@href").getall()
selectolax стоит выбрать, когда нужно разобрать сотни тысяч документов — он заметно быстрее даже lxml на CSS-выборках.
Регулярные выражения
re уместен только для простых, плоских паттернов (телефон, email, артикул). Парсить вложенный HTML регулярками не стоит — структура легко ломается.
5. Решение проблем с парсингом кириллицы
Самая частая боль на рунет-сайтах — «кракозябры» вместо русского текста (пÑÐ¸Ð²ÐµÑ вместо привет). Причина — неверно определённая кодировка ответа.
Почему так происходит
requests угадывает кодировку по заголовку Content-Type. Если сервер его не прислал или прислал неверно, текст декодируется не в той кодировке (часто берётся ISO-8859-1 вместо windows-1251 или utf-8).
Решение 1: задать кодировку вручную
response = requests.get(url)
response.encoding = "utf-8" # или "windows-1251" для старых сайтов
html = response.text
Решение 2: автоопределение
response = requests.get(url)
response.encoding = response.apparent_encoding # определяется по содержимому
html = response.text
apparent_encoding использует библиотеку charset-normalizer (или chardet), которая анализирует байты и подбирает кодировку.
Решение 3: работать с байтами напрямую
Самый надёжный путь — отдать байты парсеру и позволить ему прочитать <meta charset>:
from bs4 import BeautifulSoup
response = requests.get(url)
soup = BeautifulSoup(response.content, "lxml") # .content, а не .text
response.content — это «сырые» байты; lxml и BeautifulSoup сами найдут объявление кодировки в HTML.
Решение 4: ручное декодирование
html = response.content.decode("windows-1251", errors="replace")
Параметр errors="replace" заменит непреобразуемые символы на �, не уронив скрипт. Для XML с кириллицей нюансы описаны в «Парсинг XML на Python».
6. Использование многопоточности
Парсинг — это в основном ожидание сети (I/O-bound), поэтому потоки дают ощутимое ускорение, несмотря на GIL: пока один поток ждёт ответа, другой работает.
ThreadPoolExecutor — самый простой способ
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [f"https://example.com/page/{i}" for i in range(1, 101)]
def fetch(url):
r = requests.get(url, timeout=10)
return url, r.status_code
with ThreadPoolExecutor(max_workers=10) as executor:
for url, status in executor.map(fetch, urls):
print(url, status)
Когда брать multiprocessing
Если узкое место — не сеть, а тяжёлый разбор HTML и обработка (CPU-bound), потоки упрутся в GIL. Тогда поможет multiprocessing — несколько процессов с собственными интерпретаторами.
Лучшая альтернатива — async
Для тысяч одновременных запросов потоки расходуют слишком много памяти. Асинхронность (asyncio + aiohttp) держит десятки тысяч соединений в одном потоке. Это отдельная большая тема — см. «Асинхронный парсинг на Python».
Важно: высокая параллельность ≠ право «класть» чужой сервер. Ограничивайте скорость и уважайте
robots.txt.
7. Использование прокси
При интенсивном парсинге сайт банит ваш IP по числу запросов. Решение — пул прокси и ротация.
Простое подключение прокси
proxies = {
"http": "http://user:pass@123.45.67.89:8080",
"https": "http://user:pass@123.45.67.89:8080",
}
response = requests.get(url, proxies=proxies, timeout=15)
Ротация прокси
import random
import requests
PROXIES = [
"http://user:pass@ip1:port",
"http://user:pass@ip2:port",
"http://user:pass@ip3:port",
]
def fetch_with_rotation(url):
proxy = random.choice(PROXIES)
return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=15)
Типы прокси
- Datacenter — дёшево и быстро, но легко вычисляются и банятся.
- Residential — IP реальных провайдеров, дороже, но менее заметны.
- Mobile — IP мобильных операторов, самые «доверенные», самые дорогие.
В продакшене разумно вести список «здоровых» прокси: отбраковывать те, что вернули таймаут или 403, и периодически их проверять.
8. Парсинг через TOR
TOR — бесплатный способ менять выходной IP. Это медленнее платных прокси и подходит для скромных объёмов, но не требует затрат.
Подключение
После установки TOR (демон или Tor Browser) он поднимает SOCKS5-прокси на 127.0.0.1:9050:
import requests
proxies = {
"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050",
}
# нужен пакет: pip install requests[socks]
r = requests.get("https://httpbin.org/ip", proxies=proxies)
print(r.json()) # увидите IP выходного узла TOR, а не свой
Схема socks5h (с буквой h) важна: DNS-резолвинг идёт через TOR, а не локально — иначе утечёт ваш реальный DNS-запрос.
Смена личности (нового IP)
Чтобы получить новый выходной узел, отправьте сигнал NEWNYM через управляющий порт (9051) библиотекой stem:
from stem import Signal
from stem.control import Controller
def renew_tor_ip():
with Controller.from_port(port=9051) as controller:
controller.authenticate(password="ваш_пароль")
controller.signal(Signal.NEWNYM)
Управляющий порт нужно включить в torrc и задать хэш пароля (tor --hash-password).
Учтите: многие крупные сайты знают список выходных узлов TOR и блокируют их или показывают капчу. TOR хорош для нечувствительных к скорости задач и невысоких объёмов.
9. Работа с HTTPS/SSL
По умолчанию requests проверяет SSL-сертификаты через пакет certifi. Чаще всего ничего настраивать не нужно. Проблемы возникают на сайтах с самоподписанными или просроченными сертификатами.
Отключение проверки (только для отладки)
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get(url, verify=False) # НЕ для продакшена
Отключать проверку небезопасно — открывается возможность MITM-атаки. Это допустимо только локально для отладки.
Правильный путь — указать свой CA
response = requests.get(url, verify="/path/to/custom-ca-bundle.crt")
Обновление корневых сертификатов
Если ловите SSLCertVerificationError на нормальных сайтах, обновите certifi:
pip install --upgrade certifi
10. Работа с cookie
Cookie нужны для сессий, авторизации и обхода «защитных» страниц, которые ставят токен и редиректят.
Session хранит cookie автоматически
import requests
session = requests.Session()
# логинимся — сервер вернёт cookie сессии
session.post("https://example.com/login", data={"user": "u", "pass": "p"})
# последующие запросы уже авторизованы
profile = session.get("https://example.com/profile")
Session сам хранит и подставляет cookie между запросами, а заодно переиспользует TCP-соединения (быстрее) и общие заголовки.
Передать cookie вручную
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)
Прочитать полученные cookie
response = requests.get(url)
for name, value in response.cookies.items():
print(name, value)
11. Статус ответа и заголовки
Контролировать ответ сервера обязательно — иначе вы будете парсить страницу ошибки как «данные».
response = requests.get(url)
print(response.status_code) # 200, 404, 403, 500 ...
print(response.reason) # 'OK', 'Not Found'
print(response.headers["Content-Type"])
print(response.headers.get("Server"))
print(response.url) # финальный URL после редиректов
print(response.elapsed) # время ответа
Грамотная обработка статусов
if response.status_code == 200:
parse(response.text)
elif response.status_code == 404:
log("Страница не найдена")
elif response.status_code == 429:
# Too Many Requests — нас тормозят
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
elif response.status_code in (403, 503):
rotate_proxy() # вероятно, бан — меняем IP
Заголовок Retry-After подсказывает, сколько ждать перед повтором. Статусы 403/503 часто означают анти-бот-защиту: помогут смена прокси, User-Agent и пауза.
12. Хранение URL и очередей (обзорно)
Когда парсер обходит сайт целиком, нужно где-то хранить «ещё не посещённые» и «уже посещённые» URL. Это называется frontier (граница обхода).
Простейший вариант — множества в памяти
from collections import deque
to_visit = deque(["https://example.com"])
visited = set()
while to_visit:
url = to_visit.popleft()
if url in visited:
continue
visited.add(url)
# ... скачать, разобрать, добавить новые ссылки в to_visit
set обеспечивает мгновенную дедупликацию, deque работает как очередь FIFO.
Когда данных много
- Redis — общая очередь для нескольких воркеров, переживает перезапуск. Списки/множества Redis идеальны для распределённого обхода.
- БД (PostgreSQL/SQLite) — таблица URL со статусами
new / in_progress / done / failed, удобно для устойчивости и аналитики. - Очереди задач (Celery + брокер, RabbitMQ) — когда парсинг встроен в приложение; см. «Парсинг на Django».
- Bloom-фильтр — экономит память при миллионах URL: вероятностная проверка «видели ли мы этот URL».
В Scrapy управление очередью, дедупликацией и приоритетами встроено из коробки — это одна из главных причин выбрать фреймворк на больших проектах. Подробности — в «Парсинг сайтов на Scrapy».
13. Плюсы и минусы реализации на Python
Плюсы:
- Низкий порог входа, читаемый код, быстрый прототип.
- Богатейшая экосистема: от requests до Scrapy и Playwright.
- Огромное сообщество — почти любая проблема уже решена.
- Простая интеграция с анализом данных (pandas, numpy) и БД.
Минусы:
- GIL ограничивает CPU-bound обработку (обходится через async и multiprocessing).
- Чистый Python медленнее компилируемых языков на тяжёлом разборе (спасает lxml/selectolax на C).
- Динамические JS-сайты требуют тяжёлых браузерных движков.
- Хрупкость: при смене вёрстки сайта селекторы ломаются — нужна поддержка.
14. Юридические и этические аспекты
Парсинг — мощный инструмент, и пользоваться им стоит ответственно:
- Соблюдайте robots.txt и условия использования сайта.
- Не создавайте чрезмерную нагрузку: ставьте задержки, ограничивайте параллельность.
- Не собирайте персональные данные без законных оснований (помните про 152-ФЗ и GDPR).
- Указывайте честный User-Agent там, где это уместно, и кэшируйте, чтобы не дёргать сервер лишний раз.