在 xlsxwriter 中模拟自动调整列

Simulate autofit column in xslxwriter

我想在 Python 的 xlsxwriter 中模拟 Excel 自动调整功能。根据这个url,不直接支持: http://xlsxwriter.readthedocs.io/worksheet.html

但是,遍历 sheet 上的每个单元格并确定列的最大大小并仅使用 worksheet.set_column(row, col, width) 设置宽度。

让我无法写这篇文章的并发症是:

  1. URL 没有指定 set_column 的第三个参数的单位。
  2. 我找不到一种方法来测量要插入到单元格中的项目的宽度。
  3. xlsxwriter 似乎没有回读特定单元格的方法。这意味着我需要在编写单元格时跟踪每个单元格的宽度。如果我可以循环遍历所有单元格会更好,这样就可以编写一个通用例程。

我最近 运行 遇到了同样的问题,这就是我想出的:

r = 0
c = 0
for x in list:
    worksheet.set_column('{0}:{0}'.format(chr(c + ord('A'))), len(str(x)) + 2)
    worksheet.write(r, c, x)
    c += 1

在我的示例中,r 将是您要输出到的行号,c 将是您要输出到的列号(均为 0 索引),x 将是您希望在单元格中的 list 中的值。

'{0}:{0}'.format(chr(c + ord('A'))) 部分采用提供的列号并将其转换为 xlsxwriter 接受的列字母,因此如果 c = 0 set_column 将看到 'A:A',如果 c = 1 然后它会看到 'B:B',依此类推。

len(str(x)) + 2 部分确定您要输出的字符串的长度,然后将其加 2 以确保 excel 单元格足够宽,因为字符串的长度并不完全与单元格的宽度相关。根据您的数据,您可能想要使用而不是添加 2 或更多。

xlsxwriter 接受的单位有点难以解释。当您在 excel 中并将鼠标悬停在可以更改列宽的位置上时,您将看到 Width: 8.43 (64 pixels)。在此示例中,它接受的单位是 8.43,我认为是厘米?但是 excel 甚至没有提供单位,至少没有明确提供。

注意: 我只在包含 1 行数据的 excel 文件上尝试过这个答案。如果有多行,则需要有一种方法来确定哪一行将包含 'longest' 信息,并且仅将其应用于该行。但是,如果无论行如何,每一列的大小都大致相同,那么这对你来说应该没问题。

祝你好运,希望对你有所帮助!

作为一般规则,您希望列的宽度比列中最长字符串的大小稍大。 xlsxwriter 列的 1 个单元的宽度大约等于一个字符的宽度。因此,您可以通过将每列设置为该列中的最大字符数来模拟自动调整。

例如,在使用 pandas 数据帧和 xlsxwriter 时,我倾向于使用下面的代码。

它首先找到索引的最大宽度,它始终是 pandas 到 excel 渲染数据帧的左列。然后,它 returns 所有值的最大值和每个剩余列的列名从左向右移动。

无论您使用什么数据,调整此代码都不会太困难。

def get_col_widths(dataframe):
    # First we find the maximum length of the index column   
    idx_max = max([len(str(s)) for s in dataframe.index.values] + [len(str(dataframe.index.name))])
    # Then, we concatenate this to the max of the lengths of column name and its values for each column, left to right
    return [idx_max] + [max([len(str(s)) for s in dataframe[col].values] + [len(col)]) for col in dataframe.columns]

for i, width in enumerate(get_col_widths(dataframe)):
    worksheet.set_column(i, i, width)

我同意 Cole Diamond 的观点。我需要做一些非常相似的事情,它对我来说效果很好。其中 self.columns 是我的列列表

def set_column_width(self):
    length_list = [len(x) for x in self.columns]
    for i, width in enumerate(length_list):
        self.worksheet.set_column(i, i, width)

我在 Github site of xlsxwriter 上找到了另一种模拟 Autofit 的解决方法。我已将其修改为 return 水平文本(列宽)或 90° 旋转文本(行高)的近似大小:

from PIL import ImageFont

def get_cell_size(value, font_name, font_size, dimension="width"):
    """ value: cell content
        font_name: The name of the font in the target cell
        font_size: The size of the font in the target cell """
    font = ImageFont.truetype(font_name, size=font_size)
    (size, h) = font.getsize(str(value))
    if dimension == "height":
        return size * 0.92   # fit value experimentally determined
    return size * 0.13       # fit value experimentally determined

这不涉及粗体文本或其他可能影响文本大小的格式元素。否则效果很好。

查找自动调整列的宽度:

