אתה מבזבז ימים באיסוף ידני של 200 כתובות URL של מתחרים, רושם כותרות ותיאורי מטא. חודש לאחר מכן — ריברנדינג, ואתה מתחיל הכל מחדש. קראולר פותר את זה תוך דקות וניתן להפעלה אוטומטית חוזרת. אנחנו צוות עם 7 שנות ניסיון בגרידת אתרים: סיפקנו 15+ פתרונות כאלה לנישות שונות.
בעיה נפוצה אחת היא איסוף לא מלא עקב רינדור JavaScript. אפילו אתר סטטי עשוי להכיל אלמנטים דינמיים שאינם נראים לבקשת HTTP פשוטה. קראולר עם דפדפן headless חושף את המבנה האמיתי, כולל טעינה עצלה. התוצאה היא מפה מלאה של אתר המתחרה: כל כתובות ה-URL, הכותרות, תגי המטא, Schema.org. גישה זו חוסכת עד 20 שעות עבודה בשבוע ונותנת יתרון בניתוח SEO.
אילו בעיות הקראולר פותר עבור מבנה אתר?
נקודת כאב נפוצה — איסוף מבנה לא מלא עקב רינדור JavaScript, קינון URL, או כפילויות canonical. לדוגמה, חנות מסחר אלקטרוני על Vue.js עשויה להציג תוכן זהה בכתובות URL שונות, מה שמעוות את מפת האתר. קראולר עם דפדפן headless מזהה את המבנה האמיתי, כולל טעינות דינמיות.
בעיה נוספת — ניתוח Schema.org. ללא נתונים מובנים, אי אפשר להעריך כיצד המתחרה משתמש ב-rich snippets. הקראולר אוסף JSON-LD ו-Microdata, ומאפשר לשכפל תבניות מוצלחות.
באיזו ארכיטקטורה אנו משתמשים לקרילה?
שתי אפשרויות עבודה: Python + Scrapy/Playwright עבור SPA מורכבים עם טעינה עצלה, Node.js + Puppeteer/Cheerio עבור רוב האתרים הסטנדרטיים. למשימות ללא רינדור JS דינמי, לקוח HTTP עם מפענח HTML מספיק — מהיר פי 5–10, פשוט יותר לפריסה.
| מאפיין | קראולר HTTP | קראולר Headless |
|---|---|---|
| מהירות לעמוד | 0.3–0.8 שניות | 2–5 שניות |
| תמיכה ב-JS | לא | מלאה |
| עומס על השרת | נמוך | בינוני |
| מורכבות פריסה | מינימלית | בינונית |
מימוש Python מינימלי המבוסס על requests + lxml:
import requests from lxml import html from urllib.parse import urljoin, urlparse from collections import deque import time from urllib.robotparser import RobotFileParser class SiteStructureCrawler: def __init__(self, base_url: str, max_depth: int = 4, delay: float = 1.0): self.base_url = base_url self.domain = urlparse(base_url).netloc self.max_depth = max_depth self.delay = delay self.visited: dict[str, dict] = {} self.queue: deque = deque([(base_url, 0)]) # Проверка robots.txt rp = RobotFileParser() rp.set_url(f'{base_url}/robots.txt') rp.read() self.rp = rp def crawl(self): session = requests.Session() session.headers['User-Agent'] = ( 'Mozilla/5.0 (compatible; SiteAnalyzer/1.0; +https://example.com/bot)' ) while self.queue: url, depth = self.queue.popleft() if url in self.visited or depth > self.max_depth: continue if not self.rp.can_fetch('*', url): continue # пропускаем запрещённые пути try: resp = session.get(url, timeout=10, allow_redirects=True) resp.raise_for_status() except requests.RequestException as e: self.visited[url] = {'error': str(e), 'depth': depth} continue doc = html.fromstring(resp.content) doc.make_links_absolute(url) title = doc.findtext('.//title') or '' h1 = [h.text_content().strip() for h in doc.cssselect('h1')] meta_desc_el = doc.cssselect('meta[name="description"]') meta_desc = meta_desc_el[0].get('content', '') if meta_desc_el else '' canonical_el = doc.cssselect('link[rel="canonical"]') canonical = canonical_el[0].get('href', '') if canonical_el else '' noindex = bool(doc.cssselect('meta[name="robots"][content*="noindex"]')) # Сбор Schema.org и заголовков schemas = [] for script in doc.cssselect('script[type="application/ld+json"]'): try: import json data = json.loads(script.text_content()) schemas.append(data) except json.JSONDecodeError: pass headings = [] for tag in ['h1', 'h2', 'h3', 'h4']: for el in doc.cssselect(tag): headings.append({'tag': tag, 'text': el.text_content().strip()}) links = [] for a in doc.cssselect('a[href]'): href = a.get('href', '').strip() parsed = urlparse(href) if parsed.netloc == self.domain and href not in self.visited: links.append(href) if depth + 1 <= self.max_depth: self.queue.append((href, depth + 1)) self.visited[url] = { 'depth': depth, 'status': resp.status_code, 'title': title.strip(), 'h1': h1, 'meta_description': meta_desc, 'canonical': canonical, 'noindex': noindex, 'internal_links': links, 'content_type': resp.headers.get('Content-Type', ''), 'schema': schemas, 'headings': headings, } time.sleep(self.delay) return self.visited עבודה עם רינדור JavaScript
אם אתר המתחרה הוא SPA (React/Vue/Angular) או משתמש בטעינה עצלה לתוכן הראשי, קראולר HTTP פשוט מחזיר עמודים ריקים. כאן יש צורך בדפדפן headless:
from playwright.sync_api import sync_playwright def crawl_spa_page(url: str) -> dict: with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(url, wait_until='networkidle', timeout=30000) title = page.title() h1_elements = page.query_selector_all('h1') h1_texts = [el.inner_text() for el in h1_elements] # Сбор всех ссылок после рендеринга links = page.eval_on_selector_all( 'a[href]', 'els => els.map(e => e.href)' ) browser.close() return {'title': title, 'h1': h1_texts, 'links': links} Playwright מוסיף כ-2–5 שניות לעמוד לעומת 0.3–0.8 שניות ל-HTTP פשוט. בקרילה של 500+ עמודים זה מורגש — משתמשים בו רק במקום שצריך.
איך לאוטמט קרילה סדירה?
איסוף נתונים חד-פעמי מתיישן במהירות. מתחרים משנים מבנה, מוסיפים קטעים, משנים פורמט כותרות. כדאי להגדיר ריצות אוטומטיות כל שבוע/חודש ולהשוות תוצאות:
def diff_structures(old: dict, new: dict) -> dict: added = {url: data for url, data in new.items() if url not in old} removed = {url: data for url, data in old.items() if url not in new} changed = {} for url in old: if url in new: if old[url].get('title') != new[url].get('title'): changed[url] = { 'old_title': old[url].get('title'), 'new_title': new[url].get('title'), } return {'added': added, 'removed': removed, 'changed': changed} למה חשוב לאסוף Schema.org?
נתונים מובנים הם אינדיקטור ישיר לכמה המתחרה משקיע ב-SEO. קיום Article, Product, BreadcrumbList, FAQPage נותן יתרון בתוצאות החיפוש. הקראולר לוכד את כל סוגי הסימון, ומאפשר לאמץ סכמות מוצלחות.
התקנה והפעלת הקראולר
כדי להתחיל איסוף, בצע את השלבים הבאים:
- שכפל את המאגר עם הקראולר המוכן.
- התקן תלויות:
import requests from lxml import html from urllib.parse import urljoin, urlparse from collections import deque import time from urllib.robotparser import RobotFileParser class SiteStructureCrawler: def __init__(self, base_url: str, max_depth: int = 4, delay: float = 1.0): self.base_url = base_url self.domain = urlparse(base_url).netloc self.max_depth = max_depth self.delay = delay self.visited: dict[str, dict] = {} self.queue: deque = deque([(base_url, 0)]) # Проверка robots.txt rp = RobotFileParser() rp.set_url(f'{base_url}/robots.txt') rp.read() self.rp = rp def crawl(self): session = requests.Session() session.headers['User-Agent'] = ( 'Mozilla/5.0 (compatible; SiteAnalyzer/1.0; +https://example.com/bot)' ) while self.queue: url, depth = self.queue.popleft() if url in self.visited or depth > self.max_depth: continue if not self.rp.can_fetch('*', url): continue # пропускаем запрещённые пути try: resp = session.get(url, timeout=10, allow_redirects=True) resp.raise_for_status() except requests.RequestException as e: self.visited[url] = {'error': str(e), 'depth': depth} continue doc = html.fromstring(resp.content) doc.make_links_absolute(url) title = doc.findtext('.//title') or '' h1 = [h.text_content().strip() for h in doc.cssselect('h1')] meta_desc_el = doc.cssselect('meta[name="description"]') meta_desc = meta_desc_el[0].get('content', '') if meta_desc_el else '' canonical_el = doc.cssselect('link[rel="canonical"]') canonical = canonical_el[0].get('href', '') if canonical_el else '' noindex = bool(doc.cssselect('meta[name="robots"][content*="noindex"]')) # Сбор Schema.org и заголовков schemas = [] for script in doc.cssselect('script[type="application/ld+json"]'): try: import json data = json.loads(script.text_content()) schemas.append(data) except json.JSONDecodeError: pass headings = [] for tag in ['h1', 'h2', 'h3', 'h4']: for el in doc.cssselect(tag): headings.append({'tag': tag, 'text': el.text_content().strip()}) links = [] for a in doc.cssselect('a[href]'): href = a.get('href', '').strip() parsed = urlparse(href) if parsed.netloc == self.domain and href not in self.visited: links.append(href) if depth + 1 <= self.max_depth: self.queue.append((href, depth + 1)) self.visited[url] = { 'depth': depth, 'status': resp.status_code, 'title': title.strip(), 'h1': h1, 'meta_description': meta_desc, 'canonical': canonical, 'noindex': noindex, 'internal_links': links, 'content_type': resp.headers.get('Content-Type', ''), 'schema': schemas, 'headings': headings, } time.sleep(self.delay) return self.visited(אוfrom playwright.sync_api import sync_playwright def crawl_spa_page(url: str) -> dict: with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto(url, wait_until='networkidle', timeout=30000) title = page.title() h1_elements = page.query_selector_all('h1') h1_texts = [el.inner_text() for el in h1_elements] # Сбор всех ссылок после рендеринга links = page.eval_on_selector_all( 'a[href]', 'els => els.map(e => e.href)' ) browser.close() return {'title': title, 'h1': h1_texts, 'links': links}עבור SPA). - ציין את כתובת ה-URL ההתחלתית ועומק קרילה מקסימלי.
- הרץ את הסקריפט:
def diff_structures(old: dict, new: dict) -> dict: added = {url: data for url, data in new.items() if url not in old} removed = {url: data for url, data in old.items() if url not in new} changed = {} for url in old: if url in new: if old[url].get('title') != new[url].get('title'): changed[url] = { 'old_title': old[url].get('title'), 'new_title': new[url].get('title'), } return {'added': added, 'removed': removed, 'changed': changed}. - לאחר הסיום, קבל דוח — קובץ JSON או CSV.
תוצאות ואוטומציה
ניתן לייצא את המבנה שנאסף במספר פורמטים בהתאם למשימה:
| פורמט | מתי להשתמש | יתרונות |
|---|---|---|
| JSON | עיבוד תוכנתי, אינטגרציית API | נתונים מלאים, קינון |
| CSV | ניתוח ב-Excel/Google Sheets | פשטות, מיון |
| SQLite | קרילה סדירה, היסטוריית שינויים | שאילתות מהירות, תמיכה ב-diff |
import json import csv # JSON — для программной обработки with open('competitor_structure.json', 'w', encoding='utf-8') as f: json.dump(crawler.visited, f, ensure_ascii=False, indent=2) # CSV — для анализа в Excel/Google Sheets fieldnames = ['url', 'depth', 'status', 'title', 'meta_description', 'h1', 'noindex', 'canonical'] with open('competitor_structure.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore') writer.writeheader() for url, data in crawler.visited.items(): row = {'url': url, **data} if isinstance(row.get('h1'), list): row['h1'] = ' | '.join(row['h1']) writer.writerow(row) מה כלול בעבודה?
- פיתוח קראולר מותאם לפרטי הנישה שלך: בחירת מחסנית (Python או Node.js), הגדרת עומק, השהיות, robots.txt.
- אינטגרציה עם דפדפן headless לאתרי SPA.
- איסוף כל כתובות ה-URL, כותרות H1-H6, תגי מטא, canonical, noindex, Schema.org.
- ייצוא ל-JSON, CSV או SQLite לפי בחירתך.
- אוטומציה של ריצות דרך cron/Airflow עם היסטוריית שינויים.
- תיעוד תפעולי וייעוץ בניתוח תוצאות.
לוחות זמנים ואחריות
קראולר בסיסי (HTTP, ללא SPA) עם ייצוא CSV/JSON — בין יום ל-2 ימי עסקים. עם תמיכה ברינדור JavaScript, איסוף Schema.org, השוואת diff ואחסון SQLite — בין 3 ל-4 ימים. אינטגרציה עם מתזמן והתראות שינוי — תוספת של יום עד יומיים.
אנו מבטיחים שהקראולר מכבד pip install requests lxml (Robots.txt) — בדיקה חובה לפני כל בקשה. השהיה בין בקשות (מינימום שנייה אחת) מונעת חסימת IP. לקרילה סדירה, אנו מסובבים User-Agent ופרוקסי.
טעויות אופייניות בפיתוח קראולרים:
- התעלמות מ-robots.txt — סיכון לחסימת IP.
- קרילה מהירה מדי (השהיה < 1 שנייה) — חסימת שרת.
- דילוג על בדיקת canonical — כפילויות מנפחות את המבנה.
- ללא טיפול בקישורים מעגליים — קרילה אינסופית.
- אי התחשבות בפאג'ינציה (page/2, page/3) — איסוף לא מלא.
צור קשר לייעוץ. הזמן קראולר מותאם אישית לנישה שלך. קבל ניתוח חינם של המתחרים שלך והמלצות קרילה.







