506 lines
25 KiB
Python
506 lines
25 KiB
Python
|
||
import os
|
||
import shutil
|
||
import tempfile
|
||
import zipfile
|
||
from pathlib import Path
|
||
from lxml import etree
|
||
import re
|
||
|
||
class CadastralRecord:
|
||
"""Класс для хранения информации о кадастровом номере и связанных с ним данных."""
|
||
|
||
def __init__(self, cadastral_number, source_xml_info, temp_file_path=None):
|
||
self.cadastral_number = cadastral_number # Основной кадастровый номер
|
||
self.source_xml_info = source_xml_info # Ссылка на XMLFileInfo где найден
|
||
self.temp_file_path = temp_file_path # Путь к файлу во временной папке
|
||
self.quarter_number = self._extract_quarter_number() # Номер квартала
|
||
self.related_numbers = [] # Смежные кадастровые номера из того же файла
|
||
|
||
def _extract_quarter_number(self):
|
||
"""Извлекает номер квартала из кадастрового номера."""
|
||
# Квартал - это первые три части номера (NN:NN:NNNNNNN)
|
||
parts = self.cadastral_number.split(':')
|
||
if len(parts) >= 3:
|
||
return ':'.join(parts[:3])
|
||
return None
|
||
|
||
def set_related_numbers(self, all_numbers_from_file):
|
||
"""Устанавливает список смежных кадастровых номеров из того же файла."""
|
||
# Убираем сам номер из списка смежных
|
||
self.related_numbers = [num for num in all_numbers_from_file if num != self.cadastral_number]
|
||
|
||
def get_object_type(self):
|
||
"""Определяет тип объекта по кадастровому номеру."""
|
||
parts = self.cadastral_number.split(':')
|
||
if len(parts) == 4:
|
||
# Полный кадастровый номер объекта недвижимости
|
||
return "Объект недвижимости"
|
||
elif len(parts) == 3:
|
||
# Номер квартала
|
||
return "Квартал"
|
||
else:
|
||
return "Неизвестный тип"
|
||
|
||
def __str__(self):
|
||
return f"CadastralRecord({self.cadastral_number}, квартал: {self.quarter_number})"
|
||
|
||
def __repr__(self):
|
||
return self.__str__()
|
||
|
||
class XMLFileInfo:
|
||
"""Класс для хранения информации о XML файле и его происхождении."""
|
||
|
||
def __init__(self, original_name, content, source_path, archive_name=None):
|
||
self.original_name = original_name # Оригинальное имя файла
|
||
self.content = content # Содержимое файла (bytes)
|
||
self.source_path = source_path # Полный путь к источнику (файлу или архиву)
|
||
self.archive_name = archive_name # Имя архива, если файл из архива
|
||
self.is_from_archive = archive_name is not None
|
||
self.cadastral_numbers = [] # Список найденных кадастровых номеров
|
||
self.analysis_completed = False # Флаг выполненного анализа
|
||
|
||
def analyze_xml_content(self):
|
||
"""Анализирует содержимое XML файла и извлекает кадастровые номера."""
|
||
if self.analysis_completed:
|
||
return self.cadastral_numbers
|
||
|
||
try:
|
||
# Парсим XML из bytes
|
||
root = etree.fromstring(self.content)
|
||
self.cadastral_numbers = self._extract_cadastral_numbers(root)
|
||
self.analysis_completed = True
|
||
except Exception as e:
|
||
print(f"Ошибка при анализе XML файла {self.original_name}: {e}")
|
||
self.cadastral_numbers = []
|
||
self.analysis_completed = True
|
||
|
||
return self.cadastral_numbers
|
||
|
||
def _extract_cadastral_numbers(self, element):
|
||
"""Извлекает кадастровые номера из XML элемента."""
|
||
cadastral_numbers = set()
|
||
|
||
# Ищем кадастровые номера в атрибутах
|
||
if element.attrib:
|
||
for attr_name, attr_value in element.attrib.items():
|
||
if 'CadastralNumber' in attr_name or 'cadastralnumber' in attr_name.lower():
|
||
if self._is_valid_cadastral_number(attr_value):
|
||
cadastral_numbers.add(attr_value)
|
||
elif 'Definition' in attr_name:
|
||
if self._is_valid_cadastral_number(attr_value):
|
||
cadastral_numbers.add(attr_value)
|
||
|
||
# Ищем кадастровые номера в тексте элементов
|
||
if element.text and element.text.strip():
|
||
text = element.text.strip()
|
||
if self._is_valid_cadastral_number(text):
|
||
cadastral_numbers.add(text)
|
||
|
||
# Рекурсивно проходим по всем дочерним элементам
|
||
for child in element:
|
||
# Проверяем название тега на наличие ключевых слов
|
||
tag_lower = child.tag.lower()
|
||
if any(keyword in tag_lower for keyword in ['cad_number', 'cadastralnumber', 'quarter_cad_number', 'land_cad_number']):
|
||
if child.text and child.text.strip():
|
||
text = child.text.strip()
|
||
if self._is_valid_cadastral_number(text):
|
||
cadastral_numbers.add(text)
|
||
|
||
# Рекурсивно анализируем дочерние элементы
|
||
child_numbers = self._extract_cadastral_numbers(child)
|
||
cadastral_numbers.update(child_numbers)
|
||
|
||
return list(cadastral_numbers)
|
||
|
||
def _is_valid_cadastral_number(self, value):
|
||
"""Проверяет, является ли строка валидным кадастровым номером."""
|
||
if not value or not isinstance(value, str):
|
||
return False
|
||
|
||
# Паттерн для кадастрового номера: NN:NN:NNNNNNN:NNNN
|
||
# где N - цифра, количество цифр может варьироваться
|
||
pattern = r'^\d{1,2}:\d{1,2}:\d{6,7}:\d{1,4}$'
|
||
return bool(re.match(pattern, value.strip()))
|
||
|
||
def get_cadastral_numbers(self):
|
||
"""Возвращает список кадастровых номеров (с анализом если не выполнен)."""
|
||
if not self.analysis_completed:
|
||
self.analyze_xml_content()
|
||
return self.cadastral_numbers
|
||
|
||
def create_cadastral_records(self, temp_file_path=None):
|
||
"""Создает объекты CadastralRecord для каждого найденного кадастрового номера."""
|
||
cadastral_numbers = self.get_cadastral_numbers()
|
||
records = []
|
||
|
||
for number in cadastral_numbers:
|
||
record = CadastralRecord(number, self, temp_file_path)
|
||
record.set_related_numbers(cadastral_numbers)
|
||
records.append(record)
|
||
|
||
return records
|
||
|
||
def get_display_name(self):
|
||
"""Возвращает удобное для отображения имя файла."""
|
||
if self.is_from_archive:
|
||
return f"{self.original_name} (из архива {self.archive_name})"
|
||
else:
|
||
return self.original_name
|
||
|
||
def get_safe_filename(self):
|
||
"""Возвращает безопасное имя файла для сохранения."""
|
||
if self.is_from_archive:
|
||
archive_stem = Path(self.archive_name).stem
|
||
return f"ARCHIVE_{archive_stem}_{Path(self.original_name).stem}.xml"
|
||
else:
|
||
return self.original_name
|
||
|
||
def save_to_temp(self, temp_dir):
|
||
"""Сохраняет файл во временную папку."""
|
||
safe_name = self.get_safe_filename()
|
||
dest_path = temp_dir / safe_name
|
||
|
||
# Если файл уже существует, добавляем номер
|
||
counter = 1
|
||
while dest_path.exists():
|
||
name_parts = dest_path.stem, counter, dest_path.suffix
|
||
dest_path = temp_dir / f"{name_parts[0]}_{name_parts[1]}{name_parts[2]}"
|
||
counter += 1
|
||
|
||
dest_path.write_bytes(self.content)
|
||
return dest_path
|
||
|
||
class FileSeeker:
|
||
def __init__(self, search_dir):
|
||
self.search_dir = Path(search_dir)
|
||
# Создаем временную папку рядом с программой
|
||
program_dir = Path(__file__).parent
|
||
self.temp_dir = program_dir / "kadastr14_temp"
|
||
self.temp_dir.mkdir(exist_ok=True)
|
||
print(f"Временная папка создана: {self.temp_dir}")
|
||
|
||
def find_xml_files(self):
|
||
"""Ищет все XML-файлы рекурсивно в директории поиска и возвращает объекты XMLFileInfo."""
|
||
xml_files = list(self.search_dir.rglob('*.xml'))
|
||
xml_file_objects = []
|
||
|
||
print(f"Найдено XML файлов: {len(xml_files)}")
|
||
for file_path in xml_files:
|
||
print(f" - {file_path}")
|
||
try:
|
||
content = file_path.read_bytes()
|
||
xml_info = XMLFileInfo(
|
||
original_name=file_path.name,
|
||
content=content,
|
||
source_path=str(file_path)
|
||
)
|
||
xml_file_objects.append(xml_info)
|
||
except Exception as e:
|
||
print(f" Ошибка при чтении {file_path}: {e}")
|
||
|
||
return xml_file_objects
|
||
|
||
def find_archives(self):
|
||
"""Ищет все архивы (ZIP) рекурсивно в директории поиска."""
|
||
zip_files = list(self.search_dir.rglob('*.zip'))
|
||
print(f"Найдено ZIP архивов: {len(zip_files)}")
|
||
for file in zip_files:
|
||
print(f" - {file}")
|
||
return zip_files
|
||
|
||
def extract_xml_from_archive(self, archive_path):
|
||
"""Извлекает XML файлы из архива и возвращает объекты XMLFileInfo."""
|
||
xml_file_objects = []
|
||
archive_name = archive_path.name
|
||
|
||
try:
|
||
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
||
# Получаем список файлов в архиве
|
||
file_list = zip_ref.namelist()
|
||
xml_files_in_archive = [f for f in file_list if f.lower().endswith('.xml')]
|
||
|
||
print(f" В архиве {archive_name} найдено XML файлов: {len(xml_files_in_archive)}")
|
||
|
||
for xml_file in xml_files_in_archive:
|
||
try:
|
||
# Читаем содержимое файла из архива
|
||
content = zip_ref.read(xml_file)
|
||
xml_info = XMLFileInfo(
|
||
original_name=Path(xml_file).name,
|
||
content=content,
|
||
source_path=str(archive_path),
|
||
archive_name=archive_name
|
||
)
|
||
xml_file_objects.append(xml_info)
|
||
print(f" Найден в архиве: {xml_file}")
|
||
except Exception as e:
|
||
print(f" Ошибка при извлечении {xml_file}: {e}")
|
||
|
||
except Exception as e:
|
||
print(f" Ошибка при обработке архива {archive_path}: {e}")
|
||
|
||
return xml_file_objects
|
||
|
||
def copy_to_temp(self):
|
||
"""Собирает все XML файлы (обычные и из архивов) и сохраняет их во временную папку."""
|
||
all_xml_objects = []
|
||
|
||
# Сначала собираем обычные XML файлы
|
||
print(f"\nПоиск обычных XML файлов...")
|
||
xml_objects = self.find_xml_files()
|
||
all_xml_objects.extend(xml_objects)
|
||
|
||
# Теперь обрабатываем архивы
|
||
print(f"\nПоиск и обработка архивов...")
|
||
archives = self.find_archives()
|
||
|
||
for archive_path in archives:
|
||
print(f"\nОбработка архива: {archive_path.name}")
|
||
archive_xml_objects = self.extract_xml_from_archive(archive_path)
|
||
all_xml_objects.extend(archive_xml_objects)
|
||
|
||
# Сохраняем все найденные XML файлы во временную папку
|
||
print(f"\nСохранение XML файлов в: {self.temp_dir}")
|
||
saved_files = []
|
||
|
||
for xml_obj in all_xml_objects:
|
||
try:
|
||
saved_path = xml_obj.save_to_temp(self.temp_dir)
|
||
saved_files.append(saved_path)
|
||
print(f" Сохранен: {xml_obj.get_display_name()} -> {saved_path.name}")
|
||
except Exception as e:
|
||
print(f" Ошибка при сохранении {xml_obj.get_display_name()}: {e}")
|
||
|
||
print(f"\nВсего найдено XML файлов: {len(all_xml_objects)}")
|
||
print(f"Успешно сохранено: {len(saved_files)}")
|
||
|
||
return all_xml_objects, saved_files
|
||
|
||
def collect_all_cadastral_records(self, xml_objects, saved_files):
|
||
"""Собирает все кадастровые записи из всех XML файлов."""
|
||
print(f"\n{'='*80}")
|
||
print(f"СБОР КАДАСТРОВЫХ ЗАПИСЕЙ")
|
||
print(f"{'='*80}")
|
||
|
||
all_records = []
|
||
file_path_mapping = {}
|
||
|
||
# Создаем маппинг XML объектов к путям сохраненных файлов
|
||
for i, xml_obj in enumerate(xml_objects):
|
||
if i < len(saved_files):
|
||
file_path_mapping[xml_obj] = saved_files[i]
|
||
|
||
# Собираем записи из каждого файла
|
||
for xml_obj in xml_objects:
|
||
temp_file_path = file_path_mapping.get(xml_obj)
|
||
records = xml_obj.create_cadastral_records(temp_file_path)
|
||
all_records.extend(records)
|
||
|
||
if records:
|
||
print(f"📄 {xml_obj.get_display_name()}: {len(records)} кадастровых записей")
|
||
|
||
print(f"\nВсего собрано кадастровых записей: {len(all_records)}")
|
||
return all_records
|
||
|
||
def print_cadastral_records_summary(self, records):
|
||
"""Выводит сводку по всем кадастровым записям."""
|
||
print(f"\n{'='*80}")
|
||
print(f"СВОДКА ПО КАДАСТРОВЫМ ЗАПИСЯМ")
|
||
print(f"{'='*80}")
|
||
|
||
if not records:
|
||
print("Кадастровые записи не найдены.")
|
||
return
|
||
|
||
# Группируем по типам объектов
|
||
by_type = {}
|
||
by_quarter = {}
|
||
unique_numbers = set()
|
||
|
||
for record in records:
|
||
obj_type = record.get_object_type()
|
||
if obj_type not in by_type:
|
||
by_type[obj_type] = []
|
||
by_type[obj_type].append(record)
|
||
|
||
if record.quarter_number:
|
||
if record.quarter_number not in by_quarter:
|
||
by_quarter[record.quarter_number] = []
|
||
by_quarter[record.quarter_number].append(record)
|
||
|
||
unique_numbers.add(record.cadastral_number)
|
||
|
||
print(f"📊 Общая статистика:")
|
||
print(f" Всего записей: {len(records)}")
|
||
print(f" Уникальных номеров: {len(unique_numbers)}")
|
||
print(f" Кварталов: {len(by_quarter)}")
|
||
|
||
print(f"\n📋 По типам объектов:")
|
||
for obj_type, type_records in by_type.items():
|
||
print(f" {obj_type}: {len(type_records)} записей")
|
||
|
||
print(f"\n🏘️ По кварталам (топ-10):")
|
||
sorted_quarters = sorted(by_quarter.items(), key=lambda x: len(x[1]), reverse=True)
|
||
for quarter, quarter_records in sorted_quarters[:10]:
|
||
print(f" {quarter}: {len(quarter_records)} объектов")
|
||
|
||
def print_detailed_cadastral_records(self, records, max_records=20):
|
||
"""Выводит детальную информацию о кадастровых записях."""
|
||
print(f"\n{'='*80}")
|
||
print(f"ДЕТАЛЬНАЯ ИНФОРМАЦИЯ О КАДАСТРОВЫХ ЗАПИСЯХ (первые {max_records})")
|
||
print(f"{'='*80}")
|
||
|
||
for i, record in enumerate(records[:max_records], 1):
|
||
print(f"\n{i}. 🏠 {record.cadastral_number}")
|
||
print(f" 📍 Тип: {record.get_object_type()}")
|
||
if record.quarter_number:
|
||
print(f" 🏘️ Квартал: {record.quarter_number}")
|
||
|
||
print(f" 📁 Источник: {record.source_xml_info.get_display_name()}")
|
||
print(f" 📂 Оригинальный файл: {record.source_xml_info.source_path}")
|
||
|
||
if record.temp_file_path:
|
||
print(f" 💾 Временный файл: {record.temp_file_path}")
|
||
|
||
if record.related_numbers:
|
||
print(f" 🔗 Смежные номера ({len(record.related_numbers)}):")
|
||
for j, related in enumerate(record.related_numbers[:3], 1):
|
||
print(f" {j}. {related}")
|
||
if len(record.related_numbers) > 3:
|
||
print(f" ... и еще {len(record.related_numbers) - 3} номеров")
|
||
else:
|
||
print(f" 🔗 Смежные номера: нет")
|
||
|
||
if len(records) > max_records:
|
||
print(f"\n... и еще {len(records) - max_records} записей")
|
||
|
||
def print_all_objects_info(self, xml_objects):
|
||
"""Выводит подробную информацию о всех найденных XML объектах."""
|
||
print(f"\n{'='*80}")
|
||
print(f"ПОДРОБНАЯ ИНФОРМАЦИЯ О ВСЕХ НАЙДЕННЫХ XML ОБЪЕКТАХ")
|
||
print(f"{'='*80}")
|
||
print(f"Всего объектов: {len(xml_objects)}")
|
||
|
||
if not xml_objects:
|
||
print("Объекты не найдены.")
|
||
return
|
||
|
||
# Статистика по источникам
|
||
from_files = sum(1 for obj in xml_objects if not obj.is_from_archive)
|
||
from_archives = sum(1 for obj in xml_objects if obj.is_from_archive)
|
||
|
||
print(f"\nСтатистика по источникам:")
|
||
print(f" - Из обычных файлов: {from_files}")
|
||
print(f" - Из архивов: {from_archives}")
|
||
|
||
# Группируем по архивам для удобства
|
||
archive_groups = {}
|
||
regular_files = []
|
||
|
||
for obj in xml_objects:
|
||
if obj.is_from_archive:
|
||
if obj.archive_name not in archive_groups:
|
||
archive_groups[obj.archive_name] = []
|
||
archive_groups[obj.archive_name].append(obj)
|
||
else:
|
||
regular_files.append(obj)
|
||
|
||
# Выводим обычные файлы
|
||
if regular_files:
|
||
print(f"\n{'-'*60}")
|
||
print(f"ОБЫЧНЫЕ XML ФАЙЛЫ ({len(regular_files)} файлов)")
|
||
print(f"{'-'*60}")
|
||
|
||
for i, obj in enumerate(regular_files, 1):
|
||
self._print_object_details(obj, i)
|
||
|
||
# Выводим файлы из архивов
|
||
if archive_groups:
|
||
print(f"\n{'-'*60}")
|
||
print(f"XML ФАЙЛЫ ИЗ АРХИВОВ ({len(archive_groups)} архивов, {from_archives} файлов)")
|
||
print(f"{'-'*60}")
|
||
|
||
counter = 1
|
||
for archive_name, objects in archive_groups.items():
|
||
print(f"\n📦 АРХИВ: {archive_name}")
|
||
print(f" Путь: {objects[0].source_path}")
|
||
print(f" XML файлов в архиве: {len(objects)}")
|
||
|
||
for obj in objects:
|
||
self._print_object_details(obj, counter, indent=" ")
|
||
counter += 1
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"ИТОГО: {len(xml_objects)} XML файлов обработано")
|
||
print(f"{'='*80}")
|
||
|
||
def _print_object_details(self, obj, number, indent=""):
|
||
"""Выводит детальную информацию об одном XML объекте."""
|
||
print(f"\n{indent}{number}. {obj.get_display_name()}")
|
||
print(f"{indent} 📄 Исходное имя: {obj.original_name}")
|
||
print(f"{indent} 📁 Источник: {obj.source_path}")
|
||
print(f"{indent} 💾 Размер содержимого: {len(obj.content):,} байт")
|
||
print(f"{indent} 🗂️ Безопасное имя: {obj.get_safe_filename()}")
|
||
|
||
# Запускаем анализ кадастровых номеров если не выполнен
|
||
cadastral_numbers = obj.get_cadastral_numbers() # Это запустит анализ если нужно
|
||
cadastral_count = len(cadastral_numbers)
|
||
|
||
print(f"{indent} 🔍 Анализ завершен: найдено {cadastral_count} кадастровых номеров")
|
||
if cadastral_count > 0:
|
||
# Показываем первые 3 номера
|
||
for i, num in enumerate(cadastral_numbers[:3], 1):
|
||
print(f"{indent} {i}. {num}")
|
||
if cadastral_count > 3:
|
||
print(f"{indent} ... и еще {cadastral_count - 3} номеров")
|
||
else:
|
||
print(f"{indent} ❌ Кадастровые номера не найдены")
|
||
|
||
if __name__ == "__main__":
|
||
# Пример использования - ищем во всей папке с примерами
|
||
search_path = r"C:\Users\USER\Downloads\14. выборка выписок из ЕГРН по списку КН"
|
||
print(f"Начинаем поиск XML файлов в: {search_path}")
|
||
print("Поиск будет выполняться рекурсивно во всех подпапках...")
|
||
|
||
seeker = FileSeeker(search_path)
|
||
xml_objects, saved_files = seeker.copy_to_temp()
|
||
|
||
print(f"\nГотово! Временная папка: {seeker.temp_dir}")
|
||
print(f"Найдено XML объектов: {len(xml_objects)}")
|
||
print(f"Сохранено файлов: {len(saved_files)}")
|
||
|
||
# Собираем все кадастровые записи
|
||
cadastral_records = seeker.collect_all_cadastral_records(xml_objects, saved_files)
|
||
|
||
# Выводим сводку
|
||
seeker.print_cadastral_records_summary(cadastral_records)
|
||
|
||
# Выводим детальную информацию
|
||
seeker.print_detailed_cadastral_records(cadastral_records, max_records=10)
|
||
|
||
# Дополнительный анализ - находим записи с наибольшим количеством смежных номеров
|
||
if cadastral_records:
|
||
print(f"\n{'='*80}")
|
||
print(f"ЗАПИСИ С НАИБОЛЬШИМ КОЛИЧЕСТВОМ СМЕЖНЫХ НОМЕРОВ")
|
||
print(f"{'='*80}")
|
||
|
||
# Сортируем по количеству смежных номеров
|
||
sorted_by_related = sorted(cadastral_records, key=lambda x: len(x.related_numbers), reverse=True)
|
||
|
||
for i, record in enumerate(sorted_by_related[:5], 1):
|
||
print(f"\n{i}. {record.cadastral_number}")
|
||
print(f" Смежных номеров: {len(record.related_numbers)}")
|
||
print(f" Источник: {record.source_xml_info.get_display_name()}")
|
||
if record.related_numbers:
|
||
print(f" Связанные номера: {', '.join(record.related_numbers[:5])}")
|
||
if len(record.related_numbers) > 5:
|
||
print(f" ... и еще {len(record.related_numbers) - 5} номеров")
|
||
|
||
print(f"\n{'='*80}")
|
||
print(f"АНАЛИЗ ЗАВЕРШЕН")
|
||
print(f"{'='*80}")
|
||
print(f"📊 Всего кадастровых записей: {len(cadastral_records)}")
|
||
print(f"📁 Временная папка: {seeker.temp_dir}")
|
||
print(f"💾 Сохранено XML файлов: {len(saved_files)}")
|