Краулинг веб-страниц означает написание программного обеспечения, которое обходит набор URL, загружает каждый из них и извлекает структурированные поля из HTML. Именно так превращают страницы, созданные для человеческих глаз, в данные, пригодные для запросов: ценовые фиды для мониторинга, архивы статей для исследований, сетки объявлений для рыночного анализа или обучающий корпус для модели. Большинство таких данных публичны и лежат на виду, но читать их в каком-либо объёме вручную нереально, поэтому вы обращаетесь к краулеру.

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

Что вы построите

Scrapy-паук в одном файле, который загружает страницу результатов поиска через Crawling API и возвращает по одной структурированной записи на результат. В качестве рабочего примера используется поиск на Amazon, тот же, что в исходной версии этого руководства, и из каждой карточки товара извлекаются два поля:

  • Title, текст заголовка товара, показанный на карточке результата.
  • URL, ссылка с карточки на страницу товара.

Два поля сохраняют пример читаемым, а шаблон распространяется на любой нужный вам селектор. Та же форма паука работает для любого сайта, который у вас есть право краулить: замените начальный URL и селекторы, и цикл загрузки-разбора останется прежним.

Почему обычный запрос блокируется

Направьте голый Scrapy-запрос на загруженный коммерческий сайт, и два события обычно пойдут не так. Во-первых, многие страницы рендерят контент в браузере: начальный HTML, тонкая оболочка, и объявления появляются только после выполнения JavaScript страницы. Прямой фетч возвращает оболочку, поэтому парсить нечего. Во-вторых, крупные сайты следят за автоматизированным трафиком. Датацентровый IP, делающий быстрые, повторяющиеся запросы, не похожие на реального браузера, получает CAPTCHA или блокировку, нередко ещё до того, как вы увидите хоть один товар.

Значит, краулер, который реально работает, требует двух вещей в одном запросе: браузера, рендерящего страницу, и IP, который сайт читает как реального посетителя. Можно строить это самостоятельно с headless-браузером и пулом ротирующихся резидентных прокси, но сборка этих компонентов и поддержание их в рабочем состоянии, это и есть большая часть работы. Crawling API объединяет оба в одном вызове. Вы передаёте ему URL, он загружает страницу за доверенным резидентным IP (и рендерит её в реальном браузере, когда вы это запрашиваете) и возвращает Scrapy готовый HTML. Ваш паук работает с одним эндпойнтом и никогда не касается списка прокси.

Как работает маршрутизация

Вместо прямого запроса к целевому сайту ваш паук запрашивает https://api.crawlbase.com/?token=YOUR_CRAWLBASE_TOKEN&url=.... API загружает цель через свой пул IP от вашего имени и передаёт тело ответа обратно в Scrapy. С точки зрения Scrapy это обычный HTTP-ответ, поэтому все известные вам селекторы и пайплайны продолжают работать.

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

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

Базовый Python. Вы должны уверенно писать и запускать Python-скрипты, а также устанавливать пакеты через pip. Если вы только начинаете знакомиться с краулингом, обзорное руководство о том, как парсить сайт на Python, покрывает основы, которые данное руководство предполагает известными.

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

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

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

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

bash
python --version

python -m venv crawler_env
source crawler_env/bin/activate

pip install scrapy crawlbase

На Windows активируйте окружение командой crawler_env\Scripts\activate вместо строки с source. Две зависимости выполняют основную работу. scrapy, фреймворк для краулинга: управляет очередью запросов, загрузчиком и циклом разбора. crawlbase, официальный Python-клиент для Crawling API, а его класс CrawlingAPI имеет вспомогательный метод buildURL, который оборачивает любой целевой URL в правильный API-запрос с токеном и всем прочим, чтобы не приходилось вручную собирать эту строку запроса.

Scrapy запускает одного паука напрямую из файла командой scrapy runspider, поэтому для этого руководства не нужен полный каркас проекта. Создайте один файл для паука:

bash
touch myspider.py

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

Начните с паука, который ничего не делает, кроме как подтверждает работоспособность маршрутизации. Создайте подкласс scrapy.Spider, дайте ему name и задайте start_urls. Единственный нюанс здесь в том, что начальный URL, это не сама цель: вы оборачиваете её через api.buildURL, чтобы Scrapy запрашивал эндпойнт Crawling API, а уже API загружал цель за вас.

python
import scrapy
from crawlbase import CrawlingAPI

# Replace YOUR_CRAWLBASE_TOKEN with the token from your dashboard
api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

class AmazonSpider(scrapy.Spider):
    name = "amazonspider"

    # Target page to crawl, then route it through the Crawling API
    targets = ["https://www.amazon.com/s?k=cold+brew+coffee+maker"]
    start_urls = [api.buildURL(url, {}) for url in targets]

У паука пока нет метода parse, поэтому он загрузит страницу и остановится. Это сделано намеренно: вы хотите убедиться, что запрос доходит до цели через API и возвращается со статусом 200, прежде чем напишете хотя бы один селектор. Запустите из директории проекта:

bash
scrapy runspider myspider.py

В логе вы должны увидеть строку Crawled (200) для GET-запроса к api.crawlbase.com с целевым URL в параметре запроса url. Этот 200 и есть весь смысл этого шага: запрос прошёл через Crawling API, API загрузил страницу поиска Amazon за доверенным IP, и отрендеренный HTML вернулся в Scrapy. Поскольку парсера пока нет, Scrapy логирует, что колбэк parse по умолчанию не определён, и закрывает паука. Сантехника работает; теперь можно извлекать данные.

Crawlbase Crawling API

Строка Crawled (200) против сложной коммерческой цели, это именно то, на чём спотыкается большинство краулеров. Crawling API взял URL, переданный в buildURL, загрузил страницу за ротирующимся резидентным IP, при необходимости отрендерил её в реальном браузере и передал Scrapy готовый HTML, избавляя вас от необходимости самостоятельно запускать парк headless-браузеров и пул прокси. Сначала проверьте на своей цели на бесплатном уровне.

Шаг 2: парсинг полей с помощью CSS- и XPath-селекторов

Теперь добавьте метод parse. Scrapy автоматически вызывает его с объектом response для каждой загруженной страницы, а ответ предоставляет как CSS-, так и XPath-селекторы над HTML. Для каждой карточки товара на странице поиска вы извлекаете заголовок и ссылку и возвращаете небольшой словарь через yield. Scrapy собирает всё, что вы возвращаете через yield, как спарсенные элементы.

python
    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            title = card.css("h2 a span::text").get()
            href = card.css("h2 a::attr(href)").get()
            if not title or not href:
                continue
            yield {
                "title": title.strip(),
                "url": response.urljoin(href),
            }

Стоит отметить несколько вещей. Селектор карточки нацелен на стабильный атрибут data-component-type, а не на хрупкий служебный класс, именно такие долговечные зацепки следует предпочитать на любом сайте. response.css(...).get() возвращает первое совпадение как текст или None, если совпадений нет, поэтому проверка if not title or not href пропускает рекламные слоты и строки разметки, которые не содержат оба поля. response.urljoin(href) превращает относительную ссылку из карточки в абсолютный URL. Если вы предпочитаете XPath, те же два поля читаются как card.xpath(".//h2//a//span/text()").get() и card.xpath(".//h2/a/@href").get(). CSS и XPath здесь взаимозаменяемы; выбирайте тот, который читается понятнее для конкретного поля. Более глубокое сравнение двух подходов, в руководстве по веб-скрейпингу с XPath и CSS-селекторами.

Селекторы дрейфуют

Разметка сайта меняется без предупреждения, и приведённые выше селекторы, стартовый шаблон, а не гарантия. Если title или url возвращают None для каждой карточки, откройте живую страницу в инструментах разработчика браузера, заново изучите карточку товара и обновите селектор. Периодическое обслуживание селекторов, норма для любого продуктивного краулера, а не признак неполадки.

Шаг 3: сборка и запуск полного паука

Соберите всё вместе в один файл. Это полный, готовый к запуску паук: импорт, API-клиент, начальные URL через buildURL и метод parse.

python
import scrapy
from crawlbase import CrawlingAPI

# Replace YOUR_CRAWLBASE_TOKEN with the token from your dashboard
api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

