Etsy, это глобальный маркетплейс товаров ручной работы, винтажа и изделий народных промыслов. Каждая страница поиска на нём представляет собой открытый, структурированный поток актуальных листингов продавцов: название товара, цена, магазин и звёздный рейтинг. Для всех, кто занимается маркетинговыми исследованиями, отслеживанием цен или конкурентным анализом в нише handmade, данные листингов являются одним из наиболее чистых сигналов спроса, доступных без регистрации. Продавец может наблюдать, как конкуренты оценивают аналогичный товар; покупатель способен отслеживать тренды в категории; аналитик сможет строить графики изменений в нише на протяжении времени.

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

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

Скрипт на Python, который принимает поисковый запрос Etsy, получает каждую отрисованную страницу результатов через Crawling API и извлекает структурированную запись для каждой карточки листинга. В качестве сквозного примера используем запрос clothes, такой же, как в оригинальном руководстве. Из каждой карточки будем извлекать следующие поля:

  • Title название товара, показанное на карточке листинга.
  • Price заявленная цена в виде денежного значения, которое Etsy отображает на карточке.
  • Shop название магазина продавца, разместившего листинг.
  • Rating средний звёздный рейтинг листинга, если он отображается.
  • Link URL на страницу детального описания листинга.

Почему обычный запрос не работает на Etsy

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

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

Требования

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

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

Python 3.8 или новее. Проверьте свою версию командой python --version (или python3 --version). Если Python не установлен, загрузите его с python.org и убедитесь, что он добавлен в системный PATH.

Аккаунт Crawlbase и токен. Зарегистрируйтесь, откройте дашборд и скопируйте свой токен. Поскольку Etsy отрисовывает листинги через JavaScript, для этого парсера используйте JavaScript-токен. Бесплатный тариф включает до 20 000 запросов без привязки карты, чего вполне достаточно для сборки и тестирования. Относитесь к токену как к паролю и не храните его в системе контроля версий.

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

Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите три библиотеки, необходимые парсеру. crawlbase, официальный клиент для Crawling API, beautifulsoup4 парсит возвращённый HTML, позволяя извлекать каждое поле из карточек листингов по CSS-селектору, а pandas делает экспорт в CSV однострочным.

bash
python --version

python -m venv etsy_env
source etsy_env/bin/activate

pip install crawlbase beautifulsoup4 pandas

В Windows активируйте окружение командой etsy_env\Scripts\activate вместо строки source. После установки библиотек создайте файл скрипта, который будет разрабатываться в оставшейся части руководства:

bash
touch etsy_scraper.py

Структура страницы поиска Etsy

Страница поиска Etsy находится по стабильному URL, ключом которого является запрос: поиск по clothes выглядит как https://www.etsy.com/search?q=clothes, а переход по страницам результатов осуществляется добавлением параметра &page=. Страница отображает сетку карточек листингов, по одной на каждый товар. Каждая карточка содержит одни и те же поля: название, цену, имя магазина, звёздный рейтинг и ссылку на страницу детального описания.

Перед написанием селекторов откройте страницу поиска в браузере, кликните правой кнопкой мыши на карточку листинга и выберите «Просмотр кода». Etsy оборачивает каждый результат в элемент списка внутри контейнера результатов поиска и размещает название, цену и рейтинг внутри этой карточки. Именно эти элементы вы будете искать. Имена классов Etsy периодически меняются, поэтому приведённые ниже селекторы работали на момент написания статьи; если какое-то поле начнёт возвращать пустые значения, ожидайте повторного осмотра живой страницы и обновления селектора.

Шаг 1: получение отрисованной страницы поиска

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

python
from crawlbase import CrawlingAPI

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

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 3000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    search_url = "https://www.etsy.com/search?q=clothes"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

Два параметра ожидания важны для сетки, которая заполняется по мере загрузки страницы. ajax_wait указывает API ждать завершения загрузки асинхронного контента, а page_wait делает дополнительную паузу на фиксированное количество миллисекунд после загрузки, чтобы карточки с поздней отрисовкой появились до захвата страницы. Тело декодируется как latin1, поскольку страницы Etsy могут содержать символы, на которых строгое декодирование UTF-8 даёт сбой. Запустите скрипт и вы должны увидеть реальную разметку листингов, а не страницу-заглушку. Это подтверждает, что отрисовка работает, прежде чем вы напишете хоть один селектор.

Crawlbase Crawling API

Этот единственный вызов crawl скрывает самое сложное: Etsy требует отрисованной страницы за доверенным IP, а два параметра _wait помогают лишь тогда, когда сам запрос проходит. Crawling API принимает ваш токен, запускает страницу поиска в реальном браузере, ротирует резидентные IP на стороне сервера, обрабатывает CAPTCHA и передаёт вам готовый HTML. Вам не нужно самостоятельно запускать парк headless-браузеров и пул прокси. Начните с бесплатного лимита до 20 000 запросов на URL поиска Etsy.

