Volver al blog

Activepieces + Scrapeless: Un Flujo de Leads Local Sin Código

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

20-Aug-2026

TL;DR:

  • Activepieces llega a datos de búsqueda en vivo con su pieza HTTP integrada, por lo que un flujo de leads no necesita una pieza personalizada, un paquete publicado y ningún servicio para alojar.
  • El actor de Google Search devuelve alrededor de 20 negocios locales por solicitud — se capturaron 20, 21 y 22 — por lo que un flujo local generalmente necesita una sola llamada más una clave de deduplicación en lugar de un bucle de paginación.
  • La respuesta analizada se encuentra bajo body, lo que hace que {{step_1.body.local_results.places}} sea la referencia de trabajo; si se elimina body, el bucle se ejecuta cero veces dentro de un flujo que aún informa éxito.
  • El campo phone contiene un número de teléfono en solo alrededor de la mitad de los registros — el resto lleva horarios de apertura o una etiqueta de servicio — por lo que una pieza de Código debe validarlo, no solo recortarlo.
  • Almacena la clave API como un valor a nivel de proyecto en lugar de escribirlo en el campo de encabezado, porque los flujos se exportan y comparten.

Lo que Este Flujo Te Ofrece

Un flujo de Activepieces que convierte una categoría y una ciudad en filas de negocios locales — nombre, categoría, calificación, número de reseñas y teléfono — listos para un CRM, una hoja o una base de datos.

Activepieces orquesta aplicaciones bien y no recupera páginas. Los resultados de búsqueda provienen de Deep SerpApi a través de su actor scraper.google.search, que devuelve el paquete local analizado como JSON. El flujo a continuación se construyó en una instancia auto-alojada de Activepieces 0.82.0 con piece-http 0.11.18.

Requisitos Previos

  • Una instancia de Activepieces, en la nube o auto-alojada
  • Una clave API de Scrapeless — crea una cuenta gratuita
  • Una pieza de destino para las filas: Google Sheets, Airtable, Postgres o tu CRM

Coloca la clave en un valor a nivel de proyecto o una conexión, no en el campo de encabezado del paso. Una definición de flujo lleva sus valores de campo literales, y los flujos se exportan, duplican y comparten entre proyectos.

Configura el Paso HTTP

Agrega HTTP → Enviar solicitud HTTP y completa cinco campos:

Campo Valor
Método POST
URL https://api.scrapeless.com/api/v1/scraper/request
Encabezados x-api-token → tu clave
Tipo de cuerpo JSON
Cuerpo el objeto a continuación
json Copy
{
  "actor": "scraper.google.search",
  "input": {
    "q": "plumbers in Austin, TX",
    "tbm": "lcl"
  }
}

tbm configurado en lcl es lo que devuelve negocios en lugar de páginas web. La consulta necesita intención local: "plumbers in Austin, TX" devuelve un paquete local, mientras que un "plumbing" desnudo a menudo no lo hace.

Antes de cablear el resto del flujo, confirma la solicitud fuera del constructor. La misma llamada desde un shell te dice si un resultado vacío es culpa de la consulta o del flujo:

bash Copy
curl -sS -X POST https://api.scrapeless.com/api/v1/scraper/request \
  -H 'Content-Type: application/json' \
  -H "x-api-token: $SCRAPELESS_API_KEY" \
  -d '{"actor":"scraper.google.search","input":{"q":"plumbers in Austin, TX","tbm":"lcl"}}' \
  | python3 -c 'import json, sys
data = json.load(sys.stdin)
places = (data.get("local_results") or {}).get("places") or []
print(len(places), "places")
if places:
    print("raw phone:", repr(places[0]["phone"]), "| raw type:", repr(places[0]["type"]))
else:
    print("no local pack in this response; top-level keys were", sorted(data))'

Eso imprime la cantidad de lugares y el phone y type en bruto del primer registro — los dos campos que llegan con relleno. Leer las claves en la rama else en lugar de indexar directamente en local_results es el mismo hábito que necesita el flujo: verifica que la forma que esperas esté presente antes de dirigirte a ella.

Lo que Vuelve

Una ejecución exitosa devuelve alrededor de 20 lugares bajo local_results.places, cada uno llevando title, type, rating, reviews, phone y address. Se capturaron 20, 21 y 22 lugares para la misma consulta, así que trata el tamaño de la página como aproximado en lugar de fijo: agrega "start": 20 al objeto input para la siguiente página, y deduplica por nombre más teléfono en lugar de asumir límites de página exactos.

Con tbm configurado en lcl el sobre es local_results, metadata, pagination, y search_information — sin organic_results y sin related_searches. Una llamada rechazada devuelve un sobre que lleva code y message en lugar de resultados, por lo que el flujo debe bifurcarse en la presencia de local_results en lugar de solo en el estado HTTP.

La ruta de referencia es la parte que vale la pena hacer bien. En Activepieces, el JSON analizado vive bajo body:

Referencia Resultado
{{step_1.body.local_results.places}} el arreglo de lugares
{{step_1.body.organic_results}} resultados web, cuando tbm se omite
{{step_1.organic_results}} nada — sin error, sin advertencia

Esa última fila es la costosa. Una referencia que falta el segmento body se resuelve a nada, el paso Bucle en Elementos itera cero veces, y la ejecución aún termina como exitosa. Una tabla de destino vacía se ve idéntica ya sea que la consulta no devolvió nada o que la referencia estaba mal, así que verifica primero el camino.

Agrega Bucle en Elementos sobre {{step_1.body.local_results.places}} para que cada negocio se maneje como su propio elemento en lugar de un solo bulto escrito en una sola celda.
Construir esto en el plan gratuito es suficiente para alcanzar una respuesta completa de paquete local — comienza con una cuenta Scrapeless y mantiene la clave en un valor de proyecto.

Normaliza Antes de Almacenar

Dos comportamientos deciden si tus filas son utilizables, y el segundo es la razón por la que un flujo de leads necesita código.

Las cadenas llegan con padding. phone, type y hours llevan un espacio al inicio — " Plumber", " (512) 690-4935". Eso no es cosmético: un número de teléfono con padding usado como clave de deduplicación crea un segundo registro para el mismo negocio en la siguiente ejecución, y un filtro de categoría en "Plumber" no coincide con nada. La recomendación del plan de numeración de la UIT-T es la razón para normalizar un número de teléfono a una forma canónica antes de que se convierta en un identificador.

Varios campos están presentes pero no llevan nada utilizable. En la misma captura, place_id, thumbnail y lsig estaban vacíos en todos los 20 registros. gps_coordinates es la trampa: está presente como {"latitude": 0, "longitude": 0}, así que una verificación de veracidad pasa y un paso de mapeo coloca cada negocio en el mismo punto en el ecuador. Toma la ubicación de address y trata el par de coordenadas como ausente a menos que ambos valores sean distintos de cero.

El campo phone no siempre es un número de teléfono. En una captura de 20 lugares para "plumbers in Austin, TX", solo 11 registros llevaban un valor con forma de teléfono. Los otros nueve contenían texto de horas de apertura como " Closes 6 PM " o una etiqueta de servicio como "Online estimates". Mapea ese campo directamente en una columna de CRM y casi la mitad de las filas llegan inutilizables, sin error en ninguna parte del flujo. Algunas de esas cadenas de horas también contienen un espacio no quebrable estrecho (U+202F) en lugar de un espacio normal, así que una separación ingenua en " " se comporta inesperadamente incluso después de recortar.

Valida el campo en lugar de confiar en su nombre, y guarda el texto descartado en lugar de eliminarlo:

Agrega una Código pieza entre la solicitud y el destino. Activepieces envuelve el cuerpo como export const code = async (inputs) => { … }; la lógica dentro es JavaScript simple:

javascript Copy
// `phone` sometimes carries opening hours or a service label instead of a number,
// so the value is validated before it becomes a contact field.
const PHONE = /\(?\d{3}\)?[ -]?\d{3}-?\d{4}/;

const code = async (inputs) => {
  const clean = (value) => (typeof value === 'string' ? value.trim() : value);
  const places = inputs.response?.local_results?.places ?? [];
  return places.map((place) => {
    const contact = clean(place.phone) ?? '';
    const isPhone = PHONE.test(contact);
    return {
      name: clean(place.title),
      category: clean(place.type),
      rating: place.rating ?? null,
      reviews: place.reviews ?? 0,
      phone: isPhone ? contact : null,
      phone_field_note: isPhone ? null : contact,
      address_snippet: clean(place.address),
    };
  });
};

const sample = {
  response: {
    local_results: {
      places: [
        {
          title: 'Radiant Plumbing, Air Conditioning, & Electrical',
          type: ' Plumber',
          rating: 4.8,
          reviews: 18000,
          phone: ' (512) 690-4935',
          address: '25+ years in business \u00b7 Austin, TX',
        },
        {
          title: 'Beyond Wow Plumbing & Drains',
          type: ' Plumber',
          rating: 4.9,
          phone: ' Closes 6\u202fPM ',
          address: 'Austin, TX',
        },
      ],
    },
  },
};

code(sample).then((rows) => console.log(JSON.stringify(rows, null, 2)));

Pasa {{step_1.body}} a la entrada response de la pieza. Dos detalles son importantes aquí. El default ?? 0 existe porque un negocio sin reseñas no tiene clave reviews en absoluto, y una columna de destino numérica rechaza undefined mientras acepta 0. Y phone_field_note mantiene lo que ocupaba el campo cuando no era un número, así que un operador puede ver que una fila tiene horas de apertura en lugar de un teléfono faltante.

