网络爬虫返回列表 vs 生成器 vs producer/consumer

Web crawler returning list vs generator vs producer/consumer

我想递归地抓取托管数千个文件的 Web 服务器,然后检查它们是否与本地存储库中的不同(这是检查交付基础结构是否存在错误的一部分)。 到目前为止,我一直在尝试各种原型,这就是我注意到的。如果我做一个简单的递归并将所有文件放入一个列表中,操作将在大约 230 秒内完成。请注意,我每个目录只发出一个请求,因此实际下载我感兴趣的文件到别处是有意义的:

def recurse_links(base):
    result = []
    try:
        f = urllib.request.urlopen(base)
        soup = BeautifulSoup(f.read(), "html.parser")
        for anchor in soup.find_all('a'):
            href = anchor.get('href')
            if href.startswith('/') or href.startswith('..'):
                pass 
            elif href.endswith('/'):
                recurse_links(base + href)
            else:
                result.append(base + href)
    except urllib.error.HTTPError as httperr:
        print('HTTP Error in ' + base + ': ' + str(httperr))

我想,如果我可以在爬虫还在工作的时候开始处理我感兴趣的文件,我可以节省时间。所以我接下来尝试的是一个可以进一步用作协程的生成器。生成器用了 260 秒,稍长一些,但仍然可以接受。这是生成器:

def recurse_links_gen(base):
    try:
        f = urllib.request.urlopen(base)
        soup = BeautifulSoup(f.read(), "html.parser")
        for anchor in soup.find_all('a'):
            href = anchor.get('href')
            if href.startswith('/') or href.startswith('..'):
                pass
            elif href.endswith('/'):
                yield from recurse_links_gen(base + href)
            else:
                yield base + href
    except urllib.error.HTTPError as http_error:
        print(f'HTTP Error in {base}: {http_error}')

更新

回答评论区提出的一些问题:

最后我决定给 producer/consumer/queue 一个机会和一个简单的 PoC 运行 4 倍的时间,同时加载一个 CPU 内核的 100%。这是简短的代码(爬虫与上面的基于生成器的爬虫相同):

class ProducerThread(threading.Thread):
    def __init__(self, done_event, url_queue, crawler, name):
        super().__init__()
        self._logger = logging.getLogger(__name__)
        self.name = name
        self._queue = url_queue
        self._crawler = crawler
        self._event = done_event

    def run(self):
        for file_url in self._crawler.crawl():
            try:
                self._queue.put(file_url)
            except Exception as ex:
                self._logger.error(ex)

所以这是我的问题:

  1. 使用 threading 库创建的线程实际上是线程吗?有没有办法让它们实际分布在各种 CPU 内核之间?
  2. 我认为性能下降的主要原因是生产者等待将项目放入队列。但这可以避免吗?
  3. 生成器变慢是因为它必须保存函数上下文然后一遍又一遍地加载它吗?
  4. 在爬虫仍在填充 queue/list/whatever 时开始实际处理这些文件的最佳方法是什么,从而使整个程序更快?

1) Are the threads created with threading library actually threads and is there a way for them to be actually distributed between various CPU cores?

是的,这些是线程,但是要利用 CPU 的多个内核,您需要使用 multiprocessing 程序包。

2) I believe the great deal of performance degradation comes from the producer waiting to put an item into the queue. But can this be avoided?

这取决于您创建的线程数,原因之一可能是您的线程正在进行的上下文切换。线程的最佳值应该是2/3,即创建2/3线程并再次检查性能。

3) Is the generator slower because it has to save the function context and then load it again over and over?

生成器并不慢,它对您正在处理的问题相当有用,当您找到一个 url 时,您将其放入队列中。

4) What's the best way to start actually doing something with those files while the crawler is still populating the queue/list/whatever and thus make the whole program faster?

创建一个 ConsumerThread class,它从队列中获取数据(url 在你的例子中)并开始处理它。