如何加快读取小型电子表格的速度?

How can I speed up reading from small spreadsheet?

我有一个相对中等大小的电子表格 - 212 行 x 56 列数据。

我有一个循环,当我的搜索距离电子表格的底部越近时,它会逐渐变慢。如果可以在 200 毫秒到 7000 毫秒之间快速响应 return。

我怎样才能加快搜索速度,使时间至少保持不变或至少显着加速,使其永远不会超过 500 毫秒。

这是我打开电子表格的方式:

wb = openpyxl.load_workbook('data/%s' % filename, read_only=True)
sheet = wb.get_sheet_by_name('Service%s' % service)

这是我的循环:

for i in range(3, sheet.max_row+1):
    if str(sheet.cell(row=i, column=1).value) == country:
        for x in range(2, sheet.max_column+1):
            if weight > float(sheet.cell(row=2, column=sheet.max_column).value):
                abort(404, "Maximum Weight Exceeded for Service Class")

            if weight < float(sheet.cell(row=2, column=2).value):
                return float(sheet.cell(row=i, column=2).value)

            if weight == float(sheet.cell(row=2, column=x).value):
                return float(sheet.cell(row=i, column=x).value)

            if weight < float(sheet.cell(row=2, column=x).value):
                return float(sheet.cell(row=i, column=x).value)

编辑:

根据大家的建议,我重构了方法。它看起来要快得多,但是我不确定如何在嵌套在 for 循环中时访问 specific 行。新代码如下:

if weight > float(sheet.cell(row=2, column=sheet.max_column).value):
    abort(404, "Maximum Weight Exceeded for Service Class")

minweight = float(sheet.cell(row=2, column=2).value)

for row in sheet.rows:
    if row[0].value == country:
        if weight < minweight:
            return row[1].value

        for cell in row[1:]: # skip first item
            if weight <= float(cell.value):
            # This is wrong. I need to compare weight to cell values in the 2nd row
                return float(cell.value)

编辑 2 - 现在运行约 300 毫秒:

if weight > float(sheet.cell(row=2, column=sheet.max_column).value):
    abort(404, "Maximum Weight Exceeded for Service Class")

minweight = float(sheet.cell(row=2, column=2).value)

ignore_first_row, weight_list = islice(sheet.rows, 0, 2)

for row in islice(sheet.rows, 2, sheet.max_row):
    if row[0].value == country:
        if weight < minweight:
            return row[1].value # return country's min rate

        for ratecell, weightcell in izip(row, weight_list):
            if weight <= float(weightcell.value):
                return float(ratecell.value)

以下是我的一些直接想法:

for i in xrange(3, sheet.max_row+1):
    if str(sheet.cell(row=i, column=1).value) == country:

        if weight > float(sheet.cell(row=2, column=sheet.max_column).value):
            abort(404, "Maximum Weight Exceeded for Service Class")
        if weight < float(sheet.cell(row=2, column=2).value):
            return float(sheet.cell(row=i, column=2).value)

        for x in xrange(2, sheet.max_column+1):
            if weight <= float(sheet.cell(row=2, column=x).value):
                return float(sheet.cell(row=i, column=x).value)

这会将您的两个逻辑检查移到一起(<=)并将另外两个移到循环外

此外,根据您计算 weight 的位置,此语句应位于代码中的其他位置:

if weight > float(sheet.cell(row=2, column=sheet.max_column).value):
        abort(404, "Maximum Weight Exceeded for Service Class")

它不使用 ix,因此您无需在每次循环命中时都浪费时间检查它

你能解释一下这个块应该做什么吗:

if weight < float(sheet.cell(row=2, column=2).value):
    return float(sheet.cell(row=i, column=2).value)

在您的循环中,weight 没有改变。这是一个静态检查,它将 return 从你的函数中利用任何 i 的当前值。考虑到您显示的代码,这没有意义。

我生成了一个包含 1 sheet 的 xlsx 文件,其中包含 57 列和 200 行。每个列栏最后包含一个随机生成的 100 个字符的字符串,最后一列是一个 6 个字符的任意但非随机序列,用作搜索目标。

以下代码,使用 sheet.rows 大约快 7 倍(350 毫秒):

for row in sheet.rows:
    if str(row[sheet.max_column-1].value) == needle:
        # needle defined to match only the last row
        print 'found'
        break

比您的代码(2400 毫秒)的精简版:

for i in xrange(1, sheet.max_row+1):
    if str(sheet.cell(row=i, column=sheet.max_column).value) == needle:
        # needle defined to match only the last row
        print 'found'
        break

请注意,我有一个 SSD 和一个快速处理器 - YMMV,具体取决于硬件和实际数据。你不能真正保证搜索时间会小于给定时间,除非数据和硬件本质上是常数。

正如我在评论中所说,你真的应该学会使用 cProfile 或类似的方法来对你的代码进行基准测试。

在我的评论中,我还注意到按顺序搜索本质上需要更长的时间才能找到序列中更远的匹配项。要更改搜索的时间复杂度,您需要更改算法,这意味着以不同方式构建数据(即不使用平面文件)。二分搜索通常比顺序搜索快得多,但需要排序的数据。

取决于你还需要做什么(你需要修改sheet中的数据吗?多久一次?你的数据有多大?它真的必须保留在[=56=中吗? ] sheet?) 可能会进一步大大改进您的搜索,或者根本不会。

正如 CharlieClark 在评论中指出的那样,row[-1] 可能比 row[sheet.max_column-1] 快(或者您可以将它带到循环之外,因为您的行的长度始终相同)而您却没有如果您希望这些单元格中有字符串数据,则需要将 cell.value 转换为字符串。


更新: sheet.rows 是一个 属性,returns 是一个生成器,至少在 v2.3.5 中是这样,所以不,除非你使用 itertools.islice.[=26=,否则你不能切片它]

但是,您可以将返回的生成器存储在变量中,调用 .next() 两次以检索并存储前两行,然后迭代其余部分。

row_gen_use_once = sheet.rows
# should really try/except for StopIteration in the next() calls in case there are less than two rows, or else check the row count beforehand
first_row = row_gen_use_once.next()
second_row = row_gen_use_once.next()

for row in row_gen_use_once:
    pass # blah blah do stuff
    # now you can access the second row here :)

或者您可以使用 enumerate 并在循环中保存第二行:

first_row = None
second_row = None

for idx, row in enumerate(sheet.rows):
    if idx == 0:
        first_row = row
    elif idx == 1:
        second_row = row
    else:
        pass
        # blah blah do stuff

这意味着在循环中进行一些额外的检查,但它们不会因分支预测而产生太多开销。

itertools.islice版本,这是我认为最好的解决方案:

from itertools import islice
first_row, second_row = islice(sheet.rows, 0, 2)

for row in islice(sheet.rows, 2, sheet.max_row):
    pass # do stuff

除非您使用的是 Python 3,在这种情况下只需执行:

first_row, second_row, *other_rows = sheet.rows

for row in other_rows:
    pass # do stuff