איסוף וייבוא ביקורות מפלטפורמות מסחר: פיתוח פארסר
אנו בונים פארסרים לאיסוף אוטומטי של ביקורות מפלטפורמות מסחר. לקוח איבד 30% מהמרות בגלל ביקורות מיושנות — איסוף ידני לקח שעות, והנתונים לא עודכנו במשך שבועות. הצוות שלנו (ניסיון של 5+ שנים, 50+ פרויקטים) יצר פתרון שאוסף ביקורות מ-Wildberries, Ozon, Yandex.Market ופלטפורמות אחרות, מנרמל אותן ומייבא אותן למסד הנתונים של החנות שלך. חיסכון בזמן — עד 70%. החזר השקעה — 2–3 חודשים. הזמינו פיתוח פארסר לחנות המקוונת שלכם.
תרחיש טיפוסי: מנהל מעתיק ידנית ביקורות מ-5 פלטפורמות מסחר עבור 200 מוצרים — זה 8 שעות ביום. הנתונים מתיישנים, לקוחות לא רואים ביקורות חדשות במשך שבועות, והאמון יורד. פארסר אוסף ביקורות כל שעתיים, ומעדכן דפי מוצר בזמן אמת. תוצאה: גידול בהמרות של 15% בחודש הראשון.
מבחינה טכנית, כל פלטפורמת מסחר היא אתגר בפני עצמו. Wildberries מציעה JSON API, אבל עם מגבלה של 1000 ביקורות לכל סשן. Ozon היא SPA על Nuxt, שבו נתונים נטענים דרך GraphQL — אנחנו צריכים לחקות דפדפן עם Playwright. Yandex.Market משנה את המבנה שלו כל חצי שנה, אז אנחנו משתמשים בפארסינג אדפטיבי עם אסטרטגיית גיבוי. המחסנית שלנו: Python לפלטפורמות בעומס גבוה, PHP (Laravel) לאינטגרציה עם האתר שלך.
פלטפורמות נתמכות ושיטות
| פלטפורמה | שיטה | הערות |
|---|---|---|
| Wildberries | JSON API | API פתוח, פגינציה, עד 1000 ביקורות לכל סשן |
| Ozon | Playwright | SPA, דורש הרשאה, איטי פי 50 מ-JSON |
| Yandex.Market | API לא רשמי | הגבלת קצב, דורש סיבוב פרוקסי |
| Google Reviews | Places API | בתשלום, רשמי, עד 5 ביקורות לכל בקשה (דורש מנוי עסקי) |
| Otzovik.com | פארסינג HTML | CAPTCHA בבקשות המוניות — אנו משתמשים בפותרי CAPTCHA |
| iHerb | HTML / JSON API | HTML מובנה, הקל ביותר |
השוואת שיטות: JSON-API (Wildberries) מעבד 1000 ביקורות ב-10 שניות, בעוד Selenium על Ozon לוקח 5 דקות לאותן 1000. זה הבדל של פי 30. לביצועים, אנו בוחרים ב-JSON בכל מקום אפשרי.
איך הפארסר של Wildberries עובד
אנו משתמשים ב-httpx אסינכרוני כדי לעבור על פגינציה. דוגמה:
לחצו להרחבת דוגמת קוד Python
# scraper/reviews/wildberries.py
import httpx
import asyncio
from dataclasses import dataclass
from typing import Optional
@dataclass
class Review:
external_id: str
product_nm_id: int
author: str
rating: int
text: str
pros: Optional[str]
cons: Optional[str]
date: str
photos: list[str]
helpful_count: int
class WildberriesReviewScraper:
REVIEWS_URL = "https://feedbacks2.wb.ru/feedbacks/v2/{nm_id}"
def __init__(self):
self.client = httpx.AsyncClient(
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Origin": "https://www.wildberries.ru",
"Referer": "https://www.wildberries.ru/",
}
)
async def get_reviews(self, nm_id: int, take: int = 100) -> list[Review]:
all_reviews = []
skip = 0
while True:
url = self.REVIEWS_URL.format(nm_id=nm_id)
params = {
"immt": nm_id,
"skip": skip,
"take": take,
"order": "dateDesc",
}
resp = await self.client.get(url, params=params)
resp.raise_for_status()
data = resp.json()
feedbacks = data.get("feedbacks", [])
if not feedbacks:
break
for fb in feedbacks:
all_reviews.append(self._normalize(nm_id, fb))
skip += take
await asyncio.sleep(1.0) # ÐгÑаниÑение: не более 1000 оÑзÑвов за ÑеÑÑиÑ
if skip >= 1000:
break
return all_reviews
def _normalize(self, nm_id: int, raw: dict) -> Review:
photos = []
for photo in raw.get("photos", []):
if full_url := photo.get("fullSize"):
photos.append(full_url)
return Review(
external_id=raw.get("id", ""),
product_nm_id=nm_id,
author=raw.get("wbUserDetails", {}).get("name", "ÐокÑпаÑелÑ"),
rating=raw.get("productValuation", 0),
text=raw.get("text", ""),
pros=raw.get("pros"),
cons=raw.get("cons"),
date=raw.get("createdDate", ""),
photos=photos,
helpful_count=raw.get("feedbackValuation", 0),
)
"} פארסינג של ביקורות HTML
לאתרים ללא API, אנו מפרסים HTML באמצעות PHP ו-Symfony Crawler. דוגמה עבור iHerb:
// app/Services/ReviewScraper/HtmlReviewScraper.php
class HtmlReviewScraper
{
public function scrapeIherb(string $productUrl, int $pages = 5): array
{
$reviews = [];
for ($page = 1; $page <= $pages; $page++) {
$html = $this->fetch("{$productUrl}?p={$page}&is=1&s=6");
$crawler = new Crawler($html);
$items = $crawler->filter('[itemprop="review"]');
if (!$items->count()) break;
$items->each(function (Crawler $node) use (&$reviews) {
$reviews[] = [
'external_id' => $node->attr('data-review-id'),
'author' => trim($node->filter('[itemprop="author"]')->text('')),
'rating' => (int) $node->filter('[itemprop="ratingValue"]')->attr('content'),
'date' => $node->filter('[itemprop="datePublished"]')->attr('content'),
'title' => trim($node->filter('[itemprop="name"]')->text('')),
'text' => trim($node->filter('[itemprop="reviewBody"]')->text('')),
'helpful' => (int) $node->filter('.helpful-yes')->text('0'),
'verified' => $node->filter('.verified-buyer')->count() > 0,
];
});
sleep(rand(2, 4));
}
return $reviews;
}
} הסרת כפילויות וייבוא ל-Laravel
לאחר האיסוף, ביקורות עוברות דרך Job עם הסרת כפילויות לפי # scraper/reviews/wildberries.py import httpx import asyncio from dataclasses import dataclass from typing import Optional @dataclass class Review: external_id: str product_nm_id: int author: str rating: int text: str pros: Optional[str] cons: Optional[str] date: str photos: list[str] helpful_count: int class WildberriesReviewScraper: REVIEWS_URL = "https://feedbacks2.wb.ru/feedbacks/v2/{nm_id}" def __init__(self): self.client = httpx.AsyncClient( headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Origin": "https://www.wildberries.ru", "Referer": "https://www.wildberries.ru/", } ) async def get_reviews(self, nm_id: int, take: int = 100) -> list[Review]: all_reviews = [] skip = 0 while True: url = self.REVIEWS_URL.format(nm_id=nm_id) params = { "immt": nm_id, "skip": skip, "take": take, "order": "dateDesc", } resp = await self.client.get(url, params=params) resp.raise_for_status() data = resp.json() feedbacks = data.get("feedbacks", []) if not feedbacks: break for fb in feedbacks: all_reviews.append(self._normalize(nm_id, fb)) skip += take await asyncio.sleep(1.0) # Ограничение: не более 1000 отзывов за сессию if skip >= 1000: break return all_reviews def _normalize(self, nm_id: int, raw: dict) -> Review: photos = [] for photo in raw.get("photos", []): if full_url := photo.get("fullSize"): photos.append(full_url) return Review( external_id=raw.get("id", ""), product_nm_id=nm_id, author=raw.get("wbUserDetails", {}).get("name", "Покупатель"), rating=raw.get("productValuation", 0), text=raw.get("text", ""), pros=raw.get("pros"), cons=raw.get("cons"), date=raw.get("createdDate", ""), photos=photos, helpful_count=raw.get("feedbackValuation", 0), ) ו-// app/Services/ReviewScraper/HtmlReviewScraper.php class HtmlReviewScraper { public function scrapeIherb(string $productUrl, int $pages = 5): array { $reviews = []; for ($page = 1; $page <= $pages; $page++) { $html = $this->fetch("{$productUrl}?p={$page}&is=1&s=6"); $crawler = new Crawler($html); $items = $crawler->filter('[itemprop="review"]'); if (!$items->count()) break; $items->each(function (Crawler $node) use (&$reviews) { $reviews[] = [ 'external_id' => $node->attr('data-review-id'), 'author' => trim($node->filter('[itemprop="author"]')->text('')), 'rating' => (int) $node->filter('[itemprop="ratingValue"]')->attr('content'), 'date' => $node->filter('[itemprop="datePublished"]')->attr('content'), 'title' => trim($node->filter('[itemprop="name"]')->text('')), 'text' => trim($node->filter('[itemprop="reviewBody"]')->text('')), 'helpful' => (int) $node->filter('.helpful-yes')->text('0'), 'verified' => $node->filter('.verified-buyer')->count() > 0, ]; }); sleep(rand(2, 4)); } return $reviews; } } . דוגמה:
// app/Jobs/ImportProductReviews.php
class ImportProductReviews implements ShouldQueue {
public int $tries = 3;
public int $backoff = 120;
public function handle(ReviewImportService $service): void {
$mapping = ProductReviewMapping::where('product_id', $this->productId)
->where('source', $this->source)
->firstOrFail();
$reviews = $this->scrape($mapping->external_id);
$imported = 0;
$skipped = 0;
foreach ($reviews as $reviewData) {
$exists = ProductReview::where([
'source' => $this->source,
'external_id' => $reviewData['external_id'],
])->exists();
if ($exists) {
$skipped++;
continue;
}
$service->import($this->productId, $this->source, $reviewData);
$imported++;
}
Log::info("Reviews imported", [
'product_id' => $this->productId,
'source' => $this->source,
'imported' => $imported,
'skipped' => $skipped,
]);
}
} סינון ומודרציה
מילות עצירה (ספאם, פרסומות, ניבולי פה), ביקורות קצרות מדי (<20 תווים), ואנונימיזציה של מחברים — הכל ניתן להתאמה אישית. להלן דוגמת שירות:
// app/Services/ReviewImportService.php
class ReviewImportService
{
private array $stopWords = ['купите', 'скидка', 'промокод', 'vk.com', 't.me'];
public function import(int $productId, string $source, array $data): ?ProductReview
{
if (mb_strlen($data['text']) < 20) {
return null;
}
foreach ($this->stopWords as $word) {
if (mb_stripos($data['text'], $word) !== false) {
return null;
}
}
return ProductReview::create([
'product_id' => $productId,
'source' => $source,
'external_id' => $data['external_id'],
'author' => $this->anonymizeAuthor($data['author']),
'rating' => max(1, min(5, (int) $data['rating'])),
'text' => $this->sanitize($data['text']),
'pros' => $this->sanitize($data['pros'] ?? ''),
'cons' => $this->sanitize($data['cons'] ?? ''),
'date' => $data['date'],
'is_verified' => $data['verified'] ?? false,
'helpful' => $data['helpful'] ?? 0,
'status' => 'pending',
]);
}
private function anonymizeAuthor(string $name): string
{
$parts = explode(' ', trim($name));
if (count($parts) >= 2) {
return $parts[0] . ' ' . mb_substr($parts[1], 0, 1) . '.';
}
return $name ?: 'Покупатель';
}
private function sanitize(string $text): string
{
return strip_tags(trim($text));
}
} נתונים מובנים לקידום אתרים
לאחר הייבוא, ביקורות מתפרסמות ב-JSON-LD בדף המוצר. זה מגדיל את הסיכוי להופיע ב-rich snippets ומגביר את ה-CTR ב-20-30%. למידע נוסף על נתונים מובנים.
// app/Http/Controllers/ProductController.php
public function show(string $slug): Response
{
$product = Product::withReviews()->findBySlug($slug);
$reviewSchema = $product->reviews->map(fn($r) => [
'@type' => 'Review',
'author' => ['@type' => 'Person', 'name' => $r->author],
'datePublished' => $r->date,
'reviewBody' => $r->text,
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $r->rating,
'bestRating' => 5,
],
]);
$aggregateRating = [
'@type' => 'AggregateRating',
'ratingValue' => round($product->reviews->avg('rating'), 1),
'reviewCount' => $product->reviews->count(),
];
} איך להימנע מחסימות בזמן גירוד?
כדי להימנע מחסימות, אנו משתמשים במאגר של 50+ פרוקסי מגורים, עיכובים אקראיים של 1 עד 4 שניות, וסיבוב User-Agent (Chrome, Firefox, Safari). CAPTCHA נפתרים דרך Anti-Captcha. כל סשן מוגבל ל-1000 ביקורות.
למה אוטומציה של ביקורות משפרת קידום אתרים?
- נתונים מובנים (JSON-LD) עוזרים למנועי חיפוש להציג דירוגים ב-snippets.
- טקסטים של UGC מכילים מילות מפתח ארוכות זנב.
- עדכונים קבועים מאותתים על חנות פעילה.
השוואה: איסוף ידני של 100 ביקורות לוקח שעתיים, הפארסר שלנו — 2 דקות, מהיר פי 60. חיסכון בתקציב על כותבי תוכן ומנחים — עד 70%.
תהליך העבודה
- ניתוח: קביעת פלטפורמות, APIs, מורכבות.
- פיתוח: כתיבת פארסר, מודרציה, הסרת כפילויות.
- בדיקות: הרצה על 1000+ ביקורות, בדיקת באגים.
- פריסה: הגדרת cron, ניטור שגיאות.
- תיעוד: תיאור מחסנית, הוראות הפעלה.
| שלב | משך |
|---|---|
| ניתוח דרישות | 1-2 ימים |
| פיתוח פארסר | 2-3 ימים לכל פלטפורמה |
| בדיקות | 1-2 ימים |
| פריסה ותיעוד | יום אחד |
מחיר התחלתי לפארסר לפלטפורמה אחת הוא $500, עם הנחות לכמויות. אנו מבטיחים ביצועים אמינים, עם ניסיון של למעלה מ-5 שנים ופרקטיקות גירוד מוסמכות. בוט הפארסר לביקורות שלנו מטפל באגרגציה של ביקורות ממספר מקורות, ומבטיח שכל הנתונים מיובאים כראוי.
מה כלול
- פארסר לכל פלטפורמת מסחר (עד 5 פלטפורמות).
- מודרציה (מילות עצירה, אורך, אנונימיזציה).
- הסרת כפילויות (לפי external_id + source).
- עדכונים אוטומטיים בלוח זמנים (יומי או כל 4 שעות).
- נתונים מובנים בדף המוצר.
- רישום והתראות שגיאות.
לוח זמנים: מ-3 עד 5 ימי עסקים לכל פלטפורמה. לפרויקט מורכב (3 פלטפורמות + מודרציה + SEO) — 7-10 ימים. צרו קשר כדי להעריך את הפרויקט שלכם. קבלו ייעוץ לפרויקט שלכם.







