小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

快速提升爬蟲性能的幾種方法

 達(dá)坂城大豆 2018-01-29

作者:孟慶健

來源:http://www.cnblogs.com/mengqingjian/p/8329651.html

一、背景知識

爬蟲的本質(zhì)就是一個socket客戶端與服務(wù)端的通信過程,如果我們有多個url待爬取,只用一個線程且采用串行的方式執(zhí)行,

那只能等待爬取一個結(jié)束后才能繼續(xù)下一個,效率會非常低。需要強(qiáng)調(diào)的是:對于單線程下串行N個任務(wù),并不完全等同于低效。

如果這N個任務(wù)都是純計算的任務(wù),那么該線程對cpu的利用率仍然會很高,之所以單線程下串行多個爬蟲任務(wù)低效, 是因為爬蟲任務(wù)是明顯的IO密集型程序。

關(guān)于IO模型詳見鏈接:http://www.cnblogs.com/linhaifeng/articles/7454717.html

那么該如何提高爬取性能呢?且看下述概念。

二、同步、異步、回調(diào)機(jī)制

*1、同步調(diào)用:即提交一個任務(wù)后就在原地等待任務(wù)結(jié)束,等到拿到任務(wù)的結(jié)果后再繼續(xù)下一行代碼,效率低下*

  1. import requests

  2. def parse_page(res):

  3.    print('解析 %s' %(len(res)))

  4. def get_page(url):

  5.    print('下載 %s' %url)

  6.    response=requests.get(url)

  7.    if response.status_code == 200:

  8.        return response.text

  9. urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.']

  10. for url in urls:

  11.    res=get_page(url) #調(diào)用一個任務(wù),就在原地等待任務(wù)結(jié)束拿到結(jié)果后才繼續(xù)往后執(zhí)行

  12.    parse_page(res)

2、一個簡單的解決方案:多線程或多進(jìn)程

在服務(wù)器端使用多線程(或多進(jìn)程)。

多線程(或多進(jìn)程)的目的是讓每個連接都擁有獨立的線程(或進(jìn)程),這樣任何一個連接的阻塞都不會影響其他的連接。

  1. #IO密集型程序應(yīng)該用多線程

  2. import requests

  3. from threading import Thread,current_thread

  4. def parse_page(res):

  5.    print('%s 解析 %s' %(current_thread().getName(),len(res)))

  6. def get_page(url,callback=parse_page):

  7.    print('%s 下載 %s' %(current_thread().getName(),url))

  8.    response=requests.get(url)

  9.    if response.status_code == 200:

  10.        callback(response.text)

  11. if __name__ == '__main__':

  12.    urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.']

  13.    for url in urls:

  14.        t=Thread(target=get_page,args=(url,))

  15.        t.start()

該方案的問題是:

開啟多進(jìn)程或都線程的方式,我們是無法無限制地開啟多進(jìn)程或多線程的:在遇到要同時響應(yīng)成百上千路的連接請求,則無論多線程還是多進(jìn)程都會嚴(yán)重占據(jù)系統(tǒng)資源,降低系統(tǒng)對外界響應(yīng)效率,

而且線程與進(jìn)程本身也更容易進(jìn)入假死狀態(tài)。

3、改進(jìn)方案

線程池或進(jìn)程池+異步調(diào)用:提交一個任務(wù)后并不會等待任務(wù)結(jié)束,而是繼續(xù)下一行代碼**

很多程序員可能會考慮使用'線程池'或'連接池'。'線程池'旨在減少創(chuàng)建和銷毀線程的頻率,其維持一定合理數(shù)量的線程,并讓空閑的線程重新承擔(dān)新的執(zhí)行任務(wù)。'連接池'維持連接的緩存池,盡量重用已有的連接、減少創(chuàng)建和關(guān)閉連接的頻率。這兩種技術(shù)都可以很好的降低系統(tǒng)開銷,都被廣泛應(yīng)用很多大型系統(tǒng),如websphere、tomcat和各種數(shù)據(jù)庫等。

  1. #IO密集型程序應(yīng)該用多線程,所以此時我們使用線程池

  2. import requests

  3. from threading import current_thread

  4. from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

  5. def parse_page(res):

  6.    res=res.result()

  7.    print('%s 解析 %s' %(current_thread().getName(),len(res)))

  8. def get_page(url):

  9.    print('%s 下載 %s' %(current_thread().getName(),url))

  10.    response=requests.get(url)

  11.    if response.status_code == 200:

  12.        return response.text

  13. if __name__ == '__main__':

  14.    urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.']

  15.    pool=ThreadPoolExecutor(50)

  16.    # pool=ProcessPoolExecutor(50)

  17.    for url in urls:

  18.        pool.submit(get_page,url).add_done_callback(parse_page)

  19.    pool.shutdown(wait=True)

改進(jìn)后方案其實也存在著問題:

'線程池'和'連接池'技術(shù)也只是在一定程度上緩解了頻繁調(diào)用IO接口帶來的資源占用。而且,所謂'池'始終有其上限,當(dāng)請求大大超過上限時,'池'構(gòu)成的系統(tǒng)對外界的響應(yīng)并不比沒有池的時候效果好多少。所以使用'池'必須考慮其面臨的響應(yīng)規(guī)模,并根據(jù)響應(yīng)規(guī)模調(diào)整'池'的大小。

對應(yīng)上例中的所面臨的可能同時出現(xiàn)的上千甚至上萬次的客戶端請求,'線程池'或'連接池'或許可以緩解部分壓力,但是不能解決所有問題。

總之,多線程模型可以方便高效的解決小規(guī)模的服務(wù)請求,但面對大規(guī)模的服務(wù)請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。**

三 、高性能

上述無論哪種解決方案其實沒有解決一個性能相關(guān)的問題:IO阻塞,無論是多進(jìn)程還是多線程,在遇到IO阻塞時都會被操作系統(tǒng)強(qiáng)行剝奪走CPU的執(zhí)行權(quán)限,程序的執(zhí)行效率因此就降低了下來。

解決這一問題的關(guān)鍵在于,我們自己從應(yīng)用程序級別檢測IO阻塞,然后切換到我們自己程序的其他任務(wù)執(zhí)行,這樣把我們程序的IO降到最低,我們的程序處于就緒態(tài)就會增多,以此來迷惑操作系統(tǒng),操作系統(tǒng)便以為我們的程序是IO比較少的程序,從而會盡可能多的分配CPU給我們,這樣也就達(dá)到了提升程序執(zhí)行效率的目的**。

1、在python3.3之后新增了asyncio模塊,可以幫我們檢測IO(只能是網(wǎng)絡(luò)IO),實現(xiàn)應(yīng)用程序級別的切換

  1. import asyncio

  2. @asyncio.coroutine

  3. def task(task_id,senconds):

  4.    print('%s is start' %task_id)

  5.    yield from asyncio.sleep(senconds) #只能檢測網(wǎng)絡(luò)IO,檢測到IO后切換到其他任務(wù)執(zhí)行

  6.    print('%s is end' %task_id)

  7. tasks=[task(task_id='任務(wù)1',senconds=3),task('任務(wù)2',2),task(task_id='任務(wù)3',senconds=1)]

  8. loop=asyncio.get_event_loop()

  9. loop.run_until_complete(asyncio.wait(tasks))

  10. loop.close()

2、但asyncio模塊只能發(fā)tcp級別的請求,不能發(fā)http協(xié)議,因此,在我們需要發(fā)送http請求的時候,需要我們自定義http報頭。

  1. import asyncio

  2. import requests

  3. import uuid

  4. user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'

  5. def parse_page(host,res):

  6.    print('%s 解析結(jié)果 %s' %(host,len(res)))

  7.    with open('%s.html' %(uuid.uuid1()),'wb') as f:

  8.        f.write(res)

  9. @asyncio.coroutine

  10. def get_page(host,port=80,url='/',callback=parse_page,ssl=False):

  11.    print('下載 http://%s:%s%s' %(host,port,url))

  12.    #步驟一(IO阻塞):發(fā)起tcp鏈接,是阻塞操作,因此需要yield from

  13.    if ssl:

  14.        port=443

  15.    recv,send=yield from asyncio.open_connection(host=host,port=443,ssl=ssl)

  16.    # 步驟二:封裝http協(xié)議的報頭,因為asyncio模塊只能封裝并發(fā)送tcp包,因此這一步需要我們自己封裝http協(xié)議的包

  17.    request_headers='''GET %s HTTP/1.0rnHost: %srnUser-agent: %srnrn''' %(url,host,user_agent)

  18.    # requset_headers='''POST %s HTTP/1.0rnHost: %srnrnname=egon&password=123''' % (url, host,)

  19.    request_headers=request_headers.encode('utf-8')

  20.    # 步驟三(IO阻塞):發(fā)送http請求包

  21.    send.write(request_headers)

  22.    yield from send.drain()

  23.    # 步驟四(IO阻塞):接收響應(yīng)頭

  24.    while True:

  25.        line=yield from recv.readline()

  26.        if line == b'rn':

  27.            break

  28.        print('%s Response headers:%s' %(host,line))

  29.    # 步驟五(IO阻塞):接收響應(yīng)體

  30.    text=yield from recv.read()

  31.    # 步驟六:執(zhí)行回調(diào)函數(shù)

  32.    callback(host,text)

  33.    # 步驟七:關(guān)閉套接字

  34.    send.close() #沒有recv.close()方法,因為是四次揮手?jǐn)噫溄樱p向鏈接的兩端,一端發(fā)完數(shù)據(jù)后執(zhí)行send.close()另外一端就被動地斷開

  35. if __name__ == '__main__':

  36.    tasks=[

  37.        get_page('www.baidu.com',url='/s?wd=美女',ssl=True),

  38.        get_page('www.cnblogs.com',url='/',ssl=True),

  39.    ]

  40.    loop=asyncio.get_event_loop()

  41.    loop.run_until_complete(asyncio.wait(tasks))

  42.    loop.close()

