Зачем парсить отзывы о товарах в масштабе?
Отзывы о товарах — один из самых ценных источников данных в e-commerce. Они раскрывают настроения клиентов, проблемы качества продуктов, запросы на функции и конкурентное позиционирование — информацию, которую не может предоставить ни один другой источник данных. В масштабе данные отзывов позволяют:
- Анализ тональности: отслеживайте, как клиенты относятся к вашим продуктам и продуктам конкурентов со временем.
- Разработка продуктов: выявляйте повторяющиеся жалобы и запросы на функции из тысяч отзывов.
- Конкурентная разведка: понимайте сильные и слабые стороны конкурентов со слов их собственных клиентов.
- Исследование рынка: обнаруживайте неудовлетворённые потребности и новые тенденции, анализируя паттерны отзывов по категориям.
- Мониторинг качества: обнаруживайте проблемы с качеством продуктов на ранней стадии, мониторя тренды тональности отзывов.
Проблема в том, что данные отзывов распределены по множеству платформ (Amazon, Walmart, Best Buy, Trustpilot, Google), каждая со своей структурой и антибот-защитой. Парсинг отзывов в масштабе требует стратегий, специфичных для каждой платформы, и надёжной прокси-инфраструктуры. Для базовых паттернов e-commerce скрапинга смотрите наше руководство по скрапингу e-commerce данных.
Структура данных отзывов на разных платформах
| Платформа | Поля отзыва | Пагинация | Уровень защиты |
|---|---|---|---|
| Amazon | Рейтинг, заголовок, текст, дата, верификация, полезность | Постраничная (10/стр.) | Высокий |
| Walmart | Рейтинг, заголовок, текст, дата, источник | API с офсетом | Средний |
| Best Buy | Рейтинг, заголовок, текст, дата, полезность | Постраничный API | Средний |
| Trustpilot | Рейтинг, заголовок, текст, дата, ответ | Постраничная | Низкий-Средний |
| Google Shopping | Рейтинг, текст, дата, источник | Скролл-пагинация | Высокий |
Настройка прокси для парсинга отзывов
Парсинг отзывов включает постраничную навигацию, что означает поддержание сессий через несколько запросов. Sticky-сессии ProxyHat идеально подходят для этого паттерна.
Настройка ProxyHat
# Ротация при каждом запросе для начальных поисков товаров
http://USERNAME:PASSWORD@gate.proxyhat.com:8080
# Sticky-сессия для постраничного просмотра отзывов одного товара
http://USERNAME-session-rev001:PASSWORD@gate.proxyhat.com:8080
# Геотаргетинг для страниц отзывов конкретного региона
http://USERNAME-country-US:PASSWORD@gate.proxyhat.com:8080
Для парсинга отзывов используйте sticky-сессии при постраничном просмотре всех отзывов одного товара и ротацию при каждом запросе при переходе между товарами. Это имитирует естественное поведение, когда пользователь читает несколько страниц отзывов одного товара перед переходом к следующему.
Реализация на Python
Вот мультиплатформенный парсер отзывов с использованием Python SDK от ProxyHat.
Парсер отзывов Amazon
import requests
from bs4 import BeautifulSoup
import json
import time
import random
from dataclasses import dataclass
from datetime import datetime
PROXY_URL = "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
]
@dataclass
class Review:
platform: str
product_id: str
rating: float
title: str
text: str
date: str
author: str
verified: bool
helpful_votes: int
def scrape_amazon_reviews(asin, max_pages=10):
"""Scrape all reviews for an Amazon product."""
reviews = []
session_id = f"rev-{asin}-{random.randint(1000, 9999)}"
proxy = f"http://USERNAME-session-{session_id}:PASSWORD@gate.proxyhat.com:8080"
session = requests.Session()
session.proxies = {"http": proxy, "https": proxy}
session.headers.update({
"User-Agent": random.choice(USER_AGENTS),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
})
for page in range(1, max_pages + 1):
url = (f"https://www.amazon.com/product-reviews/{asin}"
f"?pageNumber={page}&sortBy=recent")
try:
response = session.get(url, timeout=30)
if response.status_code != 200:
break
if "captcha" in response.text.lower():
print(f"CAPTCHA on page {page}, switching session")
break
soup = BeautifulSoup(response.text, "html.parser")
review_divs = soup.find_all("div", {"data-hook": "review"})
if not review_divs:
break
for div in review_divs:
review = parse_amazon_review(div, asin)
if review:
reviews.append(review)
print(f"Page {page}: {len(review_divs)} reviews (total: {len(reviews)})")
time.sleep(random.uniform(2, 5))
except requests.RequestException as e:
print(f"Error on page {page}: {e}")
break
return reviews
def parse_amazon_review(div, asin):
"""Parse a single Amazon review element."""
try:
rating_el = div.find("i", {"data-hook": "review-star-rating"})
rating = float(rating_el.get_text().split(" ")[0]) if rating_el else None
title_el = div.find("a", {"data-hook": "review-title"})
title = title_el.get_text(strip=True) if title_el else ""
body_el = div.find("span", {"data-hook": "review-body"})
text = body_el.get_text(strip=True) if body_el else ""
date_el = div.find("span", {"data-hook": "review-date"})
date_str = date_el.get_text(strip=True) if date_el else ""
author_el = div.find("span", {"class": "a-profile-name"})
author = author_el.get_text(strip=True) if author_el else ""
verified = bool(div.find("span", {"data-hook": "avp-badge"}))
helpful_el = div.find("span", {"data-hook": "helpful-vote-statement"})
helpful = 0
if helpful_el:
text_h = helpful_el.get_text()
if "one" in text_h.lower():
helpful = 1
else:
nums = [int(s) for s in text_h.split() if s.isdigit()]
helpful = nums[0] if nums else 0
return Review(
platform="amazon",
product_id=asin,
rating=rating,
title=title,
text=text,
date=date_str,
author=author,
verified=verified,
helpful_votes=helpful,
)
except Exception:
return None
Мультиплатформенный сборщик отзывов
class ReviewCollector:
"""Collect reviews from multiple platforms for a product."""
def __init__(self):
self.scrapers = {
"amazon": scrape_amazon_reviews,
}
def collect_all(self, product_ids: dict) -> list[Review]:
"""
Collect reviews from all platforms.
product_ids: {"amazon": "B0CHX3QBCH", "walmart": "12345"}
"""
all_reviews = []
for platform, product_id in product_ids.items():
if platform in self.scrapers:
print(f"\nScraping {platform} reviews for {product_id}")
reviews = self.scrapers[platform](product_id)
all_reviews.extend(reviews)
print(f"Collected {len(reviews)} reviews from {platform}")
time.sleep(random.uniform(5, 10))
return all_reviews
def to_dataframe(self, reviews: list[Review]):
"""Convert reviews to a pandas DataFrame for analysis."""
import pandas as pd
return pd.DataFrame([vars(r) for r in reviews])
# Использование
collector = ReviewCollector()
reviews = collector.collect_all({
"amazon": "B0CHX3QBCH",
})
print(f"\nTotal reviews collected: {len(reviews)}")
Реализация на Node.js
Парсер отзывов на Node.js с использованием Node SDK от ProxyHat.
const axios = require("axios");
const cheerio = require("cheerio");
const { HttpsProxyAgent } = require("https-proxy-agent");
function getProxy(sessionId = null) {
if (sessionId) {
return `http://USERNAME-session-${sessionId}:PASSWORD@gate.proxyhat.com:8080`;
}
return "http://USERNAME:PASSWORD@gate.proxyhat.com:8080";
}
async function scrapeAmazonReviews(asin, maxPages = 10) {
const reviews = [];
const sessionId = `rev-${asin}-${Math.floor(Math.random() * 9000 + 1000)}`;
const agent = new HttpsProxyAgent(getProxy(sessionId));
for (let page = 1; page <= maxPages; page++) {
const url = `https://www.amazon.com/product-reviews/${asin}?pageNumber=${page}&sortBy=recent`;
try {
const { data } = await axios.get(url, {
httpsAgent: agent,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
},
timeout: 30000,
});
if (data.toLowerCase().includes("captcha")) {
console.log(`CAPTCHA on page ${page}`);
break;
}
const $ = cheerio.load(data);
const reviewDivs = $('[data-hook="review"]');
if (reviewDivs.length === 0) break;
reviewDivs.each((_, el) => {
const $el = $(el);
const ratingText = $el.find('[data-hook="review-star-rating"]').text();
const rating = parseFloat(ratingText.split(" ")[0]) || null;
reviews.push({
platform: "amazon",
productId: asin,
rating,
title: $el.find('[data-hook="review-title"]').text().trim(),
text: $el.find('[data-hook="review-body"]').text().trim(),
date: $el.find('[data-hook="review-date"]').text().trim(),
author: $el.find(".a-profile-name").text().trim(),
verified: $el.find('[data-hook="avp-badge"]').length > 0,
});
});
console.log(`Page ${page}: ${reviewDivs.length} reviews (total: ${reviews.length})`);
await new Promise((r) => setTimeout(r, 2000 + Math.random() * 3000));
} catch (err) {
console.error(`Error page ${page}: ${err.message}`);
break;
}
}
return reviews;
}
scrapeAmazonReviews("B0CHX3QBCH", 5).then((reviews) => {
console.log(`Collected ${reviews.length} reviews`);
});
Обработка пагинации в масштабе
Пагинация отзывов — одна из главных сложностей крупномасштабного парсинга отзывов.
Стратегия пагинации на Amazon
Amazon ограничивает страницы отзывов 10 отзывами каждая и обычно показывает до 500 страниц (5 000 отзывов). Для товаров с большим количеством отзывов используйте параметры фильтрации для сегментации:
# Фильтрация по рейтингу для получения большего числа отзывов
star_filters = [
"one_star", "two_star", "three_star",
"four_star", "five_star"
]
for star in star_filters:
url = (f"https://www.amazon.com/product-reviews/{asin}"
f"?filterByStar={star}&pageNumber={page}")
# Это позволяет получить доступ к большему числу отзывов на товар
Управление сессиями при пагинации
Каждая пагинация отзывов товара должна использовать собственную sticky-сессию. Когда вы заканчиваете один товар и переходите к следующему, создавайте новую сессию с другим IP.
| Фаза | Стратегия прокси | Причина |
|---|---|---|
| Поиск товаров | Ротация при каждом запросе | Независимые запросы, сессия не нужна |
| Пагинация отзывов | Sticky-сессия на товар | Один IP на всех страницах выглядит естественно |
| Между товарами | Новая сессия/IP | Свежая идентичность для каждого товара |
Подготовка данных для анализа тональности
Сырой текст отзывов нуждается в предобработке перед анализом тональности.
import re
from collections import Counter
def clean_review_text(text):
"""Clean review text for analysis."""
text = re.sub(r'&\w+;', ' ', text)
text = re.sub(r'\s+', ' ', text).strip()
if len(text) < 20:
return None
return text
def extract_key_phrases(reviews, min_frequency=3):
"""Extract frequently mentioned phrases from reviews."""
from collections import Counter
import re
words = []
for review in reviews:
if review.text:
tokens = re.findall(r'\b\w+\b', review.text.lower())
for i in range(len(tokens) - 1):
bigram = f"{tokens[i]} {tokens[i+1]}"
words.append(bigram)
return Counter(words).most_common(50)
def aggregate_sentiment(reviews):
"""Calculate aggregate sentiment metrics."""
if not reviews:
return {}
ratings = [r.rating for r in reviews if r.rating]
return {
"total_reviews": len(reviews),
"avg_rating": sum(ratings) / len(ratings) if ratings else 0,
"rating_distribution": {
str(i): len([r for r in reviews if r.rating == i])
for i in range(1, 6)
},
"verified_pct": (
len([r for r in reviews if r.verified]) / len(reviews) * 100
if reviews else 0
),
}
Масштабирование до миллионов отзывов
Когда список целей растёт до тысяч товаров на нескольких платформах, архитектура имеет значение.
Архитектура на основе очередей
- Используйте очередь сообщений (Redis, RabbitMQ) для управления списком товаров и распределения работы между воркерами.
- Каждый воркер обрабатывает один товар за раз: постранично просматривает все отзывы, сохраняет результаты, переходит к следующему.
- Отдельные очереди по платформам для соблюдения разных лимитов частоты.
Стратегия хранения
- Храните сырой HTML в объектном хранилище (S3) для повторной обработки при изменении парсеров.
- Храните разобранные отзывы в PostgreSQL с полнотекстовым поиском для анализа.
- Используйте дедупликацию по ID отзыва или хешу для избежания дублирования при повторном парсинге.
Инкрементальный парсинг
Для постоянного мониторинга не нужно повторно парсить все отзывы каждый раз. Сортируйте по самым новым и останавливайтесь, когда встретите уже собранный отзыв. Это кардинально сокращает использование прокси и ускоряет сбор.
Ключевой вывод: сортируйте отзывы по самым новым и останавливайте парсинг при обнаружении ранее собранного контента. Это превращает полный повторный парсинг в инкрементальное обновление.
Лучшие практики
- Используйте sticky-сессии для пагинации: сохраняйте один IP на всех страницах отзывов одного товара для избежания срабатывания антибот-защиты.
- Соблюдайте лимиты частоты: задержки 2-5 секунд между страницами, более длинные задержки между товарами. Разные платформы имеют разную толерантность.
- Обрабатывайте пустые страницы: пустая страница отзывов означает конец. Не продолжайте пробовать следующие страницы.
- Валидируйте качество данных: проверяйте страницы CAPTCHA, пустой контент и дублирующиеся отзывы в конвейере.
- Используйте резидентные прокси: необходимы для Amazon и других сильно защищённых платформ.
- Сохраняйте инкрементально: обрабатывайте и сохраняйте отзывы по мере парсинга, а не одним пакетом в конце.
Ключевые выводы
- Данные отзывов дают уникальную конкурентную разведку, которую не может предоставить ни один другой источник.
- Разные платформы требуют разных стратегий скрапинга — создавайте модульные парсеры для каждой.
- Используйте sticky-сессии для пагинации отзывов и ротацию при каждом запросе между товарами.
- Сортируйте по самым новым и останавливайтесь на ранее собранных отзывах для эффективного инкрементального парсинга.
- Предобрабатывайте текст отзывов для анализа тональности: очищайте, дедуплицируйте, извлекайте ключевые фразы.
- Используйте резидентные прокси ProxyHat с геотаргетингом для надёжного доступа к страницам отзывов на всех платформах.
Готовы начать сбор данных отзывов? Смотрите наше руководство по парсингу Amazon для деталей по платформе и руководство по скрапингу e-commerce данных для полной стратегии. Ознакомьтесь с руководствами по использованию прокси в Python и использованию прокси в Node.js для паттернов реализации.






