Как парсить отзывы о товарах в масштабе с помощью прокси

Научитесь парсить отзывы о товарах с Amazon и других платформ в масштабе. Код на Python и Node.js для мультиплатформенного сбора отзывов, обработки пагинации и подготовки к анализу тональности.

Как парсить отзывы о товарах в масштабе с помощью прокси

Зачем парсить отзывы о товарах в масштабе?

Отзывы о товарах — один из самых ценных источников данных в 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 для паттернов реализации.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог