This commit is contained in:
2025-08-12 23:53:44 +05:00
parent a882a81180
commit 6af7d54070
13 changed files with 462 additions and 431 deletions

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ build/
build_32bit/
dist_32bit/
dist/
testset/
testset/
test_results/

View File

@@ -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
## Тестирование
Перед выпуском обязательно протестируйте на целевой системе!

View File

@@ -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',
)

View File

@@ -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` - он гарантированно будет работать!

View File

@@ -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()

24
check_fix.py Normal file
View File

@@ -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('Не найдено заполненных строк')

30
main.py
View File

@@ -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:

View File

@@ -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})"

View File

@@ -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)}"

View File

@@ -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:

View File

@@ -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']} всего уникальных номеров")

View File

@@ -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()

250
tests.py Normal file
View File

@@ -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()