1 Commits
exe ... main

Author SHA1 Message Date
6af7d54070 fix bug 2025-08-12 23:53:44 +05:00
13 changed files with 462 additions and 431 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ build_32bit/
dist_32bit/ dist_32bit/
dist/ 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 файлах") messagebox.showwarning("Предупреждение", "Кадастровые номера не найдены в XML файлах")
return return
# 4. Создаем карту номер -> файл для быстрого поиска # 4. Создаем карту номер -> файл для быстрого поиска (для обратной совместимости)
cadastral_to_file_map = {} cadastral_to_file_map = {}
file_to_numbers_map = {} # файл -> множество номеров для поиска смежных
for record in records: for record in records:
# Для основной карты берем первое вхождение номера # Добавляем все номера из объекта в карту
if record.number not in cadastral_to_file_map: for number in record.all_numbers:
cadastral_to_file_map[record.number] = record.file_path if number not in cadastral_to_file_map: # Берем первый найденный файл
cadastral_to_file_map[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)
# 5. Обработка Excel файла и организация файлов # 5. Обработка Excel файла и организация файлов
excel_updated = False excel_updated = False
@@ -115,13 +110,14 @@ def Pars():
excel_numbers = excel_processor.get_cadastral_numbers() excel_numbers = excel_processor.get_cadastral_numbers()
# Находим пересечения между Excel и найденными номерами # Находим пересечения между 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: if matching_numbers:
# Обновляем Excel файл информацией о найденных файлах # Обновляем Excel файл информацией о найденных объектах
output_excel_path = str(output_directory / "updated_cadastral_list.xlsx") 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_updated = True
# Копируем файлы с номерами из Excel в отдельную папку # Копируем файлы с номерами из Excel в отдельную папку
@@ -221,8 +217,10 @@ def create_report(output_directory, stats, unique_numbers, moved_files, excel_up
f.write(f"Общая статистика:\n") f.write(f"Общая статистика:\n")
f.write(f"- Всего XML файлов найдено: {stats['total_xml_files']}\n") f.write(f"- Всего XML файлов найдено: {stats['total_xml_files']}\n")
f.write(f"- Файлов с кадастровыми номерами: {stats['files_with_numbers']}\n") f.write(f"- Файлов с кадастровыми номерами: {stats['files_with_numbers']}\n")
f.write(f"- Всего кадастровых номеров: {stats['total_cadastral_numbers']}\n") f.write(f"- Всего кадастровых объектов: {stats['total_cadastral_objects']}\n")
f.write(f"- Уникальных номеров: {stats['unique_cadastral_numbers']}\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") f.write(f"- Файлов скопировано в 'all_xml_files': {len(moved_files)}\n")
if matching_files_count > 0: if matching_files_count > 0:

View File

@@ -1,14 +1,33 @@
""" """
Модель для хранения информации о кадастровом номере. Модель для хранения информации о кадастровом объекте.
""" """
from typing import Set
class CadastralRecord: class CadastralRecord:
def __init__(self, number: str, file_path: str): def __init__(self, main_number: str, file_path: str, all_numbers: Set[str] = None):
self.number = number """
Инициализация записи о кадастровом объекте
Args:
main_number: Основной кадастровый номер объекта
file_path: Путь к XML файлу
all_numbers: Все кадастровые номера, найденные в том же XML файле
"""
self.number = main_number # Оставляем для обратной совместимости
self.main_number = main_number
self.file_path = file_path 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): 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): 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 import re
from pathlib import Path from pathlib import Path
from typing import List, Set, Optional from typing import List, Set, Optional, Tuple
from lxml import etree from lxml import etree
class XMLFileInfo: class XMLFileInfo:
def __init__(self, file_path: str): def __init__(self, file_path: str):
self.file_path = Path(file_path) 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]: def extract_cadastral_data(self) -> Tuple[Optional[str], Set[str]]:
"""Извлекает кадастровые номера из XML файла""" """
Извлекает основной номер и все номера из XML файла
Возвращает (основной_номер, всеомера_в_файле)
"""
if not self.file_path.exists(): if not self.file_path.exists():
return set() return None, set()
try: try:
# Парсим XML файл
tree = etree.parse(str(self.file_path)) tree = etree.parse(str(self.file_path))
root = tree.getroot() 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: except Exception as e:
print(f"Ошибка при обработке файла {self.file_path}: {e}") print(f"Ошибка при обработке файла {self.file_path}: {e}")
return set() return None, set()
def _extract_cadastral_numbers_enhanced(self, root) -> Set[str]: def _find_all_cadastral_numbers(self, root) -> Set[str]:
"""Улучшенный алгоритм извлечения кадастровых номеров""" """Находит все валидные кадастровые номера в XML"""
cadastral_numbers = set() all_numbers = set()
# 1. Поиск по известным тегам # Ищем по всем элементам
tags_to_search = ['cad_number', 'land_cad_number', 'cadastral_number', 'parcel_cad_number'] for elem in root.iter():
for tag in tags_to_search: if elem.text and self._is_valid_cadastral_number(elem.text):
elements = root.xpath(f".//{tag}") 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: for elem in elements:
if elem.text and self._is_valid_cadastral_number(elem.text): 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 elem in element.iter():
for attr_name, attr_value in elem.attrib.items(): if elem.text and self._is_valid_cadastral_number(elem.text):
if any(keyword in attr_name.lower() for keyword in ['cad', 'cadastral', 'кадастр']): return elem.text.strip()
if self._is_valid_cadastral_number(attr_value):
cadastral_numbers.add(attr_value.strip())
# 3. Regex поиск по тексту return None
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
def _is_valid_cadastral_number(self, text: str) -> bool: def _is_valid_cadastral_number(self, text: str) -> bool:
"""Проверяет, является ли строка валидным кадастровым номером""" """Проверяет, является ли строка валидным кадастровым номером"""
@@ -68,4 +112,4 @@ class XMLFileInfo:
return bool(re.match(pattern, text.strip())) return bool(re.match(pattern, text.strip()))
def __str__(self): 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}$' pattern = r'^\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}$'
return bool(re.match(pattern, text)) return bool(re.match(pattern, text))
def update_excel_with_file_info(self, cadastral_to_file_map: Dict[str, str], def update_excel_with_cadastral_records(self, cadastral_records: List,
output_file_path: str = None) -> bool: output_file_path: str = None) -> bool:
"""Обновляет Excel файл информацией о найденных файлах и смежных номерах""" """Обновляет Excel файл информацией о найденных кадастровых объектах"""
try: try:
from models.cadastral_record import CadastralRecord
if self.df is None or self.cadastral_column is None: if self.df is None or self.cadastral_column is None:
print("Excel файл не загружен или не найдена колонка с номерами") print("Excel файл не загружен или не найдена колонка с номерами")
return False return False
# Создаем карту файл -> множество кадастровых номеров для поиска смежных # Создаем карты для быстрого поиска
file_to_numbers_map = {} # номер -> запись объекта
for number, file_path in cadastral_to_file_map.items(): number_to_record = {}
if file_path not in file_to_numbers_map: for record in cadastral_records:
file_to_numbers_map[file_path] = set() # Основной номер
file_to_numbers_map[file_path].add(number) 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['Кадастровый блок'] = '' self.df['Кадастровый блок'] = ''
# Заполняем информацию для каждой строки # Заполняем информацию для каждой строки
matched_count = 0
for index, row in self.df.iterrows(): for index, row in self.df.iterrows():
cadastral_number = str(row[self.cadastral_column]).strip() cadastral_number = str(row[self.cadastral_column]).strip()
if cadastral_number in cadastral_to_file_map: if cadastral_number in number_to_record:
# 1. Получаем файл(ы) где найден номер record = number_to_record[cadastral_number]
main_file_path = cadastral_to_file_map[cadastral_number] matched_count += 1
file_name = Path(main_file_path).name
self.df.at[index, 'Найденные файлы'] = file_name
# 2. Находим все смежные кадастровые номера из того же файла # 1. Имя файла
if main_file_path in file_to_numbers_map: file_name = Path(record.file_path).name
all_numbers_in_file = file_to_numbers_map[main_file_path] self.df.at[index, 'Найден в файле'] = file_name
# Исключаем текущий номер из списка смежных
adjacent_numbers = all_numbers_in_file - {cadastral_number}
if adjacent_numbers:
self.df.at[index, 'Смежные кадастровые номера'] = ', '.join(sorted(adjacent_numbers))
# 3. Определяем кадастровый блок (первые три части номера) # 2. Основной номер объекта
cadastral_block = self._extract_cadastral_block(cadastral_number) 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: if cadastral_block:
self.df.at[index, 'Кадастровый блок'] = cadastral_block self.df.at[index, 'Кадастровый блок'] = cadastral_block
@@ -138,6 +149,7 @@ class ExcelProcessor:
self.df.to_excel(output_path, index=False, engine='openpyxl') self.df.to_excel(output_path, index=False, engine='openpyxl')
print(f"Обновленный Excel файл сохранен: {output_path}") print(f"Обновленный Excel файл сохранен: {output_path}")
print(f"Найдено совпадений: {matched_count} из {len(self.cadastral_numbers)}")
return True return True
except Exception as e: except Exception as e:

View File

@@ -37,39 +37,52 @@ class FileSeeker:
return xml_files return xml_files
def process_files(self) -> List[CadastralRecord]: def process_files(self) -> List[CadastralRecord]:
"""Обрабатывает все найденные XML файлы и извлекает кадастровые номера""" """Обрабатывает все найденные XML файлы и создает записи с основными и всеми номерами"""
all_records = [] all_records = []
for xml_file in self.xml_files: 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: if main_number and all_numbers:
record = CadastralRecord(number, str(xml_file.file_path)) record = CadastralRecord(main_number, str(xml_file.file_path), all_numbers)
all_records.append(record) all_records.append(record)
self.cadastral_records = all_records 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 return all_records
def get_unique_cadastral_numbers(self) -> Set[str]: 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]: def move_files_to_temp(self, cadastral_numbers_to_move: Set[str]) -> Dict[str, str]:
"""Перемещает файлы с указанными кадастровыми номерами во временную папку""" """Перемещает файлы с указанными кадастровыми номерами во временную папку"""
moved_files = {} moved_files = {}
for record in self.cadastral_records: 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) source_path = Path(record.file_path)
if source_path.exists(): 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 dest_path = self.temp_directory / dest_name
try: try:
shutil.copy2(source_path, dest_path) 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}") print(f"Файл скопирован: {source_path.name} -> {dest_path}")
except Exception as e: except Exception as e:
print(f"Ошибка при копировании файла {source_path}: {e}") print(f"Ошибка при копировании файла {source_path}: {e}")
@@ -78,14 +91,20 @@ class FileSeeker:
def get_statistics(self) -> Dict[str, int]: def get_statistics(self) -> Dict[str, int]:
"""Возвращает статистику обработки""" """Возвращает статистику обработки"""
total_all_numbers = sum(len(record.all_numbers) for record in self.cadastral_records)
return { return {
'total_xml_files': len(self.xml_files), 'total_xml_files': len(self.xml_files),
'files_with_numbers': len([f for f in self.xml_files if len(f.cadastral_numbers) > 0]), 'files_with_numbers': len([f for f in self.xml_files if f.main_cadastral_number]),
'total_cadastral_numbers': len(self.cadastral_records), 'total_cadastral_objects': len(self.cadastral_records),
'unique_cadastral_numbers': len(self.get_unique_cadastral_numbers()) '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): def __str__(self):
stats = self.get_statistics() stats = self.get_statistics()
return (f"FileSeeker: {stats['total_xml_files']} XML файлов, " 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()