Скрапинг страницы, лишь половина работы. Полученные записи должны храниться надёжно, в месте, доступном для коллег и инструментов анализа, и там, где сбой ноутбука не сотрёт их. Локальный CSV подходит для разовой задачи, но как только скрапинг превращается в регулярный пайплайн, один жёсткий диск становится уязвимостью: заканчивается место, передача данных между машинами становится неудобной, а один отказ диска уничтожает работу, которую нельзя восстановить.

Это руководство показывает, как сохранять данные парсинга в облаке с помощью Python от начала до конца. Вы создаёте небольшой работающий поток: получаете страницу через Crawling API, структурируете результаты в чистые записи, а затем записываете их в надёжные облачные хранилища: объектное хранилище с помощью S3-совместимых бакетов и управляемую реляционную базу данных. Везде используется нейтральный пример URL и переменные среды-заполнители для учётных данных, так что вы можете адаптировать это к собственной цели и провайдеру, не меняя структуру кода.

Что вы создадите

Python-скрипт, который скрапит небольшой датасет с публичного примера страницы-листинга через Crawling API, нормализует каждую строку в типизированную запись и отправляет эти записи в облако двумя способами. Можно оставить один путь или оба. Составные части:

  • Скрапинг отрендеренной страницы, полученной через Crawling API, с возвратом готового HTML.
  • Трансформация сырого HTML в список структурированных записей с единообразными именами полей и типами.
  • Объектное хранилище: файл JSON Lines, загруженный в S3-совместимый бакет для дешёвого надёжного архивирования.
  • Управляемая база данных: те же записи, вставленные в таблицу Postgres для запросов и соединений.
  • Crawlbase Cloud Storage: опциональный однопараметрический путь, сохраняющий сырой ответ краулера на стороне сервера.

Зачем хранить данные парсинга в облаке

Локальное хранилище удобно до определённого момента. По мере роста скрапинга с нескольких сотен строк до регулярной задачи, питающей дашборд, одновременно возникают три проблемы. Ёмкость становится регулярной статьёй расходов: покупаете диски для хранения резервных копий и тратите время на управление ими. Доступ становится неудобным: данные, заблокированные на одной машине, сложно передать команде или передать в инструмент, работающий в другом месте. И надёжность хрупка: проблемы с питанием, повреждения прошивки и обычная человеческая ошибка могут вывести из строя один диск, а с ним, любую работу, которую не скопировали куда-то ещё.

Облачное хранилище решает все три проблемы. Объектные хранилища и управляемые базы данных созданы для избыточности, поэтому ваши данные реплицируются по разным местоположениям, а не хранятся на одном диске. Они масштабируются без самостоятельного выделения оборудования, доступны из любого места при наличии учётных данных и передают провайдеру ответственность за резервное копирование и надёжность. Для пайплайна парсинга это означает, что вы можете считать собранные данные надёжным активом с момента их поступления, а не чем-то, за чем нужно следить на локальном диске.

Два назначения и когда использовать каждое

В этом руководстве данные записываются в два вида облачных хранилищ, потому что спарсенные данные обычно нуждаются в обоих. Объектное хранилище (S3-совместимые бакеты), правильный дом для сырых и архивных данных: дешёвое за гигабайт, безразличное к форме данных и идеальное для хранения нетронутого вывода скрапинга, чтобы можно было повторно обработать его позже. Управляемая реляционная база данных (здесь Postgres), правильный дом для структурированной, пригодной для запросов копии, где единообразные столбцы и типы позволяют фильтровать, агрегировать и соединять с помощью SQL. Общая практика, писать в оба хранилища, и код ниже делает именно это. Подробнее см. в статьях облачное хранилище против локального и преимущества облачного хранилища.

Предварительные требования

Перед написанием кода нужно подготовить несколько вещей. Это занимает немного времени.

Python 3.8 или выше. Проверьте версию с помощью python --version. Если не установлен, установите с python.org или через дистрибутив вроде Anaconda и убедитесь, что Python есть в PATH.

Аккаунт Crawlbase и токен. Зарегистрируйтесь, откройте дашборд и скопируйте токен. Crawlbase включает до 20 000 бесплатных запросов для старта, чего вполне достаточно для работы с этим руководством. Обращайтесь с токеном как с паролем и не включайте его в систему контроля версий. Если ваша цель загружает контент на стороне клиента, используйте JavaScript-токен; для статической страницы подойдёт обычный токен.

Облачные учётные данные. Для пути объектного хранилища нужен S3-совместимый бакет и пара ключей доступа. Для пути к базе данных нужна строка подключения к управляемому экземпляру Postgres. Оба передаются через переменные среды в коде ниже, не жёстко зашитыми.

Знакомство с Python и базовым скрапингом. Если сторона парсинга для вас нова, хорошими спутниками будут руководство по BeautifulSoup и руководство по скрапингу с Python.

Настройка проекта

Создайте виртуальное окружение, чтобы зависимости оставались изолированными, затем установите библиотеки, необходимые для потока.

bash
python --version

python -m venv cloud_env
source cloud_env/bin/activate

pip install crawlbase beautifulsoup4 boto3 psycopg2-binary

В Windows активируйте окружение с помощью cloud_env\Scripts\activate вместо строки с source. Четыре зависимости выполняют работу: crawlbase, официальный клиент для Crawling API, beautifulsoup4 парсит возвращённый HTML, boto3 взаимодействует с S3-совместимым объектным хранилищем, а psycopg2-binary подключается к Postgres. Модуль json поставляется со стандартной библиотекой, поэтому формат архивирования не требует ничего дополнительного.

Шаг 1: Скрапинг страницы через Crawling API

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с вашим токеном и запросите целевой URL. Проверка cb_status (legacy pc_status) от Crawlbase перед парсингом делает сбои явными, а не тихими. Здесь используется нейтральная пример-страница листинга; замените URL на свой при адаптации потока.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

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__":
    page_url = "https://example.com/products"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

Запустите с помощью python cloud_pipeline.py, и вы должны увидеть напечатанную разметку реальной страницы, подтверждая, что получение работает до написания единственного селектора. Если ваша цель заполняет контент на стороне клиента, инициализируйте клиент с JavaScript-токеном и передайте {"ajax_wait": "true", "page_wait": 5000} в api.get, чтобы API сначала отрендерил страницу. Для JS-тяжёлых целей руководство скрапинг JavaScript-страниц с Python охватывает детали.

Crawlbase Crawling API

Единственный вызов api.get выше делает больше, чем обычный запрос. Crawling API рендерит страницу при передаче JavaScript-токена, ротирует резидентные IP на стороне сервера и обрабатывает CAPTCHA, так что вы получаете готовый HTML без самостоятельного запуска флота headless-браузеров или пула прокси. Сначала направьте его на публичную страницу на бесплатном тарифе, затем масштабируйте тот же код.

Шаг 2: Трансформация HTML в структурированные записи

Сырой HTML, это не то, что нужно хранить непосредственно в базе данных. Шаг трансформации превращает его в список словарей с единообразными именами полей и типами, так что каждая запись имеет одинаковую форму. Загрузите HTML в BeautifulSoup, пройдите по каждому элементу страницы и извлеките нужные поля. Здесь используются иллюстративные селекторы; замените их на те, которые соответствуют вашей цели.

python
from bs4 import BeautifulSoup

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def to_price(raw):
    if not raw:
        return None
    digits = raw.replace("$", "").replace(",", "").strip()
    return float(digits) if digits.replace(".", "").isdigit() else None

def transform(html):
    soup = BeautifulSoup(html, "html.parser")
    records = []
    for card in soup.select("div.product"):
        records.append({
            "name": text_of(card, "h2.title"),
            "price": to_price(text_of(card, "span.price")),
            "sku": text_of(card, "span.sku"),
            "in_stock": text_of(card, "span.stock") == "In stock",
        })
    return [r for r in records if r["name"]]

Три небольших вспомогательных функции поддерживают чистоту записей. text_of возвращает очищенный текст элемента или None, если он отсутствует, чтобы пробел в одной карточке не ломал цикл. to_price убирает символ валюты и разделители тысяч и приводит к числу с плавающей точкой, чтобы столбец базы данных мог быть числовым, а не текстовым. Финальный фильтр отбрасывает строки без имени, которые обычно являются артефактами разметки, а не реальными элементами. Результат, список типизированных записей, готовых к хранению. Подробнее о правильном формировании спарсенных данных, в статье структурирование и очистка веб-данных.

