Patrones de Diseño para Apify Actors: Cómo Construir Extracción de Datos que No Falla en Producción
Patrones de diseño para Apify actors: schemas de entrada, retry logic y datasets estructurados. Aprende a construir extracción que escala sin fallos.
Tu Actor de Apify Falla en Silencio
Tu actor devuelve `extract()` sin errores. El dataset está vacío. Nadie se entera hasta que el cliente pregunta por qué no recibe datos.
*El problema real no es el parsing de HTML. Es que estás construyendo scripts, no agentes de producción.*
Paperclip explotó a 30.000 usuarios cuando la gente descubrió que construir agentes de verdad requiere contratos claros entre componentes.
Google ADK tiene 19.000 estrellas en GitHub. Su execution engine basada en grafos implementa retry, state management y error handling como features nativos.
Tú estás haciendo `fetch()` y祈祷.
Estos son los 4 patrones de diseño que necesitas para construir Apify actors que escalan sin darte vergüenza.
Por Qué Tus Selectores Funcionan en Dev y Fallan en Prod
La mayoría de developers de Apify siguen el mismo patrón:
1. Instalan `playwright` o `cheerio`
2. Hacen fetch de la URL
3. Extraen con selectores CSS
4. Guardan en dataset
5. Deploy
Esto funciona para 1 URL. Para 10.000 URLs con estructura cambiante, ratelimiting impredecible, y 30.000 usuarios esperando datos consistentes, esto es gambling con tu reputación.
*La sabiduría convencional dice que web scraping trata de parsear HTML y manejar selectores.*
La realidad: el 90% de los fallos en producción vienen de falta de validación en los inputs y outputs. No de selectores rotos.
Alphora demuestra por qué esto importa. Su `@tool` decorator genera automáticamente schemas de OpenAI function calling desde type hints y docstrings. Cero configuración manual. Validación en runtime sin esfuerzo extra.
Apify tiene un sistema similar con sus input schemas. La mayoría de developers lo ignoran porque "agrega complejidad".
Aquí está el secreto que nadie te dice: la complejidad que ignoras ahora se convierte en bugs que解决不了 en producción.
El Contrato Que Falta en Tu Actor
Google ADK implementa lo que llama "workflow runtime" con soporte nativo para retry, state management, dynamic nodes y error handling.
Esto no es sobreengineering. Es lo mínimo necesario para scraping a escala.
Patrón 1: Schema-First Input Validation
La mayoría de actors de Apify reciben inputs como esto:
```javascript
// ❌ WRONG: No validation, silent failures
async function main(input) {
const { url, selector } = input;
// What if url is missing? What if selector is wrong type?
await crawl(url, selector);
}
```
Deberías estar validando antes de ejecutar cualquier lógica de negocio. Alphora hace esto automáticamente con type hints. Tú tienes que hacerlo manualmente, pero es igual de importante:
```python
✅ RIGHT: Validate before execution
from pydantic import BaseModel, validator
from apify import Actor
class InputSchema(BaseModel):
url: str
max_depth: int = 3
selectors: list[str]
@validator('url')
def validate_url(cls, v):
if not v.startswith(('http://', 'https://')):
raise ValueError('URL must start with http:// or https://')
return v
@validator('max_depth')
def validate_depth(cls, v):
if v < 1 or v > 10:
raise ValueError('max_depth must be between 1 and 10')
return v
async def main():
actor = Actor()
input_data = await actor.get_input()
try:
validated = InputSchema(**input_data)
except ValueError as e:
await actor.fail(f'Invalid input: {e}')
return
Now you know your contract is fulfilled
await crawl(validated.url, validated.selectors, validated.max_depth)
```
Este patrón de Apify se mapea directamente al enfoque de Alphora con su `@tool` decorator. Ambos generan validación desde definición de tipos. La diferencia: tú controlas exactamente qué se acepta y qué se rechaza.
Patrón 2: Retry Logic con Circuit Breakers
Google ADK enfatiza retry y state management como primitives de primer nivel. Tú necesitas implementar esto explícitamente para cada llamada externa.
```python
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential
from functools import wraps
class CircuitBreaker:
def __init__(self, max_failures=5, timeout=60):
self.failures = 0
self.max_failures = max_failures
self.timeout = timeout
self.last_failure_time = None
self.state = 'closed' # closed, open, half-open
def call(self, func, args, *kwargs):
if self.state == 'open':
if time.time() - self.last_failure_time > self.timeout:
self.state = 'half-open'
else:
raise Exception('Circuit breaker is OPEN')
try:
result = func(args, *kwargs)
if self.state == 'half-open':
self.state = 'closed'
self.failures = 0
return result
except Exception as e:
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.max_failures:
self.state = 'open'
raise e
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
async def fetch_with_retry(session, url, selectors):
response = await session.get(url)
if response.status == 429: # Rate limited
raise RateLimitException('Too many requests')
if response.status >= 500:
raise ServerErrorException(f'Server error: {response.status}')
return parse_html(response.text, selectors)
```
La diferencia entre platform-level retries y domain-specific retry logic es crítica. Apify reintenta automáticamente en algunos casos. Pero tú sabes mejor que nadie cuándo un selector necesita fallback, cuándo una API específica requiere backoff exponencial, y cuándo es mejor saltar y marcar como error que seguir insistiendo.
Patrón 3: Structured Output Datasets
Aquí es donde la mayoría de actors fallan completamente. Escriben datos inconsistentes y se sorprenden cuando downstream consumers explotan.
Paperclip escala a 30.000 usuarios porque cada integración sabe exactamente qué esperar. Tú deberías hacer lo mismo:
```python
from datetime import datetime
from apify import Actor
class OutputSchema(BaseModel):
url: str
extracted_at: str # ISO format
status: str # 'success', 'failed', 'partial'
data: dict | None # None if failed
error: str | None # None if success
metadata: dict = {
'parser_version': '1.0.0',
'actor_version': '2.3.1',
'extraction_duration_ms': 0
}
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
async def save_result(url: str, data: dict | None, error: str | None, duration_ms: float):
start_time = datetime.utcnow()
result = OutputSchema(
url=url,
extracted_at=datetime.utcnow().isoformat(),
status='success' if data else 'failed',
data=data,
error=error,
metadata={
'parser_version': '1.0.0',
'actor_version': '2.3.1',
'extraction_duration_ms': duration_ms
}
)
dataset = await Actor.open_dataset('extraction-results')
await dataset.push_data(result)
return result
```
Este formato es predecible. Consumers pueden procesar resultados sin guesswork. Cuando algo falla, tienes el error logged con contexto. Cuando funciona, tienes versioning en metadata para debugging.
Implementa Estos Patrones en 4 Pasos
Paso 1: Define tu input contract con Pydantic o JSON Schema antes de escribir cualquier lógica de extracción.
```python
apify_input_schema.json
{
"type": "object",
"properties": {
"startUrls": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 1000
},
"maxConcurrency": {
"type": "integer",
"default": 5,
"minimum": 1,
"maximum": 50
},
"retryCount": {
"type": "integer",
"default": 3,
"minimum": 0,
"maximum": 10
}
},
"required": ["startUrls"]
}
```
Paso 2: Implementa retry con exponential backoff para HTTP requests y fallback selectors para sitios con estructura variable.
```python
SELECTOR_FALLBACKS = {
'article': ['article.main-content', '.post-body', '.article', 'main'],
'title': ['h1.title', '.headline', 'h1', 'title'],
'price': ['.price-current', '.product-price', '[data-price]']
}
async def extract_with_fallback(html: str, field: str) -> str | None:
for selector in SELECTOR_FALLBACKS.get(field, []):
try:
result = await page.query_selector(selector)
if result:
return await result.text_content()
except:
continue
return None # All fallbacks failed
```
Paso 3: Estandariza tu output dataset con schemas consistentes y metadata para tracking.
```python
OUTPUT_FIELDS = ['url', 'extracted_at', 'status', 'data', 'error', 'metadata']
Every single result has these fields. Always.
```
Paso 4: Añade lifecycle hooks para logging, metrics y alerting cuando patterns de error aparezcan.
```python
async def on_extraction_failed(url: str, error: Exception, attempt: int):
await Actor.log.error(f'Failed extracting {url} after {attempt} attempts: {error}')
Alert if failure rate exceeds threshold
failure_rate = failed_count / total_count
if failure_rate > 0.1: # 10% threshold
await send_alert(f'Failure rate {failure_rate:.1%} exceeds threshold')
```
Responde a Tus Objeciones Antes de Que las Pienses
"Esto es sobreingeniería para scrapers simples"
Scrapers simples se convierten en pipelines de producción. Cada vez. El schema que ignoras hoy es la deuda técnica que pagas mañana debuggeando datos inconsistentes a las 3 AM.
"Apify ya maneja retries y error logging"
Los retries de plataforma son genéricos. Tú sabes que cierto sitio necesita 5 segundos de backoff después de un 429. Tú sabes que cierto selector tiene fallback viable. La lógica específica del dominio requiere implementación específica del dominio.
"Los schemas añaden complejidad innecesaria para websites dinámicos"
Websites dinámicos necesitan validación más estricta, no menos. Si la estructura cambia, tu schema te lo dice inmediatamente con un error claro. Sin schema, obtienes campos vacíos que descubres 3 días después cuando el dashboard muestra gráficos extraños.
Lo Que Necesitas Recordar
Valida inputs con schemas antes de ejecutar lógica de negocio
Implementa retry domain-specific con circuit breakers, no relies en platform defaults
Estandariza outputs con campos consistentes y metadata versioning
Añade lifecycle hooks para detectar patterns de error antes de que escalen
*La diferencia entre scripts que fallan y agentes que escalan es intencional.*
Paperclip no llegó a 30.000 usuarios por suerte. Llegó porque cada componente tiene un contrato claro. Tú puedes hacer lo mismo con tus Apify actors si paras de tratar el input validation como opcional.
Construye el contrato primero. El resto viene solo.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

