Python 如何處理大檔案

cainiao_M發表於2020-12-03

Python作為一門程式設計語言,在易讀、易維護方面有獨特優勢,越來越多的人使用 Python 進行資料分析和處理,而 Pandas 正是為了解決資料分析任務而建立的,其包含大量能便捷處理資料的函式和方法,使得資料處理變得容易,它也是使 Python 成為強大而高效的資料分析環境的重要因素之一。

但是 Pandas 是個記憶體的類庫,用於處理小資料(能放入記憶體)沒問題,對於大資料(記憶體放不下)就沒有那麼方便了。而我們平時工作中卻能經常碰到這種較大的檔案(從資料庫或網站下載出來的資料),Pandas 無能為力,我們就只能自己想辦法,本文就來討論這個問題。

本文所說的大資料,並不是那種 TB、PB 級別的需要分散式處理的大資料,而是指普通 PC 機記憶體放不下,但可以存在硬碟內的 GB 級別的檔案資料,這也是很常見的情況。

由於此類檔案不可以一次性讀入記憶體,所以在資料處理的時候,通常需要採用逐行或者分塊讀取的方式進行處理,雖然 Python 和 pandas 在讀取檔案時支援這種方式,但因為沒有遊標系統,使得一些函式和方法需要分段使用或者函式和方法本身都需要自己寫程式碼來完成,下面我們就最常見的幾類問題來進行介紹,並寫出程式碼示例供讀者參考和感受。

一、 聚合

簡單聚合只要遍歷一遍資料,按照聚合目標將聚合列計算一遍即可。如:求和(sum),遍歷資料時對讀取的資料進行累加;計數(count),遍歷資料時,記錄遍歷數即可;平均(mean),遍歷時同時記錄累計和和遍歷數,最後相除即可。這裡以求和問題為例進行介紹。

設有如下檔案,資料片段如下:

..

現在需要計算銷售總額(amount 列)

(一)逐行讀取

| total=0with open(“orders.txt”,’r’) as f: line=f.readline()** while True:** line = f.readline()** if not line:** break** total += float(line.split(“\t”)[4])**print(total) | 開啟檔案標題行逐行讀入讀不到內容時結束累加 |

(二)pandas分塊讀取

使用 pandas 可以分塊讀取了,工作邏輯結構如下圖:

..

| import pandas as pdchunk_data = pd.read_csv(“orders.txt”,sep=”\t”,chunksize=100000)total=0for chunk in chunk_data: total+=chunk[‘amount’].sum()print(total) | 分段讀取檔案,每段 10 萬行累加各段的銷售額 |

pandas更擅長以大段讀取的方式進行計算,理論上 chunksize 越大,計算速度越快,但要注意記憶體的限制。如果 chunksize 設定成 1,就成了逐行讀取,速度會非常非常慢,因此不建議使用 pandas 逐行讀取檔案來完成此類任務。

二、 過濾

過濾流程圖:

..

過濾和聚合差不多,將大檔案分成 n 段,對各段進行過濾,最後將每一段的結果進行合併即可。

繼續以上面資料為例,過濾出紐約州的銷售資訊

(一)小結果集

| import pandas as pdchunk_data = pd.read_csv(“orders.txt”,sep=”\t”,chunksize=100000)chunk_list = []for chunk in chunk_data: chunk_list.append(chunk[chunk.state==”New York”])res = pd.concat(chunk_list)print(res) | 定義空列表存放結果分段過濾合併結果 |

(二)大結果集

| import pandas as pdchunk_data = pd.read_csv(“orders.txt”,sep=”\t”,chunksize=100000)n=0for chunk in chunk_data: need_data = chunk[chunk.state==’New York’]** if n == 0:** need_data.to_csv(“orders_filter.txt”,index=None)** n+=1** else:** need_data.to_csv(“orders_filter.txt”,index=None,mode=’a’,header=None) | 第一段,寫入檔案,保留表頭,不保留索引其他段,追加寫入不保留表頭和索引 |

大檔案聚合和過濾運算的邏輯相對簡單,但因為 Python 沒有直接提供遊標資料型別,程式碼也要寫很多行。

三、 排序

排序流程圖:

..

排序要麻煩得多,如上圖所示:

  1. 分段讀取資料;

  2. 對每一段進行排序;

  3. 將每一段的排序結果寫出至臨時檔案;

  4. 維護一個 k 個元素的列表(k 等於分段數),每個臨時檔案將一行資料放入該列表;

  5. 將列表中的記錄的按排序的欄位的排序 (與第二步的排序方式相同,升序都升序,降序都降序);

  6. 將列表的最小或最大記錄寫出至結果檔案 (升序時最小,降序時最大);

  7. 從寫出記錄的臨時檔案中再讀取一行放入列表;

  8. 重複 6.7 步,直至所有記錄寫出至結果檔案。

繼續以上面資料為例,用 Python 寫一段完整的外存排序演算法,將檔案中的資料按訂單金額升序排序

