excel和csv表格文件流式处理降低内存需求以及并行化读取——分块读写和计算

excel格式数据不能像csv格式一样方便的实现分块的读取。对于csv可以通过dask库和或者pd.read_csv的chunksize参数实现流式加载和运算。为了避免对excel大文件读取或运算过程中的内存不足,通过流式加载excel再进行处理,节约内存使用。

流式加载和计算主要有两种思路
1、将excel转化为csv实现流式加载和计算。
2、分块读取excel实现流式加载和计算。该方案会严重影响读写速度,如果分为n个块,则读取的时间为一次读取的n倍。仅适用于数据过大无法读入内存的情况。

一、将excel转化为csv实现流式加载和计算

这种方法主要是用了pandas中read_csv种的chunksize方法。
注意1:读取excel后写入csv后要写入utf-8格式
注意2:即读取的csv和存入的csv不能同路径。否则会不停的迭代下去,不能退出循环。因为使用chunksize分块读取后,pandas并没有真正的将csv的内容加载入内存,只是解析了csv的内容和建立了连接(类似浅拷贝),在调用迭代器时再从csv中加载。所以再使用追加写的时候,会一边写入csv,一边再从csv中读取,形成死循环。

下面的例子自动匹配了excel的后缀和适配了csv的编码方式

import os
import pandas as pd
import numpy as np                                                
from chardet.universaldetector import UniversalDetector

def encode_to_utf8(filename, des_encode):
    # 逐个读取文件的编码方式

    with open(filename, 'rb') as f:
        detector = UniversalDetector()
        for line in f.readlines():
            detector.feed(line)
            if detector.done:
                break
        original_encode = detector.result['encoding']

    # 逐个读取文件的内容

    with open(filename, 'rb') as f:
        file_content = f.read()

    file_decode = file_content.decode(original_encode, 'ignore')
    file_encode = file_decode.encode(des_encode)
    with open(filename, 'wb') as f:
        f.write(file_encode)


##加载

def load_Table_stream(file_path, col_name,row_count):
    name, ext = os.path.splitext(file_path)
    if '.csv' ==ext:
        encode_to_utf8(file_path, des_encode="utf-8")
        df_read = pd.read_csv(file_path, usecols=col_name,chunksize=row_count)
    if ".xls" ==ext:
        df_read = pd.read_excel(file_path, usecols=col_name)
        #转化为csv再分块读
        file_path_csv=file_path.replace(".xls", ".csv")
        df_read.to_csv(file_path_csv, index=False,encoding='UTF-8')
        #encode_to_utf8(file_path_csv, des_encode="utf-8")
        df_read= pd.read_csv(file_path_csv, usecols=col_name,chunksize=row_count)
    if ".xlsx" ==ext:
        df_read = pd.read_excel(file_path, usecols=col_name)
        file_path_csv=file_path.replace(".xlsx", ".csv")
        # 转化为csv再分块读
        df_read.to_csv(file_path_csv, index=False,encoding='UTF-8')
        #encode_to_utf8(file_path_csv, des_encode="utf-8")
        df_read= pd.read_csv(file_path_csv, usecols=col_name,chunksize=row_count)
    return df_read

###############################调用##############################
col_name=["A","B"]
file=r"xx"
df_iter=load_Table(file, col_name,2)

分块进行读取,对每一块进行处理后,追加写入csv。通过硬盘存储,代替内存,实现节约内存的目的。

假设func_work是一个需要处理的业务函数。

def func_work(ff):    
    return df 

for df in df_iter:
df=func_work(pf)
df.to_csv(file_result_csv,mode=“a”, index=False,encoding=‘UTF-8’,header=False)

二、分块读取excel实现流式加载和计算

