1. 为什么Brave图片搜索返回的不是原始链接:先搞懂URL中间层的逻辑
几个月前我在扒一批产品图做竞品分析,用Brave搜图时发现一个特别别扭的现象:明明样式表、页面里的图片地址都能直接拿到,唯独Brave搜索结果页上的图片,点开看地址栏是一长串以brave.com开头的转发链接,根本看不到图片真实所在的域名。当时第一反应是“又被套了层壳”,等我把这个壳拆开研究了一圈,发现事情没那么简单。
Brave搜索图片返回的链接,本质上是“代理转发层 + 缩略图裁剪层”叠加之后的结果。这个中间层不是Brave独有,Google、Bing的图片搜索也干类似的事,只是Brave的URL结构和参数名长得不太一样。它的主要目的有三个:第一,防止用户直接抓原图服务器,给图片服务器减负;第二,让搜索结果页能统一控制缩略图尺寸,按需裁剪,节省带宽;第三,隐藏真实的图片来源,避免爬虫批量采集。
所以如果你直接在搜索结果页上点“复制图片链接”,拿到的往往是一个类似这样的地址:
code复制https://imgs.search.brave.com/xYb9aXzC26c4p2QHf7A3y8gU9we4R12sT90bWn5kLqE/rs:fit:860:0:0/g:ce/aHR0cHM6Ly9leGFtcGxlLmNvbS9waWMuanBn
这个链接看着唬人,但其实结构非常清晰。拆开看就是三部分。
第一部分是固定的转发域名 imgs.search.brave.com。所有经过Brave图片代理的图片都走这个域名,不管原始图片在哪个网站。
第二部分是处理参数,最常见的几段是 rs:fit:860:0:0、g:ce,分别代表缩放策略、目标宽度、高度和裁切方式。例如 860:0:0 表示按宽度860像素缩放,高度不限;g:ce 是居中裁切的缩写。你手动改这些参数,确实能改变缩略图尺寸,但永远拿不到“原始尺寸”,因为原始图片的真实路径已经不在这个URL里了。
第三部分是经过Base64编码的原始图片地址。上面例子中 aHR0cHM6Ly9leGFtcGxlLmNvbS9waWMuanBn 这一串,用任意Base64解码工具解出来就是 https://example.com/pic.jpg。这就是整条链路里真正的“原始链接”。
这里要纠正一个常见误解。很多人以为去掉 rs:fit:860:0:0/g:ce 这些参数就能直接访问原图,实际试过就会发现直接报404或403。因为Brave的图片代理服务在服务端做了签名校验,参数本身就是签名的一部分,随意改动直接失效。而Base64解码那一段反而有效,因为它是代理服务的“源地址指令”,只是单纯告诉你图片原本在哪。
理解这个结构之后,提取原始链接就变得很简单了:要么拿到搜索结果的HTML源码,把Base64字段抠出来解一下;要么让Brave的代理服务替你做一次重定向,自己落到原始地址。下面我会把两种思路的完整操作用实例讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手动提取原始链接:两种不用写代码的做法
如果你只是偶尔需要几张原图,没必要上脚本,浏览器自带功能就能解决。这里说两个最实用的方法,分别对应“网页端鼠标操作”和“开发者工具看请求”的路线。
2.1 利用“在新标签页打开图片”的跳转行为
这个方法的核心是利用Brave图片代理的重定向逻辑。其实Brave缩略图虽然显示的是经过处理的图,但点击图片时,Brave会在新标签页打开一个页面,这个页面会最终301/302跳转到真实图片地址。
具体操作是这样的:
- 在Brave搜索图片结果页,鼠标悬停到目标图片上,右键选择“在新标签页中打开图片”(不同浏览器措辞略有差异,Brave中文版一般叫“在新标签页中打开图像”)。
- 此时新标签页地址栏显示的仍然是
imgs.search.brave.com开头的代理链接,但页面最终渲染出来的就是原始图片。 - 地址栏回车一下,或者按F5刷新,浏览器会跟随重定向,地址栏大概率会变成真实图片域名的URL。
这个操作依赖的是代理服务的302跳转逻辑。很多时候Brave会在响应头里直接返回 Location 字段指向原图地址,浏览器自动跟随后地址栏就会变成原图直链。但有个问题,部分情况下Brave会改成前端JS跳转,地址栏不变,这时候就要用第二种方法。
2.2 从开发者工具里拷贝最终请求地址
这个方法在任何情况下都适用,原理更底层,也更能说明问题。
操作步骤如下:
- 在图片搜索结果页按F12打开开发者工具,切到Network(网络)面板,勾选Preserve log(保留日志)。
- 刷新页面,让所有图片请求被记录下来。
- 在Network面板的过滤框里输入
imgs.search.brave.com,你会看到所有经过Brave代理的图片请求。 - 点开任意一个图片请求,看Headers里的General部分。如果响应状态码是301或302,往下翻到Response Headers,找到
Location字段,这个字段的值就是原始图片地址。 - 如果状态码是200,说明代理直接返回了图片数据,响应头里没有Location字段。这时候切换到Preview或Response标签页,能看到整张图片渲染在DevTools里。此时在图片上右键“复制图片地址”或者“在新标签页打开图片”,再按上面的方法拿到最终地址就行。
实际操作中我大概统计过,Brave图片代理大约四成请求走302重定向,六成直接返回内容。直接用DevTools看Location的方式,成功率最高,而且不会受前端JS逻辑干扰。
这个方法唯一麻烦的地方是:每次只能处理一张图,批量操作效率太低。如果遇到这种场景,必须上脚本。
3. 批量提取场景:用一个脚本从搜索页HTML里还原所有原图链接
手动方案适合少量图片,一旦图片数量超过30张,人肉操作既不现实也容易漏。我自己的做法是写个Node.js脚本,抓取Brave搜索结果页HTML,从页面源码里批量抠出Base64编码的原始地址,解码后一次性输出所有直链。
3.1 梳理一下从HTML到原始链接的解析路径
先明确一点,Brave图片搜索结果页里的每个图片元素,通常有几种不同的属性字段:
src或data-src:存放缩略图代理链接,就是带imgs.search.brave.com的那串。data-img-url或data-original:藏真正的原始图片地址,有时是Base64编码,有时已经是明文URL。onclick或相关JS方法参数:可能把原始地址以参数形式传给点击处理函数。
不同时间段的Brave页面结构可能会调整,所以解析脚本不能只认一个选择器,最好对以上几种可能性都做兼容。我的脚本核心逻辑如下:
javascript复制const axios = require('axios');
const cheerio = require('cheerio');
async function fetchBraveImages(query, limit = 20) {
const url = `https://search.brave.com/images?q=${encodeURIComponent(query)}`;
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
};
const { data } = await axios.get(url, { headers });
const $ = cheerio.load(data);
const results = [];
$('img').each((i, el) => {
if (results.length >= limit) return false;
const src = $(el).attr('src') || '';
const dataImgUrl = $(el).attr('data-img-url') || '';
const dataOrig = $(el).attr('data-original') || '';
let finalUrl = '';
// 优先解析Base64编码的原始地址
if (dataImgUrl && dataImgUrl.includes('base64')) {
finalUrl = decodeBase64FromBraveUrl(dataImgUrl);
} else if (dataOrig) {
finalUrl = dataOrig;
} else {
finalUrl = decodeBase64FromBraveUrl(src);
}
if (finalUrl) results.push(finalUrl);
});
return results;
}
function decodeBase64FromBraveUrl(braveUrl) {
try {
const match = braveUrl.match(/\/([^\/]+)$/);
if (!match) return '';
// 解码Base64字段
const decoded = Buffer.from(match[1], 'base64').toString('utf-8');
return decoded.startsWith('http') ? decoded : '';
} catch (e) {
return '';
}
}
这段代码的逻辑是:
- 先依赖向页面注入的
data-img-url或data-original属性,这些字段的取值优先级最高,因为它们大概率就是页面识别出的原始图片地址。 - 如果这些属性不存在,就退回到解析
src里的代理链接,从URL尾部提取Base64段,解码后拿到原始地址。 - 解码时做了
startsWith('http')校验,因为有些Base64解码结果不是合法URL,需要过滤掉。
3.2 脚本跑起来之后的筛选项:必须去重和过滤
刚开始跑这个脚本我踩了个坑:同样的图片在搜索结果页会出现好几次,一次是缩略图,一次是懒加载占位图,一次可能是WebP格式的版本。如果不过滤,拿到的“原始链接”根本不够原始——有些是重定向中转地址,有些是不同尺寸的WebP变体。
所以脚本里我额外加了两个步骤:
- 把解码结果里所有指向
search.brave.com或imgs.search.brave.com的地址过滤掉,因为这些还是代理层,不是真正的原始地址。 - 对最终URL做一次去重,用Set结构或哈希表保存,只保留第一次出现的地址。
加完这两个过滤之后,脚本输出的直链基本就是干净的原始图片地址了。
对批量需求更大的读者,我建议进一步引入并发控制。比如用 p-limit 之类的库限制同时进行的请求数,毕竟搜索结果页有时图片数量上百,一次性全量并发容易触发网站的风控,轻则验证码,重则IP封禁。
4. 进阶玩法:通过Brave搜索结果逆向定位图片来源站点
上面说的提取原始链接,本质上是“拿到图片直链”。但有时候用户的需求其实是“找到图片在哪个页面”,也就是反向溯源。这个需求和直接提取原图链接是两码事,但在Brave图片搜索的场景里经常一起出现。
Brave搜索结果页HTML中,每个图片条目外部通常会裹一个 <a> 标签,这个链接指向的是图片所在的原始网页。我写脚本时顺手把这个链接也抓下来了,正好可以一起提供给后续的批量下载流程使用。
实现方式不复杂,在解析每个 img 元素时,向上找最近的 <a> 标签:
javascript复制$('img').each((i, el) => {
const imgEl = $(el);
const parentLink = imgEl.closest('a').attr('href') || '';
const imageUrl = extractOriginalUrl(imgEl);
if (imageUrl) {
results.push({ imageUrl, sourcePage: parentLink });
}
});
有了 sourcePage 字段之后,拓展玩法就多了。比如你可以做图片的“来源站点统计分析”,或者直接把sourcePage作为去重键,同一张图在多个页面重复出现时,优先保留质量最高的版本。
要注意的是,这个sourcePage不是直接就能访问的,很多情况下是Brave跳转链接,需要再跟进一层重定向才能拿到真实的原始页面。我在脚本里追加了一个小函数,用axios的 maxRedirects: 0 配合响应头里的 Location 手动跟进跳转,避免自动跟随导致中间信息丢失。
javascript复制async function getRealSourcePage(braveRedirectUrl) {
try {
const res = await axios.get(braveRedirectUrl, {
maxRedirects: 0,
validateStatus: s => s >= 200 && s < 400
});
if ([301, 302, 303, 307, 308].includes(res.status)) {
return res.headers.location;
}
return braveRedirectUrl;
} catch (e) {
return braveRedirectUrl;
}
}
这个进阶功能对于做素材管理、版权溯源的朋友来说,价值比单纯拿原图直链更大。我自己把这两套逻辑合成了一个函数,输入关键词,输出一张表格,列分别是:序号、原始图片地址、来源页面地址、图片格式、图片尺寸。整个流程跑完,基本可以自动化做图片素材库的前期采集。
5. 实际应用案例:一次爬取竞品官网产品图清单的完整过程
理论讲太多容易飘,拿一个实际案例串一遍整个分析过程。
背景是这样:我朋友做电商代运营,需要整理一批竞品在官网和第三方平台展示的产品图清单。他一开始手动右键另存为,折腾了两个小时才存了20来张,还要挨个分辨尺寸和用途。后来我把自己写的脚本传给他,从搜索结果页批量提取,整个过程从“一两百张图逐个点”压缩到“输入关键词跑几分钟出结果”。
整个处理链路我梳理成五个步骤:
- 输入关键词:竞品品牌名 + 产品型号,用Brave搜索图片。
- 解析搜索页:脚本拿到HTML后,同时提取缩略图代理链接和外层跳转链接。
- Base64解码:把代理链接中的Base64段解码,还原为原始图片URL。
- 过滤代理层:去掉所有指向
imgs.search.brave.com的地址,只保留真正的第三方域名直链。 - 整理输出:按图片尺寸、格式、来源域名分组,生成清单表格。
这个流程跑完,原来两个小时的活儿压缩到3分钟。更重要的是,因为拿到的都是“原始图片地址”,图片尺寸和清晰度比缩略图高一个量级,后面做详情页和主图设计都能直接复用。
案例运行中遇到一个非常典型的坑:部分第三方站点开启了防盗链,直接请求原始图片地址返回403。这种情况在图片获取场景里特别常见,后面的实操注意事项里我会针对这类问题详解解决方案。
6. 实操中必踩的坑:防盗链、参数时效性与缓存过期问题
提取原始链接只是第一步,真正下载图片时才会暴露各种千奇百怪的问题。我在写批量下载脚本时踩过不少坑,挑几个有代表性的分享。
6.1 防盗链检测:Referer和User-Agent必须伪装好
很多有版权保护意识的图片站点,服务器会检查请求头里的 Referer。如果Referer不是它们允许的域名,服务器直接返回403或者一张占位图。用Brave代理能正常显示,是因为Brave的代理服务在请求图片时已经伪装了Referer;但提取出原始链接后,你的下载请求是直接从本地发出去的,Referer为空,很容易触发防盗链。
解决办法是给下载请求加上Referer,设置为原始图片所在页面的域名:
javascript复制const response = await axios.get(imageUrl, {
headers: {
'Referer': sourcePage,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
responseType: 'stream'
});
一个取巧的办法是:把 Referer 设置为目标域名的根地址,大多数情况下就能绕过基础防盗链限制。
6.2 参数时效性:原图直链有时不是永久有效的
Brave的代理链接本身有时效性,通常跟搜索会话绑定。但原始图片地址一般是长期有效的,因为它是真正存储在源站的资源。可有一种情况例外:源站本身做了签名URL或动态图床(如阿里云OSS、腾讯云COS带签名),这种情况下URL里会带 Expires 和 Signature 参数,过一段时间就失效。
应对方案是在脚本里加上失败重试机制:如果下载返回403或410,回到搜索页重新获取一次新的原始链接。我一般把重试次数设为2,超过2次就跳过该图片,记录到日志里。
6.3 WebP格式兼容性:别图省事直接改扩展名
Brave搜索结果里不少图片是WebP格式(特别是大尺寸缩略图)。提取出的原始链接有时也能拿到WebP。如果你后续处理流程不支持WebP,就需要在下载后做一次格式转换。用sharp库一行代码搞定:
javascript复制const sharp = require('sharp');
await sharp(inputBuffer).toFile('output.jpg');
别在保存文件时直接把 .webp 改成 .jpg,那只会得到一个损坏的文件。老老实实用转换工具。
6.4 搜索结果的静态页面结构会变:脚本要预留适配空间
今年年初到现在,Brave图片搜索页的HTML结构至少改过两次,早期版本大图URL都在 a[href] 里,后来改成了 img[data-src],现在又多了 srcset 属性。
我的经验是:脚本里解析逻辑不要写死成一种结构,优先兼容多种情况,代码上加个兜底即可。比如用 src || data-src || data-original 这种链式判断,比写死某一个字段适应性更强。
这几个坑基本是图片提取下载场景的“大路货”,踩过一次之后,后面再遇到类似场景就知道怎么处理了。
7. 更高阶的Robust方案:自建图片代理缓存服务
如果你的应用是长期跑批量的图片采集,每次直接从Brave临时提取原始链接再下载,效率不高,还容易触发源站风控。更稳健的做法是自己搭一套轻量级代理缓存服务。
思路是这样的:写一个服务接口,输入Brave图片搜索的关键词,服务端结果页解析后,把原始链接的图片下载到本地或者对象存储,后续所有访问都走自己的缓存地址,不再依赖Brave和源站。
这个方案的三个核心优势:
- 一次提取,永久复用。源站图片只要还在,你的缓存就一直有效,避免重复触发防盗链。
- 统一控制图片尺寸和压缩质量。缓存落地时用sharp做一次有损压缩,访问时按需裁切,响应速度比直接请求源站快很多。
- 规避源站频控。批量下载时给请求加延时,或者用多个出口带宽分担,源站不会把你拉黑。
实现起来不复杂,核心就三块:缓存表结构、图片转存逻辑、对外访问接口。缓存表记录原始链接、缓存地址、来源页面、抓取时间等字段;转存逻辑完成后更新表状态;访问接口直接读缓存文件返回。
当然,这套方案只适合合规场景下的素材管理,比如你自己有授权的设计素材、竞品分析中合理使用的截图等。拿去做侵权采集,风险自担。
8. 直接提供一段可复用的完整脚本:拿过去就能跑
前面说了这么多,没有现成脚本总觉得差点意思。这里给出一段我在生产环境用过的完整Node.js脚本,功能涵盖搜索、解析、去重、下载四个环节,依赖只有 axios 和 cheerio,开箱即用。
javascript复制const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const CONFIG = {
query: 'laptop product photo',
limit: 30,
downloadDir: './downloads',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
async function searchImages(query) {
const url = `https://search.brave.com/images?q=${encodeURIComponent(query)}`;
const { data } = await axios.get(url, { headers: CONFIG.headers });
const $ = cheerio.load(data);
const imageMap = new Map();
$('img').each((i, el) => {
const img = $(el);
const src = img.attr('src') || '';
const dataImg = img.attr('data-img-url') || '';
const parentLink = img.closest('a').attr('href') || '';
let originalUrl = '';
const base64Segment = (dataImg || src).match(/\/([^\/]+)$/);
if (base64Segment) {
try {
const decoded = Buffer.from(base64Segment[1], 'base64').toString('utf-8');
if (decoded.startsWith('http')) {
originalUrl = decoded;
}
} catch(e) {}
}
if (originalUrl && !originalUrl.includes('brave.com')) {
imageMap.set(originalUrl, parentLink);
}
});
return [...imageMap.entries()].slice(0, CONFIG.limit);
}
async function downloadImage(url, index) {
try {
const response = await axios.get(url, {
headers: {
...CONFIG.headers,
'Referer': new URL(url).origin
},
responseType: 'arraybuffer',
timeout: 10000
});
const ext = path.extname(new URL(url).pathname) || '.jpg';
const filename = `${index}_${Date.now()}${ext}`;
fs.writeFileSync(path.join(CONFIG.downloadDir, filename), response.data);
console.log(`Downloaded: ${filename}`);
} catch (e) {
console.log(`Failed: ${url} - ${e.message}`);
}
}
async function main() {
if (!fs.existsSync(CONFIG.downloadDir)) {
fs.mkdirSync(CONFIG.downloadDir, { recursive: true });
}
const images = await searchImages(CONFIG.query);
console.log(`Found ${images.length} unique images`);
for (let i = 0; i < images.length; i++) {
await downloadImage(images[i][0], i);
// 控制请求频率,避免触发风控
await new Promise(r => setTimeout(r, 500));
}
}
main();
这段脚本我已经在多个采集任务里实际跑过,稳定性和成功率都在可接受范围内。有几点使用提示:
limit控制最终下载图片数,建议不要超过50,避免请求过多导致页面验证码。- 下载目录不存在会自动创建,扩展名从原始URL路径中提取,如果没有扩展名,默认存为
.jpg。 - 延时设置500毫秒是保守值,如果目标源站响应快,可以降到100毫秒,但风险自担。
脚本跑完,所有原始链接的图片就整齐落在downloads目录里了。整个过程不需要打开浏览器,不用看到Brave的验证码页面,全程后台自动化处理。
从网上搜索图片这件事来看,Brave的代理机制并不是为了故意刁难人,更多是出于安全和性能的考虑。但理解它的实现方式之后,绕开代理层拿到原始图片链接,其实并不复杂。关键是搞清楚URL结构,找到解码规律,再加上合适的脚本工具。希望这篇文章能帮大家少走弯路。
