import requests from bs4 import BeautifulSoup import time import sys import json import sqlite3 import argparse import glob import html from datetime import datetime from urllib.parse import urljoin, urlparse from urllib.robotparser import RobotFileParser import threading import queue import os if os.name == 'nt': os.system('color') GOOGLEBOT_UA = "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" class TelegramNotifier: def __init__(self, token, chat_id_info, chat_id_errors): self.token = token self.chat_id_info = chat_id_info self.chat_id_errors = chat_id_errors or chat_id_info self.enabled = True if token and chat_id_info else False self.critical_errors = [] self.schema_errors = [] self.translation_issues = [] self.lock = threading.Lock() def send(self, message, target='info'): if not self.enabled: return cid = self.chat_id_info if target == 'info' else self.chat_id_errors url = f"https://api.telegram.org/bot{self.token}/sendMessage" try: r = requests.post(url, json={"chat_id": cid, "text": message, "parse_mode": "HTML"}, timeout=15) if r.status_code != 200: print(f"\n[!] Telegram Error ({target}): {r.text}") except Exception as e: print(f"\n[!] Connection Error (Telegram): {e}") def add_critical(self, url, msg): with self.lock: if len(self.critical_errors) < 30: self.critical_errors.append((url, msg)) def add_schema(self, url, count): with self.lock: if len(self.schema_errors) < 15: self.schema_errors.append((url, count)) def add_translation_issue(self, sku, lang1, lang2, field): with self.lock: self.translation_issues.append(f"SKU {sku}: {field} identyczny w {lang1} i {lang2}") def get_prev_404_count(self, current_db): # Szukamy baz danych w podkatalogu scans, sortujemy po czasie modyfikacji dbs = glob.glob("scans/crawler_v*.db") dbs.sort(key=os.path.getmtime, reverse=True) prev_db = None for d in dbs: if os.path.basename(d) != os.path.basename(current_db): prev_db = d break if not prev_db: return None try: conn = sqlite3.connect(prev_db) count = conn.execute("SELECT COUNT(*) FROM pages WHERE status = 404").fetchone()[0] conn.close() return count except: return None def save_translation_report(self, db_file, start_url): """Czyta pełną listę błędów tłumaczeń z DB i zapisuje do stałego pliku scans/report_translations.md.""" domain = urlparse(start_url).netloc md_file = os.path.join("scans", "report_translations.md") try: conn = sqlite3.connect(db_file) rows = conn.execute( 'SELECT sku, field, lang1, lang2 FROM translation_audit ORDER BY sku, field' ).fetchall() conn.close() except Exception as e: print(f"\n[!] Nie udało się odczytać błędów tłumaczeń z DB: {e}") return None if not rows: return None total = len(rows) try: with open(md_file, 'w', encoding='utf-8') as f: f.write(f"# Błędy tłumaczeń – {domain}\n") f.write(f"Wygenerowano: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} \n") f.write(f"Łącznie błędów: **{total}**\n\n") f.write("---\n\n") f.write("## Kontekst dla Agenta AI\n\n") f.write( "Twoim zadaniem jest weryfikacja i naprawa poniższych błędów tłumaczeń w sklepie.\n" "Znasz strukturę plików, masz dostęp do bazy danych sklepu oraz wiesz jak działają " "wszystkie mechanizmy cache.\n" "WAŻNE: Podane błędy zostały wykryte przez crawler, który analizował wyrenderowany kod HTML " "stron sklepu. Crawler nie sprawdzał bazy danych – może być tak, że w bazie dane są poprawne, " "a problem leży po stronie modułów lub cache.\n\n" ) f.write("## BŁĘDY TŁUMACZEŃ (raw list)\n\n") for sku, field, lang1, lang2 in rows: f.write(f"- SKU {sku}: {field} identyczny w {lang1} i {lang2}\n") return md_file except Exception as e: print(f"\n[!] Nie udało się zapisać raportu tłumaczeń: {e}") return None def send_final_report(self, start_url, total, errors, db_file, search_results=-1): if not self.enabled: print("\n[!] Powiadomienia Telegram są wyłączone (brak konfiguracji).") return # Analiza 404 i innych błędów current_404 = 0 schema_errs = 0 transl_errs = 0 try: conn = sqlite3.connect(db_file) current_404 = conn.execute("SELECT COUNT(*) FROM pages WHERE status = 404").fetchone()[0] schema_errs = conn.execute("SELECT COUNT(*) FROM pages WHERE schema_critical > 0").fetchone()[0] transl_errs = conn.execute("SELECT COUNT(*) FROM translation_audit").fetchone()[0] conn.close() except: pass prev_404 = self.get_prev_404_count(db_file) regression_str = "" if prev_404 is not None: diff = current_404 - prev_404 if diff > 0: regression_str = f" (+{diff} NOWE! ⚠️)" elif diff < 0: regression_str = f" ({diff} naprawione)" else: regression_str = " (bez zmian)" # 1. RAPORT INFO domain = html.escape(urlparse(start_url).netloc) total_icon = "✅" http_icon = "✅" if errors == 0 else "❌" err404_icon = "✅" if current_404 == 0 else "❌" schema_icon = "✅" if schema_errs == 0 else "❌" transl_icon = "✅" if transl_errs == 0 else "❌" search_icon = "✅" if search_results > 0 else "❌" schema_text = "poprawne" if schema_errs == 0 else f"{schema_errs} błędów" transl_text = "poprawne" if transl_errs == 0 else f"{transl_errs} błędów" search_text = f"{search_results}" if search_results >= 0 else "BŁĄD" info_msg = f"🏁 AUDYT ZAKOŃCZONY: {domain}\n\n" info_msg += f"{total_icon} Przeskanowano: {total}\n" info_msg += f"{http_icon} Błędy HTTP: {errors}\n" info_msg += f"{err404_icon} Błędy 404: {current_404}{regression_str}\n" info_msg += f"{schema_icon} Dane strukturalne: {schema_text}\n" info_msg += f"{transl_icon} Tłumaczenia: {transl_text}\n" info_msg += f"{search_icon} Wyszukiwarka: {search_text}\n\n" # Zapisz pełny raport tłumaczeń do pliku MD przed wysłaniem transl_md_file = self.save_translation_report(db_file, start_url) if self.critical_errors or self.schema_errors or self.translation_issues: info_msg += f"🚨 Wykryto błędy. Szczegóły na kanale ERRORS." else: info_msg += f"✅ Brak krytycznych błędów." if transl_md_file: info_msg += f"\n\n📄 Pełna lista błędów tłumaczeń: {html.escape(os.path.basename(transl_md_file))}" self.send(info_msg, target='info') # 2. RAPORT ERRORS if self.critical_errors or self.schema_errors or self.translation_issues: err_msg = f"🚨 BŁĘDY: {domain}\n\n" prompt_text = ( "Twoim zadaniem jest weryfikacja poniższych błędów na sklepie i przygotowanie planu naprawy. " "Znasz strukturę plików, masz dostęp do bazy danych sklepu oraz wiesz jak działają wszystkie mechanizmy cache. " "WAŻNE: Podane błędy zostały wykryte przez crawler, który analizował wyrenderowany kod HTML stron sklepu. " "Crawler nie sprawdzał bazy danych – może być tak, że w bazie dane są w pełni poprawne, " "a problem leży po stronie modułów (np. wstrzykujących dane strukturalne do źródła strony).\n" "Oto zestawienie błędów do przeanalizowania:\n\n" ) if self.critical_errors: err_msg += f"❌ KRYTYCZNE:\n" prompt_text += "BŁĘDY KRYTYCZNE:\n" for url, err in self.critical_errors[:15]: safe_url = html.escape(url) err_msg += f"• {err}: {safe_url}\n" prompt_text += f"- {err}: {url}\n" err_msg += "\n" prompt_text += "\n" if self.schema_errors: err_msg += f"🛠 SCHEMA.ORG:\n" prompt_text += "BŁĘDY SCHEMA.ORG:\n" for url, count in self.schema_errors[:10]: safe_url = html.escape(url) err_msg += f"• Brak {count} pól: {safe_url}\n" prompt_text += f"- Brak {count} pól: {url}\n" err_msg += "\n" prompt_text += "\n" if self.translation_issues: total_transl = len(self.translation_issues) shown = min(15, total_transl) err_msg += f"🌐 TŁUMACZENIA ({total_transl} błędów):\n" prompt_text += f"BŁĘDY TŁUMACZEŃ ({total_transl} łącznie):\n" for issue in self.translation_issues[:shown]: err_msg += f"• {html.escape(issue)}\n" prompt_text += f"- {issue}\n" if total_transl > shown: err_msg += f"... i {total_transl - shown} więcej. Pełna lista w pliku MD.\n" prompt_text += f"... i {total_transl - shown} więcej błędów – pełna lista w pliku _translation_errors.md\n" err_msg += "\n" prompt_text += "\n" err_msg += f"🤖 Gotowy prompt dla Agenta AI:\n" err_msg += f"
{html.escape(prompt_text.strip())}
" self.send(err_msg, target='errors') def crawler(start_url, db_file, max_threads, tg_notifier): parsed_start = urlparse(start_url) base_url = f"{parsed_start.scheme}://{parsed_start.netloc}" base_domain = parsed_start.netloc conn = sqlite3.connect(db_file, check_same_thread=False) cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, source_url TEXT, status INTEGER, total_time REAL, ttfb REAL, google_access TEXT, index_status TEXT, schema_critical INTEGER DEFAULT 0, schema_warnings INTEGER DEFAULT 0, images_no_alt INTEGER DEFAULT 0, images_no_webp INTEGER DEFAULT 0, title TEXT, meta_desc TEXT, canonical TEXT, lang TEXT, timestamp DATETIME)''') cursor.execute('''CREATE TABLE IF NOT EXISTS structured_data (id INTEGER PRIMARY KEY AUTOINCREMENT, page_id INTEGER, schema_type TEXT, full_json TEXT, sku TEXT, FOREIGN KEY(page_id) REFERENCES pages(id))''') cursor.execute('''CREATE TABLE IF NOT EXISTS translation_audit (id INTEGER PRIMARY KEY AUTOINCREMENT, sku TEXT, field TEXT, lang1 TEXT, lang2 TEXT, content TEXT)''') cursor.execute('''CREATE TABLE IF NOT EXISTS images_audit (id INTEGER PRIMARY KEY AUTOINCREMENT, page_id INTEGER, img_url TEXT, alt TEXT, is_modern INTEGER, has_modern_source INTEGER, FOREIGN KEY(page_id) REFERENCES pages(id))''') conn.commit() db_queue = queue.Queue() def db_worker(): db_conn = sqlite3.connect(db_file) db_cursor = db_conn.cursor() while True: item = db_queue.get() if item is None: break try: p = item['page'] db_cursor.execute('''INSERT OR REPLACE INTO pages (url, source_url, status, total_time, ttfb, google_access, index_status, schema_critical, schema_warnings, images_no_alt, images_no_webp, title, meta_desc, canonical, lang, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (p['url'], p['source'], p['status'], p['time'], p['ttfb'], p['access'], p['idx'], p['s_crit'], p['s_warn'], p.get('images_no_alt',0), p.get('images_no_webp',0), p.get('title',''), p.get('meta_desc',''), p.get('canonical',''), p['lang'], p['ts'])) page_id = db_cursor.lastrowid for s in item['schemas']: db_cursor.execute('INSERT INTO structured_data (page_id, schema_type, full_json, sku) VALUES (?, ?, ?, ?)', (page_id, s['type'], s['json'], s.get('sku'))) if 'images' in item: for img in item['images']: db_cursor.execute('INSERT INTO images_audit (page_id, img_url, alt, is_modern, has_modern_source) VALUES (?, ?, ?, ?, ?)', (page_id, img['img_url'], img['alt'], img['is_modern'], img['has_modern_source'])) db_conn.commit() except: pass finally: db_queue.task_done() db_conn.close() db_thread = threading.Thread(target=db_worker) db_thread.start() rp = RobotFileParser() try: rp.set_url(urljoin(base_url, "robots.txt")); rp.read() except: pass visited, crawled_count, error_count = {start_url}, 0, 0 total_response_time = 0.0 visited_lock, stats_lock = threading.Lock(), threading.Lock() url_queue = queue.Queue() url_queue.put((start_url, "Start")) stop_event = threading.Event() session = requests.Session() session.headers.update({'User-Agent': GOOGLEBOT_UA}) def analyze_schema(soup): scripts = soup.find_all('script', type='application/ld+json') results, crit, warn = [], 0, 0 def get_val(obj, path): curr = obj for p in path.split('.'): if isinstance(curr, dict) and p in curr: curr = curr[p] else: return None return curr for script in scripts: try: data = json.loads(script.string) objs = data if isinstance(data, list) else [data] for obj in objs: if not isinstance(obj, dict): continue sku = get_val(obj, 'sku') or get_val(obj, 'mpn') if 'Product' in str(obj.get('@type', '')): if not get_val(obj, 'name') or not get_val(obj, 'image') or not get_val(obj, 'offers.price'): crit += 1 results.append({'type': str(obj.get('@type', 'Unknown')), 'json': json.dumps(obj, ensure_ascii=False), 'sku': str(sku) if sku else None}) except: pass return results, crit, warn def analyze_images(soup, url): images_data = [] no_alt, no_webp = 0, 0 for img in soup.find_all('img'): src = img.get('src') or img.get('data-src') or '' if not src or src.startswith('data:image'): continue alt = img.get('alt', '').strip() if img.get('alt') is not None else '' alt_text = alt if alt else '[BRAK]' is_modern = src.lower().endswith(('.webp', '.avif', '.svg')) parent = img.find_parent('picture') has_modern_source = False if parent: for source in parent.find_all('source'): srcs = source.get('srcset', '') typ = source.get('type', '') if 'webp' in srcs.lower() or 'avif' in srcs.lower() or 'webp' in typ or 'avif' in typ: has_modern_source = True break if not has_modern_source: srcset = img.get('srcset', '') if 'webp' in srcset.lower() or 'avif' in srcset.lower(): has_modern_source = True images_data.append({ 'img_url': urljoin(url, src), 'alt': alt_text, 'is_modern': int(is_modern), 'has_modern_source': int(has_modern_source) }) if alt_text == '[BRAK]': no_alt += 1 if not is_modern and not has_modern_source: no_webp += 1 return images_data, no_alt, no_webp def process_url(url, source): nonlocal crawled_count, error_count, total_response_time if not rp.can_fetch("Googlebot", url): tg_notifier.add_critical(url, "ROBOTS.TXT BLOCK") db_queue.put({'page': {'url': url, 'source': source, 'status': 0, 'time': 0, 'ttfb': 0, 'access': 'Blocked', 'idx': '-', 's_crit': 0, 's_warn': 0, 'images_no_alt': 0, 'images_no_webp': 0, 'title': '', 'meta_desc': '', 'canonical': '', 'lang': '?', 'ts': datetime.now().isoformat()}, 'schemas': [], 'images': []}) return try: start_t = time.time() resp = session.get(url, timeout=10, stream=True) ttfb = round(time.time() - start_t, 4) soup = BeautifulSoup(resp.text, 'lxml') total_t = round(time.time() - start_t, 4) lang = soup.find('html').get('lang', 'unknown') if soup.find('html') else 'unknown' schemas, s_crit, s_warn = analyze_schema(soup) idx = "Indexable" if 'noindex' in resp.headers.get('X-Robots-Tag', '').lower(): idx = "Noindex" elif soup.find('meta', attrs={'name': ['robots', 'googlebot'], 'content': lambda x: x and 'noindex' in x.lower()}): idx = "Noindex" title_tag = soup.find('title') title = title_tag.text.strip() if title_tag else '' meta_desc_tag = soup.find('meta', attrs={'name': 'description'}) meta_desc = meta_desc_tag.get('content', '').strip() if meta_desc_tag else '' canonical_tag = soup.find('link', rel='canonical') canonical = canonical_tag.get('href', '').strip() if canonical_tag else '' images_data, no_alt, no_webp = analyze_images(soup, url) if resp.status_code >= 500: tg_notifier.add_critical(url, f"ERR {resp.status_code}") if resp.status_code == 404: tg_notifier.add_critical(url, "404") if s_crit > 0: tg_notifier.add_schema(url, s_crit) db_queue.put({'page': {'url': url, 'source': source, 'status': resp.status_code, 'time': total_t, 'ttfb': ttfb, 'access': 'Allowed', 'idx': idx, 's_crit': s_crit, 's_warn': s_warn, 'images_no_alt': no_alt, 'images_no_webp': no_webp, 'title': title, 'meta_desc': meta_desc, 'canonical': canonical, 'lang': lang, 'ts': datetime.now().isoformat()}, 'schemas': schemas, 'images': images_data}) with stats_lock: crawled_count += 1 total_response_time += total_t if resp.status_code != 200: error_count += 1 if resp.status_code == 200 and 'text/html' in resp.headers.get('Content-Type', ''): for link in soup.find_all('a', href=True): full = urljoin(url, link['href']) parsed = urlparse(full) if parsed.netloc == base_domain: clean = parsed._replace(query='', fragment='').geturl() with visited_lock: if clean not in visited: visited.add(clean); url_queue.put((clean, url)) except: with stats_lock: error_count += 1 def worker(): while not stop_event.is_set(): try: u, s = url_queue.get(timeout=0.5); process_url(u, s); url_queue.task_done() except queue.Empty: continue threads = [threading.Thread(target=worker, daemon=True) for _ in range(max_threads)] for t in threads: t.start() try: while not stop_event.is_set() and url_queue.unfinished_tasks > 0: with stats_lock: cc = crawled_count err = error_count avg = round(total_response_time / cc, 3) if cc > 0 else 0 q_size = url_queue.unfinished_tasks print(f"\r[AUDYT] Skanowanie: {cc} | Błędy: {err} | Kolejka: {q_size} | Średni czas: {avg}s ", end="") time.sleep(0.5) except KeyboardInterrupt: print("\n[!] Przerwano (Ctrl+C). Trwa bezpieczne zamykanie...") stop_event.set() while not url_queue.empty(): try: url_queue.get_nowait(); url_queue.task_done() except queue.Empty: break if not stop_event.is_set(): url_queue.join() else: # Czekamy krótką chwilę, by wątki url_queue zdążyły zobaczyć stop_event time.sleep(1) print("\n[*] Zapisywanie bazy danych, proszę czekać...") db_queue.put(None) db_thread.join() # AUDYT WIELOJĘZYCZNOŚCI — z filtrowaniem false-positive import re # --- Reguły false-positive --- # 1. Grupy blisko spokrewnionych języków (identyczne nazwy to norma) _RELATED_LANG_GROUPS = [ frozenset(['cs-CZ', 'sk-SK']), # czesko-słowacka frozenset(['no-NO', 'nn-NO']), # norweska ] def _are_related_languages(l1, l2): pair = frozenset([l1, l2]) return any(pair <= group for group in _RELATED_LANG_GROUPS) # 2. Nazwy ras psów — terminy międzynarodowe (case-insensitive, startswith) _BREED_NAMES = [ 'border collie', 'bull terrier', 'colie', 'collie', 'boxer', 'chihuahua', 'spaniel', 'bulldog', 'labrador', 'husky', 'dalmatian', 'poodle', 'beagle', 'dachshund', 'samoyed', 'samojed', ] # Sortujemy od najdłuższych, żeby "border collie" matchowało przed "collie" _BREED_NAMES.sort(key=len, reverse=True) # 3. Anglicyzmy / terminy techniczne (po odjęciu nazwy rasy i separatora) _ANGLICISM_PATTERNS = [ re.compile(r'^(crocs\s+pin|pin\s+crocs|pin\s+badge)$', re.IGNORECASE), re.compile(r'^set\s+\S+', re.IGNORECASE), # "Set 10x", "Set of" re.compile(r'^sada\s+\S+', re.IGNORECASE), # "Sada 10x" (CS/SK) ] def _is_translation_false_positive(name, l1, l2): """Zwraca True jeśli identyczna nazwa w dwóch językach to POPRAWNY przypadek.""" if not name: return False # Reguła 1: Blisko spokrewnione języki if _are_related_languages(l1, l2): return True name_lower = name.strip().lower() # Reguła 2+3: Nazwa rasy + anglicyzm remainder = name_lower for breed in _BREED_NAMES: if name_lower.startswith(breed): # Wycinamy rasę i ewentualny separator (" - ", " – ", " — ") remainder = name_lower[len(breed):].strip() remainder = re.sub(r'^[-–—]\s*', '', remainder).strip() break # Jeśli cała nazwa to rasa (bez reszty) — to też false-positive if not remainder and name_lower != remainder: return True # Sprawdź czy reszta pasuje do anglicyzmu if remainder != name_lower: # rasa została znaleziona for pattern in _ANGLICISM_PATTERNS: if pattern.match(remainder): return True # Sprawdź też bez rasy — np. "Set 10x Fluonose Nano+" for pattern in _ANGLICISM_PATTERNS: if pattern.match(name_lower): return True return False # --- Właściwy audyt --- cursor.execute('SELECT sku, lang, full_json FROM structured_data JOIN pages ON structured_data.page_id = pages.id WHERE sku IS NOT NULL AND sku != "None"') sku_map = {} for sku, lang, fjson in cursor.fetchall(): if sku not in sku_map: sku_map[sku] = {} data = json.loads(fjson) sku_map[sku][lang] = {'name': data.get('name', ''), 'description': data.get('description', '')} fp_skipped = 0 for sku, langs in sku_map.items(): lang_list = list(langs.keys()) if len(lang_list) > 1: for i in range(len(lang_list)): for j in range(i + 1, len(lang_list)): l1, l2 = lang_list[i], lang_list[j] if langs[l1]['name'] == langs[l2]['name']: if _is_translation_false_positive(langs[l1]['name'], l1, l2): fp_skipped += 1 continue cursor.execute('INSERT INTO translation_audit (sku, field, lang1, lang2, content) VALUES (?, ?, ?, ?, ?)', (sku, 'name', l1, l2, langs[l1]['name'])) tg_notifier.add_translation_issue(sku, l1, l2, 'name') if fp_skipped: print(f"\n[*] Tłumaczenia: pominięto {fp_skipped} false-positive (rasy psów, anglicyzmy, blisko spokrewnione języki)") conn.commit(); conn.close() # TEST WYSZUKIWARKI search_count = -1 try: print("\n[*] Przeprowadzanie testu wyszukiwarki (szukana fraza: karuzela)...") search_url = "https://fluo.dog/szukaj?controller=search&s=karuzela" resp_search = session.get(search_url, timeout=15) if resp_search.status_code == 200: soup_search = BeautifulSoup(resp_search.text, 'lxml') products = soup_search.find_all('article', class_='product-miniature') search_count = len(products) if search_count == 0: tg_notifier.add_critical(search_url, "TEST WYSZUKIWARKI: Brak wyników (0) dla 'karuzela'!") print("[!] Test wyszukiwarki NIEPOWODZENIE: 0 wyników.") else: print(f"[*] Test wyszukiwarki OK: znaleziono {search_count} produktów.") else: tg_notifier.add_critical(search_url, f"TEST WYSZUKIWARKI: Błąd HTTP {resp_search.status_code}") print(f"[!] Test wyszukiwarki BŁĄD HTTP: {resp_search.status_code}") except Exception as e: tg_notifier.add_critical("https://fluo.dog/szukaj?controller=search&s=karuzela", f"TEST WYSZUKIWARKI: Błąd połączenia") print(f"[!] Test wyszukiwarki BŁĄD: {e}") tg_notifier.send_final_report(start_url, crawled_count, error_count, db_file, search_results=search_count) export_reports(db_file, start_url) def export_reports(db_file, start_url): """Eksportuje wszystkie raporty błędów do stałych plików w scans/ (nadpisuje przy każdym skanie).""" domain = urlparse(start_url).netloc ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') print("[*] Eksportowanie raportów do plików...") import csv try: conn = sqlite3.connect(db_file) # --- 1. HTTP ERRORS -> scans/report_http_errors.csv --- rows_http = conn.execute( 'SELECT url, status, title, source_url, timestamp FROM pages ' 'WHERE status != 200 AND status != 0 ORDER BY status, url' ).fetchall() csv_file = os.path.join("scans", "report_http_errors.csv") with open(csv_file, 'w', encoding='utf-8', newline='') as f: writer = csv.writer(f) writer.writerow(['status', 'url', 'title', 'source_url', 'timestamp']) for url, status, title, source_url, timestamp in rows_http: writer.writerow([status, url, title or '', source_url or '', timestamp or '']) print(f" -> {csv_file} ({len(rows_http)} wpisów)") # --- 2. SCHEMA ERRORS -> scans/report_schema_errors.md --- rows_schema = conn.execute( 'SELECT p.url, p.schema_critical, p.title, sd.sku ' 'FROM pages p ' 'LEFT JOIN structured_data sd ON sd.page_id = p.id AND sd.sku IS NOT NULL ' 'WHERE p.schema_critical > 0 ' 'ORDER BY p.schema_critical DESC, p.url' ).fetchall() md_schema = os.path.join("scans", "report_schema_errors.md") with open(md_schema, 'w', encoding='utf-8') as f: f.write(f"# Błędy Schema.org – {domain}\n") f.write(f"Wygenerowano: {ts} \n") f.write(f"Łącznie stron z błędami: **{len(rows_schema)}**\n\n") f.write("---\n\n") f.write("## Kontekst dla Agenta AI\n\n") f.write( "Twoim zadaniem jest naprawa poniższych błędów danych strukturalnych schema.org.\n" "WAŻNE: Błędy wykryto analizując wyrenderowany HTML – sprawdź czy problem leży " "w samym module schema.org, cache lub konfiguracji produktu.\n\n" ) f.write("## STRONY Z BŁĘDAMI SCHEMA.ORG\n\n") for url, crit, title, sku in rows_schema: sku_str = f" (SKU: {sku})" if sku else "" f.write(f"- [{title or url}]({url}){sku_str} – {crit} brakujące pole(a)\n") print(f" -> {md_schema} ({len(rows_schema)} wpisów)") # --- 3. SUMMARY -> scans/report_summary.md --- total_pages = conn.execute('SELECT COUNT(*) FROM pages').fetchone()[0] total_200 = conn.execute('SELECT COUNT(*) FROM pages WHERE status = 200').fetchone()[0] total_404 = conn.execute('SELECT COUNT(*) FROM pages WHERE status = 404').fetchone()[0] total_5xx = conn.execute('SELECT COUNT(*) FROM pages WHERE status >= 500').fetchone()[0] total_noindex = conn.execute('SELECT COUNT(*) FROM pages WHERE index_status = "Noindex"').fetchone()[0] total_schema = conn.execute('SELECT COUNT(*) FROM pages WHERE schema_critical > 0').fetchone()[0] total_transl = conn.execute('SELECT COUNT(*) FROM translation_audit').fetchone()[0] total_noalt = conn.execute('SELECT SUM(images_no_alt) FROM pages').fetchone()[0] or 0 total_nowebp = conn.execute('SELECT SUM(images_no_webp) FROM pages').fetchone()[0] or 0 avg_time = conn.execute('SELECT ROUND(AVG(total_time), 3) FROM pages WHERE status = 200').fetchone()[0] or 0 md_summary = os.path.join("scans", "report_summary.md") def icon(val): return "✅" if val == 0 else "❌" with open(md_summary, 'w', encoding='utf-8') as f: f.write(f"# Podsumowanie audytu SEO – {domain}\n") f.write(f"Wygenerowano: {ts} \n\n") f.write("| Kategoria | Wartość | Status |\n") f.write("|---|---|---|\n") f.write(f"| Przeskanowane strony | {total_pages} | ✅ |\n") f.write(f"| Odpowiedzi 200 OK | {total_200} | ✅ |\n") f.write(f"| Błędy 404 | {total_404} | {icon(total_404)} |\n") f.write(f"| Błędy 5xx | {total_5xx} | {icon(total_5xx)} |\n") f.write(f"| Strony Noindex | {total_noindex} | – |\n") f.write(f"| Błędy schema.org | {total_schema} | {icon(total_schema)} |\n") f.write(f"| Błędy tłumaczeń | {total_transl} | {icon(total_transl)} |\n") f.write(f"| Obrazy bez atrybutu alt | {total_noalt} | {icon(total_noalt)} |\n") f.write(f"| Obrazy bez WebP/AVIF | {total_nowebp} | {icon(total_nowebp)} |\n") f.write(f"| Średni czas odpowiedzi | {avg_time}s | – |\n") f.write("\n---\n\n") f.write("## Pliki raportów\n\n") f.write("| Plik | Zawartość |\n") f.write("|---|---|\n") f.write("| `report_summary.md` | To podsumowanie |\n") f.write("| `report_translations.md` | Pełna lista błędów tłumaczeń (dla Agenta AI) |\n") f.write("| `report_http_errors.csv` | Lista URL z błędami HTTP (4xx, 5xx) |\n") f.write("| `report_schema_errors.md` | Strony z brakującymi polami schema.org |\n") print(f" -> {md_summary}") conn.close() print("[*] Eksport raportów zakończony.") except Exception as e: print(f"\n[!] Błąd podczas eksportu raportów: {e}") if __name__ == "__main__": def load_config(): try: # Wymuszamy utf-8, żeby uniknąć problemów z kodowaniem Windows with open("config.json", "r", encoding="utf-8") as f: raw_config = json.load(f) # Czyścimy klucze ze spacji na wszelki wypadek return {k.strip(): v for k, v in raw_config.items()} except Exception as e: print(f"[!] Błąd wczytywania config.json: {e}") return {} parser = argparse.ArgumentParser(description="Crawler SEO - Podgląd błędów i audyt.") parser.add_argument("--url", default="https://fluo.dog", help="Startowy URL") parser.add_argument("--threads", type=int, default=10, help="Liczba wątków") args = parser.parse_args() # Upewnij się, że katalog scans istnieje if not os.path.exists("scans"): os.makedirs("scans") config = load_config() # Debug: wypiszmy jakie klucze faktycznie widzi Python available_keys = ", ".join(config.keys()) print(f"[*] Wczytane klucze z config: {available_keys}") token = config.get("telegram_token") id_info = config.get("telegram_chat_id_info") id_err = config.get("telegram_chat_id_errors") print(f"[*] Konfiguracja: INFO_ID={id_info}, ERRORS_ID={id_err}") notifier = TelegramNotifier(token, id_info, id_err) if notifier.enabled: print("[*] Telegram powiadomienia: WŁĄCZONE") notifier.send(f"🚀 Rozpoczynam audyt SEO dla: {html.escape(args.url)}", target='info') else: print("[!] Telegram powiadomienia: WYŁĄCZONE (sprawdź czy klucze w config.json są poprawne)") db_name = f"scans/crawler_v18_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db" crawler(args.url, db_name, args.threads, notifier)