Шаг 3: Загрузка в S3-совместимое объектное хранилище

Первое облачное назначение, объектное хранилище. Оно является естественным домом для сырых или архивных данных: дешёвое, надёжное и безразличное к форме хранимого. Мы записываем записи в формате JSON Lines (по одному JSON-объекту на строку), который легко дополнять и потом читать потоком. Учётные данные поступают из переменных среды, чтобы ничего чувствительного не попало в исходный код.

python
import os
import json
import boto3

def upload_to_s3(records, key):
    s3 = boto3.client(
        "s3",
        endpoint_url=os.environ.get("S3_ENDPOINT_URL"),
        aws_access_key_id=os.environ["S3_ACCESS_KEY"],
        aws_secret_access_key=os.environ["S3_SECRET_KEY"],
    )
    body = "\n".join(json.dumps(r) for r in records)
    s3.put_object(
        Bucket=os.environ["S3_BUCKET"],
        Key=key,
        Body=body.encode("utf-8"),
        ContentType="application/x-ndjson",
    )
    print(f"Uploaded {len(records)} records to s3://{os.environ['S3_BUCKET']}/{key}")

Параметр endpoint_url делает это S3-совместимым, а не только AWS: оставьте его не установленным для AWS S3 или направьте на любой S3-совместимый провайдер (например, собственный экземпляр MinIO или объектное хранилище другого облака). Установите четыре переменные среды перед запуском, например export S3_BUCKET=my-scrape-archive и соответствующие ключи. Key, это путь объекта внутри бакета; датированный ключ вроде scrapes/2026-06-11/products.jsonl позволяет хранить каждый запуск отдельно и легко его найти.

Держите учётные данные вне кода

Никогда не зашивайте ключи доступа или строки подключения в скрипт, который фиксируете в репозитории. Читайте их из переменных среды или менеджера секретов, как это делает код здесь. Ключ, попавший в систему контроля версий, это ключ, который придётся ротировать.

Шаг 4: Вставка в управляемую базу данных

Второе назначение, управляемая база данных Postgres, где хранится структурированная копия для запросов. Функция ниже открывает соединение из единственной переменной среды, убеждается, что целевая таблица существует, и вставляет записи. Использование параметризованных запросов (заполнители %s) обеспечивает правильное экранирование значений вместо конкатенации в SQL.

python
import os
import psycopg2
from psycopg2.extras import execute_values

CREATE = """
CREATE TABLE IF NOT EXISTS products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price NUMERIC,
    sku TEXT,
    in_stock BOOLEAN,
    scraped_at TIMESTAMPTZ DEFAULT now()
)
"""

def save_to_db(records):
    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    with conn, conn.cursor() as cur:
        cur.execute(CREATE)
        rows = [(r["name"], r["price"], r["sku"], r["in_stock"]) for r in records]
        execute_values(
            cur,
            "INSERT INTO products (name, price, sku, in_stock) VALUES %s",
            rows,
        )
    conn.close()
    print(f"Inserted {len(records)} rows into products")

Установите DATABASE_URL в строку подключения к управляемому Postgres, например postgresql://user:pass@host:5432/dbname, и держите её в переменных среды, а не в файле. CREATE TABLE IF NOT EXISTS делает функцию безопасной для многократного запуска, столбец scraped_at проставляет временную метку каждой загрузки, чтобы можно было отслеживать изменения во времени, а execute_values объединяет вставки в один обмен данными вместо одного запроса на строку. После попадания строк в базу можно фильтровать и агрегировать с обычным SQL, затем вытягивать в pandas для анализа.

Шаг 5: Сборка полного пайплайна

Теперь соедините шаги в один работающий скрипт: скрапинг, трансформация, затем отправка записей в оба назначения. Оставьте тот вызов хранилища, который подходит для вашего рабочего процесса; здесь показаны оба.

python
import os
import json
from datetime import date
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

# crawl, transform, upload_to_s3 and save_to_db are defined above

def main():
    page_url = "https://example.com/products"
    html = crawl(page_url)
    if not html:
        print("Nothing scraped, stopping.")
        return

    records = transform(html)
    print(f"Parsed {len(records)} records")
    if not records:
        return

    key = f"scrapes/{date.today().isoformat()}/products.jsonl"
    upload_to_s3(records, key)
    save_to_db(records)

