Files
kadastr14/seekers.py
2025-08-03 22:50:32 +05:00

506 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)}")