Java 网络爬虫的多线程 DFS

Multithreaded DFS for web crawler in Java

我正在 Java 使用 Jsoup 编写网络爬虫。

目前我有一个使用深度优先搜索的单线程实现(它只需要抓取一个域,所以我可以选择 DFS 或 BFS,并选择 DFS,因为这意味着我可以使用队列代替的堆栈,因此在我做多线程版本时使用 LinkedBlockingQueue

我有一个 Queue 的 link 要访问和一个 HashSet 的已经访问过的 link,我的主循环弹出一个 link,访问页面,并将页面中任何未访问的 link 添加到队列中。

这是我的 class 实现我的单线程实现的内容(如果任何 throws 声明是虚假的,请告诉我原因,因为我需要掌握它)

private static LinkedBlockingQueue<String> URLSToCrawl = new LinkedBlockingQueue<String>();
private static String baseURL;
private static String HTTPSBaseURL;
private static HashSet<String> alreadyCrawledSet = new HashSet<String>();
private static List<String> deadLinks = new LinkedList<String>();

public static void main(String[] args) throws IOException, InterruptedException {

    // should output a site map, showing the static assets for each page. 

    Validate.isTrue(args.length == 1, "usage: supply url to fetch");

    baseURL = args[0];
    HTTPSBaseURL = baseURL.replace("http://", "https://");

    alreadyCrawledSet.add(baseURL);
    URLSToCrawl.add(baseURL);

    while (!URLSToCrawl.isEmpty() ) {
        String url = URLSToCrawl.take();
        crawlURL(url);
    }


}

private static void crawlURL(String url) throws IOException, InterruptedException {
    print("%s", url);
    try {
        Document doc = Jsoup.connect(url).get();
        Elements links = doc.select("a[href]");

        for (Element link : links) {
            String linkURL = link.attr("abs:href");
            if (sameDomain(linkURL) && !alreadyCrawled(linkURL)) {
                alreadyCrawledSet.add(linkURL);
                URLSToCrawl.put(linkURL);
            }
        }
    } catch (HttpStatusException e) {
        deadLinks.add(url);
    }
}   

private static boolean alreadyCrawled(String url) {
    if (alreadyCrawledSet.contains(url)) {
        return true;
    } else {
        return false;
    }
}

我想让这个多线程,以利用单线程实现必须在 Jsoup.connect(url).get() 调用 return 之前等待 HTTP 请求,然后再继续处理这一事实.我希望通过允许多个线程同时执行,一些工作将在此 I/O-bound 延迟期间完成,从而加快程序速度。

我对并发不是很有经验 - 我的第一个想法是简单地创建一个 Executor 并提交对 crawlURL 的每个调用。但我很困惑——我不知道如何确保以线程安全的方式访问我的 HashSetQueue,尤其是考虑到每个线程不仅 消耗 来自 Queue 的 URL,但也会将新 URL 推送到 Queue.

我了解原子性概念的基础知识,以及线程可以 "lock" 共享资源的想法,但我不知道如何在这种情况下将它们付诸实践。

有人对制作这个多线程有什么建议吗?

我的解决方案是一次处理一层图。因此,对于每个级别,将每个 link 提交给 ExecutorService 以进行爬网(多线程),然后等待该级别完成(使用 CountDownLatch),然后再进入下一个级别。

我使用 FixedThreadPool 作为速率限制的一种形式。

(最初我尝试只异步调度每个 url,这肯定更有效,但我不知道如何终止整个事情。)