architect/standards/1-structure/structure-component.md

type: standard
aspect: structure
title: "Архитектура компонентов: Parser / Scraper / Connector / Adapter"
version: 1.0.0
date: 2026-02-19
status: active


Архитектура компонентов: Parser / Scraper / Connector / Adapter

Версия: 1.0.0
Дата: 2025-12-24
Статус: СТАНДАРТ


ТЕРМИНОЛОГИЯ

Классы vs Экземпляры

КЛАССЫ (определения)                ЭКЗЕМПЛЯРЫ (использование)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

BaseConnector (абстрактный)         ─── нельзя создать
    │
    ├── BaseApiConnector            ─── нельзя создать
    │       │
    │       └── OzonConnector       ozon = OzonConnector(creds)
    │       └── OneC_Connector      onec = OneC_Connector(creds)
    │
    └── BaseDataConnector           ─── нельзя создать
            │
            └── PostgresConnector   pg = PostgresConnector(creds)
            └── NocoDBConnector     ndb = NocoDBConnector(creds)

КЛАСС (Class) — шаблон/чертёж, определяет ЧТО может делать.
ЭКЗЕМПЛЯР (Instance) — конкретный объект, созданный из класса.


4 ТИПА КОМПОНЕНТОВ

┌────────────────────────────────────────────────────────────────────┐
│                      ИСТОЧНИК → ПРИЁМНИК                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│   CONNECTOR          PARSER           ADAPTER          STORAGE    │
│   ═══════════        ═══════          ═══════          ═══════    │
│   Транспорт          Формат           Маппинг          Хранение   │
│                                                                    │
│   API ←→ Код         bytes → dict     dict → model     model → DB │
│   DB ←→ Код          файл → записи    чужое → своё                │
│   File ←→ Код                                                     │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

1. CONNECTOR — Транспорт

Что делает: Устанавливает связь с внешней системой (API, БД, файл).
Не делает: Не трансформирует данные, не знает о бизнес-логике.

# library/connectors/base.py
class BaseConnector(ABC):
    async def connect()  None
    async def disconnect()  None
    async def health_check()  bool

# library/connectors/api/ozon/client.py
class OzonConnector(BaseApiConnector):
    async def get_orders()  List[Dict]   # Сырые данные API

Примеры:
| Класс | Что подключает |
|-------|---------------|
| OzonConnector | OZON Seller API |
| OneC_Connector | 1С OData REST |
| PostgresConnector | PostgreSQL |
| TelegramConnector | Telegram Bot API |

2. PARSER — Формат

Что делает: Читает формат файла и возвращает структуру данных.
Не делает: Не трансформирует, не валидирует бизнес-данные.

# library/parsers/base.py
class BaseParser(ABC):
    def parse(source: bytes|Path)  List[Dict]
    def can_parse(filename: str)  bool

# library/parsers/xlsx/parser.py
class XlsxParser(BaseTabularParser):
    SUPPORTED_EXTENSIONS = [".xlsx", ".xls"]
    def parse(source, header_row=1)  List[Dict]

Примеры:
| Класс | Что читает |
|-------|-----------|
| XlsxParser | Excel файлы |
| CsvParser | CSV файлы |
| PdfParser | PDF документы |
| JsonParser | JSON файлы |

3. SCRAPER — Сбор данных с сайта (специальный Parser)

Что делает: Загружает HTML страницы и извлекает данные.
Отличие от Parser: Сам загружает данные (имеет HTTP клиент).

# library/scrapers/base.py
class BaseScraper(ABC):
    async def fetch_page(url: str)  str          # HTML
    async def scrape_product(url: str)  Dict     # Данные продукта
    async def scrape_catalog()  List[Dict]       # Все продукты

# projects/pirotehnika/app/pim/scrapers/jfpyro.py
class JFPyroScraper(BaseScraper):
    BASE_URL = "https://jf-pyro.ru"
    async def scrape_product(url)  JFProduct

Важно: Scraper = Parser + HTTP клиент. Это подтип Parser для веб-сайтов.

Примеры:
| Класс | Что парсит |
|-------|-----------|
| JFPyroScraper | jf-pyro.ru |
| MaxsemScraper | maxsem.ru |
| PiroffScraper | piroff.ru |

4. ADAPTER — Маппинг

Что делает: Преобразует данные из одного формата в другой.
Не делает: Не загружает данные, не сохраняет.

# library/adapters/base.py
class BaseAdapter(ABC, Generic[InputT, OutputT]):
    def adapt(data: InputT)  OutputT
    def adapt_many(data: List[InputT])  List[OutputT]

# projects/pirotehnika/app/pim/adapters/price_adapter.py
class MaxsemPriceAdapter(BaseAdapter[Dict, PimProduct]):
    def adapt(raw: Dict)  PimProduct:
        return PimProduct(
            sku=raw["article"],
            name=raw["name"],
            cost_price=int(raw["price"] * 100),  # руб → коп
        )

Примеры:
| Класс | Преобразование |
|-------|---------------|
| OzonOrderAdapter | OZON Order → PimOrder |
| OneCProductAdapter | 1С Номенклатура → PimProduct |
| MaxsemPriceAdapter | Maxsem Price → PimProduct |


ИЕРАРХИЯ КЛАССОВ

library/                                    projects/pirotehnika/app/pim/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CONNECTORS
├── BaseConnector (ABC)
   ├── BaseApiConnector
      ├── OzonConnector
      ├── OneC_Connector                   OneC_PimConnector (обёртка)
      ├── TelegramConnector
      └── PochtaConnector
   ├── BaseDataConnector
      ├── PostgresConnector
      └── NocoDBConnector                  PimNocoDBClient (обёртка)
   └── BaseFileConnector
       └── S3Connector

PARSERS
├── BaseParser (ABC)
   ├── BaseTabularParser
      ├── XlsxParser                       (наследуют PriceAdapters)
      └── CsvParser
   └── JsonParser

SCRAPERS
├── BaseScraper (ABC)
   └── BaseShopScraper
       ├── (пусто)                          JFPyroScraper
       └── (пусто)                          MaxsemScraper

ADAPTERS
├── BaseAdapter (ABC)
   ├── ConfigurableAdapter
   └── (пусто)                              OneCProductAdapter
                                            OzonOrderAdapter
                                            MaxsemPriceAdapter

SERVICES (L5)
├── BaseService                              PimProductService
   └── BaseSyncService                      PimSyncService

INTEGRATIONS (L6)
├── BaseIntegration
   └── BaseSyncIntegration                  OneC_PimIntegration

РАЗМЕЩЕНИЕ КОДА

Правило: Library vs Project

┌────────────────────────────────────────────────────────────────────┐
│                                                                    │
│   LIBRARY (платформа)           PROJECT (проект)                   │
│   ═══════════════════           ═══════════════                    │
│                                                                    │
│   Универсальный код             Специфичный код                    │
│   Не зависит от проекта         Зависит от данных проекта          │
│                                                                    │
│   BaseConnector                 JFPyroScraper                      │
│   OzonConnector                 MaxsemPriceAdapter                 │
│   XlsxParser                    PimProductService                  │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Полная структура

$WORKSPACE/
├── library/                            L4: ПЛАТФОРМА (переиспользуемое)
   ├── primitives/                     L2: Типы
      ├── types.py
      ├── enums.py
      └── exceptions.py
   
   ├── functions/                      L3: Чистые функции
      ├── format/
      ├── validate/
      └── normalize/
   
   ├── connectors/                     L4: Коннекторы
      ├── base.py                     BaseConnector, BaseApiConnector
      ├── api/
         ├── ozon/client.py          OzonConnector
         ├── 1c/client.py            OneC_Connector
         ├── telegram/client.py      TelegramConnector
         └── pochta/client.py        PochtaConnector
      └── data/
          ├── postgres/client.py      PostgresConnector
          └── nocodb/client.py        NocoDBConnector
   
   ├── parsers/                        L4: Парсеры форматов
      ├── base.py                     BaseParser, BaseTabularParser
      ├── xlsx/parser.py              XlsxParser
      ├── csv/parser.py               CsvParser
      └── json/parser.py              JsonParser
   
   ├── scrapers/                       L4: Скраперы (NEW!)
      ├── base.py                     BaseScraper, BaseShopScraper
      └── (проектные в projects/)
   
   ├── adapters/                       L4: Адаптеры
      ├── base.py                     BaseAdapter, ConfigurableAdapter
      └── (проектные в projects/)
   
   ├── storages/                       L4: Хранилища
      └── base.py                     BaseRepository
   
   ├── services/                       L5: Сервисы
      └── base.py                     BaseService, BaseSyncService
   
   └── integrations/                   L6: Интеграции
       └── base.py                     BaseIntegration

