云图库第五期批量抓取和创建图片 方案二
鱼总的思路是用Jsoup来解析响应到页面中的DOM元素里面的图片地址,确实很厉害。
下面给兄弟们出一个批量抓取图片的另一个思路: 调用百度的图片接口,发起一个请求,然后解析百度图片接口的响应体中的URL地址。
代码如下(Service):
▼java复制代码/** * 批量抓取和创建图片 */ Integer uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest,User loginUser`);
▼java复制代码@Override public Integer uploadPictureByBatch(PictureUploadByBatchRequest pictureUploadByBatchRequest, User loginUser) { // 校验参数 ThrowUtils.throwIf(pictureUploadByBatchRequest == null, ErrorCode.PARAMS_ERROR); String searchText = pictureUploadByBatchRequest.getSearchText(); Integer count = pictureUploadByBatchRequest.getCount(); String namePrefix = pictureUploadByBatchRequest.getNamePrefix(); ThrowUtils.throwIf(count <= 0 || count > 30, ErrorCode.PARAMS_ERROR, "数量错误"); if (StrUtil.isBlank(namePrefix)) { namePrefix = searchText; } // 使用百度图片的 JSON 接口(更稳定,不易触发验证码) String fetchUrl = String.format( "https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord=%s&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=&hd=&latest=©right=&word=%s&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=1&fr=&expermode=&nojc=&isAsync=&pn=0&rn=30", searchText, searchText ); try { // 使用 Hutool 发送请求获取 JSON 数据(Jsoup 不支持 JSON) String jsonResponse = HttpUtil.createRequest(Method.GET, fetchUrl) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") .header("Accept", "application/json, text/plain, */*") .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") .header("Referer", "https://image.baidu.com/") .timeout(15000) .execute() .body(); // 检查是否返回了验证码页面 if (jsonResponse.contains("百度安全验证") || jsonResponse.contains("security_verification")) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "百度触发安全验证,请稍后重试或更换搜索关键词"); } // 从 JSON 中提取图片 URL List<String> imgUrls = extractImageUrls(jsonResponse, count); if (imgUrls.isEmpty()) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未找到图片"); } // 批量保存图片 int successCount = 0; String uploadPathPrefix = String.format("public/%s", loginUser.getId()); Set<String> uploadedUrlSet = new HashSet<>(); int nameCounter = 0; for (String imgUrl : imgUrls) { try { // 检查 URL 是否已上传,避免重复 if (uploadedUrlSet.contains(imgUrl)) { log.warn("跳过重复图片 URL: {}", imgUrl); continue; } // 使用 Hutool 下载图片(带请求头) byte[] imageData = downloadImageWithHeaders(imgUrl); // 生成唯一文件名(使用时间戳 + UUID+ 图片 URL 的哈希) String urlHash = String.valueOf(Math.abs(imgUrl.hashCode())); String fileName = System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + "_" + urlHash + ".jpg"; File tempFile = new File(System.getProperty("java.io.tmpdir") + File.separator + fileName); FileUtil.writeBytes(imageData, tempFile); // 将 File 转换为 MultipartFile MultipartFile multipartFile = new ByteArrayMultipartFileUtil( fileName, fileName, FileUtil.getMimeType(fileName), imageData ); // 使用文件上传方式上传 UploadPictureResult uploadPictureResult = filePictureUpload.uploadPicture(multipartFile, uploadPathPrefix); // 删除临时文件 FileUtil.del(tempFile); // 构建图片信息 Picture picture = new Picture(); picture.setUrl(uploadPictureResult.getUrl()); picture.setPicSize(uploadPictureResult.getPicSize()); picture.setPicWidth(uploadPictureResult.getPicWidth()); picture.setPicHeight(uploadPictureResult.getPicHeight()); picture.setPicScale(uploadPictureResult.getPicScale()); picture.setPicFormat(uploadPictureResult.getPicFormat()); picture.setUserId(loginUser.getId()); // 处理名称:如果 namePrefix 不为空,使用 namePrefix + 数字,否则使用上传结果中的名称 nameCounter++; if (StrUtil.isNotBlank(pictureUploadByBatchRequest.getNamePrefix())) { picture.setName(pictureUploadByBatchRequest.getNamePrefix() + nameCounter); } else { picture.setName(uploadPictureResult.getPicName()); } picture.setIntroduction("批量上传-" + searchText); // 填充审核信息 fillReviewParams(picture, loginUser); // 保存图片 boolean save = this.save(picture); if (save) { successCount++; uploadedUrlSet.add(imgUrl); } } catch (Exception e) { log.warn("图片上传失败:" + imgUrl + ", 错误:" + e.getMessage()); continue; } } return successCount; } catch (Exception e) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "图片爬取失败:" + e.getMessage()); } } /** * 从 JSON 文本中提取图片 URL */ private List<String> extractImageUrls(String jsonText, int maxCount) { List<String> imgUrls = new ArrayList<>(); Set<String> urlSet = new HashSet<>(); // 优先提取 middleURL(质量较好) Pattern middlePattern = Pattern.compile("\"middleURL\":\"(.*?)\""); Matcher middleMatcher = middlePattern.matcher(jsonText); while (middleMatcher.find() && imgUrls.size() < maxCount) { String url = middleMatcher.group(1); if (isValidUrl(url) && urlSet.add(url)) { imgUrls.add(decodeUrl(url)); } } // 如果 middleURL 不够,补充 thumbURL if (imgUrls.size() < maxCount) { Pattern thumbPattern = Pattern.compile("\"thumbURL\":\"(.*?)\""); Matcher thumbMatcher = thumbPattern.matcher(jsonText); while (thumbMatcher.find() && imgUrls.size() < maxCount) { String url = thumbMatcher.group(1); if (isValidUrl(url) && urlSet.add(url)) { imgUrls.add(decodeUrl(url)); } } } // 如果还不够,补充 hoverURL if (imgUrls.size() < maxCount) { Pattern hoverPattern = Pattern.compile("\"hoverURL\":\"(.*?)\""); Matcher hoverMatcher = hoverPattern.matcher(jsonText); while (hoverMatcher.find() && imgUrls.size() < maxCount) { String url = hoverMatcher.group(1); if (isValidUrl(url) && urlSet.add(url)) { imgUrls.add(decodeUrl(url)); } } } log.info("成功提取 {} 个不重复的图片 URL", imgUrls.size()); return imgUrls; } /** * 验证 URL 是否有效 */ private boolean isValidUrl(String url) { return StrUtil.isNotBlank(url) && url.startsWith("http") && !url.contains("\\") && !url.contains("javascript"); } /** * URL 解码 */ private String decodeUrl(String url) { try { return URLDecoder.decode(url, StandardCharsets.UTF_8); } catch (Exception e) { log.warn("URL 解码失败:" + url, e); return url; } } /** * 带请求头下载图片(增强版) */ private byte[] downloadImageWithHeaders(String imageUrl) throws IOException { // 尝试多次下载,使用不同的 Referer String[] referers = { "https://image.baidu.com/", "https://www.baidu.com/", "https://baijiahao.baidu.com/" }; IOException lastException = null; for (String referer : referers) { try { cn.hutool.http.HttpResponse response = HttpUtil.createRequest(Method.GET, imageUrl) .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") .header("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8") .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") .header("Referer", referer) .header("Connection", "keep-alive") .timeout(15000) .execute(); if (response.getStatus() == 200) { return response.bodyBytes(); } } catch (Exception e) { // 继续尝试下一个 Referer log.warn("图片下载失败,正在尝试下一个 Referer:" + e); } } if (lastException != null) { throw lastException; } throw new IOException("下载失败:所有 Referer 都尝试过了"); }
下面还得用一个工具类(将 File 转换为 MultipartFile):
▼java复制代码/** * file 类转 MultipartFile */ public class ByteArrayMultipartFileUtil implements MultipartFile { private final String name; private final String originalFilename; @Nullable private final String contentType; private final byte[] content; public ByteArrayMultipartFileUtil(String name, String originalFilename, @Nullable String contentType, byte[] content) { Assert.hasLength(name, "Name must not be empty"); this.name = name; this.originalFilename = (originalFilename != null ? originalFilename : ""); this.contentType = contentType; this.content = content; } // 实现 MultipartFile 接口的方法 @Override public String getName() { return this.name; } @Override public String getOriginalFilename() { return this.originalFilename; } @Override @Nullable public String getContentType() { return this.contentType; } @Override public boolean isEmpty() { return this.content.length == 0; } @Override public long getSize() { return this.content.length; } @Override public byte[] getBytes() throws IOException { return this.content; } @Override public InputStream getInputStream() throws IOException { return new ByteArrayInputStream(this.content); } @Override public void transferTo(File dest) throws IOException, IllegalStateException { FileCopyUtils.copy(this.content, dest); } }
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
作者分享
✅ 今天做了:云图库第七期空间模块
⏰ 明天计划:第八期
📚 今日感悟:
今天看第七期的笔记的时候,也没做太多东西,就是新增空间,然后上传私人图片到私人空间中。
因为没写前端,直接拿的鱼总的,鱼总前端对按钮做了权限校验,我找了半天才看出来。浪费了挺长时间
晚上买了个域名,提交备案等日子了。😘
3
✅ 今天做了:云图库第5期(1/2)、第6期
⏰ 明天计划:第七期、第八期1/2
📚 今日感悟:
用解析URL的方式来替代了原来Jsoup解析DOM元素,
图片优化(上传、查询、加载、存储)
1
✅ 今天做了:云图库项目第四期,第五期 1/2,云图库项目面试题
⏰ 明天计划:云图库项目第五期 剩下的部分 + 第六期图片优化
📚 今日感悟:
本来还在犹豫要不要重新做一遍云图库的项目,本身前段时间写的时候,就是一点一点跟着教程学的,但是很慢,在家状态也一般,做完了云图库感觉还是浑浑噩噩的,重新再做一遍,感觉就不一样了,基本上就是一天两期内容,花大概一周就可以重新理解这个项目的精华了,比如模板方法设计模式,门面模式,AI扩图功能(异步任务 + 前端轮询),这个东西重新写一遍之后理解更深刻了,就可以写在简历上去被拷打了。
这个重新写一遍速度快了不少,还可以重新用AI优化下前端,然后重新部署下,把部署后的地址写到简历上算加分项吗😀
2
✅ 今天做了:优化了下AI零代码项目UI,接入了阿里云 ARMS 监控
⏰ 明天计划:看下Prometheus这个怎么玩
📚 今日感悟:今天终于闲下来了,看看小龙虾怎么玩😍
4
✅ 今天做了:AI 零代码第十一期 性能优化、实时优化
⏰ 明天计划:11期剩下的部分
📚 今日感悟:
项目到后面了,要着手背面试题了😭,加油
1
