leetcode
leetcode 1151 ~ 1200
多线程网页爬虫

多线程网页爬虫

难度:

标签:

题目描述

代码结果

运行时间: 206 ms, 内存: 0.0 MB


/**
 * Leetcode 1242: Web Crawler Multithreaded
 * Problem statement: Given a start URL and an interface HtmlParser, implement a multithreaded web crawler to visit all URLs from the same domain.
 * 
 * Approach:
 * 1. Use a ConcurrentHashMap to store visited URLs for thread safety.
 * 2. Use a ForkJoinPool to parallelize the crawling process.
 * 3. For each URL, we create a task that fetches the URLs from the page.
 * 4. Filter and add new URLs from the same domain to the pool for further processing.
 */

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
import java.util.stream.Collectors;

class WebCrawlerStream {
    private Set<String> visited = ConcurrentHashMap.newKeySet();
    private HtmlParser parser;
    private String startHost;

    public WebCrawlerStream(HtmlParser parser) {
        this.parser = parser;
    }

    public void startCrawling(String startUrl) {
        startHost = getHostName(startUrl);
        visited.add(startUrl);
        ForkJoinPool.commonPool().invoke(new CrawlTask(startUrl));
    }

    private class CrawlTask extends RecursiveAction {
        private String url;

        CrawlTask(String url) {
            this.url = url;
        }

        @Override
        protected void compute() {
            if (getHostName(url).equals(startHost)) {
                parser.getUrls(url).stream()
                    .filter(nextUrl -> getHostName(nextUrl).equals(startHost))
                    .filter(visited::add)
                    .map(CrawlTask::new)
                    .collect(Collectors.toList())
                    .forEach(ForkJoinPool.commonPool()::invoke);
            }
        }
    }

    private String getHostName(String url) {
        int idx = url.indexOf('/', 7); // Skip the 'http://' part
        return idx == -1 ? url : url.substring(0, idx);
    }
}

解释

方法:

该题解采用了多线程来加速网页爬虫的过程。首先,定义了一个帮助函数 `get_hostname` 用来从 URL 中提取主机名。在主函数 `crawl` 中,首先使用 `get_hostname` 得到初始 URL 的主机名。然后,使用一个集合 `seen` 来跟踪已经访问过的 URL,避免重复访问。接下来,使用列表 `to_visit` 来存储当前层待访问的 URL,并使用列表 `res` 来存储所有被访问过的 URL。对于 `to_visit` 中的每个 URL,使用线程池 `ThreadPoolExecutor` 来并行获取每个 URL 链接到的新 URL。对于每个新 URL,如果它未被访问过且与起始 URL 属于同一主机,就将其添加到 `seen`,`res`,以及下一轮的 `to_visit` 中。这个过程重复进行,直到没有新的 URL 可以访问。

时间复杂度:

O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在多线程网页爬虫中,如何确保对`seen`集合的访问是线程安全的?
在多线程环境中,确保对`seen`集合的访问是线程安全的,可以采用几种策略。一种是使用锁(如`threading.Lock()`)来控制对`seen`的访问,确保在同一时间只有一个线程可以修改`seen`集合。另一种方法是使用线程安全的数据结构,如`queue.Queue`或`concurrent.futures`中的`ConcurrentHashMap`等。在提供的代码中,可以通过在修改`seen`集合前获取一个锁,并在修改完成后释放该锁来实现线程安全。
🦆
你选择使用`ThreadPoolExecutor`的`max_workers`参数设为16的依据是什么?是否有可能因为过多线程而导致资源竞争增加?
选择`ThreadPoolExecutor`的`max_workers`参数设为16通常基于系统的CPU核心数和预期的I/O等待时间。选择适当的线程数可以使得CPU和I/O资源得到有效利用,而不至于造成过度的线程切换和资源争抢。过多线程确实可能导致资源竞争增加,特别是当线程数远大于处理器核心数时。在实际应用中,应该根据目标系统的具体配置和任务特性来调整`max_workers`的值,可能需要通过实验来找到最优的线程数。
🦆
在爬虫过程中,`to_visit`列表是如何保证不重复添加相同的URL的,特别是在多线程环境下?
在多线程环境下,确保`to_visit`列表不重复添加相同的URL需要依赖于`seen`集合的线程安全操作。在每个线程中解析URL前,应先检查该URL是否已存在于`seen`中,这一检查和添加操作应当是原子的,即在获取到锁之后进行检查并添加操作,然后再释放锁。这样可以确保不会有多个线程同时将同一个URL添加到`to_visit`列表中。

相关问题