Target.com, один из крупнейших розничных каталогов в США: миллионы публичных страниц товаров, охватывающих электронику, одежду, товары для дома и продукты питания. Каждый листинг содержит данные, на которых основывается большинство розничных исследований: название, текущая цена, идентификатор TCIN, звёздный рейтинг и чёткий сигнал о наличии или отсутствии товара. Трекеры цен, конкурентные аналитики и товарные исследователи следят за этими полями, поскольку они дают одно из самых чистых представлений о ценообразовании и ассортименте массового ритейлера в любой момент времени.
В этом руководстве показано, как парсить данные о товарах Target с помощью Python. Вы создадите небольшой работающий парсер, который загружает страницу поиска или товара Target через Crawling API, разбирает структурированную запись для каждого товара, обрабатывает пагинацию по страницам результатов и экспортирует в JSON и CSV. Всё руководство ограничено публичными каталожными данными: названиями, ценами, рейтингами и доступностью, которые каждый может увидеть на Target.com без входа в систему.
Что вы создадите
Python-скрипт, который принимает URL поиска Target, загружает отрендеренную страницу через Crawling API и извлекает структурированную запись по каждому товару. В качестве рабочего примера используется поиск "womens sweaters", тот же запрос, что и в предыдущем руководстве, из каждой карточки товара извлекаются следующие поля:
- Title название товара, отображаемое на карточке листинга.
- Price текущая указанная цена, которая может быть одним значением или диапазоном.
- TCIN / SKU собственный идентификатор товара Target, извлекаемый из URL товара.
- Rating средний звёздный рейтинг, вычисляемый из ширины полосы рейтинга.
- Review count количество отзывов, подтверждающих этот рейтинг.
- Availability статус товара: в наличии или недоступен.
- Product URL абсолютная ссылка на страницу товара.
Почему обычный запрос не работает на Target
Если направить простой HTTP-клиент на URL поиска Target, вы получите почти пустой результат. Target рендерит сетку поиска на стороне клиента: сервер отправляет лёгкую оболочку, а карточки товаров появляются только после выполнения JavaScript страницы. При разборе ответа обычного requests.get() вы увидите пустой список, поскольку нужные товары никогда не были в первоначальном HTML-ответе.
Кроме того, Target отслеживает автоматизированный трафик и блокирует паттерны запросов, не похожие на реальный браузер. Таким образом, работающий парсер Target требует двух вещей в одном запросе: браузера, рендерящего страницу, и IP-адреса, воспринимаемого Target как реального покупателя. Можно собрать такой стек самостоятельно с помощью headless-браузера и пула ротируемых резидентских прокси, но поддержание его в рабочем состоянии занимает большую часть времени. Crawling API объединяет оба требования в одном вызове: вы отправляете URL поиска, он рендерит страницу за доверенным резидентским IP, управляет ротацией и решением CAPTCHA и возвращает готовый HTML для разбора.
Предварительные требования
Перед написанием кода необходимо подготовить несколько вещей. Ни одна из них не займёт много времени.
Базовые знания Python. Вы должны уметь писать и запускать Python-скрипты, а также устанавливать пакеты с помощью pip. Если вы только начинаете, руководство по началу работы с парсингом на Python охватывает уровень, который предполагает этот урок.
Python 3.8 или выше. Проверьте версию командой python --version (или python3 --version). Если Python не установлен, скачайте его с python.org и убедитесь, что он добавлен в PATH.
Аккаунт и токен Crawlbase. Зарегистрируйтесь, откройте панель управления и скопируйте токен со страницы документации аккаунта. Crawlbase предоставляет два токена: обычный для статичных сайтов и JavaScript-токен для динамических, рендерируемых JS страниц. Target рендерится через JavaScript, поэтому в этом руководстве используется JavaScript-токен. Бесплатный уровень включает до 20 000 запросов без привязки карты, этого достаточно для создания и тестирования парсера. Храните токен как пароль и не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальное окружение для изоляции зависимостей проекта, затем установите две необходимые библиотеки. crawlbase, официальный клиент для Crawling API, а beautifulsoup4 разбирает возвращённый HTML, позволяя извлекать каждое поле из карточек товаров по CSS-селектору.
python --version python -m venv target_env source target_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой target_env\Scripts\activate вместо строки с source. После установки обеих библиотек создайте файл скрипта, который будет наполняться по мере прохождения руководства:
touch target_scraper.py
Понимание страницы поиска Target
Поиск Target находится по стабильному URL: https://www.target.com/s?searchTerm=womens+sweaters. Страница отображает сетку карточек товаров, по одной на каждый товар, каждая из которых содержит одинаковый набор полей: название, цена, полоса звёздного рейтинга, количество отзывов и ссылка на страницу товара. Target помечает большинство из них атрибутами data-test, которые намного устойчивее, чем его генерируемые имена классов.
Прежде чем писать селекторы, откройте страницу поиска в браузере, щёлкните правой кнопкой мыши по карточке товара и выберите «Просмотр кода». Каждая карточка находится внутри div[data-test="product-grid"], ссылка с названием имеет атрибут data-test="product-title", текущая цена, data-test="current-price", количество отзывов, data-test="rating-count", а сам рейтинг, это маскированная полоса с атрибутом data-ref="rating-mask", ширина которой в CSS кодирует оценку. TCIN, внутренний идентификатор товара Target, встроен в URL товара после маркера /A-, поэтому его можно читать прямо из ссылки.
Шаг 1: загрузка отрендеренной страницы поиска
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его JavaScript-токеном, задайте URL поиска и запросите его с параметрами JS-рендеринга. Проверка статуса перед разбором позволяет обнаруживать сбои явно, а не молча.
from crawlbase import CrawlingAPI from urllib.parse import quote api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) 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__": search_term = "womens sweaters" url = f"https://www.target.com/s?searchTerm={quote(search_term)}" html = crawl(url) print(html[:500] if html else "No HTML returned")
Два параметра ожидания важны для сетки, заполняемой после загрузки. ajax_wait указывает API дождаться завершения асинхронного контента, а page_wait задаёт 5000 миллисекунд задержки после загрузки, чтобы поздно рендеримые карточки успели появиться до захвата страницы. Crawling API сообщает свой результат в response["headers"]["cb_status"], поэтому проверяйте именно его, а не исходный HTTP-код. Запустите скрипт, и вы должны увидеть реальную разметку товаров, а не пустую оболочку. Это подтверждает, что рендеринг работает, ещё до написания единого селектора.
Пустая сетка, это именно та проблема, которую решает Crawling API: листинги Target существуют только после выполнения JavaScript, за IP, которому сайт доверяет. Вы отправляете токен и URL поиска, он запускает страницу в реальном браузере, ротирует резидентские IP на стороне сервера, обрабатывает решение CAPTCHA и возвращает готовый HTML. Вам не нужно самостоятельно поддерживать флот headless-браузеров и пул прокси. Начните с бесплатного уровня до 20 000 запросов.
Шаг 2: разбор карточек товаров с помощью BeautifulSoup
Получив отрендеренный HTML, загрузите его в BeautifulSoup, найдите все карточки товаров и извлеките каждое поле по его селектору. Для рейтинга нужна небольшая вспомогательная функция: Target рисует полосу звёзд в виде заполненной маски, а оценка хранится в CSS-свойстве width полосы как процент, который нужно перевести в значение из 5.
from bs4 import BeautifulSoup BASE = "https://www.target.com" GRID = 'div[data-test="product-grid"] section[class^="styles__StyledRowWrapper"] div[class^="styles__StyledCardWrapper"]' def extract_rating(element): style = element.get("style") if element else None if not style: return None for prop in style.split(";"): prop = prop.strip() if prop.startswith("width:"): value = prop[len("width:"):].strip() if value.endswith("%"): percentage = float(value[:-1]) return round((percentage / 100) * 5, 2) return None def extract_tcin(href): if href and "/A-" in href: return href.split("/A-")[1].split("?")[0].split("#")[0] return None def scrape_target_listing(html): soup = BeautifulSoup(html, "html.parser") products = soup.select(GRID) results = [] for product in products: try: title_el = product.select_one('a[data-test="product-title"]') rating_el = product.select_one('div[data-ref="rating-mask"]') reviews_el = product.select_one('span[data-test="rating-count"]') price_el = product.select_one('span[data-test="current-price"]') sold_out_el = product.select_one('[data-test="soldOut"]') href = title_el["href"] if title_el else None availability = "Out of stock" if sold_out_el else "In stock" results.append({ "title": title_el.get_text(strip=True) if title_el else None, "price": price_el.get_text(strip=True) if price_el else None, "tcin": extract_tcin(href), "rating": extract_rating(rating_el), "review_count": reviews_el.get_text(strip=True) if reviews_el else None, "availability": availability, "product_url": BASE + href if href else None, }) except Exception as e: print(f"Skipped a card: {e}") return results
Селектор сетки товаров объединяет три элемента из предыдущего скрипта: контейнер data-test="product-grid", обёртку строки и обёртку карточки. Внутри каждой карточки название и цена берутся из их data-test-селекторов, количество отзывов из rating-count, а рейтинг из маскированной полосы через extract_rating. Вспомогательная функция extract_tcin читает идентификатор товара Target из URL после /A-, а доступность определяется по наличию маркера распродажи. Каждая проверка if el else None обеспечивает устойчивость извлечения при отсутствующих полях, что распространено, поскольку не каждая карточка показывает рейтинг или количество отзывов.
Генерируемые имена классов Target (префиксы styles__StyledCardWrapper) меняются без предупреждения, тогда как атрибуты data-test и data-ref значительно устойчивее. Опирайтесь на атрибутные селекторы и рассматривайте цепочку с префиксом класса как отправную точку, а не жёсткий контракт. Если поле возвращается как None для каждой карточки, заново изучите живую страницу поиска в инструментах разработчика браузера и обновите селектор. Периодическое обновление селекторов, норма для любого производственного парсера.
Шаг 3: обработка пагинации по страницам результатов
Одна страница результатов, это демо; реальная задача перебирает весь набор результатов. Target пагинирует поиск с помощью параметра Nao в URL, который задаёт начальное смещение для каждой страницы. Результаты приходят пакетами по 24, поэтому Nao=1, первая страница, Nao=25, вторая и так далее. Вы увеличиваете смещение, загружаете каждую страницу и останавливаетесь, когда отсутствует активная кнопка следующей страницы.
PER_PAGE = 24 def has_next_page(html): soup = BeautifulSoup(html, "html.parser") return bool(soup.select_one('button[data-test="next"]:not([disabled])')) def scrape_all_pages(base_url, max_pages=5): all_products = [] for page in range(max_pages): nao = page * PER_PAGE + 1 url = f"{base_url}&Nao={nao}" html = crawl(url) if not html: break found = scrape_target_listing(html) if not found: break all_products.extend(found) print(f"Page {page + 1}: {len(found)} products") if not has_next_page(html): break return all_products
Смещение Nao вычисляется из индекса страницы: страница 0 запрашивает Nao=1, страница 1 запрашивает Nao=25, и цикл продолжается до тех пор, пока has_next_page не обнаружит отсутствие активной кнопки button[data-test="next"]. Ограничение max_pages и прерывание при пустых результатах оба останавливают прогон досрочно, поэтому поиск с двумя страницами результатов никогда не выполняет пять запросов. Ограничение страниц также сохраняет кредиты бесплатного уровня при тестировании.
Шаг 4: сборка скрипта и экспорт в JSON и CSV
Теперь соедините загрузку, разбор и пагинацию в один рабочий скрипт, затем запишите записи как в JSON, так и в CSV, чтобы можно было загрузить их в ноутбук или таблицу.
import csv import json from urllib.parse import quote from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://www.target.com" GRID = 'div[data-test="product-grid"] section[class^="styles__StyledRowWrapper"] div[class^="styles__StyledCardWrapper"]' PER_PAGE = 24 FIELDS = ["title", "price", "tcin", "rating", "review_count", "availability", "product_url"] def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None def extract_rating(element): style = element.get("style") if element else None if not style: return None for prop in style.split(";"): prop = prop.strip() if prop.startswith("width:"): value = prop[len("width:"):].strip() if value.endswith("%"): percentage = float(value[:-1]) return round((percentage / 100) * 5, 2) return None def extract_tcin(href): if href and "/A-" in href: return href.split("/A-")[1].split("?")[0].split("#")[0] return None def scrape_target_listing(html): soup = BeautifulSoup(html, "html.parser") products = soup.select(GRID) results = [] for product in products: try: title_el = product.select_one('a[data-test="product-title"]') rating_el = product.select_one('div[data-ref="rating-mask"]') reviews_el = product.select_one('span[data-test="rating-count"]') price_el = product.select_one('span[data-test="current-price"]') sold_out_el = product.select_one('[data-test="soldOut"]') href = title_el["href"] if title_el else None availability = "Out of stock" if sold_out_el else "In stock" results.append({ "title": title_el.get_text(strip=True) if title_el else None, "price": price_el.get_text(strip=True) if price_el else None, "tcin": extract_tcin(href), "rating": extract_rating(rating_el), "review_count": reviews_el.get_text(strip=True) if reviews_el else None, "availability": availability, "product_url": BASE + href if href else None, }) except Exception as e: print(f"Skipped a card: {e}") return results def has_next_page(html): soup = BeautifulSoup(html, "html.parser") return bool(soup.select_one('button[data-test="next"]:not([disabled])')) def scrape_all_pages(base_url, max_pages=5): all_products = [] for page in range(max_pages): url = f"{base_url}&Nao={page * PER_PAGE + 1}" html = crawl(url) if not html: break found = scrape_target_listing(html) if not found: break all_products.extend(found) print(f"Page {page + 1}: {len(found)} products") if not has_next_page(html): break return all_products def export(rows, name="target_products"): with open(f"{name}.json", "w", encoding="utf-8") as f: json.dump(rows, f, indent=2, ensure_ascii=False) with open(f"{name}.csv", "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) writer.writeheader() writer.writerows(rows) print(f"Saved {len(rows)} products to {name}.json and {name}.csv") def main(): search_term = "womens sweaters" base_url = f"https://www.target.com/s?searchTerm={quote(search_term)}" rows = scrape_all_pages(base_url, max_pages=3) export(rows) if __name__ == "__main__": main()
Запустите полный скрипт командой python target_scraper.py. Он перебирает до трёх страниц поиска, разбирает по одной строке на каждый товар и записывает как target_products.json, так и target_products.csv. Общий список FIELDS синхронизирует порядок столбцов CSV с ключами словаря, поэтому два экспорта никогда не расходятся. Чтобы парсить другую категорию, измените search_term на любой поддерживаемый Target запрос или направьте base_url на страницу категории вместо страницы поиска.
Как выглядит результат
Вы получаете чистый список записей о товарах в порядке страниц, готовый к записи в JSON, CSV или базу данных.
[ { "title": "Women's Fine Gauge Crewneck Sweater - A New Day", "price": "$20.00", "tcin": "88228365", "rating": 3.9, "review_count": "587", "availability": "In stock", "product_url": "https://www.target.com/p/women-s-fine-gauge-crewneck-sweater-a-new-day/-/A-88228365" }, { "title": "Women's Crew Neck Cashmere-Like Pullover Sweater - Universal Thread", "price": "$20.00 - $25.00", "tcin": "88062926", "rating": 4.2, "review_count": "746", "availability": "In stock", "product_url": "https://www.target.com/p/women-s-crew-neck-cashmere-like-pullover-sweater-universal-thread/-/A-88062926" } ]
Обратите внимание, что rating и review_count возвращаются как null для товаров без отзывов, это ожидаемо: маскированная полоса рейтинга просто не рендерится на таких карточках. Цены приходят как строки с форматированием, включая диапазоны вида "$20.00 - $25.00", поэтому нормализуйте их до чисел в конвейере обработки, если планируете сравнивать или строить графики. Именно такой набор полей нужен для рабочего процесса анализа цен.
Масштабирование на несколько запросов и предотвращение блокировок
Один поиск, это демо; реальная исследовательская задача охватывает множество запросов или категорий. Ведите словарь поисковых запросов, перебирайте его и соблюдайте темп запросов. Тот же набор полей применим и к странице отдельного товара, где название, цена, TCIN, рейтинг и явный значок наличия или отсутствия товара находятся на одном URL. Даже с решённой задачей рендеринга Target отслеживает трафик, характерный для скраперов, поэтому несколько привычек помогают поддерживать прогон в рабочем состоянии; они применимы к любой сложной коммерческой цели.
- Соблюдайте темп запросов. Делайте задержки между страницами и запросами вместо того, чтобы парсить всё на максимальной скорости. Планируйте тяжёлые задачи на часы с меньшей нагрузкой, чтобы не перегружать серверы Target.
- Опирайтесь на ротацию. Пул резидентских IP-адресов распределяет запросы между множеством реальных пользовательских адресов, не давая ни одному из них превысить ограничение скорости. Crawling API делает это за вас; если вы строите собственный стек, именно на это стоит обратить внимание.
- Сохраняйте только необходимое. Записывайте поля товаров, нужные для вашего проекта, и отбрасывайте остальное. Периодически проверяйте селекторы, чтобы парсер успевал за изменениями разметки.
Более широкое руководство по предотвращению блокировок см. в статье как парсить сайты, не попадая в блокировку, а о том, почему здесь важен рендеринг, читайте в материале как парсить JavaScript-сайты. Тот же паттерн применим к другим крупным ритейлерам, как в руководствах по парсингу данных товаров Best Buy и поиска Walmart с помощью Python.
Легально ли парсить Target?
Допустимость парсинга Target зависит от условий обслуживания Target, вашей юрисдикции и целей использования данных. Условия Target ограничивают автоматизированный доступ, поэтому парсинг может противоречить этим условиям независимо от того, насколько аккуратно настроен ваш инструментарий. Ни один из приведённых здесь кодов не меняет этого; он лишь реализует техническую часть. Ознакомьтесь с условиями обслуживания Target и файлом robots.txt и рассматривайте оба документа как границы для того, что вы собираете. При коммерческом или конкурентном использовании правовая картина усложняется, и консультация с юристом по вашему конкретному случаю будет разумным шагом.
Несколько правил, которых стоит придерживаться. Собирайте только публичные данные: названия, цены, TCIN, рейтинги и ссылки листинга, которые каждый может увидеть на странице поиска или товара Target без регистрации. Поддерживайте объём запросов достаточно низким, чтобы не перегружать серверы Target, и избегайте персональных данных, в том числе любой информации, связанной с идентифицируемыми покупателями, рецензентами или сотрудниками магазина, помимо публично опубликованной. Не распространяйте охраняемые авторским правом медиаматериалы, такие как фотографии товаров Target, а если вы планируете повторно использовать данные в коммерческих целях, получите разрешение или официальное соглашение, не считая молчание согласием.
Это руководство намеренно ограничено публичными страницами поиска и товаров, поскольку именно это делает работу защищённой. Оно не охватывает ничего за пределами публичного доступа, данных аккаунта или заказов, платёжных реквизитов или попыток обойти аутентификацию или CAPTCHA, на прохождение которой у вас нет прав. Target управляет партнёрской программой и предоставляет официальные ленты данных для одобренных партнёров, это правильный канал при необходимости больших объёмов, гарантированной структуры или коммерческих прав. Если вашему проекту нужно больше, чем публичные каталожные данные, официальное соглашение, это правильный путь, а не более хитрый парсер.
Ключевые выводы
- Данные Target публичны, но рендерятся через JavaScript. Сетка товаров существует только после выполнения скриптов страницы, поэтому обычный запрос возвращает пустой список.
-
Необходимы рендеринг и доверенный IP одновременно. Crawling API рендерит страницу за резидентским IP в одном вызове, используя JavaScript-токен вместе с
ajax_waitиpage_wait. -
Опирайтесь на атрибуты data-test. Сопоставляйте название, цену, TCIN, рейтинг, количество отзывов и доступность из маркеров
data-testиdata-refTarget, ожидая изменения генерируемых имён классов. -
Пагинация через смещение Nao. Target разбивает результаты на пакеты по 24 через параметр
Nao; останавливайтесь при отсутствии активной кнопки «далее» и экспортируйте в JSON и CSV из общего списка полей. - Работайте только с публичными данными. Соблюдайте условия и robots.txt Target, предпочитайте официальные партнёрские ленты для лицензированных или объёмных данных и никогда не касайтесь аккаунтов, заказов или персональной информации.
Часто задаваемые вопросы
Почему обычный запрос не возвращает товары с Target?
Target рендерит сетку поиска на стороне клиента: первоначальный HTML-ответ, это оболочка, а карточки товаров появляются только после выполнения JavaScript страницы. Обычный requests.get() разбирает эту оболочку и ничего не находит, поэтому предыдущая попытка сделать это самостоятельно возвращала пустой список. Рендеринг страницы через Crawling API за доверенным IP решает как проблему JavaScript, так и проблему блокировки, именно поэтому парсер здесь маршрутизирует запрос именно через него.
Что такое TCIN и как его получить?
TCIN, внутренний идентификатор товара Target, аналог SKU на сайте. Он присутствует в каждом URL товара после маркера /A-, например /A-88228365 означает TCIN 88228365. Вспомогательная функция extract_tcin читает его прямо из ссылки на товар, что даёт стабильный ключ для каждого элемента без дополнительного запроса, удобно для дедупликации или объединения записей во времени.
Как парсить конкретную категорию Target вместо поиска?
Направьте парсер на URL категории вместо URL поиска. Сетка товаров, селекторы data-test и параметр пагинации Nao работают одинаково на страницах категорий, поэтому тот же парсер и цикл пагинации переносятся без изменений. Просто замените base_url в функции main на нужную категорию и оставьте остальной скрипт как есть.
Как читать доступность товара?
На карточках поиска и категории товар с нулевым запасом показывает маркер распродажи, который парсер обнаруживает проверкой [data-test="soldOut"] и записывает как "Out of stock", по умолчанию считая "In stock". Для точного чтения наличия по магазину парсите страницу отдельного товара, где Target показывает чёткий значок наличия или отсутствия и варианты доставки или самовывоза, привязанные к местоположению.
Почему некоторые рейтинги в выводе равны null?
Значение null для рейтинга или количества отзывов означает, что у товара ещё нет отзывов, поэтому Target не рендерит маскированную полосу рейтинга на его карточке. Вспомогательная функция extract_rating читает оценку из ширины CSS-полосы, и при отсутствии полосы возвращает None. Это ожидаемое поведение, а не сбой селектора; у новых или малопопулярных листингов просто нечего показывать.
Как избежать блокировки при парсинге Target?
Поддерживайте низкую частоту запросов с одного IP, добавляйте задержки между страницами и запросами, и маршрутизируйте через ротируемые резидентские IP, чтобы ни один адрес не превысил ограничение скорости. Crawling API управляет ротацией, доверенным пулом IP и обработкой CAPTCHA за вас; если вы строите собственный стек, именно на это стоит потратить усилия. Следите за кодами cb_status (legacy pc_status), возвращаемыми API, и снижайте темп при появлении сбоев.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
