Большинство данных в интернете живёт в неструктурированном HTML, разбросанном по страницам, созданным для человеческих глаз, а не для ваших скриптов. Веб-скрапинг: это способ превратить их в структурированные записи для сохранения, запросов и анализа, не копируя вручную и не дожидаясь, пока сайт выпустит API, который, может быть, никогда и не появится. Мониторинг цен, маркетинговые исследования, списки лидов и сбор обучающих данных: всё начинается одинаково: загрузить страницу, разобрать её и сохранить нужные поля.
Это руководство проведёт вас через полный Python-рабочий процесс от начала до конца. Вы настроите инструментарий, отправите запрос, разберёте HTML с помощью CSS-селекторов, обработаете страницы с JavaScript-рендерингом, пройдёте пагинацию, сохраните результаты как CSV или JSON и разберётесь с блокировками, которые появляются в момент масштабирования. Каждый фрагмент кода реален и пригоден для копирования, а весь пример ограничен публичными данными на учебном сайте, созданном специально для обучения.
Что вы создадите
Небольшой, полноценный Python-скрапер, который читает пагинированный список книжных объявлений с публичного учебного сайта, извлекает чистую запись из каждого, проходит все страницы до конца и записывает всё на диск. Та же форма: загрузить, разобрать, пройти в цикле, сохранить: является основой почти каждого скрапера, который вы когда-либо напишете.
- Title. Название товара из каждой карточки объявления.
- Price. Строка с отображаемой ценой, готовая к преобразованию в число.
- Availability. Есть ли товар в наличии.
- Rating. Звёздный рейтинг каждой карточки.
- URL. Абсолютная ссылка на страницу товара.
В качестве цели используется books.toscrape.com: песочница, созданная специально для практики скрапинга. Она статична, хорошо структурирована и доступна для скрапинга, так что вы можете сосредоточиться на технике, не борясь с блокировками с первой попытки.
Как работает веб-скрапинг
Скрапер: это просто HTTP-клиент плюс парсер. Клиент запрашивает URL, сервер возвращает HTML; парсер загружает этот HTML в дерево, которое можно запрашивать по тегу, классу или CSS-селектору, и вы копируете нужные значения в список записей. Поисковые системы работали так с первых краулеров 1993 года, и механика почти не изменилась: обнаруживать URL, загружать каждый, извлекать структурированные поля и двигаться дальше.
Что изменилось: это современный веб. Многие сайты теперь отдают почти пустую HTML-оболочку и рендерят видимый контент в браузере с помощью JavaScript, а большинство серьёзных целей защищаются от автоматического трафика. Эти два фактора: клиентский рендеринг и защита от ботов: объясняют, почему «исчерпывающее» руководство не может ограничиться requests и BeautifulSoup. Мы начнём с простого стека, потому что он учит основам, а затем покажем, где он ломается и что его заменяет.
Настройка инструментария Python
Экосистема Python для скрапинга богата, но для покрытия почти любой задачи достаточно нескольких инструментов. Вот современный инструментарий и момент, когда каждый элемент оправдывает своё место.
- requests отправляет HTTP-запросы и возвращает ответ. Это правильный выбор по умолчанию для статических страниц.
- BeautifulSoup разбирает HTML в навигируемое дерево и терпим к «грязной» разметке, которая всегда встречается на реальных страницах.
- lxml: быстрый бэкенд парсера, который BeautifulSoup может использовать, и он привносит полноценную поддержку XPath, когда она нужна.
- Selenium или Playwright управляют реальным браузером, поэтому могут рендерить JavaScript и взаимодействовать со страницей через клики и ввод текста.
- Scrapy: полноценный фреймворк для краулинга со встроенной конкурентностью, повторными попытками и конвейерами, когда один скрипт перерастает в реальный проект.
Если вы хотите более широкий обзор доступного, посмотрите статью о лучших Python-библиотеках для веб-скрапинга. Для этого руководства начните с чистого виртуального окружения и двух библиотек, которые делают основную работу.
python --version python -m venv scraper_env source scraper_env/bin/activate pip install requests beautifulsoup4 lxml
На Windows активируйте окружение командой scraper_env\Scripts\activate вместо строки с source. Вам нужен Python 3.8 или новее; проверьте командой python --version и установите с python.org, если его нет. При активном окружении вы готовы отправить первый запрос.
Шаг 1: Отправьте запрос и прочитайте ответ
Каждый скрапинг начинается с одного HTTP-запроса. Отправьте GET на URL, убедитесь, что код статуса равен 200, прежде чем делать что-либо ещё, и HTML страницы у вас в руках.
import requests url = "https://books.toscrape.com/catalogue/page-1.html" headers = {"User-Agent": "Mozilla/5.0 (scraper tutorial)"} response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: print(response.text[:500]) else: print(f"Request failed: {response.status_code}")
Две небольшие привычки сразу окупаются. Заголовок User-Agent делает ваш запрос похожим на браузерный, а не на анонимный скрипт, что предпочитают многие сайты. timeout не даёт скраперу зависнуть навсегда при зависшем сервере. Запустите это и в терминале должны появиться первые 500 символов реального HTML: это подтвердит, что загрузка работает, прежде чем вы напишете хотя бы один селектор.
Тот простой requests.get работает на статической учебной странице, но на реальной цели он ломается на JavaScript и блокируется в масштабе. Crawling API принимает тот же URL, рендерит страницу в реальном браузере за ротируемым жилым IP и возвращает готовый HTML, так что код парсинга в следующих шагах остаётся идентичным и вы не запускаете собственный headless-парк и пул прокси.
Шаг 2: Разберите HTML с помощью селекторов
Сырой HTML: просто строка. Чтобы выбирать элементы, загрузите его в BeautifulSoup, который превращает разметку в дерево, допускающее запросы по имени тега и CSS-классу. Откройте страницу в браузере, кликните правой кнопкой по карточке книги и выберите «Inspect», чтобы увидеть структуру: на этом сайте каждая книга находится в article.product_pod, название: в атрибуте title тега h3 a, цена: в p.price_color, наличие: в p.instock, а рейтинг закодирован как класс на p.star-rating.
from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, "lxml") books = soup.select("article.product_pod") print(f"Found {len(books)} books on this page")
Аргумент "lxml" говорит BeautifulSoup использовать быстрый бэкенд lxml, который вы установили; если пропустить установку, передайте "html.parser": он идёт в комплекте с Python. Метод select принимает CSS-селектор и возвращает список всех совпадений, так что article.product_pod отдаёт все двадцать карточек книг на странице. Если вы предпочитаете find и find_all, они делают то же самое в стиле вызовов методов. Подробный обзор обоих стилей: в статье how to use BeautifulSoup in Python, а о разнице между CSS-селекторами и XPath: в статье web scraping with XPath and CSS selectors.
Шаг 3: Извлеките чистые поля
Теперь извлеките данные из каждой карточки. Пройдите по элементам, прочитайте значение из каждого дочернего элемента и соберите один аккуратный словарь на книгу. Обернув селекторы в небольшой вспомогательный метод, вы не дадите отсутствующему полю упасть всему запуску.
from urllib.parse import urljoin BASE = "https://books.toscrape.com/catalogue/" def text_of(element, selector): el = element.select_one(selector) return el.get_text(strip=True) if el else None def parse_books(soup): rows = [] for card in soup.select("article.product_pod"): link = card.select_one("h3 a") rating = card.select_one("p.star-rating") rows.append({ "title": link["title"] if link else None, "price": text_of(card, "p.price_color"), "availability": text_of(card, "p.instock"), "rating": rating["class"][1] if rating else None, "url": urljoin(BASE, link["href"]) if link else None, }) return rows
Вспомогательный метод text_of запрашивает один элемент и возвращает None при его отсутствии, вместо того чтобы падать на вызове .get_text() к несуществующему элементу. Название и URL берутся из атрибутов, а не из текста, поэтому мы читаем их с тега <a> напрямую. Рейтинг хранится как второй класс в p.star-rating (например class="star-rating Three"), поэтому берём второе имя класса. urljoin превращает относительный href в абсолютный URL. Вызовите parse_books(soup) и получите чистый список словарей, по одному на книгу.
Шаг 4: Обработка страниц с рендерингом через JavaScript
Учебный сайт статичен: именно это делает его хорошей первой целью. Многие реальные сайты таковыми не являются: они отдают почти пустую оболочку и строят контент в браузере с помощью JavaScript. requests получает только эту исходную оболочку и никогда не выполняет скрипты, поэтому при разборе ответа поля, которые вы видели в браузере, просто отсутствуют.
Классическое решение: реальный браузер. Playwright (или Selenium) запускает Chromium, позволяет JavaScript страницы выполниться, а затем отдаёт вам полностью отрендеренный HTML, который передаётся в тот же парсер BeautifulSoup, который вы уже написали.
# pip install playwright && playwright install chromium from playwright.sync_api import sync_playwright def render(url): with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(url, wait_until="networkidle") html = page.content() browser.close() return html soup = BeautifulSoup(render(url), "lxml")
Параметр wait_until="networkidle" удерживает выполнение, пока страница не прекратит делать сетевые запросы, чего обычно достаточно для появления рендеренного на клиенте контента. Это работает, но headless-браузер тяжёлый: он медленный в объёме, прожорливый по памяти и хрупкий, когда сайт обнаруживает автоматизацию. Полное рассмотрение этой проблемы: в статьях how to scrape JavaScript pages with Python и отдельном руководстве по Playwright web scraping.
Шаг 5: Пройдите по пагинации
Одна страница: это демо; реальный каталог охватывает много страниц. На этом сайте ссылка на следующую страницу: элемент li.next a, и когда он исчезает, значит, мы дошли до конца. Цикл прост: загрузить текущую страницу, разобрать её, найти следующую ссылку и повторить, пока следующей ссылки нет.
import time def scrape_all(): all_rows = [] next_url = BASE + "page-1.html" while next_url: response = requests.get(next_url, headers=headers, timeout=10) if response.status_code != 200: print(f"Stopped at {next_url}: {response.status_code}") break soup = BeautifulSoup(response.text, "lxml") all_rows.extend(parse_books(soup)) next_link = soup.select_one("li.next a") next_url = urljoin(next_url, next_link["href"]) if next_link else None time.sleep(1) return all_rows
Цикл while next_url работает, пока селектор следующей ссылки не вернёт ничего, после чего next_url становится None и цикл естественно завершается. href сайта относительный, поэтому urljoin разрешает его относительно текущей страницы. time.sleep(1) между страницами: не опциональная вежливость на реальной цели: регулировка запросов: это самый простой способ оставаться в пределах ограничений скорости сайта.
Шаг 6: Сохраните данные в формате CSV или JSON
Данные, живущие только в памяти, исчезают при завершении скрипта. Запишите их на диск, чтобы открыть в таблице, загрузить в pandas или передать в то, что идёт дальше. Встроенные модули Python csv и json обрабатывают оба формата без дополнительных зависимостей. CSV идеален для плоских табличных записей; JSON сохраняет вложенную структуру и более удобен для других программ. Если не знаете, что выбрать, смотрите статью JSON vs CSV main differences.
import csv, json def save_csv(rows, filename="books.csv"): if not rows: return with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) def save_json(rows, filename="books.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(rows, f, indent=2, ensure_ascii=False) if __name__ == "__main__": data = scrape_all() save_csv(data) save_json(data) print(f"Saved {len(data)} books")
DictWriter сопоставляет ключи каждого словаря с CSV-столбцами, так что строка заголовка записывается автоматически по выбранным именам полей. newline="" предотвращает появление пустых строк между записями в Windows, а encoding="utf-8" сохраняет символы с диакритическими знаками. В более крупных проектах вы бы писали в базу данных вместо файла, но записи идентичны: список словарей легко ложится на строки SQL или документное хранилище. Запустите скрипт и получите полный экспорт каждой книги с каждой страницы. Перед вами полноценный, рабочий скрапер.
Как выглядит результат
Каждая запись: плоский словарь, который аккуратно сериализуется в JSON. Одна запись из books.json выглядит так.
{ "title": "A Light in the Attic", "price": "£51.77", "availability": "In stock", "rating": "Three", "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html" }
Цена всё ещё содержит символ валюты, а рейтинг: слово, а не число, что нормально: скраперы захватывают то, что показывает страница, а отдельный шаг очистки преобразует "£51.77" в 51.77 и "Three" в 3 перед анализом. Разделение извлечения и очистки на отдельные шаги упрощает отладку обоих.
Почему скраперы блокируют и как избежать блокировок
Учебный сайт никогда не сопротивляется, но реальные цели: да. Два барьера появляются в момент масштабирования, и ни один из них не решается настройкой селекторов.
Первый: защита от ботов. IP датацентров, повторяющиеся паттерны запросов и трафик, непохожий на браузерный, встречают CAPTCHA или прямую блокировку. Ваш скрапер может работать десяток запросов, а потом начать возвращать 403 или пустые страницы. Второй: клиентский рендеринг, рассмотренный в Шаге 4: обычный запрос не видит контент, который браузер строит с помощью JavaScript. Можно бороться с обоими самостоятельно, поддерживая пул ротируемых жилых прокси и запуская headless-парк, но их сборка и поддержание в рабочем состоянии: большая часть инженерных усилий, а данные, которые вы хотите получить, тут ни при чём.
Управляемый crawling API сворачивает оба в один запрос. Вы отправляете ему URL, он рендерит страницу в реальном браузере за доверенным ротируемым IP и возвращает готовый HTML для того же парсера, который вы уже написали. Установите официальный клиент рядом с существующими библиотеками.
pip install crawlbase
Держите токен Crawlbase под рукой: это ключ аутентификации для каждого вызова. Замена: одна строка: там, где вы вызывали requests.get, вы вызываете API, а возвращаемый HTML передаётся в ту же функцию parse_books.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def fetch(url): options = {"ajax_wait": "true", "page_wait": 2000} result = api.get(url, options) if result["status_code"] == 200: return result["body"].decode("utf-8") return None html = fetch(url) soup = BeautifulSoup(html, "lxml") # same parser, unchanged rows = parse_books(soup)
Параметры ajax_wait и page_wait важны для цели с клиентским рендерингом: ajax_wait ожидает завершения асинхронного контента, а page_wait задерживает выполнение на фиксированное количество миллисекунд, чтобы поздние элементы появились до снятия снимка. Поскольку API возвращает HTML, встроить его: изменение одной строки, а не переписывание. Полный план по обходу блокировок, включая стратегию заголовков и ротацию прокси,: в статье how to scrape websites without getting blocked.
Crawlbase предлагает обычный токен для загрузки статического HTML и JavaScript-токен, который сначала рендерит страницу в реальном браузере. Используйте обычный токен для статических страниц вроде учебного каталога; переключайтесь на JavaScript-токен для любого сайта, который строит контент на клиенте. Если разобранные поля возвращаются пустыми на реальной цели, JavaScript-токен обычно и есть решение.
Масштабирование за пределы одного скрипта
Паттерн «загрузить-разобрать-пройти в цикле-сохранить» продвигает далеко, но два требования в итоге выталкивают за пределы одного скрипта. Первое: конкурентность: загрузка страниц по одной медленная, когда у вас тысячи URL. Второе: структура: повторные попытки, дедупликация и конвейеры данных не вписываются в произвольный цикл. Именно здесь Scrapy оправдывает своё место. Он предоставляет параллельные запросы, автоматические повторные попытки, планирование запросов и item-конвейеры из коробки, так что вы описываете, что извлекать, а фреймворк берёт на себя оркестрацию.
Даже со Scrapy два барьера из предыдущего раздела никуда не деваются: вам всё равно нужен рендеринг для страниц с JavaScript и доверенные IP для обхода блокировок в объёме. Чистое разделение: пусть фреймворк управляет конкурентностью и конвейерами, а управляемый API берёт на себя рендеринг и ротацию, так что загрузчик Scrapy-паука просто маршрутизирует каждый запрос через Crawling API. Это удерживает код на данных и перекладывает инфраструктуру, не имеющую к ним отношения.
Скрапьте ответственно и в рамках закона
Скрапинг публичных данных в целом допустим, но то, как вы это делаете и что собираете, важнее самого факта. Прежде чем направить скрапер на любой сайт, прочитайте его robots.txt и условия использования: первый сигнализирует о путях, которые сайт просит автоматических клиентов избегать, а вторые устанавливают правила, с которыми вы соглашаетесь, используя сайт. Регулируйте запросы, чтобы никогда не нагружать сервер, честно идентифицируйте клиента и при наличии официального API сайта отдавайте ему предпочтение, поскольку API: это путь доступа, который владелец действительно создал для программного использования, избавляя вас от хрупкости HTML-парсинга.
Оставайтесь на правильной стороне черты, ограничивая сбор публичными непersonальными данными. Избегайте всего, что находится за логином, всего, что требует принятия условий, которые вы обходите, и персональных данных, подпадающих под режимы конфиденциальности, такие как GDPR и CCPA, где сбор может требовать согласия и правового основания. Не распространяйте собранные авторские медиаматериалы, и когда проект коммерческий или затрагивает регулируемые данные, получите юридическое согласование, как и для любого другого источника данных. Ответственный скрапинг: это в основном здравый смысл: берите только то, что публично, берите осторожно и уважайте пожелания, которые сайт уже опубликовал.
Ключевые выводы
- Основной цикл: загрузить, разобрать, пройти в цикле, сохранить. requests получает HTML, BeautifulSoup извлекает поля, пагинация обходит страницы, а модуль csv или json сохраняет результат.
- Подбирайте инструмент под страницу. requests и BeautifulSoup покрывают статические сайты; Playwright или Selenium рендерят JavaScript; Scrapy добавляет конкурентность и конвейеры в масштабе.
- Сначала инспектируйте, потом пишите селекторы. Откройте dev tools страницы, найдите теги и классы, хранящие ваши данные, и сопоставьте каждое поле с CSS-селектором.
- У обычного requests два ограничения. Он не может выполнять JavaScript и блокируется в масштабе: ни то, ни другое селекторы не исправят.
- Управляемый API решает оба в одном вызове. Crawling API рендерит страницу за доверенным ротируемым IP и возвращает готовый HTML, так что существующий парсер продолжает работать без изменений.
Часто задаваемые вопросы
Что такое веб-скрапинг?
Веб-скрапинг: это автоматизированное извлечение данных с веб-страниц. Скрипт запрашивает URL, сервер возвращает HTML, а парсер вытаскивает нужные поля и сохраняет их в структурированном формате: CSV, JSON или базе данных. Это способ превратить страницы, созданные для чтения людьми, в данные, которые можно запрашивать и анализировать в масштабе.
Какие библиотеки Python нужны для старта?
Для типичного статичного сайта достаточно requests и BeautifulSoup: requests загружает страницу, BeautifulSoup извлекает поля по тегу и CSS-классу. Добавьте lxml для более быстрого парсинга и поддержки XPath, Playwright или Selenium: когда сайт рендерится с помощью JavaScript, и Scrapy: когда для более крупного проекта нужны конкурентность и конвейеры.
Почему собранные данные пусты, хотя на странице явно есть контент?
Почти всегда потому, что сайт рендерит контент с помощью JavaScript. requests получает только исходную HTML-оболочку и не выполняет скрипты, поэтому данные, видимые в браузере, отсутствуют в том, что вы разбираете. Сначала отрендерите страницу: с headless-браузером или с JavaScript-токеном Crawling API,: прежде чем BeautifulSoup сможет найти поля.
Как скрапить несколько страниц?
Найдите ссылку или паттерн, который сайт использует для следующей страницы, затем запустите цикл. Если есть кнопка «следующая», следуйте её href до исчезновения, как показано в Шаге 5. Если URL следуют числовому паттерну вроде page-2.html, можно строить их в цикле с диапазоном. В любом случае добавьте небольшую задержку между страницами, чтобы оставаться в пределах допустимого и незаблокированным.
Как избежать блокировки при скрапинге?
Делайте паузы между запросами, отправляйте реалистичный заголовок User-Agent и не атакуйте один путь. В масштабе вам также нужны IP, похожие на реальных пользователей, а одна машина не может их предоставить. Маршрутизация через ротируемые жилые IP: через Crawling API или Smart AI Proxy: это то, что не даёт высокообъёмным запускам попадать под ограничения скорости и CAPTCHA.
Законен ли веб-скрапинг?
Скрапинг публичных данных в целом допустим, но зависит от условий использования сайта, вашей юрисдикции и того, что вы делаете с данными. Проверяйте robots.txt и условия перед началом, избегайте персональных данных, подпадающих под законы о конфиденциальности вроде GDPR и CCPA, никогда не скрапьте контент за логином и при наличии официального API отдавайте ему предпочтение. Если есть сомнения, собирайте только публичные данные и держите объём достаточно низким, чтобы не нагружать сервер.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
