ΠŸΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ Π΄ΠΎ змісту

4. Communication Channels & Protocols (Канали ΠΊΠΎΠΌΡƒΠ½Ρ–ΠΊΠ°Ρ†Ρ–Ρ—)

πŸ“‹ Зміст

  1. Огляд ΠΊΠ°Π½Π°Π»Ρ–Π²
  2. Event Bus (Observer Pattern)
  3. Direct Method Calls
  4. Async Messaging (Celery)
  5. Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈ ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ

Огляд ΠΊΠ°Π½Π°Π»Ρ–Π²

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         COMMUNICATION CHANNELS                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                            β”‚
β”‚  β”‚   Spider    β”‚                                                            β”‚
β”‚  β”‚ (Orchestr.) β”‚                                                            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                                                            β”‚
β”‚         β”‚                                                                   β”‚
β”‚         β”‚ 1. Direct Method Calls (sync/async)                               β”‚
β”‚         β”‚    Spider β†’ Scheduler.add_url()                                   β”‚
β”‚         β”‚    Spider β†’ Driver.fetch()                                        β”‚
β”‚         β”‚    Spider β†’ Storage.save_graph()                                  β”‚
β”‚         β”‚                                                                   β”‚
β”‚         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚         β–Ό                 β–Ό                 β–Ό                 β–Ό             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  Scheduler  β”‚   β”‚   Driver    β”‚   β”‚   Storage   β”‚   β”‚   Plugins   β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚         β”‚                 β”‚                 β”‚                 β”‚             β”‚
β”‚         β”‚ 2. Event Bus (Observer Pattern - async/sync)                      β”‚
β”‚         β”‚    ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΈ ΠΏΡƒΠ±Π»Ρ–ΠΊΡƒΡŽΡ‚ΡŒ ΠΏΠΎΠ΄Ρ–Ρ— β†’ EventBus β†’ Subscribers           β”‚
β”‚         β”‚                                                                   β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                                   β”‚                                         β”‚
β”‚                                   β–Ό                                         β”‚
β”‚                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”‚
β”‚                          β”‚    EventBus     β”‚                                β”‚
β”‚                          β”‚   (Pub/Sub)     β”‚                                β”‚
β”‚                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚
β”‚                                   β”‚                                         β”‚
β”‚                                   β”‚ notify()                                β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚                    β–Ό              β–Ό              β–Ό                          β”‚
β”‚             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚             β”‚ Dashboardβ”‚  β”‚ Loggers  β”‚   β”‚ Analyticsβ”‚                       β”‚
β”‚             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                                                                             β”‚
β”‚  ════════════════════════════════════════════════════════════════════════   β”‚
β”‚                                                                             β”‚
β”‚  3. Async Messaging (Celery - для distributed mode)                         β”‚
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚ Coordinator │───▢│ Redis/RabbitMQ  │───▢│   Workers   β”‚                  β”‚
β”‚  β”‚  (Master)   β”‚    β”‚   (Broker)      β”‚    β”‚  (Celery)   β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                                                   β”‚                         β”‚
β”‚                                                   β–Ό                         β”‚
β”‚                                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚                                           β”‚  MongoDB/   β”‚                   β”‚
β”‚                                           β”‚ PostgreSQL  β”‚                   β”‚
β”‚                                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Event Bus

ΠŸΡ€ΠΈΠ·Π½Π°Ρ‡Π΅Π½Π½Ρ

Loose coupling ΠΌΡ–ΠΆ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Π°ΠΌΠΈ Ρ‡Π΅Ρ€Π΅Π· Observer Pattern.

ΠŸΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»

# ΠŸΡƒΠ±Π»Ρ–ΠΊΠ°Ρ†Ρ–Ρ (Producer)
event = CrawlerEvent.create(
    event_type=EventType.NODE_SCANNED,
    data={'url': 'https://example.com', 'status': 200},
    metadata={'spider_id': 'spider-1'}
)
event_bus.publish(event)           # Sync
await event_bus.publish_async(event)  # Async

# ΠŸΡ–Π΄ΠΏΠΈΡΠΊΠ° (Consumer)
def handler(event: CrawlerEvent):
    print(f"Event: {event.event_type}, Data: {event.data}")

