pandas讀取excel時獲取讀取進度的實現
寫在前面
QQ群裡偶然看到群友問這個問題, pandas讀取大文件時怎麼才能獲取進度? 我第一反應是: 除非pandas的read_excel等函數提供瞭回調函數的接口, 否則應該沒辦法做到. 搜索瞭一下官方文檔和網上的帖子, 果然是沒有現成的方案, 隻能自己動手.
準備工作
確定方案
一開始我就確認瞭實現方案, 那就是增加回調函數. 這裡現學現賣科普一下什麼是回調函數. 簡單的說就是:
所使用的模塊裡面, 會調用一個你給定的外部方法/函數, 就是回調函數. 拿本次的嘗試作為例子, 我會編寫一個”顯示進度函數”, 通過傳參的方式傳入pd.read_excel, 這樣pd在讀取excel時, 會邊讀取邊調用”顯示進度函數”. 為什麼不直接在pd裡面增加? 因為pd讀取excel文件時是阻塞的, 內部方法在被調用時無法拋出進度信息. (如有謬誤請指正)
理解讀取方式
先得瞭解一下pandas是怎麼讀取excel的. 在pycharm裡面按住control點擊read_excel, 再瀏覽一下代碼根據關鍵的函數繼續跳轉, 還是挺容易得到調用的路徑的.
最後OpenpyxlReader讀取excel的方法代碼如下. 很明顯重點就在其中的for循環裡. 調用get_sheet_data時, 已經通過一系列方法獲得瞭目標sheet(這裡細節不贅述), 然後在for循環裡逐行讀取數據並返回data最後生成dataframe.
def get_sheet_data(self, sheet, convert_float: bool) -> List[List[Scalar]]: # GH 39001 # Reading of excel file depends on dimension data being correct but # writers sometimes omit or get it wrong import openpyxl version = LooseVersion(get_version(openpyxl)) # There is no good way of determining if a sheet is read-only # https://foss.heptapod.net/openpyxl/openpyxl/-/issues/1605 is_readonly = hasattr(sheet, "reset_dimensions") if version >= "3.0.0" and is_readonly: sheet.reset_dimensions() data: List[List[Scalar]] = [] last_row_with_data = -1 for row_number, row in enumerate(sheet.rows): converted_row = [self._convert_cell(cell, convert_float) for cell in row] if not all(cell == "" for cell in converted_row): last_row_with_data = row_number data.append(converted_row) # Trim trailing empty rows data = data[: last_row_with_data + 1] if version >= "3.0.0" and is_readonly and len(data) > 0: # With dimension reset, openpyxl no longer pads rows max_width = max(len(data_row) for data_row in data) if min(len(data_row) for data_row in data) < max_width: empty_cell: List[Scalar] = [""] data = [ data_row + (max_width - len(data_row)) * empty_cell for data_row in data ] return data
開始改動
這裡直接暴力更改pandas庫源文件!(僅用於調試, 註意備份和保護自己的工作環境)
主程序代碼
編寫main.py, 代碼比較簡單, 相關功能我都用註釋作為解釋. 其中show_pd_read_excel_progress就是我編寫的回調函數, 通過命令行的方式輸出實時的讀取進度. 當然你如果編寫的是GUI程序比如PYQT5, 也可以在這個回調函數中發送signal給main UI, 做成progress bar或者其他的GUI樣式.
import pandas as pd from datetime import datetime ''' 定義回調函數 cur: 讀取時的當前行數 tt: 讀取文件的總行數 ''' def show_pd_read_excel_progress(cur, tt): # 進度數值 progress = " {:.2f}%".format(cur/tt*100) # 進度條 bar = " ".join("█" for _ in range(int(cur/tt*100/10))) # 顯示進度 print("\r進度:" + bar + progress, end="", flush=True) # 記錄開始時間 t = datetime.now() # 開始讀取excel print("pd.read_excel: test_4.xlsx...") xl_data = pd.read_excel("test_4.xlsx", callback=show_pd_read_excel_progress) # 打印excel頭幾行 print(xl_data.head()) print("\n") # 顯示花費的時間 print("Time spent:", datetime.now()-t)
修改pandas源碼
再自己觀察一下, 我在pd.read_excel方法的參數裡增加瞭callback參數, 這個參數是原版read_excel方法裡沒有的. 所以我們需要處理pandas源碼, 這個源碼在…/pandas/io/excel/_base.py中, pycharm中按住control點擊read_excel可以快速跳轉. 這個地方我增加瞭一個參數callback, 默認值為None. 下方io.parse同樣把callback參數傳遞給ExcelFile類.
def read_excel( io, sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=False, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, parse_dates=False, date_parser=None, thousands=None, comment=None, skipfooter=0, convert_float=True, mangle_dupe_cols=True, storage_options: StorageOptions = None, callback = None, # 增加callback參數 ): should_close = False if not isinstance(io, ExcelFile): should_close = True io = ExcelFile(io, storage_options=storage_options, engine=engine) elif engine and engine != io.engine: raise ValueError( "Engine should not be specified when passing " "an ExcelFile - ExcelFile already has the engine set" ) try: data = io.parse( sheet_name=sheet_name, header=header, names=names, index_col=index_col, usecols=usecols, squeeze=squeeze, dtype=dtype, converters=converters, true_values=true_values, false_values=false_values, skiprows=skiprows, nrows=nrows, na_values=na_values, keep_default_na=keep_default_na, na_filter=na_filter, verbose=verbose, parse_dates=parse_dates, date_parser=date_parser, thousands=thousands, comment=comment, skipfooter=skipfooter, convert_float=convert_float, mangle_dupe_cols=mangle_dupe_cols, callback = callback, # 增加callback參數 ) finally: # make sure to close opened file handles if should_close: io.close() return data ... # 省略代碼
瀏覽一下ExcelFile類(還在_base.py中)的代碼, 這個類會根據文件類型選擇引擎, 我讀取的是xlsx文件, 所以會跳轉到openpyxl並把所有的參數傳遞過去, 這個類不用處理. 下面跳轉到_openpyxl.py中看一下OpenpyxlReader類, 這個類是繼承BaseExcelReader類(在_base.py中)的, 所以還是得回去看一下BaseExcelReader, 並修改一下參數, 增加callback(如下2處).
def parse( self, sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=False, dtype=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, verbose=False, parse_dates=False, date_parser=None, thousands=None, comment=None, skipfooter=0, convert_float=True, mangle_dupe_cols=True, callback = None, # 增加callback參數 **kwds, ): ... # 省略代碼
for asheetname in sheets: if verbose: print(f"Reading sheet {asheetname}") if isinstance(asheetname, str): sheet = self.get_sheet_by_name(asheetname) else: # assume an integer if not a string sheet = self.get_sheet_by_index(asheetname) data = self.get_sheet_data(sheet, convert_float, callback) # 傳遞callback參數給get_sheet_data方法 usecols = maybe_convert_usecols(usecols) ... # 省略代碼
好瞭, 終於到重點瞭, 我們跳轉到get_sheet_data方法, 並做對應修改(方法參數, 獲取總行數, 調用回調函數). 思路非常清晰, 通過一頓操作, 終於千裡迢迢把callback給一層層傳遞過來瞭, 所以在一行行讀取excel時, 可以調用並顯示進度瞭.
def get_sheet_data(self, sheet, convert_float: bool, callback) -> List[List[Scalar]]: # 傳遞參數增加callback # GH 39001 # Reading of excel file depends on dimension data being correct but # writers sometimes omit or get it wrong import openpyxl # 獲取sheet的總行數 max_row = sheet.max_row print("sheet_max_row:", sheet.max_row) version = LooseVersion(get_version(openpyxl)) # There is no good way of determining if a sheet is read-only # https://foss.heptapod.net/openpyxl/openpyxl/-/issues/1605 is_readonly = hasattr(sheet, "reset_dimensions") if version >= "3.0.0" and is_readonly: sheet.reset_dimensions() data: List[List[Scalar]] = [] last_row_with_data = -1 for row_number, row in enumerate(sheet.rows): # 調用回調函數 if callback is not None: callback(row_number+1, max_row) converted_row = [self._convert_cell(cell, convert_float) for cell in row] if not all(cell == "" for cell in converted_row): last_row_with_data = row_number data.append(converted_row) # Trim trailing empty rows data = data[: last_row_with_data + 1] if version >= "3.0.0" and is_readonly and len(data) > 0: # With dimension reset, openpyxl no longer pads rows max_width = max(len(data_row) for data_row in data) if min(len(data_row) for data_row in data) < max_width: empty_cell: List[Scalar] = [""] data = [ data_row + (max_width - len(data_row)) * empty_cell for data_row in data ] return data
運行測試
運行一下main.py, 效果如下, 實時顯示進度功能已經實現, 且會計算出讀取所花費的時間. 如果你是要讀取csv或者sql之類的, 也可以照貓畫虎.
優化和應用
- 前面也說過直接修改pandas源碼是非常不科學的操作, 這會破壞已有的編程環境, 且源碼換到別的機器上還得重新在修改一遍
- 也嘗試過用繼承+重寫pandas, 不過水平有限沒有成功, 希望大傢指點
- 實測print進度條會非常費時間, 當然也不需要每讀一行excel都更新一次進度條, 定時(比如每秒刷一次)或者定量(每n行, 或者每1%進度刷新一次)比較合理
- 讀取大規模數據時, 頻繁調用回調函數肯定會耽誤效率, 不過如果是GUI程序或者給其他人使用的, 有實時進度肯定會改善用戶體驗, 其中優劣需要coder自己權衡
到此這篇關於pandas讀取excel時獲取讀取進度的實現的文章就介紹到這瞭,更多相關pandas讀取excel讀取內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- None Found