Cómo construir un pipeline de datos de entrenamiento de IA con Scrapeless
Advanced Data Extraction Specialist
TL;DR:
- Un conjunto de datos de ajuste fino es un producto de canalización, no una descarga. Esta guía construye uno de manera integral: obtiene páginas públicas renderizadas a través de la API Universal Scraping de Scrapeless, extrae pares etiquetados con la biblioteca estándar de Python y escribe JSONL en formato de chat listo para OpenAI con una división de entrenamiento/validación.
- La capa de obtención es un POST HTTP por página. El actor
unlocker.webunlockerdevuelve HTML renderizado desdePOST /api/v1/unlocker/request: sin navegador que gestionar y sin grupo de proxies que rotar de tu lado. - Todo hasta la carga se ejecuta solo con una clave de Scrapeless. La rastreo de demostración cubre las 10 páginas de un sitio público de citas y produce 100 ejemplos supervisados; solo el trabajo final de ajuste fino necesita una clave de OpenAI y presupuesto.
- Los errores de formato son los más baratos de prevenir. Cada línea de entrenamiento es un objeto
{"messages": [...]}con turnos de sistema, usuario y asistente: escríbelo conjson.dumpsy el archivo se analiza en la primera carga. - La procedencia es parte del conjunto de datos. Solo páginas públicas, volumen limitado y términos del sitio y directivas de robots verificadas antes del rastreo: la sección de responsabilidad cubre lo que los datos de entrenamiento añaden a las preguntas habituales sobre raspado.
- Gratis para empezar. Crea tu clave API en el plan gratuito en app.scrapeless.com.
Introducción: el conjunto de datos es la parte difícil
Ajustar un LLM es administrativamente fácil: subir un archivo, crear un trabajo, esperar. Lo que el trabajo no puede arreglar es el archivo. Un modelo ajustado en cien ejemplos bien formados y correctamente etiquetados de la tarea puede superar a uno ajustado en diez mil líneas ruidosas, y esa diferencia se decide antes de que comience la ejecución del entrenamiento, en el canal que recopiló y dio forma a los datos.
Ese canal es un problema de raspado para la mayoría de los equipos, porque el conocimiento del dominio que vale la pena afinar se encuentra en las páginas web: descripciones de productos, documentación, listados, reseñas, material de referencia. La parte conceptual del viaje se cubre en la guía de cómo funciona el entrenamiento de modelos de IA de principio a fin; esta publicación es la mitad ejecutable. Construirás un canal completo contra un sitio demo público: quotes.toscrape.com, un sitio creado para la práctica de raspado, y terminarás con archivos train.jsonl y val.jsonl que el punto final de ajuste fino de OpenAI acepta tal cual.
La tarea de demostración: enseñar a un modelo a atribuir citas famosas. Lo suficientemente pequeño para ejecutarse en minutos, estructurado exactamente como la cosa real.
Canal en Resumen
El canal tiene cinco etapas, y las primeras cuatro se ejecutan de verdad en esta guía con solo una clave API de Scrapeless:
- Obtención — recuperar cada página renderizada a través de la API Universal Scraping, un POST por página.
- Descubrimiento — seguir la propia paginación del sitio hasta que termine, con un límite de páginas como medida de seguridad.
- Extracción — analizar el texto de la cita y el autor de cada página con el analizador HTML de la biblioteca estándar de Python.
- Transformación — convertir cada par en un ejemplo de entrenamiento en formato de chat, dividido 80/20, y escribir JSONL.
- Entrenamiento — cargar ambos archivos y crear el trabajo de ajuste fino (esta etapa necesita una clave de OpenAI; el código se muestra y etiqueta en consecuencia).
Flujo por página: renderizar u obtener → descubrir siguiente página → extraer pares → transformar a ejemplos → almacenar como JSONL.
Por Qué la API Universal Scraping de Scrapeless
La API Universal Scraping convierte la recopilación de páginas en una llamada de función determinística: envías una URL, el servicio se encarga de renderizar, desbloquear y enrutar proxies del lado del servidor, y obtienes el HTML de la página de vuelta en el campo data. Para un constructor de conjuntos de datos, eso es doblemente importante. Primero, el corpus se mantiene reproducible: la misma forma de solicitud funciona independientemente de si la fuente es un sitio demo estático o un catálogo de productos pesado en JavaScript, por lo que el código del canal no cambia cuando lo hace el objetivo. En segundo lugar, no hay flota de navegadores local: un conjunto de datos de unas pocas centenas de páginas es un bucle sobre una función.
El sitio de demostración aquí es deliberadamente amigable. El objetivo de la guía es la forma del canal; intercambia la lista de URL y el analizador cuando lo apuntas a tu corpus real, y mantén los límites de volumen.
Requisitos Previos
- Python 3 con el paquete
requests: cada otra importación en el canal es de la biblioteca estándar; las ejecuciones en esta guía usaron Python 3.12. - Una clave API de Scrapeless: la documentación para desarrolladores cubre la creación de claves.
- Solo para la Etapa 5: una clave API de OpenAI con acceso y presupuesto de ajuste fino. Las Etapas 1–4 se ejecutan sin ello.
Exporta la clave de Scrapeless para que los scripts la lean desde el entorno:
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
Etapa 1 — Obtener páginas renderizadas
Una petición POST devuelve una página renderizada. El actor unlocker.webunlocker toma la URL objetivo en input y responde con el HTML en el campo data de la respuesta:
python
# fetch_page.py — Etapa 1: recuperar una página renderizada a través de Scrapeless
import os
import requests
ENDPOINT = "https://api.scrapeless.com/api/v1/unlocker/request"
HEADERS = {
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
}
def fetch_page(url: str) -> str:
resp = requests.post(
ENDPOINT,
headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET"}},
timeout=120,
)
resp.raise_for_status()
return resp.json().get("data", "")
html = fetch_page("https://quotes.toscrape.com/page/1/")
print(f"se obtuvieron {len(html):,} caracteres de HTML")
print("markup de cita presente:", '<span class="text"' in html)
print("markup de autor presente:", '<small class="author"' in html)
Contra la página 1 del sitio de citas, esto devuelve 11,021 caracteres de HTML con ambos marcadores presentes. raise_for_status() mantiene las fallas sonoras: una clave incorrecta o un objetivo inalcanzable detiene el pipeline en lugar de escribir un corpus vacío.
Etapa 2 — Descubrir el corpus completo
El sitio te dice dónde termina, por lo que el rastreador sigue el sitio en lugar de adivinar URLs. Cada página del sitio de demostración tiene un elemento li class="next" hasta la última; el bucle de rastreo obtiene, verifica ese marcador y avanza el contador de páginas. Dos límites mantienen la etapa honesta: el bucle se detiene cuando el marcador desaparece, y un límite MAX_PAGES lo detiene incluso si el marcador nunca lo hace. El límite es la diferencia entre la construcción de un conjunto de datos y un rastreo sin límites: configúralo al tamaño del corpus que realmente pretendes recolectar.
El bucle en sí son cuatro líneas dentro del script del pipeline completo en la Etapa 4.
Etapa 3 — Extraer pares de cita-autores
La extracción convierte HTML en pares etiquetados, y la biblioteca estándar es suficiente para ello. html.parser.HTMLParser dispara callbacks por cada etiqueta; el seguimiento de dos banderas mientras se recorre la página recopila el texto de cada cita y autor sin ninguna dependencia de terceros:
python
# quote_parser.py — Etapa 3: extracción de (cita, autor) por stdlib
from html.parser import HTMLParser
class QuoteParser(HTMLParser):
"""Recopilar pares (texto, autor) del markup de quotes.toscrape.com."""
def __init__(self):
super().__init__()
self.pairs, self._text, self._mode = [], "", None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "span" and a.get("class") == "text":
self._mode = "text"
elif tag == "small" and a.get("class") == "author":
self._mode = "author"
def handle_data(self, data):
if self._mode == "text":
self._text = data.strip("“”")
elif self._mode == "author":
self.pairs.append((self._text, data.strip()))
self._mode = None
def parse_quotes(html: str):
p = QuoteParser()
p.feed(html)
return p.pairs
Una biblioteca de selectores también funcionaría: la razón para mostrar la versión de la biblioteca estándar es que todo el pipeline se mantiene como un script de dos dependencias (requests más Python mismo), lo que es una cosa menos para fijar cuando el pipeline se mueve a un programador.
Etapa 4 — Transformar a JSONL con formato de chat
El endpoint de ajuste fino de OpenAI entrena en transcripciones de chat: cada línea del archivo es un objeto {"messages": [...]} con la regla del sistema, la entrada del usuario y la respuesta del asistente que deseas que el modelo aprenda. El formato es JSON Lines — la definición del formato JSON Lines es exactamente "un valor JSON por línea" — y la investigación sobre el ajuste de instrucciones, como el documento de InstructGPT, es la razón por la que la forma parece una conversación: los modelos siguen mejor las tareas cuando se entrenan con pares de demostración.
Este script es todo el pipeline — Etapas 1 a 4 compuestas, terminando en dos archivos:
python
# build_dataset.py — Etapas 1–4: rastrear, extraer, transformar, almacenar
import json
import os
import requests
from html.parser import HTMLParser
ENDPOINT = "https://api.scrapeless.com/api/v1/unlocker/request"
HEADERS = {
"Content-Type": "application/json",
"x-api-token": os.environ["SCRAPELESS_API_KEY"],
}
BASE = "https://quotes.toscrape.com"
MAX_PAGES = 15 # límite de seguridad por encima del tamaño real del sitio
SYSTEM = "Tú atribuyes citas famosas. Responde solo con el nombre del autor."
class QuoteParser(HTMLParser):
def __init__(self):
super().__init__()
self.pairs, self._text, self._mode = [], "", None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "span" and a.get("class") == "text":
self._mode = "texto"
elif tag == "small" and a.get("class") == "autor":
self._mode = "autor"
def handle_data(self, data):
if self._mode == "texto":
self._text = data.strip("“”")
elif self._mode == "autor":
self.pairs.append((self._text, data.strip()))
self._mode = None
def fetch_page(url: str) -> str:
resp = requests.post(
ENDPOINT,
headers=HEADERS,
json={"actor": "unlocker.webunlocker", "input": {"url": url, "method": "GET"}},
timeout=120,
)
resp.raise_for_status()
return resp.json().get("data", "")
def to_example(text: str, author: str) -> dict:
return {
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"¿Quién dijo esto: “{text}”"},
{"role": "assistant", "content": author},
]
}
pairs, page = [], 1
while page <= MAX_PAGES:
html = fetch_page(f"{BASE}/page/{page}/")
parser = QuoteParser()
parser.feed(html)
pairs.extend(parser.pairs)
if 'class="next"' not in html: # Etapa 2: el sitio dice cuándo termina
break
page += 1
examples = [to_example(t, a) for t, a in pairs]
split = int(len(examples) * 0.8)
for name, rows in (("train.jsonl", examples[:split]), ("val.jsonl", examples[split:])):
with open(name, "w", encoding="utf-8") as f:
f.writelines(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)
print(f"páginas rastreadas: {page} | pares extraídos: {len(pairs)}")
print(f"train.jsonl: {split} ejemplos | val.jsonl: {len(examples) - split} ejemplos")
print("primera línea de entrenamiento:")
print(json.dumps(examples[0], ensure_ascii=False)[:180])
La ejecución rastrea todas las 10 páginas del sitio, extrae 100 pares y escribe 80 ejemplos de entrenamiento contra 20 ejemplos de validación. La división 80/20 le da al trabajo de ajuste fino algo para medir la generalización: ejemplos de validación que el modelo nunca entrena. Ambos archivos se escriben con `ensure_ascii=False` y un `json.dumps` por línea, que es el seguro más barato posible: el JSONL malformado es una forma común de perder un viaje de ida y vuelta al punto de subida.
Un pase de calidad vale la pena hacerlo a mano incluso en un corpus de juguete: lea una muestra de líneas y confirme que la etiqueta realmente responde a la entrada. Un modelo aprende lo que el archivo demuestra, incluidos los errores.
## Etapa 5 — Enviar el trabajo de ajuste fino
La llamada de entrenamiento es pequeña en comparación con todo lo anterior. Sube ambos archivos con el propósito `fine-tune`, luego crea el trabajo contra un modelo ajustable: la lista y los parámetros actuales están en <a href="https://developers.openai.com/api/docs/guides/supervised-fine-tuning" rel="nofollow"><strong>la guía de ajuste fino supervisado de OpenAI</strong></a>.
> Nota: Esta etapa es la única brecha de requisitos previos de la tubería: necesita un `OPENAI_API_KEY` con acceso y presupuesto para ajuste fino, que esta guía no asume. Todo lo anterior se ejecutó realmente con solo una clave de Scrapeless.
```python
# submit_job.py — Etapa 5: subir el conjunto de datos y crear el trabajo (requiere OPENAI_API_KEY)
from openai import OpenAI
client = OpenAI() # lee OPENAI_API_KEY del entorno
train = client.files.create(file=open("train.jsonl", "rb"), purpose="fine-tune")
val = client.files.create(file=open("val.jsonl", "rb"), purpose="fine-tune")
job = client.fine_tuning.jobs.create(
training_file=train.id,
validation_file=val.id,
model="gpt-4.1-mini-2025-04-14",
)
print(job.id, job.status)
Cuando el trabajo termine, el ID del modelo resultante se coloca en la misma llamada de completaciones de chat que ya utiliza: el conjunto de datos decide si ese modelo realmente responde "Albert Einstein" cuando se le muestra una cita que nunca ha visto.
Obtén tu clave de API en el plan gratuito: app.scrapeless.com
Raspado responsable para datos de entrenamiento
Los datos de entrenamiento llevan cada obligación que llevaron las páginas de origen, además de una más: el modelo reproducirá patrones de lo que le alimentes. Cuatro prácticas mantienen el lado de la colección defensible.
- Solo páginas públicas y leer los términos. Recoge solo lo que se muestra sin una cuenta y revisa los términos de servicio del sitio objetivo antes del rastreo: el uso de entrenamiento es mencionado explícitamente por un número creciente de sitios.
- Honrar las directivas de robots. el Protocolo de Exclusión de Robots (RFC 9309) es la declaración estándar legible por máquinas de lo que un sitio permite que los rastreadores toquen; revísalo para tus rutas objetivo antes de recopilar.
- Minimizar. Toma solo los campos que necesita la tarea: aquí, texto de cita y autor, no descargas de página completa que arrastran comentarios de usuarios, nombres u otros datos incidentales. Los límites de página son parte de la minimización.
- Rastrear la procedencia y licencias. Registra de dónde proviene cada ejemplo y cuándo. El texto que es público para leer no está automáticamente licenciado para el entrenamiento de modelos en todas las jurisdicciones; cuando el corpus es algo más sensible que citas famosas, haz que un abogado revise la cuestión de la licencia antes de que se ejecute el trabajo.
Lo que obtienes
Dos archivos, listos para subir. Cada línea es un ejemplo supervisado completo — la primera línea de train.jsonl de la ejecución anterior:
text
{"messages": [{"role": "system", "content": "Atribuyes citas famosas. Responde solo con el nombre del autor."}, {"role": "user", "content": "¿Quién dijo esto: “El mundo tal como lo hemos creado es un proceso de nuestro pensamiento. No puede ser cambiado sin cambiar nuestro pensamiento.”"}, {"role": "assistant", "content": "Albert Einstein"}]}
La forma se escala sin cambios: un corpus real intercambia el analizador y la lista de URL, la regla del sistema describe tu tarea en lugar de la atribución de citas, y el escritor JSONL, el desglose y la carga permanecen idénticos. Si tu objetivo es la recuperación en lugar de actualizaciones de peso, la misma capa de obtención alimenta el pipeline RAG de texto limpio en su lugar — partes y embeddings en lugar de ejemplos de entrenamiento.
Conclusión
El pipeline se sostiene en los dos extremos. En el frente, la API de Scraping Universal hace que la recolección sea un POST determinista por página, por lo que el corpus es reproducible y el código sobrevive a un cambio de objetivo. En la parte posterior, la transformación disciplinada — formato de chat exacto, un objeto JSON por línea, una división de validación real, una revisión humana de las etiquetas — es lo que separa un ajuste fino que mejora las respuestas de uno que quema presupuesto. Entre esos extremos, el medio son cien líneas de Python de biblioteca estándar que ahora tienes.
¿Listo para construir tu pipeline de datos de entrenamiento?
Los planes y los volúmenes de solicitudes incluidos están en la página de precios, y la capa de obtención en esta guía funciona en el plan gratuito — crea tu clave API en app.scrapeless.com y la Etapa 1 devuelve tu primera página renderizada en un POST.
FAQ
P: ¿Cuántos datos necesito para ajustar un modelo?
Menos de lo que la mayoría de los equipos espera, si los ejemplos son limpios. El ajuste fino supervisado muestra un cambio de comportamiento medible con corpora en decenas a cientos de ejemplos bien etiquetados para tareas específicas; los cambios de comportamiento amplios requieren más. Comienza con el corpus más pequeño que represente la tarea, evalúa contra el archivo de validación y amplía el conjunto de datos donde el modelo realmente falla.
P: ¿Qué formato espera el endpoint de ajuste fino?
Líneas JSON en formato de chat: cada línea es un objeto JSON independiente con un arreglo messages de turnos de sistema, usuario y asistente, subido con el propósito de fine-tune. El pipeline en esta guía escribe ese formato directamente con json.dumps, un objeto por línea, sin comas finales ni arreglos envolventes.
P: ¿Debería ajustar fino o usar RAG?
Ajusta fino cuando quieras que el modelo cambie de comportamiento — tono, formato, reflejos específicos de la tarea — y recuperación cuando quieras que conozca hechos actuales. Las actualizaciones de peso incorporan patrones pero se vuelven obsoletas; la recuperación se mantiene actual pero no enseña al modelo nuevos hábitos. Los dos se componen, y ambos comienzan desde la misma capa de colección que este guía construye.
P: ¿Es legal entrenar un modelo con datos scrapeados?
Depende de lo que recojas y dónde operes, y leer páginas públicas no es automáticamente una licencia para entrenar con ellas. Los términos del sitio, las directrices de robots, los derechos de autor, y las leyes de privacidad que cubren cualquier dato personal en el corpus son todos aplicables — la sección de responsabilidad anterior enumera las prácticas de trabajo, y para cualquier cosa más allá de contenido claramente público y no personal, la cuestión de licencia pertenece al abogado.
P: ¿Funciona este pipeline también para modelos de pesos abiertos?
Sí — las etapas de recolección y transformación son agnósticas al modelo. El JSONL en formato de chat es el denominador común entre las pilas de ajuste; los flujos de trabajo de peso abierto consumen la misma estructura de conversación, por lo que la única etapa que cambia es la Etapa 5, donde la llamada de carga se reemplaza por el cargador de conjunto de datos de tu marco de entrenamiento.
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.