event_bus.subscribe(EventType.NODE_SCANNED, handler)

Π€ΠΎΡ€ΠΌΠ°Ρ‚ CrawlerEvent

@dataclass
class CrawlerEvent:
    event_type: EventType      # Enum Π· 50+ Ρ‚ΠΈΠΏΡ–Π²
    timestamp: datetime        # Час ΠΏΠΎΠ΄Ρ–Ρ—
    data: Dict[str, Any]       # Payload (JSON-serializable)
    metadata: Dict[str, Any]   # Π”ΠΎΠ΄Π°Ρ‚ΠΊΠΎΠ²Ρ– ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Ρ–

ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ— ΠΏΠΎΠ΄Ρ–ΠΉ

ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€Ρ–Ρ Event Types Опис
Node NODE_CREATED, NODE_SCAN_STARTED, NODE_SCANNED, NODE_FAILED Π–ΠΈΡ‚Ρ‚Ρ”Π²ΠΈΠΉ Ρ†ΠΈΠΊΠ» Π²ΡƒΠ·Π»Π°
Crawler CRAWL_STARTED, CRAWL_COMPLETED, CRAWL_PAUSED, CRAWL_RESUMED Π‘Ρ‚Π°Π½ ΠΊΡ€Π°ΡƒΠ»Π΅Ρ€Π°
Scheduler URL_ADDED_TO_QUEUE, URL_EXCLUDED, URL_PRIORITIZED Π§Π΅Ρ€Π³Π° URL
Storage GRAPH_SAVED, GRAPH_LOADED, STORAGE_UPGRADED ΠžΠΏΠ΅Ρ€Π°Ρ†Ρ–Ρ— Π·Ρ– сховищСм
Plugin PLUGIN_STARTED, PLUGIN_COMPLETED, PLUGIN_FAILED Виконання ΠΏΠ»Π°Π³Ρ–Π½Ρ–Π²
Middleware RATE_LIMIT_WAIT, PROXY_SELECTED, RETRY_STARTED Middleware events
Fetch FETCH_STARTED, FETCH_SUCCESS, FETCH_ERROR HTTP Π·Π°ΠΏΠΈΡ‚ΠΈ
Progress PROGRESS_UPDATE, PAGE_FETCH_TIME ΠœΠΎΠ½Ρ–Ρ‚ΠΎΡ€ΠΈΠ½Π³

ΠŸΡ€ΠΈΠΊΠ»Π°Π΄ΠΈ використання

# 1. Логування всіх ΠΏΠΎΠ΄Ρ–ΠΉ
def log_handler(event):
    logger.info(f"{event.event_type.value}: {event.data}")

for event_type in EventType:
    event_bus.subscribe(event_type, log_handler)

# 2. ΠœΠΎΠ½Ρ–Ρ‚ΠΎΡ€ΠΈΠ½Π³ прогрСсу
def progress_handler(event):
    data = event.data
    print(f"Progress: {data['scanned']}/{data['total']} ({data['percent']:.1f}%)")

event_bus.subscribe(EventType.PROGRESS_UPDATE, progress_handler)

# 3. Error alerting
async def error_alert(event):
    if event.data.get('severity') == 'critical':
        await send_slack_alert(event.data)

event_bus.subscribe(EventType.ERROR_OCCURRED, error_alert)

# 4. Analytics collection
class AnalyticsCollector:
    def __init__(self):
        self.events = []

    def collect(self, event):
        self.events.append(event.to_dict())

collector = AnalyticsCollector()
event_bus.subscribe(EventType.NODE_SCANNED, collector.collect)

Direct Method Calls

ΠŸΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ» Spider β†’ Components

