Houzz является одной из крупнейших платформ в области дизайна интерьера, мебели и ремонта, сочетающей обширный каталог товаров с редакционным вдохновением. На публичных страницах товаров и объявлений находятся именно те структурированные данные, которые лежат в основе мониторинга цен, конкурентного анализа и анализа трендов: название товара, цена, рейтинг и количество отзывов, продавец или бренд, категория и ссылка на каждую страницу товара. Собирать это вручную по категории с сотнями товаров медленно и чревато ошибками.
Это руководство показывает, как парсить данные с Houzz с помощью Python надёжным способом. Вы создадите небольшой рабочий скрейпер, который получает отрендеренные страницы Houzz через Crawling API, собирает ссылки на товары из листинга категории, разбирает нужные поля с помощью BeautifulSoup, обрабатывает пагинацию и экспортирует чистые данные в JSON и CSV. Всё руководство ограничено публичными данными товаров, а раздел о законности в конце, не просто формальность, поэтому прочитайте его перед тем, как применять это на реальных объёмах.
Что вы создадите
Python-скрипт, который принимает публичный URL категории Houzz, собирает ссылки на страницы товаров, получает каждую отрендеренную страницу через Crawling API и извлекает структурированную запись на каждый товар. Рабочий пример, категория ванных тумб и консолей для раковин. Мы извлекаем следующие поля:
- Название, название товара, как показано на карточке и странице товара.
- Цена, указанная цена товара.
- Рейтинг, средний звёздный рейтинг.
- Отзывы, количество пользовательских отзывов, стоящих за этим рейтингом.
- Продавец, продавец, магазин или бренд, предлагающий товар.
- Категория, категория, к которой относится товар.
- Ссылка, канонический URL страницы товара.
Почему обычный запрос не работает на Houzz
Если запросить URL категории или товара Houzz с помощью обычного HTTP-клиента, вы получите ответ со статусом 200 и лишь долей данных в теле. Работают против вас два фактора. Во-первых, Houzz рендерит большую часть сетки товаров и деталей товара в браузере через JavaScript, поэтому исходный HTML является тонкой оболочкой, которая заполняется только после выполнения скриптов страницы. Извлеките карточки товаров из первого ответа и вы захватите единицы позиций или совсем ничего. Во-вторых, Houzz быстро выявляет автоматизированный трафик: IP-адреса датацентров и паттерны, не похожие на браузерные, подвергаются ограничению скорости, IP-блокировке или вызовам прежде, чем добираются до отрендеренного контента.
Таким образом, рабочий скрейпер Houzz нуждается в двух вещах одновременно: в браузере, который действительно рендерит страницу, и в IP-адресе, воспринимаемом платформой как реальный посетитель. Можно собрать это самостоятельно с помощью headless-браузера и пула ротирующихся жилых прокси, но поддержка их работоспособности требует большей части усилий. Crawling API объединяет оба компонента в один вызов: вы отправляете ему URL с JavaScript-токеном, он рендерит страницу за доверенным IP и возвращает готовый HTML для парсинга. Если JavaScript-насыщенные цели для вас в новинку, в руководстве по сканированию JavaScript-сайтов более подробно раскрывается причина подобного поведения.
Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала рендерит страницу в настоящем браузере. Houzz заполняет сетку товаров и поля товаров на стороне клиента, поэтому здесь нужен JS-токен. Обычный токен возвращает ту же тонкую оболочку, что и обычный запрос, из которой мало что можно полезно разобрать.
Предварительные требования
Перед написанием кода вам потребуется несколько вещей. Ни одна из них не занимает много времени.
Базовые знания Python. Вы должны уметь писать и запускать Python-скрипты и устанавливать пакеты с помощью pip. Если вы новичок в парсинге, руководство по BeautifulSoup является хорошим дополнением к данному материалу.
Python 3.8 или новее. Проверьте версию командой python --version. Если Python не установлен, загрузите его с python.org или через дистрибутив вроде Anaconda и убедитесь, что Python находится в PATH.
Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте дашборд и скопируйте ваш JavaScript (JS) токен со страницы документации аккаунта. Crawlbase включает до 20 000 бесплатных запросов для начала, чего вполне достаточно для прохождения данного руководства. Обращайтесь с токеном как с паролем: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальное окружение, чтобы зависимости проекта были изолированы, затем установите необходимые библиотеки.
python --version python -m venv houzz_env source houzz_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой houzz_env\Scripts\activate вместо строки с source. Два зависимых пакета выполняют всю работу: crawlbase является официальным клиентом Crawling API, а beautifulsoup4 разбирает возвращаемый HTML для извлечения отдельных полей по CSS-селектору. Оба модуля json и csv входят в стандартную библиотеку, поэтому для шага экспорта ничего дополнительно устанавливать не нужно.
Шаг 1: Получение отрендеренной страницы Houzz
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с помощью JS-токена и запросите URL категории Houzz. Houzz загружает сетку асинхронно, поэтому передайте ajax_wait и page_wait для ожидания динамического контента до захвата страницы. Проверка cb_status (legacy pc_status) Crawlbase перед парсингом делает сбои явными, а не скрытыми.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } def crawl(page_url): 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__": listing_url = "https://www.houzz.com/products/bathroom-vanities-and-sink-consoles/best-sellers--best-sellers" html = crawl(listing_url) print(html[:500] if html else "No HTML returned")
Два параметра ожидания важны для цели с клиентским рендерингом, такой как Houzz. ajax_wait указывает API дождаться завершения загрузки асинхронного контента, а page_wait задаёт фиксированное количество миллисекунд после загрузки, чтобы поздно рендерящиеся карточки появились до захвата страницы. Пять секунд, разумное начало; увеличьте это значение, если результаты возвращаются неполными. Запустите скрипт командой python houzz_scraper.py и вы должны увидеть реальную разметку категории Houzz, а не оболочку от обычного запроса. Это подтверждает работоспособность рендеринга до написания единого селектора.
Houzz требует отрендеренную страницу за доверенным IP в одном вызове, что именно настраивают опции ajax_wait и page_wait выше. Crawling API принимает JS-токен, запускает страницу в настоящем браузере, ротирует жилые IP на стороне сервера и возвращает готовый HTML, избавляя вас от необходимости запускать headless-флот и пул прокси самостоятельно. Направьте его на публичную страницу категории с бесплатного уровня.
Шаг 2: Сбор ссылок на товары со страницы категории
Страница категории Houzz, это сетка карточек товаров, каждая из которых ссылается на полную страницу товара. Загрузите отрендеренный HTML в BeautifulSoup и извлеките href из ссылки заголовка каждой карточки. Houzz вкладывает их внутрь контейнера списка товаров, поэтому селектор проходит от списка до карточки и якоря её заголовка.
from bs4 import BeautifulSoup CARD_SELECTOR = ( 'div[data-container="Product List"] > div.hz-product-card ' 'a.hz-product-card__product-title' ) def get_product_urls(html): soup = BeautifulSoup(html, "html.parser") return [a["href"] for a in soup.select(CARD_SELECTOR) if a.get("href")]
Каждая карточка товара имеет класс hz-product-card, а ссылка заголовка внутри неё имеет класс hz-product-card__product-title с URL страницы товара в атрибуте href. Запуск этого кода на отрендеренном HTML категории возвращает чистый список URL страниц товаров:
[ "https://www.houzz.com/products/the-sequoia-bathroom-vanity-acacia-30-single-sink-freestanding-prvw-vr~170329010", "https://www.houzz.com/products/bosque-bath-vanity-driftwood-42-single-sink-undermount-freestanding-prvw-vr~107752516", "https://www.houzz.com/products/render-bathroom-vanity-oak-white-prvw-vr~176775440", "https://www.houzz.com/products/the-wailea-bathroom-vanity-single-sink-42-weathered-fir-freestanding-prvw-vr~188522678" ]
Имена классов и атрибуты контейнера Houzz изменяются без предупреждения. Воспринимайте приведённые здесь селекторы как отправную точку, а не контракт. Когда список возвращается пустым, повторно проверьте живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов нормально для любого продакшен-скрейпера, это не признак неисправности.
Шаг 3: Обработка пагинации по страницам категории
Одна страница категории, это срез каталога. Houzz предоставляет ссылку «следующая страница» в элементе управления пагинацией, поэтому вы получаете страницу, собираете ссылки, затем переходите по ссылке на следующую страницу до её отсутствия. Небольшая обёртка с повторными попытками вокруг функции получения не позволяет одной медленной странице завершить весь запуск.
import time BASE = "https://www.houzz.com" def fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: print(f"Retrying ({attempt + 1}/{max_retries})...") time.sleep(1) print(f"Unable to fetch {page_url}") return None def get_next_page_url(soup): nxt = soup.select_one("a.hz-pagination-link--next") return BASE + nxt["href"] if nxt and nxt.get("href") else None def collect_all_urls(start_url, max_pages): all_urls = [] url = start_url page = 0 while url and page < max_pages: html = fetch_html(url) if not html: break all_urls.extend(get_product_urls(html)) soup = BeautifulSoup(html, "html.parser") url = get_next_page_url(soup) page += 1 time.sleep(2) return all_urls
Функция fetch_html повторяет неудавшееся получение до двух раз с небольшой паузой, возвращая HTML в случае успеха и None после исчерпания попыток. Функция get_next_page_url читает якорь hz-pagination-link--next и объединяет его относительный href с хостом Houzz. Функция collect_all_urls следует по этой ссылке на следующую страницу от одной страницы к другой, ограничивая сканирование потолком max_pages, чтобы крупная категория не вышла из-под контроля, и собирает ссылки с каждой страницы. Вызов time.sleep(2) между страницами задаёт темп, чтобы не перегружать сайт.
Шаг 4: Парсинг каждой страницы товара
Имея полный список URL товаров, получите каждую страницу и извлеките поля. Houzz группирует основные детали вокруг блока цены, поэтому приведённые ниже селекторы сопоставляют название, цену, рейтинг, количество отзывов, продавца и категорию с отдельными элементами. Каждый запрос защищён, чтобы отсутствующее поле возвращало None, а не прерывало выполнение.
def text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def get_rating(soup): star = soup.select_one("span.star-rating") if star and star.get("aria-label"): return star["aria-label"].replace("Average rating: ", "") return None def scrape_product(html, url): soup = BeautifulSoup(html, "html.parser") return { "link": url, "name": text_of(soup, "span.view-product-title"), "price": text_of(soup, "span.pricing-info__price"), "rating": get_rating(soup), "reviews": text_of(soup, "span.review-count"), "seller": text_of(soup, "a.seller-name"), "category": text_of(soup, "nav.breadcrumb li:last-child"), }
Вспомогательный метод text_of запрашивает один элемент и возвращает его очищенный текст или None при отсутствии элемента, так что товар с отсутствующим полем не прерывает цикл. Селекторы берутся прямо из вёрстки товара Houzz: name читает span view-product-title, price читает span pricing-info__price, а рейтинг находится в span star-rating, чей атрибут aria-label выглядит как «4.9 out of 5 stars», который функция get_rating очищает. Количество отзывов, продавец и категория расположены в окружающих метаданных; повторно проверьте живую страницу, если любое из них возвращает None, поскольку Houzz периодически пересматривает эти обёртки.
Шаг 5: Сборка полного скрипта
Теперь объедините части в один рабочий скрипт: соберите URL по страницам, разберите каждый товар и экспортируйте записи в JSON и CSV.
import csv import json import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://www.houzz.com" OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } CARD_SELECTOR = ( 'div[data-container="Product List"] > div.hz-product-card ' 'a.hz-product-card__product-title' ) def crawl(page_url): 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 fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: time.sleep(1) return None def text_of(soup, selector): el = soup.select_one(selector) return el.get_text(strip=True) if el else None def get_rating(soup): star = soup.select_one("span.star-rating") if star and star.get("aria-label"): return star["aria-label"].replace("Average rating: ", "") return None def get_product_urls(html): soup = BeautifulSoup(html, "html.parser") return [a["href"] for a in soup.select(CARD_SELECTOR) if a.get("href")] def get_next_page_url(soup): nxt = soup.select_one("a.hz-pagination-link--next") return BASE + nxt["href"] if nxt and nxt.get("href") else None def collect_all_urls(start_url, max_pages): all_urls = [] url = start_url page = 0 while url and page < max_pages: html = fetch_html(url) if not html: break all_urls.extend(get_product_urls(html)) soup = BeautifulSoup(html, "html.parser") url = get_next_page_url(soup) page += 1 time.sleep(2) return all_urls def scrape_product(html, url): soup = BeautifulSoup(html, "html.parser") return { "link": url, "name": text_of(soup, "span.view-product-title"), "price": text_of(soup, "span.pricing-info__price"), "rating": get_rating(soup), "reviews": text_of(soup, "span.review-count"), "seller": text_of(soup, "a.seller-name"), "category": text_of(soup, "nav.breadcrumb li:last-child"), } def save_outputs(records): with open("houzz_products.json", "w") as f: json.dump(records, f, indent=2) if not records: return with open("houzz_products.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=records[0].keys()) writer.writeheader() writer.writerows(records) def main(): listing_url = "https://www.houzz.com/products/bathroom-vanities-and-sink-consoles/best-sellers--best-sellers" urls = collect_all_urls(listing_url, max_pages=2) records = [] for url in urls: html = fetch_html(url) if html: records.append(scrape_product(html, url)) time.sleep(2) save_outputs(records) print(f"Saved {len(records)} products") if __name__ == "__main__": main()
Скрипт собирает ссылки на товары по двум страницам категории, получает каждую страницу товара с обёрткой повторных попыток, разбирает её в запись и задаёт темп цикла двухсекундной паузой. Функция save_outputs записывает и JSON-файл, и CSV, используя ключи первой записи в качестве заголовка, чтобы данные были в том формате, который нужен вашему downstream-инструменту. Скорректируйте max_pages и URL категории под вашу цель.
Как выглядит результат
Запустите полный скрипт командой python houzz_scraper.py и получите чистую структурированную запись на каждый товар, готовую для анализа, базы данных или таблицы.
[ { "link": "https://www.houzz.com/products/the-sequoia-bathroom-vanity-acacia-30-single-sink-freestanding-prvw-vr~170329010", "name": "The Sequoia Bathroom Vanity, Acacia, 30\", Single Sink, Freestanding", "price": "$948", "rating": "4.9 out of 5 stars", "reviews": "128 Reviews", "seller": "Cambridge Plumbing", "category": "Bathroom Vanities" }, { "link": "https://www.houzz.com/products/render-bathroom-vanity-oak-white-prvw-vr~176775440", "name": "Render Bathroom Vanity, Oak White", "price": "$295", "rating": "4.5 out of 5 stars", "reviews": "43 Reviews", "seller": "Modway", "category": "Bathroom Vanities" } ]
Соответствующий CSV содержит те же столбцы, по одной строке на товар, которые напрямую загружаются в pandas или любую таблицу для фильтрации по ценовому диапазону, рейтингу или продавцу. Если вашим destination является рабочий лист, а не скрипт, эти же записи питают конвейер парсинга электронной коммерции без дополнительного преобразования.
Сохранение незаблокированности при масштабировании
Даже при обработке рендеринга, Houzz отслеживает трафик, похожий на скрейперский. Несколько привычек помогают поддерживать работоспособность более длительных запусков, и они применимы к любой сложной коммерческой цели.
- Задавайте темп запросов. Непрерывное обращение к страницам товаров в жёстком цикле, это самый быстрый способ получить ограничение или вызов. Двухсекундные паузы выше, это минимум, а не максимум; увеличивайте их для более крупных задач и варьируйте цели вместо сканирования одной категории на полной скорости.
- Опирайтесь на ротацию. Пул жилых IP-адресов распределяет запросы по множеству реальных пользовательских адресов, чтобы ни один не превысил лимит скорости. Crawling API делает это за вас; если вы используете собственный стек, именно здесь нужно всё правильно настроить.
-
Читайте коды статусов. Если запуск начинает возвращать значения
cb_status, отличные от 200, это означает, что текущего темпа или уровня IP уже не достаточно. Воспринимайте это как сигнал снизить нагрузку, а не шум, который можно игнорировать.
Для более крупных сканирований асинхронный Crawler ставит запросы в очередь и доставляет результаты на вебхук, что подходит для запуска многих страниц категорий без удержания открытых соединений. Для более широкого руководства по стратегии см. статью о том, как парсить сайты без блокировки.
Законно ли парсить Houzz?
Допустимость парсинга Houzz зависит от условий использования Houzz, вашей юрисдикции и того, что вы делаете с данными. Условия использования Houzz ограничивают автоматизированный доступ и массовый сбор данных, поэтому парсинг может противоречить этим условиям независимо от тщательности инструментария. Ни один из приведённых здесь примеров кода этого не меняет, он лишь обеспечивает техническую реализацию. Прочитайте Условия использования Houzz и его robots.txt, соблюдайте любые ожидания по частоте запросов и запрещённые пути, которые они устанавливают, и руководствуйтесь ими как границами того, что вы собираете. Поддерживайте объём запросов достаточно низким, чтобы не перегружать серверы.
Несколько правил, которых стоит придерживаться. Собирайте только публичные данные товаров и листинга: название товара, цену, рейтинг, количество отзывов, продавца или бренд и категорию, которые может видеть любой без авторизации. Houzz также содержит большой массив пользовательского контента, включая фотографии проектов, дизайнерские идеи и отзывы, привязанные к конкретным лицам; относитесь ко всему, что связано с реальным человеком, как к персональным данным, выходящим за рамки данного руководства, и имейте в виду, что GDPR и CCPA применяются в момент появления персональных данных. Фотографии товаров и дизайнерские изображения на Houzz защищены авторским правом продавцов, дизайнеров и самого Houzz, поэтому сбор URL изображения не даёт вам права на его распространение, повторное использование или повторную публикацию. Ограничивайтесь фактическими полями товаров, а не медиафайлами.
Это руководство намеренно ограничено публичными страницами товаров, поскольку именно здесь проходит граница, позволяющая сохранять обоснованную позицию. Оно не касается ничего за логином, данных аккаунта или сохранённых идей, персональных данных домовладельцев или профессионалов, а также защищённых авторским правом фотографий, которые вы намерены распространять. Если ваш проект требует большего, правильный путь состоит в лицензионном соглашении: Houzz располагает публичным API и партнёрскими программами для разрешённых случаев использования, таких как Trade Program и интеграции с продавцами, что является правильным маршрутом для коммерческого или массового использования, а не разработка более умного скрейпера.
Ключевые выводы
- Houzz рендерится на стороне клиента. Обычный запрос возвращает тонкую оболочку с небольшой частью сетки товаров, поэтому страницу необходимо отрендерить перед парсингом.
-
Рендеринг и доверенный IP нужны вместе. Crawling API с JS-токеном делает оба в одном вызове;
ajax_waitиpage_waitуправляют временем ожидания контента. -
Работайте в два уровня. Собирайте ссылки на товары с каждой страницы категории с помощью селектора
hz-product-card__product-title, затем получайте и разбирайте каждый товар на название, цену, рейтинг, отзывы, продавца и категорию. -
Обрабатывайте пагинацию и экспортируйте. Следуйте по ссылке
hz-pagination-link--nextHouzz до заданного потолка, задавайте темп запуска небольшими паузами и записывайте записи в JSON и CSV. - Оставайтесь в рамках публичных данных. Соблюдайте условия использования Houzz и robots.txt, ограничивайтесь фактическими полями товаров, никогда не обращайтесь к авторизованным данным, персональным данным или защищённым авторским правом изображениям для распространения и используйте официальный API для массового или коммерческого использования.
Часто задаваемые вопросы
Почему обычный запрос возвращает почти никаких товаров Houzz?
Потому что Houzz загружает сетку товаров и детали товаров на стороне клиента с помощью JavaScript. Исходный HTML является оболочкой, которая заполняется только после выполнения скриптов страницы в браузере, поэтому обычный HTTP-запрос возвращает статус 200 с большинством карточек и полей товаров, которые отсутствуют. Для получения полного набора необходимо сначала отрендерить страницу, чем и занимается JS-токен Crawling API.
Какой токен нужен для Houzz: обычный или JS?
JS-токен. Обычный токен получает статический HTML, который на Houzz представляет собой ту же тонкую оболочку, что и обычный запрос. JS-токен рендерит страницу в настоящем браузере перед тем, как вернуть HTML, поэтому карточки товаров и поля товаров присутствуют при их парсинге BeautifulSoup.
Какие данные можно парсить со страницы товара Houzz?
Публичные поля товара: название товара, цену, средний рейтинг и количество отзывов, продавца или бренд, категорию и ссылку на товар. Ограничивайтесь данными, видимыми любому посетителю без авторизации, и избегайте фотографий проектов, отзывов или любого контента, связанного с идентифицируемыми лицами, что выходит за рамки публичного товарного раздела, охватываемого данным руководством.
Мои селекторы возвращают None. Что изменилось?
Почти наверняка разметка Houzz. Имена классов, такие как hz-product-card, pricing-info__price, и star-rating, и атрибуты контейнера изменяются без предупреждения, поэтому работавшие в прошлом месяце селекторы могут сломаться. Повторно проверьте живую страницу в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание селекторов нормально для любого продакшен-скрейпера.
Как парсить более одной страницы категории Houzz?
Houzz предоставляет якорь следующей страницы с классом hz-pagination-link--next. Прочитайте его href, объедините с хостом Houzz и переходите от страницы к странице до отсутствия ссылки или достижения потолка max_pages. Функция collect_all_urls выше показывает полный цикл с небольшой паузой между страницами.
Можно ли использовать scraped данные Houzz в коммерческих целях?
Относитесь к этому как к юридическому вопросу, а не техническому. Условия использования Houzz ограничивают повторное использование, большая часть изображений товаров защищена авторским правом продавцов и дизайнеров, а любой пользовательский контент является персональными данными, поэтому коммерческое или массовое использование обычно требует разрешения. Ознакомьтесь с условиями, рассмотрите официальный API Houzz и партнёрские программы и проконсультируйтесь с юристом перед созданием продукта на основе этих данных.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
