多线程or多进程爬虫案例
前置说明
关于python多线程和多进程的说明,请参考如下:
https://zhuanlan.zhihu.com/p/46368084 (一位知乎用户)
https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064 (廖雪峰)
这是我找到的两篇很棒的文章,里面详细说明的python多进程、多线程的原理以及用法,大家可以仔细看看
多进程爬虫例子
用一个实例说明下如何使用多进程进行爬虫
目标网站:https://imgbin.com/,本次爬取的也是一个图片网站,里面是一些透明背景图
1.首先看一下不添加多进程/多线程时的爬取速度,代码如下
# -*- coding:utf-8 -*- import requests from requests.exceptions import RequestException import os, time from lxml import etree def get_html(url): """获取页面内容""" response = requests.get(url, timeout=15) # print(response.status_code) try: if response.status_code == 200: # print(response.text) return response.text else: return None except RequestException: print("请求失败") # return None def parse_html(html_text): """解析页面内容,提取图片url""" html = etree.HTML(html_text) if len(html) > 0: img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 # print(img_src) return img_src else: print("解析页面元素失败") def get_image_content(url): """请求图片url,返回二进制内容""" try: r = requests.get(url, timeout=15) if r.status_code == 200: return r.content return None except RequestException: return None def main(depth=None): """ 主函数,下载图片 :param depth: 爬取页码 :return: """ j = 1 base_url = 'https://imgbin.com/free-png/naruto/' # 定义初始url for i in range(1, depth): url = base_url + str(i) # 根据页码遍历请求url html = get_html(url) # 解析每个页面的内容 # print(html) if html: list_data = parse_html(html) # 提取页面中的图片url root_dir = os.path.dirname(os.path.abspath('.')) save_path = root_dir + '/pics/' # 定义保存路径 for t in list_data: # 遍历每个图片url try: file_path = '{0}{1}.{2}'.format(save_path, str(j), 'jpg') if not os.path.exists(file_path): # 判断是否存在文件,不存在则爬取 with open(file_path, 'wb') as f: f.write(get_image_content(t)) f.close() print('第{}个文件保存成功'.format(j)) else: print("第{}个文件已存在".format(j)) j = j + 1 except FileNotFoundError as e: print("遇到错误:", e) continue except TypeError as f: print("遇到错误:", f) continue else: print("无结果") if __name__ == '__main__': start = time.time() main(3) end = time.time() print(end-start)
测试了一下,晚上10点多,在当时的网速下,爬取2页图片,大概用了403s,并且下载失败了几张;
2.使用多进程爬取
如果要进行多进程爬取的话,必须要有一个准备并行执行的函数,既然要多进程爬取图片,所以应该把下载图片的功能定义为主函数
而上面代码中的main()函数不适合作为主函数,它是用爬取页码作为参数的,而我们并行执行时并不是一次爬取多页,而是并行爬取多个图片(有点绕)
需要改造一下:
(1)定义一个函数,来提取所有页面的图片url,并存到一个列表中(下面代码中的第39行:get_all_image_url()函数)
(2)定义一个主函数,接收图片url,然后下载图片(下面代码中的第82行:main()函数)
代码如下
1 # -*- coding:utf-8 -*- 2 import requests 3 from requests.exceptions import RequestException 4 from bs4 import BeautifulSoup 5 import bs4 6 import os, time 7 from hashlib import md5 8 from lxml import etree 9 from multiprocessing import Pool, Lock, cpu_count 10 13 def get_html(url): 14 response = requests.get(url, timeout=15) 15 # print(response.status_code) 16 try: 17 if response.status_code == 200: 18 19 # print(response.text) 20 return response.text 21 else: 22 return None 23 except RequestException: 24 print("请求失败") 25 # return None 26 27 28 def parse_html(html_text): 29 html = etree.HTML(html_text) 30 31 if len(html) > 0: 32 img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 33 # print(img_src) 34 return img_src 35 36 else: 37 print("解析页面元素失败") 38 39 def get_all_image_url(page_number): 40 """ 41 获取所有图片的下载url 42 :param page_number: 爬取页码 43 :return: 所有图片url的集合 44 """ 45 46 base_url = 'https://imgbin.com/free-png/naruto/' 47 image_urls = [] 48 49 x = 1 # 定义一个标识,用于给每个图片url编号,从1递增 50 for i in range(1, page_number): 51 url = base_url + str(i) # 根据页码遍历请求url 52 try: 53 html = get_html(url) # 解析每个页面的内容 54 if html: 55 data = parse_html(html) # 提取页面中的图片url 56 # print(data) 57 # time.sleep(3) 58 if data: 59 for j in data: 60 image_urls.append({ 61 'name': x, 62 'value': j 63 }) 64 x += 1 # 每提取一个图片url,标识x增加1 65 except RequestException as f: 66 print("遇到错误:", f) 67 continue 68 # print(image_urls) 69 return image_urls 70 71 def get_image_content(url): 72 """请求图片url,返回二进制内容""" 73 # print("正在下载", url) 74 try: 75 r = requests.get(url, timeout=15) 76 if r.status_code == 200: 77 return r.content 78 return None 79 except RequestException: 80 return None 81 82 def main(url, name): 83 """ 84 主函数:实现下载图片功能 85 :param url: 图片url 86 :param name: 图片名称 87 :return: 88 """ 89 save_path = os.path.dirname(os.path.abspath('.')) + '/pics/' 90 try: 91 file_path = '{0}/{1}.jpg'.format(save_path, name) 92 if not os.path.exists(file_path): # 判断是否存在文件,不存在则爬取 93 with open(file_path, 'wb') as f: 94 f.write(get_image_content(url)) 95 f.close() 96 97 print('第{}个文件保存成功'.format(name)) 98 else: 99 print("第{}个文件已存在".format(name)) 100 101 except FileNotFoundError as f: 102 print("第{}个文件下载时遇到错误,url为:{}:".format(name, url)) 103 print("报错:", f) 104 raise 105 106 except TypeError as e: 107 print("第{}个文件下载时遇到错误,url为:{}:".format(name, url)) 108 print("报错:", e) 109 110 111 if __name__ == '__main__': 112 start = time.time() 113 urls = get_all_image_url(3) # 获取所有图片url列表,爬取2页内容 114 # print(urls) 115 # print(cpu_count()) # 查看电脑是几核的 116 117 pool = Pool(6) # 我的电脑是6核的,所以开启6个线程试试 118 119 for t in urls: # 遍历列表中的每个图片下载url 120 # print(i) 121 pool.apply_async(main, args=(t["value"], t["name"])) # 使用apply_async函数实现多进程(并行请求url,下载图片) 122 123 pool.close() 124 pool.join() 125 126 end = time.time() 127 print(end-start)
开启了6个进程,晚上10点多,同样爬取2页内容,大概用了30s,速度提升还是挺明显的
多线程爬虫例子
看了开头分享的两篇文章后,应该了解到如下2点:
1、python解释器有GIL全局锁,导致多线程不能利用多核,多线程并发并不能在python中实现;
2、任务类型分为计算密集型和IO密集型,对于IO密集型任务,大部分时间都在等待IO操作完成,在等待时间中CPU是不需要工作的,即使提供多核CPU也利用不上
网络爬虫属于IO密集型任务,发送网络请求等待响应、把爬取图片保存到本地,很多时间都消耗在等待中,如果启动多线程会明显提高效率
改造一下上面的代码,由多进程爬虫改为多线程爬虫,如下
1 # -*- coding:utf-8 -*- 2 import requests 3 from requests.exceptions import RequestException 4 import os, time 5 from lxml import etree 6 import threading 7 8 9 def get_html(url): 10 response = requests.get(url, timeout=15) 11 # print(response.status_code) 12 try: 13 if response.status_code == 200: 14 15 # print(response.text) 16 return response.text 17 else: 18 return None 19 except RequestException: 20 print("请求失败") 21 # return None 22 23 24 def parse_html(html_text): 25 html = etree.HTML(html_text) 26 27 if len(html) > 0: 28 img_src = html.xpath("//img[@class='photothumb lazy']/@data-original") # 元素提取方法 29 # print(img_src) 30 return img_src 31 32 else: 33 print("解析页面元素失败") 34 35 def get_all_image_url(page_number): 36 """ 37 获取所有图片的下载url 38 :param page_number: 爬取页码 39 :return: 所有图片url的集合 40 """ 41 42 base_url = 'https://imgbin.com/free-png/naruto/' 43 image_urls = [] 44 45 x = 1 # 定义一个标识,用于给每个图片url编号,从1递增 46 for i in range(1, page_number): 47 url = base_url + str(i) # 根据页码遍历请求url 48 try: 49 html = get_html(url) # 解析每个页面的内容 50 if html: 51 data = parse_html(html) # 提取页面中的图片url 52 # print(data) 53 # time.sleep(3) 54 if data: 55 for j in data: 56 image_urls.append({ 57 'name': x, 58 'value': j 59 }) 60 x += 1 # 每提取一个图片url,标识x增加1 61 except RequestException as f: 62 print("遇到错误:", f) 63 continue 64 # print(image_urls) 65 return image_urls 66 67 def get_image_content(url): 68 """请求图片url,返回二进制内容""" 69 # print("正在下载", url) 70 try: 71 r = requests.get(url, timeout=15) 72 if r.status_code == 200: 73 return r.content 74 return None 75 except RequestException: 76 return None 77 78 def main(url, image_name): 79 """ 80 主函数:实现下载图片功能 81 :param url: 图片url 82 :param image_name: 图片名称 83 :return: 84 """ 85 print('当前子线程: {}'.format(threading.current_thread().name)) 86 save_path = os.path.dirname(os.path.abspath('.')) + '/pics/' 87 try: 88 file_path = '{0}/{1}.jpg'.format(save_path, image_name) 89 if not os.path.exists(file_path): # 判断是否存在文件,不存在则爬取 90 with open(file_path, 'wb') as f: 91 f.write(get_image_content(url)) 92 f.close() 93 94 print('第{}个文件保存成功'.format(image_name)) 95 else: 96 print("第{}个文件已存在".format(image_name)) 97 98 except FileNotFoundError as f: 99 print("第{}个文件下载时遇到错误,url为:{}:".format(image_name, url)) 100 print("报错:", f) 101 raise 102 103 except TypeError as e: 104 print("第{}个文件下载时遇到错误,url为:{}:".format(image_name, url)) 105 print("报错:", e) 106 107 108 if __name__ == '__main__': 109 start = time.time() 110 print('这是主线程:{}'.format(threading.current_thread().name)) 111 112 urls = get_all_image_url(3) # 获取所有图片url列表 113 thread_list = [] # 定义一个列表,向里面追加线程 114 115 for t in urls: 116 # print(i) 117 m = threading.Thread(target=main, args=(t["value"], t["name"])) # 调用threading.Thread方法,传入主函数及其参数,启动多线程 118 119 thread_list.append(m) 120 121 for m in thread_list: 122 m.start() # 调用start()方法,开始执行 123 124 for m in thread_list: 125 m.join() # 子线程调用join()方法,使主线程等待子线程运行完毕之后才退出 126 127 128 end = time.time() 129 print(end-start)
同样爬取2页,因为有100张图片,所以一共启动了100个子线程,耗时大约6.5s
如果打开文件夹来看的话,图片是一下子都出现的
通过对比,可以看到对于网络爬虫这种IO密集型任务,多线程的效率其实是比多进程高的(6.5s VS 29.9s)
小结:本篇通过一个图片爬虫实例来说了一下如何使用python的多线程与多进程,对比单线程爬虫效率有明显提高,更多细节请自行查看,网上有很多优质资料,这里就不细说了