3、自定義http報頭多少有點麻煩,于是有了aiohttp模塊,專門幫我們封裝http報頭,然后我們還需要用asyncio檢測IO實現(xiàn)切換。

  1. import aiohttp

  2. import asyncio

  3. @asyncio.coroutine

  4. def get_page(url):

  5.    print('GET:%s' %url)

  6.    response=yield from aiohttp.request('GET',url)

  7.    data=yield from response.read()

  8.    print(url,data)

  9.    response.close()

  10.    return 1

  11. tasks=[

  12.    get_page('https://www./doc'),

  13.    get_page('https://www.cnblogs.com/linhaifeng'),

  14.    get_page('https://www.')

  15. ]

  16. loop=asyncio.get_event_loop()

  17. results=loop.run_until_complete(asyncio.gather(*tasks))

  18. loop.close()

  19. print('=====>',results) #[1, 1, 1]

asyncio+aiohttp

4、此外,還可以將requests.get函數(shù)傳給asyncio,就能夠被檢測了。

  1. import requests

  2. import asyncio

  3. @asyncio.coroutine

  4. def get_page(func,*args):

  5.    print('GET:%s' %args[0])

  6.    loog=asyncio.get_event_loop()

  7.    furture=loop.run_in_executor(None,func,*args)

  8.    response=yield from furture

  9.    print(response.url,len(response.text))

  10.    return 1

  11. tasks=[

  12.    get_page(requests.get,'https://www./doc'),

  13.    get_page(requests.get,'https://www.cnblogs.com/linhaifeng'),

  14.    get_page(requests.get,'https://www.')

  15. ]

  16. loop=asyncio.get_event_loop()

  17. results=loop.run_until_complete(asyncio.gather(*tasks))

  18. loop.close()

  19. print('=====>',results) #[1, 1, 1]

5、還有之前在協(xié)程時介紹的gevent模塊

  1. from gevent import monkey;monkey.patch_all()

  2. import gevent

  3. import requests

  4. def get_page(url):

  5.    print('GET:%s' %url)

  6.    response=requests.get(url)

  7.    print(url,len(response.text))

  8.    return 1

  9. # g1=gevent.spawn(get_page,'https://www./doc')

  10. # g2=gevent.spawn(get_page,'https://www.cnblogs.com/linhaifeng')

  11. # g3=gevent.spawn(get_page,'https://www.')

  12. # gevent.joinall([g1,g2,g3,])

  13. # print(g1.value,g2.value,g3.value) #拿到返回值

  14. #協(xié)程池

  15. from gevent.pool import Pool

  16. pool=Pool(2)

  17. g1=pool.spawn(get_page,'https://www./doc')

  18. g2=pool.spawn(get_page,'https://www.cnblogs.com/linhaifeng')

  19. g3=pool.spawn(get_page,'https://www.')

  20. gevent.joinall([g1,g2,g3,])

  21. print(g1.value,g2.value,g3.value) #拿到返回值

6、封裝了gevent+requests模塊的grequests模塊

  1. #pip3 install grequests

  2. import grequests

  3. request_list=[

  4.    grequests.get('https://wwww./doc1'),

  5.    grequests.get('https://www.cnblogs.com/linhaifeng'),

  6.    grequests.get('https://www.')

  7. ]

  8. ##### 執(zhí)行并獲取響應(yīng)列表 #####

  9. # response_list = grequests.map(request_list)

  10. # print(response_list)

  11. ##### 執(zhí)行并獲取響應(yīng)列表(處理異常) #####

  12. def exception_handler(request, exception):

  13.    # print(request,exception)

  14.    print('%s Request failed' %request.url)

  15. response_list = grequests.map(request_list, exception_handler=exception_handler)

  16. print(response_list)

