Filtrowanie false-positive w audycie tlumaczen (rasy psow, anglicyzmy, CS/SK, NO/NN)
This commit is contained in:
+225
-5
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user