Inbuild / app.py
DinaFatikh's picture
Update app.py
7748b7d verified
import gradio as gr
import pdfplumber
import re
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
import tempfile
import os
from PIL import Image
# Функция для обработки PDF и извлечения данных
def process_pdf(file):
data_list = []
try:
# Открываем PDF-файл с помощью pdfplumber
with pdfplumber.open(file) as pdf:
text = ''
for page_num, page in enumerate(pdf.pages, start=1):
page_text = page.extract_text()
if page_text:
text += page_text + '\n'
# Функция для извлечения значений по ключевым словам
def extract_value(label, text):
pattern = rf'{label}\s*тенге\s*([\d\s,]+)' # Поиск ключевого слова и числового значения
match = re.search(pattern, text, flags=re.IGNORECASE)
if match:
value = match.group(1)
value = re.sub(r'[^\d]', '', value) # Убираем все, кроме цифр
return int(value) if value else 0
return 0
# Функция для извлечения значения "ВСЕГО ПО СМЕТЕ"
def extract_total_value(label, text):
pattern = rf'{label}:\s*([\d\s]+)' # Учёт двоеточия после "ВСЕГО ПО СМЕТЕ"
match = re.search(pattern, text, flags=re.IGNORECASE)
if match:
value = match.group(1)
value = re.sub(r'[^\d]', '', value) # Убираем все символы, кроме цифр
return int(value) if value else 0
# Разбиваем текст на части по ключевому слову "ЛОКАЛЬНАЯ СМЕТА №"
estimates = re.split(r'ЛОКАЛЬНАЯ СМЕТА №', text, flags=re.IGNORECASE)[1:]
for idx, estimate_text in enumerate(estimates, start=1):
# Обрезаем текст до появления слова "Раздел"
estimate_text_clean = re.split(r'Раздел', estimate_text, flags=re.IGNORECASE)[0]
# Извлекаем номер сметы
number_match = re.match(r'(\S+)', estimate_text_clean.strip())
estimate_number = number_match.group(1) if number_match else ''
# Извлекаем наименование сметы после "на"
name_match = re.search(r'на\s+(.+?)(?:\n|$)', estimate_text_clean, flags=re.IGNORECASE)
estimate_name = name_match.group(1).strip() if name_match else ''
# Извлекаем затраты в указанном порядке
labor_cost = extract_value('Затраты на труд рабочих', estimate_text_clean)
machine_cost = extract_value('Машины и механизмы', estimate_text_clean)
material_cost = extract_value('Материалы, изделия и конструкции', estimate_text_clean)
transport_cost = extract_value('Перевозки', estimate_text_clean)
equipment_cost = extract_value('Оборудование', estimate_text_clean)
total_cost = extract_total_value('ВСЕГО ПО СМЕТЕ', estimate_text_clean)
# Сохраняем данные в список с обновленными названиями столбцов
data_list.append({
'№ локальной сметы': estimate_number,
'Наименование': estimate_name,
'Рабочие': labor_cost,
'Техника': machine_cost,
'Материалы': material_cost,
'Перевозки': transport_cost,
'Оборудование': equipment_cost,
'ВСЕГО ПО СМЕТЕ': total_cost
})
except Exception as e:
print(f"Ошибка при обработке PDF-файла: {e}")
return pd.DataFrame(), pd.DataFrame(), None, None, None # Возвращаем два пустых DataFrame и None для диаграммы и файлов Excel в случае ошибки
# Преобразуем список данных в DataFrame
df = pd.DataFrame(data_list)
# Убираем лишние пробелы и символы из названий столбцов
df.columns = df.columns.str.strip()
# Рассчитываем общую сумму по всем сметам для каждой категории
total_row = pd.DataFrame({
'№ локальной сметы': ['Итого'],
'Наименование': [''],
'Рабочие': [df['Рабочие'].sum()],
'Техника': [df['Техника'].sum()],
'Материалы': [df['Материалы'].sum()],
'Перевозки': [df['Перевозки'].sum()],
'Оборудование': [df['Оборудование'].sum()],
'ВСЕГО ПО СМЕТЕ': [df['ВСЕГО ПО СМЕТЕ'].sum()]
})
# Добавляем строку "Итого" в конец таблицы
df = pd.concat([df, total_row], ignore_index=True)
# Создаём таблицу с процентным соотношением, округленным до двух знаков после запятой
total_sum = df['ВСЕГО ПО СМЕТЕ'].iloc[:-1].sum() # исключаем строку "Итого"
df_percentage = df[['№ локальной сметы', 'Наименование']].copy()
df_percentage['Процент от общей суммы'] = ((df['ВСЕГО ПО СМЕТЕ'].iloc[:-1] / total_sum) * 100).round(2)
# Создание круговой диаграммы
category_sums = {
'Категория': ['Рабочие', 'Техника', 'Материалы', 'Перевозки', 'Оборудование'],
'Сумма': [
df['Рабочие'].iloc[:-1].sum(),
df['Техника'].iloc[:-1].sum(),
df['Материалы'].iloc[:-1].sum(),
df['Перевозки'].iloc[:-1].sum(),
df['Оборудование'].iloc[:-1].sum()
]
}
df_category_percentage = pd.DataFrame(category_sums)
df_category_percentage['Процент от общей суммы'] = ((df_category_percentage['Сумма'] / total_sum) * 100).round(2)
# Создание словаря сопоставления категорий и цветов
color_map = {
'Рабочие': '#DCEDFC',
'Техника': '#94B4D4',
'Материалы': '#3C47D6',
'Перевозки': '#94B4D4',
'Оборудование': '#0E86D4'
}
# Фильтрация категорий с процентом >=1%
filtered_df = df_category_percentage[df_category_percentage['Процент от общей суммы'] >= 1].reset_index(drop=True)
# Генерация списка цветов в соответствии с порядком категорий
colors = [color_map.get(category, '#FFFFFF') for category in filtered_df['Категория']]
# Отладочная информация (можно удалить после проверки)
print("Категории:", filtered_df['Категория'].tolist())
print("Цвета:", colors)
# Сохраняем диаграмму в BytesIO с добавлением пустого пространства
fig, ax = plt.subplots(figsize=(7, 7)) # Увеличенный размер фигуры
ax.pie(
filtered_df['Сумма'],
labels=filtered_df['Категория'],
autopct='%1.2f%%',
startangle=90,
colors=colors, # Используем пользовательские цвета
labeldistance=1.1, # Расстояние до надписей
pctdistance=0.8 # Расстояние до процентных надписей
)
plt.title('')
plt.axis('equal') # Круговая диаграмма круглая по форме
plt.tight_layout() # Оптимизация расположения элементов
pie_chart = BytesIO()
plt.savefig(pie_chart, format='png', bbox_inches='tight') # bbox_inches='tight' для оптимизации
plt.close(fig)
pie_chart.seek(0)
# Форматирование чисел с пробелами в качестве разделителей тысяч
def format_number(x):
return f"{x:,}".replace(",", " ")
df_formatted = df.copy()
for col in ['Рабочие', 'Техника', 'Материалы', 'Перевозки', 'Оборудование', 'ВСЕГО ПО СМЕТЕ']:
df_formatted[col] = df_formatted[col].apply(format_number)
df_percentage_formatted = df_percentage.copy()
df_percentage_formatted['Процент от общей суммы'] = df_percentage_formatted['Процент от общей суммы'].astype(str) + '%'
# Создаём временные Excel-файлы
temp_dir = tempfile.mkdtemp()
detailed_report_path = os.path.join(temp_dir, "detailed_report.xlsx")
percentage_report_path = os.path.join(temp_dir, "percentage_report.xlsx")
df.to_excel(detailed_report_path, index=False)
df_percentage.to_excel(percentage_report_path, index=False)
# Возвращаем форматированные DataFrame и диаграмму
return df_formatted, df_percentage_formatted, pie_chart, detailed_report_path, percentage_report_path
# CSS для блокировки копирования текста
custom_css = """
* {
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Standard */
}
"""
# Создаем интерфейс Gradio с блоками и адаптивным дизайном
with gr.Blocks(css=custom_css) as demo:
# Основной заголовок и подзаголовок
with gr.Row():
gr.Markdown("### **AIGineer | ИИ УПРАВЛЕНИЕ СТРОИТЕЛЬСТВОМ**", elem_id="header")
gr.Markdown("**На текущий момент система считает сметы в ценах от 2023**", elem_id="subheader")
# Загрузка файла (текст выравнивается по центру)
with gr.Row():
upload_button = gr.File(label="ЗАГРУЗИТЕ СМЕТУ В PDF", elem_id="upload")
# Первая таблица с заголовком и описанием
with gr.Row():
gr.Markdown("<br>") # Добавляем расстояние перед таблицей
with gr.Row():
gr.Markdown("<h3 style='text-align: center;'>ДЕТАЛИЗИРОВАННЫЙ БЮДЖЕТ</h3>", elem_id="title1")
with gr.Row():
gr.Markdown("Отчет дает полное представление о том, как распределяются средства по каждому разделу сметной документации. Он разбивает затраты на ключевые категории — оплата труда, техника, материалы, логистика и оборудование. Это позволяет держать под контролем каждую статью расходов и моментально видеть, где происходят основные затраты.", elem_id="desc1")
with gr.Row():
output_table_1 = gr.Dataframe(headers=["№ локальной сметы", "Наименование", "Рабочие", "Техника", "Материалы", "Перевозки", "Оборудование", "ВСЕГО ПО СМЕТЕ"], elem_id="table1")
# Расстояние между таблицами
with gr.Row():
gr.Markdown("<br>") # Добавляем расстояние между таблицами
# Вторая таблица с заголовком и описанием
with gr.Row():
gr.Markdown("<h3 style='text-align: center;'>ПРОЦЕНТНОЕ РАСПРЕДЕЛЕНИЕ ЛОКАЛЬНЫХ СМЕТ</h3>", elem_id="title2")
with gr.Row():
gr.Markdown("Важный инструмент для стратегического планирования, который помогает легко сравнивать между собой разделы проекта, понимая, какие из них требуют большего финансирования, а какие — меньшего. Это позволяет эффективно распределять ресурсы и достигать максимальной отдачи.", elem_id="desc2")
with gr.Row():
output_table_2 = gr.Dataframe(headers=["№ локальной сметы", "Наименование", "Процент от общей суммы"], elem_id="table2")
# Третья таблица с диаграммой
with gr.Row():
gr.Markdown("<h3 style='text-align: center;'>СТРУКТУРА РАСХОДОВ ПО КАТЕГОРИЯМ</h3>", elem_id="title3")
with gr.Row():
gr.Markdown("<br>") # Добавляем пустое пространство после заголовка
with gr.Row():
output_plot = gr.Image(type="pil", elem_id="plot")
# Кнопки для скачивания Excel-файлов
with gr.Row():
download_excel_1 = gr.File(label="Скачать детализированный отчет")
download_excel_2 = gr.File(label="Скачать процентное соотношение")
def update_download_buttons(file):
df_formatted, df_percentage_formatted, pie_chart, detailed_report_path, percentage_report_path = process_pdf(file)
return df_formatted, df_percentage_formatted, Image.open(pie_chart), detailed_report_path, percentage_report_path
upload_button.change(
update_download_buttons,
inputs=upload_button,
outputs=[output_table_1, output_table_2, output_plot, download_excel_1, download_excel_2]
)
# Запуск приложения
demo.launch(share=True)