242 lines
11 KiB
Python
242 lines
11 KiB
Python
"""
|
||
Модуль для работы с 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
|