Filtrowanie false-positive w audycie tlumaczen (rasy psow, anglicyzmy, CS/SK, NO/NN)

This commit is contained in:
2026-05-09 22:30:24 +02:00
parent 8411593c55
commit f1a76139b6
+225 -5
View File
@@ -50,7 +50,6 @@ 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}")
def get_prev_404_count(self, current_db):
@@ -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ń: <code>{html.escape(os.path.basename(transl_md_file))}</code>"
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"🌐 <b>TŁUMACZENIA:</b>\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"🌐 <b>TŁUMACZENIA ({total_transl} błędów):</b>\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>... i {total_transl - shown} więcej. Pełna lista w pliku MD.</i>\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():