Проект аудита отелей: основные скрипты и документация
- Краулеры: 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
This commit is contained in:
389
semantic_audit_chukotka.py
Normal file
389
semantic_audit_chukotka.py
Normal file
@@ -0,0 +1,389 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user