网络爬虫返回列表 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}')
更新
回答评论区提出的一些问题:
- 我有大约 370k 个文件,但并非所有文件都能进入下一步。在继续之前,我将根据集合或字典检查它们(以获取 O(1) 查找)并将它们与本地 repo
进行比较
- 经过更多测试后,顺序爬虫似乎在 5 次尝试中大约有 4 次花费的时间更少。并且生成器一次花费的时间更少。所以在这一点上发电机似乎没问题
- 此时,消费者除了从队列中获取项目外什么都不做,因为这是一个概念。但是,我可以灵活地处理从制作人那里获得的文件 URL。例如,我可以只下载前 100KB 的文件,在内存中计算它的校验和,然后与预先计算的本地版本进行比较。但很清楚的是,如果简单地添加线程创建会使我的执行时间增加 4 到 5 倍,那么在消费者线程上添加工作不会使其更快。
最后我决定给 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)
所以这是我的问题:
- 使用
threading
库创建的线程实际上是线程吗?有没有办法让它们实际分布在各种 CPU 内核之间?
- 我认为性能下降的主要原因是生产者等待将项目放入队列。但这可以避免吗?
- 生成器变慢是因为它必须保存函数上下文然后一遍又一遍地加载它吗?
- 在爬虫仍在填充 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 在你的例子中)并开始处理它。
我想递归地抓取托管数千个文件的 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) 查找)并将它们与本地 repo 进行比较
- 经过更多测试后,顺序爬虫似乎在 5 次尝试中大约有 4 次花费的时间更少。并且生成器一次花费的时间更少。所以在这一点上发电机似乎没问题
- 此时,消费者除了从队列中获取项目外什么都不做,因为这是一个概念。但是,我可以灵活地处理从制作人那里获得的文件 URL。例如,我可以只下载前 100KB 的文件,在内存中计算它的校验和,然后与预先计算的本地版本进行比较。但很清楚的是,如果简单地添加线程创建会使我的执行时间增加 4 到 5 倍,那么在消费者线程上添加工作不会使其更快。
最后我决定给 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)
所以这是我的问题:
- 使用
threading
库创建的线程实际上是线程吗?有没有办法让它们实际分布在各种 CPU 内核之间? - 我认为性能下降的主要原因是生产者等待将项目放入队列。但这可以避免吗?
- 生成器变慢是因为它必须保存函数上下文然后一遍又一遍地加载它吗?
- 在爬虫仍在填充 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 在你的例子中)并开始处理它。