Веб-скрапинг даёт вам данные, но их хранение и анализ не менее важны. Папка с разрозненными файлами JSON или CSV вполне годится для разовой выгрузки, однако как только вы захотите отслеживать цены во времени, сравнивать записи из разных источников или ответить на реальный вопрос, эта плоская куча превращается в узкое место. База данных SQL даёт скрапленным данным структурированный дом, который можно запрашивать, фильтровать и агрегировать, не написав каждый раз одноразовый код для разбора.
Это руководство выстраивает весь путь от начала до конца с помощью Python и SQL. Вы скрапите небольшой набор записей в стиле товаров с нейтральной страницы-примера через Crawling API, спроектируете схему SQL, вставите разобранные строки и выполните аналитические запросы, чтобы извлечь из них инсайты. Руководство остаётся в рамках публичных, иллюстративных данных и URL-заглушки, так что вы сможете безопасно следовать ему и позже подставить свою цель.
Что вы создадите
Запускаемый поток на Python, который получает отрендеренную страницу списка через Crawling API, разбирает каждую карточку товара с помощью BeautifulSoup, записывает записи в таблицу SQLite, а затем запрашивает эту таблицу на предмет ценовых диапазонов, средних значений и количества по категориям. Каждая скрапленная запись несёт следующие поля:
- Name название товара, показанное на карточке списка.
- Price указанная цена, сохранённая как число, чтобы вы могли сравнивать и агрегировать её.
- Category раздел или тип товара, к которому относится позиция.
- URL ссылка на собственную страницу товара.
- Scraped at временная метка, которую база данных заполняет автоматически, чтобы вы могли отслеживать, когда была собрана каждая строка.
Зачем хранить скрапленные данные в SQL
То, куда попадают данные, решает, насколько они будут полезны позже. Файлы работают, пока вам не понадобится задать вопрос, охватывающий строки: какие позиции дешевле ценового порога, какова средняя цена по категории, как выгрузка этой недели сравнивается с прошлой. SQL был создан именно для этого, и реляционная база данных даёт вам несколько вещей, которых нет у папки с файлами.
- Структурированное хранение. Таблицы и столбцы обеспечивают единую форму, так что каждая запись имеет одни и те же поля одних и тех же типов.
-
Эффективные запросы. Один
SELECTфильтрует, сортирует и агрегирует тысячи строк без какого-либо ручного перебора. - Целостность данных. Первичные ключи, типы и ограничения сохраняют данные согласованными и отлавливают некорректные строки на входе.
- Масштабируемость. Храните ли вы тысячи или миллионы записей, движок SQL справляется с ростом и остаётся быстрым при правильных индексах.
- Повторяемый анализ. Запросы можно использовать повторно. Написав один раз полезный запрос, вы запускаете его снова на завтрашних данных, ничего не перестраивая.
Если вы выбираете форматы хранения более широко, сравнение в статье JSON vs CSV охватывает случаи, когда плоский файл всё ещё правильный выбор, а руководство по моделированию данных заходит глубже в проектирование схемы, чем у нас есть место здесь.
Почему обычный запрос терпит неудачу на современной странице списка
Прежде чем работа с хранением станет важна, вам нужны чистые записи для хранения, и вот тут наивный HTTP-запрос обычно даёт сбой. Многие современные страницы списков рендерят своё содержимое в браузере через JavaScript: первый HTML-ответ это тонкая оболочка, а карточки товаров появляются только после того, как отработают скрипты страницы. Вытяните данные из этого первоначального ответа и вы захватите фрагмент списка или вообще ничего.
Вторая проблема это доступ. Сайты, публикующие коммерчески ценные списки, следят за автоматизированным трафиком и блокируют запросы с IP дата-центров или со всего, что не выглядит как настоящий браузер. Так что надёжному скраперу нужны две вещи в одном запросе: браузер, который рендерит страницу, и IP, который сайт воспринимает как реального посетителя. Вы можете построить это с помощью headless-браузера и пула ротируемых резидентских прокси, но поддержание этого стека в рабочем состоянии и есть бо́льшая часть работы. Crawling API сводит оба компонента в один вызов и возвращает готовый HTML, который вы можете разобрать. Что касается стороны разбора, полезными спутниками будут руководство по веб-скрапингу на Python и руководство по структурированию скрапленных данных.
Предварительные требования
Несколько вещей должны быть на месте, прежде чем вы напишете какой-либо код. Ни одна из них не займёт много времени.
Базовые Python и SQL. Вам должно быть комфортно запускать скрипт на Python и читать оператор SELECT. Вам не нужно быть администратором баз данных; это руководство использует SQLite, который поставляется вместе с Python, так что устанавливать сервер не придётся.
Python 3.8 или новее. Подтвердите вашу версию командой python --version. Если её нет, установите Python с python.org и убедитесь, что Python есть в вашем PATH.
Аккаунт и токен Crawlbase. Зарегистрируйтесь, откройте свою панель управления и скопируйте токен. Crawlbase включает до 20 000 бесплатных запросов для старта, чего вполне достаточно, чтобы пройти это руководство. Относитесь к токену как к паролю и держите его вне системы контроля версий. Если ваша цель рендерит содержимое на стороне клиента, используйте токен JavaScript (JS), чтобы страница была отрендерена до того, как вы её получите.
Настройте проект
Создайте виртуальное окружение, чтобы зависимости оставались изолированными, затем установите две библиотеки, нужные скраперу.
python --version python -m venv scraper_env source scraper_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой scraper_env\Scripts\activate вместо строки source. Пакет crawlbase это официальный клиент для Crawling API, а beautifulsoup4 разбирает возвращённый HTML, чтобы вы могли извлечь отдельные поля. И sqlite3, и json поставляются со стандартной библиотекой Python, так что для шага хранения больше ничего устанавливать не нужно.
Шаг 1: Получите отрендеренную страницу
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его своим токеном и запросите URL списка. Мы используем заглушку, https://example.com/products; направьте её на любую публичную страницу списка, которую вы имеете право собирать. Проверка cb_status (legacy pc_status) Crawlbase перед разбором делает сбои громкими, а не молчаливыми.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) LISTING_URL = "https://example.com/products" def crawl(page_url): response = api.get(page_url) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None if __name__ == "__main__": html = crawl(LISTING_URL) print(html[:500] if html else "No HTML returned")
Запустите это и вы должны увидеть напечатанную обратно настоящую разметку страницы, что подтверждает работу запроса до того, как вы напишете хоть один селектор. Если ваша цель рендерит свои карточки на стороне клиента, передайте вместо этого токен JS и добавьте {"ajax_wait": "true", "page_wait": 5000} вторым аргументом к api.get, чтобы у динамического содержимого было время загрузиться.
Функция crawl выше скрывает самую сложную часть: получение готового HTML обратно со страницы, которая рендерится в браузере и блокирует трафик дата-центров. Crawling API запускает страницу в настоящем браузере, ротирует резидентские IP на стороне сервера и отдаёт вам отрендеренный HTML за один вызов, так что вы избавлены от запуска собственного парка headless-браузеров и пула прокси. Начните на бесплатном тарифе и сначала направьте его на публичную страницу списка.
Шаг 2: Разберите записи, которые хотите хранить
Имея готовый HTML на руках, загрузите его в BeautifulSoup и извлеките по записи из каждой карточки товара. Селекторы ниже предполагают раскладку карточки с заголовком, ценой, тегом категории и ссылкой, распространённую форму для страниц списков. Каждый поиск защищён, так что отсутствующее поле возвращает безопасное значение по умолчанию, а не роняет запуск. Деталь, которая важна для хранения, это цена: скрапьте её как текст, затем уберите символ валюты и запятые, чтобы она попала в базу данных как число, которое можно сравнивать и усреднять.
import re from bs4 import BeautifulSoup def parse_price(text): if not text: return None cleaned = re.sub(r"[^0-9.]", "", text) return float(cleaned) if cleaned else None def parse_products(html): soup = BeautifulSoup(html, "html.parser") records = [] for card in soup.select("div.product-card"): name = card.select_one("h2.product-title") price = card.select_one("span.price") category = card.select_one("span.category") link = card.select_one("a.product-link") records.append({ "name": name.get_text(strip=True) if name else None, "price": parse_price(price.get_text() if price else None), "category": category.get_text(strip=True) if category else "Uncategorized", "url": link["href"] if link and link.get("href") else None, }) return records
parse_price убирает всё, что не является цифрой или десятичной точкой, так что "$1,299.00" становится числом с плавающей точкой 1299.0. Этот единственный шаг делает возможными последующие запросы по ценам: текст сортируется по алфавиту и не может быть усреднён, а число делает и то, и другое корректно. Подстройте селекторы (product-card, product-title, price, category, product-link) под разметку вашей реальной цели; остальная часть потока остаётся той же.
Шаг 3: Спроектируйте схему SQL
Теперь спроектируйте таблицу, которая хранит эти записи. Хорошая схема отражает форму вашей записи, выбирает правильный тип для каждого столбца и добавляет первичный ключ плюс временную метку, которую база данных заполняет за вас. Обратите внимание на выбор ниже: price имеет тип REAL (число, а не текст), так что его можно сравнивать и агрегировать, а scraped_at по умолчанию принимает текущее время, так что каждая строка фиксирует, когда она была собрана.
CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL, category TEXT, url TEXT UNIQUE, scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Speed up the price filters and category rollups you will run later CREATE INDEX IF NOT EXISTS idx_price ON products(price); CREATE INDEX IF NOT EXISTS idx_category ON products(category);
Два проектных решения стоит отметить. Ограничение UNIQUE на url не даёт вставить один и тот же товар дважды, что важно, потому что скраперы перезапускаются, а вы не хотите, чтобы вчерашние строки сегодня дублировались. Индексы держат запросы по цене и категории быстрыми по мере роста таблицы. В MySQL или PostgreSQL схема читается почти так же: пишите AUTO_INCREMENT или SERIAL вместо AUTOINCREMENT, а DECIMAL(10,2) более строгий выбор для денег, чем REAL. Руководство по моделированию данных охватывает, когда каждый тип является правильным выбором.
Шаг 4: Вставьте разобранные записи
С спроектированной таблицей подключитесь из Python, создайте её при необходимости и вставьте разобранные записи. Всегда используйте параметризованные запросы: заполнители ? позволяют драйверу заниматься экранированием, что не даёт некорректному тексту сломать вставку и закрывает возможность инъекций. executemany записывает каждую запись одним пакетным вызовом, что гораздо быстрее цикла из одиночных вставок, когда у вас сотни строк.
import sqlite3 SCHEMA = """ CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL, category TEXT, url TEXT UNIQUE, scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ def store_records(records, db_path="scraped_data.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(SCHEMA) rows = [ (r["name"], r["price"], r["category"], r["url"]) for r in records if r["name"] ] cursor.executemany( "INSERT OR IGNORE INTO products (name, price, category, url) " "VALUES (?, ?, ?, ?)", rows, ) conn.commit() inserted = cursor.rowcount conn.close() return inserted
INSERT OR IGNORE сочетается с ограничением UNIQUE на url из Шага 3: строка, чей URL уже присутствует, тихо пропускается, а не вызывает ошибку, так что перезапуск скрапера добавляет новые списки, не дублируя старые. Один коммит после пакета держит запись быстрой. Для MySQL применяется та же форма кода с mysql.connector вместо sqlite3 и заполнителями %s вместо ?; всё далее по потоку идентично.
Никогда не собирайте INSERT, вставляя скрапленный текст прямо в строку SQL. Скрапленные значения это недоверенный ввод и могут содержать кавычки или символы, которые ломают оператор или, хуже того, меняют его. Заполнители ? передают этот текст драйверу как данные, а не как код, что и безопаснее, и менее подвержено ошибкам.
Шаг 5: Проанализируйте данные с помощью запросов SQL
Данные теперь в запрашиваемой форме, что и есть весь смысл использования SQL. Вот три вида анализа, к которым вы будете обращаться постоянно: фильтрация и сортировка, агрегирование для сводок и группировка для понимания по категориям. Каждый из них это обычный SELECT, который вы можете запустить из оболочки SQLite, графического интерфейса базы данных или из Python.
Сначала отфильтруйте и отсортируйте. Это вытягивает каждый товар дешевле ценового порога, сначала самые дешёвые:
SELECT name, price, category FROM products WHERE price < 500 ORDER BY price ASC;
Затем агрегируйте всю таблицу в однострочную сводку. Функции вроде COUNT, AVG, MIN и MAX сворачивают множество строк в числа, которые вы на самом деле хотите показать:
SELECT COUNT(*) AS total_products, ROUND(AVG(price), 2) AS average_price, MIN(price) AS cheapest, MAX(price) AS most_expensive FROM products;
Наконец, сгруппируйте по категории, чтобы увидеть, как разбивается каталог. GROUP BY запускает агрегаты по одному разу на категорию, а HAVING фильтрует эти группы постфактум:
SELECT category, COUNT(*) AS items, ROUND(AVG(price), 2) AS avg_price FROM products GROUP BY category HAVING COUNT(*) > 1 ORDER BY avg_price DESC;
Чтобы запустить это из Python вместо оболочки, откройте то же соединение и считайте строки обратно. Этот фрагмент запускает свод по категориям и печатает каждую группу:
def category_summary(db_path="scraped_data.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" SELECT category, COUNT(*), ROUND(AVG(price), 2) FROM products GROUP BY category ORDER BY 3 DESC """) for category, count, avg_price in cursor.fetchall(): print(f"{category}: {count} items, avg {avg_price}") conn.close()
Если ваш анализ перерастает то, с чем комфортно справляется обычный SQL, перенесите таблицу в DataFrame и продолжайте там; руководство по анализу с pandas подхватывает ровно с этой точки.
Свяжите всё в один поток
Четыре функции связываются в единый запускаемый скрипт. Скрапьте, разберите, сохраните, затем проанализируйте:
def main(): html = crawl(LISTING_URL) if not html: return records = parse_products(html) inserted = store_records(records) print(f"Parsed {len(records)} records, inserted {inserted} new rows") category_summary() if __name__ == "__main__": main()
Каждый запуск получает страницу, записывает только новые строки благодаря INSERT OR IGNORE и печатает свод по категориям. Поставьте его на задание cron, и таблица становится растущей историей, которую можно запрашивать во времени, и вот тут SQL отрабатывает свою цену по сравнению с папкой датированных файлов CSV.
Как выглядит результат
Сохранённая запись несёт структурированные поля плюс назначенные базой данных id и временную метку. Выборка пары строк обратно возвращает что-то вроде этого:
[ { "id": 1, "name": "Wireless Headphones", "price": 129.99, "category": "Audio", "url": "https://example.com/products/wireless-headphones", "scraped_at": "2026-06-11 09:42:18" }, { "id": 2, "name": "Mechanical Keyboard", "price": 89.50, "category": "Accessories", "url": "https://example.com/products/mechanical-keyboard", "scraped_at": "2026-06-11 09:42:18" } ]
Запрос свода по категориям к той же таблице печатает компактный свод, по одной строке на категорию с количеством и средней ценой, что и есть тот вид инсайта, который куча сырых файлов не может дать вам без дополнительного кода.
Масштабирование за пределы одной страницы
Поток выше обрабатывает одну страницу. Реальные наборы данных охватывают многие, и несколько корректировок переводят это из демонстрации во что-то готовое к продакшену.
-
Пагинация. Большинство страниц списков выставляют параметр страницы или ссылку «далее». Пройдите циклом по страницам, вызывая
crawlиparse_productsдля каждой, и передайте объединённые записи вstore_recordsодним пакетом. -
Пакетируйте вставки.
executemanyи одинcommitна пакет уже минимизируют обращения. Для очень больших задач коммитьте каждые несколько тысяч строк, а не все сразу, чтобы сбой не потерял весь запуск. - Индексируйте то, что запрашиваете. Два индекса из Шага 3 покрывают фильтры по цене и категории. Добавьте индекс на любой другой столбец, по которому вы интенсивно фильтруете или сортируете.
- Переходите на async ради объёма. Для тысяч страниц асинхронный Crawler ставит запросы в очередь и доставляет результаты на webhook, так что вы не держите открытыми соединения, пока крауллите. Сочетайте его с пакетными вставками, и база данных не отстанет.
Если вы проектируете более крупную систему сбор-разбор-хранение-анализ, а не один скрипт, руководство по архитектуре конвейеров данных охватывает, как эти этапы сочетаются в масштабе.
Скрапинг ответственно
Держите сбор в рамках. Скрапьте только публичные данные, которые не находятся за логином, читайте и уважайте условия обслуживания сайта и его robots.txt, и держите частоту запросов разумной, чтобы не нагружать серверы цели. Когда данные затрагивают идентифицируемых лиц, применяются правила о приватности, такие как GDPR и CCPA, так что избегайте персональных данных, если у вас нет законного основания и явной потребности в них. Пример здесь использует URL-заглушку и иллюстративные поля товаров как раз для того, чтобы держаться в стороне от этих вопросов; применяйте то же суждение, когда направляете скрапер на реальный сайт.
Ключевые выводы
- SQL превосходит плоские файлы для анализа. Структурированная таблица позволяет фильтровать, агрегировать и группировать тысячи записей одним запросом вместо одноразового кода для разбора.
- Очищайте данные на входе. Убирайте символы валюты и запятые, чтобы цена попадала как число; числовой столбец сортируется и усредняется корректно там, где текст этого не делает.
-
Проектируйте схему обдуманно. Выбирайте настоящие типы, добавьте первичный ключ и
UNIQUEURL, чтобы блокировать дубликаты, и индексируйте столбцы, по которым фильтруете. -
Вставляйте безопасно и пакетами. Параметризованные заполнители
?иexecutemanyс одним коммитом на пакет держат вставки и защищёнными, и быстрыми. -
Анализируйте обычным SELECT.
WHERE,ORDER BY, агрегатные функции иGROUP BYпревращают сохранённые строки в ценовые диапазоны, средние значения и понимание по категориям.
Часто задаваемые вопросы
Зачем хранить скрапленные данные в SQL вместо файла CSV или JSON?
Файлы вполне годятся для одной выгрузки, но они делают межстрочные вопросы болезненными. База данных SQL обеспечивает единую структуру, позволяет фильтровать, сортировать и агрегировать одним запросом и масштабируется до миллионов строк с индексами. Она также чисто обрабатывает повторные запуски: ограничение UNIQUE останавливает дубликаты, так что вы можете дописывать в ту же таблицу со временем, а не жонглировать датированными файлами.
Какую базу данных мне выбрать: SQLite, MySQL или PostgreSQL?
SQLite идеальна для малых и средних проектов и локальной работы, потому что она поставляется с Python и не требует сервера, поэтому это руководство и использует её. MySQL подходит для веб-приложений и более крупных общих наборов данных, а PostgreSQL силён для сложных запросов и аналитики. Поток на Python здесь меняет только соединение и синтаксис заполнителей при переходе между ними; схема и запросы остаются почти идентичными.
Как избежать вставки одной и той же записи дважды?
Поставьте ограничение UNIQUE на столбец, идентифицирующий запись, например URL товара, затем вставляйте с INSERT OR IGNORE (или ON CONFLICT в PostgreSQL). База данных пропускает любую строку, чей ключ уже существует, так что перезапуск скрапера добавляет только новые списки вместо дублирования старых.
Зачем преобразовывать цену в число перед хранением?
Потому что цена, хранимая как текст вроде "$1,299.00", не может быть корректно сравнена или усреднена: она сортируется по алфавиту и ломает агрегатные функции. Убирание символа и запятых для хранения числового price означает, что WHERE price < 500, AVG(price) и ORDER BY price все ведут себя так, как вы ожидаете.
Нужен ли мне токен JavaScript для этого?
Только если ваша цель рендерит своё содержимое на стороне клиента. Обычный токен возвращает статический HTML, чего достаточно для страниц с серверным рендерингом. Если список заполняется после того, как отработают скрипты страницы, используйте токен JS и добавьте опции ajax_wait и page_wait, чтобы динамическое содержимое загрузилось до захвата HTML.
Как масштабировать это на многие страницы, не перегружая базу данных?
Пагинируйте скрапинг и передавайте объединённые записи в пакетную вставку executemany, коммитя каждые несколько тысяч строк, а не после каждой. Для крупных краулов асинхронный Crawler ставит запросы в очередь и постит результаты на webhook, так что вы не держите соединения открытыми, а индексирование столбцов, которые вы запрашиваете, держит чтение быстрым по мере роста таблицы.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