Шаг 2: парсинг карточек листингов с помощью BeautifulSoup

Получив отрисованный HTML, загрузите его в BeautifulSoup, найдите все карточки листингов и извлеките каждое поле по его селектору. Etsy группирует результаты в упорядоченный список внутри контейнера результатов поиска: по одному элементу списка на каждую карточку. Внутри карточки название, цена, магазин и рейтинг находятся в своих элементах, а ссылка на листинг, это якорь карточки. Оберните каждую карточку в try/except, чтобы один некорректный листинг не прервал весь процесс.

python
from bs4 import BeautifulSoup

CONTAINER = "div.search-listings-group div[data-search-results-container] ol li"

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

def parse_link(card):
    a = card.select_one("a.listing-link[href]")
    return a["href"] if a else None

def scrape_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select(CONTAINER)
    results = []
    for card in cards:
        try:
            title = text_of(card, "div.v2-listing-card__info h3.v2-listing-card__title")
            if not title:
                continue
            results.append({
                "title": title,
                "price": text_of(card, "div.n-listing-card__price p.lc-price span.currency-value"),
                "shop": text_of(card, "div.v2-listing-card__info p.v2-listing-card__shop span"),
                "rating": text_of(card, "div.shop-name-with-rating span.larger_review_stars > div"),
                "link": parse_link(card),
            })
        except Exception as e:
            print(f"Skipped a card: {e}")
    return results

Вспомогательная функция text_of запрашивает один элемент внутри карточки и возвращает None, если он отсутствует, вместо того чтобы выбрасывать исключение при вызове .get_text() на пустом значении. Это делает извлечение устойчивым при отсутствии поля, что часто встречается, поскольку не каждый листинг показывает рейтинг. Название берётся из h3.v2-listing-card__title, цена из span.currency-value внутри блока цены, имя магазина из элемента магазина на карточке, а рейтинг из блока со звёздами отзывов. Селекторы контейнера, названия, цены и рейтинга перенесены из оригинального руководства; магазин и ссылка, это два дополнительных поля, добавленных в этой версии.

Selectors drift

Имена классов карточек листингов Etsy (зажим заголовка, блок цены, блок рейтинга) регенерируются и меняются без предупреждения. Относитесь к приведённым выше селекторам как к начальному шаблону, а не к контракту. Если поле начинает возвращать None для каждой карточки, проверьте живую страницу поиска в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, это норма для любого производственного парсера.

Шаг 3: обработка пагинации по страницам результатов

Одна страница поиска, это выборка; полный набор результатов охватывает множество страниц. Etsy разбивает результаты поиска на нумерованные страницы, и переход между ними осуществляется добавлением &page=N к URL поиска. Чтобы собрать полный набор данных, прочитайте общее количество страниц с первой страницы, а затем последовательно обходите каждую. Короткая задержка между запросами задаёт темп работы, не перегружая Etsy в плотном цикле.

python
import time

PAGINATION = 'div[data-appears-component-name="search_pagination"] nav ul.search-pagination li:nth-last-child(3) a'

def get_total_pages(html):
    soup = BeautifulSoup(html, "html.parser")
    el = soup.select_one(PAGINATION)
    try:
        return int(el.get_text(strip=True)) if el else 1
    except ValueError:
        return 1

def scrape_all_pages(query, max_pages=5):
    search_url = f"https://www.etsy.com/search?q={query}"
    first = crawl(search_url)
    if not first:
        return []
    total = min(get_total_pages(first), max_pages)
    all_listings = scrape_listings(first)
    print(f"page 1: {len(all_listings)} listings (of {total} pages)")
    for page in range(2, total + 1):
        html = crawl(f"{search_url}&page={page}")
        if not html:
            break
        found = scrape_listings(html)
        all_listings.extend(found)
        print(f"page {page}: {len(found)} listings")
        time.sleep(2)
    return all_listings

get_total_pages считывает количество страниц из элемента управления пагинацией: унаследованный селектор нацелен на третью с конца ссылку пагинации, которая является последней нумерованной страницей перед стрелками «следующая» и «последняя». Возвращает 1, если элемент управления отсутствует или текст не является числом, чтобы результаты на одной странице тоже работали. Ограничение max_pages сдерживает тестовый запуск в процессе разработки, а time.sleep(2) между запросами задаёт темп обхода, не допуская флага за интенсивный трафик.

Шаг 4: сборка скрипта и экспорт в JSON и CSV

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

