Openpyxl:如何解决打开工作簿的问题

Openpyxl: How to troubleshoot opening a workbook

tl;博士: 在尝试加载工作簿时找出挂起的内容有哪些好的第一步?

长版: 所以,我遇到了一些“为什么它不起作用”,比如关于 openpyxl 的问题,但在 discover/fix 问题的实际尝试中并没有看到太多。

我刚开始查看 openpyxl,它看起来很有希望,但刚开始时,我 运行 遇到了一个问题:我有各种非常复杂的工作簿。我想尝试至少从他们那里读取数据。我正在使用的工作簿不是 巨大 (~750kB),但它确实包含很多内容:条件格式、数据验证、命名范围、vba 内容等. 当我尝试打开工作簿时,我收到有关数据验证的警告(好吧,没什么大不了的),但随后它在 CPU 上启动并且 long[=31= 没有完成任何事情] 时间 - 我不知道它是否会结束,因为不可避免地,我需要继续前进,所以我退出了。不管怎样,加载,如果它能完成的话,太慢了,没有用。

所以,如果有人可以建议一些坚实的第一步来确定阻碍是什么,我会很高兴,这样我就可以尝试通过从工作簿中删除违规内容或理想情况下做一些事情来完成这项工作在 python 那边处理事情更顺利。

为清楚起见,这是我开始时使用的两行代码:

from openpyxl import Workbook, load_workbook
wb = load_workbook('book.xlsm')

正如@CharlieClark 所猜测的那样,我的特定工作簿的问题是整个列的数据验证集。为了给这个问题提供一个令人满意的答案,我还是做了一些实验,试图看看我是如何自己推断出来的。因为我认为我不可能写出一个完全涵盖其他人问题的方法,所以我尝试了两种方法来查看问题,基于@BoarGules 和@CharlieClark 的建议,并将它们写成示例:

方法一:将工作簿拆分成更小的部分,比较加载

如果您想弄清楚是什么阻碍了这个过程,我建议您仔细考虑工作簿包含的内容以及可能导致 openpyxl 进行大量额外处理的原因(方法 2 中对此有更多介绍)。而不是简单地将每个 sheet 拆分成一个新工作簿并尝试加载每个工作簿(我这样做了,但大多数较小的工作簿都不会加载 - 我的大多数 sheet 具有基本相同的结构和相同的加载问题),我会尝试考虑您有哪些内容 - 数据验证、条件格式、您有什么 - 并一次删除一种内容类型。

当我删除了工作簿中的所有数据验证后,它突然加载得很快!

方法 2:更加熟悉源代码并尝试 profile 解决问题所在

我发现这种方法更令人满意,因为我至少现在对 openpyxl 如何加载工作簿有了一个(非常模糊的)想法,但这种方法确实需要深入研究源代码来思考问题 - 如果您不想这样做,请坚持使用方法 1。此方法还需要有一个良好的示例工作簿,可以加载 OK 以进行比较。对我来说,因为我的第一次尝试是遵循@CharlieClark 的猜测并删除所有数据验证,所以我使用 'fixed' 工作簿进行比较,这有点作弊,但是哦。

使用我的 good 工作簿,我 运行 快速了解 workbook_load 函数以查看其外观。我发现按 'tottime' 或在 每个函数中花费的总时间 对结果排序最有用:

>>>import profile
>>>from openpyxl import load_workbook
>>>profile.run('wb = load_workbook("good.xlsm")', sort='tottime')

     4228306 function calls (4186125 primitive calls) in 12.859 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    84364    1.828    0.000    6.234    0.000 worksheet.py:138(parse_cell)
   267315    0.625    0.000    0.625    0.000 :0(get)
   197422    0.594    0.000    0.953    0.000 ElementTree.py:1286(read_events)
      284    0.578    0.002    0.578    0.002 :0(feed)
12986/8974    0.516    0.000    2.125    0.000 serialisable.py:42(from_tree)
   538565    0.500    0.000    0.500    0.000 :0(isinstance)
    89339    0.500    0.000    1.000    0.000 cell.py:43(coordinate_from_string)