| import pandas as pdimport osimport timeimport shutilimport uuidimport tracebackdef parse_type(s):** if s.isdigit():** return int(s)** try:** res = float(s)** return res** except:** return sdef pos_by(by,head,sep): by_num = 0** for col in head.split(sep):** if col.strip()==by:** break** else:** by_num+=1** return by_numdef merge_sort(directory,ofile,by,ascending=True,sep=”,”):with open(ofile,’w’) as outfile:** file_list = os.listdir(directory)** file_chunk = [open(directory+”/“+file,’r’) for file in file_list]** k_row = [file_chunk[i].readline()for i in range(len(file_chunk))]** by = pos_by(by,k_row[0],sep)** outfile.write(k_row[0])** k_row = [file_chunk[i].readline()for i in range(len(file_chunk))]k_by = [parse_type(k_row[i].split(sep)[by].strip())for i in range(len(file_chunk))]with open(ofile,’a’) as outfile:** while True:** for i in range(len(k_by)):** if i >= len(k_by):** break** sorted_k_by = sorted(k_by) if ascending else sorted(k_by,reverse=True)** if k_by[i] == sorted_k_by[0]:** outfile.write(k_row[i])** k_row[i] = file_chunk[i].readline()** if not k_row[i]:** file_chunk[i].close()** del(file_chunk[i])** del(k_row[i])** del(k_by[i])** else:** k_by[i] = parse_type(k_row[i].split(sep)[by].strip())** if len(k_by)==0:** breakdef external_sort(file_path,by,ofile,tmp_dir,ascending=True,chunksize=50000,sep=’,’,usecols=None,index_col=None):os.makedirs(tmp_dir,exist_ok=True)** try:** data_chunk = pd.read_csv(file_path,sep=sep,usecols=usecols,index_col=index_col,chunksize=chunksize)** for chunk in data_chunk:** chunk = chunk.sort_values(by,ascending=ascending)** chunk.to_csv(tmp_dir+”/“+”chunk”+str(int(time.time()107))+str(uuid.uuid4())+”.csv”,index=None,sep=sep) merge_sort(tmp_dir,ofile=ofile,by=by,ascending=ascending,sep=sep)* except Exception:** print(traceback.format_exc())** finally:** shutil.rmtree(tmp_dir, ignore_errors=True)if name == “main“: infile = “D:/python_question_data/orders.txt”** ofile = “D:/python_question_data/extra_sort_res_py.txt”** tmp = “D:/python_question_data/tmp”** external_sort(infile,’amount’,ofile,tmp,ascending=True,chunksize=1000000,sep=’\t’) | 函式解析字串的資料型別函式計算要排序的列名在表頭中的位置函式外存歸併排序列出臨時檔案開啟臨時檔案讀取表頭計算要排序的列在表頭的位置寫出表頭讀取正文第一行維護一個 k 個元素的列表,存放 k 個排序列值排序,維護的列表升序正向,降序反向寫出最小值對應的行讀完一個檔案處理一個如果檔案沒讀完更新維護的列表迴圈計算所有檔案讀完結束函式外存排序建立臨時檔案目錄分段讀取需排序的檔案分段排序寫出排好序的檔案外存歸併排序刪除臨時目錄主程式呼叫外存排序函式 |

這裡是用逐行歸併寫出的方式完成外存排序的,由於 pandas 逐行讀取的方式效率非常低,所以沒有藉助 pandas 完成逐行歸併排序。讀者感興趣的話可以嘗試使用 pandas 按塊歸併,比較下兩者的效率。

相比於聚合和過濾,這個程式碼相當複雜了,對於很多非專業程式設計師來講已經是不太可能實現的任務了,而且它的運算效率也不高。

以上程式碼也僅處理了規範的結構化檔案和單列排序。如果檔案結構不規範比如不帶表頭、各行的分隔符數量不同、排序列是不規範的日期格式或者按照多列排序等等情況,程式碼還會進一步複雜化。

四、 分組

大檔案的分組彙總也很麻煩,一個容易想到的辦法是先將檔案按分組列排序,然後再遍歷有序檔案,如果分組列值和前一行相同則彙總在同一組內,和前一行不同則新建一組繼續彙總。如果結果集過大,還要看情況把計算好的分組結果及時寫出。

這個演算法相對簡單,但效能很差,需要經過大排序的過程。一般資料庫會使用 Hash 分組的方案,能夠有效地提高速度,但程式碼複雜度要高出幾倍。普通非專業人員基本上沒有可能寫出來了。這裡也就不再列出程式碼了。

通過以上介紹,我們知道,Python 處理大檔案還是非常費勁的,這主要是因為它沒有提供為大資料服務的遊標型別及相關運算,只能自己寫程式碼,不僅繁瑣而且運算效率低。

Python不方便,那麼還有什麼工具適合非專業程式設計師來處理大檔案呢?

esProc SPL在這方面要要比 Python 方便得多,SPL 是專業的結構化資料處理語言,提供了比 pandas 更豐富的運算,內建有遊標資料型別,解決大檔案的運算就非常簡單。比如上面這些例子都可以很容易完成。

一、 聚合

| | A |
| 1 | =file(file_path).cursor@tc() |
| 2 | =A1.total(sum(col)) |

二、 過濾

| | A | B |
| 1 | =file(file_path).cursor@tc() | |
| 2 | =A1.select(key==condition) | |
| 3 | =A2.fetch() | /小結果集直接讀出 |
| 4 | =file(out_file).export@tc(A2) | /大結果集可寫入檔案 |

三、 排序

| | A |
| 1 | =file(file_path).cursor@tc() |
| 2 | =A1.sortx(key) |
| 3 | =file(out_file).export@tc(A2) |

四、 分組

| | A | B |
| 1 | =file(file_path).cursor@tc() | |
| 2 | =A1.groups(key;sum(coli):total) | /小結果集直接返回 |
| 3 | =A1.groupx(key;sum(coli):total) | |
| 4 | =file(out_file).export@tc(A3) | /大結果集寫入檔案 |

特別指出,SPL 的分組彙總就是採用前面說過的資料庫中常用的 HASH 演算法,效率很高。

SPL中還內建了平行計算,現在多核 CPU 很常見,使用平行計算可以大幅度提高效能,比如分組彙總,只多加一個 @m 就可以變成平行計算。

| | A |
| 1 | =file(file_path).cursor@mtc() |
| 2 | =A1.groups(key;sum(coli):total) |

而 Python 寫平行計算的程式就太困難了,網上說啥的都有,就是找不到一個簡單的辦法。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章