python
import json
import time
import pandas as pd
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})
CONTAINER = "div.search-listings-group div[data-search-results-container] ol li"
PAGINATION = 'div[data-appears-component-name="search_pagination"] nav ul.search-pagination li:nth-last-child(3) a'

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 3000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

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

def parse_link(card):
    a = card.select_one("a.listing-link[href]")
    return a["href"] if a else None

def scrape_listings(html):
    soup = BeautifulSoup(html, "html.parser")
    results = []
    for card in soup.select(CONTAINER):
        try:
            title = text_of(card, "div.v2-listing-card__info h3.v2-listing-card__title")
            if not title:
                continue
            results.append({
                "title": title,
                "price": text_of(card, "div.n-listing-card__price p.lc-price span.currency-value"),
                "shop": text_of(card, "div.v2-listing-card__info p.v2-listing-card__shop span"),
                "rating": text_of(card, "div.shop-name-with-rating span.larger_review_stars > div"),
                "link": parse_link(card),
            })
        except Exception as e:
            print(f"Skipped a card: {e}")
    return results

def get_total_pages(html):
    soup = BeautifulSoup(html, "html.parser")
    el = soup.select_one(PAGINATION)
    try:
        return int(el.get_text(strip=True)) if el else 1
    except ValueError:
        return 1

def scrape_all_pages(query, max_pages=5):
    search_url = f"https://www.etsy.com/search?q={query}"
    first = crawl(search_url)
    if not first:
        return []
    total = min(get_total_pages(first), max_pages)
    listings = scrape_listings(first)
    print(f"page 1: {len(listings)} listings (of {total} pages)")
    for page in range(2, total + 1):
        html = crawl(f"{search_url}&page={page}")
        if not html:
            break
        found = scrape_listings(html)
        listings.extend(found)
        print(f"page {page}: {len(found)} listings")
        time.sleep(2)
    return listings

def export(rows, name="etsy_listings"):
    with open(f"{name}.json", "w", encoding="utf-8") as f:
        json.dump(rows, f, indent=2, ensure_ascii=False)
    pd.DataFrame(rows).to_csv(f"{name}.csv", index=False)
    print(f"Saved {len(rows)} listings to {name}.json and {name}.csv")

def main():
    rows = scrape_all_pages("clothes", max_pages=5)
    if rows:
        export(rows)

if __name__ == "__main__":
    main()

Запустите полный скрипт командой python etsy_scraper.py. Он получает каждую отрисованную страницу поиска, парсит по одной строке на листинг, обходит страницы результатов до max_pages и записывает оба файла: etsy_listings.json и etsy_listings.csv. Формирование CSV из DataFrame pandas означает, что столбцы автоматически следуют ключам словаря, поэтому два экспорта никогда не расходятся.

Как выглядят результаты

Вы получаете чистый список записей листингов в порядке страниц, готовых для записи в JSON, CSV или базу данных.

json
[
  {
    "title": "Women's Christian Sweatshirt, Bible Verse Shirts, Faith Based Tshirts",
    "price": "13.85",
    "shop": "FaithfulThreadsCo",
    "rating": "4.8",
    "link": "https://www.etsy.com/listing/1234567890/womens-christian-sweatshirt"
  },
  {
    "title": "Thankful Super Soft Sweatshirt, Thanksgiving Sweatshirt, Friendsgiving Shirt",
    "price": "29.99",
    "shop": "CozyFallPrints",
    "rating": "4.9",
    "link": "https://www.etsy.com/listing/9876543210/thankful-super-soft-sweatshirt"
  }
]

Цена поступает в виде чистого денежного значения, которое Etsy отображает на карточке. Если вы хотите использовать её в анализе в виде числа, можно привести к float после удаления разделителей тысяч. Если вы предпочитаете хранить данные в формате, допускающем запросы, а не в плоских файлах, те же словари строк можно напрямую вставить в таблицу SQLite с помощью короткой вставки через sqlite3, что рассматривалось в оригинальном руководстве как альтернатива CSV.

Масштабирование на множество запросов

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

python
QUERIES = ["clothes", "ceramic mug", "leather wallet"]

def scrape_queries(queries, max_pages=3):
    everything = {}
    for query in queries:
        print(f"scraping: {query}")
        everything[query] = scrape_all_pages(query, max_pages=max_pages)
        time.sleep(3)
    return everything

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

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

