Files
hotels/generate_excel_from_db_final.py
Фёдор 0cf3297290 Проект аудита отелей: основные скрипты и документация
- Краулеры: 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
2025-10-16 10:52:09 +03:00

616 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()