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

- Краулеры: 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

364
test_comfort_hotel.py Normal file
View File

@@ -0,0 +1,364 @@
#!/usr/bin/env python3
"""
Тестирование гибридного аудита для отеля "Комфорт" (Камчатка)
Сравнение с результатами n8n AI Agent
"""
import psycopg2
from psycopg2.extras import RealDictCursor
import requests
import json
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"
# ID отеля Комфорт
HOTEL_ID = "303958ee-c607-11ef-92da-f9f9e6a4072b"
# Инициализация Natasha
print("🔧 Инициализация Natasha...")
segmenter = Segmenter()
morph_vocab = MorphVocab()
emb = NewsEmbedding()
morph_tagger = NewsMorphTagger(emb)
syntax_parser = NewsSyntaxParser(emb)
ner_tagger = NewsNERTagger(emb)
print("✅ Natasha готова!\n")
# Результаты от n8n AI Agent для сравнения
N8N_RESULTS = {
1: {"score": 0.0, "found": False},
2: {"score": 1.0, "found": True, "url": "https://hotelcomfort41.ru/o-kompanii"},
3: {"score": 0.5, "found": True},
4: {"score": 1.0, "found": True, "url": "https://hotelcomfort41.ru"},
5: {"score": 0.5, "found": True},
7: {"score": 1.0, "found": True},
8: {"score": 0.0, "found": False},
9: {"score": 0.5, "found": True},
10: {"score": 0.0, "found": False},
11: {"score": 0.0, "found": False},
12: {"score": 1.0, "found": True},
13: {"score": 0.0, "found": False},
14: {"score": 0.0, "found": False},
15: {"score": 0.0, "found": False},
16: {"score": 0.0, "found": False},
17: {"score": 0.0, "found": False},
18: {"score": 0.0, "found": False}
}
def get_db_connection():
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
def get_all_hotel_text(hotel_id: str) -> list:
"""Получить весь текст отеля из chunks"""
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
SELECT text, metadata->>'url' as url
FROM hotel_website_chunks
WHERE metadata->>'hotel_id' = %s
ORDER BY id;
""", (hotel_id,))
chunks = cur.fetchall()
cur.close()
conn.close()
return chunks
def check_criterion_with_regex(chunks: list, criterion: dict) -> dict:
"""Проверка критерия регулярными выражениями"""
result = {
'found': False,
'matches': [],
'urls': [],
'quotes': []
}
patterns = criterion.get('patterns', [])
keywords = criterion.get('keywords', [])
for chunk in chunks:
text = chunk['text']
url = chunk.get('url', 'Нет URL')
# Проверяем регулярки
for pattern in patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
if matches:
result['found'] = True
result['matches'].extend(matches[:3])
if url not in result['urls']:
result['urls'].append(url)
# Находим контекст
match_obj = re.search(pattern, text, re.IGNORECASE)
if match_obj:
idx = match_obj.start()
start = max(0, idx - 100)
end = min(len(text), idx + 200)
quote = text[start:end].strip()
result['quotes'].append({
'url': url,
'quote': quote,
'match': matches[0]
})
# Проверяем ключевые слова
if not result['found']:
text_lower = text.lower()
for keyword in keywords:
if keyword.lower() in text_lower:
result['found'] = True
if url not in result['urls']:
result['urls'].append(url)
# Находим контекст вокруг ключевого слова
idx = text_lower.find(keyword.lower())
if idx >= 0:
start = max(0, idx - 100)
end = min(len(text), idx + 200)
quote = text[start:end].strip()
result['quotes'].append({
'url': url,
'quote': quote,
'match': keyword
})
break
return result
# 17 критериев с регулярками
CRITERIA = [
{
'id': 1,
'name': 'Юридическая идентификация и верификация',
'patterns': [
r'\b\d{10}\b', # ИНН юр.лица
r'\b\d{12}\b', # ИНН ИП
r'\b\d{13}\b', # ОГРН
r'инн\s*:?\s*\d{10,12}',
r'огрн\s*:?\s*\d{13}',
],
'keywords': ['инн', 'огрн', 'егрюл', 'реквизиты']
},
{
'id': 2,
'name': 'Адрес',
'patterns': [
r'\d{6}.*?ул\.',
r'ул\.\s*[А-Яа-яёЁA-Za-z\s]+,?\s*\d+',
r'\d{6},?\s*г\.\s*[А-Яа-яёЁ-]+',
],
'keywords': ['адрес', 'местонахождение', 'г.', 'ул.']
},
{
'id': 3,
'name': 'Контакты',
'patterns': [
r'(?:\+7|8)\s*\(?\d{3,5}\)?\s*\d{1,3}[-\s]?\d{2}[-\s]?\d{2}',
r'[\w\.-]+@[\w\.-]+\.\w{2,}',
],
'keywords': ['телефон', 'email', 'контакт']
},
{
'id': 4,
'name': 'Режим работы',
'patterns': [
r'(?:с|с\s+)\d{1,2}(?::|\.)\d{2}\s*(?:до|по)\s*\d{1,2}(?::|\.)\d{2}',
r'круглосуточно',
r'24\s*[/\-]\s*7',
],
'keywords': ['режим работы', 'часы работы', 'график']
},
{
'id': 5,
'name': 'Политика ПДн (152-ФЗ)',
'patterns': [
r'152[-\s]?фз',
r'политика\s+в\s+отношении\s+обработки\s+персональных\s+данных',
],
'keywords': ['персональных данных', 'пдн', '152-фз', 'политика конфиденциальности']
},
{
'id': 7,
'name': 'Договор-оферта / Правила оказания услуг',
'patterns': [
r'публичная\s+оферта',
r'договор.*?оказани.*?услуг',
r'пользовательское\s+соглашение',
],
'keywords': ['оферта', 'договор', 'правила', 'соглашение']
},
{
'id': 8,
'name': 'Рекламации и споры',
'patterns': [],
'keywords': ['рекламация', 'претензия', 'жалоба', 'спор']
},
{
'id': 9,
'name': 'Цены/прайс',
'patterns': [
r'\d+\s*(?:руб|₽)',
r'(?:от|цена|стоимость)\s*\d+',
],
'keywords': ['цена', 'прайс', 'стоимость', 'тариф']
},
{
'id': 10,
'name': 'Способы оплаты',
'patterns': [],
'keywords': ['оплата картой', 'наличные', 'безналичный', 'карта', 'visa', 'mastercard']
},
{
'id': 11,
'name': 'Онлайн-оплата',
'patterns': [],
'keywords': ['онлайн оплата', 'оплатить онлайн', 'эквайринг']
},
{
'id': 12,
'name': 'Онлайн-бронирование',
'patterns': [],
'keywords': ['забронировать', 'бронирование', 'booking', 'форма заявки']
},
{
'id': 13,
'name': 'FAQ',
'patterns': [],
'keywords': ['faq', 'частые вопросы', 'часто задаваемые']
},
{
'id': 14,
'name': 'Доступность для ЛОВЗ',
'patterns': [],
'keywords': ['ловз', 'инвалид', 'доступность', 'безбарьерная']
},
{
'id': 15,
'name': 'Партнёры/бренды',
'patterns': [],
'keywords': ['партнер', 'партнёр', 'бренд', 'сотрудничество']
},
{
'id': 16,
'name': 'Команда/сотрудники',
'patterns': [],
'keywords': ['команда', 'сотрудник', 'персонал', 'руководство']
},
{
'id': 17,
'name': 'Уголок потребителя',
'patterns': [],
'keywords': ['уголок потребителя', 'права потребителя', 'защита прав']
},
{
'id': 18,
'name': 'Актуальность документов',
'patterns': [
r'\d{4}[-/.]\d{1,2}[-/.]\d{1,2}', # Даты
r'обновлено',
r'актуализировано',
],
'keywords': ['дата обновления', 'актуально', 'обновлено']
}
]
print('🏨 ТЕСТИРОВАНИЕ ОТЕЛЯ: Городской отель \"Комфорт\" (Камчатский край)')
print('='*80)
print(f'ID отеля: {HOTEL_ID}')
print()
# Получаем chunks
chunks = get_all_hotel_text(HOTEL_ID)
print(f'📊 Загружено {len(chunks)} chunks')
print()
# Проверяем каждый критерий
print('🔍 ПРОВЕРКА ПО РЕГУЛЯРКАМИ И КЛЮЧЕВЫМ СЛОВАМ:')
print('='*80)
total_score = 0.0
for criterion in CRITERIA:
crit_id = criterion['id']
crit_name = criterion['name']
print(f'\n{crit_id}. {crit_name}')
print('-'*80)
result = check_criterion_with_regex(chunks, criterion)
# Оценка
score = 0.0
if result['found'] and result['matches']:
score = 1.0
elif result['found']:
score = 0.5
total_score += score
# n8n результат для сравнения
n8n_score = N8N_RESULTS.get(crit_id, {}).get('score', 0.0)
print(f'Регулярки: {score}/1.0')
print(f'n8n AI: {n8n_score}/1.0')
if score != n8n_score:
diff = score - n8n_score
if diff > 0:
print(f'✅ Регулярки ЛУЧШЕ на {diff:.1f}')
else:
print(f'❌ n8n AI ЛУЧШЕ на {-diff:.1f}')
else:
print('🟰 Одинаково')
if result['found']:
print(f'Найдено совпадений: {len(result["matches"])}')
if result['matches']:
print(f'Примеры: {result["matches"][:3]}')
if result['urls']:
print(f'URL: {result["urls"][0]}')
if result['quotes']:
quote_text = result['quotes'][0]['quote'][:150]
print(f'Цитата: {quote_text}...')
else:
print('Не найдено')
print()
print('='*80)
print(f'📊 ИТОГО:')
print(f'Регулярки: {total_score}/17 ({total_score/17*100:.1f}%)')
print(f'n8n AI: 6.0/17 (35.3%)')
print()
if total_score > 6.0:
print(f'✅ Регулярки лучше на {total_score - 6.0:.1f} баллов!')
elif total_score < 6.0:
print(f'❌ n8n AI лучше на {6.0 - total_score:.1f} баллов!')
else:
print('🟰 Результаты одинаковые!')