實務上開發Python網頁爬蟲,使用一般的同步處理(Synchronous)方式爬取大量的資料時,會發現需花費蠻久的時間,這是因為同步處理(Synchronous)需等待網頁回應後,才能繼續執行下一個任務,而在等待的過程中,執行緒是完全停滯的,不會去做其它的任務,所以,為了提升執行的速度,就會使用非同步處理(Asynchronous)來進行開發。
非同步處理(Asynchronous)就是能夠在等待網頁回應的同時,可以先去做其它的任務,不會因為等待而停擺。在[Python爬蟲教學]非同步網頁爬蟲使用GRequests套件提升爬取效率的實作技巧文章中,分享了利用GRequest套件來實作Python非同步網頁爬蟲,本文將延續分享另一個實作Python非同步網頁爬蟲的方法,就是利用asyncio模組(Module)及aiohttp套件,需搭配Python 3.5+,其中的實作重點包含:
- asyncio模組
- aiohttp套件
- 安裝aiohttp套件
- 定義協程(coroutine)
- 定義協程任務(Task)
- 執行協程(coroutine)
一、asyncio模組
asyncio是在Python 3.4時引入的非同步模組(Module),使用async及await語法來支援非同步的執行,也就是在Python的函式(Function)前加上async關鍵字,來定義協程(coroutine),在其中定義非同步的任務清單,接著,透過事件迴圈(Event Loop)來進行不同任務間的切換執行,達到非同步的執行效果。
二、aiohttp套件
aiohttp是基於asyncio的非同步HTTP用戶端/伺服端套件,能夠非同步的發送請求(request)及執行非同步的程式碼,所以非常適合用來開發Python非同步的網頁爬蟲,提升執行的效率。
三、安裝aiohttp套件
本文以Visual Studio Code為例,開啟[Python爬蟲教學]非同步網頁爬蟲使用GRequests套件提升爬取效率的實作技巧文章所建置的專案,利用以下的指令安裝aiohttp套件:
$ pip install aiohttp
四、定義協程(coroutine)
協程(coroutine)簡單來說,就是所要執行的非同步內容,定義的方式就是在Python函式(Function)前加上async關鍵字,可以把它想像成非同步函式(Function)。
在專案中新增asynch2.py檔案,引用所需的模組(Module),如下:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time
利用async關鍵字,定義一個協程(coroutine),如下範例:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main():
接下來,就可以在其中定義所要執行的非同步內容。首先,建立請求(request)的網址清單,也就是104人力銀行網站,1到10頁的Python職缺網址,讓aiohttp模組(Module)能夠非同步的發送請求(request),如下範例:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main(): links = list() for page in range(1, 11): links.append( f"https://www.104.com.tw/jobs/search/?keyword=python&order=1&page={page}&jobsource=2018indexpoc&ro=0")
範例中,第9~12行所做的事情等同於以下的範例:
links = [ https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=1&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=2&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=3&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=4&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=5&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=6&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=7&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=8&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=9&jobsource=2018indexpoc&ro=0 https://www.104.com.tw/jobs/search/?keyword=python&order=1&page=10&jobsource=2018indexpoc&ro=0 ]
有了網址清單後,就可以建立aiohttp模組(Module)的用戶端session,如下範例:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main(): links = list() for page in range(1, 11): links.append( f"https://www.104.com.tw/jobs/search/?keyword=python&order=1&page={page}&jobsource=2018indexpoc&ro=0") async with ClientSession() as session:
範例中,由於是非同步的用戶端,所以需加上async關鍵字,而with陳述式則是在完成區塊中的程式碼後,自動釋放資源。
為了讓程式碼易於維護,所以再定義一個fetch協程(coroutine),用來接收請求的網址及用戶端的session,非同步發送請求(request)與解析所收到的HTML原始碼回應,爬取想要的資料,如下範例:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main(): links = list() for page in range(1, 11): links.append( f"https://www.104.com.tw/jobs/search/?keyword=python&order=1&page={page}&jobsource=2018indexpoc&ro=0") async with ClientSession() as session: for link in links: fetch(link, session) #定義協程(coroutine) async def fetch(link, session): async with session.get(link) as response: #非同步發送請求 html_body = await response.text() soup = BeautifulSoup(html_body, "lxml") # 解析HTML原始碼 blocks = soup.find_all("div", {"class": "b-block__left"}) # 職缺區塊 for block in blocks: job = block.find("a", {"class": "js-job-link"}) # 職缺名稱 if job is None: continue company = block.find_all("li")[1] # 公司名稱 salary = block.find("span", {"class": "b-tag--default"}) # 待遇 print((job.getText(),) + (company.getText().strip(),) + (salary.getText(),))
範例中第15~16行,利用迴圈讀取網址清單,將網址及用戶端session傳送至fetch協程(coroutine),第20行非同步發送請求(request),也因此在第21行接收回應時,需要加上await關鍵字,進行非同步的等待,而第23~36行則是利用BeautifulSoup套件,將接收的HTML原始碼回應進行解析,印出爬取的資料。
五、定義協程任務(Task)
在執行非同步(Asynchronous)的程式碼時,有一點非常重要的就是要建立任務清單(Task),讓事件迴圈(Event Loop)能夠知道有哪些非同步的任務需要執行,進而在多項任務中,利用等待的空檔切換執行其它的任務,提升效率。
在以上範例的第16行,在迴圈每一次執行的fetch(link, session),就是一個任務,也就是說,總共會有十個任務,因為網址清單有十個網址,也就會執行十次fetch協程(coroutine),所以,要建立任務清單(Task),可以利用asyncio模組(Module)的create_task()方法(Method),如下範例第15行:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main(): links = list() for page in range(1, 11): links.append( f"https://www.104.com.tw/jobs/search/?keyword=python&order=1&page={page}&jobsource=2018indexpoc&ro=0") async with ClientSession() as session: tasks = [asyncio.create_task(fetch(link, session)) for link in links] # 建立任務清單 await asyncio.gather(*tasks) # 打包任務清單及執行 #定義協程(coroutine) async def fetch(link, session): async with session.get(link) as response: #非同步發送請求 html_body = await response.text() soup = BeautifulSoup(html_body, "lxml") # 解析HTML原始碼 blocks = soup.find_all("div", {"class": "b-block__left"}) # 職缺區塊 for block in blocks: job = block.find("a", {"class": "js-job-link"}) # 職缺名稱 if job is None: continue company = block.find_all("li")[1] # 公司名稱 salary = block.find("span", {"class": "b-tag--default"}) # 待遇 print((job.getText(),) + (company.getText().strip(),) + (salary.getText(),))
範例中,第15行利用Python Comprehesion語法,來建立任務清單(Task),接著,第16行透過asyncio模組(Module)的gather()方法(Method)進行打包及執行,也由於呼叫的gather()為非同步的方法(Method),所以要加上await關鍵字,來進行非同步等待。
六、執行協程(coroutine)
協程(coroutine)中的非同步內容都定義完成後,接下來,就需要建立事件迴圈(Event Loop)來執行協程(coroutine),如下範例:
from aiohttp import ClientSession from bs4 import BeautifulSoup import asyncio import time #定義協程(coroutine) async def main(): links = list() for page in range(1, 11): links.append( f"https://www.104.com.tw/jobs/search/?keyword=python&order=1&page={page}&jobsource=2018indexpoc&ro=0") async with ClientSession() as session: tasks = [asyncio.create_task(fetch(link, session)) for link in links] # 建立任務清單 await asyncio.gather(*tasks) # 打包任務清單及執行 #定義協程(coroutine) async def fetch(link, session): async with session.get(link) as response: #非同步發送請求 html_body = await response.text() soup = BeautifulSoup(html_body, "lxml") # 解析HTML原始碼 blocks = soup.find_all("div", {"class": "b-block__left"}) # 職缺區塊 for block in blocks: job = block.find("a", {"class": "js-job-link"}) # 職缺名稱 if job is None: continue company = block.find_all("li")[1] # 公司名稱 salary = block.find("span", {"class": "b-tag--default"}) # 待遇 print((job.getText(),) + (company.getText().strip(),) + (salary.getText(),)) start_time = time.time() #開始執行時間 loop = asyncio.get_event_loop() #建立事件迴圈(Event Loop) loop.run_until_complete(main()) #執行協程(coroutine) print("花費:" + str(time.time() - start_time) + "秒")
範例中,第40行利用asyncio模組(Module)的get_event_loop()方法(Method)來建立事件迴圈(Event Loop),第41行呼叫run_until_complete()方法(Method),來執行協程(coroutine),這樣事件迴圈(Event Loop)就能夠監控協程(coroutine)中的任務清單(Task),非同步的切換執行。執行結果如下:
可以看到執行速度非常的快,104人力銀行網站1到10頁的Python職缺1.7秒就爬取完成。
七、小結
本文整合asyncio及aiohttp兩個非同步的模組(Module),來開發Python非同步的網頁爬蟲,除了能夠非同步的發送請求(request)外,也能夠針對先接收到的網頁回應,進行原始碼的解析及爬取,達到更快速的執行效率。您也想提升Python網頁爬蟲的執行效率嗎?不妨利用本文所分享的技巧來試試看吧 :)
Hi~Mike
回覆刪除執行開發104非同步網頁爬蟲(asycio+aiohttp)的source code
我用vscode執行時有跑出這個錯誤~請問是甚麼狀況呢?
後來是加了以下這兩行才解決問題,解決方案出處
https://stackoverflow.com/questions/46827007/runtimeerror-this-event-loop-is-already-running-in-python
import nest_asyncio
nest_asyncio.apply()
雖然解決,但不清楚問題所在,是否可以協助解惑呢?感謝
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
in
40 start_time = time.time() # 開始執行時間
41 loop = asyncio.get_event_loop() # 建立事件迴圈(Event Loop)
---> 42 loop.run_until_complete(main()) # 執行協程(coroutine)
43 print("花費:" + str(time.time() - start_time) + "秒")
~\AppData\Local\Programs\Python\Python37-32\lib\asyncio\base_events.py in run_until_complete(self, future)
568 future.add_done_callback(_run_until_complete_cb)
569 try:
--> 570 self.run_forever()
571 except:
572 if new_task and future.done() and not future.cancelled():
~\AppData\Local\Programs\Python\Python37-32\lib\asyncio\base_events.py in run_forever(self)
523 self._check_closed()
524 if self.is_running():
--> 525 raise RuntimeError('This event loop is already running')
526 if events._get_running_loop() is not None:
527 raise RuntimeError(
RuntimeError: This event loop is already running
MINI您好:
刪除自己也是用VSCode開發,但是沒有遇到這個問題,看起來可能是在執行的過程中,在非同步的等待任務時,又有另一個任務要執行,導致already running已經執行中的錯誤。您所使用的nest_asyncio套件,能夠讓事件迴圈(Event Loop)為巢狀,不會因為有少許的機會發生等待,而讓事件迴圈(Event Loop)卡住,感謝您的分享 :)