Python實(shí)用技巧大任務(wù)切分
今天來說說,Python 中的任務(wù)切分。以爬蟲為例,從一個(gè)存 url 的 txt 文件中,讀取其內(nèi)容,我們會(huì)獲取一個(gè) url 列表。我們把這一個(gè) url 列表稱為大任務(wù)。
列表切分在
不考慮內(nèi)存占用的情況下,我們對(duì)上面的大任務(wù)進(jìn)行一個(gè)切分。比如我們將大任務(wù)切分成的小任務(wù)是每秒最多只訪問5個(gè)URL。
- import os
- import time
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
- def read_file():
- file_path = os.path.join(CURRENT_DIR, "url_list.txt")
- with open(file_path, "r", encoding="utf-8") as fs:
- result = [i.strip() for i in fs.readlines()]
- return result
- def fetch(url):
- print(url)
- def run():
- max_count = 5
- url_list = read_file()
- for index in range(0, len(url_list), max_count):
- start = time.time()
- fetch(url_list[index:index + max_count])
- end = time.time() - start
- if end < 1:
- time.sleep(1 - end)
- if __name__ == '__main__':
- run()
關(guān)鍵代碼都在for循環(huán)里,首先我們通過聲明range的第三個(gè)參數(shù),該參數(shù)指定迭代的步長(zhǎng)為5,這樣每次index增加都是以5為基數(shù),即0,5,10。。。
然后我們對(duì)url_list做切片,每次取其五個(gè)元素,這五個(gè)元素會(huì)隨著index的增加不斷的在改變,如果最后不夠五個(gè)了,按照切片的特性這個(gè)時(shí)候就會(huì)有多少取多少了,不會(huì)造成索引超下標(biāo)的問題。
隨著url列表的增加,我們會(huì)發(fā)現(xiàn)內(nèi)存的占用也在提高了。這個(gè)時(shí)候我們就需要對(duì)代碼進(jìn)行修改了,我們知道生成器是比較節(jié)省內(nèi)存的空間的,修改之后代碼變成,下面的這樣。
生成器切分
- # -*- coding: utf-8 -*-
- # @時(shí)間 : 2019-11-23 23:47
- # @作者 : 陳祥安
- # @文件名 : g.py
- # @公眾號(hào): Python學(xué)習(xí)開發(fā)
- import os
- import time
- from itertools import islice
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
- def read_file():
- file_path = os.path.join(CURRENT_DIR, "url_list.txt")
- with open(file_path, "r", encoding="utf-8") as fs:
- for i in fs:
- yield i.strip()
- def fetch(url):
- print(url)
- def run():
- max_count = 5
- url_gen = read_file()
- while True:
- url_list = list(islice(url_gen, 0, max_count))
- if not url_list:
- break
- start = time.time()
- fetch(url_list)
- end = time.time() - start
- if end < 1:
- time.sleep(1 - end)
- if __name__ == '__main__':
- run()
首先,我們修改了文件讀取的方式,把原來讀列表的形式,改為了生成器的形式。這樣我們?cè)谡{(diào)用該文件讀取方法的時(shí)候大大節(jié)省了內(nèi)存。
然后就是對(duì)上面for循環(huán)進(jìn)行改造,因?yàn)樯善鞯奶匦裕@里不適合使用for進(jìn)行迭代,因?yàn)槊恳淮蔚牡紩?huì)消耗生成器的元素,通過使用itertools的islice對(duì)url_gen進(jìn)行切分,islice是生成器的切片,這里我們每次切分出含有5個(gè)元素的生成器,因?yàn)樯善鳑]有__len__方法所以,我們將其轉(zhuǎn)為列表,然后判斷列表是否為空,就可以知道迭代是否該結(jié)束了。
修改之后的代碼,不管是性能還是節(jié)省內(nèi)存上都大大的提高。讀取千萬級(jí)的文件不是問題。
除此之外,在使用異步爬蟲的時(shí)候,也許會(huì)用到異步生成器切片。下面就和大家討論,異步生成器切分的問題
異步生成器切分
首先先來看一個(gè)簡(jiǎn)單的異步生成器。
我們知道調(diào)用下面的代碼會(huì)得到一個(gè)生成器
- def foo():
- for i in range(20):
- yield i
如果在def前面加一個(gè)async,那么在調(diào)用的時(shí)候它就是個(gè)異步生成器了。
完整示例代碼如下
- import asyncio
- async def foo():
- for i in range(20):
- yield i
- async def run():
- async_gen = foo()
- async for i in async_gen:
- print(i)
- if __name__ == '__main__':
- asyncio.run(run())
關(guān)于async for的切分有點(diǎn)復(fù)雜,這里推薦使用aiostream模塊,使用之后代碼改為下面這樣
- import asyncio
- from aiostream import stream
- async def foo():
- for i in range(22):
- yield i
- async def run():
- index = 0
- limit = 5
- while True:
- xs = stream.iterate(foo())
- ys = xs[index:index + limit]
- t = await stream.list(ys)
- if not t:
- break
- print(t)
- index += limit
- if __name__ == '__main__':
- asyncio.run(run())