excel格式数据不能像csv格式一样方便的实现分块的读取。对于csv可以通过dask库和或者pd.read_csv的chunksize参数实现流式加载和运算。为了避免对excel大文件读取或运算过程中的内存不足,通过流式加载excel再进行处理,节约内存使用。
流式加载和计算主要有两种思路
1、将excel转化为csv实现流式加载和计算。
2、分块读取excel实现流式加载和计算。该方案会严重影响读写速度,如果分为n个块,则读取的时间为一次读取的n倍。仅适用于数据过大无法读入内存的情况。
这种方法主要是用了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)
考虑到代码的通用性和性能,本文当前主要使用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)