class AmazonSpider(scrapy.Spider):
    name = "amazonspider"

    targets = ["https://www.amazon.com/s?k=cold+brew+coffee+maker"]
    start_urls = [api.buildURL(url, {}) for url in targets]

    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            title = card.css("h2 a span::text").get()
            href = card.css("h2 a::attr(href)").get()
            if not title or not href:
                continue
            yield {
                "title": title.strip(),
                "url": response.urljoin(href),
            }

Запустите и запишите элементы прямо в файл с помощью встроенного экспорта фидов Scrapy, который сериализует всё, что возвращает паук через yield:

bash
scrapy runspider myspider.py -o products.json

Флаг -o products.json указывает Scrapy записывать каждый полученный элемент в JSON-файл. Без флага элементы выводятся в консоль. В любом случае каждая строка Scraped from в логе соответствует одному товару, а итоговый отчёт по статистике показывает, сколько элементов собрал запуск.

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

Каждый элемент, небольшая запись с двумя полями, которые вы вернули через yield. JSON-файл представляет собой их список, готовый для загрузки в базу данных, ноутбук или последующий пайплайн.

json
[
  {
    "title": "Airtight Cold Brew Iced Coffee Maker and Tea Infuser with Spout, 1.0L",
    "url": "https://www.amazon.com/Airtight-Coffee-Maker-Infuser-Spout/dp/B01CTIYU60"
  },
  {
    "title": "KitchenAid Cold Brew Coffee Maker, Brushed Stainless Steel",
    "url": "https://www.amazon.com/KitchenAid-KCM4212SX-Coffee-Brushed-Stainless/dp/B06XNVZDC7"
  }
]

Краулинг более одной страницы

Одна страница результатов поиска, это демо. Реальный краулинг следует по ссылкам, которые вы только что собрали, или обходит следующие страницы результатов, и Scrapy создан именно для этого. Вместо того чтобы возвращать обычный словарь через yield, возвращайте scrapy.Request для каждого URL, который хотите посетить, оборачивайте его через buildURL, чтобы он снова шёл через Crawling API, и указывайте колбэк, разбирающий следующую страницу.

python
    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            href = card.css("h2 a::attr(href)").get()
            if href:
                product_url = response.urljoin(href)
                yield scrapy.Request(
                    api.buildURL(product_url, {}),
                    callback=self.parse_product,
                )

    def parse_product(self, response):
        yield {
            "title": response.css("#productTitle::text").get(default="").strip(),
            "url": response.url,
        }

Scrapy ставит в очередь каждый запрос, который вы возвращаете через yield, загружает их через загрузчик и вызывает соответствующий колбэк для каждого ответа, поэтому двухуровневый краулинг (страница поиска, затем каждая страница товара), это просто два метода parse. Поскольку каждый последующий запрос оборачивается через buildURL, он тоже идёт через Crawling API, что сохраняет ротацию IP и рендеринг согласованными на протяжении всего краулинга. Ограничивайте краулинг настройками Scrapy вроде CLOSESPIDER_ITEMCOUNT во время тестирования и добавляйте вежливую задержку через DOWNLOAD_DELAY, чтобы не перегружать цель. Для сайтов, прорисовывающих объявления через JavaScript, та же маршрутизация справляется с ними, как только вы запросите рендеринг; руководство о том, как краулить JavaScript-сайты, объясняет, когда это необходимо.

Оставаться незаблокированным

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

  • Задавайте темп запросам. Установите DOWNLOAD_DELAY и позвольте AutoThrottle Scrapy адаптировать частоту вместо отправки запросов так быстро, как только может фреймворк. Скорость, это именно то, что привлекает внимание к краулеру.
  • Полагайтесь на ротацию. Пул резидентных IP распределяет запросы по многим адресам реальных пользователей, так что ни один из них не достигает лимита частоты. Crawling API делает это за вас; если когда-нибудь будете строить собственный стек, именно эту часть нужно сделать правильно.
  • Читайте коды статусов. Краулинг, начавший возвращать ответы не 200, говорит вам, что текущая частота или уровень IP больше недостаточен. Воспринимайте это как сигнал замедлиться, а не как шум для игнорирования.

Те же подходы применимы далеко за пределами Python. Если хотите сравнить подход на другом языке, руководство по построению веб-краулера на Java следует той же форме «загрузка через API, затем разбор» с другим инструментарием.

