You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用多线程优化递归爬取网站链接、搜索关键词的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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 01:06:08