for row in handler.find(): parse_data(row) 短短4行代碼,讀取MongoDB里面的每一行數(shù)據(jù),然后傳入parse_data做處理。處理完成以后再讀取下一行。邏輯清晰而簡(jiǎn)單,能有什么問題?只要parse_data(row)不報(bào)錯(cuò),這一段代碼就完美無缺。但事實(shí)并非這樣。
你的代碼可能會(huì)在for row in handler.find()這一行報(bào)錯(cuò)。它的原因,說來話長(zhǎng)。要解釋這個(gè)問題,我們首先就需要知道,handler.find()返回的并不是數(shù)據(jù)庫里面的數(shù)據(jù),而是一個(gè)游標(biāo)(cursor)對(duì)象。
所以pymongo會(huì)一次性獲取100行,for row in handler.find()循環(huán)第一次的時(shí)候,它會(huì)連上MongoDB,讀取一百條數(shù)據(jù),緩存到內(nèi)存中。于是第2-100次循環(huán),數(shù)據(jù)都是直接從內(nèi)存里面獲取,不會(huì)再連接數(shù)據(jù)庫。 當(dāng)循環(huán)進(jìn)行到底101次的時(shí)候,再一次連接數(shù)據(jù)庫,再讀取第101-200行內(nèi)容……,這個(gè)邏輯非常有效地降低了網(wǎng)絡(luò)I/O耗時(shí)。但是,MongoDB默認(rèn)游標(biāo)的超時(shí)時(shí)間是10分鐘。10分鐘之內(nèi),必需再次連接MongoDB讀取內(nèi)容刷新游標(biāo)時(shí)間,否則,就會(huì)導(dǎo)致游標(biāo)超時(shí)報(bào)錯(cuò):
pymongo.errors.CursorNotFound: cursor id 211526444773 not found
修改MongoDB的配置,延長(zhǎng)游標(biāo)超時(shí)時(shí)間,并重啟MongoDB。由于生產(chǎn)環(huán)境的MongoDB不能隨便重啟,所以這個(gè)方案雖然有用,但是排除。 一次性把數(shù)據(jù)全部讀取下來,再做處理: all_data = [row for row in handler.find()]
for row in all_data: parse(row) 這種方案的弊端也很明顯,如果數(shù)據(jù)量非常大,你不一定能全部放到內(nèi)存里面。即使能夠全部放到內(nèi)存中,但是列表推導(dǎo)式遍歷了所有數(shù)據(jù),緊接著for循環(huán)又遍歷一次,浪費(fèi)時(shí)間。
cursor = handler.find(no_cursor_timeout=True) for row in cursor: parse_data(row) cursor.close() # 一定要手動(dòng)關(guān)閉游標(biāo) 然而這個(gè)操作非常危險(xiǎn),因?yàn)槿绻愕腜ython程序因?yàn)槟撤N原因意外停止了,這個(gè)游標(biāo)就再也無法關(guān)閉了!除非重啟MongoDB,否則這些游標(biāo)會(huì)一直留在MongoDB上,占用資源。 當(dāng)然可能有人會(huì)說,使用try...except把讀取數(shù)據(jù)的地方包住,只要拋出了異常,在處理異常的時(shí)候關(guān)閉游標(biāo)即可: cursor = handler.find(no_cursor_timeout=True) try: for row in cursor: parse_data(row) except Exception: parse_exception() finally: cursor.close() # 一定要手動(dòng)關(guān)閉游標(biāo) 其中finally里面的代碼,無論有沒有異常,都會(huì)執(zhí)行。但這樣寫會(huì)讓代碼非常難看。為了解決這個(gè)問題,我們可以使用游標(biāo)的上下文管理器:
with handler.find(no_cursor_timeout=True) as cursor: for row in cursor: parse_data(row) 只要程序退出了with的縮進(jìn),游標(biāo)自動(dòng)就會(huì)關(guān)閉。如果程序中途報(bào)錯(cuò),游標(biāo)也會(huì)關(guān)閉。它的原理可以用下面兩段代碼來解釋: