Volver al blog

Cómo realizar consultas masivas en ChatGPT para GEO: una solución automatizada utilizando Scrapeless Cloud Browser.

Michael Lee
Michael Lee

Expert Network Defense Engineer

14-Nov-2025
¡Consigue la Ventaja en Cada Región!

Automatiza las consultas de ChatGPT con Scrapeless y desbloquea insights GEO precisos. No adivines—sabe.

Introducción

Desde el comienzo de este año, las estrategias de SEO de muchas empresas han sufrido cambios fundamentales. Cada vez más usuarios ya no abren la búsqueda de Google cuando necesitan información. En su lugar, preguntan sobre productos, servicios y marcas directamente en ChatGPT, Claude o Gemini.

Esto significa: Tu visibilidad en ChatGPT se está convirtiendo en el nuevo estándar para la competitividad de marca.

El problema es que — ChatGPT no tiene una función de "clasificación" ni una "herramienta de análisis de palabras clave."

Simplemente no puedes saber:

  • ¿Está mi marca apareciendo en las respuestas de ChatGPT?
  • ¿ChatGPT recomienda a mis competidores?
  • ¿Son diferentes las respuestas según los países o idiomas?

Para abordar estas preguntas, el primer paso es:

👉 Realizar consultas masivas de respuestas de ChatGPT para reunir datos y extraer insights accionables


¿Qué es GEO?

Optimización de Motor Generativo (GEO) es la práctica de crear y optimizar contenido para que aparezca en respuestas generadas por IA en plataformas como Google AI Overviews, AI Mode, ChatGPT y Perplexity.

En el pasado, el éxito significaba clasificar alto en las páginas de resultados de motores de búsqueda (SERPs). Mirando hacia adelante, puede que ya no exista siquiera el concepto de "clasificación superior." En cambio, necesitas ser la recomendación preferida—la solución que las herramientas de IA eligen presentar activamente en sus respuestas.

Los objetivos centrales de la optimización GEO ya no se limitan a clics, sino que se centran en tres métricas clave:

  • Visibilidad de la marca: Aumentar la probabilidad de que tu marca aparezca en respuestas generadas por IA.
  • Autoridad de la fuente: Asegurarte de que tu dominio, contenido o datos sean seleccionados como una referencia confiable por la IA.
  • Consistencia narrativa y posicionamiento positivo: Asegurarte de que la IA describa tu marca de manera profesional, precisa y positiva.

Esto significa que la lógica tradicional de SEO basada en "clasificación de palabras clave" está dando paso gradualmente a mecanismos de citación de fuentes de IA.

Las marcas deben evolucionar de ser "buscables" a ser confiables, citadas y recomendadas activamente.


¿Por qué automatizar consultas masivas de ChatGPT?

Desde una perspectiva de marketing y SEO, ChatGPT se ha convertido en un nuevo canal para el descubrimiento y la exposición de contenido.

Sin embargo, hay tres puntos de dolor principales:

  1. Sin visibilidad en la cobertura de la marca
    Las empresas no pueden saber si sus productos están siendo indexados, mencionados o recomendados por ChatGPT. Sin datos, es imposible crear estrategias de optimización o distribución de contenido específicas.

  2. Falta de insights a nivel GEO
    Las respuestas de ChatGPT varían según la región, el idioma e incluso la zona horaria. Un producto recomendado para un usuario de EE. UU. puede no aparecer para un usuario japonés. Para estrategias internacionales, las empresas deben entender estas diferencias.

  3. Las herramientas de SEO tradicionales no pueden proporcionar estos datos
    Las herramientas de SEO existentes (por ejemplo, Ahrefs, Semrush) tienen capacidades limitadas y no pueden rastrear completamente las respuestas de ChatGPT. Esto significa que se requiere un nuevo enfoque para monitorear la exposición de la marca dentro de los canales de búsqueda de IA.

Por lo tanto, el objetivo central de realizar consultas masivas a ChatGPT es recopilar, analizar y optimizar sistemáticamente la presencia de tu marca en las respuestas de ChatGPT. Esto ayuda a las empresas a:

  • Identificar preguntas de alto potencial que ya han sido mencionadas por ChatGPT;
  • Descubrir vacíos de contenido que no han sido cubiertos;
  • Desarrollar estrategias de optimización GEO específicas.

¿Por qué elegir Scrapeless Cloud Browser?

Muchos podrían considerar llamar directamente a la API de OpenAI para realizar consultas masivas.

Sin embargo, en la práctica, el enfoque de API tiene limitaciones obvias:

  • Los resultados se ven fácilmente influenciados por preferencias históricas y contexto, lo que los hace menos objetivos.
  • Es difícil cambiar rápidamente las IPs para simular acceso desde diferentes ubicaciones geográficas.
  • Los costos de consultas masivas son muy altos (cobrados por token, volviéndose costosos a gran escala).

Aquí es donde entra el Scrapeless Cloud Browser.


¿Qué es Scrapeless Browser?

Scrapeless Browser es un navegador en la nube diseñado para tareas de extracción de datos y automatización. Te permite acceder a ChatGPT desde la nube de una manera que imita de cerca el comportamiento real del usuario, entregando resultados más precisos y completos.

Comparado con las llamadas API tradicionales, Scrapeless Cloud Browser se destaca en varios aspectos:

  • Sin interferencia de preferencias de cuenta
    Todas las consultas se ejecutan en entornos de navegador aislados y sin inicio de sesión, garantizando resultados objetivos y confiables.

  • Simulación GEO multi-región
    Proxies residenciales integrados de más de 195 países, ISPs estáticos y IPs ilimitadas permiten una fácil simulación de usuarios desde diferentes ubicaciones.

  • Alta concurrencia y bajo costo
    Soporta más de 1,000 instancias concurrentes por tarea, cobradas por tiempo, con costos mucho más bajos que las API tradicionales.

  • Compatibilidad nativa con marcos principales
    Migra proyectos existentes de Puppeteer o Playwright con una sola línea de código—sin necesidad de adaptación adicional.

  • Detección inteligente y depuración visual
    Manejo incorporado para Cloudflare, reCAPTCHA y otras protecciones, con soporte para depuración en Vivo y grabación de sesión.

Lectura recomendada: Cómo eludir la protección de Cloudflare y Turnstile usando Scrapeless | Guía completa

En resumen, Scrapeless Cloud Browser te permite realizar consultas masivas “desde la perspectiva del usuario a ChatGPT” de manera eficiente, económica y precisa—sin registrar cientos de cuentas de ChatGPT—y extraer automáticamente resultados estructurados.


Ejemplo: Consulta masiva a ChatGPT usando Scrapeless Browser

Scrapeless Browser es un servicio de navegador sin cabeza basado en la nube compatible con los principales marcos de automatización como Puppeteer y Playwright. Usándolo, no necesitas mantener un navegador local, proxy o nodo; puedes iniciarlo con solo una línea de código de conexión.


1. Instalar dependencias

bash Copy
npm install puppeteer-core @scrapeless-ai/sdk node-fetch

2. Configurar Scrapeless Browser y Conectar

ts Copy
import puppeteer, { Browser, Page } from 'puppeteer-core';
import { Scrapeless, PuppeteerLaunchOptions } from '@scrapeless-ai/sdk';

const scrapeless = new Scrapeless();

async function connectBrowser(): Promise<Browser> {
  const options: PuppeteerLaunchOptions = {
    session_name: 'Consulta masiva ChatGPT',
    session_ttl: 600,
    fingerprint: {
      platform: 'macOS',
      localization: { timezone: 'America/New_York' },
      args: { '--window-size': '1920,1080' },
    },
  };

  const { browserWSEndpoint } = await scrapeless.browser.createSession(options);
  const browser = await puppeteer.connect({ browserWSEndpoint, defaultViewport: null });
  return browser;
}

Obtén tu clave API de Scrapeless Browser iniciando sesión en el Tablero de Scrapeless.

Obtén tu clave API de Scrapeless Browser

💡 Ventaja #1 de Scrapeless: Entorno de cero configuración

  • No necesidad de mantener instancias de navegador localmente, consumo mínimo de recursos y fácil escalado.
  • Proyectos existentes de Puppeteer pueden migrar a Scrapeless con cambios mínimos.
  • Simulación fácil de entornos de usuario reales, mejorando la discreción y las tasas de éxito.
  • Ideal para tareas de automatización a gran escala, como consultas masivas a ChatGPT.

3. Automatizar el acceso a ChatGPT y la entrada de solicitudes

ts Copy
async function queryChatGPT(browser: Browser, prompt: string): Promise<string> {
  const page = await browser.newPage();
  await page.goto('https://chatgpt.com/');

  const inputSelector = '[placeholder="Pregúntame cualquier cosa"]';
  await page.waitForSelector(inputSelector, { visible: true });
  await page.type(inputSelector, prompt);
  await page.keyboard.press('Enter');

  await page.waitForSelector('[data-message-author-role="assistant"]');
  const result = await page.evaluate(() => {
    const messages = document.querySelectorAll('[data-message-author-role="assistant"]');
    return messages[messages.length - 1]?.textContent || '';
  });

  await page.close();
  return result;
}

💡 Ventaja #2 de Scrapeless: Entorno web real

  • Automatiza interacciones en páginas web reales (escribir, hacer clic, enviar).
  • Captura contenido renderizado dinámicamente.
  • Los resultados coinciden con lo que un usuario real vería al visitar ChatGPT.

4. Soporte para múltiples tipos de extracción de resultados de búsqueda

4.1 Extraer respuestas de texto de ChatGPT

ts Copy
let gptAnswer: string;
gptAnswer = await waitForChatGPTResponse(page);

4.2 Extraer tarjetas de imagen

ts Copy
let gptImageCards: ChatgptResponse['image_cards'] = [];
// Usa selectores para extraer imágenes y construir { url, posición }

4.3 Extraer productos recomendados

ts Copy
const gptRecommendProducts: ChatgptResponse['products'] = [];
// Usa selectores para extraer enlaces de productos, títulos e imágenes

4.4 Extraer citas/referencias

ts Copy
let gptCitations: ChatgptResponse['citations'] = [];
// Usa botones de notas al pie para extraer enlaces de citas, íconos, títulos y descripciones

4.5 Extraer enlaces adjuntos

ts Copy
let gptLinksAttached: ChatgptResponse['links_attached'] = [];
// Usa selectores de enlaces de Markdown para extraer enlaces y su texto

4.6 Extraer HTML del cuerpo de la página

ts Copy
const body = await page.evaluate(() => document.body.innerHTML);
javascript Copy
const cleanBody = body
  .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
  .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
  .replace(/\s+/g, ' ')
  .trim();

Aquí, se realiza la limpieza de HTML: se eliminan las etiquetas <script>, <style>, <svg>, <img> para obtener un contenido corporal limpio.

💡 Ventaja de Scrapeless #3: Extracción de Resultados Multi-Tipo

  • Una sola solicitud puede obtener múltiples tipos de información estructurada, sin la necesidad de múltiples llamadas o de combinar diferentes herramientas.
  • No solo se puede extraer texto, sino también imágenes, productos, citas, enlaces adjuntos y HTML limpio.
  • Cada tipo de dato se empaqueta como un array de objetos (por ejemplo, ChatgptResponse['products']), lo que facilita la salida directa a JSON, CSV o Webhooks, y apoya flujos de trabajo de automatización posteriores.

5.1 Aislamiento a Nivel de Sesión

ts Copy
const { session_name, task_id, ... } = input;

browser = await this.connectToBrowser({
  session_name,              // Cada tarea puede especificar un nombre de sesión diferente
  session_ttl: 600,          // Tiempo de vida de la sesión
  session_recording,
  proxy_url,
  // ...
});

Utilizando el parámetro session_name, diferentes consultas pueden usar sesiones de navegador separadas, logrando un aislamiento a nivel de sesión.

ts Copy
async solver(input: QueryChatgptRequest, ...): Promise<BaseOutput> {
  let browser: Browser;
  
  try {
    // Crear una nueva conexión de navegador para cada llamada
    browser = await this.connectToBrowser(...);
    const page = await browser.newPage();
    // Ejecutar tareas
  } finally {
    // Cerrar el navegador después de que la tarea esté terminada
    await page.close();
    await browser.close();
  }
}

Cada llamada a solver() hará:

  • Crear una instancia de navegador independiente
  • Limpiar automáticamente después de usar en el bloque finally

5.3 Aislamiento de Proxy

ts Copy
const { proxy_url } = input;

browser = await this.connectToBrowser({
  proxy_url,  // Cada tarea puede usar un proxy diferente
  // ...
});

const proxy_country = /-country_([A-Z]{2,3})/.exec(proxy_url)?.[1] || 'ANY';

Diferentes tareas pueden lograr aislamiento a nivel de red utilizando diferentes valores de proxy_url.

5.4 Aislamiento de Huella Digital

ts Copy
fingerprint: {
  platform: 'macOS',
  localization: {
    timezone: 'America/New_York',
  },
  args: {
    '--window-size': '1920,1080',
  },
}

💡 Ventaja de Scrapeless #4: Estabilidad a Través del Aislamiento

  • Cada consulta de ChatGPT se ejecuta en una sesión de navegador independiente, previniendo interferencias
  • Evita la contaminación de Cookies, LocalStorage o huellas digitales, mejorando las tasas de éxito de las solicitudes
  • Permite ejecutar un gran número de consultas simultáneamente en la misma máquina sin conflictos de instancias de Puppeteer
  • Mejora la estabilidad y fiabilidad en escenarios de alta concurrencia

6. Cambio Global de GEO: Obtén Respuestas de Diferentes Regiones

6.1 Geolocalización Global

ts Copy
const proxy_country = /-country_([A-Z]{2,3})/.exec(proxy_url)?.[1] || 'ANY';

6.2 Localización de Huellas Digitales

ts Copy
fingerprint: {
  platform: 'macOS',
  localization: {
    timezone: 'America/New_York',
  },
  args: {
    '--window-size': '1920,1080',
  },
}

💡 Ventaja de Scrapeless #5: 195+ Nodos de País, Proxy Automático y Simulación Localizada

  • Selección Automática de IP del País
    El código de país se extrae de proxy_url (por ejemplo, -country_US, -country_JP), y Scrapeless enruta automáticamente las solicitudes a IPs residenciales en la región correspondiente.

  • No se Requiere Mantenimiento de Pool de Proxy
    El backend gestiona automáticamente los nodos globales, por lo que los usuarios no necesitan configurar o actualizar listas de proxies ellos mismos.

  • Entorno de Navegador Localizado
    fingerprint.localization.timezone puede establecer la zona horaria. Combinado con sesiones independientes, simula el entorno de la región objetivo, afectando la presentación de contenido y los resultados de búsqueda específicos de la región.

  • Obtén Resultados Localizados Reales
    ChatgptResponse.country_code indica la ubicación geográfica de la solicitud, haciendo que el SEO internacional, la monitorización de marcas o el análisis de contenido sensible a la región sean más precisos.

7. Extraer Resultados y Soportar Múltiples Formatos de Salida

7.1 Salida Estructurada

En el método startChat, los datos capturados se encapsulan en un objeto unificado ChatgptResponse:

ts Copy
resolve({
  prompt,
  success: true,
  answer: answerResponse,           // texto / html / crudo
  country_code: proxy_country,      // información geográfica
  citations: gptCitations,
  links_attached: gptLinksAttached,
  image_cards: gptImageCards,
  products: gptRecommendProducts,
  url: _url,
});

💡 Ventaja de Scrapeless #6: Salida Estructurada

  • Cada tarea de consulta genera un objeto estructurado.
  • Incluye campos como respuesta de texto, enlaces adjuntos, imágenes, productos recomendados, citas, código de país y URL.
  • Los objetos estructurados pueden ser utilizados directamente para la automatización, sin necesidad de análisis adicional.

7.2 Múltiples Métodos de Salida

En el método solver, los resultados pueden ser enviados o devueltos:

  1. Salida de Webhook
ts Copy

this.pushToMessage(payload, webhook);

Copy
2. **Devolución de Función**

```ts
return createResponse(JSON.stringify(payload), payload.url);

💡 Ventaja de Scrapeless #7: Soporte Nativo para la Integración de Pipelines de Datos

  • Los datos estructurados se pueden enviar directamente a sistemas externos o herramientas de automatización (por ejemplo, n8n, Zapier, Airtable).
  • No es necesario desarrollar interfaces adicionales o procesar datos manualmente, lo que permite la integración de automatización en tiempo real.
  • Al conectarse a sistemas o bases de datos internos, no se requiere análisis o conversión adicional, soportando múltiples salidas de pipelines de datos.
  • Cada resultado de tarea de consulta es un objeto estructurado, facilitando el análisis posterior, estadísticas o exportación a CSV/JSON.

Código completo

Copy
import puppeteer, { Browser, Page, Target } from 'puppeteer-core';
import fetch from 'node-fetch';
import { PuppeteerLaunchOptions, Scrapeless } from '@scrapeless-ai/sdk';
import { Logger } from '@nestjs/common';

export interface BaseInput {
  task_id: string;
  proxy_url: string;
  timeout: number;
}

export interface BaseOutput {
  url: string;
  data: number[];
  collection?: string;
  dataType?: string;
}

export interface QueryChatgptRequest extends BaseInput {
  prompt: string;
  webhook?: string;
  session_name?: string;
  web_search?: boolean;
  session_recording?: boolean;
  answer_type?: 'text' | 'html' | 'raw';
}

export interface ChatgptResponse {
  prompt: string;
  task_id?: string;
  duration?: number;
  answer?: string;
  url: string;
  success: boolean;
  country_code: string;
  error_reason?: string;
  links_attached?: Partial<{ position: number; text: string; url: string }>[];
  citations?: Partial<{ url: string; icon: string; title: string; description: string }>[];
  products?: Partial<{ url: string; title: string; image_urls: (string | null)[] }>[];
  image_cards?: Partial<{ position: number; url: string }>[];
}

interface StartChatParams extends QueryChatgptRequest {
  page: Page;
  browser: Browser;
}

export class ChatgptService {
  logger = new Logger(this.constructor.name);
  scrapeless = new Scrapeless();

  private timeoutMultiplier = 2;
  private defaultTimeout = 3 * 60 * 1000;
  private internalErrorSymbol = '[InternalError]:';

  async solver(input: QueryChatgptRequest, checkTimeout: () => boolean): Promise<BaseOutput> {
    const { session_name, task_id, webhook, session_recording, proxy_url } = input;

    let browser: Browser;

    const startTime = performance.now();
    const successful = false;

    const getTotalDuration = () => {
      const endTime = performance.now();
      const totalDuration = ((endTime - startTime) / 1000).toFixed(2);
      return totalDuration;
    };

    const handleChatResponse = (data: Partial<ChatgptResponse>) => {
      const payload = { ...data, task_id, duration: getTotalDuration() };
      return payload;
    };

    const createResponse = (data: string, url = 'https://chatgpt.com'): BaseOutput => {
      return {
        url: url,
        data: Array.from(Buffer.from(data)),
        dataType: 'json',
      };
    };

    try {
      browser = await this.connectToBrowser(
        {
          session_name,
          session_ttl: 600,
          session_recording,
          proxy_url,
          fingerprint: {
            platform: 'macOS',
            localization: {
              timezone: 'America/New_York',
            },
            args: {
              '--window-size': '1920,1080',
            },
          },
        },
        checkTimeout,
      );

      const page = await browser.newPage();

      await this.fakePageDate(page);

      const chatParams: StartChatParams = { ...input, page, browser };
      const results = await this.startChat(chatParams);
      const payload = handleChatResponse(results);
      this.pushToMessage(payload, webhook);
      return createResponse(JSON.stringify(payload), payload.url);
    } catch (error) {
      if (error.success) {
        const payload = handleChatResponse(error);
        this.pushToMessage(payload, webhook);
        return createResponse(JSON.stringify(payload), error.url);
      }
      if (error.error_reason) {
        const errorMessage = error.error_reason;
        const payload = handleChatResponse(error);
        this.pushToMessage(payload, webhook);
        this.logger.warn(`El procesamiento falló: ${errorMessage}`);
        throw { message: !errorMessage.includes(this.internalErrorSymbol) ? errorMessage : '' };
      }
      const errorMessage = error.message || 'Error desconocido';
      const payload = handleChatResponse({
        success: false,
        error_reason: errorMessage,
      });
      this.pushToMessage(payload, webhook);
      this.logger.warn(`El procesamiento falló: ${errorMessage}`);
      throw error;
    } finally {
      const totalDuration = getTotalDuration();
      this.logger.log(
        `Procesamiento ${successful ? 'exitoso' : 'completado'} | Duración total: ${totalDuration} segundos`,
      );
    }
  }
```typescript
async format(data: Uint8Array): Promise<QueryChatgptRequest> {
    if (!data) {
      throw new Error('No hay datos de entrada válidos');
    }
    const input = JSON.parse(data.toString()) as QueryChatgptRequest;

    if (!input.prompt) {
      this.logger.error(`se requiere un prompt`);
      throw new Error('se requiere un prompt');
    }

    return {
      ...input,
      timeout: input.timeout || this.defaultTimeout,
      web_search: input.web_search ?? true,
      session_name: input.session_name || 'Respuesta de Chatgpt',
      session_recording: input.session_recording || false,
      answer_type: input.answer_type || 'text',
    };
  }

  private startChat(params: StartChatParams): Promise<ChatgptResponse> {
    return new Promise(async (resolve, reject: (reason: ChatgptResponse) => void) => {
      const { prompt, answer_type, web_search, timeout, page, browser, proxy_url } = params;
      let action: string;
      let isAborted = false;
      let rawResponse: string;

      this.logger.debug((action = 'Conectando al navegador'));

      const proxy_country = /-country_([A-Z]{2,3})/.exec(proxy_url)?.[1] || 'ANY';
      const query = new URLSearchParams({ q: prompt });
      if (web_search) {
        query.set('hints', 'buscar');
      }
      const baseUrl = 'https://chatgpt.com';
      const _url = `${baseUrl}/?${query.toString()}`;

      function waitForChatGPTResponse(page: Page): Promise<string> {
        return new Promise((resolve, reject) => {
          let retryCount = 0;
          const CHECK_INTERVAL = 500; // Revisar una vez cada 500ms

          const checkResponse = async () => {
            try {
              if (isAborted) {
                resolve('timeout');
                return;
              }
              const currentContent = await page.evaluate(() => {
                const assistantMessage = '[data-message-author-role="assistant"]';
                const $assistantMessages = document.querySelectorAll(assistantMessage);
                const lastMessage = $assistantMessages[$assistantMessages.length - 1];
                if (!lastMessage) return null;

                // Cuando aparece más de un botón de copiar, significa que la respuesta de gpt se ha completado
                const $answerCopyButtons = document.querySelectorAll('button[data-testid="copy-turn-action-button"]');
                if ($answerCopyButtons.length > 1) {
                  return lastMessage.textContent || 'Sin contenido';
                } else {
                  return null;
                }
              });

              // Si el contenido no ha cambiado y no está vacío
              if (currentContent) {
                retryCount++;
                // Si el contenido es estable y no está vacío, se considera que la respuesta está completa
                if (retryCount >= 3) {
                  // Contenido estable durante 1.5 segundos
                  resolve(currentContent);
                  return;
                }
              }

              // Continuar revisando
              setTimeout(checkResponse, CHECK_INTERVAL);
            } catch (error) {
              reject(error);
            }
          };

          // Comenzar a revisar
          checkResponse();
        });
      }

      async function receiveChatGPTStream() {
        const client = await page.createCDPSession();
        await client.send('Fetch.enable', {
          patterns: [
            {
              urlPattern: '*conversation',
              requestStage: 'Response',
            },
          ],
        });

        client.on('Fetch.requestPaused', async (event) => {
          const { requestId, request, responseHeaders } = event;
          const isSSE = responseHeaders?.some(
            (h) => h.name?.toLowerCase() === 'content-type' && h.value?.includes('text/event-stream'),
          );
          if (request.url.includes('/conversation') && isSSE) {
            try {
              const { body, base64Encoded } = await client.send('Fetch.getResponseBody', { requestId });
              rawResponse = base64Encoded ? Buffer.from(body, 'base64').toString('utf-8') : body;
            } catch (err) {
              console.warn('No se pudo obtener la respuesta de flujo', err.message);
            }
          }
          await client.send('Fetch.continueRequest', { requestId });
        });
      }

      function throwError(errorReason: string) {
        const error: ChatgptResponse = {
          prompt,
          success: false,
          country_code: proxy_country,
          error_reason: errorReason,
          url: _url,
        };
        reject(error);
      }

      const timeoutId = setTimeout(() => {
        isAborted = true;
        throwError(`Tiempo de espera de chat agotado después de ${timeout}ms`);
      }, timeout);

      try {
        this.logger.debug((action = 'Registrar CDP para capturar datos de flujo de GPT (raw_response)'));
        await receiveChatGPTStream();
        this.logger.debug((action = 'Navegando a chatgpt.com'));

const navigateTimeout = 25_000 * this.timeoutMultiplier;
try {
await page.goto(_url, { timeout: navigateTimeout });
} catch {
throwError(Navegar a chatgpt.com Tiempo de espera agotado (${navigateTimeout}ms));
return;
}

Copy
    // Agregar oyente de cambio de URL
    page.on('framenavigated', async (frame) => {
      if (frame !== page.mainFrame()) return;
      const url = frame.url();
      if (!url.startsWith('https://auth.openai.com')) return;
      isAborted = true;
      throwError(`Redirigido a la página de inicio de sesión de OpenAI when <<${_url}>> - ${action}`);
      return;
    });

    if (isAborted) return;
    await this.wait(50, 150);
    this.logger.debug((action = 'Asegúrate de que exista la entrada'));
    const inputs = ['#prompt-textarea', '[placeholder="Pregunta cualquier cosa"]'];
    try {
      await Promise.race(
        inputs.map(async (input) => {
          await page.waitForSelector(input, {
            timeout: 20_000 * this.timeoutMultiplier,
            visible: true,
          });
          return input;
        }),
      );
    } catch {
      throwError('La región actual no está disponible o se redirige a la página de inicio de sesión');
      return;
    }

    if (isAborted) return;
    await this.wait(150, 250);
    this.logger.debug((action = 'Esperando respuesta de GPT'));
    let gptAnswer: string;
    try {
      gptAnswer = await waitForChatGPTResponse(page);
      this.logger.debug((action = 'Respuesta de GPT recibida'));
    } catch (error: any) {
      this.logger.error(`Error al obtener respuesta: ${error.message}`);
      throwError(`Falló al obtener respuesta de chatgpt`);
      return;
    }

    if (isAborted) return;
    await this.wait(150, 250);
    this.logger.debug((action = 'Obteniendo tarjetas de imagen de chatgpt'));
    const imageCardsSelector = 'div.no-scrollbar:has(button img) img';
    const imageCardsLightBoxSelector = 'div[data-testid="modal-image-gen-lightbox"] ol li img';
    const imageCardsLightBoxCloseSelector = 'div[data-testid="modal-image-gen-lightbox"] button';
    let gptImageCards: ChatgptResponse['image_cards'] = [];
    try {
      const firstImageCard = await page.$(imageCardsSelector);
      if (firstImageCard) {
        firstImageCard.click();
        await page.waitForSelector(imageCardsLightBoxSelector);
        gptImageCards = await page.$$eval(imageCardsLightBoxSelector, (elements) => {
          return elements.map((element, index) => {
            const url = element.getAttribute('src') || '';
            return { url, position: index + 1 };
          });
        });
        await page.waitForSelector(imageCardsLightBoxCloseSelector);
        await page.click(imageCardsLightBoxCloseSelector);
      } else {
        this.logger.debug((action = 'No se encontraron tarjetas de imagen'));
      }
    } catch (error: any) {
      this.logger.debug((action = `Obtener tarjetas de imagen de chatgpt: ${error.toString()}`));
    }

    if (isAborted) return;
    await this.wait(300, 450);
    this.logger.debug((action = 'Obteniendo productos recomendados de chatgpt'));
    const closeButtonSelector = `button[data-testid="close-button"]`;
    const recommendProductsSelector = 'div.markdown div.relative > div.flex.flex-row:has(img):not(a) > div img';
    const recommendProductDetailsSelector = `section[screen-anchor="top"] div[slot="content"]`;
    const detailLinkSelector = `${recommendProductDetailsSelector} span a`;
    const gptRecommendProducts: ChatgptResponse['products'] = [];
    try {
      const recommendProducts = await page.$$(recommendProductsSelector);
      if (recommendProducts.length) {
        let lastUrl = '';
        for (const [index] of recommendProducts.entries()) {
          // El salto de enlace externo puede ser activado
          let newPage: Page = null as unknown as Page;
          const targetCreatedHandler = async (target: Target) => {
            this.logger.debug((action = `Obteniendo productos recomendados de chatgpt: ${target.type()}`));
            try {
              if (target.type() === 'page') {
                const pageTarget = await target.page();
                const opener = await target.opener();
                if (opener && (opener as any)?._targetId === (page.target() as any)?._targetId) {
                  newPage = pageTarget as Page;
                }
              }
            } catch (e) {}
          };
          browser.once('targetcreated', targetCreatedHandler);

          // Hacer clic en el artículo recomendado
          await page.evaluate(
            (selector, index) => {
              const currentProduct = document.querySelectorAll(selector)?.[index];

(currentProduct as HTMLElement)?.click();
},
recommendProductsSelector,
index,
);

Copy
          await this.wait(750, 950);
          browser.off('targetcreated', targetCreatedHandler);

          if (newPage) {
            const url = newPage.url();
            const title = await newPage.title();
            gptRecommendProducts.push({ url, title, image_urls: [] });
            await newPage.close();
            continue;
          }

          await page.waitForSelector(detailLinkSelector, { timeout: 20_000 * this.timeoutMultiplier });

          // Esperar a que cambien los detalles
          let maxRetry = 30;
          while (maxRetry-- > 0) {
            const currentUrl = await page.$eval(detailLinkSelector, (el) => el.getAttribute('href') || '');
            if (currentUrl && currentUrl !== lastUrl) {
              lastUrl = currentUrl;
              break;
            }
            await this.wait(200, 300);
          }

          const info = await page.$eval(
            recommendProductDetailsSelector,
            (element, currentUrl) => {
              const title = element.querySelector('div.text-xl')?.textContent || '';
              const image_urls = Array.from(element.querySelectorAll('.no-scrollbar img')).map((img) =>
                img.getAttribute('src'),
              );
              return { url: currentUrl, title, image_urls };
            },
            lastUrl,
          );

          gptRecommendProducts.push(info);
        }
        await page.click(closeButtonSelector);
      } else {
        this.logger.debug((action = 'No se encontraron productos recomendados'));
      }
    } catch (error: any) {
      this.logger.debug((action = `Obtener productos recomendados de chatgpt: ${error.toString()}`));
    }

    if (isAborted) return;
    await this.wait(500, 1000);
    this.logger.debug((action = 'Obtener citas de chatgpt'));
    const citationsEntranceSelector = `button.group\\/footnote`;
    const citationsContentLinkSelector = `section[screen-anchor="top"] div[slot="content"] a`;
    let gptCitations: ChatgptResponse['citations'] = [];
    await page.bringToFront();

    try {
      const citationsButton = await page.waitForSelector(citationsEntranceSelector, {
        timeout: 3_000,
      });
      if (citationsButton) {
        await citationsButton.click();
        const citationsContent = await page.waitForSelector(citationsContentLinkSelector, {
          timeout: 20_000 * this.timeoutMultiplier,
        });
        if (citationsContent) {
          gptCitations = await page.$$eval(citationsContentLinkSelector, (elements) => {
            return elements.map((element) => {
              const url = element.href || '';
              const icon = element.querySelector('img')?.getAttribute?.('src');
              const title = element.querySelector('div:nth-child(2)')?.textContent || '';
              const description = element.querySelector('div:nth-child(3)')?.textContent || '';
              return { url, icon, title, description };
            });
          });
          await page.click(closeButtonSelector);
        }
      } else {
        this.logger.debug((action = 'No se encontraron citas'));
      }
    } catch (error: any) {
      this.logger.debug((action = `Obtener citas de chatgpt: ${error.toString()}`));
    }

    // En algunos casos es necesario añadir un elemento de posición fija a la página
    // para evitar que puppeteer haga clic en elementos aleatorios
    if (isAborted) return;
    this.logger.debug((action = 'Agregar elementos fijos para evitar clics inesperados'));
    await page.evaluate(() => {
      const element = document.createElement('div');
      element.style.position = 'fixed';
      element.style.top = '0';
      element.style.left = '0';
      element.style.width = '100%';
      element.style.height = '100%';
      element.style.zIndex = '1000';
      document.body.appendChild(element);
    });

    if (isAborted) return;
    await this.wait(150, 250);
    this.logger.debug((action = 'Obtener enlaces adjuntos de chatgpt'));
    const markdownLinksSelector = 'div.markdown a';
    let gptLinksAttached: ChatgptResponse['links_attached'] = [];
    try {
      gptLinksAttached = await page.$$eval(markdownLinksSelector, (elements) => {
        return elements.map((element, index) => {
          const url = element.href || '';
          const title = element.textContent || '';
          return { url, title, position: index + 1 };
        });
      });
    } catch (error: any) {
javascript Copy
this.logger.debug((action = 'No se encontraron enlaces adjuntos'));
}

this.logger.debug((action = 'Obteniendo el cuerpo'));
const body = await page.evaluate(() => document.body.innerHTML);
const cleanBody = body
  .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // Eliminar etiquetas de script
  .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // Eliminar etiquetas de estilo
  .replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '') // Eliminar etiquetas svg
  .replace(/<img[^>]*\/?>/gi, '') // Eliminar etiquetas img
  .replace(/style="[^"]*"/gi, '') // Eliminar todos los atributos de estilo
  .replace(/class="[^"]*"/gi, '') // Eliminar todos los atributos de clase
  .replace(/<!--[\s\S]*?-->/g, '') // Eliminar comentarios
  // Reemplazos relacionados
  .replace(/<span>·<\/span>/g, '') // Eliminar etiquetas span con ·
  .replace(/<a href="https:\/\/www\.google\.com\/maps\/[^"]*"[^>]*>[\s\S]*?<\/a>/g, '') // Eliminar todos los enlaces de google maps
  .replace(/<a href="tel:+[^"]*"[^>]*>[\s\S]*?<\/a>/g, '') // Eliminar todos los enlaces tel
  .replace(/\s+/g, ' ') // Normalizar espacios
  .trim();

this.logger.debug((action = 'Comprobando respuesta de error'));
const hasError = [
  'Algo salió mal al generar la respuesta.',
  'Se ha detectado actividad inusual desde su dispositivo.',
  'Ocurrió un error. O bien el motor que solicitó no existe, o hubo otro problema procesando su solicitud.',
].some((message) => cleanBody.includes(message));
if (hasError) {
  throwError(`ChatGPT no está disponible actualmente`);
  return;
}

this.logger.log((action = 'Chat exitoso'));

const answerMap: Record<QueryChatgptRequest['answer_type'], string> = {
  html: cleanBody,
  raw: rawResponse,
  text: gptAnswer,
};
const answerResponse = answerMap[answer_type] ?? answerMap.text;

resolve({
  prompt,
  success: true,
  answer: answerResponse,
  country_code: proxy_country,
  citations: gptCitations,
  links_attached: gptLinksAttached,
  image_cards: gptImageCards,
  products: gptRecommendProducts,
  url: _url,
});
} catch (error: any) {
  if (!isAborted) {
    throwError(this.internalErrorSymbol + (error.message || String(error)));
  }
} finally {
  clearTimeout(timeoutId);
  try {
    await page.close();
    await browser.close();
  } catch {}
}
});

private async connectToBrowser(opt: PuppeteerLaunchOptions, checkTimeout: () => boolean) {
let browser;
try {
  const { browserWSEndpoint } = await this.scrapeless.browser.createSession(opt);
  browser = await Promise.race([
    puppeteer.connect({ browserWSEndpoint, defaultViewport: null }),
    new Promise((_, reject) => {
      const interval = setInterval(() => {
        if (checkTimeout()) {
          clearInterval(interval);
          browser?.close();
          reject(new Error('Tiempo de espera de conexión con el navegador agotado'));
        }
      }, 1000);
    }),
  ]);
  return browser;
} catch (error) {
  this.logger.error(`Fallo en la conexión con el navegador: ${error.message}`);
  throw error;
}
}

private async pushToMessage(data: any, webhook?: string) {
if (!webhook) {
  return;
}
try {
  const res = await fetch(webhook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    // body: JSON.stringify({ content: JSON.stringify(data).slice(0, 1800) }),
    // body: JSON.stringify(data),
    body: JSON.stringify({
      data: data,
      content: data.answer_text,
    }),
  });
  if (res.ok) {
    this.logger.log('Envío a Webhook exitoso');
  } else {
    this.logger.error('Fallo en el envío al webhook', await res.text());
  }
} catch (err) {
  this.logger.error('Excepción en el envío al webhook', err);
}
}

private async fakePageDate(page: Page) {
await page.evaluateOnNewDocument(() => {
// Hook de nueva fecha
const fixedDate = new Date();
// establecer aleatoriamente la fecha de 1 a 3 años atrás
const days = 100 + Math.floor(Math.random() * 365 * 3 - 100);
fixedDate.setDate(fixedDate.getDay() - days);
// const fixedDate = new Date('2022-01-01T00:00:00Z');
const OriginalDate = Date;

class FakeDate extends OriginalDate {
  constructor(...args: Parameters<typeof Date>) {
    super();
    if (args.length === 0) {
      return new OriginalDate(fixedDate);
    }
    return new OriginalDate(...args);
  }

  static now() {
    return fixedDate.getTime();
  }

  static parse(str: string) {
    return OriginalDate.parse(str);
  }

  static UTC(...args: Parameters<typeof Date.UTC>) {
javascript Copy
return OriginalDate.UTC(...args);
        }
      }

      Object.getOwnPropertyNames(OriginalDate).forEach((prop) => {
        FakeDate[prop as keyof typeof FakeDate] = OriginalDate[prop as keyof typeof OriginalDate] as any;
      });
      (window.Date as typeof FakeDate) = FakeDate;
    });
  }

  private async wait(fromMs: number, toMs: number) {
    const ms = fromMs + Math.random() * (toMs - fromMs);
    await new Promise((resolve) => setTimeout(resolve, ms));
  }
}

