931 lines
44 KiB
Python
931 lines
44 KiB
Python
|
|
#!/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()
|