mirror of
https://github.com/ada-dmitry/osint_parser_ada.git
synced 2026-09-24 01:10:20 +00:00
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:
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user