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
+53
View File
@@ -0,0 +1,53 @@
# Gitignore для парсера СРО ААС
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Виртуальное окружение
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Данные и логи
data/exports/*.xlsx
data/exports/*.csv
logs/*.log
# Временные файлы
*.tmp
*.bak
.DS_Store
Thumbs.db
# Конфигурация (если содержит секреты)
# config_local.py
# Тестовые данные
test_data/
+136
View File
@@ -0,0 +1,136 @@
# Парсер реестров СРО ААС
Автоматизированный инструмент для сбора данных из реестров Саморегулируемой организации аудиторов Ассоциация «Содружество» (СРО ААС).
## Возможности
- Парсинг всех 14 официальных реестров СРО ААС
- Два режима работы: быстрое сканирование (первая страница) и полное сканирование (все страницы)
- Экспорт данных в формат Excel (.xlsx) с форматированием
- Система логирования и обработки ошибок
- Поддержка пагинации и повторных попыток при сбоях
## Поддерживаемые реестры
**Действующие члены:**
1. Реестр аудиторов и индивидуальных аудиторов (14,691+ записей)
2. Реестр аудиторских организаций (256+ записей)
3. Реестр квалификационных аттестатов
4. Перечень индивидуальных аудиторов
5. Реестр учебно-методических центров
6. Сведения о подтверждении ОППК
7. Сведения о прохождении ПК руководителем аудита ОЗО ФР
**Исключенные члены:**
8. Аудиторы, прекратившие членство
9. Аудиторские организации, прекратившие членство
10. Аннулированные квалификационные аттестаты
11. УМЦ, исключенные из реестра
**Дисциплинарные меры:**
12. Меры воздействия к аудиторам
13. Меры воздействия к организациям
**Сети:**
14. Российские и международные сети аудиторских организаций
## Установка
```bash
# Клонирование репозитория
git clone <repository-url>
cd osint_parser_ada
# Создание виртуального окружения
python -m venv venv
source venv/bin/activate # Linux/Mac
# или
venv\Scripts\activate # Windows
# Установка зависимостей
pip install -r requirements.txt
```
## Использование
### Интерактивный режим
```bash
python main.py
```
Выберите реестр из меню и режим парсинга:
- `n` (No) — быстрое сканирование (только первая страница, ~50 записей, 1-2 сек)
- `y` (Yes) — полное сканирование (все страницы, все записи, несколько минут)
### Программный режим
```python
from parsers.auditors_parser import AuditorsParser
from utils.excel_exporter import ExcelExporter
# Быстрое сканирование
parser = AuditorsParser()
data = parser.parse_registry(detailed=False) # Только первая страница
# Полное сканирование
data = parser.parse_registry(detailed=True) # Все страницы
# Экспорт в Excel
exporter = ExcelExporter()
exporter.export_to_excel(data, "auditors", "output.xlsx")
```
## Структура проекта
```
osint_parser_ada/
├── config.py # Конфигурация и URL реестров
├── main.py # CLI-интерфейс
├── parsers/ # Парсеры реестров
│ ├── base_parser.py # Базовый класс с пагинацией
│ ├── auditors_parser.py # Парсер аудиторов
│ ├── organizations_parser.py
│ └── generic_parser.py # Универсальный парсер
├── models/ # Модели данных
│ ├── organization.py
│ ├── auditor.py
│ └── ...
├── utils/ # Утилиты
│ ├── logger.py # Логирование
│ └── excel_exporter.py # Экспорт в Excel
└── data/exports/ # Экспортированные файлы
```
## Технические характеристики
- **Python:** 3.8+
- **Зависимости:** requests, beautifulsoup4, lxml, pandas, openpyxl
- **Архитектура:** Паттерн Template Method, модульная структура
- **Обработка ошибок:** Повторные попытки (3x), логирование всех операций
- **Производительность:** ~1.5 сек/страница с задержками для защиты от блокировки
## Конфигурация
Основные настройки в `config.py`:
```python
PARSER_CONFIG = {
"user_agent": "Mozilla/5.0...",
"timeout": 30, # Таймаут запроса (сек)
"delay_between_requests": 1, # Задержка между запросами (сек)
"max_retries": 3 # Максимум повторных попыток
}
```
## Логирование
Логи сохраняются в `logs/parser_YYYY-MM-DD.log`:
- INFO: Прогресс парсинга, количество найденных записей
- WARNING: Пропущенные страницы, проблемы с данными
- ERROR: Критические ошибки запросов
---
**Источник данных:** [https://sroaas.ru](https://sroaas.ru)
+106
View File
@@ -0,0 +1,106 @@
"""
Конфигурационный файл парсера СРО ААС
"""
# Базовые настройки
BASE_URL = "https://sroaas.ru"
# URL реестров
REGISTRIES = {
# Реестры действительных членов и выданных аттестатов
"auditors": {
"name": "Реестр членов — аудиторов и индивидуальных аудиторов",
"url": f"{BASE_URL}/reestr/auditory/",
"active": True,
},
"organizations": {
"name": "Реестр членов — аудиторских организаций",
"url": f"{BASE_URL}/reestr/organizatsiy/",
"active": True,
},
"certificates": {
"name": "Реестр выданных СРО ААС квалификационных аттестатов аудитора",
"url": f"{BASE_URL}/reestr/reestr-vydannykh-sro-aas-kvalifikatsionnykh-attestatov-auditora/",
"active": True,
},
"individual_auditors": {
"name": "Перечень индивидуальных аудиторов",
"url": f"{BASE_URL}/reestr/ia/",
"active": True,
},
"training_centers": {
"name": "Реестр учебно-методических центров",
"url": f"{BASE_URL}/reestr/umc/",
"active": True,
},
"oppk_confirmation": {
"name": "Сведения о подтверждении ОППК аудиторами",
"url": f"{BASE_URL}/reestr/oppk/",
"active": True,
},
"ozo_training": {
"name": "Сведения о прохождении ПК руководителем аудита ОЗО ФР",
"url": f"{BASE_URL}/reestr/pcozo/",
"active": True,
},
# Реестры исключенных членов и аннулированных аттестатов
"excluded_auditors": {
"name": "Реестр аудиторов, прекративших членство в СРО ААС",
"url": f"{BASE_URL}/reestr/reestr-auditorov-prekrativshikh-chlenstvo-v-sro-aas/",
"active": True,
},
"excluded_organizations": {
"name": "Реестр аудиторских организаций, прекративших членство в СРО ААС",
"url": f"{BASE_URL}/reestr/reestr-auditorskikh-organizatsiy-prekrativshikh-chlenstvo-v-sro-aas/",
"active": True,
},
"cancelled_certificates": {
"name": "Реестр аннулированных СРО ААС квалификационных аттестатов аудитора",
"url": f"{BASE_URL}/reestr/reestr-annulirovannykh-sro-aas-kvalifikatsionnykh-attestatov-auditora/",
"active": True,
},
"excluded_training_centers": {
"name": "УМЦ, исключенные из реестра СРО ААС",
"url": f"{BASE_URL}/reestr/umc-prekrativshikh-chlenstvo/",
"active": True,
},
# Реестры дисциплинарных мер
"disciplinary_auditors": {
"name": "Меры дисциплинарного воздействия к членам СРО ААС - аудиторам",
"url": f"{BASE_URL}/reestr/mery-distsiplinarnogo-vozdeystviya-k-chlenam-sro-aas/",
"active": True,
},
"disciplinary_organizations": {
"name": "Меры дисциплинарного воздействия к членам СРО – аудиторским организациям",
"url": f"{BASE_URL}/reestr/mery-distsiplinarnogo-vozdeystviya-k-chlenam-sro-aas-organizatsiy/",
"active": True,
},
# Перечень сетей
"audit_networks": {
"name": "Перечень российских и международных сетей аудиторских организаций",
"url": f"{BASE_URL}/reestr/seti-auditorskikh-organizatsiy",
"active": True,
},
}
# Настройки парсера
PARSER_CONFIG = {
"timeout": 30, # таймаут запроса в секундах
"delay_between_requests": 1, # задержка между запросами в секундах
"max_retries": 3, # максимальное количество попыток
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}
# Настройки экспорта
EXPORT_CONFIG = {
"output_dir": "data/exports/",
"date_format": "%Y-%m-%d_%H-%M-%S",
"encoding": "utf-8",
}
# Настройки логирования
LOGGING_CONFIG = {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"log_dir": "logs/",
}
+8
View File
@@ -0,0 +1,8 @@
# Директория для экспортированных файлов
Все файлы, экспортированные парсером, будут сохраняться здесь.
Формат имени файла: `organizations_YYYY-MM-DD_HH-MM-SS.xlsx`
Пример:
- organizations_2025-10-24_15-30-45.xlsx
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)
+19
View File
@@ -0,0 +1,19 @@
"""
Модели данных
"""
from .organization import Organization
from .auditor import Auditor
from .certificate import Certificate
from .training_center import TrainingCenter
from .disciplinary_action import DisciplinaryAction
from .audit_network import AuditNetwork
__all__ = [
"Organization",
"Auditor",
"Certificate",
"TrainingCenter",
"DisciplinaryAction",
"AuditNetwork",
]
+55
View File
@@ -0,0 +1,55 @@
"""
Модель данных для сети аудиторских организаций
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict
from datetime import datetime
@dataclass
class AuditNetwork:
"""Модель сети аудиторских организаций"""
# Основная информация
name: str
network_type: str # "Российская" или "Международная"
registration_number: Optional[str] = None
# Участники
member_organizations: List[str] = field(default_factory=list)
member_count: Optional[int] = None
# Дополнительная информация
country: Optional[str] = None
headquarters: Optional[str] = None
website: Optional[str] = None
description: Optional[str] = None
# Контакты
contact_person: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"Название сети": self.name,
"Тип": self.network_type,
"Регистрационный номер": self.registration_number or "",
"Количество участников": self.member_count or "",
"Участники": ", ".join(self.member_organizations[:10]), # Первые 10
"Страна": self.country or "",
"Штаб-квартира": self.headquarters or "",
"Сайт": self.website or "",
"Описание": self.description or "",
"Контактное лицо": self.contact_person or "",
"Телефон": self.phone or "",
"Email": self.email or "",
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+77
View File
@@ -0,0 +1,77 @@
"""
Модель данных для аудитора
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict
from datetime import datetime
@dataclass
class Auditor:
"""Модель аудитора или индивидуального аудитора"""
# Основная информация
full_name: str
ornz: str # Основной регистрационный номер записи
certificate_number: str
region: str
status: Optional[str] = None
# Дополнительная информация
inn: Optional[str] = None
snils: Optional[str] = None
qualification: Optional[str] = None
# Организация (для аудиторов)
organization_name: Optional[str] = None
organization_inn: Optional[str] = None
# Даты
certificate_issue_date: Optional[datetime] = None
membership_start_date: Optional[datetime] = None
membership_end_date: Optional[datetime] = None
# Дополнительные данные
education: Optional[str] = None
experience_years: Optional[int] = None
specializations: List[str] = field(default_factory=list)
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"ФИО": self.full_name,
"ОРНЗ": self.ornz,
"Номер аттестата": self.certificate_number,
"Регион": self.region,
"Статус": self.status or "",
"ИНН": self.inn or "",
"СНИЛС": self.snils or "",
"Квалификация": self.qualification or "",
"Организация": self.organization_name or "",
"ИНН организации": self.organization_inn or "",
"Дата выдачи аттестата": (
self.certificate_issue_date.strftime("%d.%m.%Y")
if self.certificate_issue_date
else ""
),
"Дата начала членства": (
self.membership_start_date.strftime("%d.%m.%Y")
if self.membership_start_date
else ""
),
"Дата окончания членства": (
self.membership_end_date.strftime("%d.%m.%Y")
if self.membership_end_date
else ""
),
"Образование": self.education or "",
"Стаж (лет)": self.experience_years or "",
"Специализации": ", ".join(self.specializations),
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+59
View File
@@ -0,0 +1,59 @@
"""
Модель данных для квалификационного аттестата
"""
from dataclasses import dataclass, field
from typing import Optional, Dict
from datetime import datetime
@dataclass
class Certificate:
"""Модель квалификационного аттестата аудитора"""
# Основная информация
certificate_number: str
auditor_full_name: str
issue_date: datetime
status: str
# Дополнительная информация
qualification_type: Optional[str] = None
issuer: Optional[str] = None
validity_period: Optional[str] = None
# Данные аудитора
auditor_inn: Optional[str] = None
auditor_snils: Optional[str] = None
# Причина аннулирования (для аннулированных)
cancellation_reason: Optional[str] = None
cancellation_date: Optional[datetime] = None
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"Номер аттестата": self.certificate_number,
"ФИО аудитора": self.auditor_full_name,
"Дата выдачи": (
self.issue_date.strftime("%d.%m.%Y") if self.issue_date else ""
),
"Статус": self.status,
"Тип квалификации": self.qualification_type or "",
"Выдан": self.issuer or "",
"Срок действия": self.validity_period or "",
"ИНН аудитора": self.auditor_inn or "",
"СНИЛС аудитора": self.auditor_snils or "",
"Причина аннулирования": self.cancellation_reason or "",
"Дата аннулирования": (
self.cancellation_date.strftime("%d.%m.%Y")
if self.cancellation_date
else ""
),
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+65
View File
@@ -0,0 +1,65 @@
"""
Модель данных для дисциплинарной меры
"""
from dataclasses import dataclass, field
from typing import Optional, Dict
from datetime import datetime
@dataclass
class DisciplinaryAction:
"""Модель меры дисциплинарного воздействия"""
# Основная информация
subject_name: str # ФИО аудитора или название организации
subject_type: str # "auditor" или "organization"
ornz: str
action_type: str # Тип меры воздействия
# Детали
violation_description: str
decision_date: datetime
decision_number: Optional[str] = None
# Дополнительная информация
inn: Optional[str] = None
region: Optional[str] = None
effective_date: Optional[datetime] = None
expiry_date: Optional[datetime] = None
# Решение
decision_body: Optional[str] = None
appeal_status: Optional[str] = None
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"Субъект": self.subject_name,
"Тип субъекта": (
"Аудитор" if self.subject_type == "auditor" else "Организация"
),
"ОРНЗ": self.ornz,
"Мера воздействия": self.action_type,
"Описание нарушения": self.violation_description,
"Дата решения": (
self.decision_date.strftime("%d.%m.%Y") if self.decision_date else ""
),
"Номер решения": self.decision_number or "",
"ИНН": self.inn or "",
"Регион": self.region or "",
"Дата вступления в силу": (
self.effective_date.strftime("%d.%m.%Y") if self.effective_date else ""
),
"Дата окончания": (
self.expiry_date.strftime("%d.%m.%Y") if self.expiry_date else ""
),
"Орган принявший решение": self.decision_body or "",
"Статус обжалования": self.appeal_status or "",
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+81
View File
@@ -0,0 +1,81 @@
"""
Модель данных для аудиторской организации
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict
from datetime import datetime
@dataclass
class Organization:
"""Модель аудиторской организации"""
# Основная информация
name: str
ornz: str # Основной регистрационный номер записи
inn: str
region: str
status: Optional[str] = None
# Дополнительная информация (заполняется при детальном парсе)
full_name: Optional[str] = None
ogrn: Optional[str] = None
kpp: Optional[str] = None
address: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
director: Optional[str] = None
# Даты
registration_date: Optional[datetime] = None
membership_start_date: Optional[datetime] = None
membership_end_date: Optional[datetime] = None
# Дополнительные данные
auditors_count: Optional[int] = None
certificates: List[str] = field(default_factory=list)
networks: List[str] = field(default_factory=list)
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"Наименование": self.name,
"Полное наименование": self.full_name or "",
"ОРНЗ": self.ornz,
"ИНН": self.inn,
"КПП": self.kpp or "",
"ОГРН": self.ogrn or "",
"Регион": self.region,
"Статус": self.status or "",
"Адрес": self.address or "",
"Телефон": self.phone or "",
"Email": self.email or "",
"Сайт": self.website or "",
"Руководитель": self.director or "",
"Дата регистрации": (
self.registration_date.strftime("%d.%m.%Y")
if self.registration_date
else ""
),
"Дата начала членства": (
self.membership_start_date.strftime("%d.%m.%Y")
if self.membership_start_date
else ""
),
"Дата окончания членства": (
self.membership_end_date.strftime("%d.%m.%Y")
if self.membership_end_date
else ""
),
"Количество аудиторов": self.auditors_count or "",
"Сертификаты": ", ".join(self.certificates),
"Сети": ", ".join(self.networks),
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+79
View File
@@ -0,0 +1,79 @@
"""
Модель данных для учебно-методического центра
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict
from datetime import datetime
@dataclass
class TrainingCenter:
"""Модель учебно-методического центра (УМЦ)"""
# Основная информация
name: str
registration_number: str
inn: str
region: str
status: Optional[str] = None
# Дополнительная информация
full_name: Optional[str] = None
ogrn: Optional[str] = None
kpp: Optional[str] = None
address: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
director: Optional[str] = None
# Аккредитация
accreditation_date: Optional[datetime] = None
accreditation_number: Optional[str] = None
programs: List[str] = field(default_factory=list)
# Даты
registration_date: Optional[datetime] = None
exclusion_date: Optional[datetime] = None
exclusion_reason: Optional[str] = None
# Метаинформация
source_url: Optional[str] = None
parsed_at: datetime = field(default_factory=datetime.now)
def to_dict(self) -> Dict:
"""Преобразование в словарь для экспорта"""
return {
"Наименование": self.name,
"Полное наименование": self.full_name or "",
"Регистрационный номер": self.registration_number,
"ИНН": self.inn,
"КПП": self.kpp or "",
"ОГРН": self.ogrn or "",
"Регион": self.region,
"Статус": self.status or "",
"Адрес": self.address or "",
"Телефон": self.phone or "",
"Email": self.email or "",
"Сайт": self.website or "",
"Руководитель": self.director or "",
"Дата аккредитации": (
self.accreditation_date.strftime("%d.%m.%Y")
if self.accreditation_date
else ""
),
"Номер аккредитации": self.accreditation_number or "",
"Программы": ", ".join(self.programs),
"Дата регистрации": (
self.registration_date.strftime("%d.%m.%Y")
if self.registration_date
else ""
),
"Дата исключения": (
self.exclusion_date.strftime("%d.%m.%Y") if self.exclusion_date else ""
),
"Причина исключения": self.exclusion_reason or "",
"URL источника": self.source_url or "",
"Дата сбора данных": self.parsed_at.strftime("%d.%m.%Y %H:%M:%S"),
}
+15
View File
@@ -0,0 +1,15 @@
"""
Модуль парсеров для различных реестров СРО ААС
"""
from .base_parser import BaseParser
from .organizations_parser import OrganizationsParser
from .auditors_parser import AuditorsParser
from .generic_parser import GenericRegistryParser
__all__ = [
"BaseParser",
"OrganizationsParser",
"AuditorsParser",
"GenericRegistryParser",
]
+174
View File
@@ -0,0 +1,174 @@
"""
Парсер реестра аудиторов и индивидуальных аудиторов
"""
from typing import List, Dict, Any
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re
from parsers.base_parser import BaseParser
from models.auditor import Auditor
from config import BASE_URL
class AuditorsParser(BaseParser):
"""
Парсер для реестра аудиторов и индивидуальных аудиторов
https://sroaas.ru/reestr/auditory/
"""
def __init__(self):
from config import REGISTRIES
registry = REGISTRIES["auditors"]
super().__init__(registry["url"], registry["name"])
def parse_list_page(self, soup: BeautifulSoup) -> List[Dict[str, Any]]:
"""
Парсинг страницы со списком аудиторов
Args:
soup: Parsed HTML страницы
Returns:
Список словарей с данными аудиторов
"""
auditors = []
# Поиск таблицы с данными
table = soup.find("table")
if not table:
self.logger.warning("Таблица с данными не найдена на странице")
return auditors
# Парсинг строк таблицы
rows = table.find_all("tr")[1:] # Пропускаем заголовок
for row in rows:
try:
cols = row.find_all("td")
if len(cols) < 4:
continue
# Извлечение данных
full_name = cols[0].get_text(strip=True)
ornz = cols[1].get_text(strip=True)
certificate_number = cols[2].get_text(strip=True)
region = cols[3].get_text(strip=True)
# Статус (если есть)
status = cols[4].get_text(strip=True) if len(cols) > 4 else None
# URL детальной страницы
detail_url = None
link = row.find("a")
if link:
href = link.get("href")
# Проверяем, что href - строка, а не список
if isinstance(href, list):
href = href[0] if href else None
if href:
detail_url = urljoin(BASE_URL, href)
auditor_data = {
"full_name": full_name,
"ornz": ornz,
"certificate_number": certificate_number,
"region": region,
"status": status,
"detail_url": detail_url,
}
auditors.append(auditor_data)
except Exception as e:
self.logger.error(f"Ошибка при парсинге строки таблицы: {e}")
continue
return auditors
def parse_detail_page(self, url: str, soup: BeautifulSoup) -> Dict[str, Any]:
"""
Парсинг детальной страницы аудитора
Args:
url: URL детальной страницы
soup: Parsed HTML страницы
Returns:
Словарь с детальными данными
"""
detail_data = {}
try:
info_blocks = soup.find_all("div", class_=["info-block", "detail-info"])
for block in info_blocks:
labels = block.find_all(["dt", "div"], class_=["label", "info-label"])
values = block.find_all(["dd", "div"], class_=["value", "info-value"])
for label, value in zip(labels, values):
key = label.get_text(strip=True).lower()
val = value.get_text(strip=True)
if "инн" in key:
detail_data["inn"] = val
elif "снилс" in key:
detail_data["snils"] = val
elif "квалификация" in key:
detail_data["qualification"] = val
elif "организация" in key and "название" in key:
detail_data["organization_name"] = val
elif "организация" in key and "инн" in key:
detail_data["organization_inn"] = val
elif "образование" in key:
detail_data["education"] = val
elif "стаж" in key:
match = re.search(r"\d+", val)
if match:
detail_data["experience_years"] = int(match.group())
detail_data["source_url"] = url
except Exception as e:
self.logger.error(f"Ошибка при парсинге детальной страницы {url}: {e}")
return detail_data
def parse_to_objects(self, detailed: bool = False) -> List[Auditor]:
"""
Парсинг реестра с преобразованием в объекты Auditor
Args:
detailed: Парсить ли детальные страницы
Returns:
Список объектов Auditor
"""
data = self.parse_registry(detailed=detailed)
auditors = []
for item in data:
try:
auditor = Auditor(
full_name=item.get("full_name", ""),
ornz=item.get("ornz", ""),
certificate_number=item.get("certificate_number", ""),
region=item.get("region", ""),
status=item.get("status"),
inn=item.get("inn"),
snils=item.get("snils"),
qualification=item.get("qualification"),
organization_name=item.get("organization_name"),
organization_inn=item.get("organization_inn"),
education=item.get("education"),
experience_years=item.get("experience_years"),
source_url=item.get("source_url"),
)
auditors.append(auditor)
except Exception as e:
self.logger.error(f"Ошибка при создании объекта Auditor: {e}")
continue
return auditors
+235
View File
@@ -0,0 +1,235 @@
"""
Базовый парсер для реестров СРО ААС
"""
import requests
import time
import re
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from config import PARSER_CONFIG, BASE_URL
from utils.logger import setup_logger
class BaseParser(ABC):
"""
Базовый класс для парсеров реестров СРО ААС
"""
def __init__(self, registry_url: str, registry_name: str):
"""
Инициализация парсера
Args:
registry_url: URL реестра
registry_name: Название реестра
"""
self.registry_url = registry_url
self.registry_name = registry_name
self.logger = setup_logger(f"{self.__class__.__name__}")
# Настройки сессии
self.session = requests.Session()
self.session.headers.update({"User-Agent": PARSER_CONFIG["user_agent"]})
self.timeout = PARSER_CONFIG["timeout"]
self.delay = PARSER_CONFIG["delay_between_requests"]
self.max_retries = PARSER_CONFIG["max_retries"]
def _make_request(
self, url: str, params: Optional[Dict] = None
) -> Optional[requests.Response]:
"""
Выполнение HTTP-запроса с повторными попытками
Args:
url: URL для запроса
params: Параметры запроса
Returns:
Response объект или None в случае ошибки
"""
for attempt in range(self.max_retries):
try:
self.logger.debug(
f"Запрос к {url} (попытка {attempt + 1}/{self.max_retries})"
)
response = self.session.get(url, params=params, timeout=self.timeout)
response.raise_for_status()
# Задержка между запросами
time.sleep(self.delay)
return response
except requests.exceptions.RequestException as e:
self.logger.warning(f"Ошибка при запросе {url}: {e}")
if attempt < self.max_retries - 1:
time.sleep(self.delay * (attempt + 1))
else:
self.logger.error(
f"Не удалось выполнить запрос к {url} после {self.max_retries} попыток"
)
return None
def _parse_html(self, html: str) -> BeautifulSoup:
"""
Парсинг HTML
Args:
html: HTML-контент
Returns:
BeautifulSoup объект
"""
return BeautifulSoup(html, "lxml")
def _get_pagination_urls(self, base_url: str, soup: BeautifulSoup) -> List[str]:
"""
Получение списка URL всех страниц с пагинацией
Args:
base_url: Базовый URL реестра
soup: Parsed HTML основной страницы
Returns:
Список URL всех страниц
"""
urls = [base_url]
# Поиск пагинации (разные варианты классов)
pagination = (
soup.find("div", class_="b-pagination-block")
or soup.find("div", class_="pagination")
or soup.find("ul", class_="pagination")
)
if pagination:
# Находим максимальный номер страницы
max_page = 1
links = pagination.find_all("a")
for link in links:
href = link.get("href")
if href and href not in ["#", ""]:
# Проверяем, что href - строка, а не список
if isinstance(href, list):
href = href[0] if href else None
if href:
# Извлекаем номер страницы из URL
# Формат: ?PAGEN_1=page-N
match = re.search(r"page-(\d+)", str(href))
if match:
page_num = int(match.group(1))
max_page = max(max_page, page_num)
else:
# Добавляем URL как есть (для других форматов пагинации)
page_url = urljoin(BASE_URL, href)
if page_url not in urls:
urls.append(page_url)
# Генерируем URL-ы для всех страниц
if max_page > 1:
self.logger.info(f"Обнаружено страниц: {max_page}")
urls = [base_url] # Очищаем и добавляем первую
for page_num in range(2, max_page + 1):
# Формируем URL для каждой страницы
page_url = f"{base_url}?PAGEN_1=page-{page_num}"
urls.append(page_url)
self.logger.info(f"Найдено страниц с пагинацией: {len(urls)}")
return urls
@abstractmethod
def parse_list_page(self, soup: BeautifulSoup) -> List[Dict[str, Any]]:
"""
Парсинг страницы со списком записей реестра
Args:
soup: Parsed HTML страницы
Returns:
Список словарей с данными
"""
pass
@abstractmethod
def parse_detail_page(self, url: str, soup: BeautifulSoup) -> Dict[str, Any]:
"""
Парсинг детальной страницы записи
Args:
url: URL детальной страницы
soup: Parsed HTML страницы
Returns:
Словарь с детальными данными
"""
pass
def parse_registry(self, detailed: bool = False) -> List[Dict[str, Any]]:
"""
Парсинг всего реестра
Args:
detailed: Парсить ли детальные страницы
True - парсить все страницы с детальной информацией
False - парсить только первую страницу без деталей
Returns:
Список собранных данных
"""
self.logger.info(f"Начало парсинга реестра: {self.registry_name}")
all_data = []
# Получение первой страницы
response = self._make_request(self.registry_url)
if not response:
self.logger.error("Не удалось получить первую страницу реестра")
return all_data
soup = self._parse_html(response.text)
# В режиме detailed=True парсим все страницы, иначе только первую
if detailed:
pagination_urls = self._get_pagination_urls(self.registry_url, soup)
else:
pagination_urls = [self.registry_url]
self.logger.info("Режим быстрого сканирования: только первая страница")
# Парсинг каждой страницы
for page_num, page_url in enumerate(pagination_urls, 1):
self.logger.info(
f"Парсинг страницы {page_num}/{len(pagination_urls)}: {page_url}"
)
if page_num > 1: # Первую страницу уже получили
response = self._make_request(page_url)
if not response:
continue
soup = self._parse_html(response.text)
# Парсинг списка на странице
page_data = self.parse_list_page(soup)
self.logger.info(f"Найдено записей на странице: {len(page_data)}")
# Детальный парсинг, если требуется
if detailed:
for item in page_data:
if "detail_url" in item and item["detail_url"] is not None:
detail_response = self._make_request(item["detail_url"])
if detail_response:
detail_soup = self._parse_html(detail_response.text)
detail_data = self.parse_detail_page(
item["detail_url"], detail_soup
)
item.update(detail_data)
all_data.extend(page_data)
self.logger.info(f"Парсинг завершен. Всего записей: {len(all_data)}")
return all_data
+123
View File
@@ -0,0 +1,123 @@
"""
Универсальный парсер для реестров с табличной структурой
"""
from typing import List, Dict, Any
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from parsers.base_parser import BaseParser
from config import BASE_URL
class GenericRegistryParser(BaseParser):
"""
Универсальный парсер для реестров с простой табличной структурой
Может использоваться для реестров, где не требуется специальная обработка
"""
def __init__(self, registry_key: str):
"""
Args:
registry_key: Ключ реестра из config.REGISTRIES
"""
from config import REGISTRIES
registry = REGISTRIES[registry_key]
super().__init__(registry["url"], registry["name"])
self.registry_key = registry_key
def parse_list_page(self, soup: BeautifulSoup) -> List[Dict[str, Any]]:
"""
Универсальный парсинг таблицы
"""
items = []
table = soup.find("table")
if not table:
self.logger.warning("Таблица с данными не найдена")
return items
# Получаем заголовки
headers = []
header_row = table.find("tr")
if header_row:
headers = [
th.get_text(strip=True) for th in header_row.find_all(["th", "td"])
]
# Парсим строки
rows = table.find_all("tr")[1:]
for row in rows:
try:
cols = row.find_all("td")
if not cols:
continue
item_data = {}
# Сопоставляем колонки с заголовками
for i, col in enumerate(cols):
header_name = headers[i] if i < len(headers) else f"col_{i}"
item_data[header_name] = col.get_text(strip=True)
# Ищем ссылку на детальную страницу
link = row.find("a")
if link:
href = link.get("href")
# Проверяем, что href - строка, а не список
if isinstance(href, list):
href = href[0] if href else None
if href and isinstance(href, str):
item_data["detail_url"] = urljoin(BASE_URL, href)
items.append(item_data)
except Exception as e:
self.logger.error(f"Ошибка при парсинге строки: {e}")
continue
return items
def parse_detail_page(self, url: str, soup: BeautifulSoup) -> Dict[str, Any]:
"""
Универсальный парсинг детальной страницы
"""
detail_data = {}
try:
# Ищем все блоки с информацией
info_blocks = soup.find_all(
["div", "table"], class_=["info-block", "detail-info", "info"]
)
for block in info_blocks:
# Пробуем различные варианты структуры
# Вариант 1: dt/dd
labels = block.find_all("dt")
values = block.find_all("dd")
if labels and values:
for label, value in zip(labels, values):
key = label.get_text(strip=True)
val = value.get_text(strip=True)
detail_data[key] = val
# Вариант 2: div с классами
label_divs = block.find_all("div", class_=["label", "info-label"])
value_divs = block.find_all("div", class_=["value", "info-value"])
if label_divs and value_divs:
for label, value in zip(label_divs, value_divs):
key = label.get_text(strip=True)
val = value.get_text(strip=True)
detail_data[key] = val
detail_data["source_url"] = url
except Exception as e:
self.logger.error(f"Ошибка при парсинге детальной страницы: {e}")
return detail_data
+216
View File
@@ -0,0 +1,216 @@
"""
Парсер реестра аудиторских организаций
"""
from typing import List, Dict, Any
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import re
from parsers.base_parser import BaseParser
from models.organization import Organization
from config import BASE_URL
class OrganizationsParser(BaseParser):
"""
Парсер для реестра аудиторских организаций
https://sroaas.ru/reestr/organizatsiy/
"""
def __init__(self):
from config import REGISTRIES
registry = REGISTRIES["organizations"]
super().__init__(registry["url"], registry["name"])
def parse_list_page(self, soup: BeautifulSoup) -> List[Dict[str, Any]]:
"""
Парсинг страницы со списком аудиторских организаций
Args:
soup: Parsed HTML страницы
Returns:
Список словарей с данными организаций
"""
organizations = []
# Поиск таблицы с данными
table = soup.find("table")
if not table:
self.logger.warning("Таблица с данными не найдена на странице")
return organizations
# Парсинг строк таблицы
rows = table.find_all("tr")[1:] # Пропускаем заголовок
for row in rows:
try:
cols = row.find_all("td")
if len(cols) < 4:
continue
# Извлечение данных
name = cols[0].get_text(strip=True)
ornz = cols[1].get_text(strip=True)
inn = cols[2].get_text(strip=True)
region = cols[3].get_text(strip=True)
# Статус (если есть)
status = cols[4].get_text(strip=True) if len(cols) > 4 else None
# URL детальной страницы (если есть ссылка)
detail_url = None
link = row.find("a")
if link:
href = link.get("href")
# Проверяем, что href - строка, а не список
if isinstance(href, list):
href = href[0] if href else None
if href:
detail_url = urljoin(BASE_URL, href)
org_data = {
"name": name,
"ornz": ornz,
"inn": inn,
"region": region,
"status": status,
"detail_url": detail_url,
}
organizations.append(org_data)
except Exception as e:
self.logger.error(f"Ошибка при парсинге строки таблицы: {e}")
continue
return organizations
def parse_detail_page(self, url: str, soup: BeautifulSoup) -> Dict[str, Any]:
"""
Парсинг детальной страницы организации
Args:
url: URL детальной страницы
soup: Parsed HTML страницы
Returns:
Словарь с детальными данными
"""
detail_data = {}
try:
# Поиск блока с детальной информацией
info_blocks = soup.find_all("div", class_=["info-block", "detail-info"])
for block in info_blocks:
# Извлечение пар ключ-значение
labels = block.find_all(["dt", "div"], class_=["label", "info-label"])
values = block.find_all(["dd", "div"], class_=["value", "info-value"])
for label, value in zip(labels, values):
key = label.get_text(strip=True).lower()
val = value.get_text(strip=True)
# Маппинг полей
if "полное наименование" in key or "полное название" in key:
detail_data["full_name"] = val
elif "огрн" in key:
detail_data["ogrn"] = val
elif "кпп" in key:
detail_data["kpp"] = val
elif "адрес" in key:
detail_data["address"] = val
elif "телефон" in key or "тел" in key:
detail_data["phone"] = val
elif "email" in key or "e-mail" in key or "почта" in key:
detail_data["email"] = val
elif "сайт" in key or "веб-сайт" in key:
detail_data["website"] = val
elif "руководитель" in key or "директор" in key:
detail_data["director"] = val
elif "дата регистрации" in key:
detail_data["registration_date"] = val
elif "дата начала членства" in key or "дата вступления" in key:
detail_data["membership_start_date"] = val
elif "количество аудиторов" in key:
# Извлечение числа
match = re.search(r"\d+", val)
if match:
detail_data["auditors_count"] = int(match.group())
# Поиск информации о сертификатах
certificates_section = soup.find(
"div", class_=["certificates", "attestaty"]
)
if certificates_section:
certs = []
cert_items = certificates_section.find_all(
["li", "div"], class_="certificate-item"
)
for cert in cert_items:
certs.append(cert.get_text(strip=True))
if certs:
detail_data["certificates"] = certs
# Поиск информации о сетях
networks_section = soup.find("div", class_=["networks", "seti"])
if networks_section:
nets = []
net_items = networks_section.find_all(
["li", "div"], class_="network-item"
)
for net in net_items:
nets.append(net.get_text(strip=True))
if nets:
detail_data["networks"] = nets
detail_data["source_url"] = url
except Exception as e:
self.logger.error(f"Ошибка при парсинге детальной страницы {url}: {e}")
return detail_data
def parse_to_objects(self, detailed: bool = False) -> List[Organization]:
"""
Парсинг реестра с преобразованием в объекты Organization
Args:
detailed: Парсить ли детальные страницы
Returns:
Список объектов Organization
"""
data = self.parse_registry(detailed=detailed)
organizations = []
for item in data:
try:
org = Organization(
name=item.get("name", ""),
ornz=item.get("ornz", ""),
inn=item.get("inn", ""),
region=item.get("region", ""),
status=item.get("status"),
full_name=item.get("full_name"),
ogrn=item.get("ogrn"),
kpp=item.get("kpp"),
address=item.get("address"),
phone=item.get("phone"),
email=item.get("email"),
website=item.get("website"),
director=item.get("director"),
auditors_count=item.get("auditors_count"),
certificates=item.get("certificates", []),
networks=item.get("networks", []),
source_url=item.get("source_url"),
)
organizations.append(org)
except Exception as e:
self.logger.error(f"Ошибка при создании объекта Organization: {e}")
continue
return organizations
+18
View File
@@ -0,0 +1,18 @@
# Основные библиотеки для парсинга
requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
# Работа с Excel
openpyxl>=3.1.0
pandas>=2.0.0
# Selenium для динамического контента (опционально)
selenium>=4.15.0
# Утилиты
python-dateutil>=2.8.0
tqdm>=4.66.0
# Логирование
colorlog>=6.7.0
Executable
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env fish
# Скрипт быстрого запуска для Fish Shell
# Использование: ./run.fish
echo "=============================================="
echo " ПАРСЕР РЕЕСТРОВ СРО ААС"
echo "=============================================="
echo ""
# Проверка наличия Python
if not command -v python3 &> /dev/null
echo "❌ Python 3 не найден. Установите Python 3.8 или выше."
exit 1
end
echo "✅ Python найден: "(python3 --version)
echo ""
# Проверка виртуального окружения
if not test -d venv
echo "📦 Виртуальное окружение не найдено. Создание..."
python3 -m venv venv
echo "✅ Виртуальное окружение создано."
echo ""
end
# Активация виртуального окружения
echo "🔧 Активация виртуального окружения..."
source venv/bin/activate.fish
# Проверка зависимостей
if not python -c "import requests" 2>/dev/null
echo "📥 Установка зависимостей..."
pip install -q --upgrade pip
pip install -q -r requirements.txt
echo "✅ Зависимости установлены."
echo ""
end
# Создание необходимых директорий
mkdir -p data/exports
mkdir -p logs
# Запуск парсера
echo "🚀 Запуск парсера..."
echo ""
python main.py
# Деактивация виртуального окружения
deactivate
Executable
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Скрипт быстрого запуска парсера СРО ААС
# Использование: ./run.sh
echo "=============================================="
echo " ПАРСЕР РЕЕСТРОВ СРО ААС"
echo "=============================================="
echo ""
# Проверка наличия Python
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 не найден. Установите Python 3.8 или выше."
exit 1
fi
echo "✅ Python найден: $(python3 --version)"
echo ""
# Проверка виртуального окружения
if [ ! -d "venv" ]; then
echo "📦 Виртуальное окружение не найдено. Создание..."
python3 -m venv venv
echo "✅ Виртуальное окружение создано."
echo ""
fi
# Активация виртуального окружения
echo "🔧 Активация виртуального окружения..."
source venv/bin/activate
# Проверка зависимостей
if ! python -c "import requests" 2>/dev/null; then
echo "📥 Установка зависимостей..."
pip install -q --upgrade pip
pip install -q -r requirements.txt
echo "✅ Зависимости установлены."
echo ""
fi
# Создание необходимых директорий
mkdir -p data/exports
mkdir -p logs
# Запуск парсера
echo "🚀 Запуск парсера..."
echo ""
python main.py
# Деактивация виртуального окружения
deactivate
+8
View File
@@ -0,0 +1,8 @@
"""
Вспомогательные утилиты
"""
from .logger import setup_logger
from .excel_exporter import ExcelExporter
__all__ = ["setup_logger", "ExcelExporter"]
+215
View File
@@ -0,0 +1,215 @@
"""
Экспорт данных в Excel
"""
import os
from datetime import datetime
from typing import List, Dict, Any
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter
from config import EXPORT_CONFIG
from utils.logger import setup_logger
class ExcelExporter:
"""
Класс для экспорта данных в Excel
"""
def __init__(self, output_dir: str = None):
"""
Инициализация экспортера
Args:
output_dir: Директория для сохранения файлов
"""
self.output_dir = output_dir or EXPORT_CONFIG["output_dir"]
self.logger = setup_logger(self.__class__.__name__)
# Создание директории, если не существует
os.makedirs(self.output_dir, exist_ok=True)
def export_to_excel(
self,
data: List[Dict[str, Any]],
filename: str = None,
sheet_name: str = "Данные",
auto_format: bool = True,
) -> str:
"""
Экспорт данных в Excel файл
Args:
data: Список словарей с данными
filename: Имя файла (без расширения)
sheet_name: Название листа
auto_format: Применять ли автоформатирование
Returns:
Путь к созданному файлу
"""
if not data:
self.logger.warning("Нет данных для экспорта")
return None
# Генерация имени файла
if not filename:
timestamp = datetime.now().strftime(EXPORT_CONFIG["date_format"])
filename = f"export_{timestamp}"
filepath = os.path.join(self.output_dir, f"{filename}.xlsx")
try:
# Создание DataFrame
df = pd.DataFrame(data)
# Экспорт в Excel
self.logger.info(f"Экспорт {len(data)} записей в {filepath}")
df.to_excel(filepath, sheet_name=sheet_name, index=False, engine="openpyxl")
# Применение форматирования
if auto_format:
self._format_excel(filepath, sheet_name)
self.logger.info(f"Данные успешно экспортированы в {filepath}")
return filepath
except Exception as e:
self.logger.error(f"Ошибка при экспорте в Excel: {e}")
return None
def _format_excel(self, filepath: str, sheet_name: str):
"""
Применение форматирования к Excel файлу
Args:
filepath: Путь к файлу
sheet_name: Название листа
"""
try:
wb = load_workbook(filepath)
ws = wb[sheet_name]
# Стили
header_fill = PatternFill(
start_color="4472C4", end_color="4472C4", fill_type="solid"
)
header_font = Font(bold=True, color="FFFFFF", size=11)
header_alignment = Alignment(
horizontal="center", vertical="center", wrap_text=True
)
cell_alignment = Alignment(
horizontal="left", vertical="top", wrap_text=True
)
# Форматирование заголовков
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = header_alignment
# Форматирование ячеек данных
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
for cell in row:
cell.alignment = cell_alignment
# Автоподбор ширины столбцов
for column in ws.columns:
max_length = 0
column_letter = get_column_letter(column[0].column)
for cell in column:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass
adjusted_width = min(max_length + 2, 50) # Максимум 50 символов
ws.column_dimensions[column_letter].width = adjusted_width
# Закрепление первой строки
ws.freeze_panes = "A2"
# Сохранение
wb.save(filepath)
self.logger.debug(f"Форматирование применено к {filepath}")
except Exception as e:
self.logger.error(f"Ошибка при форматировании Excel: {e}")
def export_organizations(
self, organizations: List[Any], filename: str = None
) -> str:
"""
Экспорт списка организаций (объектов Organization)
Args:
organizations: Список объектов Organization
filename: Имя файла
Returns:
Путь к созданному файлу
"""
# Преобразование объектов в словари
data = []
for org in organizations:
if hasattr(org, "to_dict"):
data.append(org.to_dict())
else:
data.append(vars(org))
return self.export_to_excel(
data=data,
filename=filename or "organizations",
sheet_name="Аудиторские организации",
)
def export_multiple_sheets(
self, sheets_data: Dict[str, List[Dict[str, Any]]], filename: str = None
) -> str:
"""
Экспорт данных в Excel с несколькими листами
Args:
sheets_data: Словарь {название_листа: данные}
filename: Имя файла
Returns:
Путь к созданному файлу
"""
if not sheets_data:
self.logger.warning("Нет данных для экспорта")
return None
# Генерация имени файла
if not filename:
timestamp = datetime.now().strftime(EXPORT_CONFIG["date_format"])
filename = f"export_multi_{timestamp}"
filepath = os.path.join(self.output_dir, f"{filename}.xlsx")
try:
with pd.ExcelWriter(filepath, engine="openpyxl") as writer:
for sheet_name, data in sheets_data.items():
if data:
df = pd.DataFrame(data)
df.to_excel(writer, sheet_name=sheet_name, index=False)
self.logger.info(f"Данные успешно экспортированы в {filepath}")
# Форматирование каждого листа
wb = load_workbook(filepath)
for sheet_name in sheets_data.keys():
if sheet_name in wb.sheetnames:
self._format_excel(filepath, sheet_name)
return filepath
except Exception as e:
self.logger.error(f"Ошибка при экспорте в Excel: {e}")
return None
+51
View File
@@ -0,0 +1,51 @@
"""
Настройка логирования
"""
import logging
import os
from datetime import datetime
from config import LOGGING_CONFIG
def setup_logger(name: str = "sro_parser") -> logging.Logger:
"""
Настройка логгера для парсера
Args:
name: Имя логгера
Returns:
Настроенный логгер
"""
# Создание директории для логов
log_dir = LOGGING_CONFIG.get("log_dir", "logs/")
os.makedirs(log_dir, exist_ok=True)
# Создание логгера
logger = logging.getLogger(name)
logger.setLevel(LOGGING_CONFIG.get("level", "INFO"))
# Очистка существующих обработчиков
if logger.hasHandlers():
logger.handlers.clear()
# Формат логов
formatter = logging.Formatter(LOGGING_CONFIG.get("format"))
# Обработчик для консоли
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Обработчик для файла
log_file = os.path.join(
log_dir, f"parser_{datetime.now().strftime('%Y-%m-%d')}.log"
)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger