Проект аудита отелей: основные скрипты и документация

- Краулеры: 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:
Фёдор
2025-10-16 10:52:09 +03:00
parent 545e199389
commit 0cf3297290
105 changed files with 28743 additions and 0 deletions

389
semantic_audit_chukotka.py Normal file
View 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()