Python如何在网络爬虫程序中使用多进程进行数据爬取
一、多进程基础
1.1 多进程程序
关于多进程相关的基础知识,已经在另一篇文章中有过详细描述,此处不再赘述。有需要的可以参考:
Python并发编程之multiprocessing模块中的进程
1.2 进程池
关于进程池相关的基础知识,可以参考另一篇文章, 此处不再赘述:
Python并发编程之进程池
二、在网络爬虫中使用多进程
2.1 需求背景
有一个图片网站,我已经可以成功提取到每一篇文章的url(即下文中的content_url), 然后针对每一篇文章(即每一个content_url), 我无法通过requests获取到完整的html代码,里面图片的url是动态加载的。然后我使用了selenium webdriver来访问每一篇文章(每一个content_url), 获取文章内所有图片的url, 然后进行下载。
说简单点就是,每一篇文章的处理,都是阻塞的(selenium加载网页+数据提取+图片下载)。这一篇文章处理完,才能处理下一篇文章。
2.2 从单进程版本入手
由于使用了Selenium webdriver,它的初始化以及相应的页面加载代码执行阶段所耗费的时间,再加上多个图片下载的时间,总的耗时其实是非常可观的。
单进程版本的爬虫程序,在遍历每一篇文章时,都需要执行Selenium webdriver的初始化->等待加载页面->数据提取->若干图片下载,也就是说,处理每一篇文章时,都会有一段可观的阻塞过程,而这个时间内,我们却不能做其他的事,只能干等。
代码片段如下:
# ContentWorker类负责处理每一篇文章
class ContentWorker():
def __init__(self, dir, content) -> None:
self.dir = dir
self.title = content['title']
self.num = content['num']
self.content_url = content['url']
self.count = 0
def run(self):
content_parser = ContentPageParser(self.content_url)
# 这里通过Selenium webdriver获取本篇文章内所有图片的url
content_parser.visit_content_page_with_firefox()
img_url_list = content_parser.get_img_src()
folder_name = self.title + '_' + self.num
path = os.path.join(self.dir, folder_name)
if os.path.isdir(path):
pass
else:
os.makedirs(path)
# 这里通过多线程下载所有图片
pic_download_threads = []
for img_url in img_url_list:
self.count += 1
file_name = 'img_' + str(self.count)
pic_download_threads.append(PicDownloader(str(self.count), img_url, file_name, path))
for working_thread in pic_download_threads:
working_thread.start()
for working_thread in pic_download_threads:
working_thread.join(3)
class Worker():
def __init__(self, dir) -> None:
self.dir = dir
def run(self):
home_parser = HomePageParser()
home_parser.parse_home_page()
all_content = home_parser.get_all_content() # 这里是获取到的每一篇文章的url
# 遍历每一篇文章
for content in all_content:
content_worker = ContentWorker(self.dir, content)
# 这个run函数里会调用Selenium webdriver来抓取所有图片的url,并使用多线程下载
content_worker.run()
2.3 将单进程版本改为多进程版本
那么如何将其改进为多进程版本呢?
首先我们定义一个函数content_worker_func(), 实现业务逻辑, 即原先ContentWorker类的run()所实现的业务逻辑。
def content_worker_func(dir, content):
title = content['title']
num = content['num']
content_url = content['url']
count = 0
content_parser = ContentPageParser(content_url)
content_parser.visit_content_page_with_firefox()
img_url_list = content_parser.get_img_src()
folder_name = title + '_' + num
path = os.path.join(dir, folder_name)
if os.path.isdir(path):
pass
else:
os.makedirs(path)
pic_download_threads = []
for img_url in img_url_list:
count += 1
file_name = 'img_' + str(count)
pic_download_threads.append(PicDownloader(str(count), img_url, file_name, path))
for working_thread in pic_download_threads:
working_thread.start()
for working_thread in pic_download_threads:
working_thread.join(3)
然后,我们需要定义一个进程池。这需要用到multiprocessing.Pool,
multiprocessing.Pool([numprocess [, initializer [, initargs]]])
numprocess是要创建的进程数。如果不指定,将使用cpu_count()的值。本例中没有指定,那么默认有几个cpu的core,就会创建几个进程。所以该程序在多核cpu上效果会比较明显。
然后调用进程池的apply_async方法,这将会在一个池工作进程中异步地执行content_worker_func(*args, **kwargs), 然后返回结果。
示例代码如下:
work_pool = multiprocessing.Pool()
for content in all_content:
work_pool.apply_async(content_worker_func, args=(self.dir, content))
2.4 将多进程应用到爬虫程序中
假设进程池的实例p
- p.close()
关闭进程池,防止进行进一步操作。如果所有操作持续挂起,它们将在工作进程终止之前完成。 - p.join()
等待所有工作进程退出。此方法只能在close()或terminate()方法之后调用。
示例代码:
class Worker():
def __init__(self, dir) -> None:
self.dir = dir
def start_work(self):
home_parser = HomePageParser()
home_parser.parse_home_page()
all_content = home_parser.get_all_content()
# 通过进程池实现并发,异步地处理每一篇文章,并下载其中的图片
work_pool = multiprocessing.Pool()
for content in all_content:
work_pool.apply_async(content_worker_func, args=(self.dir, content))
work_pool.close()
work_pool.join()
if __name__ == '__main__':
worker = Worker('d:test/mp1')
worker.start_work()
此时,多个进程可以并发地异步执行,每个进程内部可以启动自己的Selenium webdriver,并执行多线程图片下载。进程之间互不干扰。通过进程池和多线程的结合,总的图片爬取速度会得到极大飞跃。