Spider
  β”‚
  β”œβ”€β”€β–Ά Scheduler
  β”‚      β€’ add_url(url, depth) β†’ bool
  β”‚      β€’ add_node(node) β†’ bool  
  β”‚      β€’ get_next_url() β†’ Optional[Tuple[str, int]]
  β”‚      β€’ is_empty() β†’ bool
  β”‚
  β”œβ”€β”€β–Ά Driver (async)
  β”‚      β€’ await fetch(url) β†’ FetchResponse
  β”‚      β€’ await fetch_many(urls) β†’ List[FetchResponse]
  β”‚      β€’ await close()
  β”‚
  β”œβ”€β”€β–Ά Storage (async)
  β”‚      β€’ await save_graph(graph) β†’ bool
  β”‚      β€’ await load_graph() β†’ Optional[Graph]
  β”‚      β€’ await exists() β†’ bool
  β”‚      β€’ await close()
  β”‚
  β”œβ”€β”€β–Ά NodeScanner
  β”‚      β€’ await scan_node(node, html) β†’ List[str]
  β”‚
  └──▢ LinkProcessor
         β€’ process_links(source_node, links) β†’ List[Node]
         β€’ create_edge(source, target) β†’ Edge

ΠŸΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ» Node β†’ Plugins

# Node.process_html() Π²Π½ΡƒΡ‚Ρ€Ρ–ΡˆΠ½ΡŒΠΎ Π²ΠΈΠΊΠ»ΠΈΠΊΠ°Ρ”:
await plugin_manager.execute(NodePluginType.ON_BEFORE_SCAN, context)
await plugin_manager.execute(NodePluginType.ON_HTML_PARSED, context)
await plugin_manager.execute(NodePluginType.ON_AFTER_SCAN, context)

ІнтСрфСйси (Protocols)

# IDriver
class IDriver(Protocol):
    async def fetch(self, url: str) -> FetchResponse: ...
    async def fetch_many(self, urls: List[str]) -> List[FetchResponse]: ...
    async def close(self) -> None: ...

# IStorage  
class IStorage(Protocol):
    async def save_graph(self, graph) -> bool: ...
    async def load_graph(self) -> Optional[Graph]: ...
    async def exists(self) -> bool: ...
    async def close(self) -> None: ...

# IFilter
class IFilter(Protocol):
    def should_scan(self, url: str) -> bool: ...
    def apply(self, url: str, node: Node) -> bool: ...

Async Messaging

Celery Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           DISTRIBUTED CRAWLING                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                            β”‚
β”‚  LOCAL (Master)                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                   β”‚
β”‚  β”‚ EasyDistributedCrawler β”‚                                                β”‚
β”‚  β”‚                     β”‚                                                   β”‚
β”‚  β”‚ β€’ from_yaml(config) β”‚                                                   β”‚
β”‚  β”‚ β€’ crawl()           β”‚                                                   β”‚
β”‚  β”‚ β€’ get_stats()       β”‚                                                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                   β”‚
β”‚             β”‚                                                              β”‚
β”‚             β”‚ 1. Push crawl_page_task                                      β”‚
β”‚             β–Ό                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚  β”‚           REDIS/RABBITMQ BROKER             β”‚                           β”‚
β”‚  β”‚                                             β”‚                           β”‚
β”‚  β”‚  Queue: graph_crawler                       β”‚                           β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”           β”‚                           β”‚
β”‚  β”‚  β”‚Task1β”‚ β”‚Task2β”‚ β”‚Task3β”‚ β”‚...  β”‚           β”‚                           β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜           β”‚                           β”‚
β”‚  β”‚                                             β”‚                           β”‚
β”‚  β”‚  Format:                                    β”‚                           β”‚
β”‚  β”‚  {                                          β”‚                           β”‚
β”‚  β”‚    "url": "https://...",                   β”‚                           β”‚
β”‚  β”‚    "depth": 2,                              β”‚                           β”‚
β”‚  β”‚    "config": {...}                          β”‚                           β”‚
β”‚  β”‚  }                                          β”‚                           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚                     β”‚                                                      β”‚
β”‚                     β”‚ 2. Workers pull tasks                                β”‚
β”‚                     β–Ό                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚                   CELERY WORKERS (N servers)                β”‚           β”‚
β”‚  β”‚                                                             β”‚           β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚           β”‚
β”‚  β”‚  β”‚ Worker 1  β”‚  β”‚ Worker 2  β”‚  β”‚ Worker N  β”‚               β”‚           β”‚
β”‚  β”‚  β”‚           β”‚  β”‚           β”‚  β”‚           β”‚               β”‚           β”‚
β”‚  β”‚  β”‚ β€’ Driver  β”‚  β”‚ β€’ Driver  β”‚  β”‚ β€’ Driver  β”‚               β”‚           β”‚
β”‚  β”‚  β”‚ β€’ Plugins β”‚  β”‚ β€’ Plugins β”‚  β”‚ β€’ Plugins β”‚               β”‚           β”‚
β”‚  β”‚  β”‚ β€’ Parser  β”‚  β”‚ β€’ Parser  β”‚  β”‚ β€’ Parser  β”‚               β”‚           β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜               β”‚           β”‚
β”‚  β”‚        β”‚              β”‚              β”‚                      β”‚           β”‚
β”‚  β”‚        β”‚ 3. Fetch & Extract                                 β”‚           β”‚
β”‚  β”‚        β–Ό              β–Ό              β–Ό                      β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                           β”‚                                                β”‚
β”‚                           β”‚ 4. Save results                                β”‚
β”‚                           β–Ό                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚  β”‚         MONGODB/POSTGRESQL (Results)        β”‚                           β”‚
β”‚  β”‚                                             β”‚                           β”‚
β”‚  β”‚  Collection: nodes                          β”‚                           β”‚
β”‚  β”‚  Collection: edges                          β”‚                           β”‚
β”‚  β”‚  Collection: queue (pending URLs)           β”‚                           β”‚
β”‚  β”‚                                             β”‚                           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚                                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Task Protocol

# Celery Task Definition
@celery.task(bind=True, max_retries=3)
def crawl_page_task(self, url: str, depth: int, config: dict):
    """
    Crawl single page task.

    Input:
        url: URL to crawl
        depth: Current depth
        config: Crawler configuration

    Output:
        {
            'url': str,
            'status': 'success' | 'error',
            'node': NodeDTO | None,
            'links': List[str],
            'error': str | None
        }
    """
    ...

Configuration Protocol (YAML)

# config.yaml
broker:
  type: redis           # redis | rabbitmq
  host: server11.example.com
  port: 6379
  db: 0

database:
  type: mongodb         # mongodb | postgresql
  host: server12.example.com
  port: 27017
  database: crawler_results

crawl_task:
  urls:
    - https://example.com
  max_depth: 3
  max_pages: 1000
  extractors:
    - phones
    - emails
    - prices

workers: 10
task_time_limit: 600

Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈ ΠΏΠΎΠ²Ρ–Π΄ΠΎΠΌΠ»Π΅Π½ΡŒ

FetchResponse

@dataclass
class FetchResponse:
    url: str                        # Original URL
    html: Optional[str]             # HTML content
    status_code: Optional[int]      # HTTP status
    headers: Dict[str, str]         # Response headers
    error: Optional[str]            # Error message
    final_url: Optional[str]        # After redirects
    redirect_chain: List[str]       # Redirect history

# Properties
response.is_success  # error is None and html is not None
response.is_ok       # status_code 2xx
response.is_redirect # final_url != url

NodePluginContext

@dataclass
class NodePluginContext:
    # Basic (always available)
    node: Node
    url: str
    depth: int
    should_scan: bool
    can_create_edges: bool

    # HTML Stage (after fetch)
    html: Optional[str]
    html_tree: Optional[Any]       # BeautifulSoup/lxml tree
    parser: Optional[BaseAdapter]

    # Results (modifiable)
    metadata: Dict[str, Any]
    user_data: Dict[str, Any]
    extracted_links: List[str]

MiddlewareContext

@dataclass
class MiddlewareContext:
    url: str
    headers: Dict[str, str]
    cookies: Dict[str, str]
    proxy: Optional[str]
    timeout: int

    # Response (POST_REQUEST)
    response: Optional[FetchResponse]
    error: Optional[Exception]

    # Control
    skip_request: bool = False     # Skip fetch (cache hit)
    retry_count: int = 0

GraphDTO (Clean Architecture)

@dataclass
class GraphDTO:
    nodes: List[NodeDTO]
    edges: List[EdgeDTO]
    stats: GraphStatsDTO
    metadata: Dict[str, Any]

@dataclass
class NodeDTO:
    url: str
    node_id: str
    depth: int
    scanned: bool
    metadata: Dict[str, Any]
    user_data: Dict[str, Any]

@dataclass  
class EdgeDTO:
    edge_id: str
    source_node_id: str
    target_node_id: str
    anchor_text: str
    link_type: List[str]