def get_col_width(data, font_name, font_size, min_width=1):
    """ Assume 'data' to be an iterable (rows) of iterables (columns / cells)
    Also, every cell is assumed to have the same font and font size.
    Returns a list with the autofit-width per column """
    colwidth = [min_width for col in data[0]]
    for x, row in enumerate(data):
        for y, value in enumerate(row):
            colwidth[y] = max(colwidth[y], get_cell_size(value, font_name, font_size))
    return colwidth    

这是支持行和列的 MultiIndex 的代码版本 - 它不是很漂亮,但对我有用。它扩展了@cole-diamond 的回答:

def _xls_make_columns_wide_enough(dataframe, worksheet, padding=1.1, index=True):
    def get_col_widths(dataframe, padding, index):
        max_width_idx = []
        if index and isinstance(dataframe.index, pd.MultiIndex):
            # Index name lengths
            max_width_idx = [len(v) for v in dataframe.index.names]

            # Index value lengths
            for column, content in enumerate(dataframe.index.levels):
                max_width_idx[column] = max(max_width_idx[column],
                                            max([len(str(v)) for v in content.values]))
        elif index:
            max_width_idx = [
                max([len(str(s))
                     for s in dataframe.index.values] + [len(str(dataframe.index.name))])
            ]

        if isinstance(dataframe.columns, pd.MultiIndex):
            # Take care of columns - headers first.
            max_width_column = [0] * len(dataframe.columns.get_level_values(0))
            for level in range(len(dataframe.columns.levels)):
                values = dataframe.columns.get_level_values(level).values
                max_width_column = [
                    max(v1, len(str(v2))) for v1, v2 in zip(max_width_column, values)
                ]

            # Now content.
            for idx, col in enumerate(dataframe.columns):
                max_width_column[idx] = max(max_width_column[idx],
                                            max([len(str(v)) for v in dataframe[col].values]))

        else:
            max_width_column = [
                max([len(str(s)) for s in dataframe[col].values] + [len(col)])
                for col in dataframe.columns
            ]

        return [round(v * padding) for v in max_width_idx + max_width_column]

    for i, width in enumerate(get_col_widths(dataframe, padding, index)):
        worksheet.set_column(i, i, width)

我的版本将遍历一个工作表并自动设置字段长度:

from typing import Optional
from xlsxwriter.worksheet import (
    Worksheet, cell_number_tuple, cell_string_tuple)


def get_column_width(worksheet: Worksheet, column: int) -> Optional[int]:
    """Get the max column width in a `Worksheet` column."""
    strings = getattr(worksheet, '_ts_all_strings', None)
    if strings is None:
        strings = worksheet._ts_all_strings = sorted(
            worksheet.str_table.string_table,
            key=worksheet.str_table.string_table.__getitem__)
    lengths = set()
    for row_id, colums_dict in worksheet.table.items():  # type: int, dict
        data = colums_dict.get(column)
        if not data:
            continue
        if type(data) is cell_string_tuple:
            iter_length = len(strings[data.string])
            if not iter_length:
                continue
            lengths.add(iter_length)
            continue
        if type(data) is cell_number_tuple:
            iter_length = len(str(data.number))
            if not iter_length:
                continue
            lengths.add(iter_length)
    if not lengths:
        return None
    return max(lengths)


def set_column_autowidth(worksheet: Worksheet, column: int):
    """
    Set the width automatically on a column in the `Worksheet`.
    !!! Make sure you run this function AFTER having all cells filled in
    the worksheet!
    """
    maxwidth = get_column_width(worksheet=worksheet, column=column)
    if maxwidth is None:
        return
    worksheet.set_column(first_col=column, last_col=column, width=maxwidth)

只需用列调用 set_column_autowidth

太棒了。我刚刚更新了子例程来处理多索引行和列。

def get_col_widths(dataframe):
    # First we find the maximum length of the index columns   
    idx_max = [max([len(str(s)) for s in dataframe.index.get_level_values(idx)] + [len(str(idx))]) for idx in dataframe.index.names]
    # Then, we concatenate this to the max of the lengths of column name and its values for each column, left to right
    return idx_max + [max([len(str(s)) for s in dataframe[col].values] + \
                          [len(str(x)) for x in col] if dataframe.columns.nlevels > 1 else [len(str(col))]) for col in dataframe.columns]

That URL does not specify what the units are for the third argument to set_column.

列宽以 11 号 Calibri 字体(Excel 标准)中“0”字符宽度的倍数给出。

I can not find a way to measure the width of the item that I want to insert into the cell.

为了掌握字符串的准确宽度,您可以使用 tkinter 以像素为单位测量字符串长度的功能,具体取决于 font/size/weight/etc。如果您定义字体,例如

reference_font = tkinter.font.Font(family='Calibri', size=11)

之后您可以使用它的 measure 方法来确定以像素为单位的字符串宽度,例如

reference_font.measure('This is a string.')

为了对 Excel table 中的单元格执行此操作,您需要考虑其格式(它包含有关所用字体的所有信息)。这意味着,如果你使用 worksheet.write(row, col, cell_string, format) 向你的 table 写了一些东西,你可以像这样获得使用的字体:

used_font = tkinter.font.Font(family     = format.font_name,
                              size       = format.font_size,
                              weight     = ('bold' if format.bold else 'normal'),
                              slant      = ('italic' if format.italic else 'roman'),
                              underline  = format.underline,
                              overstrike = format.font_strikeout)

然后确定单元格宽度为

cell_width = used_font.measure(cell_string+' ')/reference_font.measure('0')

将空格添加到字符串以提供一些边距。这样,结果实际上非常接近 Excel 的自动调整结果,所以我假设 Excel 就是这样做的。

要使 tkinter 魔法起作用,必须打开 tkinter.Tk() 实例(window),因此 returns 函数的完整代码所需的单元格宽度如下所示:

import tkinter
import tkinter.font

def get_cell_width(cell_string, format = None):
  root = tkinter.Tk()
  reference_font = tkinter.font.Font(family='Calibri', size=11)
  if format:
    used_font = tkinter.font.Font(family     = format.font_name,
                                  size       = format.font_size,
                                  weight     = ('bold' if format.bold else 'normal'),
                                  slant      = ('italic' if format.italic else 'roman'),
                                  underline  = format.underline,
                                  overstrike = format.font_strikeout)
  else:
    used_font = reference_font
  cell_width = used_font.measure(cell_string+' ')/reference_font.measure('0')
  root.update_idletasks()
  root.destroy()
  return cell_width

如果要经常执行,您当然希望从函数中获取 root 处理和参考字体创建。此外,为您的工作簿使用查找 table 格式-> 字体可能会更快,这样您就不必每次都定义使用的字体。

最后,可以处理单元格字符串中的换行符:

pixelwidths = (used_font.measure(part) for part in cell_string.split('\n'))
cell_width = (max(pixelwidths) + used_font.measure(' '))/reference_font.measure('0')

此外,如果您使用 Excel 过滤功能,下拉箭头符号还需要 18 个像素(在 Excel 中 100% 放大)。并且可能存在跨越多列的合并单元格...还有很大的改进空间!

xlsxwriter does not appear to have a method to read back a particular cell. This means I need to keep track of each cell width as I write the cell. It would be better if I could just loop through all the cells, that way a generic routine could be written.

如果您不喜欢在自己的数据结构中跟踪,至少有三种方法:

(A) 注册一个写处理程序来完成这项工作:
您可以为所有标准类型注册一个写处理程序。在处理程序函数中,您只需传递写入命令,还可以进行簿记。列宽。这样,最后只需要读取并设置最佳列宽(关闭workbook之前)即可。

# add worksheet attribute to store column widths
worksheet.colWidths = [0]*number_of_used_columns
# register write handler
for stdtype in [str, int, float, bool, datetime, timedelta]:
  worksheet.add_write_handler(stdtype, colWidthTracker)


def colWidthTracker(sheet, row, col, value, format):
  # update column width
  sheet.colWidths[col] = max(sheet.colWidths[col], get_cell_width(value, format))
  # forward write command
  if isinstance(value, str):
    if value == '':
      sheet.write_blank(row, col, value, format)
    else:
      sheet.write_string(row, col, value, format)
  elif isinstance(value, int) or isinstance(value, float):
    sheet.write_number(row, col, value, format)
  elif isinstance(value, bool):
    sheet.write_boolean(row, col, value, format)
  elif isinstance(value, datetime) or isinstance(value, timedelta):
    sheet.write_datetime(row, col, value, format)
  else:
    raise TypeError('colWidthTracker cannot handle this type.')


# and in the end...
for col in columns_to_be_autofitted:    
  worksheet.set_column(col, col, worksheet.colWidths[col])

(B) 使用 to go through the data stored within XlsxWriter's internal variables. However, this is ,因为它可能会在未来的版本中损坏。

(C) 关注 : Inherit from and override the default worksheet class and add in some autofit code, like this example: xlsxwriter.readthedocs.io/example_inheritance2.html