1. 程式人生 > 其它 >網路爬蟲之協程

網路爬蟲之協程

一、協程的定義
協程又叫微執行緒,比執行緒還要小的一個單位;協程不是計算機提供的,是程式設計師自己創造出來的;協程是一個使用者態的上下文切換技術,簡單來說,就是通過一個執行緒去實現程式碼塊(函式)之間的相互切換執行。

二、協程的特點
1. 使用協程時不需要考慮全域性變數安全性的問題。
2. 協程必須要在單執行緒中實現併發。
3. 當協程遇到IO操作時,會自動切換到另一個協程中繼續執行。
4. 協程能夠完美解決IO密集型的問題,但是cpu密集型不是他的強項。
5. 協程執行效率非常高,因為協程的切換是子程式函式的切換,相比於執行緒的開銷來說要小很多,同時,執行緒越多開銷越大。

三、協程的原理


協程擁有自己的暫存器、上下文和棧,協程在排程切換函式時會將暫存器上下文和棧儲存到其它地方,再切回來的時候會恢復之前儲存的暫存器、上下文和棧繼續從上一次呼叫的狀態下繼續執行。

四、程序、執行緒和協程的對比
1. 協程既不是程序也不是執行緒,是一個特殊的函式,和程序、執行緒不是一個維度的。
2. 一個程序可以有多個執行緒,一個執行緒可以包含多個協程。
3. 一個執行緒內的多個協程可以相互切換,但是多個協程之間是串聯執行的,並且一個只能在一個執行緒內執行,所以,沒有辦法利用cpu的多核能力。

五、協程的實現
(1) 使用greenlet模組:最早實現協程的第三方模組
(2) yield關鍵字
(3) asyncio裝飾器(python直譯器版本3.4之後才有的)
(4) async、await關鍵字:非常好用,極力推薦(主要說這個實現方式)

六、案例介紹

1. 360圖片下載協程實現(非同步網路請求aiohttp介紹網址:https://www.cnblogs.com/fengting0913/p/14926893.html

#async是用來定義好協程的,是定義的時候是用的,真正的呼叫使用的是await,
# 利用協程來下載圖片
async def download(url):
    # 建立session物件,其中async with 是一個整體,表示一個非同步的上下文管理器
    async with aiohttp.ClientSession() as session:
        # 發起請求與接收響應
        async with session.get(url=url) as response:
            content 
= await response.content.read() # 儲存圖片,注意本地的檔案讀寫不需要await imag_name = url.split('/')[-1] with open(imag_name,'wb') as fp: fp.write(content) async def main(): # 定義url_list url_list = [ 'https://img0.baidu.com/it/u=291378222,233871465&fm=26&fmt=auto&gp=0.jpg', 'https://img2.baidu.com/it/u=3466049587,2049802835&fm=26&fmt=auto&gp=0.jpg', 'https://img0.baidu.com/it/u=213410053,396892388&fm=26&fmt=auto&gp=0.jpg', 'https://img0.baidu.com/it/u=1380950348,3018255149&fm=26&fmt=auto&gp=0.jpg', 'https://img1.baidu.com/it/u=4110196045,3829597861&fm=26&fmt=auto&gp=0.jpg' ] # 建立tasks物件,建立協程,將協程封裝到Task物件中並新增到事件迴圈的任務列表中,等待事件迴圈去執行(預設是就緒狀態) tasks = [ asyncio.ensure_future(download(i)) for i in url_list ] #將任務新增到事件迴圈中,等待協程的排程,await:當執行某協程遇到IO操作時,會自動化切換執行其他任務。 await asyncio.wait(tasks) if __name__ == '__main__': # 建立事件迴圈 loop = asyncio.get_event_loop() # 將協程當做任務提交到事件迴圈的任務列表中,協程執行完成之後終止。 loop.run_until_complete(main())

2. 小程式社群的title獲取:asyncio+aiohttp+aiomysql實現高併發爬蟲

思路:
1. 請求小程式社群列表頁第一頁,獲取所有的詳情頁連結
2. 進入到每一個文章的詳情頁中,獲取上一篇和下一篇的連結,並請求,重複這一步
3. 獲取標題,存到MySQL中(使用MySQL的非同步連線池)
aiomysql非同步操作:https://www.yangyanxing.com/article/aiomysql_in_python.html
aiomysql使用介紹:https://www.cnblogs.com/zwb8848happy/p/8809861.html
from lxml import etree
import asyncio
import aiohttp
import aiomysql
import re

# 請求函式
async def get_request(url):
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(url=url, headers=headers) as response:
                if response.status == 200:
                    content = await response.text()
                    return content
    except:
        pass

# 定義解析url的函式
# 注意:解析URL,不屬於io操作,是通過cpu完成的,所以,我們定義普通函式即可
def get_url(content):
    html = etree.HTML(content)
    a_href_list = html.xpath('//a')
    for a_href in a_href_list:
        # 注意xpath返回的是一個列表
        href = a_href.xpath('./@href')
        if href and url_pattern.findall(href[0]) and href[0] not in url_set:
            url_set.add(href[0])
            wait_url.append(href[0])

async def parse_article(url, pool):
    content = await get_request(url)
    try:
        get_url(content)
        html = etree.HTML(content)
        title = html.xpath('//h1/text()')[0]
        if not title:
            title = ''
        print(title)
        async with pool.acquire() as connect:
            async with connect.cursor() as cursor:
                insert_into = 'insert into titles(title)values (%s)'
                await cursor.execute(insert_into, title)
    except:
        pass

# 定義消費者函式
async def consumer(pool):
    while True:
        if len(wait_url) == 0:
            await asyncio.sleep(0.5)
            continue
        url = wait_url.pop()
        asyncio.ensure_future(parse_article(url, pool))
    pass

async def main():
    """
    1、建立mysql非同步連線池,需要注意的是 aiomysql 是基於協程的,因此需要通過 await 的方式來呼叫。
    2、使用連線池的意義在於,有一個池子,它裡保持著指定數量的可用連線,當一個查詢結執行之前從這個池子裡取一個連線,
    查詢結束以後將連線放回池子中,這樣可以避免頻繁的連線資料庫,節省大量的資源。
    3、高併發情況下,非同步連線池可以顯著提升總體讀寫的效率,這是單連線無法比擬的。
    """
    pool = await aiomysql.create_pool(
        host='127.0.0.1',
        port=3306,
        user='root',
        password='123456',
        db='mina',
        charset='utf8',
        autocommit=True
    )
    content = await get_request(start_url)
    # 將start_url放置到url_et集合中
    url_set.add(start_url)
    get_url(content)
    await asyncio.ensure_future(consumer(pool))

if __name__ == '__main__':
    start_url = 'https://www.wxapp-union.com/portal.php?mod=list&catid=1&page=1'
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)\
                 Chrome/80.0.3987.163 Safari/537.36'
    }
    # 定義去重集合
    url_set = set()
    # 定義待獲取的url列表
    wait_url = []
    # 匹配路由的正則
    url_pattern = re.compile(r'article-\d+-\d+\.html')
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
天青色等煙雨而我在等你!