考虑到代码的通用性和性能,本文当前主要使用pandas库和 openpyxl能实现。首先通过openpyxl的ead_only读出excel的最大行数,然后对对打行数分块,然后使用pandas的skiprows和nrows参数进行分块读写。
注:excel在读取分为获取数据和数据解析两部分,其中获取数据时阻塞的,只有一个进程能访问,因此除非解析数据部分的工作量非常大,否则不能直接通过多进程加速。可以通过os模块将原excel复制多个副本,然后多进程分别读取不同的部分。多进程比较消耗内存,本文主要时解决内存不足的问题,没有通过该方法加速读取。
1、获取excel最大行数
通过openpyxl 的 read_only参数可以在只读模式下,获取表的结构而不加载整个表,速度比加载表可以提升百倍(但是如果加载表需要逐行读取,反而比一次性加载慢的多)。获取表结构后得到excel表的最大行max_row,按行分块。
注1:max_row获取的是最大的有数据的行,不剔除空行。
注2:按列分块使用max_column参数

def excel_max_row(file_path, sheetname):
    ws = load_workbook(file_path, read_only="read_only")
    wb = ws[sheetname]
    #print(f"文件最大行数{wb.max_row}")
    return (wb.max_row)

2、对excel的行数进行分块
2.1、按步长分块

使用range的step来分块

ls_step=[]
max=10
step=3

for i in range(0,max,step):
    if i+step<=max:
        ls_step.append((i,i+step))
    else:
        ls_step.append((i,max))

输出每个块的起始和结束位置
print(ls_step)

[(0, 3), (3, 6), (6, 9), (9, 10)]

2.2 、按数量分块
一方面可以转换为指定步长,另一方面也可以用以下方法
先把余数放在最后,每个分块按照均分的数量作为步长进行分配。
chunk_size是每个块的大小,也是每次分块的步长。max_row // n 其中// 是除法向下取整,得到一个整数的步长。
mode_size 是对最大行数按n整除后的余数,对于不能整除的组后一个块是 tuple_max = (max_row - mode_size, mode_size)

chunk_size = max_row // n
mode_size = max_row % n

将分块数据放在列表中用于下一步的遍历,列表中的元素为元组。元组的两个元素分别用于每次读取excel的起始位置和读了多少行skiprows=start, nrows=row_num_read。

注意:分块的for循环中range(n+1)是n+1,因为第n个块有数据,但是range(n)不包括n。最终也是分成了n+1个区间,前n个均分,最后一个是余下的部分。如果最大行数能均分则余数是0,最后一个块只读一行。

    def chunk_size_list(max_l, n):
        lst = []
        chunk_size = max_l // n
        mode_size = max_l % n
        if mode_size == 0:
            for i in range(n + 1):  # 是n+1,因为第n个块有数据,但是range(n)不包括n
                lst.append(((i * chunk_size, (i+1)*chunk_size)))

        if mode_size > 0:
            for i in range(n):# 是n,因为第n+1数据已经在i+1体现
                lst.append(((i * chunk_size, (i+1)*chunk_size)))
            tuple_max = (max_l - mode_size, max_l)
            lst.append(tuple_max)

        return lst

3、按照分好的行数,分块读取excel,将功能分装成函数

def read_data(file_path, start, row_num_read):
    df = pd.read_excel(file_path, skiprows=start, nrows=row_num_read)
    return df

4、分块进行流处理

分块进行读取,对每一块进行处理后,追加写入csv。通过硬盘存储,代替内存,实现节约内存的目的。

假设func_work是一个需要处理的业务函数。

def func_work(ff):    
    return df 

该部分的可以将功能封装为一个函数,功能为流式处理

def read_excel_stream(path,n,sheetname="Sheet1"):
    max_row=excel_max_row(path, sheetname)
    ls_chunk=chunk_size_list(max_row, n)
    print(ls_chunk)
    for row_read in ls_chunk:
        pf=read_data(path, row_read[0], row_read[1])
        df=func_work(pf)
        df.to_csv(file_result_csv,mode="a", index=False,encoding='UTF-8',header=False)

5、以分块读取excel然后合并成一个表为例,整体代码如下:

import time
import pandas as pd
import numpy as np
from openpyxl import load_workbook