24877/9318    0.438    0.000    0.984    0.000 serialisable.py:187(__hash__)
   193137    0.422    0.000    0.422    0.000 :0(match)
     5119    0.422    0.000    6.734    0.001 worksheet.py:259(parse_row)
    73194    0.406    0.000    0.562    0.000 base.py:40(__set__)
   168920    0.375    0.000    0.375    0.000 :0(find)
   197144    0.344    0.000    1.906    0.000 ElementTree.py:1218(iterator)
   251298    0.312    0.000    0.312    0.000 :0(getattr)
    84364    0.312    0.000    0.812    0.000 cell.py:106(__init__)
    84364    0.297    0.000    1.203    0.000 cell.py:181(coordinate_to_tuple)
...

我 运行 加载这个和其他一些工作簿的配置文件,看起来执行时间主要花在 worksheet.py 模块(如上)和 serialisable.py 模块,我认为这是有道理的,因为这是大部分读取/处理数据发生的地方。

为了比较,当我让 糟糕的 工作簿加载一段时间然后中止时,这是我得到的配置文件:

>>>import profile
>>>from openpyxl import load_workbook
>>>profile.run('wb = load_workbook("bad.xlsm")', sort='tottime')

     14111962 function calls (14076527 primitive calls) in 27.797 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  3045757    9.797    0.000   20.359    0.000 cell.py:157(rows_from_range)
6091514/6091513    7.062    0.000   10.562    0.000 cell.py:166(<genexpr>)
  3045783    3.500    0.000    3.500    0.000 :0(format)
       19    2.797    0.147   23.156    1.219 :0(extend)
       19    0.469    0.025   23.625    1.243 datavalidation.py:59(expand_cell_ranges)
   366802    0.375    0.000    0.375    0.000 :0(isinstance)
24877/9318    0.344    0.000    0.672    0.000 serialisable.py:187(__hash__)
    15947    0.234    0.000    1.125    0.000 worksheet.py:138(parse_cell)
12686/8831    0.219    0.000   25.172    0.003 serialisable.py:42(from_tree)
   250829    0.188    0.000    0.188    0.000 :0(getattr)
       90    0.172    0.002    0.172    0.002 :0(feed)
    63452    0.172    0.000    0.297    0.000 base.py:40(__set__)
    54974    0.125    0.000    0.125    0.000 :0(get)
    44462    0.125    0.000    0.125    0.000 :0(match)
    18172    0.109    0.000    0.203    0.000 cell.py:43(coordinate_from_string)
    56006    0.094    0.000    0.156    0.000 ElementTree.py:1286(read_events)
    83605    0.078    0.000    0.078    0.000 base.py:25(__set__)
    17709    0.078    0.000    0.141    0.000 sequence.py:24(__set__)
...

因此查看此配置文件,您可以看到大部分执行时间都花在了处理单元格地址 (rows_from_range) 上,而不是查看实际数据,就像我们在第一个配置文件中看到的那样。我在这里假设这是不需要的。如果您查看配置文件 table 中的第五行,我们也在 datavalidation.py 函数 expand_cell_ranges 中或下方(cumtime 列)花费了大量时间,事实上,它只被调用了几次,并且没有出现在其他配置文件顶部附近的任何地方。当我翻阅源代码时,我看到 expand_cell_ranges 函数循环调用 rows_from_range 函数!我认为从那里,我们可以合理地得出结论,在这种情况下,与数据验证有关的某些事情导致 openpyxl 尝试处理一大堆毫无用处的单元格地址。由于我已经知道我的工作簿为整列空单元格设置了数据验证集,因此我认为这是对诊断的非常可靠的确认。

如果阅读本文的任何人需要尝试通过反向工程来诊断无法加载的工作簿,我会将上面的第一个配置文件与加载问题工作簿的配置文件进行比较,看看有什么变化。这至少应该为猜测为什么它改变提供了一个很好的起点。