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

- Краулеры: 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,615 @@
#!/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()