前置说明
关于python多线程和多进程的说明,请参考如下:
这是我找到的两篇很棒的文章,里面详细说明的python多进程、多线程的原理以及用法,大家可以仔细看看
多进程爬虫例子
用一个实例说明下如何使用多进程进行爬虫
目标网站:https://imgbin.com/,本次爬取的也是一个图片网站,里面是一些透明背景图
1.首先看一下不添加多进程/多线程时的爬取速度,代码如下
#-*- coding:utf-8 -*-
importrequestsfrom requests.exceptions importRequestExceptionimportos, timefrom lxml importetreedefget_html(url):"""获取页面内容"""response= requests.get(url, timeout=15)#print(response.status_code)
try:if response.status_code == 200:#print(response.text)
returnresponse.textelse:returnNoneexceptRequestException:print("请求失败")#return None
defparse_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)
returnimg_srcelse:print("解析页面元素失败")defget_image_content(url):"""请求图片url,返回二进制内容"""
try:
r= requests.get(url, timeout=15)if r.status_code == 200:returnr.contentreturnNoneexceptRequestException:returnNonedef main(depth=None):"""主函数,下载图片
:param depth: 爬取页码
:return:"""j= 1base_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)
ifhtml:
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
exceptFileNotFoundError as e:print("遇到错误:", e)continue
exceptTypeError 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 importrequests3 from requests.exceptions importRequestException4 from bs4 importBeautifulSoup5 importbs46 importos, time7 from hashlib importmd58 from lxml importetree9 from multiprocessing importPool, Lock, cpu_count10
13 defget_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 returnresponse.text21 else:22 returnNone23 exceptRequestException:24 print("请求失败")25 #return None
26
27
28 defparse_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 returnimg_src35
36 else:37 print("解析页面元素失败")38
39 defget_all_image_url(page_number):40 """
41 获取所有图片的下载url42 :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 ifhtml:55 data = parse_html(html) #提取页面中的图片url
56 #print(data)
57 #time.sleep(3)
58 ifdata:59 for j indata:60 image_urls.append({61 'name': x,62 'value': j63 })64 x += 1 #每提取一个图片url,标识x增加1
65 exceptRequestException as f:66 print("遇到错误:", f)67 continue
68 #print(image_urls)
69 returnimage_urls70
71 defget_image_content(url):72 """请求图片url,返回二进制内容"""
73 #print("正在下载", url)
74 try:75 r = requests.get(url, timeout=15)76 if r.status_code == 200:77 returnr.content78 returnNone79 exceptRequestException:80 returnNone81
82 defmain(url, name):83 """
84 主函数:实现下载图片功能85 :param url: 图片url86 :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 exceptFileNotFoundError as f:102 print("第{}个文件下载时遇到错误,url为:{}:".format(name, url))103 print("报错:", f)104 raise
105
106 exceptTypeError 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 inurls: # 遍历列表中的每个图片下载url120 #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 importrequests3 from requests.exceptions importRequestException4 importos, time5 from lxml importetree6 importthreading7
8
9 defget_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 returnresponse.text17 else:18 returnNone19 exceptRequestException:20 print("请求失败")21 #return None
22
23
24 defparse_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 returnimg_src31
32 else:33 print("解析页面元素失败")34
35 defget_all_image_url(page_number):36 """
37 获取所有图片的下载url38 :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 ifhtml:51 data = parse_html(html) #提取页面中的图片url
52 #print(data)
53 #time.sleep(3)
54 ifdata:55 for j indata:56 image_urls.append({57 'name': x,58 'value': j59 })60 x += 1 #每提取一个图片url,标识x增加1
61 exceptRequestException as f:62 print("遇到错误:", f)63 continue
64 #print(image_urls)
65 returnimage_urls66
67 defget_image_content(url):68 """请求图片url,返回二进制内容"""
69 #print("正在下载", url)
70 try:71 r = requests.get(url, timeout=15)72 if r.status_code == 200:73 returnr.content74 returnNone75 exceptRequestException:76 returnNone77
78 defmain(url, image_name):79 """
80 主函数:实现下载图片功能81 :param url: 图片url82 :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 exceptFileNotFoundError as f:99 print("第{}个文件下载时遇到错误,url为:{}:".format(image_name, url))100 print("报错:", f)101 raise
102
103 exceptTypeError 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 inurls: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 inthread_list:122 m.start() #调用start()方法,开始执行
123
124 for m inthread_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的多线程与多进程,对比单线程爬虫效率有明显提高,更多细节请自行查看,网上有很多优质资料,这里就不细说了