🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

如何在ChatGPT上执行批量查询:使用Scrapeless Cloud Browser的自动化解决方案

Michael Lee
Michael Lee

Expert Network Defense Engineer

14-Nov-2025
在每个地区获得竞争优势!

使用 Scrapeless 自动化 ChatGPT 查询,解锁精准的地理洞察。不要猜测——要知道。

介绍

自今年年初以来,许多公司的 SEO 策略发生了根本性的变化。
越来越多的用户在需要信息时不再打开 Google 搜索。相反,他们直接在 ChatGPT、Claude 或 Gemini 中询问有关产品、服务和品牌的信息。

这意味着:您在 ChatGPT 中的可见性 正在成为品牌竞争力的新基准。

问题是——ChatGPT 没有“排名”功能,也没有“关键字分析工具”。

您根本无法知道:

  • 我的品牌是否出现在 ChatGPT 的回答中?
  • ChatGPT 是否推荐我的竞争对手?
  • 不同国家或语言的回答是否不同?

要解决这些问题,第一步是:

👉 执行 ChatGPT 回答的批量查询以收集数据并提取可操作的洞察


什么是 GEO?

生成引擎优化(GEO) 是创建和优化内容的实践,以便它出现在像 Google AI 概述、AI 模式、ChatGPT 和 Perplexity 等平台的 AI 生成答案中。

过去,成功意味着在搜索引擎结果页面 (SERPs) 中排名靠前。展望未来,甚至可能不再有“顶级排名”的概念。相反,您需要 成为首选推荐——AI 工具主动选择在其答案中呈现的解决方案。

GEO 优化的核心目标不再仅限于点击,而是关注三个关键指标:

  • 品牌可见性:提高您的品牌出现在 AI 生成的答案中的可能性。
  • 来源权威性:确保您的域、内容或数据被 AI 选为可信的参考。
  • 叙事一致性和积极定位:确保 AI 以专业、准确和积极的方式描述您的品牌。

这意味着基于“关键词排名”的传统 SEO 逻辑正逐渐让位于 AI 来源引用机制

品牌必须从“可搜索”转变为 被信任、被引用和被积极推荐


为什么要自动化批量 ChatGPT 查询?

从营销和 SEO 的角度来看,ChatGPT 已成为内容发现和曝光的新渠道。

然而,有三个主要痛点:

  1. 品牌覆盖率没有可见性
    公司无法知道他们的产品是否被 ChatGPT 索引、提及或推荐。没有数据,就无法创建有针对性的内容优化或分发策略。

  2. 缺乏 GEO 级洞察
    ChatGPT 的回答因地区、语言甚至时区而异。针对美国用户推荐的产品可能不会出现在日本用户面前。对于国际策略,公司必须了解这些差异。

  3. 传统 SEO 工具无法提供此数据
    现有的 SEO 工具(例如 Ahrefs、Semrush)能力有限,无法全面跟踪 ChatGPT 的回应。这意味着需要 新的方法来监控品牌在 AI 搜索渠道中的曝光

因此,批量查询 ChatGPT 的核心目标是系统地收集、分析和优化您的品牌在 ChatGPT 回答中的存在。这帮助公司:

  • 识别 ChatGPT 已提及的高潜力问题;
  • 发现尚未涵盖的内容缺口;
  • 制定有针对性的 GEO 优化策略。

为什么选择 Scrapeless Cloud Browser?

许多人可能会考虑直接调用 OpenAI API 进行批量查询。

然而,实际上,API 方法有明显的局限性:

  • 结果容易受到历史偏好和上下文的影响,从而降低客观性。
  • 难以快速切换 IP 以模拟来自不同地理位置的访问。
  • 批量查询的成本非常高(按令牌收费,规模扩大后变得昂贵)。

这正是 Scrapeless Cloud Browser 的用武之地。


什么是 Scrapeless Browser?

Scrapeless Browser 是一款云浏览器,旨在用于数据提取和自动化任务。它允许您以接近真实用户行为的方式从云端访问 ChatGPT,从而提供更准确和全面的结果。

与传统的 API 调用相比,Scrapeless Cloud Browser 在几个方面脱颖而出:

  • 没有账户偏好干扰
    所有查询都在隔离的无登录浏览器环境中执行,确保结果的客观和可靠。

  • 多区域GEO模拟
    内置来自195个以上国家的住宅代理、静态ISP和无限IP,轻松模拟来自不同地点的用户。

  • 高并发和低成本
    每个任务支持1000多个并发实例,按时间计费,成本远低于传统API。

  • 与主流框架的原生兼容性
    只需一行代码即可迁移现有的Puppeteer或Playwright项目—无需额外适配。

  • 智能反检测和可视调试
    内置对Cloudflare、reCAPTCHA和其他保护的处理,支持实时视图调试和会话记录。

推荐阅读:如何绕过Cloudflare保护和Turnstile使用Scrapeless | 完整指南

简而言之,Scrapeless Cloud Browser让您能够高效、经济且准确地执行批量“用户视角的ChatGPT查询”—无需注册数百个ChatGPT帐户,并自动提取结构化结果。


示例:使用Scrapeless Browser批量查询ChatGPT

Scrapeless Browser是一种云端无头浏览器服务,兼容Puppeteer和Playwright等主要自动化框架。使用它,您无需维护本地浏览器、代理或节点;只需一行连接代码即可启动。


1. 安装依赖

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

2. 配置Scrapeless Browser并连接

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: '批量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;
}

登录Srapeless Dashboard以获得您的Scrapeless Browser API密钥。

获取您的Scrapeless Browser API密钥

💡 Scrapeless 优势 #1:零配置环境

  • 无需本地维护浏览器实例,资源消耗最小,并且易于扩展。
  • 现有的Puppeteer项目可以通过最小的更改迁移到Scrapeless。
  • 容易模拟真实用户环境,提高隐蔽性和成功率。
  • 非常适合大规模自动化任务,如批量ChatGPT查询。

3. 自动化ChatGPT访问和输入提示

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="Ask anything"]';
  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;
}

💡 Scrapeless 优势 #2:真实网页环境

  • 自动化真实网页上的交互(输入、点击、提交)。
  • 捕获动态渲染的内容。
  • 结果与真实用户访问ChatGPT时看到的内容一致。

4. 支持多种类型的搜索结果提取

4.1 提取ChatGPT文本响应

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

4.2 提取图片卡片

ts Copy
let gptImageCards: ChatgptResponse['image_cards'] = [];
// 使用选择器提取图片并构建 { url, position }

4.3 提取推荐产品

ts Copy
const gptRecommendProducts: ChatgptResponse['products'] = [];
// 使用选择器提取产品链接、标题和图片

4.4 提取引用/参考文献

ts Copy
let gptCitations: ChatgptResponse['citations'] = [];
// 使用脚注按钮提取引用链接、图标、标题和描述

4.5 提取附加链接

ts Copy
let gptLinksAttached: ChatgptResponse['links_attached'] = [];
// 使用Markdown链接选择器提取链接及其文本

4.6 提取页面主体HTML

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

在这里,执行了HTML清理:<script><style><svg><img>标签被移除,以获得干净的主体内容。

💡 Scrapeless 优势 #3:多类型结果提取

  • 单个请求可以获取多种结构化信息,而无需多个调用或组合不同的工具。
  • 不仅可以提取文本,还可以提取图像、产品、引用、附加链接和干净的HTML。
  • 每种数据类型都被打包成对象数组(例如,ChatgptResponse['products']),便于直接输出为JSON、CSV或Webhooks,并支持下游自动化工作流。

5. 独立的浏览器上下文和会话

5.1 会话级隔离

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

browser = await this.connectToBrowser({
  session_name,              // 每个任务可以指定一个不同的会话名称
  session_ttl: 600,          // 会话生命周期
  session_recording,
  proxy_url,
  // ...
});

使用session_name参数,不同的查询可以使用单独的浏览器会话,实现会话级隔离。

5.2 浏览器实例隔离

ts Copy
async solver(input: QueryChatgptRequest, ...): Promise<BaseOutput> {
  let browser: Browser;
  
  try {
    // 为每次调用创建一个新的浏览器连接
    browser = await this.connectToBrowser(...);
    const page = await browser.newPage();
    // 执行任务
  } finally {
    // 任务完成后关闭浏览器
    await page.close();
    await browser.close();
  }
}

每次solver()调用将会:

  • 创建一个独立的浏览器实例
  • finally块中自动清理使用后的资源

5.3 代理隔离

ts Copy
const { proxy_url } = input;

browser = await this.connectToBrowser({
  proxy_url,  // 每个任务可以使用不同的代理
  // ...
});

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

不同的任务可以使用不同的proxy_url值,实现网络级隔离。

5.4 指纹隔离

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

💡 Scrapeless 优势 #4:通过隔离提高稳定性

  • 每个ChatGPT查询在独立的浏览器会话中运行,防止干扰
  • 避免了Cookies、本地存储或指纹的污染,提高请求成功率
  • 可以在同一台机器上同时运行大量查询,而不会发生Puppeteer实例冲突
  • 在高并发场景中提升了稳定性和可靠性

6. 全球GEO切换:从不同区域获取响应

6.1 全球地理定位

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

6.2 指纹本地化

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

💡 Scrapeless 优势 #5:195+ 个国家节点,自动代理与本地化仿真

  • 自动国家IP选择
    proxy_url中解析国家代码(例如,-country_US-country_JP),Scrapeless自动将请求路由到对应区域的住宅IP。

  • 无需维护代理池
    后端自动管理全球节点,用户无需自己设置或更新代理列表。

  • 本地化浏览器环境
    fingerprint.localization.timezone可以设置时区。结合独立会话,它模拟目标区域的环境,影响内容显示和区域特定的搜索结果。

  • 获取真实的本地化结果
    返回的ChatgptResponse.country_code表示请求的地理位置,使国际SEO、品牌监控或区域敏感内容分析更为准确。

7. 提取结果并支持多种输出格式

7.1 结构化输出

startChat方法中,捕获的数据被封装到统一的ChatgptResponse对象中:

ts Copy
resolve({
  prompt,
  success: true,
  answer: answerResponse,           // 文本 / HTML / 原始
  country_code: proxy_country,      // 地理信息
  citations: gptCitations,
  links_attached: gptLinksAttached,
  image_cards: gptImageCards,
  products: gptRecommendProducts,
  url: _url,
});

💡 Scrapeless 优势 #6:结构化输出

  • 每个查询任务生成一个结构化对象。
  • 包含文本答案、附加链接、图像、推荐产品、引用、国家代码和URL等字段。
  • 结构化对象可以直接用于自动化,无需额外解析。

7.2 多种输出方式

solver方法中,结果可以推送或返回:

  1. Webhook输出
ts Copy
ts Copy
this.pushToMessage(payload, webhook);
  1. 函数返回
ts Copy
return createResponse(JSON.stringify(payload), payload.url);

💡 Scrapeless 优势 #7:对数据管道集成的原生支持

  • 结构化数据可以直接推送到外部系统或自动化工具(例如,n8n、Zapier、Airtable)。
  • 无需开发额外的接口或手动处理数据,从而实现实时自动化集成。
  • 连接内部系统或数据库时,无需额外解析或转换,支持多种数据管道输出。
  • 每个查询任务结果都是一个结构化对象,便于进一步分析、统计或导出为 CSV/JSON。

完整代码

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(`处理失败:${errorMessage}`);
        throw { message: !errorMessage.includes(this.internalErrorSymbol) ? errorMessage : '' };
      }
      const errorMessage = error.message || '未知错误';
      const payload = handleChatResponse({
        success: false,
        error_reason: errorMessage,
      });
      this.pushToMessage(payload, webhook);
      this.logger.warn(`处理失败:${errorMessage}`);
      throw error;
    } finally {
      const totalDuration = getTotalDuration();
      this.logger.log(
        `处理 ${successful ? '成功' : '完成'} | 总时长:${totalDuration} 秒`,
      );
    }
  }
typescript Copy
async format(data: Uint8Array): Promise<QueryChatgptRequest> {
    if (!data) {
      throw new Error('无效的输入数据');
    }
    const input = JSON.parse(data.toString()) as QueryChatgptRequest;

    if (!input.prompt) {
      this.logger.error(`prompt 是必需的`);
      throw new Error('prompt 是必需的');
    }

    return {
      ...input,
      timeout: input.timeout || this.defaultTimeout,
      web_search: input.web_search ?? true,
      session_name: input.session_name || 'Chatgpt 答案',
      session_recording: input.session_recording || false,
      answer_type: input.answer_type || '文本',
    };
  }

  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 = '连接到浏览器'));

      const proxy_country = /-country_([A-Z]{2,3})/.exec(proxy_url)?.[1] || '任何';
      const query = new URLSearchParams({ q: prompt });
      if (web_search) {
        query.set('hints', '搜索');
      }
      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; // 每500毫秒检查一次

          const checkResponse = async () => {
            try {
              if (isAborted) {
                resolve('超时');
                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;

                // 当出现大于一个的复制按钮时,意味着gpt的答案已经完成
                const $answerCopyButtons = document.querySelectorAll('button[data-testid="copy-turn-action-button"]');
                if ($answerCopyButtons.length > 1) {
                  return lastMessage.textContent || '无内容';
                } else {
                  return null;
                }
              });

              // 如果内容没有改变且不为空
              if (currentContent) {
                retryCount++;
                // 如果内容稳定且不为空,则认为响应已完成
                if (retryCount >= 3) {
                  // 内容稳定1.5秒
                  resolve(currentContent);
                  return;
                }
              }

              // 继续检查
              setTimeout(checkResponse, CHECK_INTERVAL);
            } catch (error) {
              reject(error);
            }
          };

          // 开始检查
          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('获取流响应失败', 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(`聊天超时,超过 ${timeout} 毫秒`);
      }, timeout);

      try {
        this.logger.debug((action = '注册CDP以捕获GPT流数据(原始响应)'));
        await receiveChatGPTStream();
        this.logger.debug((action = '导航到chatgpt.com'));
javascript Copy
const navigateTimeout = 25_000 * this.timeoutMultiplier; 
try { 
  await page.goto(_url, { timeout: navigateTimeout }); 
} catch { 
  throwError(`转到chatgpt.com超时 (${navigateTimeout}ms)`); 
  return; 
} 

// 添加 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(`当 <<${_url}>> 被重定向到 OpenAI 登录页面 - ${action}`); 
  return; 
}); 

if (isAborted) return; 
await this.wait(50, 150); 
this.logger.debug((action = '确保输入框存在')); 
const inputs = ['#prompt-textarea', '[placeholder="询问任何问题"]']; 
try { 
  await Promise.race( 
    inputs.map(async (input) => { 
      await page.waitForSelector(input, { 
        timeout: 20_000 * this.timeoutMultiplier, 
        visible: true, 
      }); 
      return input; 
    }), 
  ); 
} catch { 
  throwError('当前区域不可用或已重定向至登录页面'); 
  return; 
} 

if (isAborted) return; 
await this.wait(150, 250); 
this.logger.debug((action = '等待 GPT 响应')); 
let gptAnswer: string; 
try { 
  gptAnswer = await waitForChatGPTResponse(page); 
  this.logger.debug((action = '收到 GPT 响应')); 
} catch (error: any) { 
  this.logger.error(`获取响应失败: ${error.message}`); 
  throwError(`获取 chatgpt 响应失败`); 
  return; 
} 

if (isAborted) return; 
await this.wait(150, 250); 
this.logger.debug((action = '获取 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 = '未找到图片卡片')); 
  } 
} catch (error: any) { 
  this.logger.debug((action = `获取 chatgpt 图片卡片时出错: ${error.toString()}`)); 
} 

if (isAborted) return; 
await this.wait(300, 450); 
this.logger.debug((action = '获取 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()) { 
      // 可能会触发外部链接跳转 
      let newPage: Page = null as unknown as Page; 
      const targetCreatedHandler = async (target: Target) => { 
        this.logger.debug((action = `获取 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); 

      // 点击推荐的项目 
      await page.evaluate( 
        (selector, index) => { 
          const currentProduct = document.querySelectorAll(selector)?.[index];
zh Copy
(currentProduct as HTMLElement)?.click();
                },
                recommendProductsSelector,
                index,
              );

              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 });

              // 等待详情发生变化
              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 = '未找到推荐产品'));
          }
        } catch (error: any) {
          this.logger.debug((action = `获取ChatGPT推荐产品时出错: ${error.toString()}`));
        }

        if (isAborted) return;
        await this.wait(500, 1000);
        this.logger.debug((action = '获取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 = '未找到引用'));
          }
        } catch (error: any) {
          this.logger.debug((action = `获取ChatGPT引用时出错: ${error.toString()}`));
        }

        // 在某些情况下,有必要向页面添加固定定位元素
        // 以防止Puppeteer点击随机元素
        if (isAborted) return;
        this.logger.debug((action = '添加固定元素以避免意外点击'));
        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 = '获取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) {

this.logger.debug((action = '未找到附加链接'));
}

Copy
    this.logger.debug((action = '获取主体'));
    const body = await page.evaluate(() => document.body.innerHTML);
    const cleanBody = body
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // 移除脚本标签
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // 移除样式标签
      .replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '') // 移除svg标签
      .replace(/<img[^>]*\/?>/gi, '') // 移除img标签
      .replace(/style="[^"]*"/gi, '') // 移除所有样式属性
      .replace(/class="[^"]*"/gi, '') // 移除所有类属性
      .replace(/<!--[\s\S]*?-->/g, '') // 移除注释
      // 相关替换
      .replace(/<span>·<\/span>/g, '') // 移除带有·的span标签
      .replace(/<a href="https:\/\/www\.google\.com\/maps\/[^"]*"[^>]*>[\s\S]*?<\/a>/g, '') // 移除所有谷歌地图链接
      .replace(/<a href="tel:+[^"]*"[^>]*>[\s\S]*?<\/a>/g, '') // 移除所有电话链接
      .replace(/\s+/g, ' ') // 规范化空格
      .trim();

    this.logger.debug((action = '检查错误响应'));
    const hasError = [
      '生成响应时出现问题。',
      '检测到来自您的设备的异常活动。',
      '发生错误。您请求的引擎不存在,或者处理您的请求时出现了其他问题。',
    ].some((message) => cleanBody.includes(message));
    if (hasError) {
      throwError(`ChatGPT 当前不可用`);
      return;
    }

    this.logger.log((action = '聊天成功'));

    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('浏览器连接超时'));
}
}, 1000);
}),
]);
return browser;
} catch (error) {
this.logger.error(浏览器连接失败: ${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({
data: data,
content: data.answer_text,
}),
});
if (res.ok) {
this.logger.log('Webhook 推送成功');
} else {
this.logger.error('Webhook 推送失败', await res.text());
}
} catch (err) {
this.logger.error('Webhook 推送异常', err);
}
}

private async fakePageDate(page: Page) {
await page.evaluateOnNewDocument(() => {
// 钩取新的日期
const fixedDate = new Date();
// 随机将日期设置为1-3年前
const days = 100 + Math.floor(Math.random() * 365 * 3 - 100);
fixedDate.setDate(fixedDate.getDay() - days);
const OriginalDate = Date;

Copy
  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>) {

返回 OriginalDate.UTC(...args);
}
}

Copy
  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));
}
}

查询后:将原始响应转化为可操作的 GEO 运营策略,时间 30 分钟

快速理解搜索结果并识别内容主题:

  1. 复制结果 JSON → 打开 https://csvjson.com/json2csv → 获取 CSV → 粘贴到 Excel 中。
  2. 添加两个新列:
    • brandCount = =IF(ISNUMBER(SEARCH("YourBrand",D2)),1,0)
    • gap = F2-E2(F 列是竞争对手出现次数,E 列是 brandCount
查询后:将原始响应转化为可操作的 GEO 运营策略,时间 30 分钟

👉 结论: 目前,没有人“主张这个主题”,因此你可以立即撰写一篇文章,例如 “ABCProxy 是什么?” 来抓住答案空间。

提示: 下次批量运行 100 次查询后,按 gap 降序排序 → 前 20 个结果成为优先内容创意。

字段 商业意义
prompt 原始用户查询
answer_text ChatGPT 的完整回答
brandCount 你的品牌在回答中出现的次数
rivalCount 竞争对手出现的次数
gap rivalCount - brandCount0 = 未被主张,优先内容; >0 = 立即选择主题,撰写比较/排名文章; <0 = 维护更新,继续优化

结论

通过 Scrapeless 云浏览器,你可以自动化 ChatGPT 查询,实现跨国家、跨时区的 GEO 优化,并轻松获得本地化、精准的搜索结果。无论是国际 SEO、品牌监测还是市场洞察分析,Scrapeless 帮助你快速建立高效、稳定、可扩展的自动查询系统。

Scrapeless 不仅提供 GEO 数据的浏览器自动化,还提供高级工具和数据策略,以全面控制 AI 引用机制。联系我们,解锁完整的 GEO 数据解决方案!

展望未来,Scrapeless 将继续专注于云浏览器技术,为企业提供高性能的数据提取、自动化工作流和 AI 代理基础设施支持。服务于金融、零售、电子商务、SEO 和营销等行业,Scrapeless 提供定制化、场景驱动的解决方案,帮助企业在智能数据时代保持领先。

Scrapeless 浏览器不仅是自动化工具 —— 它是:

可扩展的“AI 搜索生态系统数据采集基础设施”

使你真正量化 ChatGPT 的品牌可见性、关键词覆盖率和内容趋势。


在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录