如何在ChatGPT上执行批量查询:使用Scrapeless Cloud Browser的自动化解决方案
Expert Network Defense Engineer
使用 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 已成为内容发现和曝光的新渠道。
然而,有三个主要痛点:
-
品牌覆盖率没有可见性
公司无法知道他们的产品是否被 ChatGPT 索引、提及或推荐。没有数据,就无法创建有针对性的内容优化或分发策略。 -
缺乏 GEO 级洞察
ChatGPT 的回答因地区、语言甚至时区而异。针对美国用户推荐的产品可能不会出现在日本用户面前。对于国际策略,公司必须了解这些差异。 -
传统 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和其他保护的处理,支持实时视图调试和会话记录。
简而言之,Scrapeless Cloud Browser让您能够高效、经济且准确地执行批量“用户视角的ChatGPT查询”—无需注册数百个ChatGPT帐户,并自动提取结构化结果。
示例:使用Scrapeless Browser批量查询ChatGPT
Scrapeless Browser是一种云端无头浏览器服务,兼容Puppeteer和Playwright等主要自动化框架。使用它,您无需维护本地浏览器、代理或节点;只需一行连接代码即可启动。
1. 安装依赖
bash
npm install puppeteer-core @scrapeless-ai/sdk node-fetch
2. 配置Scrapeless Browser并连接
ts
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 优势 #1:零配置环境
- 无需本地维护浏览器实例,资源消耗最小,并且易于扩展。
- 现有的Puppeteer项目可以通过最小的更改迁移到Scrapeless。
- 容易模拟真实用户环境,提高隐蔽性和成功率。
- 非常适合大规模自动化任务,如批量ChatGPT查询。
3. 自动化ChatGPT访问和输入提示
ts
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
let gptAnswer: string;
gptAnswer = await waitForChatGPTResponse(page);
4.2 提取图片卡片
ts
let gptImageCards: ChatgptResponse['image_cards'] = [];
// 使用选择器提取图片并构建 { url, position }
4.3 提取推荐产品
ts
const gptRecommendProducts: ChatgptResponse['products'] = [];
// 使用选择器提取产品链接、标题和图片
4.4 提取引用/参考文献
ts
let gptCitations: ChatgptResponse['citations'] = [];
// 使用脚注按钮提取引用链接、图标、标题和描述
4.5 提取附加链接
ts
let gptLinksAttached: ChatgptResponse['links_attached'] = [];
// 使用Markdown链接选择器提取链接及其文本
4.6 提取页面主体HTML
ts
const body = await page.evaluate(() => document.body.innerHTML);
zh
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
const { session_name, task_id, ... } = input;
browser = await this.connectToBrowser({
session_name, // 每个任务可以指定一个不同的会话名称
session_ttl: 600, // 会话生命周期
session_recording,
proxy_url,
// ...
});
使用session_name参数,不同的查询可以使用单独的浏览器会话,实现会话级隔离。
5.2 浏览器实例隔离
ts
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
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
fingerprint: {
platform: 'macOS',
localization: {
timezone: 'America/New_York',
},
args: {
'--window-size': '1920,1080',
},
}
💡 Scrapeless 优势 #4:通过隔离提高稳定性
- 每个ChatGPT查询在独立的浏览器会话中运行,防止干扰
- 避免了Cookies、本地存储或指纹的污染,提高请求成功率
- 可以在同一台机器上同时运行大量查询,而不会发生Puppeteer实例冲突
- 在高并发场景中提升了稳定性和可靠性
6. 全球GEO切换:从不同区域获取响应
6.1 全球地理定位
ts
const proxy_country = /-country_([A-Z]{2,3})/.exec(proxy_url)?.[1] || 'ANY';
6.2 指纹本地化
ts
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
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方法中,结果可以推送或返回:
- Webhook输出
ts
ts
this.pushToMessage(payload, webhook);
- 函数返回
ts
return createResponse(JSON.stringify(payload), payload.url);
💡 Scrapeless 优势 #7:对数据管道集成的原生支持
- 结构化数据可以直接推送到外部系统或自动化工具(例如,n8n、Zapier、Airtable)。
- 无需开发额外的接口或手动处理数据,从而实现实时自动化集成。
- 连接内部系统或数据库时,无需额外解析或转换,支持多种数据管道输出。
- 每个查询任务结果都是一个结构化对象,便于进一步分析、统计或导出为 CSV/JSON。
完整代码
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
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
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
(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 = '未找到附加链接'));
}
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;
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);
}
}
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 分钟
快速理解搜索结果并识别内容主题:
- 复制结果 JSON → 打开 https://csvjson.com/json2csv → 获取 CSV → 粘贴到 Excel 中。
- 添加两个新列:
brandCount==IF(ISNUMBER(SEARCH("YourBrand",D2)),1,0)gap=F2-E2(F 列是竞争对手出现次数,E 列是brandCount)

👉 结论: 目前,没有人“主张这个主题”,因此你可以立即撰写一篇文章,例如 “ABCProxy 是什么?” 来抓住答案空间。
提示: 下次批量运行 100 次查询后,按 gap 降序排序 → 前 20 个结果成为优先内容创意。
| 字段 | 商业意义 |
|---|---|
| prompt | 原始用户查询 |
| answer_text | ChatGPT 的完整回答 |
| brandCount | 你的品牌在回答中出现的次数 |
| rivalCount | 竞争对手出现的次数 |
| gap | rivalCount - brandCount → 0 = 未被主张,优先内容; >0 = 立即选择主题,撰写比较/排名文章; <0 = 维护更新,继续优化 |
结论
通过 Scrapeless 云浏览器,你可以自动化 ChatGPT 查询,实现跨国家、跨时区的 GEO 优化,并轻松获得本地化、精准的搜索结果。无论是国际 SEO、品牌监测还是市场洞察分析,Scrapeless 帮助你快速建立高效、稳定、可扩展的自动查询系统。
Scrapeless 不仅提供 GEO 数据的浏览器自动化,还提供高级工具和数据策略,以全面控制 AI 引用机制。联系我们,解锁完整的 GEO 数据解决方案!
展望未来,Scrapeless 将继续专注于云浏览器技术,为企业提供高性能的数据提取、自动化工作流和 AI 代理基础设施支持。服务于金融、零售、电子商务、SEO 和营销等行业,Scrapeless 提供定制化、场景驱动的解决方案,帮助企业在智能数据时代保持领先。
Scrapeless 浏览器不仅是自动化工具 —— 它是:
可扩展的“AI 搜索生态系统数据采集基础设施”
使你真正量化 ChatGPT 的品牌可见性、关键词覆盖率和内容趋势。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。




