├── .gitignore ├── HTMLTable ├── __init__.py ├── cell.py ├── column.py ├── common.py ├── openpyxl_util.py ├── row.py └── table.py ├── README.md ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /HTMLTable/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | FileName: __init__.py 6 | Author: Fasion Chan 7 | @contact: fasionchan@gmail.com 8 | @version: $Id$ 9 | 10 | Description: 11 | 12 | Changelog: 13 | 14 | ''' 15 | 16 | from .cell import ( 17 | HTMLTableCell, 18 | ) 19 | from .row import ( 20 | HTMLTableRow, 21 | ) 22 | from .table import ( 23 | HTMLTable, 24 | ) 25 | -------------------------------------------------------------------------------- /HTMLTable/cell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-02-08 16:17:38 9 | ''' 10 | 11 | import html 12 | 13 | from .common import ( 14 | HTMLStyle, 15 | HTMLTag, 16 | ) 17 | 18 | 19 | class HTMLTableCell(HTMLTag): 20 | 21 | def __init__(self, value, tag='td', colspan=1, rowspan=1, escape=True): 22 | super().__init__(tag=tag, value=value, escape=escape) 23 | 24 | self.__is_span = False 25 | 26 | self.set_colspan(span=colspan) 27 | self.set_rowspan(span=rowspan) 28 | 29 | def set_header(self): 30 | self.set_tag(tag='th') 31 | 32 | def set_colspan(self, span): 33 | if span == 1: 34 | self.attr.pop('colspan', None) 35 | return 36 | 37 | self.attr.colspan = span 38 | 39 | def set_rowspan(self, span): 40 | if span == 1: 41 | self.attr.pop('rowspan', None) 42 | return 43 | 44 | self.attr.rowspan = span 45 | 46 | def set_span(self, is_span): 47 | self.__is_span = is_span 48 | 49 | def to_html_chips(self): 50 | if self.__is_span: 51 | return [] 52 | 53 | return super().to_html_chips() 54 | -------------------------------------------------------------------------------- /HTMLTable/column.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-01-21 17:14:31 9 | ''' 10 | 11 | 12 | class HTMLTableColumn(object): 13 | 14 | def __init__(self, table, index): 15 | self.table = table 16 | self.index = index 17 | 18 | def iter_cells(self): 19 | for row in self.table: 20 | yield row[self.index] 21 | 22 | def iter_data_cells(self): 23 | for row in self.table.iter_data_rows(): 24 | yield row[self.index] 25 | 26 | def set_cell_style(self, style): 27 | for cell in self.iter_cells(): 28 | cell.set_style(style=style) 29 | -------------------------------------------------------------------------------- /HTMLTable/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-01-21 17:14:20 9 | ''' 10 | 11 | import html 12 | 13 | 14 | class SmartDict(dict): 15 | 16 | ATTR_NAMES = set() 17 | 18 | def __getattr__(self, name): 19 | if name in self.ATTR_NAMES: 20 | return super(SmartDict, self).__getattr__(name) 21 | 22 | return self[name] 23 | 24 | def __setattr__(self, name, value): 25 | if name in self.ATTR_NAMES: 26 | return super(SmartDict, self).__setattr__(name, value) 27 | 28 | self[name] = value 29 | 30 | 31 | class HTMLAttribute(SmartDict): 32 | 33 | def to_html_chips(self): 34 | chips = [] 35 | for k, v in self.items(): 36 | chips.append('%s="%s"' % (k, v)) 37 | return chips 38 | 39 | def to_html(self): 40 | return ' '.join(self.to_html_chips()) 41 | 42 | 43 | class HTMLStyle(dict): 44 | 45 | def to_html_chips(self): 46 | chips = [] 47 | for k, v in self.items(): 48 | chips.append('%s:%s;' % (k, v)) 49 | return chips 50 | 51 | def to_html(self): 52 | return ''.join(self.to_html_chips()) 53 | 54 | 55 | class HTMLTag(object): 56 | 57 | def __init__(self, tag, value=None, value_formatter=str, escape=True): 58 | self.set_tag(tag=tag) 59 | self.set_value(value=value) 60 | self.set_value_formatter(formatter=value_formatter) 61 | self.set_escape(escape=escape) 62 | 63 | self.attr = HTMLAttribute() 64 | self.style = HTMLStyle() 65 | 66 | def set_tag(self, tag): 67 | self.tag = tag 68 | 69 | def set_value(self, value): 70 | self.value = value 71 | 72 | def set_value_formatter(self, formatter): 73 | self.value_formatter = formatter 74 | 75 | def set_single_style(self, name, value): 76 | self.style[name] = value 77 | 78 | def set_style(self, style): 79 | self.style.update(style) 80 | 81 | def set_escape(self, escape): 82 | self.escape = escape 83 | 84 | def to_html_inner_chips(self): 85 | chips = [] 86 | if self.value: 87 | value = self.value_formatter(self.value) 88 | if self.escape: 89 | value = html.escape(value) 90 | 91 | chips.append(value) 92 | return chips 93 | 94 | def to_html_inner(self): 95 | return ''.join(self.to_html_inner_chips()) 96 | 97 | def to_html_chips(self): 98 | chips = ['<', self.tag] 99 | 100 | if self.attr: 101 | for chip in self.attr.to_html_chips(): 102 | chips.append(' ') 103 | chips.append(chip) 104 | 105 | if self.style: 106 | chips.append(' style="') 107 | chips.extend(self.style.to_html_chips()) 108 | chips.append('"') 109 | 110 | chips.append('>') 111 | 112 | chips.extend(self.to_html_inner_chips()) 113 | 114 | chips.append('' % (self.tag,)) 115 | 116 | return chips 117 | 118 | def to_html(self): 119 | return ''.join(self.to_html_chips()) 120 | -------------------------------------------------------------------------------- /HTMLTable/openpyxl_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-01-21 17:13:31 9 | ''' 10 | 11 | from openpyxl.utils.cell import ( 12 | column_index_from_string, 13 | coordinate_to_tuple, 14 | get_column_letter, 15 | ) 16 | 17 | default_datetime_format_mapping = { 18 | 'h:mm': '%H:%M', 19 | 'mm-dd-yy': '%m-%d-%y' 20 | } 21 | 22 | def coordinates_of_cell_region(start, rows, columns): 23 | startColumn, startRow = coordinate_to_tuple(start) 24 | return tuple( 25 | '{:s}{:d}'.format(get_column_letter(column), row) 26 | for row in range(startRow, startRow+rows) 27 | for column in range(startColumn, startColumn+columns) 28 | ) 29 | 30 | def coordinates_of_merged_cell(merged): 31 | min_row, min_col = merged.min_row, merged.min_col 32 | max_row, max_col = merged.max_row, merged.max_col 33 | return tuple( 34 | '{:s}{:d}'.format(get_column_letter(col), row) 35 | for row in range(min_row, max_row+1) 36 | for col in range(min_col, max_col+1) 37 | ) 38 | 39 | def hidden_coordinates_of_merged_cell(merged): 40 | return coordinates_of_merged_cell(merged)[1:] 41 | 42 | def hidden_coordinate_set_of_merged_cells(mergeds): 43 | s = set() 44 | for merged in mergeds: 45 | for coordinate in hidden_coordinates_of_merged_cell(merged): 46 | s.add(coordinate) 47 | return s 48 | 49 | def all_cells_empty(cells): 50 | for cell in cells: 51 | if cell.value is not None: 52 | return False 53 | return True 54 | 55 | def detect_sheet_end_column(sheet): 56 | index = sheet.max_column 57 | name = get_column_letter(index) 58 | while index > 1 and all_cells_empty(sheet[name]): 59 | index -= 1 60 | name = get_column_letter(index) 61 | return name 62 | 63 | def merge_cell_mapping_by_top_left(ranges): 64 | return { 65 | '{:s}{:d}'.format(get_column_letter(item.min_col), item.min_row): item 66 | for item in ranges 67 | } 68 | 69 | def fill_html_table_from_excel_sheet(table, sheet, start_row=1, end_row=None, start_column='A', end_column=None, ignoreEmptyRow=True, datetime_format_mapping=None): 70 | if not end_column: 71 | end_column = detect_sheet_end_column(sheet) 72 | 73 | start_column_index = column_index_from_string(start_column) 74 | end_column_index = column_index_from_string(end_column) 75 | 76 | hidden_cells = hidden_coordinate_set_of_merged_cells(sheet.merged_cells.ranges) 77 | merged_mapping = merge_cell_mapping_by_top_left(sheet.merged_cells.ranges) 78 | 79 | for ri, row in enumerate(sheet): 80 | ri += 1 81 | if ri < start_row: 82 | continue 83 | 84 | if end_row is not None and ri > end_row: 85 | continue 86 | 87 | if ignoreEmptyRow and all_cells_empty(row): 88 | continue 89 | 90 | html_row = table.append_row() 91 | 92 | for cell in row[start_column_index-1:end_column_index]: 93 | value = str(cell.value or '').replace('\n', '
') 94 | if cell.is_date and cell.value: 95 | fmt = (datetime_format_mapping or {}).get(cell.number_format) 96 | if not fmt: 97 | fmt = default_datetime_format_mapping.get(cell.number_format) 98 | if fmt: 99 | value = cell.value.strftime(fmt) 100 | 101 | html_cell = html_row.append_cell(value) 102 | html_cell.set_escape(False) 103 | 104 | merged = merged_mapping.get(cell.coordinate) 105 | if merged: 106 | html_cell.set_colspan(merged.max_col+1-merged.min_col) 107 | html_cell.set_rowspan(merged.max_row+1-merged.min_row) 108 | 109 | html_cell_style = { 110 | 'text-align': cell.alignment.horizontal, 111 | 'vertical-align': cell.alignment.vertical, 112 | } 113 | 114 | fill_color = cell.fill.start_color.index 115 | if fill_color and fill_color.startswith('FF'): 116 | html_cell_style['background-color'] = '#{:s}'.format(fill_color[2:]) 117 | 118 | html_cell.set_style(html_cell_style) 119 | -------------------------------------------------------------------------------- /HTMLTable/row.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-02-08 16:18:33 9 | ''' 10 | 11 | from .cell import ( 12 | HTMLTableCell, 13 | ) 14 | from .common import ( 15 | HTMLTag, 16 | ) 17 | 18 | 19 | class HTMLTableRow(list, HTMLTag): 20 | 21 | def __init__(self, cells=(), is_header=False, escape_cell=True): 22 | list.__init__(self) 23 | HTMLTag.__init__(self, tag='tr') 24 | 25 | self.is_header = is_header 26 | self.escape_cell = escape_cell 27 | 28 | self.append_cells(cells=cells) 29 | 30 | def append_cell(self, value): 31 | cell_tag = 'th' if self.is_header else 'td' 32 | cell = HTMLTableCell( 33 | tag=cell_tag, 34 | value=value, 35 | escape=self.escape_cell, 36 | ) 37 | self.append(cell) 38 | return cell 39 | 40 | def append_cells(self, cells): 41 | for cell in cells: 42 | self.append_cell(cell) 43 | 44 | def set_cell_style(self, style): 45 | for cell in self: 46 | cell.set_style(style=style) 47 | 48 | def to_html_inner_chips(self): 49 | chips = [] 50 | for cell in self: 51 | chips.extend(cell.to_html_chips()) 52 | return chips 53 | -------------------------------------------------------------------------------- /HTMLTable/table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | Author: fasion 6 | Created time: 2022-01-21 17:13:09 7 | Last Modified by: fasion 8 | Last Modified time: 2022-12-20 14:49:33 9 | ''' 10 | 11 | from .cell import ( 12 | HTMLTableCell, 13 | ) 14 | from .column import ( 15 | HTMLTableColumn, 16 | ) 17 | from .common import ( 18 | HTMLTag, 19 | ) 20 | from .row import ( 21 | HTMLTableRow, 22 | ) 23 | 24 | class HTMLTable(list, HTMLTag): 25 | 26 | def __init__(self, caption='', rows=(), escape_cell=True): 27 | list.__init__(self) 28 | HTMLTag.__init__(self, tag='table') 29 | 30 | self.caption = HTMLTag('caption') 31 | self.caption.set_value(value=caption) 32 | self.escape_cell = escape_cell 33 | 34 | self.append_rows(rows=rows) 35 | 36 | self.colname2index = {} 37 | self.index2colname = {} 38 | 39 | def feed_openpyxl_sheet(self, *args, **kwargs): 40 | from .openpyxl_util import fill_html_table_from_excel_sheet 41 | fill_html_table_from_excel_sheet(self, *args, **kwargs) 42 | 43 | def set_colname(self, index, name): 44 | old = self.index2colname.pop(index, None) 45 | if old is not None: 46 | self.colname2index.pop(old, None) 47 | 48 | self.colname2index[name] = index 49 | self.index2colname[index] = name 50 | 51 | def set_colnames(self, names): 52 | for index, name in enumerate(names): 53 | self.set_colname(index=index, name=name) 54 | 55 | def append_row(self, cells=(), is_header=False, escape_cell=None): 56 | row = HTMLTableRow( 57 | cells=cells, 58 | is_header=is_header, 59 | escape_cell=self.escape_cell if escape_cell is None else escape_cell, 60 | ) 61 | 62 | self.append(row) 63 | 64 | return row 65 | 66 | def append_header_rows(self, rows, escape_cell=True): 67 | return self.append_rows(rows=rows, is_header=True, escape_cell=escape_cell) 68 | 69 | def append_data_rows(self, rows, escape_cell=True): 70 | return self.append_rows(rows=rows, is_header=False, escape_cell=escape_cell) 71 | 72 | def append_rows(self, rows, is_header=False, escape_cell=None): 73 | for row in rows: 74 | self.append_row(cells=row, is_header=is_header, escape_cell=escape_cell) 75 | 76 | def iter_header_rows(self): 77 | for row in self: 78 | if row.is_header: 79 | yield row 80 | 81 | def iter_data_rows(self): 82 | for row in self: 83 | if not row.is_header: 84 | yield row 85 | 86 | def get_column(self, name): 87 | index = self.colname2index.get(name) 88 | if index is None: 89 | return 90 | return HTMLTableColumn(table=self, index=index) 91 | 92 | def iter_cols(self, *names): 93 | for name in names: 94 | yield self.get_column(name=name) 95 | 96 | def set_header_row_style(self, style): 97 | for row in self.iter_header_rows(): 98 | row.set_style(style=style) 99 | 100 | def set_header_cell_style(self, style): 101 | for row in self.iter_header_rows(): 102 | for cell in row: 103 | cell.set_style(style=style) 104 | 105 | def set_data_row_style(self, style): 106 | for row in self.iter_data_rows(): 107 | row.set_style(style=style) 108 | 109 | def set_data_cell_style(self, style): 110 | for row in self.iter_data_rows(): 111 | for cell in row: 112 | cell.set_style(style=style) 113 | 114 | def set_row_style(self, style): 115 | for row in self: 116 | row.set_style(style=style) 117 | 118 | def set_cell_style(self, style): 119 | for row in self: 120 | for cell in row: 121 | cell.set_style(style=style) 122 | 123 | def set_basic_style(self): 124 | border_style = { 125 | 'border-color': '#000', 126 | 'border-width': '1px', 127 | 'border-style': 'solid', 128 | 'border-collapse': 'collapse', 129 | } 130 | 131 | self.set_style(border_style) 132 | self.set_cell_style(border_style) 133 | 134 | header_cell_style = { 135 | 'padding': '15px', 136 | 'background-color': '#48a6fb', 137 | 138 | 'color': '#fff', 139 | 'font-size': '18px', 140 | } 141 | 142 | self.set_header_cell_style(header_cell_style) 143 | 144 | def mark_span(self): 145 | for row in self: 146 | for cell in row: 147 | cell.set_span(False) 148 | 149 | for i, row in enumerate(self): 150 | for j, cell in enumerate(row): 151 | for di in range(cell.attr.get('rowspan', 1)): 152 | for dj in range(cell.attr.get('colspan', 1)): 153 | if di == 0 and dj == 0: 154 | continue 155 | 156 | self[i+di][j+dj].set_span(True) 157 | 158 | def to_html_inner_chips(self): 159 | self.mark_span() 160 | 161 | chips = [] 162 | 163 | if self.caption.value: 164 | chips.extend(self.caption.to_html_chips()) 165 | 166 | for row in self: 167 | chips.extend(row.to_html_chips()) 168 | 169 | return chips 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-table 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | FileName: setup.py 6 | Author: Fasion Chan 7 | @contact: fasionchan@gmail.com 8 | @version: $Id$ 9 | 10 | Description: 11 | 12 | Changelog: 13 | 14 | ''' 15 | 16 | VERSION = '1.0' 17 | 18 | from setuptools import ( 19 | setup, 20 | ) 21 | 22 | if __name__ == '__main__': 23 | setup( 24 | name='html-table', 25 | version=VERSION, 26 | author='Fasion Chan', 27 | author_email='fasionchan@gmail.com', 28 | packages=[ 29 | 'HTMLTable', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding=utf8 -*- 3 | 4 | ''' 5 | FileName: test.py 6 | Author: Fasion Chan 7 | @contact: fasionchan@gmail.com 8 | @version: $Id$ 9 | 10 | Description: 11 | 12 | Changelog: 13 | 14 | ''' 15 | 16 | from HTMLTable import ( 17 | HTMLTableCell, 18 | HTMLTableRow, 19 | HTMLTable, 20 | ) 21 | 22 | # 标题 23 | table = HTMLTable(caption='果园收成表') 24 | 25 | # 表头行 26 | table.append_header_rows(( 27 | ('名称', '产量 (吨)', '环比', ''), 28 | ('', '', '增长量 (吨)', '增长率 (%)'), 29 | )) 30 | 31 | # 合并单元格 32 | table[0][0].attr.rowspan = 2 33 | table[0][1].attr.rowspan = 2 34 | table[0][2].attr.colspan = 2 35 | 36 | # 数据行 37 | table.append_data_rows(( 38 | ('荔枝', 11, 1, 10), 39 | ('芒果', 9, -1, -10), 40 | ('香蕉', 6, 1, 20), 41 | )) 42 | 43 | # 标题样式 44 | table.caption.set_style({ 45 | 'font-size': '15px', 46 | }) 47 | 48 | # 表格样式,即标签样式 49 | table.set_style({ 50 | 'border-collapse': 'collapse', 51 | 52 | 'word-break': 'keep-all', 53 | 'white-space': 'nowrap', 54 | 'font-size': '14px', 55 | }) 56 | 57 | # 统一设置所有单元格样式,
58 | table.set_cell_style({ 59 | 'border-color': '#000', 60 | 'border-width': '1px', 61 | 'border-style': 'solid', 62 | 63 | 'padding': '5px', 64 | }) 65 | 66 | # 表头颜色 67 | table.set_header_row_style({ 68 | 'color': '#fff', 69 | 'background-color': '#48a6fb', 70 | 'font-size': '18px', 71 | }) 72 | # 覆盖表头单元格字体样式 73 | table.set_header_cell_style({ 74 | 'padding': '15px', 75 | }) 76 | 77 | # 调小次表头字体大小 78 | table[1].set_cell_style({ 79 | 'padding': '8px', 80 | 'font-size': '15px', 81 | }) 82 | 83 | # 遍历数据行,如果增长量为负,标红背景颜色 84 | for row in table.iter_data_rows(): 85 | if row[2].value < 0: 86 | row.set_style({ 87 | 'background-color': '#ffdddd', 88 | }) 89 | 90 | html = table.to_html() 91 | print(html) 92 | --------------------------------------------------------------------------------