如何用多线程优化递归爬取网站链接、搜索关键词的Java程序
多线程递归爬取实现方案
你提到的Fork/Join框架非常适配这个分层递归爬取的场景,它可以自动将每层爬取产生的子任务分配给空闲线程执行,充分利用CPU资源提升爬取速度,具体实现步骤如下:
1. 先解决基础线程安全问题
- 原有
HashSet是非线程安全的,多线程下执行contains、add操作会出现竞态条件,需要替换为线程安全的ConcurrentHashMap.newKeySet()来存储已爬取链接 - 匹配到关键词的链接建议存储到
CopyOnWriteArrayList这类线程安全集合中,后续更新GUI时再按对应GUI框架的线程规则更新界面(比如Swing需要用SwingUtilities.invokeLater调度到UI线程执行更新)
2. Fork/Join版本代码实现
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; import java.util.Set; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.RecursiveAction; import java.util.concurrent.ForkJoinPool; public class CrawlerTask extends RecursiveAction { private final String url; private final Set<String> visitedUrls; private final String keyword; private final int depth; private static final int MAX_DEPTH = 3; // 存储匹配到的结果,供GUI展示用 public static final List<String> matchedUrls = new CopyOnWriteArrayList<>(); public CrawlerTask(String url, Set<String> visitedUrls, String keyword, int depth) { this.url = url; this.visitedUrls = visitedUrls; this.keyword = keyword; this.depth = depth; } @Override protected void compute() { // 匹配关键词逻辑 if (url.contains(keyword)) { System.out.println("Found: " + url); matchedUrls.add(url); // 如果是Swing GUI,这里可以加 SwingUtilities.invokeLater(() -> 更新界面逻辑) } // 去重判断,双重校验避免重复爬取 if (visitedUrls.contains(url)) { return; } synchronized (visitedUrls) { if (visitedUrls.contains(url)) { return; } visitedUrls.add(url); } // 递归深度判断 if (depth >= MAX_DEPTH) { return; } try { // 加超时避免线程长时间阻塞 Document doc = Jsoup.connect(url).timeout(5000).get(); Elements links = doc.select("a[href]"); // 为每个子链接创建子任务 for (Element element : links) { String subUrl = element.absUrl("href"); System.out.println(subUrl); CrawlerTask subTask = new CrawlerTask(subUrl, visitedUrls, keyword, depth + 1); // 异步提交子任务 subTask.fork(); } } catch (IOException e) { // 可以根据需求调整异常处理逻辑,避免单个链接爬取失败影响整个任务 e.printStackTrace(); } } public static void main(String[] args) { Set<String> visitedUrls = ConcurrentHashMap.newKeySet(); String keyword = "root"; // 配置并行度,可根据实际需求调整,避免线程过多触发反爬 ForkJoinPool forkJoinPool = new ForkJoinPool(10); CrawlerTask rootTask = new CrawlerTask("https://example.com", visitedUrls, keyword, 0); // 执行根任务,阻塞等待所有任务完成 forkJoinPool.invoke(rootTask); forkJoinPool.shutdown(); // 所有任务执行完成后可以在这里统一处理结果,或者实时更新GUI System.out.println("所有匹配结果:" + matchedUrls); } }
3. 额外优化建议
- 可以给每个爬取请求加随机延迟,避免短时间内请求过多被目标网站封禁
- 可以增加无效链接过滤逻辑,比如过滤掉图片、视频、压缩包等非网页资源的链接,减少无效请求
- 如果爬取的站点数量多,可以考虑给不同域名分配独立的请求频率限制
内容的提问来源于stack exchange,提问作者vvooki.
相关产品推荐
相关产品推荐

