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

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

930
audit_system_new.py Normal file
View File

@@ -0,0 +1,930 @@
#!/usr/bin/env python3
"""
Система аудита отелей по 18 критериям с новой логикой оценки доступности информации
- 1.0 балл: прямая ссылка на страницу (видна с главной)
- 0.5 балла: информация найдена, но спрятана глубоко
- 0 баллов: информация не найдена
"""
import psycopg2
from psycopg2.extras import Json
import requests
import json
import logging
from datetime import datetime
from urllib.parse import unquote
from typing import List, Dict, Optional
import pandas as pd
import re
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
DB_CONFIG = {
'host': "147.45.189.234",
'port': 5432,
'database': "default_db",
'user': "gen_user",
'password': unquote("2~~9_%5EkVsU%3F2%5CS")
}
# 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}', # ИНН: 1234567890 или 123456789012
r'огрн\s*:?\s*\d{13}', # ОГРН: 1234567890123
r'огрнип\s*:?\s*\d{15}', # ОГРНИП: 123456789012345
],
'direct_link_patterns': ['реквизиты', 'о компании', 'контакты', 'сведения', 'информация', 'about', 'company', 'details']
},
{
'id': 2,
'name': 'Адрес',
'query': 'юридический адрес фактический адрес местонахождение',
'keywords': ['адрес', 'address', 'местонахождение', 'г.', 'ул.'],
'priority_patterns': [
r'\d{6}.*?ул\.', # Индекс (689251) + что-то + ул.
r'ул\.\s*[А-Яа-яёЁA-Za-z\s]+,?\s*\d+', # ул. Название, дом
],
'direct_link_patterns': ['адрес', 'контакты', 'где мы', 'местоположение', 'address', 'location', 'contacts', 'найти нас']
},
{
'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}', # Телефон: +7(xxx)xxx-xx-xx или 8(xxx)xxx-xx-xx
r'[\w\.-]+@[\w\.-]+\.\w{2,}', # Email: name@domain.com
],
'direct_link_patterns': ['контакты', 'связаться', 'телефон', 'email', 'contacts', 'contact', 'связь']
},
{
'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'(?:с|с\s+)\d{1,2}\s+(?:до|по)\s+\d{1,2}(?:\s+час)', # с 9 до 18 часов (с обязательным "час")
r'\d{1,2}:\d{2}\s*-\s*\d{1,2}:\d{2}', # 9:00 - 18:00
r'круглосуточно', # круглосуточно
r'24\s*[/\-]\s*7', # 24/7
],
'direct_link_patterns': ['режим работы', 'часы работы', 'график работы', 'working hours', 'schedule', 'время работы'],
'require_patterns': True # Требуем обязательное наличие паттернов времени
},
{
'id': 5,
'name': 'Политика ПДн (152-ФЗ)',
'query': 'политика персональных данных обработка ПДн 152-ФЗ',
'keywords': ['персональных данных', 'пдн', '152-фз', 'privacy'],
'priority_patterns': [
r'политика\s+в\s+отношении\s+обработки\s+персональных\s+данных', # Точное название документа
r'152[-\s]?фз', # 152-ФЗ
r'федеральный\s+закон.*?персональных\s+данных', # Федеральный закон о персональных данных
],
'direct_link_patterns': ['политика', 'персональные данные', 'конфиденциальность', 'privacy', 'policy', '152-фз']
},
{
'id': 6,
'name': 'Роскомнадзор (реестр)',
'query': 'реестр операторов персональных данных Роскомнадзор',
'keywords': ['роскомнадзор', 'реестр оператор'],
'direct_link_patterns': ['роскомнадзор', 'реестр', 'регистрация', 'registry', 'ркн']
},
{
'id': 7,
'name': 'Договор-оферта / Правила оказания услуг',
'query': 'публичная оферта договор пользовательское соглашение условия оказания услуг правила проживания бронирования размещения посещения',
'keywords': [
# Договор/оферта
'публичная оферта', 'публичный договор', 'договор оказания услуг', 'договор на оказание',
'пользовательское соглашение', 'условия договора',
# Правила/условия (специфичные для отелей)
'условия проживания', 'правила проживания', 'условия размещения', 'правила размещения',
'условия бронирования', 'правила отмены', 'условия оказания услуг', 'входит в стоимость',
'правила оказания услуг', 'порядок оказания услуг',
# Универсальные (для любых организаций)
'правила посещения', 'правила использования', 'правила и условия',
'terms and conditions', 'terms of service', 'terms of use'
],
'priority_patterns': [
r'правила\s+посещения\s+(?:парка|территории|объекта)', # "Правила посещения" как заголовок (не в меню)
r'публичная\s+оферта', # Публичная оферта
r'договор.*?оказани.*?услуг', # Договор оказания услуг
],
'direct_link_patterns': ['правила', 'условия', 'договор', 'оферта', 'правила посещения', 'rules', 'terms', 'условия оказания услуг']
},
{
'id': 8,
'name': 'Рекламации и споры',
'query': 'претензия рекламация споры возврат обмен гарантия жалоба',
'keywords': ['претензия', 'претензии', 'рекламация', 'рекламации', 'возврат средств', 'возврат денег', 'порядок рассмотрения споров', 'досудебный порядок', 'жалоба', 'жалобы', 'книга жалоб'],
'direct_link_patterns': ['рекламации', 'претензии', 'споры', 'жалобы', 'complaints', 'disputes', 'возврат']
},
{
'id': 9,
'name': 'Цены/прайс',
'query': 'цены прайс стоимость тарифы',
'keywords': ['цен', 'руб', '', 'price', 'стоимость', 'тариф', 'платн'],
'priority_urls': ['price', 'prices', 'ceny', 'tarif', 'tariff', 'stoimost'],
'direct_link_patterns': ['цены', 'прайс', 'тарифы', 'стоимость', 'price', 'tariff', 'rates', 'прайс-лист']
},
{
'id': 10,
'name': 'Способы оплаты',
'query': 'способы оплаты наличные карта СБП оплата банковская карта',
'keywords': ['способы оплаты', 'методы оплаты', 'оплата картой', 'банковская карта', 'банковские карты', 'наличные', 'наличными', 'сбп', 'qr-код', 'visa', 'mastercard', 'мир'],
'direct_link_patterns': ['оплата', 'способы оплаты', 'payment', 'pay', 'как оплатить']
},
{
'id': 11,
'name': 'Онлайн-оплата',
'query': 'онлайн оплата эквайринг оплатить online payment',
'keywords': ['онлайн оплат', 'эквайринг', 'оплатить'],
'direct_link_patterns': ['онлайн оплата', 'оплатить онлайн', 'online payment', 'pay online', 'эквайринг']
},
{
'id': 12,
'name': 'Онлайн-бронирование',
'query': 'онлайн бронирование забронировать booking',
'keywords': ['бронирован', 'забронировать', 'booking'],
'direct_link_patterns': ['бронирование', 'забронировать', 'booking', 'reserve', 'онлайн бронирование']
},
{
'id': 13,
'name': 'FAQ',
'query': 'FAQ частые вопросы вопрос-ответ часто задаваемые',
'keywords': ['faq', 'частые вопросы', 'часто задаваемые вопросы', 'часто задаваемые', 'вопрос-ответ', 'вопросы и ответы'],
'priority_urls': ['faq', 'chasto-zadavaemye', 'voprosy', 'questions'],
'direct_link_patterns': ['faq', 'частые вопросы', 'вопросы', 'questions', 'help', 'помощь']
},
{
'id': 14,
'name': 'Доступность для ЛОВЗ',
'query': 'доступность инвалиды ЛОВЗ безбарьерная среда маломобильные',
'keywords': ['для инвалидов', 'для лиц с ограниченными возможностями', 'ловз', 'доступная среда', 'безбарьерная среда', 'маломобильные граждане', 'пандус', 'поручни', 'доступность для инвалидов', 'специальные условия'],
'direct_link_patterns': ['доступность', 'для инвалидов', 'ловз', 'accessibility', 'безбарьерная среда']
},
{
'id': 15,
'name': 'Партнёры/бренды',
'query': 'партнеры поставщики бренды сотрудничество',
'keywords': ['партнер', 'поставщик', 'бренд'],
'priority_urls': ['partner', 'partners', 'partnery', 'brand', 'brands', 'sotrudnichestvo'],
'direct_link_patterns': ['партнеры', 'бренды', 'сотрудничество', 'partners', 'brands', 'поставщики']
},
{
'id': 16,
'name': 'Команда/сотрудники',
'query': 'команда сотрудники персонал руководство',
'keywords': ['команда', 'сотрудник', 'персонал', 'руководство'],
'priority_urls': ['sotrudniki', 'staff', 'team', 'komanda', 'personal', 'about-us', 'o-nas'],
'direct_link_patterns': ['сотрудники', 'команда', 'персонал', 'о нас', 'staff', 'team', 'about', 'руководство']
},
{
'id': 17,
'name': 'Уголок потребителя',
'query': 'уголок потребителя права закон защита потребителей',
'keywords': ['уголок потребител', 'права потребител', 'защита потребител'],
'direct_link_patterns': ['уголок потребителя', 'права потребителей', 'consumer', 'защита прав']
},
{
'id': 18,
'name': 'Актуальность документов',
'query': 'дата обновления дата публикации актуально версия',
'keywords': ['дата обновления', 'дата публикации', 'обновлено', 'опубликовано', 'актуально на', 'версия от', 'дата создания', 'последнее обновление'],
'direct_link_patterns': ['актуальность', 'обновления', 'версия', 'дата', 'updates', 'version', 'последнее обновление']
}
]
class AuditSystem:
def __init__(self, region_name: str, group_id: str):
self.region_name = region_name
self.group_id = group_id
self.conn = None
def connect_db(self):
self.conn = psycopg2.connect(**DB_CONFIG)
def create_audit_table(self):
"""Создать таблицу для результатов аудита"""
cur = self.conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS hotel_audit_results (
id SERIAL PRIMARY KEY,
hotel_id UUID REFERENCES hotel_main(id),
region_name TEXT,
hotel_name TEXT,
website TEXT,
has_website BOOLEAN,
-- Результаты по 18 критериям
criteria_results JSONB,
-- Общие метрики
total_score FLOAT, -- Сумма баллов (может быть дробной)
max_score FLOAT DEFAULT 18.0,
score_percentage FLOAT,
-- Мета
audit_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
audit_version TEXT,
UNIQUE(hotel_id, audit_version)
);
CREATE INDEX IF NOT EXISTS idx_audit_region ON hotel_audit_results(region_name);
CREATE INDEX IF NOT EXISTS idx_audit_score ON hotel_audit_results(total_score);
""")
self.conn.commit()
cur.close()
logger.info("✓ Таблица hotel_audit_results готова")
def check_direct_links(self, hotel_id: str, criterion: dict) -> dict:
"""Проверка прямых ссылок на главной странице - 1.0 балл"""
cur = self.conn.cursor()
# Получаем URL главной страницы
cur.execute("""
SELECT website_address FROM hotel_main WHERE id = %s
""", (hotel_id,))
result = cur.fetchone()
if not result:
cur.close()
return None
main_url = result[0]
if not main_url:
cur.close()
return None
# Ищем прямые ссылки на главной странице
patterns = criterion.get('direct_link_patterns', [])
for pattern in patterns:
cur.execute("""
SELECT cleaned_text, url
FROM hotel_website_processed
WHERE hotel_id = %s
AND url = %s
AND LOWER(cleaned_text) LIKE %s
LIMIT 1
""", (hotel_id, main_url, f'%{pattern}%'))
result = cur.fetchone()
if result:
text_row, url_row = result
# Проверяем что на главной странице есть ссылка на нужную информацию
if any(keyword in text_row.lower() for keyword in criterion['keywords']):
cur.close()
return {
'score': 1.0,
'verdict': 'ПРЯМАЯ_ССЫЛКА',
'confidence': 0.95,
'found_keywords': [pattern],
'approval_urls': [url_row],
'approval_quotes': [{
'url': url_row,
'quote': f"Найдена прямая ссылка: {pattern}",
'keyword': pattern
}]
}
cur.close()
return None
def check_deep_search(self, hotel_id: str, criterion: dict) -> dict:
"""Поиск информации в глубине сайта - 0.5 балла"""
cur = self.conn.cursor()
# Проверяем required_patterns для критерия 1 (юридическая идентификация)
found_required_patterns = []
if criterion.get('required_patterns'):
cur.execute("""
SELECT cleaned_text FROM hotel_website_processed
WHERE hotel_id = %s
""", (hotel_id,))
all_text = ""
for row in cur.fetchall():
all_text += " " + row[0]
for pattern in criterion['required_patterns']:
if re.search(pattern, all_text, re.IGNORECASE):
found_required_patterns.append(pattern)
# Если есть required_patterns и они найдены - 0.5 балла
if found_required_patterns:
approval_urls = []
approval_quotes = []
for pattern in found_required_patterns:
cur.execute("""
SELECT cleaned_text, url
FROM hotel_website_processed
WHERE hotel_id = %s AND cleaned_text ~* %s
LIMIT 1
""", (hotel_id, pattern))
result = cur.fetchone()
if result:
text_row, url_row = result
approval_urls.append(url_row)
# Ищем точное вхождение паттерна
match = re.search(pattern, text_row, re.IGNORECASE)
if match:
idx = match.start()
start = max(0, idx - 100)
end = min(len(text_row), idx + 200)
quote_text = text_row[start:end].strip()
else:
quote_text = text_row[:200] + "..."
approval_quotes.append({
'url': url_row,
'quote': quote_text,
'keyword': f"Pattern: {pattern}"
})
cur.close()
return {
'score': 0.5,
'verdict': 'СКРЫТО',
'confidence': 0.8,
'found_keywords': found_required_patterns,
'approval_urls': approval_urls,
'approval_quotes': approval_quotes
}
# Ищем ключевые слова
found_keywords = []
for keyword in criterion['keywords']:
# Экранируем специальные символы для PostgreSQL regex
escaped_keyword = re.escape(keyword.lower())
cur.execute("""
SELECT COUNT(*) FROM hotel_website_processed
WHERE hotel_id = %s AND LOWER(cleaned_text) ~ %s
""", (hotel_id, r'\y' + escaped_keyword + r'\y'))
if cur.fetchone()[0] > 0:
found_keywords.append(keyword)
if not found_keywords:
cur.close()
return {
'score': 0.0,
'verdict': 'НЕ_НАЙДЕНО',
'confidence': 0.0,
'found_keywords': [],
'approval_urls': [],
'approval_quotes': []
}
# ПРИОРИТЕТ: Ищем по priority_patterns (более специфичные)
found_priority_patterns = []
approval_urls = []
approval_quotes = []
if criterion.get('priority_patterns'):
cur.execute("""
SELECT cleaned_text FROM hotel_website_processed
WHERE hotel_id = %s
""", (hotel_id,))
all_text = ""
for row in cur.fetchall():
all_text += " " + row[0]
for pattern in criterion['priority_patterns']:
if re.search(pattern, all_text, re.IGNORECASE):
found_priority_patterns.append(pattern)
if found_priority_patterns and not approval_urls:
for pattern in found_priority_patterns:
cur.execute("""
SELECT cleaned_text, url
FROM hotel_website_processed
WHERE hotel_id = %s AND cleaned_text ~* %s
LIMIT 1
""", (hotel_id, pattern))
result = cur.fetchone()
if result:
text_row, url_row = result
approval_urls.append(url_row)
# Ищем точное вхождение паттерна
match = re.search(pattern, text_row, re.IGNORECASE)
if match:
idx = match.start()
start = max(0, idx - 100)
end = min(len(text_row), idx + 200)
quote_text = text_row[start:end].strip()
else:
quote_text = text_row[:200] + "..."
approval_quotes.append({
'url': url_row,
'quote': quote_text,
'keyword': f"Pattern: {pattern}"
})
# ПРИОРИТЕТ: Ищем по priority_urls (специальные страницы)
if criterion.get('priority_urls') and not approval_urls:
for priority_path in criterion['priority_urls']:
cur.execute("""
SELECT cleaned_text, url
FROM hotel_website_processed
WHERE hotel_id = %s AND url LIKE %s
LIMIT 1
""", (hotel_id, f'%{priority_path}%'))
result = cur.fetchone()
if result:
text_row, url_row = result
# Проверяем что на этой странице есть хоть одно ключевое слово
text_lower = text_row.lower()
for kw in found_keywords:
pattern = r'\b' + re.escape(kw.lower()) + r'\b'
if re.search(pattern, text_lower, re.IGNORECASE):
approval_urls.append(url_row)
# Ищем точное вхождение ключевого слова
match = re.search(r'\b' + re.escape(kw.lower()) + r'\b', text_lower, re.IGNORECASE)
if match:
idx = match.start()
start = max(0, idx - 100)
end = min(len(text_row), idx + 200)
quote_text = text_row[start:end].strip()
else:
quote_text = text_row[:200] + "..."
approval_quotes.append({
'url': url_row,
'quote': quote_text,
'keyword': kw
})
break
if approval_urls:
break # Нашли на приоритетной странице - хватит
# Если не нашли по priority_patterns или priority_urls - ищем по keywords
if not approval_urls:
for keyword in found_keywords:
# Экранируем специальные символы для PostgreSQL regex
escaped_keyword = re.escape(keyword.lower())
cur.execute("""
SELECT cleaned_text, url
FROM hotel_website_processed
WHERE hotel_id = %s AND LOWER(cleaned_text) ~ %s
LIMIT 1
""", (hotel_id, r'\y' + escaped_keyword + r'\y'))
result = cur.fetchone()
if result:
text_row, url_row = result
approval_urls.append(url_row)
# Ищем точное вхождение ключевого слова
match = re.search(r'\b' + re.escape(keyword.lower()) + r'\b', text_row, re.IGNORECASE)
if match:
idx = match.start()
start = max(0, idx - 100)
end = min(len(text_row), idx + 200)
quote_text = text_row[start:end].strip()
else:
quote_text = text_row[:200] + "..."
approval_quotes.append({
'url': url_row,
'quote': quote_text,
'keyword': keyword
})
break # Нашли первое вхождение - хватит
cur.close()
# Если нашли информацию - возвращаем 0.5 балла
if approval_urls:
return {
'score': 0.5,
'verdict': 'СКРЫТО',
'confidence': 0.7,
'found_keywords': found_keywords,
'approval_urls': approval_urls,
'approval_quotes': approval_quotes
}
else:
return {
'score': 0.0,
'verdict': 'НЕ_НАЙДЕНО',
'confidence': 0.0,
'found_keywords': [],
'approval_urls': [],
'approval_quotes': []
}
def check_criterion_simple(self, hotel_id: str, criterion: dict) -> dict:
"""
Новая логика оценки доступности информации:
- 1.0 балл: прямая ссылка на страницу (видна с главной)
- 0.5 балла: информация найдена, но спрятана глубоко
- 0 баллов: информация не найдена
"""
# ОСОБАЯ ЛОГИКА ДЛЯ КРИТЕРИЕВ С ОБЯЗАТЕЛЬНЫМИ ПАТТЕРНАМИ
# (Критерий 1: ИНН/ОГРН, Критерий 4: режим работы с временем)
if criterion.get('require_patterns') or criterion['id'] == 1:
deep_search_score = self.check_deep_search(hotel_id, criterion)
# Если нашли required_patterns или priority_patterns - это уже хорошо
if deep_search_score['score'] > 0:
# Проверяем есть ли прямая ссылка на главной
direct_links_score = self.check_direct_links(hotel_id, criterion)
if direct_links_score:
# Есть прямая ссылка И найдены паттерны - 1.0 балл
# Но апрув URL берём из deep_search (где реально найдена информация)
return {
'score': 1.0,
'verdict': 'ПРЯМАЯ_ССЫЛКА',
'confidence': 0.95,
'found_keywords': deep_search_score['found_keywords'],
'approval_urls': deep_search_score['approval_urls'],
'approval_quotes': deep_search_score['approval_quotes']
}
else:
# Паттерны найдены, но нет прямой ссылки - 0.5 балла
return deep_search_score
else:
# Паттерны не найдены
return deep_search_score
# Для остальных критериев - стандартная логика
# Сначала проверяем прямые ссылки на главной странице
direct_links_score = self.check_direct_links(hotel_id, criterion)
if direct_links_score:
return direct_links_score
# Затем ищем информацию в глубине сайта
deep_search_score = self.check_deep_search(hotel_id, criterion)
return deep_search_score
def check_rkn_registry(self, hotel_id: str) -> dict:
"""Проверка статуса в реестре РКН"""
cur = self.conn.cursor()
cur.execute("""
SELECT rkn_registry_status, rkn_registry_number, rkn_registry_date
FROM hotel_main
WHERE id = %s
""", (hotel_id,))
result = cur.fetchone()
cur.close()
if not result:
return {
'status': 'not_found',
'number': None,
'date': None,
'approval_url': 'https://pd.rkn.gov.ru/operators-registry/operators-list/'
}
status, number, date = result
if status == 'found' and number:
approval_url = f'https://pd.rkn.gov.ru/operators-registry/operators-list/?operator={number}'
return {
'status': 'found',
'number': number,
'date': date,
'approval_url': approval_url
}
else:
return {
'status': 'not_found',
'number': None,
'date': None,
'approval_url': 'https://pd.rkn.gov.ru/operators-registry/operators-list/'
}
def audit_hotel_with_website(self, hotel_id: str, hotel_name: str, website: str) -> dict:
"""Аудит отеля с сайтом"""
logger.info(f"🔍 Аудит: {hotel_name}")
criteria_results = {}
total_score = 0.0
for criterion in AUDIT_CRITERIA:
logger.info(f" Критерий {criterion['id']}: {criterion['name']}")
if criterion['id'] == 6: # Роскомнадзор - специальная обработка
rkn_result = self.check_rkn_registry(hotel_id)
if rkn_result['status'] == 'found':
result = {
'score': 1.0,
'verdict': 'ПРЯМАЯ_ССЫЛКА',
'confidence': 0.95,
'found_keywords': ['РКН реестр'],
'approval_urls': [rkn_result['approval_url']],
'approval_quotes': [{
'url': rkn_result['approval_url'],
'quote': f"Найден в реестре РКН: {rkn_result['number']} от {rkn_result['date']}",
'keyword': 'РКН реестр'
}]
}
else:
result = {
'score': 0.0,
'verdict': 'НЕ_НАЙДЕНО',
'confidence': 0.0,
'found_keywords': [],
'approval_urls': [rkn_result['approval_url']],
'approval_quotes': []
}
else:
result = self.check_criterion_simple(hotel_id, criterion)
criteria_results[criterion['id']] = {
'name': criterion['name'],
'score': result['score'],
'verdict': result['verdict'],
'confidence': result.get('confidence', 0.0),
'found_keywords': result.get('found_keywords', []),
'approval_urls': result.get('approval_urls', []),
'approval_quotes': result.get('approval_quotes', [])
}
total_score += result['score']
score_percentage = (total_score / 18.0) * 100 if total_score > 0 else 0
logger.info(f" ✅ Сайт есть: {total_score}/18.0 критериев ({score_percentage:.1f}%)")
return {
'has_website': True,
'total_score': total_score,
'score_percentage': score_percentage,
'criteria_results': criteria_results
}
def audit_hotel_no_website(self, hotel_id: str, hotel_name: str) -> dict:
"""Аудит отеля без сайта - все критерии 0"""
logger.info(f" ❌ Сайта нет: 0/18 (автоматически)")
criteria_results = {}
for criterion in AUDIT_CRITERIA:
criteria_results[criterion['id']] = {
'name': criterion['name'],
'score': 0.0,
'verdict': 'НЕ_НАЙДЕНО',
'confidence': 0.0,
'found_keywords': [],
'approval_urls': [],
'approval_quotes': []
}
return {
'has_website': False,
'total_score': 0.0,
'score_percentage': 0.0,
'criteria_results': criteria_results
}
def run_audit(self, force_audit: bool = False):
"""Запуск аудита региона"""
logger.info(f"🚀 Запуск аудита: {self.region_name}")
self.connect_db()
self.create_audit_table()
cur = self.conn.cursor()
# Получаем отели региона
cur.execute("""
SELECT id, full_name, website_address,
CASE WHEN website_address IS NOT NULL AND website_address != '' THEN true ELSE false END as has_website
FROM hotel_main
WHERE region_name = %s
ORDER BY full_name
""", (self.region_name,))
hotels = cur.fetchall()
total_hotels = len(hotels)
with_websites = sum(1 for _, _, _, has_website in hotels if has_website)
without_websites = total_hotels - with_websites
logger.info(f"📊 Отелей: {total_hotels}")
logger.info(f" С сайтами: {with_websites} ({with_websites/total_hotels*100:.1f}%)")
logger.info(f" БЕЗ сайтов: {without_websites} ({without_websites/total_hotels*100:.1f}%)")
logger.info("")
# Проверяем есть ли уже результаты аудита
if not force_audit:
cur.execute("""
SELECT COUNT(*) FROM hotel_audit_results
WHERE region_name = %s AND audit_version = %s
""", (self.region_name, self.group_id))
if cur.fetchone()[0] > 0:
logger.info("⚠️ Результаты аудита уже существуют. Используйте force_audit=True для перезапуска")
cur.close()
return
# Аудит каждого отеля
for i, (hotel_id, hotel_name, website, has_website) in enumerate(hotels, 1):
logger.info(f"[{i}/{total_hotels}] {hotel_name}")
if has_website and website:
audit_result = self.audit_hotel_with_website(hotel_id, hotel_name, website)
else:
audit_result = self.audit_hotel_no_website(hotel_id, hotel_name)
# Сохраняем результат
cur.execute("""
INSERT INTO hotel_audit_results
(hotel_id, region_name, hotel_name, website, has_website,
criteria_results, total_score, score_percentage, audit_version)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (hotel_id, audit_version)
DO UPDATE SET
criteria_results = EXCLUDED.criteria_results,
total_score = EXCLUDED.total_score,
score_percentage = EXCLUDED.score_percentage,
audit_date = CURRENT_TIMESTAMP
""", (
hotel_id, self.region_name, hotel_name, website, audit_result['has_website'],
Json(audit_result['criteria_results']), audit_result['total_score'],
audit_result['score_percentage'], self.group_id
))
logger.info("")
self.conn.commit()
cur.close()
logger.info("✅ Аудит завершён!")
def export_to_excel(self, region_name: str):
"""Экспорт результатов в Excel с новой структурой"""
cur = self.conn.cursor()
cur.execute("""
SELECT hotel_name, website, has_website, total_score, score_percentage, criteria_results
FROM hotel_audit_results
WHERE region_name = %s AND audit_version = %s
ORDER BY total_score DESC, hotel_name
""", (region_name, self.group_id))
results = cur.fetchall()
cur.close()
if not results:
logger.error("❌ Нет результатов для экспорта")
return
# Создаем DataFrame
data = []
for hotel_name, website, has_website, total_score, score_percentage, criteria_results in results:
row = {
'Отель': hotel_name,
'Сайт': website or 'Нет сайта',
'Балл': f"{total_score:.1f}",
'Процент': f"{score_percentage:.1f}%"
}
# Парсим criteria_results если это строка JSON
if isinstance(criteria_results, str):
criteria_results = json.loads(criteria_results)
# Добавляем колонки для каждого критерия
for i in range(1, 19): # 18 критериев
# Проверяем как по числовому ключу, так и по строковому
key = str(i) if str(i) in criteria_results else i
if key in criteria_results:
result = criteria_results[key]
row[f'{i}. {result["name"]}'] = result['verdict']
row[f'{i}. Балл'] = f"{result['score']:.1f}"
# Апрув URL
approval_urls = result.get('approval_urls', [])
if approval_urls:
row[f'{i}. Апрув URL'] = approval_urls[0]
else:
row[f'{i}. Апрув URL'] = ''
# Пояснение
quotes = result.get('approval_quotes', [])
if quotes:
row[f'{i}. Пояснение'] = quotes[0].get('quote', '')[:200] + '...' if len(quotes[0].get('quote', '')) > 200 else quotes[0].get('quote', '')
else:
row[f'{i}. Пояснение'] = ''
data.append(row)
df = pd.DataFrame(data)
# Сохраняем в Excel
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"audit_{region_name.replace(' ', '_')}_{timestamp}.xlsx"
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Аудит', index=False)
# Получаем объект рабочей книги для форматирования
workbook = writer.book
worksheet = writer.sheets['Аудит']
# Автоширина колонок
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
logger.info(f"📊 Экспортировано в: {filename}")
return filename
def generate_final_report(self, region_name: str):
"""Генерация итогового отчёта"""
cur = self.conn.cursor()
cur.execute("""
SELECT COUNT(*) as total,
AVG(total_score) as avg_score,
MAX(total_score) as max_score,
MIN(total_score) as min_score
FROM hotel_audit_results
WHERE region_name = %s AND audit_version = %s
""", (region_name, self.group_id))
stats = cur.fetchone()
cur.execute("""
SELECT hotel_name, website, total_score
FROM hotel_audit_results
WHERE region_name = %s AND audit_version = %s
ORDER BY total_score DESC
LIMIT 10
""", (region_name, self.group_id))
top_hotels = cur.fetchall()
cur.close()
print("\n" + "="*70)
print(f"📊 ИТОГОВЫЙ ОТЧЁТ: {region_name.upper()}")
print("="*70)
print(f"\n📈 ОБЩАЯ СТАТИСТИКА:")
print(f" Всего отелей проверено: {stats[0]}")
print(f" Средний балл: {stats[1]:.1f}/18.0 ({stats[1]/18*100:.1f}%)")
print(f" Лучший результат: {stats[2]:.1f}/18.0")
print(f" Худший результат: {stats[3]:.1f}/18.0")
print(f"\n🏨 ТОП-10 ОТЕЛЕЙ:")
for i, (name, website, score) in enumerate(top_hotels, 1):
status = "🌐" if website else ""
print(f" {status} {name[:50]}")
print(f" Сайт: {website or 'Нет сайта'}")
print(f" Балл: {score:.1f}/18.0")
print("\n" + "="*70)
def main():
import sys
if len(sys.argv) < 2:
print("Использование: python audit_system_new.py <регион> [group_id]")
sys.exit(1)
region_name = sys.argv[1]
group_id = sys.argv[2] if len(sys.argv) > 2 else f"AUDIT_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
audit = AuditSystem(region_name, group_id)
audit.run_audit()
audit.export_to_excel(region_name)
audit.generate_final_report(region_name)
if __name__ == "__main__":
main()