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):
|
def add_translation_issue(self, sku, lang1, lang2, field):
|
||||||
with self.lock:
|
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):
|
def get_prev_404_count(self, current_db):
|
||||||
@@ -71,6 +70,45 @@ class TelegramNotifier:
|
|||||||
return count
|
return count
|
||||||
except: return None
|
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):
|
def send_final_report(self, start_url, total, errors, db_file, search_results=-1):
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
print("\n[!] Powiadomienia Telegram są wyłączone (brak konfiguracji).")
|
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"{transl_icon} Tłumaczenia: {transl_text}\n"
|
||||||
info_msg += f"{search_icon} Wyszukiwarka: {search_text}\n\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:
|
if self.critical_errors or self.schema_errors or self.translation_issues:
|
||||||
info_msg += f"🚨 Wykryto błędy. Szczegóły na kanale ERRORS."
|
info_msg += f"🚨 Wykryto błędy. Szczegóły na kanale ERRORS."
|
||||||
else:
|
else:
|
||||||
info_msg += f"✅ Brak krytycznych błędów."
|
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')
|
self.send(info_msg, target='info')
|
||||||
|
|
||||||
# 2. RAPORT ERRORS
|
# 2. RAPORT ERRORS
|
||||||
@@ -158,11 +201,16 @@ class TelegramNotifier:
|
|||||||
prompt_text += "\n"
|
prompt_text += "\n"
|
||||||
|
|
||||||
if self.translation_issues:
|
if self.translation_issues:
|
||||||
err_msg += f"🌐 <b>TŁUMACZENIA:</b>\n"
|
total_transl = len(self.translation_issues)
|
||||||
prompt_text += "BŁĘDY TŁUMACZEŃ:\n"
|
shown = min(15, total_transl)
|
||||||
for issue in self.translation_issues[:10]:
|
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"
|
err_msg += f"• {html.escape(issue)}\n"
|
||||||
prompt_text += f"- {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"
|
err_msg += "\n"
|
||||||
prompt_text += "\n"
|
prompt_text += "\n"
|
||||||
|
|
||||||
@@ -357,13 +405,83 @@ def crawler(start_url, db_file, max_threads, tg_notifier):
|
|||||||
db_queue.put(None)
|
db_queue.put(None)
|
||||||
db_thread.join()
|
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"')
|
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 = {}
|
sku_map = {}
|
||||||
for sku, lang, fjson in cursor.fetchall():
|
for sku, lang, fjson in cursor.fetchall():
|
||||||
if sku not in sku_map: sku_map[sku] = {}
|
if sku not in sku_map: sku_map[sku] = {}
|
||||||
data = json.loads(fjson)
|
data = json.loads(fjson)
|
||||||
sku_map[sku][lang] = {'name': data.get('name', ''), 'description': data.get('description', '')}
|
sku_map[sku][lang] = {'name': data.get('name', ''), 'description': data.get('description', '')}
|
||||||
|
|
||||||
|
fp_skipped = 0
|
||||||
for sku, langs in sku_map.items():
|
for sku, langs in sku_map.items():
|
||||||
lang_list = list(langs.keys())
|
lang_list = list(langs.keys())
|
||||||
if len(lang_list) > 1:
|
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)):
|
for j in range(i + 1, len(lang_list)):
|
||||||
l1, l2 = lang_list[i], lang_list[j]
|
l1, l2 = lang_list[i], lang_list[j]
|
||||||
if langs[l1]['name'] == langs[l2]['name']:
|
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']))
|
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')
|
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()
|
conn.commit(); conn.close()
|
||||||
|
|
||||||
# TEST WYSZUKIWARKI
|
# TEST WYSZUKIWARKI
|
||||||
@@ -398,6 +522,102 @@ def crawler(start_url, db_file, max_threads, tg_notifier):
|
|||||||
print(f"[!] Test wyszukiwarki BŁĄD: {e}")
|
print(f"[!] Test wyszukiwarki BŁĄD: {e}")
|
||||||
|
|
||||||
tg_notifier.send_final_report(start_url, crawled_count, error_count, db_file, search_results=search_count)
|
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__":
|
if __name__ == "__main__":
|
||||||
def load_config():
|
def load_config():
|
||||||
|
|||||||
Reference in New Issue
Block a user