La calificación y el conteo de reseñas son los dos campos que vale la pena mantener numéricos. Todo lo demás es texto, y si el flujo termina en una exportación de hoja de cálculo en lugar de una base de datos, la especificación del formato de valores separados por comas es lo que decide cómo un nombre de negocio que contiene una coma sobrevive el viaje de ida y vuelta.

Manejo Responsable de Datos de Contacto Empresarial

Este flujo recoge detalles de contacto empresarial, así que vienen algunas obligaciones con él. Recoge solo de resultados de búsqueda públicos y solo los campos que necesita el flujo de trabajo. Mantén una base legal para almacenar datos de contacto y respeta las solicitudes de exclusión, ya que un número de teléfono de negocio aún puede identificar a un comerciante individual como una persona — el Reglamento General de Protección de Datos se aplica a los datos personales incluso en un contexto comercial, y existen reglas equivalentes en otras jurisdicciones. Respeta los términos de cada plataforma de destino para los contactos importados, establece un período de retención en lugar de mantener filas indefinidamente, y sigue las reglas de consentimiento comercial del país que estás contactando. Nada de esto es asesoría legal; verifica tus propias obligaciones antes de realizar el alcance.

Conclusión

Tres piezas conforman todo el flujo: HTTP para llamar al actor, Código para recortar y establecer por defecto los campos, Bucle sobre Elementos para escribir una fila por negocio. El modo de fallo a observar se encuentra en la ruta de referencia: elimina body y una ejecución exitosa escribe una tabla vacía.

Desde aquí, intercambia la consulta por una lista de ciudades y el mismo flujo se convierte en una construcción territorial. La guía de integración de Make cubre la misma solicitud desde un constructor sin código diferente, y la construcción de monitoreo de Dify muestra la versión impulsada por agentes del mismo actor.
¿Listo para construirlo? Revisa la documentación de Deep SerpApi para el conjunto completo de parámetros, compara planes y volumen incluido, y comienza con el plan gratuito.

FAQ

Q: ¿Necesito una pieza personalizada de Activepieces para usar Scrapeless?

No. La pieza HTTP incorporada cubre a cada actor, porque la API toma un solo POST con un campo actor y un objeto input. Una pieza personalizada solo ayuda si deseas un paso de marca con campos tipados para un equipo que no debería ver la solicitud en bruto, y eso es una decisión de empaquetado más que de capacidad.

Q: ¿Por qué mi paso de Bucle sobre Elementos itera cero veces cuando el paso HTTP tuvo éxito?

La respuesta analizada está anidada bajo body, así que {{step_1.local_results.places}} se resuelve a nada mientras {{step_1.body.local_results.places}} se resuelve al array. Una referencia faltante no genera error, así que el flujo informa éxito con un bucle vacío. Revisa la ruta de referencia antes de investigar la consulta.

Q: ¿Cuántos resultados devuelve una solicitud y cómo obtengo más?

Una solicitud de paquete local devuelve alrededor de 20 lugares; repeticiones de una consulta devolverán 20, 21 y 22. Agrega "start": 20 al objeto input para la siguiente página, "start": 40 para la que sigue, y así sucesivamente. Dado que el tamaño de página no es exactamente fijo, deduplica por nombre más teléfono en lugar de confiar en los desplazamientos para alinearse, y considera una página final corta como el final del conjunto.

Q: ¿Por qué place_id y gps_coordinates no son utilizables en resultados locales?

place_id, thumbnail, y lsig vuelven vacíos en cada registro de paquete local, así que un flujo que requiere un place_id descarta todos los 20 resultados. gps_coordinates se comporta de manera diferente y más peligrosa: se llena con {"latitude": 0, "longitude": 0}, que sobrevive a una verificación de vacuidad mientras apunta cada negocio a la misma coordenada. Usa address para ubicación y confía en el par de coordenadas solo cuando ambos números son diferentes de cero.

Q: ¿Dónde debe vivir la clave API en un flujo de Activepieces?

En un valor a nivel de proyecto o una conexión, referenciada desde el campo de encabezado. Las definiciones de flujo llevan valores de campo literales y se exportan y duplican entre proyectos, así que una clave tipeada directamente en el paso viaja con cada copia.

Q: ¿Puede este flujo ejecutarse en un horario en lugar de un webhook?

Sí. Cambia el disparador por Horario y el resto del flujo permanece sin cambios, que es la forma habitual para una actualización territorial. Mantén la frecuencia de ejecución correspondiente a la frecuencia con la que realmente se mueven las clasificaciones locales; diariamente es suficiente para la mayoría de las categorías, y un ritmo más lento mantiene el volumen predecible.

Q: ¿Funciona el mismo flujo para resultados web en lugar de negocios?

Sí. Elimina tbm del objeto input y los resultados llegan bajo {{step_1.body.organic_results}} en su lugar, cada uno con title, link, y snippet. La pieza de Código necesita que se actualice su ruta para coincidir, y el recorte no es necesario en resultados web.

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.

Artículos más populares

Catalogar