├── .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('%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/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 | # 表格样式,即
或 | 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 | -------------------------------------------------------------------------------- |
---|