#获取excel最大行数
def excel_max_row(file_path, sheetname):
    ws = load_workbook(file_path, read_only="read_only")
    wb = ws[sheetname]
    print(f"文件最大行数{wb.max_row}")
    return (wb.max_row)
#对excel的行数进行分块
    def chunk_size_list(max_l, n):
        lst = []
        chunk_size = max_l // n
        mode_size = max_l % n
        if mode_size == 0:
            for i in range(n + 1):  # 是n+1,因为第n个块有数据,但是range(n)不包括n
                lst.append(((i * chunk_size, (i+1)*chunk_size)))

        if mode_size > 0:
            for i in range(n):# 是n,因为第n+1数据已经在i+1体现
                lst.append(((i * chunk_size, (i+1)*chunk_size)))
            tuple_max = (max_l - mode_size, max_l)
            lst.append(tuple_max)

        return lst


#按照分好的行数,分块读取excel
def read_data(file_path, start, row_num_read):
    df = pd.read_excel(file_path, skiprows=start, nrows=row_num_read)
    return df

#将功能封装为一个函数,功能为流式读取
def read_excel_stream(path,n,sheetname="Sheet1"):
    max_row=excel_max_row(path, sheetname)
    ls_chunk=chunk_size_list(max_row, n)
    print(ls_chunk)
    ls_df=[]
    for row_read in ls_chunk:
        pf=read_data(path, row_read[0], row_read[1])
        ls_df.append(pf)

    #results = Parallel(n_jobs=2)(delayed(read_data)(path1, start, row_num) for start, row_num in ranges)
    #合并结果。后面的df没有列名,需要转化为np再合并,再转为df, 否则会把各df的第一行当做列名,然后对齐
    col = ls_df[0].columns
    lst_np=[]
    lst_col_n=[]

    #剔除空行和给有列名无数据的列进行填充
    for i in ls_df:
        np_i=i.values
        a,b=np_i.shape[0],np_i.shape[1]
    #获取最大的列号,用于下一步的填充,即对最大的列好,没数据的填充nan
        if a*b!=0:
            lst_np.append(np_i)
            lst_col_n.append(b)

    #对于只有部分行有数据的列进行填充,否则无法合并
    max_c = max(lst_col_n)
    d_np=[]
    for j in lst_np:
        if j.shape[1]

三、优化思路

两种思路可以进一步优化,如第一种思路可以使用pickle等模块序列化中间过程增加读写速度或者使用polars库等高性能库读写等。第二种使用通过多进程的生产者-消费者模型,一部分cpu加载excel,另一部分cpu进行运算。由于时间关系不载进行编写,希望有读者编写后分享。

思路一

使用openpyxl只读模式,read_only=True快速读取列名,然后通过不同的列名多进程读取表,转换为pandas。
其中转pandas 可以直接调用pandas或者polars的read_excel也可以使用openpyxl的rows = ws.iter_rows(min_row=xx, max_row=xx),然后逐行读取转pandas。

openpyxl文档
https://openpyxl.readthedocs.io/en/stable/

定义:

def load_workbook(filename, read_only=False, keep_vba=KEEP_VBA, data_only=False, keep_links=True)

参数:
read_only:是否只读,默认False
keep_vba:是否使用VBA编程,默认False
data_only:是否只加载数据值,即丢弃公式、排序等操作,默认False
keep_links:是否保留超链接,默认True

openpyxl逐行读取转pandas

from itertools import islice
data = ws.values
cols = next(data)[1:]
data = list(data)
idx = [r[0] for r in data]
data = (islice(r, 1, None) for r in data)
df = DataFrame(data, index=idx, columns=cols)

使用openpyxl只读模式,read_only=True快速读取列名

import os
import pandas as pd 
from openpyxl import load_workbook
#加载工作薄和工作表
wb = load_workbook(path_xlsx, read_only=True)
#加载工作表方法1
ws = wb['Sheet1']
#加载工作表方法2
sheet = wb.worksheets[0]

