Рефакторинг проекта: разделение на модули, улучшенный интерфейс, обработка Excel
This commit is contained in:
1
services/__init__.py
Normal file
1
services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Сервисы
|
||||
171
services/excel_processor.py
Normal file
171
services/excel_processor.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Сервис для работы с Excel файлами.
|
||||
"""
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from typing import List, Set, Dict, Optional
|
||||
|
||||
class ExcelProcessor:
|
||||
def __init__(self, excel_file_path: str):
|
||||
self.excel_file_path = Path(excel_file_path)
|
||||
self.cadastral_numbers: Set[str] = set()
|
||||
self.df: Optional[pd.DataFrame] = None
|
||||
self.cadastral_column: Optional[str] = None
|
||||
|
||||
def load_excel_file(self) -> bool:
|
||||
"""Загружает Excel файл и находит колонку с кадастровыми номерами"""
|
||||
try:
|
||||
if not self.excel_file_path.exists():
|
||||
print(f"Excel файл {self.excel_file_path} не найден")
|
||||
return False
|
||||
|
||||
# Пробуем разные форматы Excel файлов
|
||||
try:
|
||||
self.df = pd.read_excel(self.excel_file_path, engine='openpyxl')
|
||||
except:
|
||||
self.df = pd.read_excel(self.excel_file_path, engine='xlrd')
|
||||
|
||||
# Ищем колонку с кадастровыми номерами
|
||||
self.cadastral_column = self._find_cadastral_column()
|
||||
|
||||
if self.cadastral_column:
|
||||
# Извлекаем кадастровые номера
|
||||
self._extract_cadastral_numbers()
|
||||
print(f"Загружено {len(self.cadastral_numbers)} кадастровых номеров из Excel")
|
||||
return True
|
||||
else:
|
||||
print("Не найдена колонка с кадастровыми номерами в Excel файле")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при загрузке Excel файла: {e}")
|
||||
return False
|
||||
|
||||
def _find_cadastral_column(self) -> Optional[str]:
|
||||
"""Находит колонку с кадастровыми номерами"""
|
||||
if self.df is None:
|
||||
return None
|
||||
|
||||
# Ключевые слова для поиска колонки
|
||||
keywords = ['кадастр', 'номер', 'cadastral', 'number', 'кн', 'cad']
|
||||
|
||||
for column in self.df.columns:
|
||||
column_str = str(column).lower()
|
||||
if any(keyword in column_str for keyword in keywords):
|
||||
return column
|
||||
|
||||
# Если не найдено по названию, ищем по содержимому
|
||||
for column in self.df.columns:
|
||||
# Проверяем первые несколько значений в колонке
|
||||
sample_values = self.df[column].dropna().head(5)
|
||||
cadastral_count = 0
|
||||
|
||||
for value in sample_values:
|
||||
if self._is_cadastral_number_format(str(value)):
|
||||
cadastral_count += 1
|
||||
|
||||
# Если больше половины значений похожи на кадастровые номера
|
||||
if cadastral_count >= len(sample_values) * 0.5:
|
||||
return column
|
||||
|
||||
return None
|
||||
|
||||
def _extract_cadastral_numbers(self):
|
||||
"""Извлекает кадастровые номера из найденной колонки"""
|
||||
if self.df is None or self.cadastral_column is None:
|
||||
return
|
||||
|
||||
cadastral_numbers = set()
|
||||
|
||||
for value in self.df[self.cadastral_column].dropna():
|
||||
value_str = str(value).strip()
|
||||
if self._is_cadastral_number_format(value_str):
|
||||
cadastral_numbers.add(value_str)
|
||||
|
||||
self.cadastral_numbers = cadastral_numbers
|
||||
|
||||
def _is_cadastral_number_format(self, text: str) -> bool:
|
||||
"""Проверяет, соответствует ли текст формату кадастрового номера"""
|
||||
import re
|
||||
pattern = r'^\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}$'
|
||||
return bool(re.match(pattern, text))
|
||||
|
||||
def update_excel_with_file_info(self, cadastral_to_file_map: Dict[str, str],
|
||||
output_file_path: str = None) -> bool:
|
||||
"""Обновляет Excel файл информацией о найденных файлах и смежных номерах"""
|
||||
try:
|
||||
if self.df is None or self.cadastral_column is None:
|
||||
print("Excel файл не загружен или не найдена колонка с номерами")
|
||||
return False
|
||||
|
||||
# Создаем карту файл -> множество кадастровых номеров для поиска смежных
|
||||
file_to_numbers_map = {}
|
||||
for number, file_path in cadastral_to_file_map.items():
|
||||
if file_path not in file_to_numbers_map:
|
||||
file_to_numbers_map[file_path] = set()
|
||||
file_to_numbers_map[file_path].add(number)
|
||||
|
||||
# Добавляем новые колонки
|
||||
self.df['Найденные файлы'] = ''
|
||||
self.df['Смежные кадастровые номера'] = ''
|
||||
self.df['Кадастровый блок'] = ''
|
||||
|
||||
# Заполняем информацию для каждой строки
|
||||
for index, row in self.df.iterrows():
|
||||
cadastral_number = str(row[self.cadastral_column]).strip()
|
||||
|
||||
if cadastral_number in cadastral_to_file_map:
|
||||
# 1. Получаем файл(ы) где найден номер
|
||||
main_file_path = cadastral_to_file_map[cadastral_number]
|
||||
file_name = Path(main_file_path).name
|
||||
self.df.at[index, 'Найденные файлы'] = file_name
|
||||
|
||||
# 2. Находим все смежные кадастровые номера из того же файла
|
||||
if main_file_path in file_to_numbers_map:
|
||||
all_numbers_in_file = file_to_numbers_map[main_file_path]
|
||||
# Исключаем текущий номер из списка смежных
|
||||
adjacent_numbers = all_numbers_in_file - {cadastral_number}
|
||||
if adjacent_numbers:
|
||||
self.df.at[index, 'Смежные кадастровые номера'] = ', '.join(sorted(adjacent_numbers))
|
||||
|
||||
# 3. Определяем кадастровый блок (первые три части номера)
|
||||
cadastral_block = self._extract_cadastral_block(cadastral_number)
|
||||
if cadastral_block:
|
||||
self.df.at[index, 'Кадастровый блок'] = cadastral_block
|
||||
|
||||
# Сохраняем обновленный файл
|
||||
output_path = output_file_path or str(self.excel_file_path.with_suffix('.updated.xlsx'))
|
||||
self.df.to_excel(output_path, index=False, engine='openpyxl')
|
||||
|
||||
print(f"Обновленный Excel файл сохранен: {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при обновлении Excel файла: {e}")
|
||||
return False
|
||||
|
||||
def _extract_cadastral_block(self, cadastral_number: str) -> str:
|
||||
"""Извлекает кадастровый блок из номера (первые три части)"""
|
||||
try:
|
||||
parts = cadastral_number.split(':')
|
||||
if len(parts) >= 3:
|
||||
return ':'.join(parts[:3])
|
||||
return cadastral_number
|
||||
except:
|
||||
return cadastral_number
|
||||
|
||||
def get_cadastral_numbers(self) -> Set[str]:
|
||||
"""Возвращает множество кадастровых номеров из Excel"""
|
||||
return self.cadastral_numbers.copy()
|
||||
|
||||
def get_statistics(self) -> Dict[str, any]:
|
||||
"""Возвращает статистику по Excel файлу"""
|
||||
if self.df is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'total_rows': len(self.df),
|
||||
'cadastral_column': self.cadastral_column,
|
||||
'cadastral_numbers_count': len(self.cadastral_numbers),
|
||||
'columns': list(self.df.columns)
|
||||
}
|
||||
91
services/file_seeker.py
Normal file
91
services/file_seeker.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Сервис для поиска и обработки XML файлов.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Set, Dict
|
||||
from models.xml_file_info import XMLFileInfo
|
||||
from models.cadastral_record import CadastralRecord
|
||||
|
||||
class FileSeeker:
|
||||
def __init__(self, base_directory: str, temp_directory: str = None):
|
||||
self.base_directory = Path(base_directory)
|
||||
self.temp_directory = Path(temp_directory) if temp_directory else Path(tempfile.mkdtemp(prefix="kadastr14_"))
|
||||
self.xml_files: List[XMLFileInfo] = []
|
||||
self.cadastral_records: List[CadastralRecord] = []
|
||||
|
||||
# Создаем временную директорию если её нет
|
||||
self.temp_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def find_xml_files(self) -> List[XMLFileInfo]:
|
||||
"""Находит все XML файлы в базовой директории"""
|
||||
xml_files = []
|
||||
|
||||
if not self.base_directory.exists():
|
||||
print(f"Директория {self.base_directory} не существует")
|
||||
return xml_files
|
||||
|
||||
for file_path in self.base_directory.rglob("*.xml"):
|
||||
if file_path.is_file():
|
||||
xml_info = XMLFileInfo(str(file_path))
|
||||
xml_files.append(xml_info)
|
||||
|
||||
self.xml_files = xml_files
|
||||
print(f"Найдено XML файлов: {len(xml_files)}")
|
||||
return xml_files
|
||||
|
||||
def process_files(self) -> List[CadastralRecord]:
|
||||
"""Обрабатывает все найденные XML файлы и извлекает кадастровые номера"""
|
||||
all_records = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
cadastral_numbers = xml_file.extract_cadastral_numbers()
|
||||
|
||||
for number in cadastral_numbers:
|
||||
record = CadastralRecord(number, str(xml_file.file_path))
|
||||
all_records.append(record)
|
||||
|
||||
self.cadastral_records = all_records
|
||||
print(f"Всего найдено кадастровых номеров: {len(all_records)}")
|
||||
return all_records
|
||||
|
||||
def get_unique_cadastral_numbers(self) -> Set[str]:
|
||||
"""Возвращает уникальные кадастровые номера"""
|
||||
return {record.number for record in self.cadastral_records}
|
||||
|
||||
def move_files_to_temp(self, cadastral_numbers_to_move: Set[str]) -> Dict[str, str]:
|
||||
"""Перемещает файлы с указанными кадастровыми номерами во временную папку"""
|
||||
moved_files = {}
|
||||
|
||||
for record in self.cadastral_records:
|
||||
if record.number in cadastral_numbers_to_move:
|
||||
source_path = Path(record.file_path)
|
||||
if source_path.exists():
|
||||
# Создаем уникальное имя для файла во временной папке
|
||||
dest_name = f"{record.number.replace(':', '_')}_{source_path.name}"
|
||||
dest_path = self.temp_directory / dest_name
|
||||
|
||||
try:
|
||||
shutil.copy2(source_path, dest_path)
|
||||
moved_files[record.number] = str(dest_path)
|
||||
print(f"Файл скопирован: {source_path.name} -> {dest_path}")
|
||||
except Exception as e:
|
||||
print(f"Ошибка при копировании файла {source_path}: {e}")
|
||||
|
||||
return moved_files
|
||||
|
||||
def get_statistics(self) -> Dict[str, int]:
|
||||
"""Возвращает статистику обработки"""
|
||||
return {
|
||||
'total_xml_files': len(self.xml_files),
|
||||
'files_with_numbers': len([f for f in self.xml_files if len(f.cadastral_numbers) > 0]),
|
||||
'total_cadastral_numbers': len(self.cadastral_records),
|
||||
'unique_cadastral_numbers': len(self.get_unique_cadastral_numbers())
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
stats = self.get_statistics()
|
||||
return (f"FileSeeker: {stats['total_xml_files']} XML файлов, "
|
||||
f"{stats['unique_cadastral_numbers']} уникальных номеров")
|
||||
Reference in New Issue
Block a user