if __name__ == "__main__":
    main()

Поток линейный и простой для понимания: получаем страницу, выходим досрочно при неудаче скрапинга, трансформируем HTML в записи, выходим снова если нечего хранить, затем архивируем записи в бакет и загружаем в базу данных. Датированный ключ объекта хранит архив каждого запуска отдельно, в то время как база данных накапливает каждую загрузку с временной меткой. Запустите с помощью python cloud_pipeline.py после установки переменных среды.

Как выглядит вывод

Путь объектного хранилища записывает файл JSON Lines, по одной записи на строку, который попадает в бакет:

json
{"name": "Aluminium Tripod", "price": 129.99, "sku": "TRP-014", "in_stock": true}
{"name": "USB-C Hub", "price": 39.5, "sku": "HUB-203", "in_stock": false}
{"name": "Wireless Mouse", "price": 24.0, "sku": "MSE-088", "in_stock": true}

Путь к базе данных хранит те же записи в виде типизированных столбцов, поэтому быстрый запрос подтверждает загрузку и показывает форму, которую можно анализировать:

sql
SELECT name, price, in_stock FROM products WHERE in_stock = true ORDER BY price;

--      name       | price  | in_stock
-- ----------------+--------+----------
--  Wireless Mouse |  24.00 | t
--  Aluminium Tripod| 129.99 | t

С обеими копиями на месте у вас есть дешёвый надёжный архив сырых записей и пригодная для запросов структурированная таблица, записанные одним запуском.

Однопараметрический ярлык: Crawlbase Cloud Storage

Если цель, просто сохранить серверную копию каждого ответа краулера без предварительного создания собственного бакета или базы данных, Crawlbase Cloud Storage предлагает однопараметрический путь. Добавьте &store=true к запросу Crawling API, и копия ответа автоматически сохраняется в облаке, откуда её можно искать, получать или удалять позже через API или дашборд.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

response = api.get("https://example.com/products", {"store": "true"})
# the response also includes a storage RID you can use to fetch it later
print(response["headers"].get("storage_url"))

Каждый сохранённый запрос получает уникальный идентификатор (RID), который можно использовать для просмотра или удаления. Этот путь, самый быстрый способ сохранять сырые ответы, и он хорошо сочетается с асинхронным Crawler, когда выполняется много запросов и нужно, чтобы хранение обрабатывалось на стороне сервера. Для больших структурированных датасетов вам всё равно понадобится собственная база данных, но для архивирования сырых ответов параметр store трудно превзойти по простоте.

Масштабирование пайплайна

Описанный поток обрабатывает одну страницу. Превратить его в регулярную задачу, в основном вопрос темпа и устойчивости. Несколько привычек поддерживают здоровье большого запуска:

  • Пакетируйте записи. Накапливайте записи и загружайте или вставляйте их пакетами, а не по одной строке. execute_values уже пакетирует вставки в базу данных; делайте то же самое для загрузки в объектное хранилище, записывая один файл на запуск, а не на запись.
  • Датируйте ключи. Используйте датированный ключ объекта вроде scrapes/2026-06-11/products.jsonl, чтобы каждый запуск был изолирован и история никогда не перезаписывалась. Столбец scraped_at базы данных выполняет ту же роль на стороне запросов.
  • Запускайте по расписанию. Оберните скрипт в cron-задачу или планировщик, чтобы облачная копия оставалась актуальной. Поскольку таблица использует IF NOT EXISTS, а ключ бакета датирован, повторные запуски безопасны.
  • При масштабе передавайте получение данных. Для многих страниц асинхронный Crawler ставит запросы в очередь и доставляет результаты на webhook, что подходит для большого объёма без удержания открытых соединений.

Ответственный скрапинг

Собирайте данные ответственно. Скрапьте только публичные данные, соблюдайте условия использования каждого сайта и его robots.txt, и держите разумную скорость запросов, чтобы не нагружать сервера, от которых зависите. Когда собранные данные включают что-либо, связанное с идентифицируемыми людьми, применяются законы о конфиденциальности, такие как GDPR и CCPA: избегайте персональных данных, если у вас нет законного основания и чёткой цели их хранения. Хранение данных в облаке ничего из этого не меняет: та же тщательность, с которой вы их собираете, должна проявляться и в том, как вы их храните, а хранение персональных данных дольше, чем нужно, только добавляет риски.

