🚀 Full project sync: Hotels RAG & Audit System
✨ Major Features: - Complete RAG system for hotel website analysis - Hybrid audit with BGE-M3 embeddings + Natasha NER - Universal horizontal Excel reports with dashboards - Multi-region processing (SPb, Orel, Chukotka, Kamchatka) 📊 Completed Regions: - Орловская область: 100% (36/36) - Чукотский АО: 100% (4/4) - г. Санкт-Петербург: 93% (893/960) - Камчатский край: 87% (89/102) 🔧 Infrastructure: - PostgreSQL with pgvector extension - BGE-M3 embeddings API - Browserless for web scraping - N8N workflows for automation - S3/Nextcloud file storage 📝 Documentation: - Complete DB schemas - API documentation - Setup guides - Status reports
This commit is contained in:
579
hybrid_audit_spb.py
Normal file
579
hybrid_audit_spb.py
Normal file
@@ -0,0 +1,579 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ГИБРИДНЫЙ АУДИТ ОТЕЛЕЙ САНКТ-ПЕТЕРБУРГА
|
||||
Комбинирует 3 подхода:
|
||||
1. Семантический поиск (BGE-M3 embeddings)
|
||||
2. Регулярные выражения (точные паттерны)
|
||||
3. NER с Natasha (извлечение сущностей)
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
import requests
|
||||
import json
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import time
|
||||
from urllib.parse import unquote
|
||||
import re
|
||||
|
||||
# Natasha для NER
|
||||
from natasha import (
|
||||
Segmenter,
|
||||
MorphVocab,
|
||||
NewsEmbedding,
|
||||
NewsMorphTagger,
|
||||
NewsSyntaxParser,
|
||||
NewsNERTagger,
|
||||
Doc
|
||||
)
|
||||
|
||||
# Конфигурация БД
|
||||
DB_CONFIG = {
|
||||
'host': "147.45.189.234",
|
||||
'port': 5432,
|
||||
'database': "default_db",
|
||||
'user': "gen_user",
|
||||
'password': unquote("2~~9_%5EkVsU%3F2%5CS")
|
||||
}
|
||||
|
||||
# Конфигурация для BGE-M3 API
|
||||
BGE_API_URL = "http://147.45.146.17:8002/embed"
|
||||
BGE_API_KEY = "22564b177aa73b6ac0b8642d7773350ff4c01d4983f028beff15ea247f09fa89"
|
||||
|
||||
# Инициализация Natasha
|
||||
print("🔧 Инициализация Natasha...")
|
||||
segmenter = Segmenter()
|
||||
morph_vocab = MorphVocab()
|
||||
emb = NewsEmbedding()
|
||||
morph_tagger = NewsMorphTagger(emb)
|
||||
syntax_parser = NewsSyntaxParser(emb)
|
||||
ner_tagger = NewsNERTagger(emb)
|
||||
print("✅ Natasha готова!")
|
||||
|
||||
# 18 НАСТОЯЩИХ критериев аудита с регулярками
|
||||
AUDIT_CRITERIA = [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Юридическая идентификация и верификация',
|
||||
'query': 'полное наименование организации ОПФ ИНН ОГРН ЕГРЮЛ ЕГРИП проверить',
|
||||
'keywords': ['инн', 'огрн', 'егрюл', 'егрип', 'организация', 'ооо', 'ип'],
|
||||
'required_patterns': [
|
||||
r'\b\d{10}\b', # ИНН юридического лица (10 цифр)
|
||||
r'\b\d{12}\b', # ИНН ИП (12 цифр)
|
||||
r'\b\d{13}\b', # ОГРН (13 цифр)
|
||||
r'\b\d{15}\b', # ОГРНИП (15 цифр)
|
||||
r'инн\s*:?\s*\d{10,12}',
|
||||
r'огрн\s*:?\s*\d{13}',
|
||||
r'огрнип\s*:?\s*\d{15}',
|
||||
],
|
||||
'use_ner': True, # Использовать Natasha для извлечения названий организаций
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Адрес',
|
||||
'query': 'юридический адрес фактический адрес местонахождение',
|
||||
'keywords': ['адрес', 'address', 'местонахождение', 'г.', 'ул.'],
|
||||
'priority_patterns': [
|
||||
r'\d{6}.*?ул\.', # Индекс + ул.
|
||||
r'ул\.\s*[А-Яа-яёЁA-Za-z\s]+,?\s*\d+', # ул. Название, дом
|
||||
],
|
||||
'use_ner': True, # Использовать Natasha для извлечения адресов
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'name': 'Контакты',
|
||||
'query': 'телефон email форма обратной связи чат контакты',
|
||||
'keywords': ['телефон', 'phone', 'email', '@', '+7', '8-800'],
|
||||
'priority_patterns': [
|
||||
r'(?:\+7|8)\s*\(?\d{3,5}\)?\s*\d{1,3}[-\s]?\d{2}[-\s]?\d{2}', # Телефон
|
||||
r'[\w\.-]+@[\w\.-]+\.\w{2,}', # Email
|
||||
],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'name': 'Режим работы',
|
||||
'query': 'часы работы график приема режим работы колл-центр',
|
||||
'keywords': ['часы работы', 'график работы', 'режим работы', 'круглосуточно', '24/7'],
|
||||
'priority_patterns': [
|
||||
r'(?:с|с\s+)\d{1,2}(?::|\.)\d{2}\s*(?:до|по)\s*\d{1,2}(?::|\.)\d{2}', # с 9:00 до 18:00
|
||||
r'\d{1,2}:\d{2}\s*-\s*\d{1,2}:\d{2}', # 9:00 - 18:00
|
||||
r'круглосуточно',
|
||||
r'24\s*[/\-]\s*7',
|
||||
],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 5,
|
||||
'name': 'Политика ПДн (152-ФЗ)',
|
||||
'query': 'политика персональных данных обработка ПДн 152-ФЗ',
|
||||
'keywords': ['персональных данных', 'пдн', '152-фз', 'privacy'],
|
||||
'priority_patterns': [
|
||||
r'политика\s+в\s+отношении\s+обработки\s+персональных\s+данных',
|
||||
r'152[-\s]?фз',
|
||||
r'федеральный\s+закон.*?персональных\s+данных',
|
||||
],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 6,
|
||||
'name': 'Роскомнадзор (реестр)',
|
||||
'query': 'роскомнадзор реестр операторов персональных данных',
|
||||
'keywords': ['роскомнадзор', 'реестр', 'оператор'],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 7,
|
||||
'name': 'Договор-оферта / Правила оказания услуг',
|
||||
'query': 'договор оферта правила оказания услуг условия',
|
||||
'keywords': ['договор', 'оферта', 'правила', 'условия', 'услуг'],
|
||||
'priority_patterns': [
|
||||
r'публичная\s+оферта',
|
||||
r'договор.*?оказани.*?услуг',
|
||||
],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 8,
|
||||
'name': 'Рекламации и споры',
|
||||
'query': 'рекламации споры жалобы претензии решение конфликтов',
|
||||
'keywords': ['рекламация', 'спор', 'жалоба', 'претензия', 'конфликт'],
|
||||
'weight': 1.0
|
||||
},
|
||||
{
|
||||
'id': 9,
|
||||
'name': 'Цены/прайс',
|
||||
'query': 'цены прайс тарифы стоимость номера',
|
||||
'keywords': ['цена', 'прайс', 'тариф', 'стоимость', 'номер'],
|
||||
'priority_patterns': [
|
||||
r'\d+\s*(?:руб|₽)', # Цены в рублях
|
||||
],
|
||||
'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 generate_embedding(text: str) -> list:
|
||||
"""Генерация эмбеддинга для текста через API"""
|
||||
headers = {
|
||||
"X-API-Key": BGE_API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {"text": [text]}
|
||||
response = requests.post(BGE_API_URL, json=payload, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json().get('embeddings', [[]])[0]
|
||||
|
||||
def semantic_search_for_criterion(hotel_id: str, query: str, limit: int = 3):
|
||||
"""Семантический поиск по chunks отеля"""
|
||||
try:
|
||||
query_embedding = generate_embedding(query)
|
||||
embedding_str = json.dumps(query_embedding)
|
||||
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
query_sql = f"""
|
||||
SELECT
|
||||
text,
|
||||
metadata->>'url' as url,
|
||||
embedding <-> %s::vector as distance
|
||||
FROM hotel_website_chunks
|
||||
WHERE metadata->>'hotel_id' = %s AND embedding IS NOT NULL
|
||||
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"Ошибка семантического поиска: {e}")
|
||||
return []
|
||||
|
||||
def check_patterns(text: str, patterns: list) -> dict:
|
||||
"""Проверка текста на соответствие регулярным выражениям"""
|
||||
matches = []
|
||||
for pattern in patterns:
|
||||
found = re.findall(pattern, text, re.IGNORECASE)
|
||||
if found:
|
||||
matches.extend(found[:3]) # Макс 3 совпадения на паттерн
|
||||
return {
|
||||
'found': len(matches) > 0,
|
||||
'matches': matches[:5], # Макс 5 совпадений всего
|
||||
'count': len(matches)
|
||||
}
|
||||
|
||||
def extract_entities_with_natasha(text: str) -> dict:
|
||||
"""Извлечение сущностей с помощью Natasha"""
|
||||
try:
|
||||
doc = Doc(text[:5000]) # Ограничиваем длину для производительности
|
||||
doc.segment(segmenter)
|
||||
doc.tag_morph(morph_tagger)
|
||||
doc.parse_syntax(syntax_parser)
|
||||
doc.tag_ner(ner_tagger)
|
||||
|
||||
entities = {
|
||||
'ORG': [], # Организации
|
||||
'PER': [], # Люди
|
||||
'LOC': [], # Локации/адреса
|
||||
}
|
||||
|
||||
for span in doc.spans:
|
||||
if span.type in entities:
|
||||
entities[span.type].append(span.text)
|
||||
|
||||
return entities
|
||||
except Exception as e:
|
||||
print(f"Ошибка Natasha: {e}")
|
||||
return {'ORG': [], 'PER': [], 'LOC': []}
|
||||
|
||||
def hybrid_audit_criterion(hotel_id: str, criterion: dict) -> dict:
|
||||
"""
|
||||
Гибридный аудит по одному критерию:
|
||||
1. Семантический поиск
|
||||
2. Проверка регулярками
|
||||
3. NER с Natasha (если включено)
|
||||
"""
|
||||
result = {
|
||||
'semantic_score': 0.0,
|
||||
'pattern_score': 0.0,
|
||||
'ner_score': 0.0,
|
||||
'final_score': 0.0,
|
||||
'evidence': [],
|
||||
'explanation': '',
|
||||
'approval_urls': [], # Ссылки на страницы
|
||||
'approval_quotes': [] # Цитаты с контекстом
|
||||
}
|
||||
|
||||
# 1. СЕМАНТИЧЕСКИЙ ПОИСК
|
||||
semantic_matches = semantic_search_for_criterion(hotel_id, criterion['query'], limit=3)
|
||||
|
||||
if semantic_matches:
|
||||
best_match = semantic_matches[0]
|
||||
distance = best_match['distance']
|
||||
url = best_match.get('url', 'Нет URL')
|
||||
|
||||
if distance < 0.7:
|
||||
result['semantic_score'] = 1.0
|
||||
result['evidence'].append(f"🔍 Семантика (отлично, {distance:.3f})")
|
||||
result['approval_urls'].append(url)
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': best_match['text'][:300],
|
||||
'method': 'Семантический поиск',
|
||||
'distance': f"{distance:.3f}"
|
||||
})
|
||||
elif distance < 0.9:
|
||||
result['semantic_score'] = 0.5
|
||||
result['evidence'].append(f"🔍 Семантика (средне, {distance:.3f})")
|
||||
result['approval_urls'].append(url)
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': best_match['text'][:300],
|
||||
'method': 'Семантический поиск',
|
||||
'distance': f"{distance:.3f}"
|
||||
})
|
||||
else:
|
||||
result['semantic_score'] = 0.2
|
||||
result['evidence'].append(f"🔍 Семантика (слабо, {distance:.3f})")
|
||||
result['approval_urls'].append(url)
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': best_match['text'][:300],
|
||||
'method': 'Семантический поиск',
|
||||
'distance': f"{distance:.3f}"
|
||||
})
|
||||
|
||||
# 2. ПРОВЕРКА РЕГУЛЯРКАМИ
|
||||
if 'priority_patterns' in criterion or 'required_patterns' in criterion:
|
||||
patterns = criterion.get('priority_patterns', []) + criterion.get('required_patterns', [])
|
||||
|
||||
# Проверяем все найденные семантикой тексты
|
||||
for match in semantic_matches:
|
||||
pattern_check = check_patterns(match['text'], patterns)
|
||||
|
||||
if pattern_check['found']:
|
||||
result['pattern_score'] = 1.0
|
||||
result['evidence'].append(f"✅ Регулярки: найдено {pattern_check['count']} совпадений")
|
||||
|
||||
# Добавляем цитату с найденными паттернами
|
||||
url = match.get('url', 'Нет URL')
|
||||
if url not in result['approval_urls']:
|
||||
result['approval_urls'].append(url)
|
||||
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': match['text'][:300],
|
||||
'method': 'Регулярные выражения',
|
||||
'matches': ', '.join(pattern_check['matches'])
|
||||
})
|
||||
break # Нашли - хватит
|
||||
else:
|
||||
result['pattern_score'] = 0.0
|
||||
|
||||
# 3. NER С NATASHA
|
||||
if criterion.get('use_ner', False) and semantic_matches:
|
||||
all_text = " ".join([m['text'] for m in semantic_matches])
|
||||
entities = extract_entities_with_natasha(all_text)
|
||||
|
||||
if criterion['id'] == 1: # Юридическая идентификация
|
||||
if entities['ORG']:
|
||||
result['ner_score'] = 1.0
|
||||
result['evidence'].append(f"🏢 Natasha (организации): {', '.join(entities['ORG'][:3])}")
|
||||
|
||||
# Добавляем цитату с найденными организациями
|
||||
url = semantic_matches[0].get('url', 'Нет URL')
|
||||
if url not in result['approval_urls']:
|
||||
result['approval_urls'].append(url)
|
||||
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': all_text[:300],
|
||||
'method': 'Natasha NER (организации)',
|
||||
'entities': ', '.join(entities['ORG'][:3])
|
||||
})
|
||||
else:
|
||||
result['ner_score'] = 0.0
|
||||
|
||||
elif criterion['id'] == 2: # Адрес
|
||||
if entities['LOC']:
|
||||
result['ner_score'] = 1.0
|
||||
result['evidence'].append(f"📍 Natasha (адреса): {', '.join(entities['LOC'][:3])}")
|
||||
|
||||
# Добавляем цитату с найденными адресами
|
||||
url = semantic_matches[0].get('url', 'Нет URL')
|
||||
if url not in result['approval_urls']:
|
||||
result['approval_urls'].append(url)
|
||||
|
||||
result['approval_quotes'].append({
|
||||
'url': url,
|
||||
'quote': all_text[:300],
|
||||
'method': 'Natasha NER (адреса)',
|
||||
'entities': ', '.join(entities['LOC'][:3])
|
||||
})
|
||||
else:
|
||||
result['ner_score'] = 0.0
|
||||
|
||||
# ИТОГОВАЯ ОЦЕНКА (взвешенная)
|
||||
weights = {
|
||||
'semantic': 0.4,
|
||||
'pattern': 0.4,
|
||||
'ner': 0.2
|
||||
}
|
||||
|
||||
result['final_score'] = (
|
||||
result['semantic_score'] * weights['semantic'] +
|
||||
result['pattern_score'] * weights['pattern'] +
|
||||
result['ner_score'] * weights['ner']
|
||||
)
|
||||
|
||||
# Объяснение
|
||||
if result['final_score'] >= 0.8:
|
||||
result['explanation'] = "🟢 Высокая: информация найдена и подтверждена"
|
||||
elif result['final_score'] >= 0.5:
|
||||
result['explanation'] = "🟡 Средняя: информация найдена частично"
|
||||
elif result['final_score'] >= 0.3:
|
||||
result['explanation'] = "🟠 Низкая: информация найдена, но не подтверждена"
|
||||
else:
|
||||
result['explanation'] = "🔴 Очень низкая: информация не найдена"
|
||||
|
||||
return result
|
||||
|
||||
def audit_hotel_hybrid(hotel_info: dict):
|
||||
"""Проводит гибридный аудит для одного отеля"""
|
||||
hotel_id = hotel_info['id']
|
||||
hotel_name = hotel_info['full_name']
|
||||
region_name = hotel_info['region_name']
|
||||
|
||||
print(f"\n🏨 ГИБРИДНЫЙ АУДИТ: {hotel_name}")
|
||||
print("=" * 80)
|
||||
|
||||
results = {
|
||||
'hotel_id': hotel_id,
|
||||
'hotel_name': hotel_name,
|
||||
'region_name': region_name,
|
||||
'total_score': 0.0,
|
||||
'criteria_results': {}
|
||||
}
|
||||
|
||||
for criterion in AUDIT_CRITERIA:
|
||||
print(f" 🔍 Критерий {criterion['id']}: {criterion['name']}")
|
||||
|
||||
audit_result = hybrid_audit_criterion(hotel_id, criterion)
|
||||
|
||||
results['criteria_results'][criterion['name']] = audit_result
|
||||
results['total_score'] += audit_result['final_score']
|
||||
|
||||
print(f" {audit_result['explanation']} (Итого: {audit_result['final_score']:.2f}/1.0)")
|
||||
print(f" └─ Семантика: {audit_result['semantic_score']:.2f} | Регулярки: {audit_result['pattern_score']:.2f} | NER: {audit_result['ner_score']:.2f}")
|
||||
|
||||
for evidence in audit_result['evidence'][:2]: # Показываем первые 2 доказательства
|
||||
print(f" {evidence}")
|
||||
|
||||
time.sleep(0.5) # Небольшая пауза между критериями
|
||||
|
||||
print(f"\n📊 ИТОГОВАЯ ОЦЕНКА: {results['total_score']:.2f}/{len(AUDIT_CRITERIA)} ({results['total_score']/len(AUDIT_CRITERIA)*100:.1f}%)")
|
||||
print("=" * 80)
|
||||
return results
|
||||
|
||||
def main():
|
||||
print("🚀 ГИБРИДНЫЙ АУДИТ ОТЕЛЕЙ ЧУКОТКИ")
|
||||
print("=" * 80)
|
||||
print("Методы:")
|
||||
print(" 1️⃣ Семантический поиск (BGE-M3)")
|
||||
print(" 2️⃣ Регулярные выражения")
|
||||
print(" 3️⃣ NER с Natasha")
|
||||
print("=" * 80)
|
||||
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Получаем ВСЕ отели Чукотского автономного округа с эмбеддингами
|
||||
cur.execute("""
|
||||
SELECT DISTINCT ON (hm.id) hm.id, hm.full_name, hm.region_name
|
||||
FROM hotel_main hm
|
||||
JOIN hotel_website_chunks hwc ON hm.id::text = hwc.metadata->>'hotel_id'
|
||||
WHERE hm.region_name = 'г. Санкт-Петербург';
|
||||
""")
|
||||
chukotka_hotels = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if not chukotka_hotels:
|
||||
print("❌ Не найдено отелей в Чукотском автономном округе с эмбеддингами.")
|
||||
return
|
||||
|
||||
print(f"\n📊 Найдено {len(chukotka_hotels)} отелей для гибридного аудита:")
|
||||
for hotel in chukotka_hotels:
|
||||
print(f" • {hotel['full_name']}")
|
||||
print()
|
||||
|
||||
all_audit_results = []
|
||||
for hotel_info in chukotka_hotels:
|
||||
audit_results = audit_hotel_hybrid(hotel_info)
|
||||
all_audit_results.append(audit_results)
|
||||
|
||||
# Создание Excel отчета
|
||||
df_data = []
|
||||
for hotel_result in all_audit_results:
|
||||
row = {
|
||||
'ID Отеля': hotel_result['hotel_id'],
|
||||
'Название Отеля': hotel_result['hotel_name'],
|
||||
'Регион': hotel_result['region_name'],
|
||||
'Общий балл': f"{hotel_result['total_score']:.2f}/{len(AUDIT_CRITERIA)}"
|
||||
}
|
||||
|
||||
for criterion_name, crit_data in hotel_result['criteria_results'].items():
|
||||
row[f'{criterion_name} (Итого)'] = f"{crit_data['final_score']:.2f}"
|
||||
row[f'{criterion_name} (Семантика)'] = f"{crit_data['semantic_score']:.2f}"
|
||||
row[f'{criterion_name} (Регулярки)'] = f"{crit_data['pattern_score']:.2f}"
|
||||
row[f'{criterion_name} (NER)'] = f"{crit_data['ner_score']:.2f}"
|
||||
row[f'{criterion_name} (Объяснение)'] = crit_data['explanation']
|
||||
row[f'{criterion_name} (Доказательства)'] = "\n".join(crit_data['evidence'])
|
||||
|
||||
# ДОБАВЛЯЕМ URL И ЦИТАТЫ!
|
||||
if crit_data.get('approval_urls'):
|
||||
row[f'{criterion_name} (URL)'] = "\n".join(crit_data['approval_urls'])
|
||||
else:
|
||||
row[f'{criterion_name} (URL)'] = "Не найдено"
|
||||
|
||||
if crit_data.get('approval_quotes'):
|
||||
quotes_text = []
|
||||
for quote_data in crit_data['approval_quotes']:
|
||||
quote_str = f"[{quote_data['method']}]\n"
|
||||
quote_str += f"URL: {quote_data['url']}\n"
|
||||
quote_str += f"Цитата: {quote_data['quote']}\n"
|
||||
if 'matches' in quote_data:
|
||||
quote_str += f"Найдено: {quote_data['matches']}\n"
|
||||
if 'entities' in quote_data:
|
||||
quote_str += f"Сущности: {quote_data['entities']}\n"
|
||||
if 'distance' in quote_data:
|
||||
quote_str += f"Distance: {quote_data['distance']}\n"
|
||||
quotes_text.append(quote_str)
|
||||
row[f'{criterion_name} (Цитаты)'] = "\n---\n".join(quotes_text)
|
||||
else:
|
||||
row[f'{criterion_name} (Цитаты)'] = "Не найдено"
|
||||
|
||||
df_data.append(row)
|
||||
|
||||
df = pd.DataFrame(df_data)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output_filename = f"hybrid_audit_chukotka_{timestamp}.xlsx"
|
||||
df.to_excel(output_filename, index=False)
|
||||
print(f"\n✅ Гибридный отчет сохранен в {output_filename}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user