在xlsxwriter中模拟自动调整列

时间:2015-04-05 23:13:24

标签: python xlsxwriter

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

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

阻止我写这篇文章的并发症是:

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

8 个答案:

答案 0 :(得分:20)

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

例如,在使用pandas dataframes和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)

答案 1 :(得分:4)

我同意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)

答案 2 :(得分:3)

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

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',则{{1}然后它会看到c = 1,依此类推。

'B:B'段确定您尝试输出的字符串的长度,然后向其添加2以确保excel单元格足够宽,因为字符串的长度与宽度不完全相关细胞。您可能想玩,而不是根据您的数据添加len(str(x)) + 2或更多。

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

注意:我只在包含1行数据的excel文件上尝试过此答案。如果您有多行,则需要确定哪一行具有“最长”信息并仅将其应用于该行。但是,如果每列不管行数大致相同,那么这应该适合你。

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

答案 3 :(得分:2)

我在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    

答案 4 :(得分:2)

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

列宽以字体Calibri,大小11(这是Excel标准)中'0'字符宽度的倍数给出。

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

为了掌握字符串的确切宽度,可以使用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)注册一个写处理程序来完成这项工作:
您可以为所有标准类型注册写处理程序。在处理程序函数中,您只需传递写命令,还可以执行簿记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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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

答案 7 :(得分:0)

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]