Краулите ответственно

Придерживайтесь публичных данных, заголовков объявлений и ссылок, которые любой может видеть без авторизации, и держитесь подальше от чего-либо за аутентификацией, личной информации или защищённых авторским правом медиа, которые вы намерены распространять. Соблюдайте robots.txt и условия использования каждого сайта, устанавливающие границы того, что вы можете собирать и как, и поддерживайте разумную частоту запросов, чтобы не перегружать чужие серверы. Если сайт предоставляет официальный API для нужных вам данных, предпочитайте его: это санкционированный путь и обычно более стабильный. Ничто из приведённого здесь инструментария не отменяет этих обязательств; он лишь помогает техническому аспекту работать.

Итоги

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

  • Scrapy предоставляет фреймворк для краулинга. Подкласс паука с start_urls и методом parse, это всё ядро, а scrapy runspider запускает его из одного файла.
  • Маршрутизируйте каждый запрос через Crawling API. Оборачивайте каждый целевой URL через api.buildURL, чтобы запрос шёл через ротирующийся доверенный IP и возвращался отрендеренным, а не попадал на сайт с вашего датацентрового адреса.
  • Подтвердите 200 до парсинга. Запустите паука без парсера сначала; строка Crawled (200) против API-эндпойнта доказывает работоспособность маршрутизации ещё до работы с селекторами.
  • Извлекайте с помощью CSS или XPath. Ответ предоставляет оба; сопоставьте каждое поле с долговечным селектором, защищайтесь от отсутствующих совпадений и ожидайте, что селекторы со временем будут дрейфовать.
  • Краулите ответственно. Работайте только с публичными данными, соблюдайте robots.txt и условия использования, задавайте темп запросам и предпочитайте официальный API там, где он существует.

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

Зачем маршрутизировать Scrapy через Crawling API вместо прямой загрузки?

Потому что прямой Scrapy-запрос попадает на цель с вашего IP и получает сырой, нередко неотрендеренный HTML. На загруженных коммерческих сайтах это означает CAPTCHA, блокировки или пустую JavaScript-оболочку. Маршрутизация через Crawling API загружает страницу за ротирующимся резидентным IP и рендерит её при необходимости, так что HTML, доходящий до Scrapy, это готовая страница, которую действительно можно парсить.

Что делает api.buildURL?

Он принимает целевой URL и возвращает полный URL запроса к Crawling API с вашим токеном и целью в качестве параметров запроса. Вы направляете Scrapy на URL, который возвращает buildURL, и API загружает цель от вашего имени. Это избавляет от необходимости вручную составлять https://api.crawlbase.com/?token=...&url=... и ошибаться с экранированием.

Нужен обычный токен или JavaScript-токен?

Зависит от цели. Если страница отдаёт контент в начальном HTML, достаточно обычного токена. Если объявления появляются только после выполнения JavaScript страницы, нужен JavaScript-токен, чтобы API отрендерил страницу в реальном браузере перед её возвратом. Когда поля, видимые в браузере, возвращаются пустыми в Scrapy, это обычный признак того, что следует переключиться на JavaScript-токен.

Можно ли использовать CSS и XPath в одном пауке?

Да. Каждый ответ Scrapy предоставляет и response.css(...), и response.xpath(...), и их можно свободно смешивать, вплоть до уровня отдельного поля. CSS обычно компактнее для совпадений по классу и атрибуту, тогда как XPath удобнее для обхода вверх по дереву или совпадений по тексту. Используйте тот, который читается понятнее для поля перед вами.

Как краулить несколько страниц или следовать по ссылкам?

Возвращайте через yield scrapy.Request для каждого URL, который хотите посетить, а не обычный элемент, оборачивайте этот URL через api.buildURL, чтобы он снова прошёл через Crawling API, и указывайте колбэк, разбирающий следующую страницу. Scrapy ставит в очередь и загружает каждый возвращённый через yield запрос, поэтому краулинг «страница поиска, затем страница товара», это просто два метода parse. Ограничивайте запуск настройками вроде CLOSESPIDER_ITEMCOUNT во время тестирования.

Мои селекторы возвращают None. Что изменилось?

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

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

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

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

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