└── projects/org/pirotehnika/
    └── app/pim/                        ПРОЕКТ: PIM Пиротехника
        ├── connectors/                 Обёртки над library
           ├── onec.py                 OneC_PimConnector(OneC_Connector)
           └── nocodb.py               PimNocoDBClient(NocoDBConnector)
        
        ├── scrapers/                   Скраперы производителей
           ├── base.py                 BasePyroScraper(BaseScraper)
           ├── jfpyro.py               JFPyroScraper
           └── maxsem.py               MaxsemScraper
        
        ├── adapters/                   Адаптеры данных
           ├── price_adapters/         Прайсы  PimProduct
              ├── base.py             BasePriceAdapter
              ├── maxsem.py           MaxsemPriceAdapter
              └── jf.py               JFPriceAdapter
           └── onec_adapter.py         1С  PimProduct
        
        ├── core/                       Бизнес-логика
           ├── products.py             PimProductService
           ├── pricing.py              PimPricingService
           └── sync.py                 PimSyncService
        
        └── models/                     Модели данных
            └── product.py              PimProduct, PimProductImage

ПОЛНЫЙ ПЕРЕЧЕНЬ КОМПОНЕНТОВ

Library (платформа)

Компонент Путь Назначение
Connectors
BaseConnector library/connectors/base.py Абстрактный базовый
BaseApiConnector library/connectors/base.py HTTP API
BaseDataConnector library/connectors/base.py Базы данных
BaseFileConnector library/connectors/base.py Файловые хранилища
OzonConnector library/connectors/api/ozon/client.py OZON API
OneC_Connector library/connectors/api/1c/client.py 1С OData
TelegramConnector library/connectors/api/telegram/client.py Telegram Bot
PostgresConnector library/connectors/data/postgres/client.py PostgreSQL
NocoDBConnector library/connectors/data/nocodb/client.py NocoDB
Parsers
BaseParser library/parsers/base.py Абстрактный базовый
BaseTabularParser library/parsers/base.py Табличные данные
XlsxParser library/parsers/xlsx/parser.py Excel
CsvParser library/parsers/csv/parser.py CSV
Scrapers
BaseScraper library/scrapers/base.py Абстрактный базовый
BaseShopScraper library/scrapers/base.py Интернет-магазины
Adapters
BaseAdapter library/adapters/base.py Абстрактный базовый
ConfigurableAdapter library/adapters/base.py С конфигом маппинга

Project: PIM Pirotehnika

Компонент Путь Наследует от
Connectors (обёртки)
OneC_PimConnector app/pim/connectors/onec.py OneC_Connector
PimNocoDBClient app/pim/connectors/nocodb.py NocoDBConnector
Scrapers
BasePyroScraper app/pim/scrapers/base.py BaseScraper
JFPyroScraper app/pim/scrapers/jfpyro.py BasePyroScraper
MaxsemScraper app/pim/scrapers/maxsem.py BasePyroScraper
Adapters
BasePriceAdapter app/pim/adapters/price_adapters/base.py BaseAdapter
MaxsemPriceAdapter app/pim/adapters/price_adapters/maxsem.py BasePriceAdapter
JFPriceAdapter app/pim/adapters/price_adapters/jf.py BasePriceAdapter
OneCProductAdapter app/pim/adapters/onec_adapter.py BaseAdapter
Services
PimProductService app/pim/core/products.py BaseService
PimSyncService app/pim/core/sync.py BaseSyncService

КАК ОФОРМЛЯТЬ КОМПОНЕНТЫ

Connector

# library/connectors/api/{name}/client.py
"""
{Name} Connector — подключение к {API_NAME}.
"""
from library.connectors.base import BaseApiConnector

