Рефакторинг проекта: разделение на модули, улучшенный интерфейс, обработка Excel

This commit is contained in:
2025-08-04 00:31:05 +05:00
parent 059f174dd5
commit a882a81180
22 changed files with 1936 additions and 530 deletions

241
excel_processor.py Normal file
View File

@@ -0,0 +1,241 @@
"""
Модуль для работы с Excel файлами.
Обрабатывает список кадастровых номеров и заполняет информацию о них.
"""
import pandas as pd
from pathlib import Path
from typing import List, Dict, Optional
from collections import defaultdict
class ExcelProcessor:
"""Класс для работы с Excel файлами списков кадастровых номеров."""
def __init__(self, excel_file_path: str):
self.excel_file_path = Path(excel_file_path)
self.df = None
self.cadastral_column = None
def load_excel(self, sheet_name: Optional[str] = None, cadastral_column: Optional[str] = None):
"""Загружает Excel файл и определяет колонку с кадастровыми номерами."""
try:
# Загружаем Excel файл
if sheet_name:
self.df = pd.read_excel(self.excel_file_path, sheet_name=sheet_name)
else:
# Берем первый лист
self.df = pd.read_excel(self.excel_file_path)
print(f"✅ Загружен Excel файл: {self.excel_file_path}")
print(f"📊 Размер таблицы: {len(self.df)} строк, {len(self.df.columns)} колонок")
print(f"📋 Колонки: {list(self.df.columns)}")
# Определяем колонку с кадастровыми номерами
if cadastral_column:
self.cadastral_column = cadastral_column
else:
self.cadastral_column = self._find_cadastral_column()
if self.cadastral_column:
print(f"🎯 Найдена колонка с кадастровыми номерами: '{self.cadastral_column}'")
cadastral_count = self.df[self.cadastral_column].notna().sum()
print(f"📄 Кадастровых номеров в файле: {cadastral_count}")
else:
raise ValueError("Не удалось найти колонку с кадастровыми номерами")
except Exception as e:
print(f"❌ Ошибка при загрузке Excel файла: {e}")
raise
def _find_cadastral_column(self) -> Optional[str]:
"""Автоматически находит колонку с кадастровыми номерами."""
# Возможные названия колонок
possible_names = [
'кадастровый номер', 'кадастровый_номер', 'кн', 'cadastral_number',
'номер', 'cadastral', 'кадастр', 'kad_number'
]
# Ищем по названию колонки
for col in self.df.columns:
col_lower = str(col).lower().strip()
for name in possible_names:
if name in col_lower:
return col
# Ищем по содержимому (ищем колонку где есть номера вида XX:XX:XXXXXX:XXXX)
import re
pattern = r'\d{1,2}:\d{1,2}:\d{6,7}:\d{1,4}'
for col in self.df.columns:
# Проверяем первые несколько непустых значений
sample_values = self.df[col].dropna().head(10)
match_count = 0
for value in sample_values:
if isinstance(value, str) and re.match(pattern, value.strip()):
match_count += 1
# Если больше половины значений похожи на кадастровые номера
if match_count > len(sample_values) * 0.5 and match_count > 0:
return col
return None
def get_cadastral_numbers_from_excel(self) -> List[str]:
"""Возвращает список всех кадастровых номеров из Excel файла."""
if not self.cadastral_column:
return []
# Получаем все непустые кадастровые номера
cadastral_numbers = []
for value in self.df[self.cadastral_column].dropna():
if isinstance(value, str):
clean_value = value.strip()
if clean_value:
cadastral_numbers.append(clean_value)
elif pd.notna(value):
# Если число, преобразуем в строку
cadastral_numbers.append(str(value).strip())
return cadastral_numbers
def create_cadastral_mapping(self, cadastral_records: List) -> Dict[str, Dict]:
"""Создает маппинг кадастровых номеров к их данным."""
mapping = defaultdict(lambda: {
'source_files': set(),
'related_numbers': set(),
'quarter_numbers': set()
})
for record in cadastral_records:
cadastral_num = record.cadastral_number
# Добавляем источник файла
source_file = record.source_xml_info.get_display_name()
mapping[cadastral_num]['source_files'].add(source_file)
# Добавляем связанные номера
for related in record.related_numbers:
mapping[cadastral_num]['related_numbers'].add(related)
# Добавляем квартал
if record.quarter_number:
mapping[cadastral_num]['quarter_numbers'].add(record.quarter_number)
# Преобразуем sets в списки для удобства
for cadastral_num in mapping:
mapping[cadastral_num]['source_files'] = list(mapping[cadastral_num]['source_files'])
mapping[cadastral_num]['related_numbers'] = list(mapping[cadastral_num]['related_numbers'])
mapping[cadastral_num]['quarter_numbers'] = list(mapping[cadastral_num]['quarter_numbers'])
return dict(mapping)
def fill_cadastral_info(self, cadastral_records: List, output_file: Optional[str] = None):
"""Заполняет информацию о кадастровых номерах в Excel файле."""
if self.df is None or not self.cadastral_column:
raise ValueError("Excel файл не загружен или не найдена колонка с кадастровыми номерами")
print(f"\n{'='*80}")
print(f"ЗАПОЛНЕНИЕ ИНФОРМАЦИИ В EXCEL ФАЙЛЕ")
print(f"{'='*80}")
# Создаем маппинг
cadastral_mapping = self.create_cadastral_mapping(cadastral_records)
# Добавляем новые колонки если их нет
if 'Источники файлов' not in self.df.columns:
self.df['Источники файлов'] = ''
if 'Смежные номера' not in self.df.columns:
self.df['Смежные номера'] = ''
if 'Кадастровый блок' not in self.df.columns:
self.df['Кадастровый блок'] = ''
# Статистика
found_count = 0
not_found_count = 0
# Заполняем информацию для каждого кадастрового номера
for index, row in self.df.iterrows():
cadastral_num = str(row[self.cadastral_column]).strip()
if cadastral_num in cadastral_mapping:
data = cadastral_mapping[cadastral_num]
# Заполняем источники файлов
sources = ', '.join(data['source_files'])
self.df.at[index, 'Источники файлов'] = sources
# Заполняем смежные номера
related = ', '.join(data['related_numbers'])
self.df.at[index, 'Смежные номера'] = related
# Заполняем кадастровый блок (квартал)
quarters = ', '.join(data['quarter_numbers'])
self.df.at[index, 'Кадастровый блок'] = quarters
found_count += 1
print(f"{cadastral_num}: найден в {len(data['source_files'])} файлах")
else:
not_found_count += 1
print(f"{cadastral_num}: не найден в XML файлах")
print(f"\n📊 Статистика заполнения:")
print(f" Найдено: {found_count}")
print(f" Не найдено: {not_found_count}")
print(f" Всего: {found_count + not_found_count}")
# Сохраняем результат
if output_file:
output_path = Path(output_file)
else:
# Создаем имя файла с результатами
output_path = self.excel_file_path.parent / f"{self.excel_file_path.stem}_результат.xlsx"
try:
self.df.to_excel(output_path, index=False)
print(f"\n💾 Результат сохранен в: {output_path}")
return output_path
except Exception as e:
print(f"❌ Ошибка при сохранении: {e}")
raise
def print_excel_preview(self, rows: int = 10):
"""Выводит предварительный просмотр Excel файла."""
if self.df is None:
print("Excel файл не загружен")
return
print(f"\n📋 Предварительный просмотр Excel файла (первые {rows} строк):")
print("-" * 80)
# Показываем первые строки
preview_df = self.df.head(rows)
# Ограничиваем ширину колонок для читаемости
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', 50)
print(preview_df.to_string(index=False))
if len(self.df) > rows:
print(f"\n... и еще {len(self.df) - rows} строк")
def get_statistics(self):
"""Возвращает статистику по Excel файлу."""
if self.df is None:
return {}
stats = {
'total_rows': len(self.df),
'total_columns': len(self.df.columns),
'columns': list(self.df.columns),
'cadastral_column': self.cadastral_column
}
if self.cadastral_column:
cadastral_numbers = self.get_cadastral_numbers_from_excel()
stats['cadastral_numbers_count'] = len(cadastral_numbers)
stats['empty_cadastral_count'] = len(self.df) - len(cadastral_numbers)
return stats