用图搜接口做反向海淘选品:从一张爆款主图到 12 条货源,全链路实战
反向海淘选品最难受的场景是:在海外社媒上刷到一个爆款,知道它长什么样,却不知道国内哪个供应商在做。靠关键词搜,翻译过去再搜回来,结果往往差得很远。这篇把我用图搜接口搭的选品链路完整写一遍——图片预处理、接口调用、结果去重排序、失败降级,附实测数据。
一、为什么关键词搜索在选品场景里不够用
先说我踩过的坑。早期我的选品流程是:看到爆款 → 人工描述品类词 → 翻译成中文 → 丢进搜索接口。
这条路有三个系统性误差:
品类词翻译误差。
tote bag翻成"托特包",但供应商标题里写的是"大容量帆布单肩包",词对不上,召回直接归零。供应商命名不规范。同一个产品,标题里可能写"ins风收纳盒"“桌面杂物篮”“北欧简约置物筐”,没有稳定词根。
外观特征无法用文字表达。版型、花色、结构这些东西,文字描述天然是丢失信息的。
图搜正好补上第三点:它用视觉特征做召回,绕过了语言和命名的双重损耗。
二、接口形态与参数
请求走统一网关,图搜对应淘宝平台的 item_search_img:
GET https://api-gw.onebound.cn/taobao/item_search_img公共参数三个平台一致:key、secret、api_name、cache、result_type、lang。图搜特有的业务参数:
| 参数 | 说明 | 实战取值 |
|---|---|---|
| imgid | 图片地址或 base64 | 公网可访问 URL,别用 localhost |
| page | 页码 | 1 起步,选品场景 1~2 页足够 |
| sort | 排序 | 默认综合;找低价货源用价格升序 |
| cache | 是否走缓存 | 离线选品库用 yes,实时比价用 no |
| result_type | 返回格式 | json |
为什么选它而不是别家:后台统计里,淘宝 item_search_img 的成功率是 92%,同一个平台的 item_search 是 85%。对比之下,1688 的 item_search 只有 53%,我从来不把它放在实时链路上。做图搜选品,入口选 92% 那个,不要为了"货源更全"去接 53% 的接口。
调用代码没什么花头:
import requests
KEY = "your_key"
SECRET = "your_secret"
def search_by_image(img_url, page=1, sort="price"):
url = "https://api-gw.onebound.cn/taobao/item_search_img"
params = {
"key": KEY, "secret": SECRET, "api_name": "item_search_img",
"imgid": img_url, "page": page, "sort": sort,
"result_type": "json", "lang": "zh-CN",
}
r = requests.get(url, params=params, timeout=15).json()
if r.get("code") != "0000":
return None, r.get("code"), r.get("msg")
return r.get("items", {}).get("item", []), "0000", ""三、预处理这一步决定了 80% 的效果
**我把同一张原始主图直接丢进去,只回了 3 条结果,而且两条是无关品类。**把图做完预处理之后,同样一张图回了 12 条,前 5 条里有 4 条是同款。
预处理做四件事:
裁主体。社媒截图里通常有大量背景和文字浮层,主体占比可能不到 30%。先做主体检测裁剪,让商品占画面 70% 以上。
去水印和图注。浮层文字会被当成视觉特征参与匹配,直接污染结果。
压到 800px 长边、转 JPEG、质量 85。再大没有收益,只是白等网络。
统一白底。浅色商品配深色背景,容易召回一堆背景相似但品类不同的东西。
from PIL import Image, ImageOps
import io, oss_client # 换成你用的对象存储
def preprocess(path, out_dir="tmp"):
im = Image.open(path).convert("RGB")
# 1. 主体裁剪:这里用你擅长的方式,检测框 + 10% 留白
box = detect_subject(im) # 返回 (l, t, r, b)
l, t, r, b = pad_box(box, im.size, ratio=0.10)
im = im.crop((l, t, r, b))
# 2. 统一白底
im = ImageOps.pad(im, (im.width, im.height), color=(255, 255, 255))
# 3. 尺寸与格式
im.thumbnail((800, 800), Image.LANCZOS)
buf = io.BytesIO(); im.save(buf, "JPEG", quality=85)
return upload_to_cdn(buf.getvalue(), "goods.jpg") # 必须返回公网 URL这里有个容易忽略的点:imgid 必须是公网可访问的 URL。我一开始图省事直接传本地路径和 127.0.0.1 的地址,接口一律返回参数错误。要么先把图传到对象存储,要么传 base64(但 base64 会让请求体膨胀三四倍,批量选品时建议还是走 URL)。
四、结果去重与排序
图搜返回的是 items.item[],每条字段大致是 num_iid、title、price、pic_url、detail_url、seller_info。选品场景要解决两件事:同一个货源被多个卖家重复铺货,以及价差巨大要挑出真实低价。
去重用感知哈希,不要用主图 URL 字符串比较——同一张图在不同店铺会被重新压缩、加边框:
def dhash(im, size=8):
im = im.convert("L").resize((size + 1, size), Image.LANCZOS)
px = list(im.getdata())
bits = []
for row in range(size):
for col in range(size):
bits.append(px[row * (size + 1) + col] > px[row * (size + 1) + col + 1])
return int("".join("1" if b else "0" for b in bits), 2)
def dedup(items, threshold=6):
kept = []
for it in sorted(items, key=lambda x: float(x.get("price") or 0)):
h = hash_of(it) # 缓存主图的 dhash,别每次重算
if all(bin(h ^ k).count("1") > threshold for k in kept_hashes):
kept.append(it)
return kept排序策略按业务目标走:做铺货选品按"销量 × 评分"排,做价格对标按价格升序 + 过滤掉明显异常的 0.01 元占位价。实测 12 条结果经过去重后剩 7 条独立货源,去重率大约四成——如果你不做这一步,选出来的"爆款"很可能全是同一家。
批量选品时把图片的 dhash 和结果一起落库,第二次遇到同一主图就能直接命中本地库,省掉一次调用。
五、失败降级与成本控制
图搜 92% 不低,但剩下 8% 你要有交代。我的降级链是:图搜 → 关键词搜索(item_search,85%) → 入人工待处理队列。
def pick_by_image(img_url, fallback_kw=None):
items, code, msg = search_by_image(img_url)
if items:
return {"source": "img", "items": dedup(items)}
if fallback_kw:
kw_items, code2, _ = search_by_keyword(fallback_kw)
if kw_items:
return {"source": "kw", "items": dedup(kw_items)}
return {"source": "manual", "items": []}两个成本细节值得记住:
错误码决定钱。
0000(成功)和2000(搜索成功但没有结果)都会计费;4000 / 4001 / 4002 / 4017这类鉴权、参数、服务端与网络异常不计费。所以参数校验要在本地做干净,别把本地拼错的 URL 发出去——那虽然不计费,但浪费时间配额。cache用对地方。选品库是典型的离线场景,同一张图短期内反复搜没有意义,一律cache=yes。只有在做实时比价、需要最新价格时才传cache=no。我的选品库任务全量打开缓存后,同一批图片的重复调用成本基本被吃平了。
最后一个经验:图搜返回的是平台在售商品,不是供应商。看到合适的结果后,还要再走一次详情接口拿 sku 和起订量,才能判断能不能代采。这一步我用 1688 的 item_get(后台统计 99%),稳定得几乎不用兜底。
六、写在最后
图搜选品的核心链路其实是四段:把图洗干净 → 用可靠性高的入口搜 → 去重再排序 → 给失败留退路。真正拉开效果差距的是第一段和第三段,接口本身反而最简单。
测试 key 我是在开放平台的控制台申请的,入口放这里,需要的自取: API 控制台



