#!/usr/bin/env python3 """ ФИНАЛЬНАЯ ВЕРСИЯ - Генерация Excel из БД Работает с данными напрямую из PostgreSQL """ import psycopg2 import json import openpyxl from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.utils import get_column_letter from openpyxl.chart import BarChart, PieChart, Reference from datetime import datetime from urllib.parse import unquote # Конфигурация БД DB_CONFIG = { 'host': '147.45.189.234', 'port': 5432, 'database': 'default_db', 'user': 'gen_user', 'password': unquote('2~~9_%5EkVsU%3F2%5CS') } def get_audit_results_from_db(): """Получить результаты аудита из БД""" print("📡 Подключаюсь к БД...") conn = psycopg2.connect(**DB_CONFIG) cur = conn.cursor() query = """ SELECT hotel_id, region_name, hotel_name, website, has_website, criteria_results, total_score, max_score, score_percentage, audit_date, audit_version FROM hotel_audit_results WHERE audit_version = 'v1.0_with_rkn' ORDER BY region_name, hotel_name """ cur.execute(query) results = cur.fetchall() # Преобразуем в словари columns = [desc[0] for desc in cur.description] results = [dict(zip(columns, row)) for row in results] cur.close() conn.close() print(f"✅ Получено результатов: {len(results)}") return results def create_excel_report(results): """Создать Excel отчёт из данных БД""" wb = openpyxl.Workbook() ws = wb.active ws.title = "Аудит" # Стили header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") header_font = Font(color="FFFFFF", bold=True, size=10) found_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") not_found_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # ЗАГОЛОВКИ (строка 1) col = 1 base_headers = ['Отель', 'Сайт', 'Есть сайт', 'Балл', 'Процент'] for header in base_headers: cell = ws.cell(row=1, column=col, value=header) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) cell.border = border col += 1 # Заголовки критериев (включая РКН в правильном месте) if results and results[0].get('criteria_results'): criteria_results = results[0]['criteria_results'] if isinstance(criteria_results, str): criteria_results = json.loads(criteria_results) print(f"🔍 criteria_results type: {type(criteria_results)}") # Если это список (из n8n) - используем напрямую if isinstance(criteria_results, list): criteria_list = criteria_results print(f"🔍 Найдено критериев (список): {len(criteria_list)}") # Если это словарь (из БД) - преобразуем elif isinstance(criteria_results, dict): criteria_list = [] for i in range(1, 19): # критерии 1-18 key = f'criterion_{i:02d}' if key in criteria_results: criterion_data = criteria_results[key] criteria_list.append({ 'criterion_id': i, 'criterion_name': criterion_data.get('name', f'Критерий {i}'), }) print(f"🔍 Найдено критериев (словарь): {len(criteria_list)}") else: criteria_list = [] print(f"🔍 Неизвестный тип criteria_results") for criterion_idx, criterion in enumerate(criteria_list): # Вставляем РКН заголовки после критерия 5 (индекс 5) - РЕАЛЬНЫЕ данные из БД if criterion_idx == 5: rkn_headers = ['6. РКН Реестр', '6. РКН Номер/Дата', '6. РКН Ссылка'] for header in rkn_headers: cell = ws.cell(row=1, column=col, value=header) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) cell.border = border ws.column_dimensions[get_column_letter(col)].width = 30 col += 1 criterion_name = f"{criterion.get('criterion_id', criterion_idx+1)}. {criterion.get('criterion_name', f'Критерий {criterion_idx+1}')}" # Колонка 1: Статус (ДА/НЕТ) cell = ws.cell(row=1, column=col, value=criterion_name) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) cell.border = border ws.column_dimensions[get_column_letter(col)].width = 35 col += 1 # Колонка 2: URL cell = ws.cell(row=1, column=col, value=f"{criterion.get('criterion_id', criterion_idx+1)}. Апрув URL") cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) cell.border = border ws.column_dimensions[get_column_letter(col)].width = 40 col += 1 # Колонка 3: Цитата/Детали cell = ws.cell(row=1, column=col, value=f"{criterion.get('criterion_id', criterion_idx+1)}. Комментарий") cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) cell.border = border ws.column_dimensions[get_column_letter(col)].width = 50 col += 1 # Высота строки заголовков ws.row_dimensions[1].height = 40 print(f"✅ Заголовки созданы, всего колонок: {col-1}") # ДАННЫЕ (строки 2+) for row_idx, result in enumerate(results, 2): col = 1 # Базовые данные cell = ws.cell(row=row_idx, column=col, value=result['hotel_name']) cell.border = border cell.alignment = Alignment(vertical='top', wrap_text=True) col += 1 cell = ws.cell(row=row_idx, column=col, value=result.get('website', 'НЕТ САЙТА')) cell.border = border cell.alignment = Alignment(vertical='top') col += 1 has_website = "Да" if result.get('has_website') else "Нет" cell = ws.cell(row=row_idx, column=col, value=has_website) cell.border = border cell.alignment = Alignment(horizontal='center', vertical='center') col += 1 cell = ws.cell(row=row_idx, column=col, value=result['total_score']) cell.border = border cell.alignment = Alignment(horizontal='center', vertical='center') col += 1 perc_cell = ws.cell(row=row_idx, column=col, value=f"{result['score_percentage']:.1f}%") perc_cell.border = border perc_cell.alignment = Alignment(horizontal='center', vertical='center') if result['score_percentage'] >= 70: perc_cell.fill = found_fill elif result['score_percentage'] < 50: perc_cell.fill = not_found_fill col += 1 # Данные по критериям criteria_results = result['criteria_results'] if isinstance(criteria_results, str): criteria_results = json.loads(criteria_results) # Если это список (из n8n) if isinstance(criteria_results, list): criteria_list = criteria_results # Если это словарь (из БД) elif isinstance(criteria_results, dict): criteria_list = [] for i in range(1, 19): key = f'criterion_{i:02d}' if key in criteria_results: criteria_list.append(criteria_results[key]) else: criteria_list = [] for criterion_idx, criterion in enumerate(criteria_list): # Пропускаем критерий 6 (индекс 5) - РКН данные добавляем отдельно после критерия 5 if criterion_idx == 5: # Добавляем РКН данные из hotel_main после критерия 5 rkn_status = result.get('rkn_registry_status', '') rkn_in_registry = "ДА" if rkn_status and rkn_status.lower() == 'found' else "НЕТ" rkn_status_cell = ws.cell(row=row_idx, column=col, value=rkn_in_registry) rkn_status_cell.border = border rkn_status_cell.alignment = Alignment(horizontal='center', vertical='center') if rkn_in_registry == "ДА": rkn_status_cell.fill = found_fill # Зелёный - хорошо если в реестре else: rkn_status_cell.fill = not_found_fill # Красный - плохо если НЕ в реестре col += 1 rkn_number = result.get('rkn_registry_number', '') rkn_date = result.get('rkn_registry_date', '') rkn_info_text = f"{rkn_number}\n{rkn_date}" if rkn_number or rkn_date else "-" cell = ws.cell(row=row_idx, column=col, value=rkn_info_text) cell.border = border cell.alignment = Alignment(vertical='top', wrap_text=True) col += 1 rkn_url = f"https://rkn.gov.ru/mass-communications/reestr/search/?q={rkn_number}" if rkn_number else "-" cell = ws.cell(row=row_idx, column=col, value=rkn_url) cell.border = border cell.alignment = Alignment(vertical='top') col += 1 # Пропускаем обработку критерия 6 continue # Колонка 1: Статус (ДА/НЕТ) status = "ДА" if criterion.get('found') else "НЕТ" status_cell = ws.cell(row=row_idx, column=col, value=status) status_cell.border = border status_cell.alignment = Alignment(horizontal='center', vertical='center') if criterion.get('found'): status_cell.fill = found_fill else: status_cell.fill = not_found_fill col += 1 # Колонка 2: URL url = '-' if criterion.get('ai_agent', {}).get('url'): url = criterion['ai_agent']['url'] cell = ws.cell(row=row_idx, column=col, value=url) cell.border = border cell.alignment = Alignment(vertical='top') col += 1 # Колонка 3: Комментарий/Цитата comment = "Не найдено" if criterion.get('found'): # Приоритет: ai_agent.details → ai_agent.quote → regex.extracted if criterion.get('ai_agent', {}).get('details'): comment = criterion['ai_agent']['details'] elif criterion.get('ai_agent', {}).get('quote'): comment = criterion['ai_agent']['quote'] elif criterion.get('regex', {}).get('extracted'): comment = f"[Regex] {criterion['regex']['extracted']}" else: comment = "Найдено" # Ограничиваем длину comment = comment[:200] + "..." if len(comment) > 200 else comment cell = ws.cell(row=row_idx, column=col, value=comment) cell.border = border cell.alignment = Alignment(vertical='top', wrap_text=True) col += 1 # Высота строки ws.row_dimensions[row_idx].height = 50 print(f"✅ Данные добавлены, всего строк: {len(results)}") # Создаём дашборд create_dashboard(wb, results) # Сохраняем файл timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"audit_from_db_{timestamp}.xlsx" wb.save(filename) return filename def create_dashboard(wb, results): """Создать дашборд с графиками и статистикой""" # Создаём новый лист для дашборда ws = wb.create_sheet("📊 Дашборд", 0) # Вставляем первым # Стили title_font = Font(size=16, bold=True, color="366092") header_font = Font(size=12, bold=True, color="FFFFFF") header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") value_font = Font(size=14, bold=True) green_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") yellow_fill = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid") # Заголовок ws['A1'] = '📊 ДАШБОРД АУДИТА ОТЕЛЕЙ ЧУКОТКИ' ws['A1'].font = title_font ws.merge_cells('A1:F1') # Получаем статистику из общей базы отелей db_stats = get_database_statistics() # Общая статистика row = 3 ws[f'A{row}'] = 'ОБЩАЯ СТАТИСТИКА ПО ЧУКОТКЕ' ws[f'A{row}'].font = Font(size=14, bold=True) ws.merge_cells(f'A{row}:B{row}') row += 1 # Статистика из БД total_hotels = db_stats['total_hotels'] hotels_with_website = db_stats['hotels_with_website'] hotels_without_website = total_hotels - hotels_with_website hotels_accessible = db_stats['hotels_accessible'] hotels_inaccessible = hotels_with_website - hotels_accessible hotels_in_rkn = db_stats['hotels_in_rkn'] # Статистика по аудиту audited_hotels = len(results) avg_score = sum(r['score_percentage'] for r in results) / audited_hotels if audited_hotels > 0 else 0 stats = [ ('Всего отелей в Чукотке:', total_hotels, None), ('С сайтами:', hotels_with_website, green_fill), ('Без сайтов:', hotels_without_website, red_fill), ('Сайты доступны для анализа:', hotels_accessible, green_fill), ('Сайты недоступны:', hotels_inaccessible, red_fill), ('В реестре РКН:', hotels_in_rkn, green_fill if hotels_in_rkn > 0 else red_fill), ('Проведено аудитов:', audited_hotels, yellow_fill), ('Средний балл (аудит):', f"{avg_score:.1f}%", yellow_fill if avg_score < 50 else green_fill), ] for label, value, fill in stats: ws[f'A{row}'] = label ws[f'B{row}'] = value ws[f'B{row}'].font = value_font if fill: ws[f'B{row}'].fill = fill ws[f'B{row}'].alignment = Alignment(horizontal='center') row += 1 # Данные для графика "Статус сайтов" (строки 12-16) row = 12 ws[f'A{row}'] = 'Категория' ws[f'B{row}'] = 'Количество' ws[f'A{row}'].fill = header_fill ws[f'B{row}'].fill = header_fill ws[f'A{row}'].font = header_font ws[f'B{row}'].font = header_font row += 1 ws[f'A{row}'] = 'Сайты доступны' ws[f'B{row}'] = hotels_accessible row += 1 ws[f'A{row}'] = 'Сайты недоступны' ws[f'B{row}'] = hotels_inaccessible row += 1 ws[f'A{row}'] = 'Без сайтов' ws[f'B{row}'] = hotels_without_website row += 1 ws[f'A{row}'] = 'В реестре РКН' ws[f'B{row}'] = hotels_in_rkn # Круговая диаграмма "Статус сайтов" pie = PieChart() labels = Reference(ws, min_col=1, min_row=13, max_row=16) data = Reference(ws, min_col=2, min_row=12, max_row=16) pie.add_data(data, titles_from_data=True) pie.set_categories(labels) pie.title = "Статус сайтов отелей" pie.height = 10 pie.width = 15 ws.add_chart(pie, "D3") # Статистика по критериям (строки 18+) row = 18 ws[f'A{row}'] = 'СТАТИСТИКА ПО КРИТЕРИЯМ' ws[f'A{row}'].font = Font(size=14, bold=True) ws.merge_cells(f'A{row}:C{row}') row += 1 ws[f'A{row}'] = 'Критерий' ws[f'B{row}'] = 'Найдено' ws[f'C{row}'] = 'Не найдено' for col in ['A', 'B', 'C']: ws[f'{col}{row}'].fill = header_fill ws[f'{col}{row}'].font = header_font row += 1 # Собираем статистику по каждому критерию if results and results[0]['criteria_results']: criteria_results = results[0]['criteria_results'] if isinstance(criteria_results, str): criteria_results = json.loads(criteria_results) if isinstance(criteria_results, list): criteria_stats = [] for criterion_idx in range(18): criterion_id = criterion_idx + 1 criterion_name = f"{criterion_id}. Критерий {criterion_id}" # Получаем название критерия из первого результата if criterion_idx < len(criteria_results): criterion_name = f"{criterion_id}. {criteria_results[criterion_idx].get('criterion_name', f'Критерий {criterion_id}')[:30]}" found_count = 0 for r in results: criteria_res = r['criteria_results'] if isinstance(criteria_res, str): criteria_res = json.loads(criteria_res) if isinstance(criteria_res, list) and criterion_idx < len(criteria_res): if criteria_res[criterion_idx].get('found'): found_count += 1 not_found_count = total_hotels - found_count criteria_stats.append((criterion_name, found_count, not_found_count)) # НЕ добавляем РКН отдельно - он уже есть в критериях как #6 criteria_stats_with_rkn = criteria_stats start_row = row for criterion_name, found, not_found in criteria_stats_with_rkn: ws[f'A{row}'] = criterion_name ws[f'B{row}'] = found ws[f'C{row}'] = not_found row += 1 # Столбчатая диаграмма по критериям chart = BarChart() chart.type = "col" chart.style = 10 chart.title = "Результаты по критериям" chart.y_axis.title = 'Количество отелей' chart.x_axis.title = 'Критерии' data = Reference(ws, min_col=2, min_row=start_row-1, max_row=row-1, max_col=3) cats = Reference(ws, min_col=1, min_row=start_row, max_row=row-1) chart.add_data(data, titles_from_data=True) chart.set_categories(cats) chart.height = 15 chart.width = 25 ws.add_chart(chart, f"E{start_row}") # Распределение по баллам (строки row+2) row += 2 ws[f'A{row}'] = 'РАСПРЕДЕЛЕНИЕ ПО БАЛЛАМ' ws[f'A{row}'].font = Font(size=14, bold=True) ws.merge_cells(f'A{row}:B{row}') row += 1 ws[f'A{row}'] = 'Диапазон' ws[f'B{row}'] = 'Количество' ws[f'A{row}'].fill = header_fill ws[f'B{row}'].fill = header_fill ws[f'A{row}'].font = header_font ws[f'B{row}'].font = header_font row += 1 # Распределение по диапазонам ranges = [ ('0-25%', 0, 25), ('26-50%', 26, 50), ('51-75%', 51, 75), ('76-100%', 76, 100) ] start_row = row for range_name, min_val, max_val in ranges: count = sum(1 for r in results if min_val <= r['score_percentage'] <= max_val) ws[f'A{row}'] = range_name ws[f'B{row}'] = count row += 1 # Столбчатая диаграмма распределения chart2 = BarChart() chart2.type = "col" chart2.style = 11 chart2.title = "Распределение по баллам" chart2.y_axis.title = 'Количество отелей' data = Reference(ws, min_col=2, min_row=start_row-1, max_row=row-1) cats = Reference(ws, min_col=1, min_row=start_row, max_row=row-1) chart2.add_data(data, titles_from_data=True) chart2.set_categories(cats) chart2.height = 10 chart2.width = 15 ws.add_chart(chart2, f"D{start_row-1}") # Настройка ширины колонок ws.column_dimensions['A'].width = 35 ws.column_dimensions['B'].width = 15 ws.column_dimensions['C'].width = 15 print(" 📊 Дашборд создан") def get_database_statistics(): """Получить статистику по отелям из БД""" try: conn = psycopg2.connect(**DB_CONFIG) cursor = conn.cursor() # Общее количество отелей в Чукотке cursor.execute(""" SELECT COUNT(*) FROM hotel_main WHERE region_name = 'Чукотский автономный округ' """) total_hotels = cursor.fetchone()[0] # Отели с сайтами cursor.execute(""" SELECT COUNT(*) FROM hotel_main WHERE region_name = 'Чукотский автономный округ' AND website_address IS NOT NULL AND website_address != '' """) hotels_with_website = cursor.fetchone()[0] # Отели с доступными сайтами (есть в processed) cursor.execute(""" SELECT COUNT(DISTINCT h.id) FROM hotel_main h JOIN hotel_website_processed p ON h.id = p.hotel_id WHERE h.region_name = 'Чукотский автономный округ' AND h.website_address IS NOT NULL AND h.website_address != '' """) hotels_accessible = cursor.fetchone()[0] # Отели в реестре РКН cursor.execute(""" SELECT COUNT(*) FROM hotel_main WHERE region_name = 'Чукотский автономный округ' AND rkn_registry_status = 'found' """) hotels_in_rkn = cursor.fetchone()[0] cursor.close() conn.close() return { 'total_hotels': total_hotels, 'hotels_with_website': hotels_with_website, 'hotels_accessible': hotels_accessible, 'hotels_in_rkn': hotels_in_rkn } except Exception as e: print(f"❌ Ошибка получения статистики: {e}") return { 'total_hotels': 0, 'hotels_with_website': 0, 'hotels_accessible': 0, 'hotels_in_rkn': 0 } def main(): """Основная функция""" print("🚀 ГЕНЕРАЦИЯ EXCEL ИЗ БД - ФИНАЛЬНАЯ ВЕРСИЯ") print("=" * 50) try: # Получаем данные из БД results = get_audit_results_from_db() if not results: print("❌ Нет данных аудита в БД") return print(f"📊 Найдено результатов аудита: {len(results)}") # Создаём Excel отчёт filename = create_excel_report(results) print(f"✅ Excel отчёт сохранён: {filename}") print(f"📊 Обработано отелей: {len(results)}") if results: avg_score = sum(r['score_percentage'] for r in results) / len(results) print(f"📈 Средний % соответствия: {avg_score:.1f}%") except Exception as e: print(f"❌ Ошибка: {e}") import traceback traceback.print_exc() if __name__ == "__main__": main()