├── README.md ├── HTMLTable ├── __init__.py ├── column.py ├── cell.py ├── row.py ├── common.py ├── openpyxl_util.py └── table.py ├── setup.py ├── test.py └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # html-table 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | # 表格样式,即
| 或 |
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 |
--------------------------------------------------------------------------------
/.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/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('%s>' % (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/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 | -------------------------------------------------------------------------------- |
|---|