class {Name}Connector(BaseApiConnector):
    """
    Коннектор к {API_NAME}.

    Usage:
        async with {Name}Connector(credentials) as conn:
            data = await conn.get_something()
    """

    BASE_URL = "https://api.example.com"

    def _get_auth_headers(self) -> Dict[str, str]:
        return {"Authorization": f"Bearer {self.credentials['api_key']}"}

    async def get_something(self) -> List[Dict]:
        return await self.get("/v1/something")

Parser

# library/parsers/{format}/parser.py
"""
{Format} Parser — парсинг {FORMAT} файлов.
"""
from library.parsers.base import BaseParser

class {Format}Parser(BaseParser):
    """
    Парсер {FORMAT} файлов.

    Usage:
        parser = {Format}Parser()
        data = parser.parse("/path/to/file.{ext}")
    """

    SUPPORTED_EXTENSIONS = [".{ext}"]

    def parse(self, source, **options) -> List[Dict]:
        content = self._read_source(source)
        # ... парсинг
        return records

Scraper

# projects/{project}/app/pim/scrapers/{site}.py
"""
{Site} Scraper — парсинг сайта {domain}.
"""
from library.scrapers.base import BaseScraper

class {Site}Scraper(BaseScraper):
    """
    Скрапер сайта {domain}.

    Usage:
        scraper = {Site}Scraper()
        products = await scraper.scrape_catalog()
    """

    BASE_URL = "https://{domain}"
    DELAY = 2.0  # Секунды между запросами

    async def scrape_product(self, url: str) -> Dict:
        html = await self.fetch_page(url)
        # ... извлечение данных
        return product

Adapter

# projects/{project}/app/pim/adapters/{source}_adapter.py
"""
{Source} → PIM Adapter
"""
from library.adapters.base import BaseAdapter
from app.pim.models import PimProduct

class {Source}Adapter(BaseAdapter[Dict, PimProduct]):
    """
    Преобразование данных {source} в PimProduct.

    Usage:
        adapter = {Source}Adapter()
        product = adapter.adapt(raw_data)
    """

    def adapt(self, data: Dict) -> PimProduct:
        return PimProduct(
            sku=data.get("article", ""),
            name=data.get("name", ""),
            cost_price=self._safe_int(data.get("price", 0) * 100),
        )

МИГРАЦИЯ СУЩЕСТВУЮЩЕГО КОДА

Что перенести

Откуда Куда Статус
data/adapters/price_adapters.py app/pim/adapters/price_adapters/
mp1/solution/lib/scraper_jfpyro.py app/pim/scrapers/jfpyro.py
mp1/solution/lib/scraper_maxsem.py app/pim/scrapers/maxsem.py
mp1/solution/lib/connector_1c.py library/connectors/api/1c/
data/connectors/pochta/ library/connectors/api/pochta/

Что создать

Компонент Путь
BaseScraper library/scrapers/base.py
BaseShopScraper library/scrapers/base.py
BasePyroScraper app/pim/scrapers/base.py

ПОТОКИ ДАННЫХ

Импорт прайса

xlsx файл
    
    
XlsxParser.parse()               library/parsers/xlsx/
    
    
List[Dict] (сырые строки)
    
    
MaxsemPriceAdapter.adapt_many()  app/pim/adapters/
    
    
List[PimProduct]
    
    
PimProductService.save()         app/pim/core/
    
    
NocoDB (pim_staging_products)

Парсинг сайта производителя

URL сайта
    
    
JFPyroScraper.scrape_catalog()   app/pim/scrapers/
    
    
List[Dict] (спарсенные товары)
    
    
JFPyroAdapter.adapt_many()       app/pim/adapters/
    
    
List[PimProduct]
    
    
PimProductService.enrich()       Обогащение существующих
    
    
NocoDB (pim_products)

Синхронизация с 1С

1С OData API
    
    
OneC_PimConnector.get_products()  app/pim/connectors/
    
    
List[Dict] (данные 1С)
    
    
OneCProductAdapter.adapt_many()   app/pim/adapters/
    
    
List[PimProduct]
    
    
PimSyncService.sync()             app/pim/core/
    
    
NocoDB (1c_products  pim_products)

Версия: 1.0.0