如何使用Restful API与Spring实现多站点数据提取及并发调用?
问题解决指南
1. 前置依赖配置
首先在项目的pom.xml(Maven项目)中添加Jsoup依赖:
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.15.3</version> </dependency>
如果使用Lombok,可同时添加Lombok依赖简化日志声明。
2. 核心问题修复说明
2.1 log未定义问题
两种解决方案任选其一:
- 方式1(推荐):在控制器类上添加
@Slf4j注解(需要Lombok支持) - 方式2:在类中手动声明日志对象:
注意需要导入private static final Logger log = LoggerFactory.getLogger(APIController.class);org.slf4j.Logger和org.slf4j.LoggerFactory相关包。
2.2 成功/失败调用统计
在每个网页的抓取逻辑外层包裹try-catch块,并行场景下使用线程安全的原子类统计数值,避免并发数据错误。
2.3 Jsoup使用修正
原有代码硬编码了抓取地址,需要替换为循环遍历的website变量,页面元素选择器可根据目标站点的实际DOM结构调整,如需提取特定内容可通过浏览器F12开发者工具右键对应元素,复制CSS选择器传入select方法即可。
2.4 并行/串行调用实现
根据入参numberOfWebsites动态创建对应大小的线程池:入参为1时线程池大小为1即全串行执行,入参为4时线程池大小为4即全并行执行,搭配CountDownLatch等待所有任务执行完成后再聚合返回结果。
3. 完整实现代码
首先定义结构化返回实体类:
// 如需简化代码可添加@Data注解(Lombok) public class ScrapeResult { private int successCount; private int failCount; private List<String> extractedTitles; private List<String> extractedAddresses; // 此处省略getter、setter方法 }
控制器完整代码:
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.CopyOnWriteArrayList; @RestController public class APIController { // 解决log未定义问题 private static final Logger log = LoggerFactory.getLogger(APIController.class); private final List<String> websites = Arrays.asList( "https://pizzerijalimbo.si/meni/", "https://pizzerijalimbo.si/kontakt/", "https://pizzerijalimbo.si/my-account/", "https://pizzerijalimbo.si/o-nas/" ); @GetMapping("/podatki") public ScrapeResult getData(@RequestParam(required = true) int numberOfWebsites) throws InterruptedException { // 参数合法性校验 if (numberOfWebsites < 1 || numberOfWebsites > 4) { throw new IllegalArgumentException("参数范围必须为1-4"); } List<String> websitesToScrape = websites.subList(0, numberOfWebsites); // 线程安全的统计变量和结果集合 AtomicInteger successCount = new AtomicInteger(0); AtomicInteger failCount = new AtomicInteger(0); List<String> titles = new CopyOnWriteArrayList<>(); List<String> addresses = new CopyOnWriteArrayList<>(); // 按入参创建对应大小线程池,实现并行/串行控制 ExecutorService executor = Executors.newFixedThreadPool(numberOfWebsites); CountDownLatch latch = new CountDownLatch(websitesToScrape.size()); for (String website : websitesToScrape) { executor.submit(() -> { try { // Jsoup抓取页面,设置5秒超时 Document doc = Jsoup.connect(website).timeout(5000).get(); // 提取页面短标题 String title = doc.title(); log.info("抓取页面成功,标题:{}", title); titles.add(title); // 示例:提取联系页地址,可根据实际需求调整选择器 Elements addressElements = doc.select(".contact-content .address"); addressElements.forEach(ele -> addresses.add(ele.text().trim())); successCount.incrementAndGet(); } catch (IOException e) { log.error("抓取页面{}失败:{}", website, e.getMessage()); failCount.incrementAndGet(); } finally { latch.countDown(); } }); } // 最多等待30秒,避免死等 latch.await(30, TimeUnit.SECONDS); executor.shutdown(); // 组装返回结果 ScrapeResult result = new ScrapeResult(); result.setSuccessCount(successCount.get()); result.setFailCount(failCount.get()); result.setExtractedTitles(titles); result.setExtractedAddresses(addresses); return result; } }
4. 注意事项
- 可根据实际需求调整Jsoup的超时时间、请求头、User-Agent等参数,避免被站点反爬拦截
- 生产环境建议自定义线程池参数而非使用Executors默认实现,避免资源耗尽风险
- 页面元素选择器需要和目标站点当前DOM结构匹配,站点更新后需要同步调整选择器规则
内容的提问来源于stack exchange,提问作者AquaBalls
相关产品推荐
相关产品推荐

