Python網路爬蟲中的同步與非同步樣本詳解,python爬蟲
一、同步與非同步
#同步編程(同一時間只能做一件事,做完了才能做下一件事情)<-a_url-><-b_url-><-c_url->#非同步編程 (可以近似的理解成同一時間有多個事情在做,但有先後)<-a_url-> <-b_url-> <-c_url-> <-d_url-> <-e_url-> <-f_url-> <-g_url-> <-h_url-> <--i_url--> <--j_url-->
模板
import asyncio#函數名:做現在的任務時不等待,能繼續做別的任務。async def donow_meantime_dontwait(url): response = await requests.get(url)#函數名:快速高效的做任務async def fast_do_your_thing(): await asyncio.wait([donow_meantime_dontwait(url) for url in urls])#下面兩行都是套路,記住就好loop = asyncio.get_event_loop()loop.run_until_complete(fast_do_your_thing())
tips:
- await運算式中的對象必須是awaitable
- requests不支援非阻塞
- aiohttp是用於非同步請求的庫
代碼
import asyncioimport requestsimport timeimport aiohttpurls = ['https://book.douban.com/tag/小說','https://book.douban.com/tag/科幻', 'https://book.douban.com/tag/漫畫','https://book.douban.com/tag/奇幻', 'https://book.douban.com/tag/曆史','https://book.douban.com/tag/經濟學']async def requests_meantime_dont_wait(url): print(url) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: print(resp.status) print("{url} 得到響應".format(url=url))async def fast_requsts(urls): start = time.time() await asyncio.wait([requests_meantime_dont_wait(url) for url in urls]) end = time.time() print("Complete in {} seconds".format(end - start))loop = asyncio.get_event_loop()loop.run_until_complete(fast_requsts(urls))
gevent簡介
gevent是一個python的並發庫,它為各種並發和網路相關的任務提供了整潔的API。
gevent中用到的主要模式是greenlet,它是以C擴充模組形式接入Python的輕量級協程。 greenlet全部運行在主程式作業系統進程的內部,但它們被協作式地調度。
猴子補丁
requests庫是阻塞式的,為了將requests同步更改為非同步。只有將requests庫阻塞式更改為非阻塞,非同步作業才能實現。
而gevent庫中的猴子補丁(monkey patch),gevent能夠修改標準庫裡面大部分的阻塞式系統調用。這樣在不改變原有代碼的情況下,將應用的阻塞式方法,變成協程式的(非同步)。
代碼
from gevent import monkeyimport geventimport requestsimport timemonkey.patch_all()def req(url): print(url) resp = requests.get(url) print(resp.status_code,url)def synchronous_times(urls): """同步請求已耗用時間""" start = time.time() for url in urls: req(url) end = time.time() print('同步執行時間 {} s'.format(end-start))def asynchronous_times(urls): """非同步請求已耗用時間""" start = time.time() gevent.joinall([gevent.spawn(req,url) for url in urls]) end = time.time() print('非同步執行時間 {} s'.format(end - start))urls = ['https://book.douban.com/tag/小說','https://book.douban.com/tag/科幻', 'https://book.douban.com/tag/漫畫','https://book.douban.com/tag/奇幻', 'https://book.douban.com/tag/曆史','https://book.douban.com/tag/經濟學']synchronous_times(urls)asynchronous_times(urls)
gevent:非同步理論與實戰
gevent庫中使用的最核心的是Greenlet-一種用C寫的輕量級python模組。在任意時間,系統只能允許一個Greenlet處於運行狀態
一個greenlet遇到IO操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
串列和非同步
高並發的核心是讓一個大的任務分成一批子任務,並且子任務會被被系統高效率的調度,實現同步或者非同步。在兩個子任務之間切換,也就是經常說到的環境切換。
同步就是讓子任務串列,而非同步有點影分身之術,但在任意時間點,真身只有一個,子任務並不是真正的並行,而是充分利用了片段化的時間,讓程式不要浪費在等待上。這就是非同步,效率槓桿的。
gevent中的環境切換是通過yield實現。在這個例子中,我們會有兩個子任務,互相利用對方等待的時間做自己的事情。這裡我們使用gevent.sleep(0)代表程式會在這裡停0秒。
import geventdef foo(): print('Running in foo') gevent.sleep(0) print('Explicit context switch to foo again')def bar(): print('Explicit context to bar') gevent.sleep(0) print('Implicit context switch back to bar')gevent.joinall([ gevent.spawn(foo), gevent.spawn(bar) ])
啟動並執行順序:
Running in fooExplicit context to barExplicit context switch to foo againImplicit context switch back to bar
同步非同步順序問題
同步運行就是串列,123456...,但是非同步順序是隨機的任意的(根據子任務消耗的時間而定)
代碼
import geventimport randomdef task(pid): """ Some non-deterministic task """ gevent.sleep(random.randint(0,2)*0.001) print('Task %s done' % pid)#同步(結果更像串列)def synchronous(): for i in range(1,10): task(i)#非同步(結果更像亂步)def asynchronous(): threads = [gevent.spawn(task, i) for i in range(10)] gevent.joinall(threads)print('Synchronous同步:')synchronous()print('Asynchronous非同步:')asynchronous()
輸出
Synchronous同步:Task 1 doneTask 2 doneTask 3 doneTask 4 doneTask 5 doneTask 6 doneTask 7 doneTask 8 doneTask 9 doneAsynchronous非同步:Task 1 doneTask 5 doneTask 6 doneTask 2 doneTask 4 doneTask 7 doneTask 8 doneTask 9 doneTask 0 doneTask 3 done
同步案例中所有的任務都是按照順序執行,這導致主程式是阻塞式的(阻塞會暫停主程式的執行)。
gevent.spawn會對傳入的任務(子任務集合)進行進行調度,gevent.joinall方法會阻塞當前程式,除非所有的greenlet都執行完畢,程式才會結束。
實戰
實現gevent到底怎麼用,把非同步訪問得到的資料提取出來。
在有道詞典搜尋方塊輸入“hello”按斷行符號。觀察資料請求情況 觀察有道的url構建。
分析url規律
#url構建只需要傳入word即可url = "http://dict.youdao.com/w/eng/{}/".format(word)
解析網頁資料
def fetch_word_info(word): url = "http://dict.youdao.com/w/eng/{}/".format(word) resp = requests.get(url,headers=headers) doc = pq(resp.text) pros = '' for pro in doc.items('.baav .pronounce'): pros+=pro.text() description = '' for li in doc.items('#phrsListTab .trans-container ul li'): description +=li.text() return {'word':word,'音標':pros,'注釋':description}
因為requests庫在任何時候只允許有一個訪問結束完全結束後,才能進行下一次訪問。無法通過正規途徑拓展成非同步,因此這裡使用了monkey補丁
同步代碼
import requestsfrom pyquery import PyQuery as pqimport geventimport timeimport gevent.monkeygevent.monkey.patch_all()words = ['good','bad','cool', 'hot','nice','better', 'head','up','down', 'right','left','east']def synchronous(): start = time.time() print('同步開始了') for word in words: print(fetch_word_info(word)) end = time.time() print("同步已耗用時間: %s 秒" % str(end - start))#執行同步synchronous()
非同步代碼
import requestsfrom pyquery import PyQuery as pqimport geventimport timeimport gevent.monkeygevent.monkey.patch_all()words = ['good','bad','cool', 'hot','nice','better', 'head','up','down', 'right','left','east']def asynchronous(): start = time.time() print('非同步開始了') events = [gevent.spawn(fetch_word_info,word) for word in words] wordinfos = gevent.joinall(events) for wordinfo in wordinfos: #擷取到資料get方法 print(wordinfo.get()) end = time.time() print("非同步已耗用時間: %s 秒"%str(end-start))#執行非同步asynchronous()
我們可以對待耙梳站即時非同步訪問,速度會大大提高。我們現在是爬取12個詞語的資訊,也就是說一瞬間我們對網站訪問了12次,這還沒啥問題,假如爬10000+個詞語,使用gevent的話,那幾秒鐘之內就給網站一股腦的發請求,說不定網站就把爬蟲封了。
解決辦法
將列表等分為若干個子列表,分批爬取。舉例我們有一個數字列表(0-19),要均勻的等分為4份,也就是子列表有5個數。下面是我在stackoverflow尋找到的列表等分方案:
方法1
seqence = list(range(20))size = 5 #子列表長度output = [seqence[i:i+size] for i in range(0, len(seqence), size)]print(output)
方法2
chunks = lambda seq, size: [seq[i: i+size] for i in range(0, len(seq), size)]print(chunks(seq, 5))
方法3
def chunks(seq,size): for i in range(0,len(seq), size): yield seq[i:i+size]prinT(chunks(seq,5)) for x in chunks(req,5): print(x)
資料量不大的情況下,選哪一種方法都可以。如果特別大,建議使用方法3.
動手實現
import requestsfrom pyquery import PyQuery as pqimport geventimport timeimport gevent.monkeygevent.monkey.patch_all()words = ['good','bad','cool', 'hot','nice','better', 'head','up','down', 'right','left','east']def fetch_word_info(word): url = "http://dict.youdao.com/w/eng/{}/".format(word) resp = requests.get(url,headers=headers) doc = pq(resp.text) pros = '' for pro in doc.items('.baav .pronounce'): pros+=pro.text() description = '' for li in doc.items('#phrsListTab .trans-container ul li'): description +=li.text() return {'word':word,'音標':pros,'注釋':description}def asynchronous(words): start = time.time() print('非同步開始了') chunks = lambda seq, size: [seq[i: i + size] for i in range(0, len(seq), size)] for subwords in chunks(words,3): events = [gevent.spawn(fetch_word_info, word) for word in subwords] wordinfos = gevent.joinall(events) for wordinfo in wordinfos: # 擷取到資料get方法 print(wordinfo.get()) time.sleep(1) end = time.time() print("非同步已耗用時間: %s 秒" % str(end - start))asynchronous(words)
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對幫客之家的支援。