Web爬虫返回列表与生成器与生产者/消费者

时间:2017-07-18 11:10:00

标签: python python-3.x generator python-multithreading

我想以递归方式抓取托管数千个文件的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}')

更新

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

  • 我有大约370k文件,但并非所有文件都可以进入下一步。在继续进行比较之前,我将对照集合或字典(以获得O(1)查找)检查它们并将它们与本地存储库进行比较
  • 经过多次测试后,看起来连续爬虫在大约4次尝试中花费的时间更少。发电机一次用的时间就少了。所以在这一点似乎发电机是好的
  • 此时,除了从队列中获取项目之外,消费者不会做任何事情,因为它是一个概念。但是,我对从生产者处获得的文件URL的处理方式具有灵活性。例如,我可以只下载前100KB的文件,在内存中计算它的校验和,然后与预先计算的本地版本进行比较。但很明显的是,如果简单地添加线程创建会使我的执行时间减少4到5倍,那么在消费者线程上添加工作将不会使它更快。

最后,我决定给生产者/消费者/队列一个镜头,一个简单的PoC运行4倍,同时加载100%的一个CPU核心。以下是简要代码(爬虫与上面的基于生成器的爬虫相同):

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. 当抓取工具仍然填充队列/列表/任何内容并因此使整个程序更快时,开始实际使用这些文件的最佳方法是什么?

1 个答案:

答案 0 :(得分:1)

  

1)使用线程库创建的线程是否实际上是线程,是否有办法在各种CPU内核之间实际分配?

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

  

2)我认为大量的性能下降来自生产者等待将项目放入队列。但这可以避免吗?

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

  

3)发生器是否较慢,因为它必须保存功能上下文然后反复加载?

生成器并不慢,对你正在处理的问题来说相当好,因为你找到了一个url,你把它放到队列中。

  

4)当爬虫仍在填充队列/列表/其他内容时,开始实际使用这些文件的最佳方法是什么,从而使整个程序更快?

创建一个ConsumerThread类,它从队列中获取数据(在您的情况下为url)并开始处理它。