Zoro.com это крупный розничный поставщик промышленных товаров с миллионами позиций: инструменты, оборудование, крепёж, средства защиты и детали MRO, каждое объявление несёт бренд, цену, номер Zoro и статус наличия. Эти объявления это чистый, публичный сигнал для любого, кто занимается отслеживанием цен, исследованием каталога или конкурентным анализом в промышленной сфере, поэтому за ними следят дистрибьюторы, команды закупок и аналитики.
Это руководство показывает, как парсить данные товаров Zoro с помощью Python. Вы создадите небольшой готовый к запуску парсер, который получает страницы поиска и товаров Zoro через Crawling API, разбирает чистую запись для каждой позиции, обрабатывает пагинацию и экспортирует результаты в JSON и CSV. Весь разбор ограничен публичными данными каталога: брендами, названиями, ценами, номерами Zoro и доступностью, которые любой может увидеть на странице поиска или товара без входа в систему.
Что вы создадите
Скрипт на Python, который принимает URL поиска Zoro, получает отрисованную страницу через Crawling API и извлекает структурированную запись по каждому товару. В качестве сквозного примера мы используем поиск по запросу tool box, тот же запрос, который использовался в устаревшем руководстве, и выберем следующие поля из каждого объявления:
- Бренд имя производителя, показанное на карточке товара.
- Название название товара, как оно указано.
- Цена указанная цена, когда товар её показывает.
- Номер Zoro идентификатор в стиле SKU из URL товара, например G6893443.
- Доступность статус наличия, например в наличии или под заказ.
- Ссылка абсолютный URL на собственную страницу деталей товара.
Почему обычный запрос не работает на Zoro
Если вы направите простой HTTP клиент на URL поиска Zoro, вы редко получите сетку товаров, за которой пришли. Против вас работают две вещи. Во-первых, Zoro отрисовывает значительную часть объявления на стороне клиента: страница поставляет лёгкую оболочку и заполняет карточки товаров по мере выполнения своего JavaScript, поэтому в исходном HTML часто отсутствуют позиции, которые вы хотите разобрать. Во-вторых, Zoro отмечает автоматический трафик. Диапазоны IP дата-центров и шаблоны запросов, которые не похожи на настоящий браузер, встречают страницу проверки или прямую блокировку ещё до того, как вы доберётесь до объявлений.
Поэтому работающему парсеру Zoro нужны две вещи в одном запросе: браузер, который отрисовывает страницу, и IP, который Zoro воспринимает как настоящего покупателя. Вы можете собрать это самостоятельно с помощью headless браузера и пула ротируемых резидентных прокси, но поддерживать этот стек в рабочем состоянии это и есть большая часть работы. Crawling API объединяет оба компонента в одном вызове: вы отправляете ему URL, он отрисовывает страницу за доверенным резидентным IP, обрабатывает ротацию и решение CAPTCHA и возвращает вам готовый HTML для разбора.
Предварительные требования
Прежде чем писать какой-либо код, вам нужно подготовить несколько вещей. Ни одна из них не займёт много времени.
Базовый Python. Вам стоит уверенно писать и запускать скрипт на Python и устанавливать пакеты через pip. Если вы новичок в языке, руководство о веб-скрапинге с помощью Python охватывает уровень, который предполагает этот учебник.
Python 3.8 или новее. Проверьте свою версию командой python --version (или python3 --version). Если у вас её нет, установите её с python.org и убедитесь, что Python находится в системном PATH.
Аккаунт Crawlbase и токен. Зарегистрируйте бесплатный аккаунт, откройте панель управления и скопируйте свой токен со страницы документации аккаунта. Бесплатный тариф включает до 5 000 запросов без карты, чего вполне достаточно, чтобы собрать и протестировать этот парсер. Поскольку Zoro отрисовывается с помощью JavaScript, используйте свой токен JavaScript для этих запросов. Относитесь к токену как к паролю и держите его вне системы контроля версий.
Настройте проект
Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите две библиотеки, которые нужны парсеру. crawlbase это официальный клиент для Crawling API, а beautifulsoup4 разбирает возвращённый HTML, чтобы вы могли извлекать каждое поле из карточек товаров по CSS селектору.
python --version python -m venv zoro_env source zoro_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой zoro_env\Scripts\activate вместо строки source. С установленными обеими библиотеками создайте файл скрипта, который остальная часть руководства будет наращивать:
touch zoro_scraper.py
Понимание страницы поиска Zoro
Поиск Zoro живёт по стабильному URL, построенному из параметра запроса q, например https://www.zoro.com/search?q=tool+box, и делает пагинацию через параметр page, добавляемый как &page=2. Страница выкладывает сетку карточек товаров, по одной на позицию, каждая несёт ту же горстку полей: имя бренда, название, цену, миниатюру изображения и ссылку на страницу деталей товара.
Прежде чем писать селекторы, откройте страницу поиска Zoro в своём браузере, щёлкните правой кнопкой мыши по карточке товара и выберите Inspect. Из обозревателя DOM вы можете считать CSS селекторы для каждого поля:
-
Имя бренда внутри
<span>с классомbrand-name. -
Название товара внутри
<div>с классомproduct-title. -
Цена внутри
<div>с классомprice. -
URL товара в
hrefэлемента<a>, вложенного внутрьdiv.product-title. -
Изображение товара в
srcэлемента<img>, несущегоdata-za="product-image".
Сами карточки товаров находятся внутри контейнера section[data-za="product-cards-list"], каждая из них это div.search-product-card, которая и является целью цикла. Номер Zoro не является собственным элементом на карточке: это сегмент SKU в URL товара (G6893443 в /i/G6893443/), поэтому вы выводите его из ссылки, а не выбираете напрямую.
Шаг 1: Получите отрисованную страницу поиска
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его своим токеном, задайте URL поиска и запросите его. Проверка кода статуса перед разбором делает сбои громкими, а не тихими.
from crawlbase import CrawlingAPI 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["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": search_url = "https://www.zoro.com/search?q=tool+box" html = crawl(search_url) print(html[:500] if html else "No HTML returned")
Две опции ожидания важны для сетки, которая заполняется по мере загрузки страницы. ajax_wait сообщает API, что нужно дождаться окончания асинхронного контента, а page_wait выдерживает фиксированное число миллисекунд (здесь 5000, поскольку страницы объявлений Zoro могут быть медленными), чтобы поздно отрисовываемые карточки появились до того, как страница будет захвачена. Запустите скрипт, и вы должны увидеть реальную разметку объявлений, а не оболочку проверки. Это подтверждает, что отрисовка работает, прежде чем вы напишете хоть один селектор.
Сетке товаров Zoro нужна отрисованная страница за доверенным IP, в одном вызове. Crawling API принимает ваш токен, запускает страницу поиска в настоящем браузере с опциями ajax_wait и page_wait, которые вы только что задали, ротирует резидентные IP на стороне сервера и обрабатывает решение CAPTCHA, затем отдаёт вам готовый HTML. Вам не нужно самостоятельно держать парк headless браузеров и пул прокси. Сначала направьте его на URL поиска на бесплатном тарифе до 5 000 запросов.
Шаг 2: Разберите карточки товаров с помощью BeautifulSoup
Имея на руках отрисованный HTML, загрузите его в BeautifulSoup, найдите каждую карточку товара и извлеките каждое поле по его селектору. Каждая карточка живёт внутри контейнера section[data-za="product-cards-list"] как div.search-product-card. Считайте бренд, название, цену, изображение и ссылку с карточки, затем выведите номер Zoro из ссылки. Оберните каждую карточку в try/except, чтобы одно неправильно сформированное объявление не обрушило запуск.
import re from bs4 import BeautifulSoup BASE = "https://www.zoro.com" def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def zoro_number(url): match = re.search(r"/i/(G\d+)/", url) return match.group(1) if match else None def scrape_listings(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('section[data-za="product-cards-list"] > div.search-product-card') results = [] for card in cards: try: anchor = card.select_one("div.product-title a") href = anchor["href"] if anchor else "" link = BASE + href if href.startswith("/") else href img = card.select_one('img[data-za="product-image"]') results.append({ "brand": text_of(card, "span.brand-name"), "title": text_of(card, "div.product-title"), "price": text_of(card, "div.price"), "zoro_number": zoro_number(link), "availability": text_of(card, "div.availability"), "image_url": img["src"] if img else None, "link": link, }) except Exception as e: print(f"Skipped a card: {e}") return results
Помощник text_of запрашивает один элемент внутри карточки и возвращает None, когда тот отсутствует, вместо того чтобы выбросить ошибку при вызове .get_text() на ничём. Это сохраняет извлечение устойчивым, когда поле отсутствует, что часто встречается, поскольку не каждая карточка показывает цену или значок доступности. Помощник zoro_number вытягивает SKU с префиксом G из URL товара с помощью небольшого регулярного выражения, а ссылка нормализуется до абсолютного URL, поскольку Zoro отдаёт относительный href.
Имена классов вроде brand-name, product-title и price могут измениться, когда Zoro переработает свою разметку, тогда как структурные маркеры вроде атрибута data-za="product-cards-list" обычно более долговечны. Относитесь к селекторам выше как к стартовому шаблону, а не как к контракту. Когда поле возвращается как None для каждой карточки, заново осмотрите живую страницу поиска в инструментах разработчика своего браузера и обновите селектор. Периодическое обслуживание селекторов это норма для любого боевого парсера.
Шаг 3: Обработайте пагинацию и экспортируйте JSON и CSV
Одна страница поиска это демонстрация; реальная задача обходит весь набор результатов. Zoro делает пагинацию через параметр page, поэтому вы добавляете &page=N и зацикливаетесь, пока страница не вернёт отсутствие карточек. Теперь свяжите получение, разбор и цикл пагинации в один готовый к запуску скрипт, затем запишите записи и в JSON, и в CSV, чтобы вы могли загрузить их в блокнот или электронную таблицу.
import csv import json import re import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://www.zoro.com" FIELDS = ["brand", "title", "price", "zoro_number", "availability", "image_url", "link"] def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") 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 zoro_number(url): match = re.search(r"/i/(G\d+)/", url) return match.group(1) if match else None def scrape_listings(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select('section[data-za="product-cards-list"] > div.search-product-card') results = [] for card in cards: try: anchor = card.select_one("div.product-title a") href = anchor["href"] if anchor else "" link = BASE + href if href.startswith("/") else href img = card.select_one('img[data-za="product-image"]') results.append({ "brand": text_of(card, "span.brand-name"), "title": text_of(card, "div.product-title"), "price": text_of(card, "div.price"), "zoro_number": zoro_number(link), "availability": text_of(card, "div.availability"), "image_url": img["src"] if img else None, "link": link, }) except Exception as e: print(f"Skipped a card: {e}") return results def scrape_all_pages(search_url, max_pages=5): all_rows = [] for page in range(1, max_pages + 1): page_url = f"{search_url}&page={page}" print(f"Scraping page {page}...") html = crawl(page_url) if not html: break rows = scrape_listings(html) if not rows: print("No more products. Stopping.") break all_rows.extend(rows) time.sleep(2) return all_rows def export(rows, name="zoro_listings"): 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_url = "https://www.zoro.com/search?q=tool+box" rows = scrape_all_pages(search_url, max_pages=3) export(rows) if __name__ == "__main__": main()
Запустите полный скрипт командой python zoro_scraper.py. Он обходит до max_pages страниц поиска, разбирает по одной строке на товар и записывает и zoro_listings.json, и zoro_listings.csv. Цикл scrape_all_pages останавливается рано, когда страница возвращает отсутствие карточек, а time.sleep(2) между запросами регулирует темп запуска. Общий список FIELDS держит порядок столбцов CSV в согласии с ключами словаря, так что два экспорта никогда не расходятся.
Как выглядит вывод
Вы получаете чистый список записей товаров, в порядке страниц, готовый к записи в JSON, CSV или базу данных.
[ { "brand": "Apex Tool Group", "title": "3 Drawer Tool Box", "price": "$149.99 /ea", "zoro_number": "G6893443", "availability": "In Stock", "image_url": "https://www.zoro.com/static/cms/product/prev/KDT83151xx1200.jpg", "link": "https://www.zoro.com/apex-tool-group-3-drawer-tool-box-83151/i/G6893443/" }, { "brand": "Dewalt", "title": "Rolling Tool Box, Plastic, Black, 28 in W", "price": "$43.19 /ea", "zoro_number": "G3778857", "availability": "In Stock", "image_url": "https://www.zoro.com/static/cms/product/prev/Z1wK0zqcpEx-.JPG", "link": "https://www.zoro.com/dewalt-rolling-tool-box-plastic-black-28-in-w-dwst28100/i/G3778857/" } ]
Парсинг отдельных страниц товаров
Парсер поиска даёт вам сетку объявлений. Когда вам нужны более глубокие детали (полное описание, таблица характеристик, каждое изображение), пройдите по link из каждой записи на её страницу товара и разберите её. Zoro показывает эти элементы на странице товара:
-
Название товара в
<h1>сdata-za="product-name". -
Цена в
<div>сdata-za="product-price". -
Описание в
div.product-description div.description-text. -
Характеристики строки в
<table>внутриdiv.product-details-info, две ячейки<td>на строку. -
Изображения товара теги
<img>с классомproduct-imageвнутриdiv.product-images.
import re from bs4 import BeautifulSoup def clean(value): return re.sub(r"\s+", " ", value).strip() if value else None def scrape_product_page(product_url): html = crawl(product_url) if not html: return {} soup = BeautifulSoup(html, "html.parser") title = soup.select_one('h1[data-za="product-name"]') price = soup.select_one('div[data-za="product-price"]') desc = soup.select_one("div.product-description div.description-text") specs = {} for row in soup.select("div.product-details-info table tr"): cells = row.find_all("td") if len(cells) == 2: specs[clean(cells[0].text)] = clean(cells[1].text) images = [img["src"] for img in soup.select("div.product-images img.product-image") if img.get("src")] return { "title": clean(title.text) if title else None, "price": clean(price.text) if price else None, "zoro_number": zoro_number(product_url), "description": clean(desc.text) if desc else None, "specifications": specs, "image_urls": images, "url": product_url, }
Это переиспользует те же помощники crawl и zoro_number из парсера объявлений, так что вы можете передать ему любую link из результатов поиска. Помощник clean сворачивает последовательности пробелов в одиночные пробелы, что важно для таблиц характеристик Zoro и многострочных описаний. Характеристики возвращаются как плоский словарь ключ-значение, по одной записи на каждую строку таблицы из двух ячеек, что является той формой, которая чисто загружается в DataFrame или набор столбцов базы данных.
Как оставаться незаблокированным
Даже когда отрисовка решена, Zoro следит за трафиком, похожим на парсер. Несколько привычек помогают запуску оставаться здоровым, и они применимы к любой сложной коммерческой цели.
- Регулируйте темп запросов. Распределяйте запросы с задержкой между страницами, а не обходите всё на полной скорости. Планируйте более тяжёлые задачи на часы низкой нагрузки, чтобы облегчить нагрузку на серверы Zoro.
- Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, так что ни один из них не срабатывает лимит частоты. Crawling API делает это за вас; если вы собираете свой собственный стек, именно эту часть нужно сделать правильно.
- Храните только то, что вам нужно. Сохраняйте поля каталога, которые использует ваш проект, и отбрасывайте остальное. Периодически перепроверяйте свои селекторы, чтобы парсер успевал за изменениями разметки.
За более широким сценарием по избеганию блокировок обратитесь к тому, как парсить сайты, не получая блокировок. Если вашей целью является отслеживание цен, руководство о веб-скрапинге для ценовой аналитики показывает, как превратить эти снимки в поток трендов, а обзор веб-скрапинга для электронной коммерции охватывает более широкий шаблон по розничным сайтам. Для похожей цели MRO и офисных товаров обратитесь к тому, как парсить Office Depot.
Законно ли парсить Zoro?
Разрешено ли парсить Zoro, зависит от условий обслуживания Zoro, вашей юрисдикции и того, что вы делаете с данными. Условия Zoro накладывают ограничения на автоматический доступ, поэтому парсинг может противоречить этим условиям независимо от того, насколько аккуратен ваш инструментарий. Ничего из кода здесь это не меняет; он лишь заставляет работать техническую часть. Прочитайте условия обслуживания Zoro и его robots.txt и относитесь к обоим как к границе того, что вы собираете. Для коммерческого или конкурентного использования юридическая картина становится сложнее, и консультация с юристом по вашему конкретному случаю это разумный шаг.
Несколько правил, которых стоит придерживаться. Собирайте только публичные данные: бренды, названия, цены, номера Zoro, доступность и ссылки на объявления, которые любой может увидеть на странице поиска или товара без аккаунта. Держите объём своих запросов достаточно низким, чтобы не нагружать серверы Zoro, и избегайте персональных данных, включая всё, что привязано к идентифицируемым покупателям, рецензентам или продавцам, сверх того, что указано публично. Не распространяйте оптом защищённую авторским правом фотографию товаров или описания Zoro. Если вы планируете повторно использовать данные коммерчески, получите разрешение или официальное соглашение, а не предполагайте, что молчание это согласие.
Это руководство намеренно ограничено публичными страницами каталога, потому что это та граница, которая делает работу защитимой. Оно не охватывает что-либо за входом в систему, данные аккаунта или заказа, личную информацию либо любую попытку обойти аутентификацию или CAPTCHA, которую вы не имеете права пройти. Если Zoro предлагает поток бизнес-данных, партнёрскую программу или официальный API для вашего сценария, это правильный инструмент, когда вам нужны большие объёмы, гарантированная структура или коммерческие права. Если вашему проекту нужно больше, чем публичные данные каталога, правильным путём является официальный канал или соглашение о данных, а не более хитрый парсер.
Ключевые выводы
- Объявления Zoro это публичные данные каталога. Каждая страница поиска и товара несёт бренд, название, цену, номер Zoro и доступность, поэтому они так полезны для отслеживания цен и исследования промышленных поставок.
-
Вам нужны отрисовка и доверенный IP вместе. Zoro заполняет свою сетку товаров на стороне клиента и бросает вызов трафику ботов, поэтому Crawling API отрисовывает страницу за резидентным IP в одном вызове с
ajax_waitиpage_wait. -
BeautifulSoup выполняет извлечение. Пройдите по карточкам
div.search-product-card, сопоставьте бренд, название, цену, доступность и ссылку с текущими селекторами и выведите номер Zoro из URL с помощью небольшого регулярного выражения. -
Делайте пагинацию и экспортируйте в JSON и CSV. Добавляйте
&page=Nи зацикливайтесь, пока страница не вернёт отсутствие карточек; общий список полей держит экспорты JSON и CSV синхронизированными. - Оставайтесь на публичных данных. Уважайте условия обслуживания и robots.txt Zoro, регулируйте темп своих запросов, предпочитайте официальный поток для лицензированных или массовых данных и никогда не трогайте аккаунты, заказы или личную информацию.
Часто задаваемые вопросы
Почему обычный запрос не возвращает товары из Zoro?
Две причины. Zoro заполняет значительную часть своей сетки товаров на стороне клиента по мере загрузки страницы, поэтому сырой запрос часто получает оболочку без объявлений. Вдобавок к этому Zoro бросает вызов или блокирует трафик, который не похож на настоящий браузер. Отрисовка страницы через Crawling API за доверенным IP решает оба, поэтому парсер здесь направляет свой запрос через него с ajax_wait и page_wait в 5000 миллисекунд.
Какие поля я могу извлечь со страницы поиска Zoro?
Из каждой карточки товара вы можете считать бренд (span.brand-name), название (div.product-title), цену (div.price), миниатюру изображения (img[data-za="product-image"]) и ссылку на товар. Номер Zoro это SKU с префиксом G в этой ссылке, который вы вытягиваете коротким регулярным выражением. Страницы товаров добавляют полное описание, таблицу характеристик и больший набор изображений.
Как мне обработать пагинацию на Zoro?
Zoro делает пагинацию через параметр запроса page. Добавляйте &page=2, &page=3 и так далее к URL поиска и зацикливайтесь, останавливаясь, когда страница возвращает отсутствие карточек товаров или когда вы достигаете установленного вами лимита страниц. Добавьте короткую задержку между запросами, чтобы не обходить на полной скорости.
Как мне получить номер Zoro для товара?
Номер Zoro это сегмент SKU в URL товара, G6893443 в /i/G6893443/. Парсер выводит его регулярным выражением по ссылке, а не выбирая отдельный элемент, поэтому он доступен и для записей результатов поиска, и для отдельных страниц товаров.
Могу ли я хранить спарсенные данные как CSV вместо JSON?
Да. Скрипт записывает оба: файл JSON для вложенной структуры и CSV для электронных таблиц. Общий список FIELDS управляет заголовком CSV и порядком столбцов, так что он остаётся согласованным с ключами словаря. Вы также можете загрузить записи прямо в базу данных, если предпочитаете.
Как избежать блокировки при парсинге Zoro?
Держите частоту запросов на один IP низкой, добавьте задержку между страницами и направляйте трафик через ротируемые резидентные IP, чтобы ни один адрес не срабатывал лимит частоты. Crawling API управляет ротацией, пулом доверенных IP и обработкой CAPTCHA за вас; если вы строите свой собственный стек, именно в эту часть стоит вложиться. Следите за кодами статуса и отступайте, когда начинаете видеть проверки.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
