0%

判断Excel文件中是否存在合并单元格

背景

​ 在用例管理平台上可以编辑Excel用例文件,但如果上传了合并单元格的Excel用例文件,编辑时候处理起来会无法判断如何去拆解单元格,最终解决方式是:禁止上传存在合并单元格的Excel文件。(那么就需要在上传的时候校验文件中是否存在合并单元格。

​ 通过网上的资料查询了下,实现方案就是通过 sheet.merged_cells 来解决该问题,核心代码如下:

1
2
3
4
workbook = xlrd.open_workbook(r'py.xlsx')
sheet = workbook.sheet_by_index(0)
# 如果该值返回的列表长度非0则存在合并单元格,否则不存在。
return sheet.merged_cells

PS: 本文主要是记录下过程中遇到的问题以及一些扩展。

问题

问题1: 后端报错:expected str, bytes or os.PathLike object, not FileStorage

​ 由于文件是直接通过前端上传的,所以传到后端是一个 FileStorage 对象,通过上述方式实现会直接报错。如果仅仅是简单的通过 file_storage.read() 转换成 bytes 后,仍然会有其他新的报错(embedded null byte)。

​ 最终使用如下方案来进行解决(因为后端原本都是使用 Pandas 库去处理Excel文件的):

1
2
3
4
5
6
7
8
9
book = openpyxl.load_workbook(case_file)
writer = pd.ExcelWriter(case_file, engine='openpyxl')
writer.book = book

writer.sheets = dict((ws.title, ws) for ws in book.worksheets)
for sheet in writer.sheets.values():
merged_list = sheet.merged_cells
if len(merged_list.ranges) > 0:
raise ValueError("【%s】文件内容错误, 暂不支持文件中存在合并的单元格。" % case_file.filename)

验证了下,在上传带有单元格的文件时,会触发异常,正准备高高兴兴码完代码休息下~( ̄▽ ̄~)(~ ̄▽ ̄)~ 。然而出于基本的开发素质,我又验证了不带单元格的文件。然而打脸总是来的这么突然。。。😭。

​ 上传失败,后端返回错误 OSError: [Errno 22] Invalid argument。(事实再次证明:不要以为功能简单就不验证( ̄ε  ̄) 。。。老铁,来吧!继续撸袖子修BUG。)

问题2: OSError: [Errno 22] Invalid argument

​ 通过本地调试,发现触发异常的位置在文件被上传保存后,再次去读取该文件内容的时候。抛错的代码为:

1
f = pd.ExcelFile(file_abs_path)

看了下文件的剧对路径,确定文件是存在的,然后去下载下来打开时,提示文件打开遇到错误。。。(并且此时的文件大小小于实际文件的大小,原文件17KB,上传后的文件12KB)。

file-storage-4.png

​ 通过一步步的调试,发现罪魁祸首在于 openpyxl.load_workbook(case_file),该操作会直接对传入的 FileStorage 对象进行操作,导致文件内容变化。

1
2
3
4
5
6
# 原数据 case_file.read() 大小为16804,处理后case_file.read() 大小为11528
openpyxl.load_workbook(case_file)

# 对于为什么文件大小会变小,是因为操作中会将缓存内的文件内容读取,导致实际在缓存内的内容变少,有兴趣的可以深入研究,核心代码如下:
reader = ExcelReader(filename, read_only, keep_vba, data_only, keep_links)
reader.read()

最终解决方案则是:在问题1的代码块后, 新增代码:case_file.stream.seek(0) 即可,通过将原 FileStorage 对象缓存里面的内容指向最初的位置即可。

问题3: xlrd.biffh.XLRDError: File size is 0 bytes

在获得最终解决方案的过程之前,还遇到了问题3,因为 FileStorage 对象在内存中的内容已经被拿出来操作了,实际缓存中的数据大小变成了0,导致了上述的报错。(参考链接),如下是对于 FileStorage 对象的一个注解。

1
2
3
4
5
6
7
8
9
from werkzeug.datastructures import FileStorage

class FileStorage(object):
"""The :class:`FileStorage` class is a thin wrapper over incoming files.
It is used by the request object to represent uploaded files. All the
attributes of the wrapper stream are proxied by the file storage so
it's possible to do ``storage.read()`` instead of the long form
``storage.stream.read()``.
"""

总结

​ 通过上述一系列的骚操作(瞎折腾)后,终于完成了最初的需求。。。╮(╯Д╰)╭ ,因为过程中很多奇奇怪怪得报错导致整个过程还是挺艰辛的。

⚠️ 心得:大多数时候都是网上直接搜了解决方案,但是由于基础不扎实,在进行实施的时候,各种坑就埋在你脚下了。。。有些时候需要更多的去思考或者了解为什么使用这个方案,以及询问自己更多次为什么会出现这个问题,而不仅仅是解决。

------------- 本 文 结 束 感 谢 您 的 阅 读 -------------