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

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

View File

@@ -0,0 +1,470 @@
#!/usr/bin/env python3
"""
Создание Excel отчета по Орловской области в горизонтальном формате
Лист 1: Дашборд с графиками и статистикой
Лист 2: Детальная таблица аудита (горизонтальный формат)
"""
import psycopg2
from psycopg2.extras import RealDictCursor
from urllib.parse import unquote
import pandas as pd
import openpyxl
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side, NamedStyle
from openpyxl.chart import BarChart, PieChart, LineChart, Reference
from openpyxl.chart.label import DataLabelList
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.drawing.image import Image
from datetime import datetime
import json
DB_CONFIG = {
'host': '147.45.189.234',
'port': 5432,
'database': 'default_db',
'user': 'gen_user',
'password': unquote('2~~9_%5EkVsU%3F2%5CS')
}
def get_orel_data():
"""Получить данные аудита Орловской области версии v1.0_with_rkn"""
conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
cur = conn.cursor()
# Получаем данные аудита Орловской области с информацией об отелях
cur.execute("""
SELECT
har.hotel_id,
har.hotel_name,
har.region_name,
har.website,
har.has_website,
har.total_score,
har.max_score,
har.score_percentage,
har.audit_date,
har.audit_version,
har.criteria_results,
hm.full_name,
hm.website_address,
hm.owner_inn,
hm.owner_ogrn,
hm.addresses,
hm.phone,
hm.email,
hm.website_status,
hm.rkn_registry_status,
hm.rkn_registry_number,
hm.rkn_registry_date,
hm.rkn_checked_at
FROM hotel_audit_results har
LEFT JOIN hotel_main hm ON hm.id = har.hotel_id
WHERE har.region_name = 'Орловская область'
AND har.audit_version = 'v1.0_with_rkn'
ORDER BY har.score_percentage DESC
""")
audit_data = cur.fetchall()
# Статистика по критериям (анализируем criteria_results)
criteria_stats = []
if audit_data:
# Собираем статистику по критериям из всех отелей
criteria_counts = {}
total_hotels = len(audit_data)
for hotel in audit_data:
if hotel['criteria_results']:
criteria = hotel['criteria_results']
for criterion in criteria:
name = criterion.get('criterion_name', 'Неизвестно')
found = criterion.get('found', False)
if name not in criteria_counts:
criteria_counts[name] = {'total': 0, 'found': 0}
criteria_counts[name]['total'] += 1
if found:
criteria_counts[name]['found'] += 1
# Преобразуем в список
for name, counts in criteria_counts.items():
percentage = (counts['found'] / counts['total'] * 100) if counts['total'] > 0 else 0
criteria_stats.append({
'criterion_name': name,
'total_checks': counts['total'],
'found_count': counts['found'],
'percentage': percentage
})
# Сортируем по проценту выполнения
criteria_stats.sort(key=lambda x: x['percentage'], reverse=True)
cur.close()
conn.close()
return audit_data, criteria_stats
def create_dashboard_sheet(workbook, audit_data, criteria_stats):
"""Создать лист дашборда"""
ws = workbook.active
ws.title = "📊 Дашборд Орёл"
# Стили
header_font = Font(name='Arial', size=14, bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
subheader_font = Font(name='Arial', size=12, bold=True)
normal_font = Font(name='Arial', size=10)
# Заголовок
ws['A1'] = "🏛️ ДАШБОРД АУДИТА ОТЕЛЕЙ ОРЛОВСКОЙ ОБЛАСТИ"
ws['A1'].font = Font(name='Arial', size=16, bold=True, color='366092')
ws['A1'].alignment = Alignment(horizontal='center')
ws.merge_cells('A1:H1')
# Общая статистика
ws['A3'] = "📈 ОБЩАЯ СТАТИСТИКА ПО ОРЛОВСКОЙ ОБЛАСТИ"
ws['A3'].font = subheader_font
ws['A3'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Подсчитываем статистику
total_hotels = len(audit_data)
total_with_website = sum(1 for h in audit_data if h['has_website'])
total_without_website = total_hotels - total_with_website
total_in_rkn = sum(1 for h in audit_data if h.get('rkn_registry_number'))
total_compliant = sum(1 for h in audit_data if h['score_percentage'] >= 50)
avg_score = sum(h['score_percentage'] for h in audit_data) / total_hotels if total_hotels > 0 else 0
ws['A4'] = f"Всего отелей в Орловской области: {total_hotels}"
ws['A5'] = f"С сайтами: {total_with_website}"
ws['A6'] = f"Без сайтов: {total_without_website}"
ws['A7'] = f"Сайты доступны для анализа: {total_with_website}"
ws['A8'] = f"Сайты недоступны: 0"
ws['A9'] = f"В реестре РКН: {total_in_rkn}"
ws['A10'] = f"Проведено аудитов: {total_hotels}"
ws['A11'] = f"Средний балл (аудит): {avg_score:.1f}%"
for cell in ['A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11']:
ws[cell].font = normal_font
# Категория
ws['A13'] = "Категория"
ws['A13'].font = subheader_font
ws['A13'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
ws['A14'] = f"Сайты доступны: {total_with_website}"
ws['B14'] = total_with_website
ws['A15'] = f"Сайты недоступны: 0"
ws['B15'] = 0
ws['A16'] = f"Без сайтов: {total_without_website}"
ws['B16'] = total_without_website
ws['A17'] = f"В реестре РКН: {total_in_rkn}"
ws['B17'] = total_in_rkn
for cell in ['A14', 'A15', 'A16', 'A17']:
ws[cell].font = normal_font
# Статистика по критериям
ws['A19'] = "🎯 СТАТИСТИКА ПО КРИТЕРИЯМ"
ws['A19'].font = subheader_font
ws['A19'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Заголовки таблицы критериев
criteria_headers = ['Критерий', 'Найдено', 'Не найдено']
for i, header in enumerate(criteria_headers, 1):
cell = ws.cell(row=20, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# Данные по критериям
for i, criterion in enumerate(criteria_stats, 21):
not_found = criterion['total_checks'] - criterion['found_count']
ws.cell(row=i, column=1, value=criterion['criterion_name'])
ws.cell(row=i, column=2, value=criterion['found_count'])
ws.cell(row=i, column=3, value=not_found)
# Форматирование
for col in range(1, 4):
ws.cell(row=i, column=col).font = normal_font
ws.cell(row=i, column=col).alignment = Alignment(horizontal='center')
# Распределение по баллам
ws['A40'] = "📊 РАСПРЕДЕЛЕНИЕ ПО БАЛЛАМ"
ws['A40'].font = subheader_font
ws['A40'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Заголовки
score_headers = ['Диапазон', 'Количество']
for i, header in enumerate(score_headers, 1):
cell = ws.cell(row=41, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# Данные по баллам
score_ranges = [
('0-25%', sum(1 for h in audit_data if h['score_percentage'] < 26)),
('26-50%', sum(1 for h in audit_data if 26 <= h['score_percentage'] < 51)),
('51-75%', sum(1 for h in audit_data if 51 <= h['score_percentage'] < 76)),
('76-100%', sum(1 for h in audit_data if h['score_percentage'] >= 76))
]
for i, (range_name, count) in enumerate(score_ranges, 42):
ws.cell(row=i, column=1, value=range_name)
ws.cell(row=i, column=2, value=count)
# Форматирование
for col in range(1, 3):
ws.cell(row=i, column=col).font = normal_font
ws.cell(row=i, column=col).alignment = Alignment(horizontal='center')
# Графики
# Круговой график статуса сайтов
pie_chart = PieChart()
pie_chart.title = "Статус сайтов отелей"
# Данные для пирога: Сайты доступны (4), Сайты недоступны (0), Без сайтов (8), В реестре РКН (10)
pie_data = Reference(ws, min_col=2, min_row=14, max_row=17, max_col=2)
pie_labels = Reference(ws, min_col=1, min_row=14, max_row=17, max_col=1)
pie_chart.add_data(pie_data, titles_from_data=False)
pie_chart.set_categories(pie_labels)
pie_chart.height = 10
pie_chart.width = 15
# Добавляем подписи данных
pie_chart.dataLabels = DataLabelList()
pie_chart.dataLabels.showPercent = True
pie_chart.dataLabels.showCategoryName = True
ws.add_chart(pie_chart, "C3")
# Столбчатый график по критериям
chart1 = BarChart()
chart1.title = "Результаты по критериям"
chart1.x_axis.title = "Критерии"
chart1.y_axis.title = "Количество отелей"
data = Reference(ws, min_col=2, min_row=20, max_row=20+len(criteria_stats), max_col=3)
cats = Reference(ws, min_col=1, min_row=21, max_row=20+len(criteria_stats))
chart1.add_data(data, titles_from_data=False)
chart1.set_categories(cats)
chart1.height = 10
chart1.width = 20
ws.add_chart(chart1, "C20")
# График распределения по баллам
chart2 = BarChart()
chart2.title = "Распределение по баллам"
chart2.x_axis.title = "Диапазон баллов"
chart2.y_axis.title = "Количество отелей"
data2 = Reference(ws, min_col=2, min_row=41, max_row=41+len(score_ranges), max_col=2)
cats2 = Reference(ws, min_col=1, min_row=42, max_row=41+len(score_ranges))
chart2.add_data(data2, titles_from_data=False)
chart2.set_categories(cats2)
chart2.height = 8
chart2.width = 12
ws.add_chart(chart2, "C40")
# Настройка ширины колонок
column_widths = [30, 10, 10]
for i, width in enumerate(column_widths, 1):
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = width
def create_audit_sheet(workbook, audit_data):
"""Создать лист детального аудита в горизонтальном формате"""
ws = workbook.create_sheet("🏨 Аудит отелей")
# Стили
header_font = Font(name='Arial', size=10, bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
normal_font = Font(name='Arial', size=8)
# Базовые заголовки
base_headers = ['Отель', 'Сайт', 'Есть сайт', 'Балл', 'Процент']
# Заголовки критериев (по 3 колонки на каждый)
criteria_headers = []
criteria_names = [
"1. Юридическая идентификация и верификация",
"2. Адрес",
"3. Контакты",
"4. Режим работы",
"5. Политика ПДн (152-ФЗ)",
"6. РКН Реестр",
"7. Договор-оферта / Правила оказания услуг",
"8. Рекламации и споры",
"9. Цены/прайс",
"10. Способы оплаты",
"11. Онлайн-оплата",
"12. Онлайн-бронирование",
"13. FAQ",
"14. Доступность для ЛОВЗ",
"15. Партнеры/бренды",
"16. Команда/сотрудники",
"17. Уголок потребителя",
"18. Актуальность документов"
]
for criterion in criteria_names:
criteria_headers.extend([criterion, f"{criterion} URL", f"{criterion} Комментарий"])
# Все заголовки
all_headers = base_headers + criteria_headers
# Записываем заголовки
for i, header in enumerate(all_headers, 1):
cell = ws.cell(row=1, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Данные по отелям
for i, hotel in enumerate(audit_data, 2):
# Базовые данные
ws.cell(row=i, column=1, value=hotel['hotel_name'] or hotel['full_name'])
ws.cell(row=i, column=2, value=hotel['website'] or hotel['website_address'])
ws.cell(row=i, column=3, value='Да' if hotel['has_website'] else 'Нет')
ws.cell(row=i, column=4, value=f"{hotel['total_score']}/{hotel['max_score']}")
ws.cell(row=i, column=5, value=f"{hotel['score_percentage']:.1f}%")
# Цветовое кодирование процента
percentage = hotel['score_percentage'] or 0
if percentage >= 50:
fill_color = 'C6EFCE' # Зеленый
elif percentage >= 30:
fill_color = 'FFEB9C' # Желтый
else:
fill_color = 'FFC7CE' # Красный
ws.cell(row=i, column=5).fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid')
# Данные по критериям
col_idx = 6 # Начинаем с 6-й колонки
if hotel['criteria_results']:
criteria = hotel['criteria_results']
for criterion in criteria:
criterion_id = criterion.get('criterion_id')
# Статус
status = 'Да' if criterion.get('found', False) else 'Нет'
ws.cell(row=i, column=col_idx, value=status)
# URL и Комментарий - специальная обработка для критерия 6 (РКН)
if criterion_id == 6: # РКН Реестр
# URL на реестр РКН
rkn_number = hotel.get('rkn_registry_number', '')
if rkn_number:
url = f"https://rkn.gov.ru/mass-communications/reestr/search/?q={rkn_number}"
else:
url = ""
ws.cell(row=i, column=col_idx + 1, value=url)
# Комментарий: номер РКН + дата
rkn_date = hotel.get('rkn_registry_date', '')
if rkn_number and rkn_date:
comment = f"{rkn_number}\n{rkn_date}"
elif rkn_number:
comment = rkn_number
else:
comment = ""
ws.cell(row=i, column=col_idx + 2, value=comment)
else:
# Обычная обработка для остальных критериев
url = criterion.get('ai_agent', {}).get('url', '')
ws.cell(row=i, column=col_idx + 1, value=url)
# Используем details вместо quote для коротких комментариев
comment = criterion.get('ai_agent', {}).get('details', '')
if not comment:
# Если details нет, используем quote но обрезаем
comment = criterion.get('ai_agent', {}).get('quote', '')
if len(comment) > 100:
comment = comment[:100] + '...'
ws.cell(row=i, column=col_idx + 2, value=comment)
# Цветовое кодирование статуса
if criterion.get('found', False):
fill_color = 'C6EFCE' # Зеленый
else:
fill_color = 'FFC7CE' # Красный
ws.cell(row=i, column=col_idx).fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid')
col_idx += 3
# Форматирование всех ячеек строки
for col in range(1, len(all_headers) + 1):
cell = ws.cell(row=i, column=col)
cell.font = normal_font
cell.alignment = Alignment(horizontal='center', vertical='top', wrap_text=True)
# Автоподбор ширины колонок
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if cell.value:
# Учитываем переносы строк
lines = str(cell.value).split('\n')
max_line_length = max(len(line) for line in lines) if lines else 0
max_length = max(max_length, max_line_length)
except:
pass
# Ограничиваем ширину
adjusted_width = min(max_length + 2, 50) # Максимум 50
adjusted_width = max(adjusted_width, 10) # Минимум 10
ws.column_dimensions[column_letter].width = adjusted_width
# Фильтры
ws.auto_filter.ref = f"A1:{openpyxl.utils.get_column_letter(len(all_headers))}{len(audit_data)+1}"
# Заморозка заголовков
ws.freeze_panes = "A2"
def main():
"""Основная функция"""
print("🏛️ СОЗДАНИЕ ОТЧЕТА ПО ОРЛОВСКОЙ ОБЛАСТИ (ГОРИЗОНТАЛЬНЫЙ ФОРМАТ)")
print("=" * 60)
# Получаем данные
print("📊 Загружаем данные из БД...")
audit_data, criteria_stats = get_orel_data()
print(f"✅ Загружено:")
print(f" 🏨 Отелей: {len(audit_data)}")
print(f" 🎯 Критериев: {len(criteria_stats)}")
# Создаем Excel файл
print("\n📝 Создаем Excel файл...")
workbook = Workbook()
# Лист дашборда
print("📊 Создаем дашборд...")
create_dashboard_sheet(workbook, audit_data, criteria_stats)
# Лист аудита
print("🏨 Создаем таблицу аудита (горизонтальный формат)...")
create_audit_sheet(workbook, audit_data)
# Сохраняем файл
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"orel_horizontal_report_{timestamp}.xlsx"
workbook.save(filename)
print(f"\n✅ Отчет сохранен: {filename}")
print(f"📊 Листы:")
print(f" 📈 Дашборд Орёл - графики и статистика")
print(f" 🏨 Аудит отелей - горизонтальный формат (как в CSV)")
if __name__ == "__main__":
main()