openpyxl没有columns属性,他的属性类似vba都是对应得excel操作。对于列名在第一行的表使用以下两种方法获取列名。

获取列名方法1
先生成一个迭代器,然后用用next()方法获取第一列

rows = ws.iter_rows(min_row=1, max_row=1) # returns a generator of rows
first_row = next(rows) # get the first row
headings = [c.value for c in first_row] # extract the values from the cells

获取列名方法2
#读一行就break

lst_col=[]
for row in ws.rows:
    for cell in row:
        lst_col.append(cell.value)
    break
    

并行化读取

本代码进行了并行读取和解析,但是并没有取得效果。

1、使用read_only=True模式,该模式读取快,但是不能进行多进程加速。因为该模式本质是懒加载的,先生成一个迭代器再逐步从磁盘读取。读取磁盘的过程会导致多进程阻塞,无法并行,会顺序执行。实验了先读取文件再多进程分析生成器的方式、在每个进程中分别加载xlsx文件和将文件复制多个副本,每个进程读取副本三种方式,均无法加速。

with open(xlsx_filename, "rb") as f:
    in_mem_file = io.BytesIO(f.read())

2、不使用read_only模式,该方式由于是一次加载入内存,使用实验了先读取文件再多进程解析是可以多进程的,但是该模式加载时间过长,不如read_only单线程。

整体代码如下:

import io
import pandas as pd
import time
from openpyxl import load_workbook
from collections import deque
from joblib import Parallel, delayed

def chunk_size_list(max_l, n):
    lst = []
    chunk_size = max_l // n
    mode_size = max_l % n
    if mode_size == 0:
        for i in range(n + 1):  # 是n+1,因为第n个块有数据,但是range(n)不包括n
            lst.append(((i * chunk_size, (i+1)*chunk_size)))

    if mode_size > 0:
        for i in range(n):# 是n,因为第n+1数据已经在i+1体现
            lst.append(((i * chunk_size, (i+1)*chunk_size)))
        tuple_max = (max_l - mode_size, max_l)
        lst.append(tuple_max)

    return lst


def read_v1(r):
    t1 = time.time()
    que = deque()
    wb = load_workbook(filename=xlsx_filename, read_only=True)
    ws = wb['Sheet1']

    for row in ws.iter_rows(min_row=r[0], max_row=r[1]):
        ls = [cell.value for cell in row]
        que.append(ls)
    #df=pd.DataFrame(que)
    t = time.time() - t1
    print(f"{r}耗时{t}", t1)
    wb.close
    df=pd.DataFrame(que)
    return df

def read_v2(r):
    t1=time.time()
    lst=[]
    wb = load_workbook(filename=xlsx_filename, read_only=True)
    ws = wb['Sheet1']

    for row in ws.iter_rows(min_row=r[0], max_row=r[1]):
        ls=[cell.value for cell in row]
        lst.append(ls)
    #df=pd.DataFrame(lst)
    t=time.time()-t1
    print(f"{r}耗时{t}",t1)
    return df


def read_vv1(in_mem_file ,r):
    t1 = time.time()
    que = deque()


    wb = load_workbook(in_mem_file,read_only=True)
    ws = wb['Sheet1']

    for row in ws.iter_rows(min_row=r[0], max_row=r[1]):
        ls = [cell.value for cell in row]
        que.append(ls)
    t = time.time() - t1
    print(f"{r}耗时{t}", t1)

    return que

if __name__ == '__main__':
    xlsx_filename = r"C:\Users\Administrator\Desktop\XX"
    with open(xlsx_filename, "rb") as f:
        in_mem_file = io.BytesIO(f.read())
    t1=time.time()

    lst_r = chunk_size_list(190000, 6)
    res = Parallel(n_jobs=3)([delayed(read_vv1)(in_mem_file,r) for r in lst_r])
    print(time.time()-t1)
    r=[1,19000]
    read_v1(r)

你可能感兴趣的:(Python常用小框架,excel)