Files
hotels/semantic_audit_chukotka.py
Фёдор 0cf3297290 Проект аудита отелей: основные скрипты и документация
- Краулеры: smart_crawler.py, regional_crawler.py
- Аудит: audit_orel_to_excel.py, audit_chukotka_to_excel.py
- РКН проверка: check_rkn_registry.py, recheck_unclear_rkn.py
- Отчёты: create_orel_horizontal_report.py
- Обработка: process_all_hotels_embeddings.py
- Документация: README.md, DB_SCHEMA_REFERENCE.md
2025-10-16 10:52:09 +03:00

390 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Семантический аудит отелей Чукотки с использованием эмбеддингов
18 критериев аудита
"""
import psycopg2
from psycopg2.extras import RealDictCursor
from urllib.parse import unquote
import requests
import json
import pandas as pd
from datetime import datetime
import time
# Конфигурация
DB_CONFIG = {
'host': "147.45.189.234",
'port': 5432,
'database': "default_db",
'user': "gen_user",
'password': unquote("2~~9_%5EkVsU%3F2%5CS")
}
BGE_API_URL = "http://147.45.146.17:8002/embed"
BGE_API_KEY = "22564b177aa73b6ac0b8642d7773350ff4c01d4983f028beff15ea247f09fa89"
# 18 НАСТОЯЩИХ критериев аудита из audit_system_new.py
AUDIT_CRITERIA = [
{
'id': 1,
'name': 'Юридическая идентификация и верификация',
'query': 'полное наименование организации ОПФ ИНН ОГРН ЕГРЮЛ ЕГРИП проверить',
'keywords': ['инн', 'огрн', 'егрюл', 'егрип', 'организация', 'ооо', 'ип'],
'weight': 1.0
},
{
'id': 2,
'name': 'Адрес',
'query': 'юридический адрес фактический адрес местонахождение',
'keywords': ['адрес', 'address', 'местонахождение', 'г.', 'ул.'],
'weight': 1.0
},
{
'id': 3,
'name': 'Контакты',
'query': 'телефон email форма обратной связи чат контакты',
'keywords': ['телефон', 'phone', 'email', '@', '+7', '8-800'],
'weight': 1.0
},
{
'id': 4,
'name': 'Режим работы',
'query': 'часы работы график приема режим работы колл-центр',
'keywords': ['часы работы', 'график работы', 'режим работы', 'круглосуточно', '24/7', 'пн-пт', 'пн-вс', 'время работы'],
'weight': 1.0
},
{
'id': 5,
'name': 'Политика ПДн (152-ФЗ)',
'query': 'политика персональных данных обработка ПДн 152-ФЗ',
'keywords': ['персональных данных', 'пдн', '152-фз', 'privacy'],
'weight': 1.0
},
{
'id': 6,
'name': 'Роскомнадзор (реестр)',
'query': 'роскомнадзор реестр операторов персональных данных',
'keywords': ['роскомнадзор', 'реестр', 'оператор'],
'weight': 1.0
},
{
'id': 7,
'name': 'Договор-оферта / Правила оказания услуг',
'query': 'договор оферта правила оказания услуг условия',
'keywords': ['договор', 'оферта', 'правила', 'условия', 'услуг'],
'weight': 1.0
},
{
'id': 8,
'name': 'Рекламации и споры',
'query': 'рекламации споры жалобы претензии решение конфликтов',
'keywords': ['рекламация', 'спор', 'жалоба', 'претензия', 'конфликт'],
'weight': 1.0
},
{
'id': 9,
'name': 'Цены/прайс',
'query': 'цены прайс тарифы стоимость номера',
'keywords': ['цена', 'прайс', 'тариф', 'стоимость', 'номер'],
'weight': 1.0
},
{
'id': 10,
'name': 'Способы оплаты',
'query': 'способы оплаты платеж банковская карта наличные',
'keywords': ['оплата', 'платеж', 'карта', 'наличные', 'способ'],
'weight': 1.0
},
{
'id': 11,
'name': 'Онлайн-оплата',
'query': 'онлайн оплата интернет платеж карта через сайт',
'keywords': ['онлайн', 'интернет', 'платеж', 'карта', 'сайт'],
'weight': 1.0
},
{
'id': 12,
'name': 'Онлайн-бронирование',
'query': 'онлайн бронирование заказ номера через сайт',
'keywords': ['бронирование', 'заказ', 'номер', 'сайт', 'онлайн'],
'weight': 1.0
},
{
'id': 13,
'name': 'FAQ',
'query': 'часто задаваемые вопросы FAQ помощь',
'keywords': ['faq', 'вопрос', 'ответ', 'помощь', 'часто'],
'weight': 1.0
},
{
'id': 14,
'name': 'Доступность для ЛОВЗ',
'query': 'доступность инвалиды ЛОВЗ безбарьерная среда',
'keywords': ['доступность', 'инвалид', 'ловз', 'безбарьерная'],
'weight': 1.0
},
{
'id': 15,
'name': 'Партнёры/бренды',
'query': 'партнеры бренды сотрудничество франшиза',
'keywords': ['партнер', 'бренд', 'сотрудничество', 'франшиза'],
'weight': 1.0
},
{
'id': 16,
'name': 'Команда/сотрудники',
'query': 'команда сотрудники персонал коллектив',
'keywords': ['команда', 'сотрудник', 'персонал', 'коллектив'],
'weight': 1.0
},
{
'id': 17,
'name': 'Уголок потребителя',
'query': 'уголок потребителя права потребителя защита прав',
'keywords': ['потребитель', 'права', 'защита', 'уголок'],
'weight': 1.0
},
{
'id': 18,
'name': 'Актуальность документов',
'query': 'актуальность документов дата обновления свежая информация',
'keywords': ['актуальность', 'документ', 'дата', 'обновление', 'свежая'],
'weight': 1.0
}
]
def get_db_connection():
"""Получить подключение к БД"""
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
def semantic_search_hotel(hotel_id: str, query: str, limit: int = 5):
"""Семантический поиск для конкретного отеля"""
try:
# Генерируем эмбеддинг для запроса
headers = {
"X-API-Key": BGE_API_KEY,
"Content-Type": "application/json"
}
payload = {"text": [query]}
response = requests.post(BGE_API_URL, json=payload, headers=headers, timeout=30)
if response.status_code != 200:
return []
result = response.json()
query_embedding = result.get('embeddings', [[]])[0]
if not query_embedding:
return []
embedding_str = json.dumps(query_embedding)
# Поиск по конкретному отелю
conn = get_db_connection()
cur = conn.cursor()
query_sql = """
SELECT
metadata->>'hotel_name' as hotel_name,
metadata->>'url' as url,
text,
embedding <-> %s::vector as distance
FROM hotel_website_chunks
WHERE metadata->>'hotel_id' = %s
ORDER BY embedding <-> %s::vector
LIMIT %s;
"""
cur.execute(query_sql, [embedding_str, hotel_id, embedding_str, limit])
results = cur.fetchall()
cur.close()
conn.close()
return results
except Exception as e:
print(f"Ошибка семантического поиска для отеля {hotel_id}: {e}")
return []
def evaluate_criterion(hotel_id: str, hotel_name: str, criterion: dict):
"""Оценка критерия для отеля"""
print(f" 🔍 Критерий {criterion['id']}: {criterion['name']}")
# Семантический поиск
search_results = semantic_search_hotel(hotel_id, criterion['query'], limit=3)
if not search_results:
return {
'criterion_id': criterion['id'],
'criterion_name': criterion['name'],
'score': 0,
'max_score': criterion['weight'],
'found_text': None,
'relevance': 0.0,
'url': None
}
# Анализируем результаты
best_result = search_results[0]
distance = best_result['distance']
# Конвертируем расстояние в оценку (чем меньше расстояние, тем выше оценка)
if distance < 0.7:
score = criterion['weight'] # Отлично
relevance = "🟢 Высокая"
elif distance < 0.9:
score = criterion['weight'] * 0.7 # Хорошо
relevance = "🟡 Средняя"
elif distance < 1.1:
score = criterion['weight'] * 0.4 # Удовлетворительно
relevance = "🟠 Низкая"
else:
score = 0 # Не найдено
relevance = "🔴 Очень низкая"
# Проверяем наличие ключевых слов в тексте
text_lower = best_result['text'].lower()
keywords_found = [kw for kw in criterion['keywords'] if kw in text_lower]
if keywords_found:
score = min(score + 0.1 * len(keywords_found), criterion['weight'])
return {
'criterion_id': criterion['id'],
'criterion_name': criterion['name'],
'score': round(score, 2),
'max_score': criterion['weight'],
'found_text': best_result['text'][:200] + "..." if len(best_result['text']) > 200 else best_result['text'],
'relevance': relevance,
'distance': round(distance, 3),
'keywords_found': keywords_found,
'url': best_result['url']
}
def audit_hotel(hotel_id: str, hotel_name: str):
"""Аудит одного отеля"""
print(f"\n🏨 АУДИТ ОТЕЛЯ: {hotel_name}")
print("=" * 60)
results = []
total_score = 0
max_total_score = sum(criterion['weight'] for criterion in AUDIT_CRITERIA)
for criterion in AUDIT_CRITERIA:
result = evaluate_criterion(hotel_id, hotel_name, criterion)
results.append(result)
total_score += result['score']
print(f"{criterion['name']}: {result['score']}/{result['max_score']} {result['relevance']}")
if result['found_text']:
print(f" 📄 {result['found_text']}")
print()
# Небольшая пауза между запросами
time.sleep(0.5)
percentage = (total_score / max_total_score) * 100 if max_total_score > 0 else 0
print(f"📊 ИТОГОВАЯ ОЦЕНКА: {total_score:.2f}/{max_total_score} ({percentage:.1f}%)")
return {
'hotel_id': hotel_id,
'hotel_name': hotel_name,
'total_score': round(total_score, 2),
'max_score': max_total_score,
'percentage': round(percentage, 1),
'criteria_results': results
}
def main():
"""Основная функция"""
print("🚀 СЕМАНТИЧЕСКИЙ АУДИТ ОТЕЛЕЙ ЧУКОТКИ")
print("=" * 50)
# Получаем отели Чукотки с эмбеддингами
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
SELECT DISTINCT
h.id,
h.full_name,
COUNT(c.id) as chunks_count
FROM hotel_main h
JOIN hotel_website_chunks c ON h.id::text = c.metadata->>'hotel_id'
WHERE h.region_name ILIKE '%чукот%'
AND c.id IS NOT NULL
GROUP BY h.id, h.full_name
HAVING COUNT(c.id) > 0
ORDER BY chunks_count DESC;
""")
hotels = cur.fetchall()
cur.close()
conn.close()
print(f"📊 Найдено {len(hotels)} отелей с эмбеддингами:")
for hotel in hotels:
print(f"{hotel['full_name']} ({hotel['chunks_count']} chunks)")
# Проводим аудит
audit_results = []
for hotel in hotels:
result = audit_hotel(hotel['id'], hotel['full_name'])
audit_results.append(result)
# Создаем Excel отчет
create_excel_report(audit_results)
print(f"\n✅ АУДИТ ЗАВЕРШЕН! Результаты сохранены в Excel.")
def create_excel_report(audit_results):
"""Создание Excel отчета"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"semantic_audit_chukotka_{timestamp}.xlsx"
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
# Сводная таблица
summary_data = []
for result in audit_results:
summary_data.append({
'Отель': result['hotel_name'],
'Общая оценка': result['total_score'],
'Максимальная оценка': result['max_score'],
'Процент': f"{result['percentage']}%",
'Статус': 'Отлично' if result['percentage'] >= 80 else 'Хорошо' if result['percentage'] >= 60 else 'Удовлетворительно' if result['percentage'] >= 40 else 'Неудовлетворительно'
})
summary_df = pd.DataFrame(summary_data)
summary_df.to_excel(writer, sheet_name='Сводка', index=False)
# Детальные результаты по каждому отелю
for result in audit_results:
sheet_name = result['hotel_name'][:30] # Ограничиваем длину имени листа
detailed_data = []
for criterion in result['criteria_results']:
detailed_data.append({
'Критерий': criterion['criterion_name'],
'Оценка': criterion['score'],
'Максимальная оценка': criterion['max_score'],
'Релевантность': criterion['relevance'],
'Расстояние': criterion['distance'],
'Найденный текст': criterion['found_text'],
'Найденные ключевые слова': ', '.join(criterion.get('keywords_found', [])),
'URL': criterion['url']
})
detailed_df = pd.DataFrame(detailed_data)
detailed_df.to_excel(writer, sheet_name=sheet_name, index=False)
print(f"📊 Excel отчет сохранен: {filename}")
if __name__ == "__main__":
main()