## Después de consultar: convierte respuestas en bruto en estrategias de operación GEO accionables en 30 minutos

Comprende rápidamente los resultados de búsqueda e identifica temas de contenido:

1. Copia el JSON resultante → Abre [https://csvjson.com/json2csv](https://csvjson.com/json2csv) → obtén CSV → pégalo en Excel.
2. Agrega dos nuevas columnas:
   - `brandCount` = `=SI(ESNUMERO(BUSCAR("TuMarca",D2)),1,0)`
   - `gap` = `F2-E2` (La columna F es el conteo de ocurrencias del competidor, la columna E es `brandCount`)

👉 **Conclusión:** Actualmente, nadie ha “reclamado este tema”, así que puedes escribir inmediatamente un artículo como *“¿Qué es ABCProxy?”* para captar el espacio de respuestas.  

**Consejos:** Después de ejecutar 100 consultas en lote la próxima vez, ordena por `gap` descendente → los 20 mejores resultados se convierten en ideas de contenido prioritarias.

| Campo        | Significado empresarial |
|--------------|-------------------------|
| prompt       | Consulta original del usuario |
| answer_text  | Respuesta completa de ChatGPT |
| brandCount   | Número de veces que aparece tu marca en la respuesta |
| rivalCount   | Número de veces que aparecen los competidores |
| gap          | `rivalCount - brandCount` → `0` = no reclamado, contenido prioritario; `>0` = seleccionar tema inmediatamente, escribir artículo de comparación/ranking; `<0` = mantener actualizaciones, continuar optimización |

---

## Conclusión

Con Scrapeless Cloud Browser, puedes automatizar consultas de ChatGPT para lograr optimización GEO de país a país y de zona horaria a zona horaria y obtener fácilmente resultados de búsqueda localizados y precisos. Ya sea para SEO internacional, monitoreo de marca o análisis de insights de mercado, Scrapeless te ayuda a construir rápidamente un sistema de consulta automatizado eficiente, estable y escalable.

Scrapeless no solo proporciona automatización de navegadores para datos GEO, sino que también ofrece herramientas avanzadas y estrategias de datos para controlar completamente los mecanismos de citación de IA. ¡Contáctanos para desbloquear una solución completa de datos GEO!

De cara al futuro, Scrapeless seguirá centrándose en la tecnología de navegadores en la nube, proporcionando a las empresas extracción de datos de alto rendimiento, flujos de trabajo automatizados y soporte para infraestructura de agentes de IA. Sirviendo a industrias como finanzas, comercio minorista, comercio electrónico, SEO y marketing, Scrapeless ofrece soluciones personalizadas impulsadas por escenarios para ayudar a las empresas a mantenerse por delante en la era de datos inteligentes.

Scrapeless Browser es más que solo una herramienta de automatización — es:  
> **Una infraestructura de “ecosistema de búsqueda de datos de IA escalable”**  

Te permite cuantificar realmente la visibilidad de marca de ChatGPT, la cobertura de palabras clave y las tendencias de contenido.

---

<div style="padding: 20px 0; text-align: center;">
  <a
    style="
      margin: 8px;
      display: inline-block;
      text-decoration: none;
    "
    href="https://www.goproxy.com/register?link=https://app.scrapeless.com/passport/login?utm_source=official&utm_medium=blog&utm_campaign=chatgpt-geo"
  >
    <div
      style="
        font-weight: bold;
        width: 100%;
        max-width: 400px;
        padding: 12px 40px;
        background: #12A594;
        border-radius: 5px;
        border: 2px solid #12A594;
        color: #fff;
        cursor: pointer;
        box-sizing: border-box;
        font-size: 18px;
      "
    >
      👉 Iniciar sesión en Scrapeless Browser &gt;
    </div>
  </a>
</div>

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