datraxlsx 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datraxlsx/__init__.py +4 -0
- datraxlsx/styles.py +82 -0
- datraxlsx/writer.py +271 -0
- datraxlsx-0.3.0.dist-info/METADATA +122 -0
- datraxlsx-0.3.0.dist-info/RECORD +6 -0
- datraxlsx-0.3.0.dist-info/WHEEL +4 -0
datraxlsx/__init__.py
ADDED
datraxlsx/styles.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Reusable style objects for Excel writing."""
|
|
2
|
+
from dataclasses import dataclass, replace
|
|
3
|
+
|
|
4
|
+
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class CellStyle:
|
|
9
|
+
font: Font | None = None
|
|
10
|
+
fill: PatternFill | None = None
|
|
11
|
+
alignment: Alignment | None = None
|
|
12
|
+
border: Border | None = None
|
|
13
|
+
number_format: str | None = None
|
|
14
|
+
|
|
15
|
+
def apply(self, cell) -> None:
|
|
16
|
+
if self.font is not None:
|
|
17
|
+
cell.font = self.font
|
|
18
|
+
if self.fill is not None:
|
|
19
|
+
cell.fill = self.fill
|
|
20
|
+
if self.alignment is not None:
|
|
21
|
+
cell.alignment = self.alignment
|
|
22
|
+
if self.border is not None:
|
|
23
|
+
cell.border = self.border
|
|
24
|
+
if self.number_format is not None:
|
|
25
|
+
cell.number_format = self.number_format
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class RowBorder:
|
|
30
|
+
top: Side | None = None
|
|
31
|
+
bottom: Side | None = None
|
|
32
|
+
left_edge: Side | None = None
|
|
33
|
+
right_edge: Side | None = None
|
|
34
|
+
inner_vertical: Side | None = None
|
|
35
|
+
|
|
36
|
+
def apply_to_range(self, ws, start_col: int, start_row: int, end_col: int, end_row: int) -> None:
|
|
37
|
+
for r in range(start_row, end_row + 1):
|
|
38
|
+
for c in range(start_col, end_col + 1):
|
|
39
|
+
top = self.top if r == start_row else None
|
|
40
|
+
bottom = self.bottom if r == end_row else None
|
|
41
|
+
left = self.left_edge if c == start_col else self.inner_vertical
|
|
42
|
+
right = self.right_edge if c == end_col else self.inner_vertical
|
|
43
|
+
ws.cell(row=r, column=c).border = Border(top=top, bottom=bottom, left=left, right=right)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class TableTheme:
|
|
48
|
+
title: CellStyle
|
|
49
|
+
header: CellStyle
|
|
50
|
+
body: CellStyle
|
|
51
|
+
summary: CellStyle
|
|
52
|
+
title_border: RowBorder | None = None
|
|
53
|
+
header_border: RowBorder | None = None
|
|
54
|
+
body_border: RowBorder | None = None
|
|
55
|
+
summary_border: RowBorder | None = None
|
|
56
|
+
title_height: int = 75
|
|
57
|
+
row_height: int = 33
|
|
58
|
+
column_width: float = 17.5
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def default(cls) -> "TableTheme":
|
|
62
|
+
return cls(
|
|
63
|
+
title=CellStyle(
|
|
64
|
+
font=Font(name="微软雅黑", size=23, bold=True),
|
|
65
|
+
alignment=Alignment(horizontal="center", vertical="center"),
|
|
66
|
+
),
|
|
67
|
+
header=CellStyle(
|
|
68
|
+
font=Font(name="微软雅黑", size=11.5, bold=True),
|
|
69
|
+
alignment=Alignment(horizontal="center", vertical="center", wrapText=True),
|
|
70
|
+
),
|
|
71
|
+
body=CellStyle(
|
|
72
|
+
font=Font(name="微软雅黑", size=11.5),
|
|
73
|
+
alignment=Alignment(horizontal="center", vertical="center"),
|
|
74
|
+
),
|
|
75
|
+
summary=CellStyle(
|
|
76
|
+
font=Font(name="微软雅黑", size=11.5, bold=True),
|
|
77
|
+
alignment=Alignment(horizontal="center", vertical="center"),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def replace(self, **fields) -> "TableTheme":
|
|
82
|
+
return replace(self, **fields)
|
datraxlsx/writer.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""DataFrame-to-Excel writer with style-based configuration."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import polars as pl
|
|
6
|
+
from openpyxl import Workbook
|
|
7
|
+
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
8
|
+
from openpyxl.utils.cell import cols_from_range, coordinate_from_string, get_column_letter
|
|
9
|
+
from openpyxl.worksheet import cell_range
|
|
10
|
+
from typing import Self
|
|
11
|
+
|
|
12
|
+
from .styles import CellStyle, RowBorder, TableTheme
|
|
13
|
+
|
|
14
|
+
# `range` is shadowed by the `write_cell` parameter; keep a builtin reference.
|
|
15
|
+
_range = range
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def transColname2Letter(colnames: list[str], xlsDataRange: str) -> dict[str, str]:
|
|
19
|
+
colLetter = [i[0][0] for i in cols_from_range(xlsDataRange)]
|
|
20
|
+
if len(colnames) != len(colLetter):
|
|
21
|
+
raise ValueError("colnames must be equal to xlsDataRange")
|
|
22
|
+
return dict(zip(colnames, colLetter))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExcelWriter:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
filename: str | Path,
|
|
29
|
+
*,
|
|
30
|
+
theme: TableTheme | None = None,
|
|
31
|
+
start_row: int = 1,
|
|
32
|
+
start_col: int = 1,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._filename = filename
|
|
35
|
+
self._wb = Workbook()
|
|
36
|
+
self._theme = theme or TableTheme.default()
|
|
37
|
+
self._start_row = start_row
|
|
38
|
+
self._start_col = start_col
|
|
39
|
+
self._first_sheet_used = False
|
|
40
|
+
|
|
41
|
+
def sheet(self, name: str | None = None, *, show_gridlines: bool = False) -> "Sheet":
|
|
42
|
+
if not self._first_sheet_used:
|
|
43
|
+
ws = self._wb.active
|
|
44
|
+
self._first_sheet_used = True
|
|
45
|
+
else:
|
|
46
|
+
ws = self._wb.create_sheet()
|
|
47
|
+
ws.sheet_view.showGridLines = show_gridlines
|
|
48
|
+
if name:
|
|
49
|
+
ws.title = name
|
|
50
|
+
return Sheet(ws, self._theme, self._start_row, self._start_col)
|
|
51
|
+
|
|
52
|
+
def save(self) -> None:
|
|
53
|
+
self._wb.save(self._filename)
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> Self:
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
59
|
+
if exc_type is None:
|
|
60
|
+
self.save()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Sheet:
|
|
64
|
+
def __init__(self, ws, theme: TableTheme, start_row: int = 1, start_col: int = 1) -> None:
|
|
65
|
+
self._ws = ws
|
|
66
|
+
self._theme = theme
|
|
67
|
+
self._start_row = start_row
|
|
68
|
+
self._start_col = start_col
|
|
69
|
+
self._cursor = start_row
|
|
70
|
+
self._df = None
|
|
71
|
+
self._columns = None
|
|
72
|
+
|
|
73
|
+
def use(self, df, *, columns: list[str] | None = None) -> None:
|
|
74
|
+
if isinstance(df, pl.DataFrame):
|
|
75
|
+
df = df.to_pandas()
|
|
76
|
+
self._df = df
|
|
77
|
+
self._columns = columns if columns is not None else df.columns.tolist()
|
|
78
|
+
|
|
79
|
+
def _ncols(self) -> int:
|
|
80
|
+
return len(self._columns)
|
|
81
|
+
|
|
82
|
+
def write_title(
|
|
83
|
+
self,
|
|
84
|
+
text: str,
|
|
85
|
+
*,
|
|
86
|
+
crossline: int = 0,
|
|
87
|
+
style: CellStyle | None = None,
|
|
88
|
+
border: RowBorder | None = None,
|
|
89
|
+
height: int | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
if self._df is None:
|
|
92
|
+
raise ValueError("call use(df) before write_title()")
|
|
93
|
+
st = style or self._theme.title
|
|
94
|
+
bd = border if border is not None else self._theme.title_border
|
|
95
|
+
h = height if height is not None else self._theme.title_height
|
|
96
|
+
n = self._ncols()
|
|
97
|
+
start_row = self._cursor
|
|
98
|
+
end_row = start_row + crossline
|
|
99
|
+
start_cell = f"{get_column_letter(self._start_col)}{start_row}"
|
|
100
|
+
end_cell = f"{get_column_letter(self._start_col + n - 1)}{end_row}"
|
|
101
|
+
if end_cell != start_cell:
|
|
102
|
+
self._ws.merge_cells(f"{start_cell}:{end_cell}")
|
|
103
|
+
self._ws[start_cell].value = text
|
|
104
|
+
st.apply(self._ws[start_cell])
|
|
105
|
+
for r in range(start_row, end_row + 1):
|
|
106
|
+
self._ws.row_dimensions[r].height = h
|
|
107
|
+
if bd is not None:
|
|
108
|
+
bd.apply_to_range(self._ws, self._start_col, start_row, self._start_col + n - 1, end_row)
|
|
109
|
+
self._cursor = end_row + 1
|
|
110
|
+
|
|
111
|
+
def write_data(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
header_style: CellStyle | None = None,
|
|
115
|
+
body_style: CellStyle | None = None,
|
|
116
|
+
header_border: RowBorder | None = None,
|
|
117
|
+
body_border: RowBorder | None = None,
|
|
118
|
+
number_format: str | dict | None = None,
|
|
119
|
+
height: int | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
if self._df is None:
|
|
122
|
+
raise ValueError("call use(df) before write_data()")
|
|
123
|
+
hs = header_style or self._theme.header
|
|
124
|
+
bs = body_style or self._theme.body
|
|
125
|
+
hb = header_border if header_border is not None else self._theme.header_border
|
|
126
|
+
bb = body_border if body_border is not None else self._theme.body_border
|
|
127
|
+
h = height if height is not None else self._theme.row_height
|
|
128
|
+
n = self._ncols()
|
|
129
|
+
df = self._df[self._columns]
|
|
130
|
+
|
|
131
|
+
hr = self._cursor
|
|
132
|
+
for j, col in enumerate(self._columns):
|
|
133
|
+
cell = self._ws.cell(row=hr, column=self._start_col + j, value=col)
|
|
134
|
+
hs.apply(cell)
|
|
135
|
+
self._ws.row_dimensions[hr].height = h
|
|
136
|
+
if hb is not None:
|
|
137
|
+
hb.apply_to_range(self._ws, self._start_col, hr, self._start_col + n - 1, hr)
|
|
138
|
+
self._cursor = hr + 1
|
|
139
|
+
|
|
140
|
+
for row_values in df.to_numpy().tolist():
|
|
141
|
+
r = self._cursor
|
|
142
|
+
for j, val in enumerate(row_values):
|
|
143
|
+
cell = self._ws.cell(row=r, column=self._start_col + j, value=val)
|
|
144
|
+
bs.apply(cell)
|
|
145
|
+
self._ws.row_dimensions[r].height = h
|
|
146
|
+
if bb is not None:
|
|
147
|
+
bb.apply_to_range(self._ws, self._start_col, r, self._start_col + n - 1, r)
|
|
148
|
+
self._cursor = r + 1
|
|
149
|
+
|
|
150
|
+
if number_format is not None:
|
|
151
|
+
self._apply_number_format(number_format, hr + 1, self._cursor - 1)
|
|
152
|
+
|
|
153
|
+
def write_summary(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
agg_cols: list[str] | None = None,
|
|
157
|
+
pass_cols: list[str] | None = None,
|
|
158
|
+
agg_fun: str = "sum",
|
|
159
|
+
row_name: str = "总计",
|
|
160
|
+
merge_cols: list[str] | None = None,
|
|
161
|
+
pass_seq: str = "--",
|
|
162
|
+
style: CellStyle | None = None,
|
|
163
|
+
border: RowBorder | None = None,
|
|
164
|
+
number_format: str | dict | None = None,
|
|
165
|
+
height: int | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
if self._df is None:
|
|
168
|
+
raise ValueError("call use(df) before write_summary()")
|
|
169
|
+
st = style or self._theme.summary
|
|
170
|
+
bd = border if border is not None else self._theme.summary_border
|
|
171
|
+
h = height if height is not None else self._theme.row_height
|
|
172
|
+
n = self._ncols()
|
|
173
|
+
df = self._df[self._columns]
|
|
174
|
+
if merge_cols:
|
|
175
|
+
if merge_cols != self._columns[: len(merge_cols)]:
|
|
176
|
+
raise ValueError("merge_cols must be the leading columns")
|
|
177
|
+
nm = len(merge_cols)
|
|
178
|
+
else:
|
|
179
|
+
nm = 0
|
|
180
|
+
|
|
181
|
+
values = [row_name] * nm
|
|
182
|
+
for col in self._columns[nm:]:
|
|
183
|
+
if agg_cols and col in agg_cols:
|
|
184
|
+
values.append(df[col].agg(agg_fun))
|
|
185
|
+
elif pass_cols and col in pass_cols:
|
|
186
|
+
values.append(pass_seq)
|
|
187
|
+
elif pd.api.types.is_numeric_dtype(df[col].dtype):
|
|
188
|
+
values.append(df[col].agg(agg_fun))
|
|
189
|
+
else:
|
|
190
|
+
values.append(df[col].iloc[0])
|
|
191
|
+
|
|
192
|
+
r = self._cursor
|
|
193
|
+
for j, val in enumerate(values):
|
|
194
|
+
cell = self._ws.cell(row=r, column=self._start_col + j, value=val)
|
|
195
|
+
st.apply(cell)
|
|
196
|
+
if nm > 0:
|
|
197
|
+
sc = f"{get_column_letter(self._start_col)}{r}"
|
|
198
|
+
ec = f"{get_column_letter(self._start_col + nm - 1)}{r}"
|
|
199
|
+
if ec != sc:
|
|
200
|
+
self._ws.merge_cells(f"{sc}:{ec}")
|
|
201
|
+
self._ws[sc].value = row_name
|
|
202
|
+
st.apply(self._ws[sc])
|
|
203
|
+
self._ws.row_dimensions[r].height = h
|
|
204
|
+
if bd is not None:
|
|
205
|
+
bd.apply_to_range(self._ws, self._start_col, r, self._start_col + n - 1, r)
|
|
206
|
+
self._cursor = r + 1
|
|
207
|
+
if number_format is not None:
|
|
208
|
+
self._apply_number_format(number_format, r, r)
|
|
209
|
+
|
|
210
|
+
def write_cell(
|
|
211
|
+
self,
|
|
212
|
+
range: str,
|
|
213
|
+
value=None,
|
|
214
|
+
*,
|
|
215
|
+
style: CellStyle | None = None,
|
|
216
|
+
height: int | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
h = height if height is not None else self._theme.row_height
|
|
219
|
+
if ":" in range:
|
|
220
|
+
self._ws.merge_cells(range)
|
|
221
|
+
top_left = range.split(":")[0]
|
|
222
|
+
cell = self._ws[top_left]
|
|
223
|
+
if value is not None:
|
|
224
|
+
cell.value = value
|
|
225
|
+
if style is not None:
|
|
226
|
+
style.apply(cell)
|
|
227
|
+
bounds = cell_range.CellRange(range_string=range).bounds
|
|
228
|
+
for r in _range(bounds[1], bounds[3] + 1):
|
|
229
|
+
self._ws.row_dimensions[r].height = h
|
|
230
|
+
else:
|
|
231
|
+
cell = self._ws[range]
|
|
232
|
+
if value is not None:
|
|
233
|
+
cell.value = value
|
|
234
|
+
if style is not None:
|
|
235
|
+
style.apply(cell)
|
|
236
|
+
bounds = cell_range.CellRange(range_string=range).bounds
|
|
237
|
+
self._ws.row_dimensions[bounds[1]].height = h
|
|
238
|
+
|
|
239
|
+
def set_column_width(self, width: float, *, columns: str | list[str] | None = None) -> None:
|
|
240
|
+
if self._df is None:
|
|
241
|
+
raise ValueError("call use(df) before set_column_width()")
|
|
242
|
+
if columns is None:
|
|
243
|
+
for j in range(self._ncols()):
|
|
244
|
+
self._ws.column_dimensions[get_column_letter(self._start_col + j)].width = width
|
|
245
|
+
elif isinstance(columns, str):
|
|
246
|
+
if columns in self._columns:
|
|
247
|
+
j = self._columns.index(columns)
|
|
248
|
+
letter = get_column_letter(self._start_col + j)
|
|
249
|
+
else:
|
|
250
|
+
letter = columns
|
|
251
|
+
self._ws.column_dimensions[letter].width = width
|
|
252
|
+
else:
|
|
253
|
+
for c in columns:
|
|
254
|
+
j = self._columns.index(c)
|
|
255
|
+
self._ws.column_dimensions[get_column_letter(self._start_col + j)].width = width
|
|
256
|
+
|
|
257
|
+
def _apply_number_format(self, number_format, first_row: int, last_row: int) -> None:
|
|
258
|
+
if isinstance(number_format, str):
|
|
259
|
+
for r in range(first_row, last_row + 1):
|
|
260
|
+
for j in range(self._ncols()):
|
|
261
|
+
self._ws.cell(row=r, column=self._start_col + j).number_format = number_format
|
|
262
|
+
elif isinstance(number_format, dict):
|
|
263
|
+
col_to_idx = {name: i for i, name in enumerate(self._columns)}
|
|
264
|
+
for name, fmt in number_format.items():
|
|
265
|
+
if name not in col_to_idx:
|
|
266
|
+
raise KeyError(name)
|
|
267
|
+
j = col_to_idx[name]
|
|
268
|
+
for r in range(first_row, last_row + 1):
|
|
269
|
+
self._ws.cell(row=r, column=self._start_col + j).number_format = fmt
|
|
270
|
+
else:
|
|
271
|
+
raise TypeError("number_format must be str or dict")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datraxlsx
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: 把 DataFrame 保存为带有 Excel 格式的 XLSX 文件的可复用样式工具
|
|
5
|
+
Author: SidneyZhang
|
|
6
|
+
Author-email: SidneyZhang <zly@lyzhang.me>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
9
|
+
Requires-Dist: pandas>=3.0.3
|
|
10
|
+
Requires-Dist: polars>=1.42.1
|
|
11
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# DaTraXLSX
|
|
16
|
+
|
|
17
|
+
这是一个把DataFrame保存为带有Excel格式的XLSX文件的工具。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
datraxlsx 需要Python 3.13及以上版本,依赖 openpyxl、pandas、polars。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 本地开发安装
|
|
25
|
+
pip install -e .
|
|
26
|
+
|
|
27
|
+
# 或使用 uv
|
|
28
|
+
uv add datraxlsx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 核心概念
|
|
32
|
+
|
|
33
|
+
datraxlsx 由四个可复用的构建块组成:
|
|
34
|
+
|
|
35
|
+
- `CellStyle` — 可复用单元格样式,聚合 `font`/`fill`/`alignment`/`border`/`number_format` 五个字段(均可选),通过 `apply(cell)` 应用到单元格。
|
|
36
|
+
- `RowBorder` — 以"边"描述一行单元格的边框:`top`/`bottom`/`left_edge`/`right_edge`/`inner_vertical`。它会自动区分左右边缘与内部竖线,无需手动处理首尾列。
|
|
37
|
+
- `TableTheme` — 聚合 `title`/`header`/`body`/`summary` 四套 `CellStyle` 及其对应 `RowBorder`,外加 `title_height`/`row_height`/`column_width`。`TableTheme.default()` 开箱即用,`.replace(...)` 定制单个字段。
|
|
38
|
+
- `ExcelWriter` + `Sheet` — 工作簿与工作表写入器。`ExcelWriter` 持有主题与游标起点,通过 `.sheet(name)` 创建工作表;`Sheet` 提供 `use`/`write_title`/`write_data`/`write_summary`/`write_cell`/`set_column_width` 等方法,支持多 sheet 写入。
|
|
39
|
+
|
|
40
|
+
## 快速开始
|
|
41
|
+
|
|
42
|
+
零配置即可写出带标题、表头、数据、合计行的格式化报表:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import pandas as pd
|
|
46
|
+
from datraxlsx import ExcelWriter
|
|
47
|
+
|
|
48
|
+
df = pd.DataFrame({"产品": ["A", "B", "C"], "数量": [10, 20, 30], "金额": [100.5, 200.5, 300.5]})
|
|
49
|
+
|
|
50
|
+
with ExcelWriter("output.xlsx") as wb:
|
|
51
|
+
ws = wb.sheet("销售")
|
|
52
|
+
ws.use(df)
|
|
53
|
+
ws.write_title("销售报表")
|
|
54
|
+
ws.write_data()
|
|
55
|
+
ws.write_summary(agg_cols=["数量", "金额"], merge_cols=["产品"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
polars DataFrame 同样可用,`use()` 会自动转换。
|
|
59
|
+
|
|
60
|
+
## 复用主题
|
|
61
|
+
|
|
62
|
+
定义一次主题,跨 sheet、跨文件复用。`TableTheme.default().replace(...)` 只覆盖传入的字段,其余沿用默认值:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from datraxlsx import ExcelWriter, TableTheme, CellStyle, RowBorder
|
|
66
|
+
from openpyxl.styles import Font, PatternFill, Side
|
|
67
|
+
|
|
68
|
+
theme = TableTheme.default().replace(
|
|
69
|
+
header=CellStyle(
|
|
70
|
+
font=Font(name="微软雅黑", size=12, bold=True, color="FFFFFF"),
|
|
71
|
+
fill=PatternFill(patternType="solid", fgColor="0066CC"),
|
|
72
|
+
),
|
|
73
|
+
header_border=RowBorder(
|
|
74
|
+
top=Side(border_style="thick"),
|
|
75
|
+
bottom=Side(border_style="double"),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
with ExcelWriter("output.xlsx", theme=theme) as wb:
|
|
80
|
+
for name, df in sheets.items():
|
|
81
|
+
ws = wb.sheet(name)
|
|
82
|
+
ws.use(df)
|
|
83
|
+
ws.write_title(name)
|
|
84
|
+
ws.write_data()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 按列名指定数字格式
|
|
88
|
+
|
|
89
|
+
`write_data` 与 `write_summary` 的 `number_format` 参数支持字符串(应用到所有列)或字典(按列名指定):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
ws.write_data(number_format={"金额": "#,##0.00", "比例": "0.00%"})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 一次性覆盖
|
|
96
|
+
|
|
97
|
+
每个写入方法都接受 `style`/`border`/`height` 等关键字参数,传入即覆盖该次调用的主题样式,不修改主题本身:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
ws.write_data(body_style=CellStyle(font=Font(italic=True)))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API 速览
|
|
104
|
+
|
|
105
|
+
`Sheet` 方法(写入前须先调用 `use(df)`):
|
|
106
|
+
|
|
107
|
+
- `use(df, *, columns=None)` — 绑定 DataFrame(pandas 或 polars),可选重排列或选取列子集。
|
|
108
|
+
- `write_title(text, *, crossline=0, style=None, border=None, height=None)` — 写入跨列合并标题,`crossline` 控制跨行行数。
|
|
109
|
+
- `write_data(*, header_style=None, body_style=None, header_border=None, body_border=None, number_format=None, height=None)` — 写入表头与数据行。
|
|
110
|
+
- `write_summary(*, agg_cols=None, pass_cols=None, agg_fun="sum", row_name="总计", merge_cols=None, pass_seq="--", style=None, border=None, number_format=None, height=None)` — 写入合计行,`agg_cols` 求和、`pass_cols` 填占位符、`merge_cols` 合并前导列。
|
|
111
|
+
- `write_cell(range, value=None, *, style=None, height=None)` — 写入或合并写入任意单元格区域,`range` 支持 `"A1"` 或 `"A1:C3"`。
|
|
112
|
+
- `set_column_width(width, *, columns=None)` — 设置列宽,`columns` 省略则全部列,可传列名或列名列表。
|
|
113
|
+
|
|
114
|
+
`ExcelWriter` 方法:
|
|
115
|
+
|
|
116
|
+
- `ExcelWriter(filename, *, theme=None, start_row=1, start_col=1)` — 构造工作簿,可指定主题与写入起点。
|
|
117
|
+
- `.sheet(name=None, *, show_gridlines=False)` — 创建工作表,默认隐藏网格线。
|
|
118
|
+
- `.save()` — 保存文件;作为上下文管理器使用时退出即保存。
|
|
119
|
+
|
|
120
|
+
## 工具函数
|
|
121
|
+
|
|
122
|
+
`transColname2Letter(colnames, xlsDataRange)` 将列名映射到 Excel 列字母。传入列名列表与对应的 Excel 区域字符串(如 `"A1:D10"`),返回 `{列名: 列字母}` 字典。列名数量须与区域列数一致,否则抛出 `ValueError`。
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
datraxlsx/__init__.py,sha256=SEsBJsXct6zZ_6cKbqs3bOB6xjerKMXOdyi42MDxUco,212
|
|
2
|
+
datraxlsx/styles.py,sha256=7bdj4V5Cy5CnojgpnCuUVvHCkwDPufeIpDdXEpT3So0,2932
|
|
3
|
+
datraxlsx/writer.py,sha256=7eLde5e0AX164h1ifH-94xYrmd0JqS9Vsa2ZKfeAyrY,10737
|
|
4
|
+
datraxlsx-0.3.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
|
|
5
|
+
datraxlsx-0.3.0.dist-info/METADATA,sha256=2swSbub_y796tVl52AfAC-jE_kzNkDT8SMY9IHeMzCs,5243
|
|
6
|
+
datraxlsx-0.3.0.dist-info/RECORD,,
|