Итоги

Ключевые выводы

  • Хранилище, часть пайплайна. Считайте спарсенные записи надёжным активом с момента их поступления; локальный CSV подходит для разовой задачи, но не для регулярной.
  • Трансформируйте перед хранением. Нормализуйте сырой HTML в типизированные записи с единообразными именами полей, чтобы столбец базы данных мог быть числовым, а архив оставался согласованным.
  • Используйте подходящее хранилище для задачи. Объектное хранилище (S3-совместимые бакеты) дешёво и надёжно для сырых или архивных данных; управляемая база данных, для структурированной пригодной для запросов копии, и запись в оба является распространённой практикой.
  • Держите учётные данные вне кода. Читайте ключи доступа и строки подключения из переменных среды или менеджера секретов, никогда не зашивайте их в зафиксированный скрипт.
  • Параметр store, ярлык. Добавление &store=true сохраняет сырой ответ краулера в Crawlbase Cloud Storage одним параметром, что является самым быстрым способом сохранять ответы без создания собственной инфраструктуры.

Часто задаваемые вопросы

Хранить спарсенные данные в объектном хранилище или в базе данных?

Зависит от того, что вы с ними делаете. Объектное хранилище (S3-совместимые бакеты) дешёво, надёжно и идеально для сырых или архивных данных любой формы, поэтому это правильный дом для нетронутого вывода скрапинга. Управляемая реляционная база данных, для структурированной копии, которую вы запрашиваете, фильтруете и соединяете с помощью SQL. Многие пайплайны пишут в оба: архивируют сырые записи в бакет и загружают очищенные записи в базу данных.

Как хранить облачные учётные данные вне кода?

Читайте их из переменных среды или менеджера секретов, а не зашивайте жёстко. Код в этом руководстве извлекает ключи S3 и строку подключения Postgres из os.environ, поэтому ничего чувствительного не находится в зафиксированном файле. Ключ, попавший в систему контроля версий, это ключ, который придётся ротировать, поэтому держите их в переменных среды.

В чём разница между Crawlbase Cloud Storage и загрузкой в собственный бакет?

Crawlbase Cloud Storage, это однопараметрический путь: добавьте &store=true к запросу Crawling API, и сырой ответ сохраняется на стороне сервера, извлекаемый по RID, без собственной инфраструктуры. Загрузка в собственный бакет или базу данных даёт полный контроль над форматом, схемой, сроком хранения и местоположением, что нужно для структурированных датасетов. Эти подходы дополняют друг друга: параметр store для быстрого архивирования сырых ответов, собственные хранилища для обработанных данных.

Будет ли S3-код работать с провайдерами, отличными от AWS?

Да. Клиент boto3 принимает параметр endpoint_url; оставьте его не установленным для AWS S3 или направьте на любой S3-совместимый провайдер, например собственный экземпляр MinIO или объектное хранилище другого облака. Остальной код не меняется, поэтому в примере эндпоинт читается из переменной среды.

Как запускать это по расписанию, чтобы облачная копия оставалась актуальной?

Оберните скрипт в cron-задачу или планировщик с нужной периодичностью обновления данных. Пайплайн безопасен для повторного запуска: таблица базы данных использует CREATE TABLE IF NOT EXISTS, ключ объекта датирован, поэтому запуски никогда не перезаписывают друг друга, а каждая строка базы данных содержит временную метку scraped_at для отслеживания изменений во времени. Для многих страниц передайте получение асинхронному Crawler, чтобы задача не упиралась в одно соединение.

Безопасно ли хранить спарсенные персональные данные в облаке?

Рассматривайте это прежде всего как правовой и этический вопрос. Избегайте сбора данных, связанных с идентифицируемыми людьми, если у вас нет законного основания и чёткой цели, поскольку законы о конфиденциальности, такие как GDPR и CCPA, применяются независимо от места хранения данных. Если вы всё же храните персональные данные, храните только необходимое, не дольше нужного, и обеспечьте безопасность доступа. Хранение персональных данных дольше, чем требуется, только добавляет риски, не добавляя ценности.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга