摘要:本节扩展图片功能,实现以图搜图功能。采用数据抓取方案,以图搜图支持用户通过图片精准搜索相似图片,经后端开发(含数据模型、API 及接口开发,运用门面模式简化调用)从全网获取结果。

以图搜图效果展示:

思维导图:

图片搜索 - 以图搜图
需求分析
用户可通过图片搜索相似图片,相比关键词搜索更精准,需从全网搜索以获取更多结果。
方案设计
- 有第三方 API 和数据抓取两种方案。第三方 API 中,百度 AI 图片搜索 API 适用于自建图库,Bing 以图搜图 API 可全网搜索且免费。
- 为便于学习,选择数据抓取方案,即利用现有以图搜图网站,实时抓取其返回结果。
数据抓取
百度搜图流程与接口分析示例:
-
进入百度图片搜索,通过 URL 上传图片,涉及接口https://graph.baidu.com/upload?uptime=其返回以图搜图的页面地址。
-
访问该页面地址,可在返回值中找到 firstUrl。

使用以图搜图功能:

后端开发
新建 api
包,由于项目可能会用到多个 api,可以将每个 api 都放在 api 目录下的一个包中。比如图片搜索 api 的相关代码,全部放在 api.imagesearch
包下。
1、数据模型开发
在**imagesearch.model
**包中,新建一个图片搜索结果类,用于接受 API 的返回值:
java
@Data
public class ImageSearchResult {
/**
* 缩略图地址
*/
private String thumbUrl;
/**
* 来源地址
*/
private String fromUrl;
}
2、API 开发
根据方案,我们要调用多个 API,每个子 API 可以作为一个静态类来实现,统一放在 imagesearch.sub
包中,并且每个类都包含一个 main
方法,用于进行本地测试。
1)获取以图搜图的页面地址
通过向百度发送 POST 请求,获取给定图片 URL 的相似图片页面地址。
java
@Slf4j
public class GetImagePageUrlApi {
/**
* 获取图片页面地址
*
* @param imageUrl
* @return
*/
public static String getImagePageUrl(String imageUrl) {
// 1. 准备请求参数
Map<String, Object> formData = new HashMap<>();
formData.put("image", imageUrl);
formData.put("tn", "pc");
formData.put("from", "pc");
formData.put("image_source", "PC_UPLOAD_URL");
// 获取当前时间戳
long uptime = System.currentTimeMillis();
// 请求地址
String url = "https://graph.baidu.com/upload?uptime=" + uptime;
try {
// 2. 发送 POST 请求到百度接口
HttpResponse response = HttpRequest.post(url)
.form(formData)
.timeout(5000)
.execute();
// 判断响应状态
if (HttpStatus.HTTP_OK != response.getStatus()) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
}
// 解析响应
String responseBody = response.body();
Map<String, Object> result = JSONUtil.toBean(responseBody, Map.class);
// 3. 处理响应结果
if (result == null || !Integer.valueOf(0).equals(result.get("status"))) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
}
Map<String, Object> data = (Map<String, Object>) result.get("data");
String rawUrl = (String) data.get("url");
// 对 URL 进行解码
String searchResultUrl = URLUtil.decode(rawUrl, StandardCharsets.UTF_8);
// 如果 URL 为空
if (searchResultUrl == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未返回有效结果");
}
return searchResultUrl;
} catch (Exception e) {
log.error("搜索失败", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜索失败");
}
}
public static void main(String[] args) {
// 测试以图搜图功能
String imageUrl = "https://www.codefather.cn/logo.png";
String result = getImagePageUrl(imageUrl);
System.out.println("搜索成功,结果 URL:" + result);
}
}
2)获取图片列表页面地址
通过 jsoup 爬取 HTML,提取其中包含 firstUrl
的 JavaScript 脚本,并返回图片列表的页面地址。
java
@Slf4j
public class GetImageFirstUrlApi {
/**
* 获取图片列表页面地址
*
* @param url
* @return
*/
public static String getImageFirstUrl(String url) {
try {
// 使用 Jsoup 获取 HTML 内容
Document document = Jsoup.connect(url)
.timeout(5000)
.get();
// 获取所有 <script> 标签
Elements scriptElements = document.getElementsByTag("script");
// 遍历找到包含 `firstUrl` 的脚本内容
for (Element script : scriptElements) {
String scriptContent = script.html();
if (scriptContent.contains("\"firstUrl\"")) {
// 正则表达式提取 firstUrl 的值
Pattern pattern = Pattern.compile("\"firstUrl\"\\s*:\\s*\"(.*?)\"");
Matcher matcher = pattern.matcher(scriptContent);
if (matcher.find()) {
String firstUrl = matcher.group(1);
// 处理转义字符
firstUrl = firstUrl.replace("\\/", "/");
return firstUrl;
}
}
}
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未找到 url");
} catch (Exception e) {
log.error("搜索失败", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "搜索失败");
}
}
public static void main(String[] args) {
// 请求目标 URL
String url = "https://graph.baidu.com/s?card_key=&entrance=GENERAL&extUiData[isLogoShow]=1&f=all&isLogoShow=1&session_id=16250747570487381669&sign=1265ce97cd54acd88139901733452612&tpl_from=pc";
String imageFirstUrl = getImageFirstUrl(url);
System.out.println("搜索成功,结果 URL:" + imageFirstUrl);
}
}
3)获取图片列表
通过调用百度接口返回的 JSON 数据,提取出其中的图片列表并返回。
java
@Slf4j
public class GetImageListApi {
/**
* 获取图片列表
*
* @param url
* @return
*/
public static List<ImageSearchResult> getImageList(String url) {
try {
// 发起GET请求
HttpResponse response = HttpUtil.createGet(url).execute();
// 获取响应内容
int statusCode = response.getStatus();
String body = response.body();
// 处理响应
if (statusCode == 200) {
// 解析 JSON 数据并处理
return processResponse(body);
} else {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "接口调用失败");
}
} catch (Exception e) {
log.error("获取图片列表失败", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取图片列表失败");
}
}
/**
* 处理接口响应内容
*
* @param responseBody 接口返回的JSON字符串
*/
private static List<ImageSearchResult> processResponse(String responseBody) {
// 解析响应对象
JSONObject jsonObject = new JSONObject(responseBody);
if (!jsonObject.containsKey("data")) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
}
JSONObject data = jsonObject.getJSONObject("data");
if (!data.containsKey("list")) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "未获取到图片列表");
}
JSONArray list = data.getJSONArray("list");
return JSONUtil.toList(list, ImageSearchResult.class);
}
public static void main(String[] args) {
String url = "https://graph.baidu.com/ajax/pcsimi?carousel=503&entrance=GENERAL&extUiData%5BisLogoShow%5D=1&inspire=general_pc&limit=30&next=2&render_type=card&session_id=16250747570487381669&sign=1265ce97cd54acd88139901733452612&tk=4caaa&tpl_from=pc";
List<ImageSearchResult> imageList = getImageList(url);
System.out.println("搜索成功" + imageList);
}
}
3、门面模式改造
这里我们运用一种设计模式来提供图片搜索服务。门面模式通过提供一个统一的接口来简化多个接口的调用,使得客户端不需要关注内部的具体实现。
我们可以将多个 API 整合到一个门面类中,简化调用过程。在 imagesearch
包下新建门面类,整合几个接口的调用:
java
@Slf4j
public class ImageSearchApiFacade {
/**
* 搜索图片
*
* @param imageUrl
* @return
*/
public static List<ImageSearchResult> searchImage(String imageUrl) {
String imagePageUrl = GetImagePageUrlApi.getImagePageUrl(imageUrl);
String imageFirstUrl = GetImageFirstUrlApi.getImageFirstUrl(imagePageUrl);
List<ImageSearchResult> imageList = GetImageListApi.getImageList(imageFirstUrl);
return imageList;
}
public static void main(String[] args) {
// 测试以图搜图功能
String imageUrl = "https://www.codefather.cn/logo.png";
List<ImageSearchResult> resultList = searchImage(imageUrl);
System.out.println("结果列表" + resultList);
}
}
4、接口开发
开发请求类:
java
@Data
public class SearchPictureByPictureRequest implements Serializable {
/**
* 图片 id
*/
private Long pictureId;
private static final long serialVersionUID = 1L;
}
开发接口:
java
/**
* 以图搜图
*/
@PostMapping("/search/picture")
public BaseResponse<List<ImageSearchResult>> searchPictureByPicture(@RequestBody SearchPictureByPictureRequest searchPictureByPictureRequest) {
ThrowUtils.throwIf(searchPictureByPictureRequest == null, ErrorCode.PARAMS_ERROR);
Long pictureId = searchPictureByPictureRequest.getPictureId();
ThrowUtils.throwIf(pictureId == null || pictureId <= 0, ErrorCode.PARAMS_ERROR);
Picture oldPicture = pictureService.getById(pictureId);
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
List<ImageSearchResult> resultList = ImageSearchApiFacade.searchImage(oldPicture.getUrl());
return ResultUtils.success(resultList);
}
**至此,相关的后端接口开发完毕,大功告成!**🎉🎉🎉