python 高性能异步爬虫线程&线程池

爬虫本质

其实爬虫的本质就是Client发请求批量获取Server的响应数据,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行,那只能等待爬取一个结束后才能继续下一个,效率会非常低。需要强调的是:对于单线程下串行N个任务,并不完全等同于低效,如果这N个任务都是纯计算的任务,那么该线程对CPU的利用率仍然会很高,之所以单线程下串行多个爬虫任务低效,是因为爬虫任务是明显的IO密集型(阻塞)程序。那么该如何提高爬取性能呢?

同步调用

即提交一个任务后就在原地等待任务结束,等到拿到任务的结果后再继续下一行代码,效率低下

import requests
def parse_page(res):
    print('解析 %s' %(len(res)))
def get_page(url):
    print('下载 %s' %url)
    response=requests.get(url)
    if response.status_code == 200:
        return response.text
urls = [
    'http://v.stu.126.net/mooc-video/nos/mp4/2017/02/28/1005853348_f171329df9a543528f1d3661025dafb4_shd.mp4?ak=99ed7479ee303d1b1361b0ee5a4abcee533c409d7b6d3ba248b25062b105fb8a0daa0c5df5b2483dc54be2b27e62827d917f197453fd5e721b09e15b813d89270015e48ffc49c659b128bfe612dda086d65894b8ef217f1626539e3c9eb40879c29b730d22bdcadb1b4f67996129275fa4c38c6336120510aea1ae1790819de86e0fa3e09eeabea1b068b3d9b9b6597acf0c219eb000a69c12ce9d568813365b3e099fcdb77c69ca7cd6141d92c122af',
    'http://v.stu.126.net/mooc-video/nos/mp4/2017/02/28/1005857381_359806fd2b7743459a756c23a5bc74f5_shd.mp4?ak=99ed7479ee303d1b1361b0ee5a4abcee533c409d7b6d3ba248b25062b105fb8a9bac4ab36b09a42f7cfb2ae827a13bbc0dcfecf63835960a43d311794f003b570015e48ffc49c659b128bfe612dda086d65894b8ef217f1626539e3c9eb40879c29b730d22bdcadb1b4f67996129275fa4c38c6336120510aea1ae1790819de86e0fa3e09eeabea1b068b3d9b9b6597acf0c219eb000a69c12ce9d568813365b3e099fcdb77c69ca7cd6141d92c122af',
    'http://v.stu.126.net/mooc-video/nos/mp4/2017/02/28/1005857376_8adb233ed5f447618ad06eec04de1c72_shd.mp4?ak=99ed7479ee303d1b1361b0ee5a4abcee533c409d7b6d3ba248b25062b105fb8a20e529816fcf5545ed862ea3625aba274af0bdd7a1c1a8142b45237805f97b2f0015e48ffc49c659b128bfe612dda086d65894b8ef217f1626539e3c9eb40879c29b730d22bdcadb1b4f67996129275fa4c38c6336120510aea1ae1790819de86e0fa3e09eeabea1b068b3d9b9b6597acf0c219eb000a69c12ce9d568813365b3e099fcdb77c69ca7cd6141d92c122af'
]
for url in urls:
    res=get_page(url) #调用一个任务,就在原地等待任务结束拿到结果后才继续往后执行
    parse_page(res)

解决同步调用方案之多线程/多进程(不建议使用)
1.好处:在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
2.弊端:开启多进程或都线程的方式,我们是无法无限制地开启多进程或多线程的:在遇到要同时处理成百上千个的连接请求时,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。
解决同步调用方案之线程/进程池(适当使用)
1.好处:很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。可以很好的降低系统开销。
2.弊端:“线程池”和“连接池”技术也只是在一定程度上缓解了频繁创建和销毁线程带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

对比同步和使用线程池的执行效率

同步执行

#同步执行
import time
def sayhello(str):
    print("Hello ",str)
    time.sleep(2)
name_list =['code_community','aa','bb','cc']
start_time = time.time()
for i in range(len(name_list)):
    sayhello(name_list[i])
print('%d second'% (time.time()-start_time))

返回结果:
Hello code_community
Hello aa
Hello bb
Hello cc
8 second

异步基于线程池

#异步基于线程池
import time
from multiprocessing.dummy import Pool
def sayhello(str):
    print("Hello ",str)
    time.sleep(2)
start = time.time()
name_list =['code_community','aa','bb','cc']
#实例化线程池对象,开启了4个线程
pool = Pool(4)
pool.map(sayhello,name_list)
pool.close()
pool.join()
end = time.time()
print(end-start)

返回结果:
Hello code_community
Hello aa
Hello bb
Hello cc
2.0805933475494385

基于multiprocessing.dummy线程池爬取梨视频的视频信息

import requests
import random
from lxml import etree
import re
from fake_useragent import UserAgent
#安装fake-useragent库:pip install fake-useragent
#导入线程池模块
from multiprocessing.dummy import Pool
#实例化线程池对象
pool = Pool()
url = 'http://www.pearvideo.com/category_1'
#随机产生UA
ua = UserAgent().random
headers = {
    'User-Agent':ua
}
#获取首页页面数据
page_text = requests.get(url=url,headers=headers).text
#对获取的首页页面数据中的相关视频详情链接进行解析
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@id="listvideoList"]/ul/li')
detail_urls = []#存储二级页面的url
for li in li_list:
    detail_url = 'http://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
    title = li.xpath('.//div[@class="vervideo-title"]/text()')[0]
    detail_urls.append(detail_url)
vedio_urls = []#存储视频的url
for url in detail_urls:
    page_text = requests.get(url=url,headers=headers).text
    vedio_url = re.findall('srcUrl="(.*?)"',page_text,re.S)[0]
    vedio_urls.append(vedio_url)
#使用线程池进行视频数据下载
func_request = lambda link:requests.get(url=link,headers=headers).content
video_data_list = pool.map(func_request,vedio_urls)
#使用线程池进行视频数据保存
func_saveData = lambda data:save(data)
pool.map(func_saveData,video_data_list)
def save(data):
    fileName = str(random.randint(1,10000))+'.mp4'
    with open(fileName,'wb') as fp:
        fp.write(data)
        print(fileName+'已存储')
pool.close()
pool.join()

总结
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

原文地址:https://www.cnblogs.com/gqv2009/p/14986716.html