Даже при обработке отрисовки Etsy отслеживает трафик, характерный для парсеров. Несколько привычек помогают поддерживать работоспособность парсера и применимы к любому жёсткому коммерческому ресурсу.

  • Задавайте темп запросов. Распределяйте запросы с задержкой между страницами и запросами, а не обходите всё на полной скорости. Планируйте более интенсивные задачи в часы низкой нагрузки, чтобы не перегружать серверы Etsy.
  • Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один из них не активировал ограничение частоты запросов. Crawling API берёт это на себя; если вы собираете собственный стек, именно это место стоит сделать правильно.
  • Храните только необходимое. Сохраняйте поля листинга, нужные вашему проекту, и отбрасывайте остальное. Периодически проверяйте селекторы, чтобы парсер не отставал от изменений разметки.

Более широкий план по избеганию блокировок описан в статье как парсить сайты без блокировок, а со стороны парсинга руководство по использованию BeautifulSoup в Python подробно охватывает библиотеку. Если вы выстраиваете полноценный исследовательский рабочий процесс, в статье как автоматизировать исследование товаров в электронной коммерции объясняется, где вписывается такой поток листингов.

Законно ли парсить Etsy?

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

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

Это руководство намеренно ограничено публичными листингами поиска Etsy, поскольку именно это позволяет сохранить работу обоснованной. Для лицензированного или массового доступа Etsy предлагает официальный Open API для разработчиков, который является правильным инструментом, когда вам нужны большие объёмы, гарантированная структура или коммерческие права. Если ваш проект требует больше, чем публичные данные листингов, официальный API или соглашение о данных, это правильный путь, а не более умный парсер.

Итоги

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

  • Страницы поиска Etsy, публичный поток листингов. Страница результатов каждого запроса содержит название, цену, магазин и рейтинг, что делает её полезной для маркетинговых исследований и отслеживания цен.
  • Вам нужны отрисовка и доверенный IP вместе. Etsy заполняет сетку листингов на стороне клиента и блокирует бот-трафик, поэтому Crawling API отрисовывает страницу за резидентным IP в одном вызове.
  • BeautifulSoup выполняет извлечение. Перебирайте элементы списка результатов поиска и сопоставляйте название, цену, магазин, рейтинг и ссылку с актуальными селекторами, учитывая их возможное изменение.
  • Обработайте пагинацию и экспортируйте в JSON и CSV. Прочитайте количество страниц, обходите каждую страницу результатов с задержкой и записывайте оба файла, чтобы данные можно было загрузить в код или таблицу.
  • Оставайтесь в рамках публичных данных. Соблюдайте Условия использования Etsy и robots.txt, предпочитайте официальный Etsy Open API для лицензированных или массовых данных и никогда не трогайте аккаунты, персональные данные или медиа, принадлежащие продавцам.

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

Почему обычный запрос не возвращает листинги с Etsy?

По двум причинам. Etsy заполняет большую часть сетки листингов на стороне клиента при загрузке страницы, поэтому прямой запрос часто возвращает оболочку без большинства карточек. Вдобавок Etsy блокирует или ставит под сомнение трафик, не похожий на реальный браузер. Отрисовка страницы через Crawling API за доверенным IP решает обе проблемы, именно поэтому парсер здесь маршрутизирует запрос через него с установленными параметрами ajax_wait и page_wait.

Как парсить конкретный поисковый запрос Etsy?

Каждый поиск Etsy имеет собственный URL, ключом которого является запрос, например https://www.etsy.com/search?q=clothes. Измените значение q на нужный запрос, затем переходите по страницам результатов, добавляя &page=N. Для охвата многих запросов составьте их список и запустите постраничный парсер по каждому, добавляя короткую задержку между запросами.

Какие поля можно извлечь из карточки листинга Etsy?

Из каждой карточки результата поиска этот парсер извлекает название товара, цену (денежное значение, отображаемое Etsy), имя магазина, звёздный рейтинг и ссылку на листинг. В оригинальном руководстве также упоминалось наличие описаний и изображений товаров на Etsy; их можно добавить, осмотрев карточку и добавив соответствующие селекторы на шаге парсинга.

Как обрабатывать пагинацию при парсинге Etsy?

Прочитайте общее количество страниц из элемента управления пагинацией на первой странице результатов, затем зациклитесь со второй страницы до этого числа, добавляя &page=N к URL поиска каждый раз. Парсер здесь ограничивает число с помощью аргумента max_pages, чтобы тестовый запуск оставался ограниченным, и делает паузу между запросами, чтобы не нагружать Etsy в плотном цикле.

Как сохранить спарсенные данные Etsy?

Скрипт записывает как JSON-файл, сохраняющий структуру для кода, так и CSV, сформированный из DataFrame pandas, который открывается прямо в таблице. Те же словари строк также можно вставить в таблицу SQLite с помощью короткой вставки через sqlite3, если вы хотите хранилище с поддержкой запросов вместо плоских файлов.

Как избежать блокировки при парсинге Etsy?

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

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

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

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

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