7、twisted:是一個網(wǎng)絡(luò)框架,其中一個功能是發(fā)送異步請求,檢測IO并自動切換。

  1. '''

  2. #問題一:error: Microsoft Visual C++ 14.0 is required. Get it with 'Microsoft Visual C++ Build Tools': http://landinghub./visual-cpp-build-tools

  3. https://www.lfd./~gohlke/pythonlibs/#twisted

  4. pip3 install C:UsersAdministratorDownloadsTwisted-17.9.0-cp36-cp36m-win_amd64.whl

  5. pip3 install twisted

  6. #問題二:ModuleNotFoundError: No module named 'win32api'

  7. https:///projects/pywin32/files/pywin32/

  8. #問題三:openssl

  9. pip3 install pyopenssl

  10. '''

  11. #twisted基本用法

  12. from twisted.web.client import getPage,defer

  13. from twisted.internet import reactor

  14. def all_done(arg):

  15.    # print(arg)

  16.    reactor.stop()

  17. def callback(res):

  18.    print(res)

  19.    return 1

  20. defer_list=[]

  21. urls=[

  22.    'http://www.baidu.com',

  23.    'http://www.bing.com',

  24.    'https://www.',

  25. ]

  26. for url in urls:

  27.    obj=getPage(url.encode('utf=-8'),)

  28.    obj.addCallback(callback)

  29.    defer_list.append(obj)

  30. defer.DeferredList(defer_list).addBoth(all_done)

  31. reactor.run()

  32. #twisted的getPage的詳細(xì)用法

  33. from twisted.internet import reactor

  34. from twisted.web.client import getPage

  35. import urllib.parse

  36. def one_done(arg):

  37.    print(arg)

  38.    reactor.stop()

  39. post_data = urllib.parse.urlencode({'check_data': 'adf'})

  40. post_data = bytes(post_data, encoding='utf8')

  41. headers = {b'Content-Type': b'application/x-www-form-urlencoded'}

  42. response = getPage(bytes('http://dig./login', encoding='utf8'),

  43.                   method=bytes('POST', encoding='utf8'),

  44.                   postdata=post_data,

  45.                   cookies={},

  46.                   headers=headers)

  47. response.addBoth(one_done)

  48. reactor.run()

8、tornado

  1. from tornado.httpclient import AsyncHTTPClient

  2. from tornado.httpclient import HTTPRequest

  3. from tornado import ioloop

  4. def handle_response(response):

  5.    '''

  6.    處理返回值內(nèi)容(需要維護(hù)計數(shù)器,來停止IO循環(huán)),調(diào)用 ioloop.IOLoop.current().stop()

  7.    :param response:

  8.    :return:

  9.    '''

  10.    if response.error:

  11.        print('Error:', response.error)

  12.    else:

  13.        print(response.body)

  14. def func():

  15.    url_list = [

  16.        'http://www.baidu.com',

  17.        'http://www.bing.com',

  18.    ]

  19.    for url in url_list:

  20.        print(url)

  21.        http_client = AsyncHTTPClient()

  22.        http_client.fetch(HTTPRequest(url), handle_response)

  23. ioloop.IOLoop.current().add_callback(func)

  24. ioloop.IOLoop.current().start()

發(fā)現(xiàn)上例在所有任務(wù)都完畢后也不能正常結(jié)束,為了解決該問題,讓我們來加上計數(shù)器。

  1. from tornado.httpclient import AsyncHTTPClient

  2. from tornado.httpclient import HTTPRequest

  3. from tornado import ioloop

  4. count=0

  5. def handle_response(response):

  6.    '''

  7.    處理返回值內(nèi)容(需要維護(hù)計數(shù)器,來停止IO循環(huán)),調(diào)用 ioloop.IOLoop.current().stop()

  8.    :param response:

  9.    :return:

  10.    '''

  11.    if response.error:

  12.        print('Error:', response.error)

  13.    else:

  14.        print(len(response.body))

  15.    global count

  16.    count-=1 #完成一次回調(diào),計數(shù)減1

  17.    if count == 0:

  18.        ioloop.IOLoop.current().stop()

  19. def func():

  20.    url_list = [

  21.        'http://www.baidu.com',

  22.        'http://www.bing.com',

  23.    ]

  24.    global count

  25.    for url in url_list:

  26.        print(url)

  27.        http_client = AsyncHTTPClient()

  28.        http_client.fetch(HTTPRequest(url), handle_response)

  29.        count+=1 #計數(shù)加1

  30. ioloop.IOLoop.current().add_callback(func)

  31. ioloop.IOLoop.current().start()


題圖:pexels,CC0 授權(quán)。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多