#!/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()