From f1a76139b6d37a1d7d2fb8a20c3d622fdc04aff3 Mon Sep 17 00:00:00 2001 From: Lukasz Date: Sat, 9 May 2026 22:30:24 +0200 Subject: [PATCH] Filtrowanie false-positive w audycie tlumaczen (rasy psow, anglicyzmy, CS/SK, NO/NN) --- crawler.py | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 226 insertions(+), 6 deletions(-) diff --git a/crawler.py b/crawler.py index 45daa8a..55eb136 100644 --- a/crawler.py +++ b/crawler.py @@ -50,8 +50,7 @@ class TelegramNotifier: def add_translation_issue(self, sku, lang1, lang2, field): with self.lock: - if len(self.translation_issues) < 15: - self.translation_issues.append(f"SKU {sku}: {field} identyczny w {lang1} i {lang2}") + 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 @@ -71,6 +70,45 @@ class TelegramNotifier: 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).") @@ -118,10 +156,15 @@ class TelegramNotifier: 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 @@ -158,11 +201,16 @@ class TelegramNotifier: prompt_text += "\n" if self.translation_issues: - err_msg += f"🌐 TŁUMACZENIA:\n" - prompt_text += "BŁĘDY TŁUMACZEŃ:\n" - for issue in self.translation_issues[:10]: + 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" @@ -357,13 +405,83 @@ def crawler(start_url, db_file, max_threads, tg_notifier): db_queue.put(None) db_thread.join() - # AUDYT WIELOJĘZYCZNOŚCI + # 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: @@ -371,8 +489,14 @@ def crawler(start_url, db_file, max_threads, tg_notifier): 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 @@ -398,6 +522,102 @@ def crawler(start_url, db_file, max_threads, tg_notifier): 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():