feat: Add data models for disciplinary actions, organizations, and training centers

- Implemented DisciplinaryAction model for tracking disciplinary measures.
- Created Organization model to represent auditing organizations with relevant details.
- Developed TrainingCenter model for educational and methodological centers.
- Added methods to convert models to dictionaries for easy export.

feat: Implement parsers for auditors and organizations

- Developed AuditorsParser to scrape and parse auditor registry data.
- Created OrganizationsParser for scraping auditing organizations' registry.
- Implemented GenericRegistryParser for handling generic tabular data.

chore: Set up base parser functionality

- Established BaseParser class with common methods for making requests and parsing HTML.
- Added pagination handling and detailed page parsing capabilities.

feat: Implement Excel export functionality

- Created ExcelExporter class for exporting data to Excel files with formatting options.
- Added support for exporting multiple sheets and organization data specifically.

chore: Add logging utilities

- Implemented setup_logger function for consistent logging across the application.
- Configured logging to output to both console and file.

chore: Update requirements and scripts

- Added necessary dependencies for parsing, Excel handling, and logging.
- Created run.sh and run.fish scripts for easy execution of the parser.
This commit is contained in:
ada
2025-10-25 20:31:45 +03:00
commit 12d5c6eaff
23 changed files with 2210 additions and 0 deletions
Executable
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""
Парсер реестров СРО ААС (Саморегулируемая организация аудиторов)
Автор: [ada]
"""
import sys
import os
from datetime import datetime
from config import REGISTRIES
from parsers.organizations_parser import OrganizationsParser
from parsers.auditors_parser import AuditorsParser
from parsers.generic_parser import GenericRegistryParser
from utils.excel_exporter import ExcelExporter
from utils.logger import setup_logger
def print_banner():
"""Вывод баннера приложения"""
banner = """
╔═══════════════════════════════════════════════════════════╗
║ ПАРСЕР РЕЕСТРОВ СРО ААС ║
║ Саморегулируемая организация аудиторов ║
║ Ассоциация «Содружество» ║
╚═══════════════════════════════════════════════════════════╝
"""
print(banner)
def print_menu():
"""Вывод главного меню"""
print("\n" + "=" * 60)
print("ДОСТУПНЫЕ РЕЕСТРЫ:")
print("=" * 60)
print("\n📋 ДЕЙСТВИТЕЛЬНЫЕ ЧЛЕНЫ И ВЫДАННЫЕ АТТЕСТАТЫ:")
print(" 1. Реестр членов — аудиторов и индивидуальных аудиторов ⭐")
print(" 2. Реестр членов — аудиторских организаций ⭐")
print(" 3. Реестр выданных СРО ААС квалификационных аттестатов ⭐")
print(" 4. Перечень индивидуальных аудиторов ⭐")
print(" 5. Реестр учебно-методических центров ⭐")
print(" 6. Сведения о подтверждении ОППК аудиторами ⭐")
print(" 7. Сведения о прохождении ПК руководителем аудита ОЗО ФР ⭐")
print("\n❌ ИСКЛЮЧЕННЫЕ ЧЛЕНЫ И АННУЛИРОВАННЫЕ АТТЕСТАТЫ:")
print(" 8. Реестр аудиторов, прекративших членство в СРО ААС ⭐")
print(" 9. Реестр аудиторских организаций, прекративших членство ⭐")
print(" 10. Реестр аннулированных квалификационных аттестатов ⭐")
print(" 11. УМЦ, исключенные из реестра СРО ААС ⭐")
print("\n⚠️ ДИСЦИПЛИНАРНЫЕ МЕРЫ:")
print(" 12. Меры дисциплинарного воздействия к аудиторам ⭐")
print(" 13. Меры дисциплинарного воздействия к организациям ⭐")
print("\n🌐 СЕТИ АУДИТОРСКИХ ОРГАНИЗАЦИЙ:")
print(" 14. Перечень российских и международных сетей ⭐")
print("\n" + "=" * 60)
print(" 0. Выход")
print("=" * 60)
print("\n⭐ - Полностью реализованный парсер")
def parse_organizations(detailed=False):
"""
Парсинг реестра аудиторских организаций
Args:
detailed: Парсить ли детальные страницы
"""
logger = setup_logger("main")
print("\n" + "=" * 60)
print("ПАРСИНГ РЕЕСТРА АУДИТОРСКИХ ОРГАНИЗАЦИЙ")
print("=" * 60)
# Подтверждение пользователя
if detailed:
print("\n⚠️ ВНИМАНИЕ: Детальный парсинг может занять продолжительное время!")
choice = input("Продолжить? (y/n): ").lower()
if choice != "y":
print("Операция отменена.")
return
try:
# Создание парсера
parser = OrganizationsParser()
# Парсинг данных
print("\n🔄 Начало парсинга...")
organizations = parser.parse_to_objects(detailed=detailed)
if not organizations:
print("\n❌ Не удалось получить данные из реестра.")
return
print(f"\n✅ Успешно собрано записей: {len(organizations)}")
# Экспорт в Excel
print("\n📊 Экспорт данных в Excel...")
exporter = ExcelExporter()
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"organizations_{timestamp}"
filepath = exporter.export_organizations(organizations, filename=filename)
if filepath:
print(f"\n✅ Данные успешно экспортированы!")
print(f"📁 Файл: {os.path.abspath(filepath)}")
print(f"📊 Количество записей: {len(organizations)}")
else:
print("\n❌ Ошибка при экспорте данных.")
except KeyboardInterrupt:
print("\n\n⚠️ Операция прервана пользователем.")
except Exception as e:
logger.error(f"Ошибка при парсинге: {e}")
print(f"\n❌ Произошла ошибка: {e}")
def parse_generic_registry(registry_key: str, registry_name: str, detailed=False):
"""
Универсальная функция парсинга любого реестра
Args:
registry_key: Ключ реестра из config.REGISTRIES
registry_name: Название реестра для отображения
detailed: Парсить ли детальные страницы
"""
logger = setup_logger("main")
print("\n" + "=" * 60)
print(f"ПАРСИНГ: {registry_name.upper()}")
print("=" * 60)
# Подтверждение пользователя
if detailed:
print("\n⚠️ ВНИМАНИЕ: Детальный парсинг может занять продолжительное время!")
choice = input("Продолжить? (y/n): ").lower()
if choice != "y":
print("Операция отменена.")
return
try:
# Создание парсера
parser = GenericRegistryParser(registry_key)
# Парсинг данных
print("\n🔄 Начало парсинга...")
data = parser.parse_registry(detailed=detailed)
if not data:
print("\n❌ Не удалось получить данные из реестра.")
return
print(f"\n✅ Успешно собрано записей: {len(data)}")
# Экспорт в Excel
print("\n📊 Экспорт данных в Excel...")
exporter = ExcelExporter()
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"{registry_key}_{timestamp}"
filepath = exporter.export_to_excel(
data=data,
filename=filename,
sheet_name=registry_name[:30], # Ограничение длины для Excel
)
if filepath:
print(f"\n✅ Данные успешно экспортированы!")
print(f"📁 Файл: {os.path.abspath(filepath)}")
print(f"📊 Количество записей: {len(data)}")
else:
print("\n❌ Ошибка при экспорте данных.")
except KeyboardInterrupt:
print("\n\n⚠️ Операция прервана пользователем.")
except Exception as e:
logger.error(f"Ошибка при парсинге: {e}")
print(f"\n❌ Произошла ошибка: {e}")
def parse_auditors(detailed=False):
"""Парсинг реестра аудиторов"""
logger = setup_logger("main")
print("\n" + "=" * 60)
print("ПАРСИНГ РЕЕСТРА АУДИТОРОВ")
print("=" * 60)
if detailed:
print("\n⚠️ ВНИМАНИЕ: Детальный парсинг может занять продолжительное время!")
choice = input("Продолжить? (y/n): ").lower()
if choice != "y":
print("Операция отменена.")
return
try:
parser = AuditorsParser()
print("\n🔄 Начало парсинга...")
auditors = parser.parse_to_objects(detailed=detailed)
if not auditors:
print("\n❌ Не удалось получить данные из реестра.")
return
print(f"\n✅ Успешно собрано записей: {len(auditors)}")
# Экспорт в Excel
print("\n📊 Экспорт данных в Excel...")
exporter = ExcelExporter()
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"auditors_{timestamp}"
# Преобразуем в словари
data = [auditor.to_dict() for auditor in auditors]
filepath = exporter.export_to_excel(
data=data, filename=filename, sheet_name="Аудиторы"
)
if filepath:
print(f"\n✅ Данные успешно экспортированы!")
print(f"📁 Файл: {os.path.abspath(filepath)}")
print(f"📊 Количество записей: {len(auditors)}")
else:
print("\n❌ Ошибка при экспорте данных.")
except KeyboardInterrupt:
print("\n\n⚠️ Операция прервана пользователем.")
except Exception as e:
logger.error(f"Ошибка при парсинге: {e}")
print(f"\n❌ Произошла ошибка: {e}")
def not_implemented():
"""Заглушка для нереализованных функций"""
print("\n⚠️ Данный парсер находится в разработке.")
print("💡 Вы можете реализовать его по аналогии с OrganizationsParser.")
def main():
"""Главная функция приложения"""
print_banner()
# Маппинг выбора на реестры
registry_map = {
"1": ("auditors", "Реестр аудиторов и индивидуальных аудиторов"),
"2": ("organizations", "Реестр аудиторских организаций"),
"3": ("certificates", "Реестр квалификационных аттестатов"),
"4": ("individual_auditors", "Перечень индивидуальных аудиторов"),
"5": ("training_centers", "Реестр учебно-методических центров"),
"6": ("oppk_confirmation", "Сведения о подтверждении ОППК"),
"7": ("ozo_training", "Сведения о прохождении ПК руководителем"),
"8": ("excluded_auditors", "Реестр исключенных аудиторов"),
"9": ("excluded_organizations", "Реестр исключенных организаций"),
"10": ("cancelled_certificates", "Реестр аннулированных аттестатов"),
"11": ("excluded_training_centers", "УМЦ исключенные из реестра"),
"12": ("disciplinary_auditors", "Дисциплинарные меры к аудиторам"),
"13": ("disciplinary_organizations", "Дисциплинарные меры к организациям"),
"14": ("audit_networks", "Перечень сетей аудиторских организаций"),
}
while True:
print_menu()
choice = input("\n👉 Выберите реестр (0-14): ").strip()
if choice == "0":
print("\n👋 До свидания!")
sys.exit(0)
elif choice == "1":
# Реестр аудиторов - специализированный парсер
print(f"\n📋 Выбран: {registry_map[choice][1]}")
detail_choice = input("Парсить детальные страницы? (y/n): ").lower()
detailed = detail_choice == "y"
parse_auditors(detailed=detailed)
elif choice == "2":
# Реестр аудиторских организаций - специализированный парсер
print(f"\n📋 Выбран: {registry_map[choice][1]}")
detail_choice = input("Парсить детальные страницы? (y/n): ").lower()
detailed = detail_choice == "y"
parse_organizations(detailed=detailed)
elif choice in registry_map:
# Все остальные реестры - универсальный парсер
registry_key, registry_name = registry_map[choice]
print(f"\n📋 Выбран: {registry_name}")
detail_choice = input("Парсить детальные страницы? (y/n): ").lower()
detailed = detail_choice == "y"
parse_generic_registry(registry_key, registry_name, detailed=detailed)
else:
print("\n❌ Неверный выбор. Попробуйте снова.")
# Пауза перед возвратом в меню
input("\n⏸️ Нажмите Enter для возврата в меню...")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n👋 Программа завершена.")
sys.exit(0)
except Exception as e:
print(f"\n❌ Критическая ошибка: {e}")
sys.exit(1)