diff --git a/.gitignore b/.gitignore index ab7e7b7..cf840c0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ build/ build_32bit/ dist_32bit/ dist/ -testset/ \ No newline at end of file +testset/ +test_results/ \ No newline at end of file diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md deleted file mode 100644 index 1ad1458..0000000 --- a/BUILD_INSTRUCTIONS.md +++ /dev/null @@ -1,52 +0,0 @@ -# Инструкция по созданию 32-битной версии программы - -## Текущее состояние -- ✅ Программа успешно работает на 64-битных системах -- ❌ Для 32-битных систем требуется пересборка - -## Для создания 32-битной версии: - -### Шаг 1: Установка 32-битного Python -1. Скачайте Python 3.12 (32-bit) с https://www.python.org/downloads/ -2. При установке выберите "Add to PATH" -3. Убедитесь, что установлен именно 32-битный Python - -### Шаг 2: Проверка архитектуры -```bash -python -c "import platform; print(platform.architecture())" -# Должно показать: ('32bit', 'WindowsPE') -``` - -### Шаг 3: Установка зависимостей -```bash -pip install pyinstaller pandas openpyxl lxml pathlib2 -``` - -### Шаг 4: Сборка программы -```bash -python build_32bit.py -``` - -## Альтернативный вариант - -Если у вас нет доступа к 32-битному Python, можно: - -1. **Использовать виртуальную машину** с 32-битной Windows -2. **Попросить кого-то собрать** на 32-битной системе -3. **Использовать Docker** с 32-битным образом Python - -## Проверка совместимости -Созданный `KadastrParser.exe` (64-bit) будет работать на: -- ✅ Windows 7/8/10/11 (64-bit) -- ❌ Windows 7/8/10/11 (32-bit) - -Для универсальности рекомендуется создать обе версии: -- `KadastrParser_64bit.exe` - для 64-битных систем -- `KadastrParser_32bit.exe` - для 32-битных систем - -## Размер программы -- 64-битная версия: ~50-70 MB -- 32-битная версия: ~45-65 MB - -## Тестирование -Перед выпуском обязательно протестируйте на целевой системе! diff --git a/KadastrParser.spec b/KadastrParser.spec deleted file mode 100644 index 22f9580..0000000 --- a/KadastrParser.spec +++ /dev/null @@ -1,39 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['main.py'], - pathex=[], - binaries=[], - datas=[('services', 'services'), ('models', 'models')], - hiddenimports=['tkinter', 'pandas', 'openpyxl', 'lxml'], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='KadastrParser', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - icon='NONE', -) diff --git a/VERSIONS_INFO.md b/VERSIONS_INFO.md deleted file mode 100644 index 2625fcb..0000000 --- a/VERSIONS_INFO.md +++ /dev/null @@ -1,117 +0,0 @@ -# KadastrParser - Парсер кадастровых номеров - -## 📦 Доступные версии - -### 🖥️ 64-битная версия -- **Файл**: `dist/KadastrParser.exe` -- **Размер**: ~52 MB -- **Совместимость**: Windows 7/8/10/11 (только 64-bit) -- **Python**: 3.12.11 (64-bit) - -### 💻 32-битная версия -- **Файл**: `dist_32bit/KadastrParser_32bit.exe` -- **Размер**: ~29 MB -- **Совместимость**: Windows 7/8/10/11 (32-bit и 64-bit) -- **Python**: 3.8.20 (32-bit) - -## ✅ Ответ на ваш вопрос - -**Да, программа будет запускаться на 32-битной системе!** - -Используйте файл `KadastrParser_32bit.exe` из папки `dist_32bit/`. - -## 🎯 Рекомендации по выбору версии - -### Для 32-битных систем: -- ✅ Используйте `KadastrParser_32bit.exe` -- ✅ Меньший размер файла -- ✅ Гарантированная совместимость - -### Для 64-битных систем: -- 🔄 Можно использовать любую версию -- 🚀 `KadastrParser.exe` - более новый Python, быстрее -- 💾 `KadastrParser_32bit.exe` - универсальная - -## 🔧 Функциональность - -Обе версии имеют **абсолютно одинаковый функционал**: - -### ✨ Основные возможности: -- 📁 Поиск XML файлов в директории -- 🔍 Извлечение кадастровых номеров -- 📊 Обработка Excel файлов со списками номеров -- 📋 Создание подробных отчетов -- 🗂️ Организация файлов по папкам - -### 📈 Excel интеграция: -- ✅ **Найденные файлы** - где обнаружен номер -- ✅ **Смежные кадастровые номера** - другие номера из тех же файлов -- ✅ **Кадастровый блок** - первые 3 части номера -- ✅ Автоматическое обновление Excel с путями к файлам - -### 📁 Структура результатов: -``` -results/ -├── all_xml_files/ # Все найденные XML файлы -├── matching_cadastral_files/ # Файлы из Excel списка -├── updated_cadastral_list.xlsx # Обновленный Excel файл -└── processing_report.txt # Подробный отчет -``` - -## 🚀 Как использовать: - -1. **Скачайте нужную версию**: - - 32-bit система → `KadastrParser_32bit.exe` - - 64-bit система → `KadastrParser.exe` или `KadastrParser_32bit.exe` - -2. **Поместите файл в любую папку** - -3. **Запустите двойным кликом** - -4. **Выберите папку с XML файлами** - -5. **Опционально: выберите Excel файл** - -6. **Нажмите "Начать обработку"** - - ✅ **В этот момент автоматически создается папка `results/` в той же папке, где лежит программа** - - ✅ **Создаются подпапки для организации файлов** - -## ⏰ Когда и где создаются директории: - -- 📁 **Сразу при нажатии "Начать обработку"** программа создает папку `results/` **в той же директории, где находится исполняемый файл**: - - `results/` - основная папка результатов - - `results/all_xml_files/` - для всех найденных XML файлов - - `results/matching_cadastral_files/` - для файлов из Excel списка - -- 📄 **Во время обработки** добавляются: - - `results/processing_report.txt` - подробный отчет - - `results/updated_cadastral_list.xlsx` - обновленный Excel (если указан) - -### 📍 Пример расположения файлов: -``` -C:\MyFolder\ -├── KadastrParser.exe # Ваша программа -└── results/ # Создается автоматически - ├── all_xml_files/ - ├── matching_cadastral_files/ - ├── processing_report.txt - └── updated_cadastral_list.xlsx -``` - -## 💡 Важные особенности: - -- 🔄 **Без консоли** - чистый графический интерфейс -- 📦 **Один файл** - не требует установки -- 🛡️ **Безопасно** - только чтение XML и создание результатов -- 🎨 **Интуитивный интерфейс** - простое использование -- ⚡ **Быстрая обработка** - многопоточность - -## 🔍 Поддерживаемые форматы: - -- **Кадастровые номера**: `XX:XX:XXXXXXX:XXX` -- **XML файлы**: все варианты из ЕГРН -- **Excel файлы**: `.xlsx` и `.xls` - ---- - -**Итог**: Для 32-битной системы используйте `KadastrParser_32bit.exe` - он гарантированно будет работать! diff --git a/build_32bit.py b/build_32bit.py deleted file mode 100644 index f19691a..0000000 --- a/build_32bit.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Скрипт для создания 32-битной версии программы -ВАЖНО: Этот скрипт нужно запускать на 32-битном Python! -""" -import PyInstaller.__main__ -import sys -import platform - -def check_architecture(): - """Проверяет архитектуру Python""" - arch = platform.architecture()[0] - machine = platform.machine() - - print(f"🔍 Текущая архитектура Python: {arch}") - print(f"🖥️ Тип машины: {machine}") - - if arch == '32bit': - print("✅ Python 32-bit - подходит для создания 32-битной программы") - return True - else: - print("❌ Python 64-bit - НЕ подходит для создания 32-битной программы") - print("💡 Для создания 32-битной версии нужен 32-битный Python") - return False - -def build_32bit_executable(): - """Создает 32-битный исполняемый файл""" - - if not check_architecture(): - print("\n⚠️ ВНИМАНИЕ: Для создания 32-битной программы:") - print("1. Скачайте 32-битный Python с https://www.python.org/downloads/") - print("2. Установите зависимости: pip install pyinstaller pandas openpyxl lxml") - print("3. Запустите этот скрипт снова") - return False - - # Параметры для PyInstaller (32-бит) - args = [ - 'main.py', - '--onefile', - '--windowed', - '--name=KadastrParser_32bit', - '--icon=NONE', - '--add-data=services;services', - '--add-data=models;models', - '--hidden-import=tkinter', - '--hidden-import=pandas', - '--hidden-import=openpyxl', - '--hidden-import=lxml', - '--clean', - '--noconfirm', - '--target-architecture=x86', # Явно указываем 32-бит - ] - - print("\n🚀 Начинаем создание 32-битного исполняемого файла...") - print("📋 Параметры сборки:") - for arg in args: - print(f" {arg}") - print() - - try: - # Запускаем PyInstaller - PyInstaller.__main__.run(args) - print("✅ 32-битная сборка завершена!") - print("📁 Исполняемый файл: dist/KadastrParser_32bit.exe") - return True - except Exception as e: - print(f"❌ Ошибка при сборке: {e}") - return False - -if __name__ == "__main__": - print("🔧 Создание 32-битной версии программы") - print("=" * 50) - build_32bit_executable() diff --git a/check_fix.py b/check_fix.py new file mode 100644 index 0000000..6369f97 --- /dev/null +++ b/check_fix.py @@ -0,0 +1,24 @@ +import pandas as pd + +df = pd.read_excel('test_fix.xlsx') +print('Проверяем данные в Excel:') +print('Всего строк:', len(df)) +filled = df[df['Найден в файле'].notna() & (df['Найден в файле'] != '')] +print('Заполненных строк:', len(filled)) + +if len(filled) > 0: + for i, (idx, row) in enumerate(filled.head(3).iterrows()): + print(f'Строка {i+1}:') + print(f' Номер в Excel: {row.iloc[0]}') + main_num = row['Основной номер объекта'] + additional = row['Дополнительные номера'] + print(f' Основной: {main_num}') + print(f' Дополнительные: {additional}') + + # Проверяем, что основной номер не дублируется + if pd.notna(main_num) and pd.notna(additional): + main_in_additional = str(main_num) in str(additional) + print(f' ✅ Основной номер НЕ повторяется в дополнительных: {not main_in_additional}') + print() +else: + print('❌ Не найдено заполненных строк') diff --git a/main.py b/main.py index 5e4472f..0d17f61 100644 --- a/main.py +++ b/main.py @@ -89,19 +89,14 @@ def Pars(): messagebox.showwarning("Предупреждение", "Кадастровые номера не найдены в XML файлах") return - # 4. Создаем карту номер -> файл для быстрого поиска + # 4. Создаем карту номер -> файл для быстрого поиска (для обратной совместимости) cadastral_to_file_map = {} - file_to_numbers_map = {} # файл -> множество номеров для поиска смежных for record in records: - # Для основной карты берем первое вхождение номера - if record.number not in cadastral_to_file_map: - cadastral_to_file_map[record.number] = record.file_path - - # Для карты файл->номера добавляем все номера из каждого файла - if record.file_path not in file_to_numbers_map: - file_to_numbers_map[record.file_path] = set() - file_to_numbers_map[record.file_path].add(record.number) + # Добавляем все номера из объекта в карту + for number in record.all_numbers: + if number not in cadastral_to_file_map: # Берем первый найденный файл + cadastral_to_file_map[number] = record.file_path # 5. Обработка Excel файла и организация файлов excel_updated = False @@ -115,13 +110,14 @@ def Pars(): excel_numbers = excel_processor.get_cadastral_numbers() # Находим пересечения между Excel и найденными номерами - found_numbers = set(cadastral_to_file_map.keys()) - matching_numbers = excel_numbers.intersection(found_numbers) + # Теперь ищем по всем номерам в объектах, не только по основным + all_found_numbers = seeker.get_all_cadastral_numbers() + matching_numbers = excel_numbers.intersection(all_found_numbers) if matching_numbers: - # Обновляем Excel файл информацией о найденных файлах + # Обновляем Excel файл информацией о найденных объектах output_excel_path = str(output_directory / "updated_cadastral_list.xlsx") - excel_processor.update_excel_with_file_info(cadastral_to_file_map, output_excel_path) + excel_processor.update_excel_with_cadastral_records(records, output_excel_path) excel_updated = True # Копируем файлы с номерами из Excel в отдельную папку @@ -221,8 +217,10 @@ def create_report(output_directory, stats, unique_numbers, moved_files, excel_up f.write(f"Общая статистика:\n") f.write(f"- Всего XML файлов найдено: {stats['total_xml_files']}\n") f.write(f"- Файлов с кадастровыми номерами: {stats['files_with_numbers']}\n") - f.write(f"- Всего кадастровых номеров: {stats['total_cadastral_numbers']}\n") - f.write(f"- Уникальных номеров: {stats['unique_cadastral_numbers']}\n") + f.write(f"- Всего кадастровых объектов: {stats['total_cadastral_objects']}\n") + f.write(f"- Уникальных основных номеров: {stats['unique_main_numbers']}\n") + f.write(f"- Всего номеров во всех объектах: {stats['total_all_numbers']}\n") + f.write(f"- Всего уникальных номеров: {stats['unique_all_numbers']}\n") f.write(f"- Файлов скопировано в 'all_xml_files': {len(moved_files)}\n") if matching_files_count > 0: diff --git a/models/cadastral_record.py b/models/cadastral_record.py index 31a8a1b..3a673a8 100644 --- a/models/cadastral_record.py +++ b/models/cadastral_record.py @@ -1,14 +1,33 @@ """ -Модель для хранения информации о кадастровом номере. +Модель для хранения информации о кадастровом объекте. """ +from typing import Set class CadastralRecord: - def __init__(self, number: str, file_path: str): - self.number = number + def __init__(self, main_number: str, file_path: str, all_numbers: Set[str] = None): + """ + Инициализация записи о кадастровом объекте + + Args: + main_number: Основной кадастровый номер объекта + file_path: Путь к XML файлу + all_numbers: Все кадастровые номера, найденные в том же XML файле + """ + self.number = main_number # Оставляем для обратной совместимости + self.main_number = main_number self.file_path = file_path + self.all_numbers = all_numbers if all_numbers is not None else {main_number} + + @property + def other_numbers(self) -> Set[str]: + """Возвращает все номера кроме основного""" + return self.all_numbers - {self.main_number} def __str__(self): - return f"Кадастровый номер: {self.number}, файл: {self.file_path}" + other_count = len(self.other_numbers) + if other_count > 0: + return f"Объект {self.main_number} (+{other_count} номеров), файл: {self.file_path}" + return f"Объект {self.main_number}, файл: {self.file_path}" def __repr__(self): - return f"CadastralRecord('{self.number}', '{self.file_path}')" + return f"CadastralRecord('{self.main_number}', '{self.file_path}', {self.all_numbers})" diff --git a/models/xml_file_info.py b/models/xml_file_info.py index 2b16b5f..15f4b39 100644 --- a/models/xml_file_info.py +++ b/models/xml_file_info.py @@ -3,60 +3,104 @@ """ import re from pathlib import Path -from typing import List, Set, Optional +from typing import List, Set, Optional, Tuple from lxml import etree class XMLFileInfo: def __init__(self, file_path: str): self.file_path = Path(file_path) - self.cadastral_numbers: Set[str] = set() + self.main_cadastral_number: Optional[str] = None + self.all_cadastral_numbers: Set[str] = set() - def extract_cadastral_numbers(self) -> Set[str]: - """Извлекает кадастровые номера из XML файла""" + def extract_cadastral_data(self) -> Tuple[Optional[str], Set[str]]: + """ + Извлекает основной номер и все номера из XML файла + Возвращает (основной_номер, все_номера_в_файле) + """ if not self.file_path.exists(): - return set() + return None, set() try: - # Парсим XML файл tree = etree.parse(str(self.file_path)) root = tree.getroot() - # Используем улучшенный алгоритм извлечения - return self._extract_cadastral_numbers_enhanced(root) + # Находим все кадастровые номера в файле + all_numbers = self._find_all_cadastral_numbers(root) + + # Определяем основной номер (из объекта) + main_number = self._find_main_cadastral_number(root, all_numbers) + + self.main_cadastral_number = main_number + self.all_cadastral_numbers = all_numbers + + return main_number, all_numbers except Exception as e: print(f"Ошибка при обработке файла {self.file_path}: {e}") - return set() + return None, set() - def _extract_cadastral_numbers_enhanced(self, root) -> Set[str]: - """Улучшенный алгоритм извлечения кадастровых номеров""" - cadastral_numbers = set() + def _find_all_cadastral_numbers(self, root) -> Set[str]: + """Находит все валидные кадастровые номера в XML""" + all_numbers = set() - # 1. Поиск по известным тегам - tags_to_search = ['cad_number', 'land_cad_number', 'cadastral_number', 'parcel_cad_number'] - for tag in tags_to_search: - elements = root.xpath(f".//{tag}") + # Ищем по всем элементам + for elem in root.iter(): + if elem.text and self._is_valid_cadastral_number(elem.text): + all_numbers.add(elem.text.strip()) + + return all_numbers + + def _find_main_cadastral_number(self, root, all_numbers: Set[str]) -> Optional[str]: + """ + Определяет основной кадастровый номер из объекта + Приоритет: object > другие контейнеры > первый найденный + """ + # 1. Ищем в объектах - высший приоритет + objects = root.xpath(".//object") + for obj in objects: + number = self._get_first_cadastral_from_element(obj) + if number and number in all_numbers: + return number + + # 2. Ищем в других контейнерах объектов недвижимости + containers = [ + 'land_record', 'building_record', 'construction_record', 'flat_record', + 'extract_about_property_land', 'extract_about_property_building', + 'extract_about_property_flat', 'extract_about_property_construction', + 'property_object', 'realty_object' + ] + + for container in containers: + elements = root.xpath(f".//{container}") + for elem in elements: + number = self._get_first_cadastral_from_element(elem) + if number and number in all_numbers: + return number + + # 3. Если ничего не найдено в контейнерах, берем первый из всех найденных + if all_numbers: + return sorted(all_numbers)[0] + + return None + + def _get_first_cadastral_from_element(self, element) -> Optional[str]: + """Находит первый кадастровый номер в элементе""" + # Приоритетные теги для поиска + priority_tags = ['cad_number', 'cadastral_number', 'land_cad_number', 'parcel_cad_number'] + + # Сначала ищем по приоритетным тегам + for tag in priority_tags: + elements = element.xpath(f".//{tag}") for elem in elements: if elem.text and self._is_valid_cadastral_number(elem.text): - cadastral_numbers.add(elem.text.strip()) + return elem.text.strip() - # 2. Поиск в атрибутах - for elem in root.iter(): - for attr_name, attr_value in elem.attrib.items(): - if any(keyword in attr_name.lower() for keyword in ['cad', 'cadastral', 'кадастр']): - if self._is_valid_cadastral_number(attr_value): - cadastral_numbers.add(attr_value.strip()) + # Если в приоритетных тегах не найдено, ищем по всем элементам + for elem in element.iter(): + if elem.text and self._is_valid_cadastral_number(elem.text): + return elem.text.strip() - # 3. Regex поиск по тексту - text_content = etree.tostring(root, encoding='unicode', method='text') - if text_content: - regex_matches = re.findall(r'\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}', text_content) - for match in regex_matches: - if self._is_valid_cadastral_number(match): - cadastral_numbers.add(match) - - self.cadastral_numbers = cadastral_numbers - return cadastral_numbers + return None def _is_valid_cadastral_number(self, text: str) -> bool: """Проверяет, является ли строка валидным кадастровым номером""" @@ -68,4 +112,4 @@ class XMLFileInfo: return bool(re.match(pattern, text.strip())) def __str__(self): - return f"XML файл: {self.file_path.name}, номеров: {len(self.cadastral_numbers)}" + return f"XML файл: {self.file_path.name}, основной номер: {self.main_cadastral_number}, всего номеров: {len(self.all_cadastral_numbers)}" diff --git a/services/excel_processor.py b/services/excel_processor.py index 6f7038a..174fb5a 100644 --- a/services/excel_processor.py +++ b/services/excel_processor.py @@ -90,46 +90,57 @@ class ExcelProcessor: 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 файл информацией о найденных файлах и смежных номерах""" + def update_excel_with_cadastral_records(self, cadastral_records: List, + output_file_path: str = None) -> bool: + """Обновляет Excel файл информацией о найденных кадастровых объектах""" try: + from models.cadastral_record import CadastralRecord + 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) + # Создаем карты для быстрого поиска + # номер -> запись объекта + number_to_record = {} + for record in cadastral_records: + # Основной номер + number_to_record[record.main_number] = record + # Все остальные номера тоже ведут к тому же объекту + for number in record.other_numbers: + number_to_record[number] = record # Добавляем новые колонки - self.df['Найденные файлы'] = '' - self.df['Смежные кадастровые номера'] = '' + self.df['Найден в файле'] = '' + self.df['Основной номер объекта'] = '' + self.df['Дополнительные номера'] = '' self.df['Кадастровый блок'] = '' # Заполняем информацию для каждой строки + matched_count = 0 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 + if cadastral_number in number_to_record: + record = number_to_record[cadastral_number] + matched_count += 1 - # 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)) + # 1. Имя файла + file_name = Path(record.file_path).name + self.df.at[index, 'Найден в файле'] = file_name - # 3. Определяем кадастровый блок (первые три части номера) - cadastral_block = self._extract_cadastral_block(cadastral_number) + # 2. Основной номер объекта + self.df.at[index, 'Основной номер объекта'] = record.main_number + + # 3. Дополнительные номера в объекте (без основного) + other_numbers_sorted = sorted(record.other_numbers) + if other_numbers_sorted: + self.df.at[index, 'Дополнительные номера'] = ', '.join(other_numbers_sorted) + else: + self.df.at[index, 'Дополнительные номера'] = '' + + # 4. Кадастровый блок (от основного номера) + cadastral_block = self._extract_cadastral_block(record.main_number) if cadastral_block: self.df.at[index, 'Кадастровый блок'] = cadastral_block @@ -138,6 +149,7 @@ class ExcelProcessor: self.df.to_excel(output_path, index=False, engine='openpyxl') print(f"Обновленный Excel файл сохранен: {output_path}") + print(f"Найдено совпадений: {matched_count} из {len(self.cadastral_numbers)}") return True except Exception as e: diff --git a/services/file_seeker.py b/services/file_seeker.py index aefa0a9..c22c67f 100644 --- a/services/file_seeker.py +++ b/services/file_seeker.py @@ -37,39 +37,52 @@ class FileSeeker: return xml_files def process_files(self) -> List[CadastralRecord]: - """Обрабатывает все найденные XML файлы и извлекает кадастровые номера""" + """Обрабатывает все найденные XML файлы и создает записи с основными и всеми номерами""" all_records = [] for xml_file in self.xml_files: - cadastral_numbers = xml_file.extract_cadastral_numbers() + main_number, all_numbers = xml_file.extract_cadastral_data() - for number in cadastral_numbers: - record = CadastralRecord(number, str(xml_file.file_path)) + if main_number and all_numbers: + record = CadastralRecord(main_number, str(xml_file.file_path), all_numbers) all_records.append(record) self.cadastral_records = all_records - print(f"Всего найдено кадастровых номеров: {len(all_records)}") + print(f"Создано записей объектов: {len(all_records)}") + + # Выводим статистику + total_numbers = sum(len(record.all_numbers) for record in all_records) + print(f"Всего номеров во всех объектах: {total_numbers}") + return all_records def get_unique_cadastral_numbers(self) -> Set[str]: - """Возвращает уникальные кадастровые номера""" - return {record.number for record in self.cadastral_records} + """Возвращает уникальные основные кадастровые номера""" + return {record.main_number for record in self.cadastral_records} + + def get_all_cadastral_numbers(self) -> Set[str]: + """Возвращает все уникальные кадастровые номера из всех объектов""" + all_numbers = set() + for record in self.cadastral_records: + all_numbers.update(record.all_numbers) + return all_numbers 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: + # Проверяем, есть ли какой-либо из номеров объекта в списке для перемещения + if any(num in cadastral_numbers_to_move for num in record.all_numbers): source_path = Path(record.file_path) if source_path.exists(): # Создаем уникальное имя для файла во временной папке - dest_name = f"{record.number.replace(':', '_')}_{source_path.name}" + dest_name = f"{record.main_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) + moved_files[record.main_number] = str(dest_path) print(f"Файл скопирован: {source_path.name} -> {dest_path}") except Exception as e: print(f"Ошибка при копировании файла {source_path}: {e}") @@ -78,14 +91,20 @@ class FileSeeker: def get_statistics(self) -> Dict[str, int]: """Возвращает статистику обработки""" + total_all_numbers = sum(len(record.all_numbers) for record in self.cadastral_records) + 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()) + 'files_with_numbers': len([f for f in self.xml_files if f.main_cadastral_number]), + 'total_cadastral_objects': len(self.cadastral_records), + 'unique_main_numbers': len(self.get_unique_cadastral_numbers()), + 'total_all_numbers': total_all_numbers, + 'unique_all_numbers': len(self.get_all_cadastral_numbers()) } def __str__(self): stats = self.get_statistics() return (f"FileSeeker: {stats['total_xml_files']} XML файлов, " - f"{stats['unique_cadastral_numbers']} уникальных номеров") + f"{stats['total_cadastral_objects']} объектов, " + f"{stats['unique_main_numbers']} уникальных основных номеров, " + f"{stats['unique_all_numbers']} всего уникальных номеров") diff --git a/test_modules.py b/test_modules.py deleted file mode 100644 index 1e14c16..0000000 --- a/test_modules.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Тестовый скрипт для проверки работы новой модульной структуры -""" - -def test_modules(): - """Тестирует импорт и базовую функциональность модулей""" - - print("=== ТЕСТ МОДУЛЬНОЙ СТРУКТУРЫ ===\n") - - try: - # Тест импорта моделей - print("1. Тестирование моделей...") - from models.cadastral_record import CadastralRecord - from models.xml_file_info import XMLFileInfo - - # Создаем тестовую запись - record = CadastralRecord("74:34:0400003:1234", "test.xml") - print(f" ✅ CadastralRecord создан: {record}") - - # Тест сервисов - print("\n2. Тестирование сервисов...") - from services.file_seeker import FileSeeker - from services.excel_processor import ExcelProcessor - - # Создаем FileSeeker с тестовой директорией - import tempfile - temp_dir = tempfile.mkdtemp() - seeker = FileSeeker(temp_dir) - print(f" ✅ FileSeeker создан: {seeker}") - - # Тест статистики - stats = seeker.get_statistics() - print(f" ✅ Статистика получена: {stats}") - - print("\n3. Тестирование Excel процессора...") - # Создаем фиктивный Excel процессор - excel_path = "test.xlsx" # Не существует, но это OK для теста импорта - try: - excel_processor = ExcelProcessor(excel_path) - print(f" ✅ ExcelProcessor создан для: {excel_path}") - except Exception as e: - print(f" ⚠️ ExcelProcessor создан (ожидаемая ошибка файла): {type(e).__name__}") - - print("\n🎉 ВСЕ МОДУЛИ УСПЕШНО ИМПОРТИРОВАНЫ И ПРОТЕСТИРОВАНЫ!") - - return True - - except ImportError as e: - print(f"❌ Ошибка импорта: {e}") - return False - except Exception as e: - print(f"❌ Неожиданная ошибка: {e}") - return False - -if __name__ == "__main__": - test_modules() diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..21a1261 --- /dev/null +++ b/tests.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Объединенный класс тестов для проекта KadastrParser +""" + +import os +import tempfile +from pathlib import Path +import unittest +from typing import Optional + +# Импорт наших модулей +from models.cadastral_record import CadastralRecord +from models.xml_file_info import XMLFileInfo +from services.file_seeker import FileSeeker +from services.excel_processor import ExcelProcessor + + +class KadastrParserTests(unittest.TestCase): + """Объединенный класс тестов для всех компонентов системы""" + + @classmethod + def setUpClass(cls): + """Настройка тестовой среды""" + cls.test_xml_dir = r"c:\Users\USER\Downloads\14. выборка выписок из ЕГРН по списку КН\пример исх.папки 2 вар" + cls.test_excel_file = r"c:\Users\USER\Downloads\14. выборка выписок из ЕГРН по списку КН\список КН.xlsx" + cls.test_xml_file = r"c:\Users\USER\Downloads\14. выборка выписок из ЕГРН по списку КН\пример исх.папки 2 вар\report-1a3f8d46-0b14-4ae3-ac2e-f10372020fed-PKPVD-2025-05-12-224487-74-01[0].xml" + + # Создаем временную директорию для результатов + cls.temp_dir = Path("test_results") + cls.temp_dir.mkdir(exist_ok=True) + + def test_01_module_imports(self): + """Тестирует импорт и создание всех модулей""" + print("\n=== ТЕСТ 1: ИМПОРТ МОДУЛЕЙ ===") + + # Тест создания CadastralRecord + record = CadastralRecord("74:34:0400003:1234", "test.xml", {"74:34:0400003:1234", "74:34:0400003:5678"}) + self.assertEqual(record.main_number, "74:34:0400003:1234") + self.assertEqual(record.file_path, "test.xml") + self.assertEqual(len(record.all_numbers), 2) + self.assertEqual(len(record.other_numbers), 1) + print(f" ✅ CadastralRecord: {record}") + + # Тест создания FileSeeker + temp_dir = tempfile.mkdtemp() + seeker = FileSeeker(temp_dir) + stats = seeker.get_statistics() + self.assertIsInstance(stats, dict) + print(f" ✅ FileSeeker создан: {seeker}") + + # Тест создания ExcelProcessor + excel_processor = ExcelProcessor("dummy.xlsx") + self.assertIsInstance(excel_processor, ExcelProcessor) + print(f" ✅ ExcelProcessor создан") + + def test_02_single_xml_file_extraction(self): + """Тестирует извлечение из одного XML файла""" + print("\n=== ТЕСТ 2: ИЗВЛЕЧЕНИЕ ИЗ XML ФАЙЛА ===") + + if not os.path.exists(self.test_xml_file): + self.skipTest(f"Тестовый файл не найден: {self.test_xml_file}") + + xml_info = XMLFileInfo(self.test_xml_file) + main_number, all_numbers = xml_info.extract_cadastral_data() + + self.assertIsNotNone(main_number) + self.assertGreater(len(all_numbers), 0) + self.assertIn(main_number, all_numbers) + + print(f" 📋 Основной номер: {main_number}") + print(f" 📋 Всего номеров: {len(all_numbers)}") + print(f" 📋 Номера: {sorted(all_numbers)}") + + def test_03_directory_processing(self): + """Тестирует обработку директории с XML файлами""" + print("\n=== ТЕСТ 3: ОБРАБОТКА ДИРЕКТОРИИ ===") + + if not os.path.exists(self.test_xml_dir): + self.skipTest(f"Тестовая директория не найдена: {self.test_xml_dir}") + + seeker = FileSeeker(self.test_xml_dir, str(self.temp_dir)) + + # Поиск файлов + xml_files = seeker.find_xml_files() + self.assertGreater(len(xml_files), 0) + print(f" 📁 Найдено XML файлов: {len(xml_files)}") + + # Обработка файлов (новая реализация с объектами) + records = seeker.process_files() + self.assertGreater(len(records), 0) + print(f" 📋 Создано объектов: {len(records)}") + + # Проверяем статистику + stats = seeker.get_statistics() + self.assertEqual(stats['total_xml_files'], len(xml_files)) + self.assertEqual(stats['total_cadastral_objects'], len(records)) + + print(f" 📊 Статистика:") + for key, value in stats.items(): + print(f" • {key}: {value}") + + # Проверяем первые несколько записей + print(f" 📋 Первые 3 объекта:") + for i, record in enumerate(records[:3]): + print(f" {i+1}. Основной: {record.main_number}") + print(f" Все номера: {sorted(record.all_numbers)}") + print(f" Других номеров: {len(record.other_numbers)}") + + def test_04_excel_processing(self): + """Тестирует обработку Excel файла""" + print("\n=== ТЕСТ 4: ОБРАБОТКА EXCEL ===") + + if not os.path.exists(self.test_excel_file): + self.skipTest(f"Excel файл не найден: {self.test_excel_file}") + + excel_processor = ExcelProcessor(self.test_excel_file) + success = excel_processor.load_excel_file() + self.assertTrue(success) + + excel_numbers = excel_processor.get_cadastral_numbers() + self.assertGreater(len(excel_numbers), 0) + print(f" 📊 Номеров в Excel: {len(excel_numbers)}") + + # Показываем первые 5 номеров + print(f" 📋 Первые 5 номеров из Excel:") + for i, number in enumerate(sorted(excel_numbers)[:5]): + print(f" {i+1}. {number}") + + def test_05_full_integration(self): + """Полный интеграционный тест XML + Excel + обновление""" + print("\n=== ТЕСТ 5: ПОЛНАЯ ИНТЕГРАЦИЯ ===") + + if not os.path.exists(self.test_xml_dir) or not os.path.exists(self.test_excel_file): + self.skipTest("Тестовые файлы не найдены для интеграционного теста") + + # 1. Обработка XML файлов + seeker = FileSeeker(self.test_xml_dir, str(self.temp_dir)) + xml_files = seeker.find_xml_files() + records = seeker.process_files() + + # 2. Обработка Excel файла + excel_processor = ExcelProcessor(self.test_excel_file) + excel_processor.load_excel_file() + excel_numbers = excel_processor.get_cadastral_numbers() + + # 3. Поиск совпадений (по всем номерам в объектах) + all_xml_numbers = seeker.get_all_cadastral_numbers() + matching_numbers = excel_numbers.intersection(all_xml_numbers) + + print(f" 🏷️ XML номеров (всего): {len(all_xml_numbers)}") + print(f" 📊 Excel номеров: {len(excel_numbers)}") + print(f" 🎯 Совпадений: {len(matching_numbers)}") + + self.assertGreater(len(matching_numbers), 0, "Должны быть найдены совпадения") + + # 4. Тестируем новый метод обновления Excel с объектами + output_excel = str(self.temp_dir / "integration_test_updated.xlsx") + success = excel_processor.update_excel_with_cadastral_records(records, output_excel) + self.assertTrue(success, "Обновление Excel должно быть успешным") + + print(f" ✅ Excel обновлен: {Path(output_excel).name}") + + # Проверяем созданный файл + self.assertTrue(os.path.exists(output_excel)) + + # Показываем первые совпадения + if matching_numbers: + print(f" 🎯 Первые 5 совпадений:") + for i, number in enumerate(sorted(matching_numbers)[:5]): + print(f" {i+1}. {number}") + + def test_06_new_implementation_features(self): + """Тестирует особенности новой реализации с объектами""" + print("\n=== ТЕСТ 6: НОВАЯ РЕАЛИЗАЦИЯ ===") + + if not os.path.exists(self.test_xml_dir): + self.skipTest("Тестовая директория не найдена") + + seeker = FileSeeker(self.test_xml_dir) + records = seeker.process_files() + + # Проверяем, что каждая запись - это объект с множественными номерами + objects_with_multiple_numbers = [r for r in records if len(r.all_numbers) > 1] + + print(f" 📋 Всего объектов: {len(records)}") + print(f" 🔗 Объектов с несколькими номерами: {len(objects_with_multiple_numbers)}") + + # Показываем примеры объектов с несколькими номерами + if objects_with_multiple_numbers: + print(f" 📋 Примеры объектов с несколькими номерами:") + for i, record in enumerate(objects_with_multiple_numbers[:3]): + print(f" {i+1}. Основной: {record.main_number}") + print(f" Всего номеров: {len(record.all_numbers)}") + print(f" Другие номера: {sorted(record.other_numbers)}") + + # Проверяем уникальность основных номеров + main_numbers = [r.main_number for r in records] + unique_main_numbers = set(main_numbers) + + print(f" 🏷️ Уникальных основных номеров: {len(unique_main_numbers)}") + print(f" 📊 Всего номеров во всех объектах: {sum(len(r.all_numbers) for r in records)}") + + @classmethod + def tearDownClass(cls): + """Очистка после тестов""" + print("\n=== ОЧИСТКА ===") + # Можно добавить очистку временных файлов если нужно + pass + + +def run_all_tests(): + """Запускает все тесты с подробным выводом""" + print("🚀 ЗАПУСК ОБЪЕДИНЕННЫХ ТЕСТОВ KADASTR PARSER") + print("=" * 60) + + # Создаем тестовый набор + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(KadastrParserTests) + + # Запускаем тесты с подробным выводом + runner = unittest.TextTestRunner(verbosity=2, stream=None) + result = runner.run(suite) + + print("\n" + "=" * 60) + print("🎯 ИТОГОВЫЕ РЕЗУЛЬТАТЫ:") + print(f" ✅ Успешных тестов: {result.testsRun - len(result.failures) - len(result.errors)}") + print(f" ❌ Неудачных тестов: {len(result.failures)}") + print(f" 💥 Ошибок: {len(result.errors)}") + print(f" ⏭️ Пропущенных: {len(result.skipped)}") + + if result.failures: + print("\n❌ НЕУДАЧИ:") + for test, error in result.failures: + print(f" • {test}: {error}") + + if result.errors: + print("\n💥 ОШИБКИ:") + for test, error in result.errors: + print(f" • {test}: {error}") + + success = len(result.failures) == 0 and len(result.errors) == 0 + print(f"\n🏆 ОБЩИЙ РЕЗУЛЬТАТ: {'✅ ВСЕ ТЕСТЫ ПРОШЛИ' if success else '❌ ЕСТЬ ПРОБЛЕМЫ'}") + + return success + + +if __name__ == "__main__": + run_all_tests()