【问题标题】:Simulate autofit column in xslxwriter模拟 xlsxwriter 中的自动调整列
【发布时间】:2015-06-10 09:05:05
【问题描述】:

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

但是,循环遍历工作表上的每个单元格并确定列的最大大小并使用 worksheet.set_column(row, col, width) 设置宽度应该非常简单。

阻碍我写这篇文章的复杂因素是:

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

【问题讨论】:

  • 尝试设置自动宽度并不是那么简单。字形宽度取决于字体。如果您将方程式写入单元格怎么办?然后,您无法根据写入的内容来判断单元格的宽度。单位是任意的,这是真的,但有一个实际的定义!一个单位近似于默认字体中一个字符的宽度。见support.microsoft.com/en-us/kb/214123
  • 有关 xlwt 的相关问题(XlsxWriter 的总体方法是相同的;尽管您可能想要 Calibri 11 而不是 Arial 10 的宽度):stackoverflow.com/questions/6929115/…

标签: python xlsxwriter


【解决方案1】:

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

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

它首先找到索引的最大宽度,它始终是 pandas 以 excel 呈现的数据帧的左列。然后,它返回所有值的最大值以及从左到右移动的每个剩余列的列名。

根据您使用的任何数据调整这段代码应该不会太难。

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)

【讨论】:

  • 什么是指标?
  • 抱歉,metrics 是数据框的名称。为了清楚起见,更新了数据框的答案。
  • “你希望列的宽度比列中最长字符串的大小大一点。” 我所做的是取最大字符长度,并且乘以 1.25。在大多数情况下似乎工作正常。
  • 使用@DanLenski 的逻辑,这是我用过的:widths = [len(col) * 1.5 if len(col) > 25 else 25 for col in cols]。这允许您设置最小宽度。我的基于列标题长度,但可以根据需要进行调整。
  • 这仅在您的数据帧上没有 MultiIndex 时才有效,仅供参考。
【解决方案2】:

我同意科尔戴蒙德的观点。我需要做一些非常相似的事情,它对我来说很好。其中 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)

【讨论】:

  • 您好,我正在尝试将您的代码与上述代码一起使用。你能帮帮我吗?
  • 当然,提出问题,发布链接。我会回答的。
【解决方案3】:

该 URL 没有指定 set_column 的第三个参数的单位。

列宽是 Calibri 字体中“0”字符宽度的倍数,大小为 11(这是 Excel 标准)。

我找不到测量要插入单元格的项目宽度的方法。

为了掌握字符串的确切宽度,您可以使用tkinter 以像素为单位测量字符串长度的能力,具体取决于字体/大小/重量/等。如果您定义字体,例如

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

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

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

为了对 Excel 表格中的单元格执行此操作,您需要考虑其格式(它包含有关所用字体的所有信息)。这意味着,如果您使用worksheet.write(row, col, cell_string, 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)

然后确定单元格宽度为

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

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

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

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 处理和引用字体创建从函数中取出。此外,为您的工作簿使用查找表格式->字体可能会更快,这样您就不必每次都定义使用的字体。

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

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 似乎没有读回特定单元格的方法。这意味着我需要在编写单元格时跟踪每个单元格的宽度。如果我可以循环遍历所有单元格会更好,这样就可以编写通用例程。

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

(A) 注册一个写处理程序来完成这项工作:
您可以为所有标准类型注册一个写处理程序。在处理函数中,您只需传递 write 命令,还可以进行簿记 wrt。列宽。这样,您只需要在最后(关闭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) 使用karolyi's answer above 遍历XlsxWriter 内部变量中存储的数据。但是,这是 discouraged by the module's author,因为它可能会在未来的版本中中断。

(C) 遵循recommendation of jmcnamara:继承并覆盖默认工作表类并添加一些自动调整代码,例如:xlsxwriter.readthedocs.io/example_inheritance2.html

【讨论】:

  • 在这方面做得很好。可能值得将其中的一些工作汇总到一个工作示例中,以包含在 XlsxWriter 示例目录中。基于选项 A 或 C。如果您在 Github 上提出问题/功能请求,我们可以在那里进行讨论。
  • 这是一个绝妙的答案。在我最近的记忆中最有用的之一!我使用了答案中的所有提示。我按照建议遵循了样式(C)。关于“下拉箭头符号需要另外 18 个像素(在 Excel 中以 100% 放大)”的好提示,我通过添加:(18 / (64 / 8.43)) 来调整自动过滤列的宽度。我使用的是香草(默认)字体,所以默认列宽是 8.43 或 64 像素。
【解决方案4】:

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

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 文件上尝试过这个答案。如果您将有多行,则需要有一种方法来确定哪一行将具有“最长”信息,并且仅将其应用于该行。但是,如果无论行如何,每一列的大小都大致相同,那么这对你来说应该很好。

祝你好运,我希望这会有所帮助!

【讨论】:

    【解决方案5】:

    Cole Diamond's answer 太棒了。我刚刚更新了处理多索引行和列的子例程。

    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]
    

    【讨论】:

      【解决方案6】:

      我在 Github site of xlsxwriter 上找到了另一种模拟 Autofit 的解决方法。我已经修改它以返回水平文本(列宽)或 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    
      

      【讨论】:

        【解决方案7】:

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

        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

        【讨论】:

        • 鉴于我们的数据在 pandas 数据框中,我们如何使用您的代码?
        【解决方案8】:

        这是一个支持行和列的 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)
        

        【讨论】:

        • 我发现这个解决方案更简单,并且在循环多个数据帧时有效:for idx, col in enumerate(firm.columns[:-1]): #stopped before last column 因为它的内容,我需要仅包装该列 max_len = Firm[col].map(lambda x: len(x)).max() #获取最长字符串的长度 worksheet.set_column(idx, idx, max_len+4) #将列设置为最长字符串的长度 + 4 以确保一切都合适,我认为是 excel 问题
        猜你喜欢
        • 2022-10-05
        • 2012-03-04
        • 2014-06-28
        • 1970-01-01
        • 2017-02-12
        • 2018-05-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多