一、业务场景的复杂性
做反向海淘相关的跨境独立站,最前面的一个拦路虎就是商品采集。用户从微信、小红书、Instagram给你扔过来各种格式的“想买的东西”——有的是淘宝链接,有的是淘口令,有的连链接都没有就是一张截图,还有的只记得关键词。作为代购系统,你必须全部接住。
我维护Taocarts系统这套代购源码三年了,代购系统里采集模块重写过两次。第一次太死板,只支持链接解析,结果客户反馈说“我妈妈只会复制口令,你这个平台用不了”。后来加了口令解析,又有人说“我从1688截图想要同款,你们能不能以图搜图”。最终我们搞了一个多策略解析引擎,支持链接、口令、关键词、图片四种入口。今天就把这个模块的设计和代码分享出来,希望对做跨境代购或淘宝1688代购系统的朋友有帮助。
二、整体架构:策略模式 + 责任链
一开始我们用if-else判断输入类型,结果代码膨胀得厉害。后来改成策略模式,每种解析方式独立成一个Strategy,通过一个输入识别器自动路由。
java
// 定义解析策略接口
public interface ProductParseStrategy {
boolean support(String input); // 判断是否能处理该输入
Mono parse(String input);
}
// 策略注册器
@Component
public class ParseStrategyRegistry {
private final List strategies;
public ParseStrategyRegistry(List<ProductParseStrategy> strategies) {
this.strategies = strategies;
}
public ProductParseStrategy getStrategy(String input) {
return strategies.stream()
.filter(s -> s.support(input))
.findFirst()
.orElseThrow(() -> new BusinessException("无法识别的输入格式"));
}
}
// 统一入口服务
@Service
public class ProductCrawlService {
@Autowired
private ParseStrategyRegistry registry;
public Mono<ProductInfo> crawl(String userInput) {
ProductParseStrategy strategy = registry.getStrategy(userInput);
return strategy.parse(userInput)
.doOnNext(product -> log.info("采集成功: {} - {}", product.getTitle(), product.getPrice()));
}
}
这个设计的好处是,以后增加新的采集方式(比如支持拼多多链接)只需要新增一个Strategy实现,完全符合开闭原则。
三、链接解析:淘宝/1688通用适配器
链接解析是最常见的。但淘宝和1688的链接格式不一样,而且经常变。我们维护了一个链接模式库。
java
@Component
public class LinkParseStrategy implements ProductParseStrategy {
// 匹配淘宝、天猫、1688等链接的正则
private static final Pattern TAOBAO_PATTERN = Pattern.compile("(https?://)?(item\.taobao\.com|detail\.tmall\.com|item\.1688\.com)/.*?(id=(\d+)|(\d+)\.html)");
@Override
public boolean support(String input) {
return TAOBAO_PATTERN.matcher(input).find();
}
@Override
public Mono<ProductInfo> parse(String url) {
String itemId = extractItemId(url);
String platform = detectPlatform(url); // taobao, tmall, 1688
// 调用对应平台的API(模拟请求或官方接口)
return fetchProductFromPlatform(platform, itemId);
}
private String extractItemId(String url) {
Matcher m = TAOBAO_PATTERN.matcher(url);
if (m.find()) {
return m.group(3) != null ? m.group(3) : m.group(4);
}
throw new ParseException("无法提取商品ID");
}
private Mono<ProductInfo> fetchProductFromPlatform(String platform, String itemId) {
// 这里使用WebClient,前面文章已写过,简略一下
// 注意:要处理反爬,携带动态UA、Referer,必要时使用代理
return webClient.get()
.uri(apiUrl(platform, itemId))
.header("User-Agent", UserAgentPool.random())
.retrieve()
.bodyToMono(String.class)
.map(html -> parseHtmlToProduct(html, platform));
}
}
踩过的坑:淘宝的API返回的是JSONP格式,而且有时会返回验证码页面。我们的解决办法是:先用无头浏览器尝试获取真实页面,失败则降级到缓存。缓存没有就返回友好提示“请稍后重试”。反向海淘业务对实时性要求没那么极端,给用户一个友好的等待提示比死磕反爬更重要。
四、口令解析:从淘口令中提取商品
淘口令是一段乱码文本,但里面隐藏了商品ID。实际上淘宝官方提供了一个开放接口:https://main.m.taobao.com/iwant/wireless/alisrv?code=口令。我们需要模拟请求这个接口。
java
@Component
public class PasswordParseStrategy implements ProductParseStrategy {
private static final Pattern PASSWORD_PATTERN = Pattern.compile("(¥|\$)(\w+)(¥|\$)");
@Override
public boolean support(String input) {
return PASSWORD_PATTERN.matcher(input).find();
}
@Override
public Mono<ProductInfo> parse(String passwordText) {
String code = extractCode(passwordText);
String url = "https://main.m.taobao.com/iwant/wireless/alisrv?code=" + code;
return webClient.get().uri(url)
.retrieve()
.bodyToMono(String.class)
.map(this::extractItemIdFromResponse)
.flatMap(itemId -> linkParseStrategy.parse("https://item.taobao.com/item.htm?id=" + itemId));
}
}
注意:这个接口偶尔会返回302重定向,需要跟随重定向。还有一点,淘口令有时效性,过期的口令会失效,记得给用户提示“口令已过期”。
五、图片搜索:以图搜图的技术实现
图片搜索是最有技术含量的。在代购转运场景中,客户可能在小红书看到一个包,没有链接只有图片。我们接入了阿里云的图像搜索服务,同时也自建了一个简单的以图搜图缓存。
java
@Component
public class ImageParseStrategy implements ProductParseStrategy {
// 判断是否是图片(base64或者上传的文件)
@Override
public boolean support(String input) {
return input.startsWith("data:image") || input.length() > 200 && !input.contains("http");
}
@Override
public Mono<ProductInfo> parse(String imageBase64) {
byte[] imageBytes = Base64.getDecoder().decode(imageBase64.split(",")[1]);
// 调用阿里云图搜API(需要申请开通)
return aliyunImageSearch.search(imageBytes)
.flatMap(similarItems -> {
if (similarItems.isEmpty()) {
return Mono.error(new BusinessException("未找到相似商品"));
}
// 取第一个相似商品ID,再走链接解析
String firstItemId = similarItems.get(0).getItemId();
return linkParseStrategy.parse("https://item.taobao.com/item.htm?id=" + firstItemId);
});
}
}
实际经验:阿里云图搜的准确率没有想象中高,尤其是服饰类。我们加了一个“用户可选择多个候选商品”的界面,让用户自己选哪个最像。另外图片大小限制也很坑,超过2M的要先压缩。我们用了Thumbnailator库压缩图片再上传。
六、关键词搜索:交给外部搜索引擎
关键词搜索我们不自己做,因为淘宝的搜索太复杂(排序、个性化、广告)。我们直接跳转到淘宝的搜索结果页,或者调用淘宝开放平台的搜索API(需要申请权限)。简单做法:
java
@Component
public class KeywordParseStrategy implements ProductParseStrategy {
@Override
public boolean support(String input) {
// 不是链接、不是口令、不是图片,就认为是关键词
return !input.startsWith("http") && !input.contains("¥") && input.length() < 100;
}
@Override
public Mono<ProductInfo> parse(String keyword) {
// 这里不直接返回商品,而是返回一个搜索结果的URL,让用户自己点
String searchUrl = "https://s.taobao.com/search?q=" + URLEncoder.encode(keyword, StandardCharsets.UTF_8);
return Mono.just(ProductInfo.searchResult(searchUrl));
}
}
当然也有更高级的做法:爬取淘宝搜索结果页,解析出商品列表展示给用户。但那样做容易被封,而且维护成本高。我们选择把选择权还给用户。
七、与Taocarts系统的集成
上述设计已经完整实现在Taocarts系统中。Taocarts系统是一套开箱即用的代购系统源码,除了商品采集,还涵盖了代购转运仓库管理、代购集运打包、国际集运运费计算等模块。如果你正在搭建自己的反向海淘**跨境独立站,可以直接参考这套代购源码,省去大量重复造轮子的工作。目前已经有不少跨境代购团队基于Taocarts系统二次开发,对接淘宝1688代购系统的业务。
八、小结与优化建议
多源采集看起来复杂,但用策略模式组织后其实很清晰。有几个优化点可以分享:
缓存策略:同一商品在短时间内被多次采集,直接返回缓存,避免频繁请求淘宝。
异步处理:采集是IO密集型,建议使用WebFlux或异步线程池,不要阻塞Tomcat线程。
降级机制:当淘宝接口不稳定时,返回上次缓存的商品信息,并提示“价格可能不是最新”。
限流:同一个IP对淘宝的请求频率不能太高,使用令牌桶限流。
曾经有一次双11期间,我们的采集接口被刷爆,因为一个爬虫脚本无限请求。后来加上了用户级别的限流:每个用户每分钟最多搜索10次。很管用。
最后,如果你的反向海淘业务刚刚起步,不妨直接看看Taocarts系统的实现,它已经把上述所有坑都踩过了。代码开源,欢迎交流。