qrpa 1.0.13__py3-none-any.whl → 1.1.50__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.
- qrpa/RateLimitedSender.py +45 -45
- qrpa/__init__.py +31 -17
- qrpa/db_migrator.py +600 -600
- qrpa/feishu_bot_app.py +268 -0
- qrpa/feishu_client.py +410 -0
- qrpa/feishu_logic.py +1443 -0
- qrpa/fun_base.py +339 -107
- qrpa/fun_excel.py +907 -262
- qrpa/fun_file.py +319 -173
- qrpa/fun_web.py +258 -148
- qrpa/fun_win.py +198 -198
- qrpa/mysql_module/__init__.py +0 -0
- qrpa/mysql_module/new_product_analysis_model.py +556 -0
- qrpa/mysql_module/shein_ledger_model.py +468 -0
- qrpa/mysql_module/shein_product_model.py +495 -0
- qrpa/mysql_module/shein_return_order_model.py +569 -0
- qrpa/mysql_module/shein_store_model.py +595 -0
- qrpa/shein_daily_report_model.py +375 -0
- qrpa/shein_excel.py +3318 -12
- qrpa/shein_lib.py +3784 -100
- qrpa/shein_mysql.py +92 -0
- qrpa/shein_sqlite.py +154 -0
- qrpa/shein_ziniao.py +529 -450
- qrpa/temu_chrome.py +56 -0
- qrpa/temu_excel.py +139 -0
- qrpa/temu_lib.py +156 -0
- qrpa/time_utils.py +87 -50
- qrpa/time_utils_example.py +243 -243
- qrpa/wxwork.py +46 -0
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/METADATA +1 -1
- qrpa-1.1.50.dist-info/RECORD +33 -0
- qrpa-1.0.13.dist-info/RECORD +0 -18
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/WHEEL +0 -0
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/top_level.txt +0 -0
qrpa/fun_excel.py
CHANGED
|
@@ -12,66 +12,112 @@ import concurrent.futures
|
|
|
12
12
|
from collections import defaultdict
|
|
13
13
|
import threading
|
|
14
14
|
from playwright.sync_api import sync_playwright
|
|
15
|
+
import psutil
|
|
16
|
+
|
|
17
|
+
import os, sys
|
|
18
|
+
from pathlib import Path
|
|
15
19
|
|
|
16
20
|
from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
|
|
17
21
|
|
|
18
22
|
excel_color_index = {
|
|
19
|
-
"无色(自动)": 0, # 透明/默认
|
|
20
|
-
"黑色": 1, # #000000
|
|
21
|
-
"白色": 2, # #FFFFFF
|
|
22
|
-
"红色": 3, # #FF0000
|
|
23
|
-
"绿色": 4, # #00FF00
|
|
24
|
-
"蓝色": 5, # #0000FF
|
|
25
|
-
"黄色": 6, # #FFFF00
|
|
26
|
-
"粉红色": 7, # #FF00FF
|
|
27
|
-
"青绿色": 8, # #00FFFF
|
|
28
|
-
"深红色": 9, # #800000
|
|
29
|
-
"深绿色": 10, # #008000
|
|
30
|
-
"深蓝色": 11, # #000080
|
|
31
|
-
"橄榄色(深黄)": 12, # #808000
|
|
32
|
-
"紫色": 13, # #800080
|
|
33
|
-
"蓝绿色(水色)": 14, # #008080
|
|
34
|
-
"灰色(25%)": 15, # #808080
|
|
23
|
+
"无色(自动)" : 0, # 透明/默认
|
|
24
|
+
"黑色" : 1, # #000000
|
|
25
|
+
"白色" : 2, # #FFFFFF
|
|
26
|
+
"红色" : 3, # #FF0000
|
|
27
|
+
"绿色" : 4, # #00FF00
|
|
28
|
+
"蓝色" : 5, # #0000FF
|
|
29
|
+
"黄色" : 6, # #FFFF00
|
|
30
|
+
"粉红色" : 7, # #FF00FF
|
|
31
|
+
"青绿色" : 8, # #00FFFF
|
|
32
|
+
"深红色" : 9, # #800000
|
|
33
|
+
"深绿色" : 10, # #008000
|
|
34
|
+
"深蓝色" : 11, # #000080
|
|
35
|
+
"橄榄色(深黄)" : 12, # #808000
|
|
36
|
+
"紫色" : 13, # #800080
|
|
37
|
+
"蓝绿色(水色)" : 14, # #008080
|
|
38
|
+
"灰色(25%)" : 15, # #808080
|
|
35
39
|
"浅灰色(12.5%)": 16, # #C0C0C0
|
|
36
40
|
# 17-19:系统保留(通常不可用)
|
|
37
|
-
"深玫瑰红": 20, # #FF99CC
|
|
38
|
-
"深金色": 21, # #FFCC99
|
|
39
|
-
"深橙红色": 22, # #FF6600
|
|
40
|
-
"深灰色(50%)": 23, # #666666
|
|
41
|
-
"深紫色": 24, # #660066
|
|
42
|
-
"蓝灰色": 25, # #3366FF
|
|
43
|
-
"浅蓝色": 26, # #99CCFF
|
|
44
|
-
"浅紫色": 27, # #CC99FF
|
|
45
|
-
"浅青绿色": 28, # #99FFFF
|
|
46
|
-
"浅绿色": 29, # #CCFFCC
|
|
47
|
-
"浅黄色": 30, # #FFFFCC
|
|
48
|
-
"浅橙红色": 31, # #FFCC99
|
|
49
|
-
"玫瑰红": 32, # #FF9999
|
|
50
|
-
"浅天蓝色": 33, # #99CCFF
|
|
51
|
-
"浅海绿色": 34, # #99FFCC
|
|
52
|
-
"浅草绿色": 35, # #CCFF99
|
|
53
|
-
"浅柠檬黄": 36, # #FFFF99
|
|
54
|
-
"浅珊瑚色": 37, # #FFCC99
|
|
55
|
-
"浅玫瑰红": 38, # #FF9999
|
|
56
|
-
"棕褐色": 39, # #CC9966
|
|
57
|
-
"浅棕褐色": 40, # #FFCC99
|
|
58
|
-
"浅橄榄色": 41, # #CCCC99
|
|
59
|
-
"浅蓝灰色": 42, # #9999FF
|
|
60
|
-
"浅灰绿色": 43, # #99CC99
|
|
61
|
-
"金色": 44, # #FFCC00
|
|
62
|
-
"浅橙黄色": 45, # #FFCC66
|
|
63
|
-
"橙红色": 46, # #FF6600
|
|
64
|
-
"深天蓝色": 47, # #0066CC
|
|
65
|
-
"深海绿色": 48, # #009966
|
|
66
|
-
"深草绿色": 49, # #669900
|
|
67
|
-
"深柠檬黄": 50, # #CCCC00
|
|
68
|
-
"深珊瑚色": 51, # #FF9933
|
|
69
|
-
"深玫瑰红(暗)": 52, # #CC6699
|
|
70
|
-
"深棕褐色": 53, # #996633
|
|
71
|
-
"深橄榄色": 54, # #666600
|
|
72
|
-
"深蓝灰色": 55, # #333399
|
|
41
|
+
"深玫瑰红" : 20, # #FF99CC
|
|
42
|
+
"深金色" : 21, # #FFCC99
|
|
43
|
+
"深橙红色" : 22, # #FF6600
|
|
44
|
+
"深灰色(50%)" : 23, # #666666
|
|
45
|
+
"深紫色" : 24, # #660066
|
|
46
|
+
"蓝灰色" : 25, # #3366FF
|
|
47
|
+
"浅蓝色" : 26, # #99CCFF
|
|
48
|
+
"浅紫色" : 27, # #CC99FF
|
|
49
|
+
"浅青绿色" : 28, # #99FFFF
|
|
50
|
+
"浅绿色" : 29, # #CCFFCC
|
|
51
|
+
"浅黄色" : 30, # #FFFFCC
|
|
52
|
+
"浅橙红色" : 31, # #FFCC99
|
|
53
|
+
"玫瑰红" : 32, # #FF9999
|
|
54
|
+
"浅天蓝色" : 33, # #99CCFF
|
|
55
|
+
"浅海绿色" : 34, # #99FFCC
|
|
56
|
+
"浅草绿色" : 35, # #CCFF99
|
|
57
|
+
"浅柠檬黄" : 36, # #FFFF99
|
|
58
|
+
"浅珊瑚色" : 37, # #FFCC99
|
|
59
|
+
"浅玫瑰红" : 38, # #FF9999
|
|
60
|
+
"棕褐色" : 39, # #CC9966
|
|
61
|
+
"浅棕褐色" : 40, # #FFCC99
|
|
62
|
+
"浅橄榄色" : 41, # #CCCC99
|
|
63
|
+
"浅蓝灰色" : 42, # #9999FF
|
|
64
|
+
"浅灰绿色" : 43, # #99CC99
|
|
65
|
+
"金色" : 44, # #FFCC00
|
|
66
|
+
"浅橙黄色" : 45, # #FFCC66
|
|
67
|
+
"橙红色" : 46, # #FF6600
|
|
68
|
+
"深天蓝色" : 47, # #0066CC
|
|
69
|
+
"深海绿色" : 48, # #009966
|
|
70
|
+
"深草绿色" : 49, # #669900
|
|
71
|
+
"深柠檬黄" : 50, # #CCCC00
|
|
72
|
+
"深珊瑚色" : 51, # #FF9933
|
|
73
|
+
"深玫瑰红(暗)" : 52, # #CC6699
|
|
74
|
+
"深棕褐色" : 53, # #996633
|
|
75
|
+
"深橄榄色" : 54, # #666600
|
|
76
|
+
"深蓝灰色" : 55, # #333399
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
def aggregate_by_column(data, group_by_col_name):
|
|
80
|
+
"""
|
|
81
|
+
根据指定列名对二维表数据聚合:
|
|
82
|
+
- 数字列求和
|
|
83
|
+
- 字符串列用换行符拼接
|
|
84
|
+
|
|
85
|
+
:param data: 二维列表,第一行为表头
|
|
86
|
+
:param group_by_col_name: 要聚合的列名,如 "店长"
|
|
87
|
+
:return: 聚合后的二维列表
|
|
88
|
+
"""
|
|
89
|
+
headers = data[0]
|
|
90
|
+
group_index = headers.index(group_by_col_name)
|
|
91
|
+
grouped = defaultdict(list)
|
|
92
|
+
|
|
93
|
+
# 按 group_by 列聚合行
|
|
94
|
+
for row in data[1:]:
|
|
95
|
+
key = row[group_index]
|
|
96
|
+
grouped[key].append(row)
|
|
97
|
+
|
|
98
|
+
result = [headers]
|
|
99
|
+
|
|
100
|
+
for key, rows in grouped.items():
|
|
101
|
+
agg_row = []
|
|
102
|
+
for col_idx in range(len(headers)):
|
|
103
|
+
col_values = [r[col_idx] for r in rows]
|
|
104
|
+
# 聚合字段
|
|
105
|
+
if col_idx == group_index:
|
|
106
|
+
agg_value = key
|
|
107
|
+
else:
|
|
108
|
+
# 尝试将值转为 float,如果成功就求和,否则拼接
|
|
109
|
+
try:
|
|
110
|
+
nums = [float(v) for v in col_values if
|
|
111
|
+
isinstance(v, (int, float)) or (isinstance(v, str) and v.strip() != '')]
|
|
112
|
+
agg_value = sum(nums)
|
|
113
|
+
except ValueError:
|
|
114
|
+
# 拼接字符串(去重可加 set)
|
|
115
|
+
strings = [str(v).strip() for v in col_values if str(v).strip()]
|
|
116
|
+
agg_value = '\n'.join(strings)
|
|
117
|
+
agg_row.append(agg_value)
|
|
118
|
+
result.append(agg_row)
|
|
119
|
+
|
|
120
|
+
return result
|
|
75
121
|
|
|
76
122
|
def set_cell_prefix_red(cell, n, color_name):
|
|
77
123
|
"""
|
|
@@ -90,14 +136,56 @@ def set_cell_prefix_red(cell, n, color_name):
|
|
|
90
136
|
except Exception as e:
|
|
91
137
|
print(f"设置字体颜色失败: {e}")
|
|
92
138
|
|
|
139
|
+
def wrap_column(sheet, columns=None, WrapText=True):
|
|
140
|
+
if columns is None:
|
|
141
|
+
return
|
|
142
|
+
used_range_col = sheet.range('A1').expand('right')
|
|
143
|
+
for j, cell in enumerate(used_range_col):
|
|
144
|
+
col = j + 1
|
|
145
|
+
col_name = index_to_column_name(col)
|
|
146
|
+
col_val = sheet.range(f'{col_name}1').value
|
|
147
|
+
if col_val is None:
|
|
148
|
+
continue
|
|
149
|
+
for c in columns:
|
|
150
|
+
if c in col_val:
|
|
151
|
+
log(f'设置[{c}] 换行 {WrapText}')
|
|
152
|
+
sheet.range(f'{col_name}:{col_name}').api.WrapText = WrapText
|
|
153
|
+
|
|
154
|
+
def sort_by_column_excel(sheet, sort_col: str, has_header=True, order="desc"):
|
|
155
|
+
"""
|
|
156
|
+
对整个表格按照某一列排序
|
|
93
157
|
|
|
94
|
-
|
|
95
|
-
|
|
158
|
+
:param sheet: xlwings 的 sheet 对象
|
|
159
|
+
:param sort_col: 排序依据的列(如 'D')
|
|
160
|
+
:param has_header: 是否有表头(默认 True)
|
|
161
|
+
:param order: 'asc' 升序,'desc' 降序
|
|
162
|
+
"""
|
|
163
|
+
# 找到表格的最后一行和列
|
|
164
|
+
last_cell = sheet.used_range.last_cell
|
|
165
|
+
rng = sheet.range((1, 1), (last_cell.row, last_cell.column))
|
|
166
|
+
|
|
167
|
+
# 排序依据列
|
|
168
|
+
col_index = ord(sort_col.upper()) - ord('A') + 1
|
|
169
|
+
key = sheet.range((2 if has_header else 1, col_index)).api
|
|
170
|
+
|
|
171
|
+
# 排序顺序
|
|
172
|
+
order_val = 1 if order == "asc" else 2
|
|
173
|
+
|
|
174
|
+
# 调用 Excel 的 Sort 方法
|
|
175
|
+
rng.api.Sort(
|
|
176
|
+
Key1=key,
|
|
177
|
+
Order1=order_val,
|
|
178
|
+
Orientation=1,
|
|
179
|
+
Header=1 if has_header else 0
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def sort_by_column(data, col_index, header_rows=2, reverse=True):
|
|
183
|
+
if not data or header_rows >= len(data):
|
|
96
184
|
return data
|
|
97
185
|
|
|
98
186
|
try:
|
|
99
|
-
header = data[:
|
|
100
|
-
new_data_sorted = data[
|
|
187
|
+
header = data[:header_rows]
|
|
188
|
+
new_data_sorted = data[header_rows:]
|
|
101
189
|
|
|
102
190
|
def get_key(row):
|
|
103
191
|
value = row[col_index]
|
|
@@ -114,7 +202,6 @@ def sort_by_column(data, col_index, start_row=2, reverse=True):
|
|
|
114
202
|
print(f"Error: Column index {col_index} out of range")
|
|
115
203
|
return data
|
|
116
204
|
|
|
117
|
-
|
|
118
205
|
def column_exists(sheet, column_name, header_row=1):
|
|
119
206
|
"""
|
|
120
207
|
检查工作表中是否存在指定列名
|
|
@@ -128,7 +215,6 @@ def column_exists(sheet, column_name, header_row=1):
|
|
|
128
215
|
|
|
129
216
|
return column_name in header_values
|
|
130
217
|
|
|
131
|
-
|
|
132
218
|
def merge_by_column_v2(sheet, column_name, other_columns):
|
|
133
219
|
log('正在处理合并单元格')
|
|
134
220
|
# 最好放到 open_excel 后面,不然容易出错
|
|
@@ -137,38 +223,88 @@ def merge_by_column_v2(sheet, column_name, other_columns):
|
|
|
137
223
|
log(f'未找到合并的列名: {column_name}')
|
|
138
224
|
return
|
|
139
225
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
226
|
+
# 更安全的数据获取方式,确保获取完整的数据范围
|
|
227
|
+
last_row = get_last_row(sheet, col_letter)
|
|
228
|
+
data = sheet.range(f'{col_letter}1:{col_letter}{last_row}').value
|
|
229
|
+
|
|
230
|
+
# 确保data是列表格式
|
|
231
|
+
if not isinstance(data, list):
|
|
232
|
+
data = [data]
|
|
233
|
+
|
|
234
|
+
log(f'数据范围: {col_letter}1:{col_letter}{last_row}, 数据长度: {len(data)}')
|
|
144
235
|
|
|
145
|
-
#
|
|
146
|
-
|
|
236
|
+
start_row = 2 # 从第2行开始,跳过表头
|
|
237
|
+
merge_row_ranges = [] # 用来存储需要合并的行范围 (start_row, end_row)
|
|
238
|
+
|
|
239
|
+
# 获取所有需要合并的列
|
|
240
|
+
all_columns = [col_letter] # 主列
|
|
147
241
|
for col in other_columns:
|
|
148
242
|
col_name = find_column_by_data(sheet, 1, col)
|
|
149
243
|
if col_name:
|
|
150
|
-
|
|
244
|
+
all_columns.append(col_name)
|
|
151
245
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
246
|
+
log(f'需要合并的列: {all_columns}')
|
|
247
|
+
|
|
248
|
+
# 遍历数据行,从第3行开始比较(因为第1行是表头,第2行是第一个数据行)
|
|
249
|
+
for row in range(3, len(data) + 1):
|
|
250
|
+
log(f'查找 {row}/{len(data)}, 当前值: {data[row - 1] if row - 1 < len(data) else "超出范围"}, 前一个值: {data[row - 2] if row - 2 < len(data) else "超出范围"}')
|
|
251
|
+
|
|
252
|
+
# 检查值是否发生变化
|
|
253
|
+
if row <= len(data) and data[row - 1] != data[row - 2]:
|
|
254
|
+
# 值发生变化,处理前一组
|
|
255
|
+
end_row = row - 1
|
|
256
|
+
log(f'添加合并范围: {start_row} 到 {end_row}')
|
|
257
|
+
merge_row_ranges.append((start_row, end_row))
|
|
160
258
|
start_row = row
|
|
161
259
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
260
|
+
# 处理最后一组数据(循环结束后,start_row 到数据末尾)
|
|
261
|
+
if start_row <= len(data):
|
|
262
|
+
end_row = len(data)
|
|
263
|
+
log(f'处理最后一组: {start_row} 到 {end_row}')
|
|
264
|
+
merge_row_ranges.append((start_row, end_row))
|
|
166
265
|
|
|
167
|
-
|
|
168
|
-
for col_name, start, end in merge_ranges:
|
|
169
|
-
log(f'处理 {col_name}{start}:{col_name}{end} merge')
|
|
170
|
-
sheet.range(f'{col_name}{start}:{col_name}{end}').merge()
|
|
266
|
+
log(f'行合并范围: {merge_row_ranges}')
|
|
171
267
|
|
|
268
|
+
# 对每个行范围,在所有指定列中执行合并
|
|
269
|
+
for start_row, end_row in merge_row_ranges:
|
|
270
|
+
if start_row < end_row: # 只有当开始行小于结束行时才合并(多行)
|
|
271
|
+
for col_name in all_columns:
|
|
272
|
+
try:
|
|
273
|
+
cell_range = sheet.range(f'{col_name}{start_row}:{col_name}{end_row}')
|
|
274
|
+
|
|
275
|
+
# 验证:检查范围内的值是否都相同
|
|
276
|
+
values = cell_range.value
|
|
277
|
+
if not isinstance(values, list):
|
|
278
|
+
values = [values]
|
|
279
|
+
|
|
280
|
+
# 检查是否所有值都相同(忽略 None)
|
|
281
|
+
non_none_values = [v for v in values if v is not None]
|
|
282
|
+
if non_none_values and len(set(non_none_values)) > 1:
|
|
283
|
+
log(f'警告:{col_name}{start_row}:{col_name}{end_row} 包含不同的值,跳过合并: {set(non_none_values)}')
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
log(f'处理 {col_name}{start_row}:{col_name}{end_row} merge')
|
|
287
|
+
|
|
288
|
+
# 保存第一个单元格的值
|
|
289
|
+
first_cell_value = sheet.range(f'{col_name}{start_row}').value
|
|
290
|
+
|
|
291
|
+
# 先清空所有单元格(避免多行文本导致的合并问题)
|
|
292
|
+
cell_range.value = None
|
|
293
|
+
|
|
294
|
+
# 执行合并
|
|
295
|
+
cell_range.merge()
|
|
296
|
+
|
|
297
|
+
# 恢复第一个单元格的值
|
|
298
|
+
cell_range.value = first_cell_value
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
log(f'合并失败 {col_name}{start_row}:{col_name}{end_row}: {e}')
|
|
302
|
+
# 继续处理其他列
|
|
303
|
+
continue
|
|
304
|
+
elif start_row == end_row:
|
|
305
|
+
log(f'单行数据无需合并: {start_row} 到 {end_row}')
|
|
306
|
+
else:
|
|
307
|
+
log(f'跳过无效合并范围: {start_row} 到 {end_row}')
|
|
172
308
|
|
|
173
309
|
def merge_by_column(sheet, column_name, other_columns):
|
|
174
310
|
log('正在处理合并单元格')
|
|
@@ -198,7 +334,6 @@ def merge_by_column(sheet, column_name, other_columns):
|
|
|
198
334
|
if col_name is not None:
|
|
199
335
|
sheet.range(f'{col_name}{start_row}:{col_name}{len(data)}').merge()
|
|
200
336
|
|
|
201
|
-
|
|
202
337
|
def merge_column_v2(sheet, columns):
|
|
203
338
|
if columns is None:
|
|
204
339
|
return
|
|
@@ -229,7 +364,6 @@ def merge_column_v2(sheet, columns):
|
|
|
229
364
|
log(f'处理 {col_letter}{start}:{col_letter}{end} merge')
|
|
230
365
|
sheet.range(f'{col_letter}{start}:{col_letter}{end}').merge()
|
|
231
366
|
|
|
232
|
-
|
|
233
367
|
# 按列相同值合并
|
|
234
368
|
def merge_column(sheet, columns):
|
|
235
369
|
# 最后放到 open_excel 后面,不然容易出错
|
|
@@ -252,7 +386,6 @@ def merge_column(sheet, columns):
|
|
|
252
386
|
if len(data) - start_row > 1:
|
|
253
387
|
sheet.range(f'{col_letter}{start_row}:{col_letter}{len(data)}').merge()
|
|
254
388
|
|
|
255
|
-
|
|
256
389
|
def remove_excel_columns(sheet, columns):
|
|
257
390
|
# 获取第一行(标题行)的所有值
|
|
258
391
|
header_row = sheet.range('1:1').value
|
|
@@ -276,7 +409,6 @@ def remove_excel_columns(sheet, columns):
|
|
|
276
409
|
print(f"成功移除列: {columns_to_remove}")
|
|
277
410
|
return True
|
|
278
411
|
|
|
279
|
-
|
|
280
412
|
def delete_sheet_if_exists(wb, sheet_name):
|
|
281
413
|
"""
|
|
282
414
|
如果工作簿中存在指定名称的工作表,则将其删除。
|
|
@@ -290,11 +422,11 @@ def delete_sheet_if_exists(wb, sheet_name):
|
|
|
290
422
|
sheet_names = [s.name for s in wb.sheets]
|
|
291
423
|
if sheet_name in sheet_names:
|
|
292
424
|
wb.sheets[sheet_name].delete()
|
|
425
|
+
wb.save()
|
|
293
426
|
print(f"已删除 Sheet: {sheet_name}")
|
|
294
427
|
else:
|
|
295
428
|
print(f"Sheet 不存在: {sheet_name}")
|
|
296
429
|
|
|
297
|
-
|
|
298
430
|
# 水平对齐:
|
|
299
431
|
# -4108:居中
|
|
300
432
|
# -4131:左对齐
|
|
@@ -316,7 +448,6 @@ def index_to_column_name(index):
|
|
|
316
448
|
index = index // 26
|
|
317
449
|
return column_name
|
|
318
450
|
|
|
319
|
-
|
|
320
451
|
# # 示例:将列索引转换为列名
|
|
321
452
|
# log(index_to_column_name(1)) # 输出: 'A'
|
|
322
453
|
# log(index_to_column_name(26)) # 输出: 'Z'
|
|
@@ -334,7 +465,6 @@ def column_name_to_index(column_name):
|
|
|
334
465
|
index = index * 26 + (ord(char.upper()) - 64)
|
|
335
466
|
return index - 1
|
|
336
467
|
|
|
337
|
-
|
|
338
468
|
# # 示例:将列名转换为列索引
|
|
339
469
|
# log(column_name_to_index('A')) # 输出: 1
|
|
340
470
|
# log(column_name_to_index('Z')) # 输出: 26
|
|
@@ -361,7 +491,6 @@ def find_row_by_data(sheet, column, target_value):
|
|
|
361
491
|
# 如果未找到,返回 None
|
|
362
492
|
return None
|
|
363
493
|
|
|
364
|
-
|
|
365
494
|
def find_column_by_data(sheet, row, target_value):
|
|
366
495
|
"""
|
|
367
496
|
查找指定数据在某一行中第一次出现的列名,包括隐藏的列。
|
|
@@ -382,7 +511,6 @@ def find_column_by_data(sheet, row, target_value):
|
|
|
382
511
|
|
|
383
512
|
return None # 未找到返回 None
|
|
384
513
|
|
|
385
|
-
|
|
386
514
|
def find_column_by_data_old(sheet, row, target_value):
|
|
387
515
|
"""
|
|
388
516
|
查找指定数据在某一行中第一次出现的列名。
|
|
@@ -404,7 +532,6 @@ def find_column_by_data_old(sheet, row, target_value):
|
|
|
404
532
|
# 如果未找到,返回 None
|
|
405
533
|
return None
|
|
406
534
|
|
|
407
|
-
|
|
408
535
|
def set_print_area(sheet, print_range, pdf_path=None, fit_to_width=True, landscape=False):
|
|
409
536
|
"""
|
|
410
537
|
设置指定sheet的打印区域和打印布局为适合A4宽度打印。
|
|
@@ -453,12 +580,10 @@ def set_print_area(sheet, print_range, pdf_path=None, fit_to_width=True, landsca
|
|
|
453
580
|
sheet.to_pdf(path=pdf_path)
|
|
454
581
|
log(f"PDF已成功生成:{pdf_path}")
|
|
455
582
|
|
|
456
|
-
|
|
457
583
|
def minimize(app):
|
|
458
584
|
# 让 Excel 窗口最小化
|
|
459
585
|
app.api.WindowState = -4140 # -4140 对应 Excel 中的 xlMinimized 常量
|
|
460
586
|
|
|
461
|
-
|
|
462
587
|
def insert_fixed_scale_image_v2(sheet, cell, image_path):
|
|
463
588
|
"""
|
|
464
589
|
将图片插入到指定单元格中,自动缩放以适应单元格尺寸,但保持宽高比例不变。
|
|
@@ -521,7 +646,6 @@ def insert_fixed_scale_image_v2(sheet, cell, image_path):
|
|
|
521
646
|
|
|
522
647
|
return None
|
|
523
648
|
|
|
524
|
-
|
|
525
649
|
def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
|
|
526
650
|
"""
|
|
527
651
|
按固定比例放大图片并插入到单元格
|
|
@@ -559,12 +683,11 @@ def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
|
|
|
559
683
|
|
|
560
684
|
return None
|
|
561
685
|
|
|
562
|
-
|
|
563
|
-
def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150, img_save_key=None, dir_name=None):
|
|
686
|
+
def InsertImageV2(sheet, columns=None, platform='shein', img_width=150, img_save_key=None, dir_name=None, cell_height_with_img=False, start_row=2):
|
|
564
687
|
if not columns:
|
|
565
688
|
return
|
|
566
689
|
|
|
567
|
-
minimize(app)
|
|
690
|
+
minimize(sheet.book.app)
|
|
568
691
|
|
|
569
692
|
# 清空所有图片
|
|
570
693
|
clear_all_pictures(sheet)
|
|
@@ -591,10 +714,10 @@ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150,
|
|
|
591
714
|
|
|
592
715
|
img_key_letter = find_column_by_data(sheet, 1, img_save_key)
|
|
593
716
|
|
|
594
|
-
#
|
|
717
|
+
# 阶段1:调整所有单元格尺寸 (优化点1)
|
|
595
718
|
area_map = {}
|
|
596
|
-
for row in range(
|
|
597
|
-
log(f'计算 {row}/{last_row}')
|
|
719
|
+
for row in range(start_row, last_row + 1):
|
|
720
|
+
log(f'计算 {row}/{last_row}')
|
|
598
721
|
for col_letter in col_letter_map.values():
|
|
599
722
|
cell_ref = f'{col_letter}{row}'
|
|
600
723
|
cell_range = sheet.range(cell_ref)
|
|
@@ -605,33 +728,50 @@ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150,
|
|
|
605
728
|
cell_address = cell_range.address
|
|
606
729
|
|
|
607
730
|
if cell_address not in area_map:
|
|
608
|
-
#
|
|
609
|
-
cell_width = cell_range.width
|
|
610
|
-
cell_height = cell_range.height
|
|
611
|
-
|
|
612
|
-
# 调整列宽和行高
|
|
613
|
-
if cell_width < img_width:
|
|
614
|
-
cell_range.column_width = img_width / 6.1
|
|
615
|
-
|
|
616
|
-
# 这一行暂时先自动控制宽度
|
|
731
|
+
# 调整列宽
|
|
617
732
|
cell_range.column_width = img_width / 6.1
|
|
618
733
|
|
|
619
|
-
|
|
620
|
-
|
|
734
|
+
# 调整行高
|
|
735
|
+
if cell_range.height < img_width:
|
|
736
|
+
if cell_range.merge_cells:
|
|
737
|
+
# 合并单元格:为每一行设置高度
|
|
738
|
+
rows_count = cell_range.rows.count
|
|
739
|
+
per_row_height = img_width / rows_count
|
|
740
|
+
for single_row in cell_range.rows:
|
|
741
|
+
single_row.row_height = max(per_row_height, 150 / 8)
|
|
742
|
+
else:
|
|
743
|
+
cell_range.row_height = max(img_width, 150 / 8)
|
|
621
744
|
|
|
745
|
+
if cell_height_with_img:
|
|
746
|
+
if cell_range.merge_cells:
|
|
747
|
+
rows_count = cell_range.rows.count
|
|
748
|
+
for single_row in cell_range.rows:
|
|
749
|
+
single_row.row_height = img_width / rows_count
|
|
750
|
+
else:
|
|
751
|
+
cell_range.row_height = img_width
|
|
752
|
+
|
|
753
|
+
# 重新读取调整后的宽高
|
|
754
|
+
if cell_range.merge_cells:
|
|
755
|
+
cell_range = sheet.range(cell_ref).merge_area
|
|
756
|
+
else:
|
|
757
|
+
cell_range = sheet.range(cell_ref)
|
|
758
|
+
|
|
622
759
|
# 计算居中位置
|
|
623
|
-
|
|
624
|
-
|
|
760
|
+
actual_width = cell_range.width
|
|
761
|
+
actual_height = cell_range.height
|
|
762
|
+
actual_img_size = img_width - 4
|
|
763
|
+
top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
|
|
764
|
+
left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
|
|
625
765
|
|
|
626
766
|
area_map[cell_address] = {
|
|
627
|
-
'top': top,
|
|
628
|
-
'left': left,
|
|
629
|
-
'width': img_width,
|
|
630
|
-
'cell_list': [c.address for c in cell_range]
|
|
767
|
+
'top' : top,
|
|
768
|
+
'left' : left,
|
|
769
|
+
'width' : img_width,
|
|
770
|
+
'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
|
|
631
771
|
}
|
|
632
772
|
|
|
633
773
|
# 处理图片插入 (优化点2)
|
|
634
|
-
for row in range(
|
|
774
|
+
for row in range(start_row, last_row + 1):
|
|
635
775
|
for img_col_name, col_letter in col_letter_map.items():
|
|
636
776
|
cell_ref = f'{col_letter}{row}'
|
|
637
777
|
cell_range = sheet.range(cell_ref)
|
|
@@ -684,6 +824,237 @@ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150,
|
|
|
684
824
|
else:
|
|
685
825
|
log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
|
|
686
826
|
|
|
827
|
+
def InsertImageV3(sheet, columns=None, platform='shein', img_widths=None, img_save_key=None, dir_name=None, cell_height_with_img=False, start_row=2):
|
|
828
|
+
"""
|
|
829
|
+
V3版本:支持一次性插入多列图片,每列可以设置不同的宽度
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
sheet: Excel工作表对象
|
|
833
|
+
columns: 图片列名列表,如 ['SKC图片', 'SKU图片']
|
|
834
|
+
platform: 平台名称,如 'shein'
|
|
835
|
+
img_widths: 图片宽度列表,与columns对应,如 [90, 60]
|
|
836
|
+
img_save_key: 图片保存时的key列
|
|
837
|
+
dir_name: 图片保存目录名
|
|
838
|
+
cell_height_with_img: 是否根据图片调整单元格高度
|
|
839
|
+
start_row: 开始行号,默认为2
|
|
840
|
+
"""
|
|
841
|
+
if not columns:
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# 如果没有提供宽度列表,使用默认宽度150
|
|
845
|
+
if not img_widths:
|
|
846
|
+
img_widths = [150] * len(columns)
|
|
847
|
+
|
|
848
|
+
# 确保宽度列表长度与列名列表一致
|
|
849
|
+
if len(img_widths) != len(columns):
|
|
850
|
+
raise ValueError(f"img_widths长度({len(img_widths)})必须与columns长度({len(columns)})一致")
|
|
851
|
+
|
|
852
|
+
minimize(sheet.book.app)
|
|
853
|
+
|
|
854
|
+
# 只清空一次所有图片
|
|
855
|
+
clear_all_pictures(sheet)
|
|
856
|
+
|
|
857
|
+
# 获取每列图片列的列号,并批量下载图片
|
|
858
|
+
col_letter_map = {}
|
|
859
|
+
col_width_map = {} # 存储每列对应的宽度
|
|
860
|
+
|
|
861
|
+
for idx, img_col in enumerate(columns):
|
|
862
|
+
col_letter = find_column_by_data(sheet, 1, img_col)
|
|
863
|
+
if col_letter is not None:
|
|
864
|
+
col_letter_map[img_col] = col_letter
|
|
865
|
+
col_width_map[col_letter] = img_widths[idx]
|
|
866
|
+
# 下载图片
|
|
867
|
+
log(f'批量下载图片: {img_col} => {col_letter} (宽度: {img_widths[idx]})')
|
|
868
|
+
last_row = get_last_row(sheet, col_letter)
|
|
869
|
+
images = sheet.range(f'{col_letter}2:{col_letter}{last_row}').value
|
|
870
|
+
images = images if isinstance(images, list) else [images]
|
|
871
|
+
download_images_concurrently(images, platform)
|
|
872
|
+
|
|
873
|
+
# 任意一个列作为主参考列,用来确定行数
|
|
874
|
+
if not col_letter_map:
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
ref_col_letter = next(iter(col_letter_map.values()))
|
|
878
|
+
last_row = get_last_row(sheet, ref_col_letter)
|
|
879
|
+
|
|
880
|
+
img_key_letter = find_column_by_data(sheet, 1, img_save_key)
|
|
881
|
+
|
|
882
|
+
# 阶段1:收集每个单元格需要的尺寸要求
|
|
883
|
+
cell_size_requirements = {} # {cell_address: {'width': max_width, 'height': max_height, 'merge': is_merge}}
|
|
884
|
+
|
|
885
|
+
for row in range(start_row, last_row + 1):
|
|
886
|
+
for col_letter in col_letter_map.values():
|
|
887
|
+
cell_ref = f'{col_letter}{row}'
|
|
888
|
+
cell_range = sheet.range(cell_ref)
|
|
889
|
+
cell_address = cell_range.address
|
|
890
|
+
img_width = col_width_map[col_letter]
|
|
891
|
+
|
|
892
|
+
if cell_range.merge_cells:
|
|
893
|
+
cell_range = cell_range.merge_area
|
|
894
|
+
cell_address = cell_range.address
|
|
895
|
+
|
|
896
|
+
# 记录每个单元格需要的最大尺寸
|
|
897
|
+
if cell_address not in cell_size_requirements:
|
|
898
|
+
cell_size_requirements[cell_address] = {
|
|
899
|
+
'width': img_width,
|
|
900
|
+
'height': img_width,
|
|
901
|
+
'cell_range': cell_range,
|
|
902
|
+
'merge': cell_range.merge_cells
|
|
903
|
+
}
|
|
904
|
+
else:
|
|
905
|
+
# 取最大值
|
|
906
|
+
cell_size_requirements[cell_address]['width'] = max(
|
|
907
|
+
cell_size_requirements[cell_address]['width'], img_width
|
|
908
|
+
)
|
|
909
|
+
cell_size_requirements[cell_address]['height'] = max(
|
|
910
|
+
cell_size_requirements[cell_address]['height'], img_width
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# 阶段2:统一调整所有单元格的宽高(按列分别处理)
|
|
914
|
+
log(f'调整单元格尺寸...')
|
|
915
|
+
adjusted_cells = {} # 记录已调整的单元格,避免重复调整
|
|
916
|
+
|
|
917
|
+
for col_letter in col_letter_map.values():
|
|
918
|
+
img_width = col_width_map[col_letter]
|
|
919
|
+
|
|
920
|
+
for row in range(start_row, last_row + 1):
|
|
921
|
+
cell_ref = f'{col_letter}{row}'
|
|
922
|
+
cell_range = sheet.range(cell_ref)
|
|
923
|
+
cell_address = cell_range.address
|
|
924
|
+
|
|
925
|
+
if cell_range.merge_cells:
|
|
926
|
+
cell_range = cell_range.merge_area
|
|
927
|
+
cell_address = cell_range.address
|
|
928
|
+
|
|
929
|
+
# 调整列宽(按原来的逻辑,每列都调整)
|
|
930
|
+
if cell_range.width < img_width:
|
|
931
|
+
cell_range.column_width = img_width / 6.1
|
|
932
|
+
# 这一行暂时先自动控制宽度
|
|
933
|
+
cell_range.column_width = img_width / 6.1
|
|
934
|
+
|
|
935
|
+
# 行高只调整一次(使用最大需求)
|
|
936
|
+
if cell_address not in adjusted_cells:
|
|
937
|
+
adjusted_cells[cell_address] = True
|
|
938
|
+
required_height = cell_size_requirements[cell_address]['height']
|
|
939
|
+
|
|
940
|
+
# 调整行高
|
|
941
|
+
if cell_range.height < required_height:
|
|
942
|
+
if cell_range.merge_cells:
|
|
943
|
+
# 合并单元格:为每一行设置高度
|
|
944
|
+
rows_count = cell_range.rows.count
|
|
945
|
+
per_row_height = required_height / rows_count
|
|
946
|
+
for single_row in cell_range.rows:
|
|
947
|
+
single_row.row_height = max(per_row_height, 150 / 8)
|
|
948
|
+
else:
|
|
949
|
+
cell_range.row_height = max(required_height, 150 / 8)
|
|
950
|
+
|
|
951
|
+
if cell_height_with_img:
|
|
952
|
+
if cell_range.merge_cells:
|
|
953
|
+
rows_count = cell_range.rows.count
|
|
954
|
+
for single_row in cell_range.rows:
|
|
955
|
+
single_row.row_height = required_height / rows_count
|
|
956
|
+
else:
|
|
957
|
+
cell_range.row_height = required_height
|
|
958
|
+
|
|
959
|
+
# 阶段3:计算所有图片的位置
|
|
960
|
+
area_map = {}
|
|
961
|
+
for row in range(start_row, last_row + 1):
|
|
962
|
+
log(f'计算位置 {row}/{last_row}')
|
|
963
|
+
for col_letter in col_letter_map.values():
|
|
964
|
+
cell_ref = f'{col_letter}{row}'
|
|
965
|
+
cell_range = sheet.range(cell_ref)
|
|
966
|
+
cell_address = cell_range.address
|
|
967
|
+
img_width = col_width_map[col_letter]
|
|
968
|
+
|
|
969
|
+
if cell_range.merge_cells:
|
|
970
|
+
cell_range = cell_range.merge_area
|
|
971
|
+
cell_address = cell_range.address
|
|
972
|
+
|
|
973
|
+
# 重新读取调整后的宽高
|
|
974
|
+
actual_width = cell_range.width
|
|
975
|
+
actual_height = cell_range.height
|
|
976
|
+
|
|
977
|
+
# 计算该列图片的居中位置
|
|
978
|
+
# 图片实际大小是 img_width-4,插入时偏移+2,所以这里-2补偿
|
|
979
|
+
actual_img_size = img_width - 4
|
|
980
|
+
top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
|
|
981
|
+
left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
|
|
982
|
+
|
|
983
|
+
# 每个列都单独保存位置
|
|
984
|
+
area_map[f'{cell_address}_{col_letter}'] = {
|
|
985
|
+
'top': top,
|
|
986
|
+
'left': left,
|
|
987
|
+
'width': img_width,
|
|
988
|
+
'cell_address': cell_address,
|
|
989
|
+
'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
# 阶段4:插入图片
|
|
993
|
+
for row in range(start_row, last_row + 1):
|
|
994
|
+
for img_col_name, col_letter in col_letter_map.items():
|
|
995
|
+
cell_ref = f'{col_letter}{row}'
|
|
996
|
+
cell_range = sheet.range(cell_ref)
|
|
997
|
+
original_address = cell_range.address
|
|
998
|
+
|
|
999
|
+
if cell_range.merge_cells:
|
|
1000
|
+
cell_range = cell_range.merge_area
|
|
1001
|
+
cell_address = cell_range.address
|
|
1002
|
+
else:
|
|
1003
|
+
cell_address = original_address
|
|
1004
|
+
|
|
1005
|
+
# 检查是否是合并单元格的非首单元格(跳过)
|
|
1006
|
+
area_key = f'{cell_address}_{col_letter}'
|
|
1007
|
+
if area_key not in area_map:
|
|
1008
|
+
continue
|
|
1009
|
+
|
|
1010
|
+
area_info = area_map[area_key]
|
|
1011
|
+
|
|
1012
|
+
# 对于合并单元格,只在第一个单元格处理
|
|
1013
|
+
if cell_range.merge_cells:
|
|
1014
|
+
# 获取合并区域的第一个单元格地址
|
|
1015
|
+
first_cell_in_merge = area_info['cell_list'][0] if area_info['cell_list'] else cell_address
|
|
1016
|
+
# 如果当前单元格不是合并区域的第一个单元格,跳过
|
|
1017
|
+
if original_address != first_cell_in_merge:
|
|
1018
|
+
continue
|
|
1019
|
+
|
|
1020
|
+
# 使用预计算的位置信息
|
|
1021
|
+
top = area_info['top']
|
|
1022
|
+
left = area_info['left']
|
|
1023
|
+
width = area_info['width']
|
|
1024
|
+
|
|
1025
|
+
# 获取图片链接
|
|
1026
|
+
if cell_range.merge_cells:
|
|
1027
|
+
img_url = cell_range.value[0]
|
|
1028
|
+
else:
|
|
1029
|
+
img_url = cell_range.value
|
|
1030
|
+
|
|
1031
|
+
if img_url:
|
|
1032
|
+
if img_key_letter is not None:
|
|
1033
|
+
image_dir = Path(f'{os.getenv('auto_dir')}/image') / dir_name
|
|
1034
|
+
extension = Path(img_url).suffix
|
|
1035
|
+
filename = str(sheet.range(f'{img_key_letter}{row}').value)
|
|
1036
|
+
img_save_path = image_dir / f"{sanitize_filename(filename)}{extension}"
|
|
1037
|
+
else:
|
|
1038
|
+
img_save_path = None
|
|
1039
|
+
|
|
1040
|
+
img_path = download_img_v2(img_url, platform, img_save_path)
|
|
1041
|
+
log(f'插入图片 {sheet.name} [{img_col_name}] {row}/{last_row} {img_path}')
|
|
1042
|
+
if not img_path:
|
|
1043
|
+
log('跳过:', img_path, img_url)
|
|
1044
|
+
continue
|
|
1045
|
+
cell_value = cell_range.value
|
|
1046
|
+
|
|
1047
|
+
# 插入图片
|
|
1048
|
+
try:
|
|
1049
|
+
# 使用预计算的位置直接插入图片
|
|
1050
|
+
sheet.pictures.add(img_path, top=top + 2, left=left + 2, width=width - 4, height=width - 4)
|
|
1051
|
+
cell_range.value = None
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
# 插入图片失败恢复链接地址
|
|
1054
|
+
cell_range.value = cell_value
|
|
1055
|
+
send_exception()
|
|
1056
|
+
else:
|
|
1057
|
+
log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
|
|
687
1058
|
|
|
688
1059
|
def download_images_concurrently(image_urls, platform='shein', img_save_dir=None):
|
|
689
1060
|
# 使用线程池执行并发下载
|
|
@@ -692,21 +1063,23 @@ def download_images_concurrently(image_urls, platform='shein', img_save_dir=None
|
|
|
692
1063
|
results = list(executor.map(lambda url: download_img_v2(url, platform, img_save_path=img_save_dir), image_urls))
|
|
693
1064
|
return results
|
|
694
1065
|
|
|
695
|
-
|
|
696
1066
|
def download_img_by_chrome(image_url, save_name):
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1067
|
+
try:
|
|
1068
|
+
with sync_playwright() as p:
|
|
1069
|
+
browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
|
|
1070
|
+
context = browser.new_context()
|
|
1071
|
+
page = context.new_page()
|
|
1072
|
+
# 直接通过Playwright下载图片
|
|
1073
|
+
response = page.request.get(image_url)
|
|
1074
|
+
with open(save_name, 'wb') as f:
|
|
1075
|
+
f.write(response.body()) # 将下载的内容保存为文件
|
|
1076
|
+
log(f"图片已通过chrome下载并保存为:{save_name}")
|
|
1077
|
+
# 关闭浏览器
|
|
1078
|
+
browser.close()
|
|
1079
|
+
return save_name
|
|
1080
|
+
except:
|
|
1081
|
+
send_exception()
|
|
1082
|
+
return None
|
|
710
1083
|
|
|
711
1084
|
def download_img_v2(image_url, platform='shein', img_save_path=None):
|
|
712
1085
|
image_url = add_https(image_url)
|
|
@@ -745,8 +1118,8 @@ def download_img_v2(image_url, platform='shein', img_save_path=None):
|
|
|
745
1118
|
# return False
|
|
746
1119
|
|
|
747
1120
|
headers = {
|
|
748
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
749
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
1121
|
+
"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
1122
|
+
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
750
1123
|
"Accept-Encoding": "gzip, deflate",
|
|
751
1124
|
"Accept-Language": "zh-CN,zh;q=0.9"
|
|
752
1125
|
}
|
|
@@ -779,7 +1152,6 @@ def download_img_v2(image_url, platform='shein', img_save_path=None):
|
|
|
779
1152
|
|
|
780
1153
|
return file_path
|
|
781
1154
|
|
|
782
|
-
|
|
783
1155
|
# 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
|
|
784
1156
|
# C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
|
|
785
1157
|
def insert_cell_image(sheet, cell, file_path, img_width=120):
|
|
@@ -825,7 +1197,6 @@ def insert_cell_image(sheet, cell, file_path, img_width=120):
|
|
|
825
1197
|
log(f'插入图片失败: {e}, {file_path}')
|
|
826
1198
|
send_exception()
|
|
827
1199
|
|
|
828
|
-
|
|
829
1200
|
# 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
|
|
830
1201
|
# C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
|
|
831
1202
|
def insert_image_from_local(sheet, cell, file_path, cell_width=90, cell_height=90):
|
|
@@ -864,7 +1235,6 @@ def insert_image_from_local(sheet, cell, file_path, cell_width=90, cell_height=9
|
|
|
864
1235
|
except Exception as e:
|
|
865
1236
|
log(f'插入图片失败: {e}, {file_path}')
|
|
866
1237
|
|
|
867
|
-
|
|
868
1238
|
# 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
|
|
869
1239
|
# C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
|
|
870
1240
|
def insert_skc_image_from_local(sheet, cell, file_path):
|
|
@@ -897,7 +1267,6 @@ def insert_skc_image_from_local(sheet, cell, file_path):
|
|
|
897
1267
|
except Exception as e:
|
|
898
1268
|
log(f'插入图片失败: {e}')
|
|
899
1269
|
|
|
900
|
-
|
|
901
1270
|
# # 设置 A 列和第 1 行为接近 100x100 的正方形
|
|
902
1271
|
# set_square_cells(sheet, 'A', 1, 100)
|
|
903
1272
|
|
|
@@ -916,35 +1285,34 @@ def clear_all_pictures(sheet):
|
|
|
916
1285
|
send_exception()
|
|
917
1286
|
log(f"清空图片失败: {e}")
|
|
918
1287
|
|
|
919
|
-
|
|
920
1288
|
def get_excel_format(sheet, cell_range):
|
|
921
1289
|
rng = sheet.range(cell_range)
|
|
922
1290
|
|
|
923
1291
|
format_settings = {
|
|
924
1292
|
"numberFormat": rng.number_format,
|
|
925
|
-
"font": {
|
|
926
|
-
"name": rng.api.Font.Name,
|
|
927
|
-
"size": rng.api.Font.Size,
|
|
928
|
-
"bold": rng.api.Font.Bold,
|
|
1293
|
+
"font" : {
|
|
1294
|
+
"name" : rng.api.Font.Name,
|
|
1295
|
+
"size" : rng.api.Font.Size,
|
|
1296
|
+
"bold" : rng.api.Font.Bold,
|
|
929
1297
|
"italic": rng.api.Font.Italic,
|
|
930
|
-
"color": rng.api.Font.Color
|
|
1298
|
+
"color" : rng.api.Font.Color
|
|
931
1299
|
},
|
|
932
|
-
"alignment": {
|
|
1300
|
+
"alignment" : {
|
|
933
1301
|
"horizontalAlignment": rng.api.HorizontalAlignment,
|
|
934
|
-
"verticalAlignment": rng.api.VerticalAlignment,
|
|
935
|
-
"wrapText": rng.api.WrapText
|
|
1302
|
+
"verticalAlignment" : rng.api.VerticalAlignment,
|
|
1303
|
+
"wrapText" : rng.api.WrapText
|
|
936
1304
|
},
|
|
937
|
-
"borders": []
|
|
1305
|
+
"borders" : []
|
|
938
1306
|
}
|
|
939
1307
|
|
|
940
1308
|
# 获取所有边框设置(Excel 有 8 种边框)
|
|
941
1309
|
for index in range(5, 13):
|
|
942
1310
|
border = rng.api.Borders(index)
|
|
943
1311
|
format_settings["borders"].append({
|
|
944
|
-
"index": index,
|
|
1312
|
+
"index" : index,
|
|
945
1313
|
"lineStyle": border.LineStyle,
|
|
946
|
-
"color": border.Color,
|
|
947
|
-
"weight": border.Weight
|
|
1314
|
+
"color" : border.Color,
|
|
1315
|
+
"weight" : border.Weight
|
|
948
1316
|
})
|
|
949
1317
|
|
|
950
1318
|
# 获取背景色
|
|
@@ -958,7 +1326,6 @@ def get_excel_format(sheet, cell_range):
|
|
|
958
1326
|
|
|
959
1327
|
return json.dumps(format_settings, indent=2)
|
|
960
1328
|
|
|
961
|
-
|
|
962
1329
|
def set_excel_format(sheet, cell_range, json_setting):
|
|
963
1330
|
settings = json.loads(json_setting)
|
|
964
1331
|
|
|
@@ -1017,7 +1384,6 @@ def set_excel_format(sheet, cell_range, json_setting):
|
|
|
1017
1384
|
if "formulaHidden" in settings:
|
|
1018
1385
|
rng.api.FormulaHidden = settings["formulaHidden"]
|
|
1019
1386
|
|
|
1020
|
-
|
|
1021
1387
|
# # 获取 A1 单元格格式
|
|
1022
1388
|
# json_format = get_excel_format(sheet, "A1")
|
|
1023
1389
|
# log("Original Format:", json_format)
|
|
@@ -1056,7 +1422,6 @@ def get_unique_values(sheet, column, start_row, end_row=None):
|
|
|
1056
1422
|
# unique_values = get_unique_values(sheet, 'A', 2)
|
|
1057
1423
|
# log(unique_values)
|
|
1058
1424
|
|
|
1059
|
-
|
|
1060
1425
|
def get_unique_values_by_row(sheet, row, start_col, end_col=None):
|
|
1061
1426
|
"""
|
|
1062
1427
|
获取指定行从指定列开始的不重复值列表,确保读取的值与 Excel 中显示的内容完全一致。
|
|
@@ -1088,7 +1453,6 @@ def get_unique_values_by_row(sheet, row, start_col, end_col=None):
|
|
|
1088
1453
|
# 获取第 2 行从 A 列开始的不重复值
|
|
1089
1454
|
# unique_values = get_unique_values_by_row(sheet, 2, 'A')
|
|
1090
1455
|
|
|
1091
|
-
|
|
1092
1456
|
def find_rows_by_criteria(sheet, col, search_text, match_type='equals'):
|
|
1093
1457
|
"""
|
|
1094
1458
|
在指定列中查找符合条件的数据所在行。
|
|
@@ -1140,7 +1504,6 @@ def find_rows_by_criteria(sheet, col, search_text, match_type='equals'):
|
|
|
1140
1504
|
# result_negative_col = find_rows_by_criteria(sheet, -1, 'xyz', match_type='equals')
|
|
1141
1505
|
# log("倒数第一列匹配结果:", result_negative_col)
|
|
1142
1506
|
|
|
1143
|
-
|
|
1144
1507
|
def find_columns_by_criteria(sheet, row, search_text, match_type='equals'):
|
|
1145
1508
|
"""
|
|
1146
1509
|
在指定行中查找符合条件的数据所在列。
|
|
@@ -1186,12 +1549,10 @@ def find_columns_by_criteria(sheet, row, search_text, match_type='equals'):
|
|
|
1186
1549
|
# result_negative_row = find_columns_by_criteria(sheet, -1, 'xyz', match_type='equals')
|
|
1187
1550
|
# log("倒数第一行匹配结果:", result_negative_row)
|
|
1188
1551
|
|
|
1189
|
-
|
|
1190
1552
|
def check_data(data):
|
|
1191
1553
|
for row in data:
|
|
1192
1554
|
log(len(row), row)
|
|
1193
1555
|
|
|
1194
|
-
|
|
1195
1556
|
def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
|
|
1196
1557
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1197
1558
|
# 清空工作表中的所有数据
|
|
@@ -1205,9 +1566,8 @@ def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
|
|
|
1205
1566
|
wb.save()
|
|
1206
1567
|
close_excel(app, wb)
|
|
1207
1568
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
minimize(app)
|
|
1569
|
+
def colorize_by_field(sheet, field):
|
|
1570
|
+
minimize(sheet.book.app)
|
|
1211
1571
|
# 读取数据
|
|
1212
1572
|
field_column = find_column_by_data(sheet, 1, field) # 假设 SPU 在 C 列
|
|
1213
1573
|
if field_column is None:
|
|
@@ -1228,6 +1588,72 @@ def colorize_by_field(app, wb, sheet, field):
|
|
|
1228
1588
|
row_range.color = bg_color # 应用背景色
|
|
1229
1589
|
sheet.range(f"A{row}").api.Font.Bold = True # 让店铺名称加粗
|
|
1230
1590
|
|
|
1591
|
+
def colorize_by_field_v2(sheet, field):
|
|
1592
|
+
"""
|
|
1593
|
+
改进版:按指定字段为行着色,正确处理合并单元格
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
sheet: Excel工作表对象
|
|
1597
|
+
field: 用于分组着色的字段名(列名)
|
|
1598
|
+
"""
|
|
1599
|
+
minimize(sheet.book.app)
|
|
1600
|
+
|
|
1601
|
+
# 查找字段所在的列
|
|
1602
|
+
field_column = find_column_by_data(sheet, 1, field)
|
|
1603
|
+
if field_column is None:
|
|
1604
|
+
log(f'未找到字段列: {field}')
|
|
1605
|
+
return
|
|
1606
|
+
|
|
1607
|
+
log(f'按字段 {field} (列 {field_column}) 着色')
|
|
1608
|
+
|
|
1609
|
+
# 获取最后一行和最后一列
|
|
1610
|
+
last_row = get_last_row(sheet, field_column)
|
|
1611
|
+
max_column_letter = get_max_column_letter(sheet)
|
|
1612
|
+
|
|
1613
|
+
# 记录字段值对应的颜色
|
|
1614
|
+
field_color_map = {}
|
|
1615
|
+
last_field_value = None # 记录上一个非空值
|
|
1616
|
+
|
|
1617
|
+
# 从第2行开始遍历(跳过表头)
|
|
1618
|
+
for row in range(2, last_row + 1):
|
|
1619
|
+
# 读取当前行的字段值
|
|
1620
|
+
cell = sheet.range(f'{field_column}{row}')
|
|
1621
|
+
current_value = cell.value
|
|
1622
|
+
|
|
1623
|
+
# 如果是合并单元格的非首单元格,值可能为 None,使用上一个非空值
|
|
1624
|
+
if current_value is None or current_value == '':
|
|
1625
|
+
# 检查是否是合并单元格
|
|
1626
|
+
if cell.merge_cells:
|
|
1627
|
+
# 使用合并区域的值
|
|
1628
|
+
merge_area = cell.merge_area
|
|
1629
|
+
current_value = merge_area.value
|
|
1630
|
+
if isinstance(current_value, (list, tuple)):
|
|
1631
|
+
current_value = current_value[0] if current_value else None
|
|
1632
|
+
|
|
1633
|
+
# 如果仍然为空,使用上一个非空值
|
|
1634
|
+
if current_value is None or current_value == '':
|
|
1635
|
+
current_value = last_field_value
|
|
1636
|
+
else:
|
|
1637
|
+
# 更新上一个非空值
|
|
1638
|
+
last_field_value = current_value
|
|
1639
|
+
|
|
1640
|
+
# 跳过空值
|
|
1641
|
+
if current_value is None or current_value == '':
|
|
1642
|
+
continue
|
|
1643
|
+
|
|
1644
|
+
# 为新的字段值分配颜色
|
|
1645
|
+
if current_value not in field_color_map:
|
|
1646
|
+
field_color_map[current_value] = random_color()
|
|
1647
|
+
|
|
1648
|
+
# 应用背景色到整行
|
|
1649
|
+
bg_color = field_color_map[current_value]
|
|
1650
|
+
row_range = sheet.range(f'A{row}:{max_column_letter}{row}')
|
|
1651
|
+
row_range.color = bg_color
|
|
1652
|
+
|
|
1653
|
+
# 可选:让第一列加粗(店铺信息等)
|
|
1654
|
+
# sheet.range(f'A{row}').api.Font.Bold = True
|
|
1655
|
+
|
|
1656
|
+
log(f'着色完成,共 {len(field_color_map)} 个不同的 {field} 值')
|
|
1231
1657
|
|
|
1232
1658
|
def add_borders(sheet, lineStyle=1):
|
|
1233
1659
|
log('添加边框')
|
|
@@ -1248,7 +1674,6 @@ def add_borders(sheet, lineStyle=1):
|
|
|
1248
1674
|
range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
|
|
1249
1675
|
range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
|
|
1250
1676
|
|
|
1251
|
-
|
|
1252
1677
|
def add_range_border(sheet, coor_A=(1, 1), coor_B=(1, 1), lineStyle=1):
|
|
1253
1678
|
range_to_border = sheet.range(coor_A, coor_B) # 定义范围
|
|
1254
1679
|
|
|
@@ -1264,7 +1689,6 @@ def add_range_border(sheet, coor_A=(1, 1), coor_B=(1, 1), lineStyle=1):
|
|
|
1264
1689
|
range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
|
|
1265
1690
|
range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
|
|
1266
1691
|
|
|
1267
|
-
|
|
1268
1692
|
def open_excel(excel_path, sheet_name='Sheet1'):
|
|
1269
1693
|
try:
|
|
1270
1694
|
# 创建新实例
|
|
@@ -1321,7 +1745,6 @@ def open_excel(excel_path, sheet_name='Sheet1'):
|
|
|
1321
1745
|
# wxwork.notify_error_msg(f'打开 Excel 失败: {traceback.format_exc()}')
|
|
1322
1746
|
return None, None, None
|
|
1323
1747
|
|
|
1324
|
-
|
|
1325
1748
|
def close_excel(app, wb):
|
|
1326
1749
|
if wb is not None:
|
|
1327
1750
|
wb.save()
|
|
@@ -1329,7 +1752,6 @@ def close_excel(app, wb):
|
|
|
1329
1752
|
if app is not None:
|
|
1330
1753
|
app.quit()
|
|
1331
1754
|
|
|
1332
|
-
|
|
1333
1755
|
# 获取某列最后非空行
|
|
1334
1756
|
def get_last_row(sheet, column):
|
|
1335
1757
|
last_row = sheet.range(column + str(sheet.cells.last_cell.row)).end('up').row
|
|
@@ -1340,38 +1762,32 @@ def get_last_row(sheet, column):
|
|
|
1340
1762
|
last_row = cell.merge_area.last_cell.row
|
|
1341
1763
|
return last_row
|
|
1342
1764
|
|
|
1343
|
-
|
|
1344
1765
|
# 获取最后一列字母
|
|
1345
1766
|
def get_last_col(sheet):
|
|
1346
1767
|
# # 获取最后一行的索引
|
|
1347
1768
|
last_col = index_to_column_name(sheet.range('A1').end('right').column) # 里面是索引 返回最后一列 如 C
|
|
1348
1769
|
return last_col
|
|
1349
1770
|
|
|
1350
|
-
|
|
1351
1771
|
# 获取最大列名字母
|
|
1352
1772
|
def get_max_column_letter(sheet):
|
|
1353
1773
|
"""获取当前 sheet 中最大有数据的列的列名(如 'A', 'B', ..., 'Z', 'AA', 'AB')"""
|
|
1354
1774
|
last_col = sheet.used_range.last_cell.column # 获取最大列索引
|
|
1355
1775
|
return xw.utils.col_name(last_col) # 将索引转换为列名
|
|
1356
1776
|
|
|
1357
|
-
|
|
1358
1777
|
# 随机生成颜色
|
|
1359
1778
|
def random_color():
|
|
1360
1779
|
return (random.randint(180, 255), random.randint(180, 255), random.randint(180, 255)) # 亮色背景
|
|
1361
1780
|
|
|
1362
|
-
|
|
1363
1781
|
def get_contrast_text_color(rgb):
|
|
1364
1782
|
"""根据背景色亮度返回适合的字体颜色(黑色或白色)"""
|
|
1365
1783
|
r, g, b = rgb
|
|
1366
1784
|
brightness = r * 0.299 + g * 0.587 + b * 0.114 # 亮度计算公式
|
|
1367
1785
|
return (0, 0, 0) if brightness > 186 else (255, 255, 255) # 186 是经验值
|
|
1368
1786
|
|
|
1369
|
-
|
|
1370
1787
|
def rgb_to_long(r, g, b):
|
|
1371
1788
|
"""将 RGB 颜色转换为 Excel Long 类型"""
|
|
1372
1789
|
return r + (g * 256) + (b * 256 * 256)
|
|
1373
1790
|
|
|
1374
|
-
|
|
1375
1791
|
def read_excel_to_json(file_path, sheet_name="Sheet1"):
|
|
1376
1792
|
app, wb, sheet = open_excel(file_path, sheet_name)
|
|
1377
1793
|
|
|
@@ -1409,20 +1825,20 @@ def read_excel_to_json(file_path, sheet_name="Sheet1"):
|
|
|
1409
1825
|
diagonal_down_info = {"style": diagonal_down.LineStyle, "color": diagonal_down.Color}
|
|
1410
1826
|
|
|
1411
1827
|
cell_info = {
|
|
1412
|
-
"value": cell.value,
|
|
1413
|
-
"color": cell.color,
|
|
1414
|
-
"font_name": cell.api.Font.Name,
|
|
1415
|
-
"font_size": cell.api.Font.Size,
|
|
1416
|
-
"bold": cell.api.Font.Bold,
|
|
1417
|
-
"italic": cell.api.Font.Italic,
|
|
1418
|
-
"font_color": cell.api.Font.Color,
|
|
1828
|
+
"value" : cell.value,
|
|
1829
|
+
"color" : cell.color,
|
|
1830
|
+
"font_name" : cell.api.Font.Name,
|
|
1831
|
+
"font_size" : cell.api.Font.Size,
|
|
1832
|
+
"bold" : cell.api.Font.Bold,
|
|
1833
|
+
"italic" : cell.api.Font.Italic,
|
|
1834
|
+
"font_color" : cell.api.Font.Color,
|
|
1419
1835
|
"horizontal_align": cell.api.HorizontalAlignment,
|
|
1420
|
-
"vertical_align": cell.api.VerticalAlignment,
|
|
1421
|
-
"number_format": cell.api.NumberFormat,
|
|
1422
|
-
"border": {
|
|
1423
|
-
"left": {"style": cell.api.Borders(1).LineStyle, "color": cell.api.Borders(1).Color},
|
|
1424
|
-
"right": {"style": cell.api.Borders(2).LineStyle, "color": cell.api.Borders(2).Color},
|
|
1425
|
-
"top": {"style": cell.api.Borders(3).LineStyle, "color": cell.api.Borders(3).Color},
|
|
1836
|
+
"vertical_align" : cell.api.VerticalAlignment,
|
|
1837
|
+
"number_format" : cell.api.NumberFormat,
|
|
1838
|
+
"border" : {
|
|
1839
|
+
"left" : {"style": cell.api.Borders(1).LineStyle, "color": cell.api.Borders(1).Color},
|
|
1840
|
+
"right" : {"style": cell.api.Borders(2).LineStyle, "color": cell.api.Borders(2).Color},
|
|
1841
|
+
"top" : {"style": cell.api.Borders(3).LineStyle, "color": cell.api.Borders(3).Color},
|
|
1426
1842
|
"bottom": {"style": cell.api.Borders(4).LineStyle, "color": cell.api.Borders(4).Color},
|
|
1427
1843
|
}
|
|
1428
1844
|
}
|
|
@@ -1445,10 +1861,10 @@ def read_excel_to_json(file_path, sheet_name="Sheet1"):
|
|
|
1445
1861
|
app.quit()
|
|
1446
1862
|
|
|
1447
1863
|
final_data = {
|
|
1448
|
-
"cells": data,
|
|
1449
|
-
"merged_cells": merged_cells,
|
|
1864
|
+
"cells" : data,
|
|
1865
|
+
"merged_cells" : merged_cells,
|
|
1450
1866
|
"column_widths": column_widths,
|
|
1451
|
-
"row_heights": row_heights
|
|
1867
|
+
"row_heights" : row_heights
|
|
1452
1868
|
}
|
|
1453
1869
|
|
|
1454
1870
|
with open("excel_data.json", "w", encoding="utf-8") as f:
|
|
@@ -1456,7 +1872,6 @@ def read_excel_to_json(file_path, sheet_name="Sheet1"):
|
|
|
1456
1872
|
|
|
1457
1873
|
print("✅ Excel 数据已存储为 JSON")
|
|
1458
1874
|
|
|
1459
|
-
|
|
1460
1875
|
def write_json_to_excel(json_file, new_excel="new_test.xlsx", sheet_name="Sheet1"):
|
|
1461
1876
|
with open(json_file, "r", encoding="utf-8") as f:
|
|
1462
1877
|
final_data = json.load(f)
|
|
@@ -1519,7 +1934,6 @@ def write_json_to_excel(json_file, new_excel="new_test.xlsx", sheet_name="Sheet1
|
|
|
1519
1934
|
print(f"✅ 数据已成功写入 {new_excel}")
|
|
1520
1935
|
time.sleep(2) # 这里需要一个延时
|
|
1521
1936
|
|
|
1522
|
-
|
|
1523
1937
|
def safe_expand_down(sheet, start_cell='A2'):
|
|
1524
1938
|
rng = sheet.range(start_cell)
|
|
1525
1939
|
if not rng.value:
|
|
@@ -1530,7 +1944,6 @@ def safe_expand_down(sheet, start_cell='A2'):
|
|
|
1530
1944
|
log(f'safe_expand_down failed: {e}')
|
|
1531
1945
|
return [rng] # 返回单元格本身
|
|
1532
1946
|
|
|
1533
|
-
|
|
1534
1947
|
# 初始化一个表格
|
|
1535
1948
|
# data 需要是一个二维列表
|
|
1536
1949
|
def init_progress_ex(key_id, excel_path, sheet_name='Sheet1'):
|
|
@@ -1565,7 +1978,6 @@ def init_progress_ex(key_id, excel_path, sheet_name='Sheet1'):
|
|
|
1565
1978
|
|
|
1566
1979
|
wb.save()
|
|
1567
1980
|
|
|
1568
|
-
|
|
1569
1981
|
def init_data_ex(key_id, excel_path, header, sheet_name='Sheet1'):
|
|
1570
1982
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1571
1983
|
|
|
@@ -1594,7 +2006,6 @@ def init_data_ex(key_id, excel_path, header, sheet_name='Sheet1'):
|
|
|
1594
2006
|
|
|
1595
2007
|
wb.save()
|
|
1596
2008
|
|
|
1597
|
-
|
|
1598
2009
|
def format_header_row(sheet, column_count):
|
|
1599
2010
|
"""
|
|
1600
2011
|
设置标题行样式和列对齐
|
|
@@ -1616,7 +2027,6 @@ def format_header_row(sheet, column_count):
|
|
|
1616
2027
|
# 自动调整列宽
|
|
1617
2028
|
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
1618
2029
|
|
|
1619
|
-
|
|
1620
2030
|
# 初始化一个表格
|
|
1621
2031
|
# data 需要是一个二维列表
|
|
1622
2032
|
def init_progress(excel_path, keyID, sheet_name='Sheet1'):
|
|
@@ -1668,7 +2078,6 @@ def init_progress(excel_path, keyID, sheet_name='Sheet1'):
|
|
|
1668
2078
|
|
|
1669
2079
|
wb.save()
|
|
1670
2080
|
|
|
1671
|
-
|
|
1672
2081
|
def get_progress(excel_path, keyID, sheet_name="Sheet1"):
|
|
1673
2082
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1674
2083
|
# 遍历可用行
|
|
@@ -1685,7 +2094,6 @@ def get_progress(excel_path, keyID, sheet_name="Sheet1"):
|
|
|
1685
2094
|
else:
|
|
1686
2095
|
return False
|
|
1687
2096
|
|
|
1688
|
-
|
|
1689
2097
|
def get_progress_ex(keyID, excel_path, sheet_name="Sheet1"):
|
|
1690
2098
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1691
2099
|
# 遍历可用行
|
|
@@ -1703,7 +2111,6 @@ def get_progress_ex(keyID, excel_path, sheet_name="Sheet1"):
|
|
|
1703
2111
|
return False
|
|
1704
2112
|
close_excel(app, wb)
|
|
1705
2113
|
|
|
1706
|
-
|
|
1707
2114
|
def get_progress_data(excel_path, keyID, sheet_name="Sheet1"):
|
|
1708
2115
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1709
2116
|
# 遍历可用行
|
|
@@ -1718,7 +2125,6 @@ def get_progress_data(excel_path, keyID, sheet_name="Sheet1"):
|
|
|
1718
2125
|
return result
|
|
1719
2126
|
return None
|
|
1720
2127
|
|
|
1721
|
-
|
|
1722
2128
|
def get_progress_data_ex(keyID, excel_path, sheet_name="Sheet1"):
|
|
1723
2129
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1724
2130
|
# 遍历可用行
|
|
@@ -1733,7 +2139,6 @@ def get_progress_data_ex(keyID, excel_path, sheet_name="Sheet1"):
|
|
|
1733
2139
|
return result
|
|
1734
2140
|
return None
|
|
1735
2141
|
|
|
1736
|
-
|
|
1737
2142
|
def set_progress(excel_path, keyID, status='已完成', sheet_name="Sheet1"):
|
|
1738
2143
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1739
2144
|
# 遍历可用行
|
|
@@ -1748,7 +2153,6 @@ def set_progress(excel_path, keyID, status='已完成', sheet_name="Sheet1"):
|
|
|
1748
2153
|
wb.save()
|
|
1749
2154
|
return
|
|
1750
2155
|
|
|
1751
|
-
|
|
1752
2156
|
def set_progress_ex(keyID, excel_path, status='已完成', sheet_name="Sheet1"):
|
|
1753
2157
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1754
2158
|
# 遍历可用行
|
|
@@ -1765,7 +2169,6 @@ def set_progress_ex(keyID, excel_path, status='已完成', sheet_name="Sheet1"):
|
|
|
1765
2169
|
return
|
|
1766
2170
|
close_excel(app, wb)
|
|
1767
2171
|
|
|
1768
|
-
|
|
1769
2172
|
def set_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
|
|
1770
2173
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1771
2174
|
# 遍历可用行
|
|
@@ -1780,7 +2183,6 @@ def set_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
|
|
|
1780
2183
|
wb.save()
|
|
1781
2184
|
return
|
|
1782
2185
|
|
|
1783
|
-
|
|
1784
2186
|
def set_progress_data(excel_path, keyID, data, sheet_name="Sheet1"):
|
|
1785
2187
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1786
2188
|
# 遍历可用行
|
|
@@ -1796,7 +2198,6 @@ def set_progress_data(excel_path, keyID, data, sheet_name="Sheet1"):
|
|
|
1796
2198
|
wb.save()
|
|
1797
2199
|
return
|
|
1798
2200
|
|
|
1799
|
-
|
|
1800
2201
|
def set_progress_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
|
|
1801
2202
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1802
2203
|
# 遍历可用行
|
|
@@ -1812,7 +2213,6 @@ def set_progress_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
|
|
|
1812
2213
|
wb.save()
|
|
1813
2214
|
return
|
|
1814
2215
|
|
|
1815
|
-
|
|
1816
2216
|
def check_progress(excel_path, listKeyID, sheet_name="Sheet1"):
|
|
1817
2217
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1818
2218
|
# 读取整个任务表数据
|
|
@@ -1827,7 +2227,6 @@ def check_progress(excel_path, listKeyID, sheet_name="Sheet1"):
|
|
|
1827
2227
|
incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
|
|
1828
2228
|
return len(incomplete_tasks) == 0, incomplete_tasks
|
|
1829
2229
|
|
|
1830
|
-
|
|
1831
2230
|
def check_progress_ex(listKeyID, excel_path, sheet_name="Sheet1"):
|
|
1832
2231
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1833
2232
|
# 读取整个任务表数据
|
|
@@ -1842,7 +2241,6 @@ def check_progress_ex(listKeyID, excel_path, sheet_name="Sheet1"):
|
|
|
1842
2241
|
incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
|
|
1843
2242
|
return len(incomplete_tasks) == 0, incomplete_tasks
|
|
1844
2243
|
|
|
1845
|
-
|
|
1846
2244
|
def read_excel_sheet_to_list(file_path, sheet_name=None):
|
|
1847
2245
|
"""
|
|
1848
2246
|
使用 xlwings 读取 Excel 文件中指定工作表的数据,并返回为二维列表。
|
|
@@ -1864,7 +2262,6 @@ def read_excel_sheet_to_list(file_path, sheet_name=None):
|
|
|
1864
2262
|
else:
|
|
1865
2263
|
return [data]
|
|
1866
2264
|
|
|
1867
|
-
|
|
1868
2265
|
def excel_to_dict(excel_path, column_key, column_value, sheet_name=None):
|
|
1869
2266
|
"""
|
|
1870
2267
|
从 Excel 文件中读取指定两列,生成字典返回(不受中间空行影响)
|
|
@@ -1912,7 +2309,6 @@ def excel_to_dict(excel_path, column_key, column_value, sheet_name=None):
|
|
|
1912
2309
|
wb.close()
|
|
1913
2310
|
app.quit()
|
|
1914
2311
|
|
|
1915
|
-
|
|
1916
2312
|
def format_to_text_v2(sheet, columns=None):
|
|
1917
2313
|
if columns is None or len(columns) == 0:
|
|
1918
2314
|
return
|
|
@@ -1922,6 +2318,140 @@ def format_to_text_v2(sheet, columns=None):
|
|
|
1922
2318
|
log(f'设置[{col_name}] 文本格式')
|
|
1923
2319
|
sheet.range(f'{col_name}:{col_name}').number_format = '@'
|
|
1924
2320
|
|
|
2321
|
+
def format_to_text_v2_safe(sheet, columns=None, data_rows=None):
|
|
2322
|
+
"""
|
|
2323
|
+
更安全的文本格式化函数,避免COM异常
|
|
2324
|
+
|
|
2325
|
+
Args:
|
|
2326
|
+
sheet: Excel工作表对象
|
|
2327
|
+
columns: 要格式化的列名列表
|
|
2328
|
+
data_rows: 数据行数,用于限制格式化范围
|
|
2329
|
+
"""
|
|
2330
|
+
if columns is None or len(columns) == 0:
|
|
2331
|
+
return
|
|
2332
|
+
|
|
2333
|
+
# 确保columns是列表
|
|
2334
|
+
if not isinstance(columns, list):
|
|
2335
|
+
columns = [columns]
|
|
2336
|
+
|
|
2337
|
+
for col_name in columns:
|
|
2338
|
+
try:
|
|
2339
|
+
if isinstance(col_name, int):
|
|
2340
|
+
col_name = xw.utils.col_name(col_name)
|
|
2341
|
+
|
|
2342
|
+
log(f'安全设置[{col_name}] 文本格式')
|
|
2343
|
+
|
|
2344
|
+
# 如果指定了数据行数,只格式化有数据的范围
|
|
2345
|
+
if data_rows and data_rows > 0:
|
|
2346
|
+
# 格式化从第1行到数据行数的范围
|
|
2347
|
+
range_str = f'{col_name}1:{col_name}{data_rows}'
|
|
2348
|
+
sheet.range(range_str).number_format = '@'
|
|
2349
|
+
else:
|
|
2350
|
+
# 检查列是否有数据,如果没有则跳过
|
|
2351
|
+
try:
|
|
2352
|
+
# 先检查第一个单元格是否存在
|
|
2353
|
+
test_range = sheet.range(f'{col_name}1')
|
|
2354
|
+
if test_range.value is not None or sheet.used_range.last_cell.column >= column_name_to_index(col_name) + 1:
|
|
2355
|
+
sheet.range(f'{col_name}:{col_name}').number_format = '@'
|
|
2356
|
+
else:
|
|
2357
|
+
log(f'列 {col_name} 没有数据,跳过格式化')
|
|
2358
|
+
except:
|
|
2359
|
+
log(f'列 {col_name} 格式化失败,跳过')
|
|
2360
|
+
|
|
2361
|
+
except Exception as e:
|
|
2362
|
+
log(f'设置列 {col_name} 文本格式失败: {e},继续处理其他列')
|
|
2363
|
+
|
|
2364
|
+
def pre_format_columns_safe(sheet, columns, data_rows):
|
|
2365
|
+
"""
|
|
2366
|
+
预格式化函数:在写入数据前安全地设置列格式
|
|
2367
|
+
|
|
2368
|
+
Args:
|
|
2369
|
+
sheet: Excel工作表对象
|
|
2370
|
+
columns: 要格式化的列名列表
|
|
2371
|
+
data_rows: 预期数据行数
|
|
2372
|
+
"""
|
|
2373
|
+
if not columns or not isinstance(columns, list):
|
|
2374
|
+
return
|
|
2375
|
+
|
|
2376
|
+
for col_name in columns:
|
|
2377
|
+
try:
|
|
2378
|
+
if isinstance(col_name, int):
|
|
2379
|
+
col_name = xw.utils.col_name(col_name)
|
|
2380
|
+
|
|
2381
|
+
log(f'预格式化列 [{col_name}] 为文本格式')
|
|
2382
|
+
|
|
2383
|
+
# 方法1:先创建最小范围,避免整列操作
|
|
2384
|
+
try:
|
|
2385
|
+
# 创建足够大的范围来覆盖预期数据
|
|
2386
|
+
range_str = f'{col_name}1:{col_name}{max(data_rows, 1000)}'
|
|
2387
|
+
sheet.range(range_str).number_format = '@'
|
|
2388
|
+
log(f'预格式化成功: {range_str}')
|
|
2389
|
+
except Exception as e1:
|
|
2390
|
+
log(f'预格式化方法1失败: {e1}')
|
|
2391
|
+
|
|
2392
|
+
# 方法2:逐行设置格式,更安全但稍慢
|
|
2393
|
+
try:
|
|
2394
|
+
for row in range(1, data_rows + 1):
|
|
2395
|
+
cell = sheet.range(f'{col_name}{row}')
|
|
2396
|
+
cell.number_format = '@'
|
|
2397
|
+
log(f'逐行预格式化成功: {col_name}')
|
|
2398
|
+
except Exception as e2:
|
|
2399
|
+
log(f'逐行预格式化也失败: {e2}')
|
|
2400
|
+
|
|
2401
|
+
except Exception as e:
|
|
2402
|
+
log(f'预格式化列 {col_name} 失败: {e},继续处理其他列')
|
|
2403
|
+
|
|
2404
|
+
def post_format_columns_safe(sheet, columns, data_rows):
|
|
2405
|
+
"""
|
|
2406
|
+
后格式化函数:在写入数据后确认列格式并强制转换为文本
|
|
2407
|
+
|
|
2408
|
+
Args:
|
|
2409
|
+
sheet: Excel工作表对象
|
|
2410
|
+
columns: 要格式化的列名列表
|
|
2411
|
+
data_rows: 实际数据行数
|
|
2412
|
+
"""
|
|
2413
|
+
if not columns or not isinstance(columns, list):
|
|
2414
|
+
return
|
|
2415
|
+
|
|
2416
|
+
for col_name in columns:
|
|
2417
|
+
try:
|
|
2418
|
+
if isinstance(col_name, int):
|
|
2419
|
+
col_name = xw.utils.col_name(col_name)
|
|
2420
|
+
|
|
2421
|
+
log(f'后格式化列 [{col_name}] 为文本格式')
|
|
2422
|
+
|
|
2423
|
+
# 只对实际有数据的行进行格式化
|
|
2424
|
+
if data_rows > 0:
|
|
2425
|
+
range_str = f'{col_name}1:{col_name}{data_rows}'
|
|
2426
|
+
target_range = sheet.range(range_str)
|
|
2427
|
+
|
|
2428
|
+
# 设置格式为文本
|
|
2429
|
+
target_range.number_format = '@'
|
|
2430
|
+
|
|
2431
|
+
# 关键步骤:读取数据并重新写入,触发文本转换
|
|
2432
|
+
# 这样可以将已经写入的数字转换为文本格式
|
|
2433
|
+
values = target_range.value
|
|
2434
|
+
if values is not None:
|
|
2435
|
+
# 处理单个值的情况
|
|
2436
|
+
if not isinstance(values, list):
|
|
2437
|
+
if values != '':
|
|
2438
|
+
target_range.value = str(values)
|
|
2439
|
+
# 处理列表的情况(单列多行)
|
|
2440
|
+
elif len(values) > 0:
|
|
2441
|
+
# 检查是否是二维数组(实际上单列应该是一维数组)
|
|
2442
|
+
if isinstance(values[0], list):
|
|
2443
|
+
# 二维数组,取第一列
|
|
2444
|
+
converted_values = [[str(row[0]) if row[0] is not None and row[0] != '' else row[0]] for row in values]
|
|
2445
|
+
else:
|
|
2446
|
+
# 一维数组
|
|
2447
|
+
converted_values = [[str(val)] if val is not None and val != '' else [val] for val in values]
|
|
2448
|
+
# 重新写入(这次会按照文本格式写入)
|
|
2449
|
+
target_range.value = converted_values
|
|
2450
|
+
|
|
2451
|
+
log(f'后格式化并转换成功: {range_str}')
|
|
2452
|
+
|
|
2453
|
+
except Exception as e:
|
|
2454
|
+
log(f'后格式化列 {col_name} 失败: {e},继续处理其他列')
|
|
1925
2455
|
|
|
1926
2456
|
def format_to_text(sheet, columns=None):
|
|
1927
2457
|
if columns is None:
|
|
@@ -1936,7 +2466,6 @@ def format_to_text(sheet, columns=None):
|
|
|
1936
2466
|
log(f'设置[{c}] 文本格式')
|
|
1937
2467
|
sheet.range(f'{col_name}:{col_name}').number_format = '@'
|
|
1938
2468
|
|
|
1939
|
-
|
|
1940
2469
|
def format_to_date(sheet, columns=None):
|
|
1941
2470
|
if columns is None:
|
|
1942
2471
|
return
|
|
@@ -1952,7 +2481,6 @@ def format_to_date(sheet, columns=None):
|
|
|
1952
2481
|
log(f'设置[{c}] 时间格式')
|
|
1953
2482
|
sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd'
|
|
1954
2483
|
|
|
1955
|
-
|
|
1956
2484
|
def format_to_datetime(sheet, columns=None):
|
|
1957
2485
|
if columns is None:
|
|
1958
2486
|
return
|
|
@@ -1968,7 +2496,6 @@ def format_to_datetime(sheet, columns=None):
|
|
|
1968
2496
|
log(f'设置[{c}] 时间格式')
|
|
1969
2497
|
sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd hh:mm:ss'
|
|
1970
2498
|
|
|
1971
|
-
|
|
1972
2499
|
def format_to_month(sheet, columns=None):
|
|
1973
2500
|
if columns is None:
|
|
1974
2501
|
return
|
|
@@ -1982,15 +2509,14 @@ def format_to_month(sheet, columns=None):
|
|
|
1982
2509
|
log(f'设置[{c}] 年月格式')
|
|
1983
2510
|
sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm'
|
|
1984
2511
|
|
|
1985
|
-
|
|
1986
2512
|
def add_sum_for_cell(sheet, col_list, row=2):
|
|
1987
2513
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2514
|
+
if last_row > row:
|
|
2515
|
+
for col_name in col_list:
|
|
2516
|
+
col_letter = find_column_by_data(sheet, 1, col_name)
|
|
2517
|
+
sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
|
|
2518
|
+
sheet.range(f'{col_letter}{row}').api.Font.Color = 255
|
|
2519
|
+
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
1994
2520
|
|
|
1995
2521
|
def clear_for_cell(sheet, col_list, row=2):
|
|
1996
2522
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
@@ -1998,7 +2524,6 @@ def clear_for_cell(sheet, col_list, row=2):
|
|
|
1998
2524
|
col_letter = find_column_by_data(sheet, 1, col_name)
|
|
1999
2525
|
sheet.range(f'{col_letter}{row}').value = ''
|
|
2000
2526
|
|
|
2001
|
-
|
|
2002
2527
|
def color_for_column(sheet, col_list, color_name, start_row=2):
|
|
2003
2528
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
2004
2529
|
for col_name in col_list:
|
|
@@ -2007,7 +2532,6 @@ def color_for_column(sheet, col_list, color_name, start_row=2):
|
|
|
2007
2532
|
sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api.Font.ColorIndex = excel_color_index[
|
|
2008
2533
|
color_name]
|
|
2009
2534
|
|
|
2010
|
-
|
|
2011
2535
|
def add_formula_for_column(sheet, col_name, formula, start_row=2):
|
|
2012
2536
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
2013
2537
|
col_letter = find_column_by_data(sheet, 1, col_name)
|
|
@@ -2020,7 +2544,7 @@ def add_formula_for_column(sheet, col_name, formula, start_row=2):
|
|
|
2020
2544
|
# AutoFill 快速填充到所有行(start_row 到 last_row)
|
|
2021
2545
|
sheet.range(f'{col_letter}{start_row}').api.AutoFill(
|
|
2022
2546
|
sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api)
|
|
2023
|
-
|
|
2547
|
+
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
2024
2548
|
|
|
2025
2549
|
def autofit_column(sheet, columns=None):
|
|
2026
2550
|
if columns is None:
|
|
@@ -2040,7 +2564,6 @@ def autofit_column(sheet, columns=None):
|
|
|
2040
2564
|
sheet.range(f'{col_name}:{col_name}').api.WrapText = True
|
|
2041
2565
|
sheet.range(f'{col_name}:{col_name}').autofit()
|
|
2042
2566
|
|
|
2043
|
-
|
|
2044
2567
|
def specify_column_width(sheet, columns=None, width=150):
|
|
2045
2568
|
if columns is None:
|
|
2046
2569
|
return
|
|
@@ -2056,7 +2579,6 @@ def specify_column_width(sheet, columns=None, width=150):
|
|
|
2056
2579
|
log(f'设置[{c}]宽度: {width}')
|
|
2057
2580
|
sheet.range(f'{col_name}:{col_name}').column_width = width
|
|
2058
2581
|
|
|
2059
|
-
|
|
2060
2582
|
def format_to_money(sheet, columns=None):
|
|
2061
2583
|
if columns is None:
|
|
2062
2584
|
return
|
|
@@ -2072,7 +2594,6 @@ def format_to_money(sheet, columns=None):
|
|
|
2072
2594
|
log(f'设置[{c}] 金额格式')
|
|
2073
2595
|
sheet.range(f'{col_name}:{col_name}').number_format = '¥#,##0.00'
|
|
2074
2596
|
|
|
2075
|
-
|
|
2076
2597
|
def format_to_percent(sheet, columns=None, decimal_places=2):
|
|
2077
2598
|
if columns is None:
|
|
2078
2599
|
return
|
|
@@ -2092,7 +2613,6 @@ def format_to_percent(sheet, columns=None, decimal_places=2):
|
|
|
2092
2613
|
else:
|
|
2093
2614
|
sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}%'
|
|
2094
2615
|
|
|
2095
|
-
|
|
2096
2616
|
def format_to_number(sheet, columns=None, decimal_places=2):
|
|
2097
2617
|
if not columns or not isinstance(columns, (list, tuple, set)):
|
|
2098
2618
|
log(f'未提供有效列名列表({columns}),跳过格式转换')
|
|
@@ -2117,7 +2637,6 @@ def format_to_number(sheet, columns=None, decimal_places=2):
|
|
|
2117
2637
|
sheet.range(f'{col_name}:{col_name}').number_format = number_format
|
|
2118
2638
|
break # 如果一列只匹配一个关键词可提前退出
|
|
2119
2639
|
|
|
2120
|
-
|
|
2121
2640
|
# def format_to_number(sheet, columns=None, decimal_places=2):
|
|
2122
2641
|
# if columns is None or not isinstance(columns, list):
|
|
2123
2642
|
# log('跳过格式化成数字', columns)
|
|
@@ -2138,6 +2657,21 @@ def format_to_number(sheet, columns=None, decimal_places=2):
|
|
|
2138
2657
|
# else:
|
|
2139
2658
|
# sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}'
|
|
2140
2659
|
|
|
2660
|
+
def hidden_columns(sheet, columns=None):
|
|
2661
|
+
if columns is None:
|
|
2662
|
+
return
|
|
2663
|
+
used_range_col = sheet.range('A1').expand('right')
|
|
2664
|
+
for j, cell in enumerate(used_range_col):
|
|
2665
|
+
col = j + 1
|
|
2666
|
+
col_name = index_to_column_name(col)
|
|
2667
|
+
col_val = sheet.range(f'{col_name}1').value
|
|
2668
|
+
if col_val is None:
|
|
2669
|
+
continue
|
|
2670
|
+
for c in columns:
|
|
2671
|
+
if c in col_val:
|
|
2672
|
+
log(f'设置[{c}] 隐藏')
|
|
2673
|
+
sheet.range(f'{col_name}:{col_name}').column_width = 0
|
|
2674
|
+
|
|
2141
2675
|
def column_to_right(sheet, columns=None):
|
|
2142
2676
|
if columns is None:
|
|
2143
2677
|
return
|
|
@@ -2153,11 +2687,10 @@ def column_to_right(sheet, columns=None):
|
|
|
2153
2687
|
# 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
|
|
2154
2688
|
# 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
|
|
2155
2689
|
# 所有列水平居中和垂直居中
|
|
2156
|
-
log(f'设置[{c}]
|
|
2690
|
+
log(f'设置[{c}] 水平右对齐')
|
|
2157
2691
|
sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4152
|
|
2158
2692
|
sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
|
|
2159
2693
|
|
|
2160
|
-
|
|
2161
2694
|
def column_to_left(sheet, columns=None):
|
|
2162
2695
|
if columns is None:
|
|
2163
2696
|
return
|
|
@@ -2177,7 +2710,6 @@ def column_to_left(sheet, columns=None):
|
|
|
2177
2710
|
sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4131
|
|
2178
2711
|
sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
|
|
2179
2712
|
|
|
2180
|
-
|
|
2181
2713
|
def beautify_title(sheet):
|
|
2182
2714
|
log('美化标题')
|
|
2183
2715
|
used_range_col = sheet.range('A1').expand('right')
|
|
@@ -2196,6 +2728,15 @@ def beautify_title(sheet):
|
|
|
2196
2728
|
sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
|
|
2197
2729
|
sheet.autofit()
|
|
2198
2730
|
|
|
2731
|
+
def set_body_style(sheet, row_start, row_end=None):
|
|
2732
|
+
if row_end is None:
|
|
2733
|
+
row_end = get_last_used_row(sheet)
|
|
2734
|
+
|
|
2735
|
+
range = sheet.range(f'{row_start}:{row_end}')
|
|
2736
|
+
# 设置字体名称
|
|
2737
|
+
range.font.name = 'Calibri'
|
|
2738
|
+
# 设置字体大小
|
|
2739
|
+
range.font.size = 11
|
|
2199
2740
|
|
|
2200
2741
|
def set_title_style(sheet, rows=2):
|
|
2201
2742
|
col = get_max_column_letter(sheet)
|
|
@@ -2219,7 +2760,6 @@ def set_title_style(sheet, rows=2):
|
|
|
2219
2760
|
|
|
2220
2761
|
sheet.autofit()
|
|
2221
2762
|
|
|
2222
|
-
|
|
2223
2763
|
def move_sheet_to_position(wb, sheet_name, position):
|
|
2224
2764
|
# 获取要移动的工作表
|
|
2225
2765
|
sheet = wb.sheets[sheet_name]
|
|
@@ -2233,13 +2773,11 @@ def move_sheet_to_position(wb, sheet_name, position):
|
|
|
2233
2773
|
# 保存工作簿
|
|
2234
2774
|
wb.save()
|
|
2235
2775
|
|
|
2236
|
-
|
|
2237
2776
|
# Excel 文件锁管理器
|
|
2238
2777
|
import threading
|
|
2239
2778
|
import time
|
|
2240
2779
|
from collections import defaultdict
|
|
2241
2780
|
|
|
2242
|
-
|
|
2243
2781
|
class ExcelFileLockManager:
|
|
2244
2782
|
"""Excel 文件锁管理器,用于管理不同 Excel 文件的并发访问"""
|
|
2245
2783
|
|
|
@@ -2272,7 +2810,7 @@ class ExcelFileLockManager:
|
|
|
2272
2810
|
# 记录等待请求
|
|
2273
2811
|
with self._lock:
|
|
2274
2812
|
self._waiting_queue[excel_path].append({
|
|
2275
|
-
'priority': priority,
|
|
2813
|
+
'priority' : priority,
|
|
2276
2814
|
'timestamp': time.time(),
|
|
2277
2815
|
'thread_id': threading.get_ident()
|
|
2278
2816
|
})
|
|
@@ -2345,11 +2883,9 @@ class ExcelFileLockManager:
|
|
|
2345
2883
|
# 比如检查文件最后访问时间等
|
|
2346
2884
|
pass
|
|
2347
2885
|
|
|
2348
|
-
|
|
2349
2886
|
# 全局 Excel 文件锁管理器实例
|
|
2350
2887
|
excel_lock_manager = ExcelFileLockManager()
|
|
2351
2888
|
|
|
2352
|
-
|
|
2353
2889
|
def open_excel_with_lock(excel_path, sheet_name='Sheet1', timeout=30):
|
|
2354
2890
|
"""
|
|
2355
2891
|
带锁的 Excel 打开函数,支持复用已打开的实例
|
|
@@ -2409,7 +2945,6 @@ def open_excel_with_lock(excel_path, sheet_name='Sheet1', timeout=30):
|
|
|
2409
2945
|
excel_lock_manager.release_excel_lock(excel_path)
|
|
2410
2946
|
return None, None, None
|
|
2411
2947
|
|
|
2412
|
-
|
|
2413
2948
|
def close_excel_with_lock(excel_path, app, wb, force_close=False):
|
|
2414
2949
|
"""
|
|
2415
2950
|
带锁的 Excel 关闭函数
|
|
@@ -2435,7 +2970,6 @@ def close_excel_with_lock(excel_path, app, wb, force_close=False):
|
|
|
2435
2970
|
finally:
|
|
2436
2971
|
excel_lock_manager.release_excel_lock(excel_path)
|
|
2437
2972
|
|
|
2438
|
-
|
|
2439
2973
|
def write_data_with_lock(excel_path, sheet_name, data, format_to_text_colunm=None):
|
|
2440
2974
|
"""
|
|
2441
2975
|
带锁的数据写入函数,复用 Excel 实例
|
|
@@ -2466,7 +3000,6 @@ def write_data_with_lock(excel_path, sheet_name, data, format_to_text_colunm=Non
|
|
|
2466
3000
|
log(f"写入数据失败: {e}")
|
|
2467
3001
|
return False
|
|
2468
3002
|
|
|
2469
|
-
|
|
2470
3003
|
def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs):
|
|
2471
3004
|
"""
|
|
2472
3005
|
带锁的 Excel 格式化函数
|
|
@@ -2493,21 +3026,80 @@ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs)
|
|
|
2493
3026
|
log(f"格式化失败: {e}")
|
|
2494
3027
|
return False
|
|
2495
3028
|
|
|
2496
|
-
|
|
3029
|
+
# 经过观察 fortmat时 传入函数需要为类函数且第二个参数必须是 sheet
|
|
2497
3030
|
def batch_excel_operations(excel_path, operations):
|
|
2498
3031
|
"""
|
|
2499
|
-
批量 Excel
|
|
2500
|
-
|
|
3032
|
+
批量 Excel 操作函数,自动分批处理,避免一次操作过多sheet导致Excel COM错误
|
|
3033
|
+
保持操作的原始顺序执行
|
|
3034
|
+
|
|
2501
3035
|
Args:
|
|
2502
3036
|
excel_path: Excel 文件路径
|
|
2503
3037
|
operations: 操作列表,每个操作是 (sheet_name, operation_type, data, format_func) 的元组
|
|
2504
|
-
operation_type: 'write'
|
|
2505
|
-
|
|
2506
|
-
format_func: 格式化函数(仅 format 操作需要)
|
|
2507
|
-
|
|
3038
|
+
operation_type: 'write', 'format', 'delete', 'move', 'active'
|
|
3039
|
+
|
|
2508
3040
|
Returns:
|
|
2509
3041
|
bool: 是否全部操作成功
|
|
2510
3042
|
"""
|
|
3043
|
+
if not operations:
|
|
3044
|
+
return True
|
|
3045
|
+
|
|
3046
|
+
# 批处理大小设置:每批最多处理8个操作
|
|
3047
|
+
MAX_OPERATIONS_PER_BATCH = 8
|
|
3048
|
+
|
|
3049
|
+
try:
|
|
3050
|
+
# 计算需要分几批
|
|
3051
|
+
total_batches = (len(operations) + MAX_OPERATIONS_PER_BATCH - 1) // MAX_OPERATIONS_PER_BATCH
|
|
3052
|
+
log(f"分{total_batches}批执行{len(operations)}个操作,每批最多{MAX_OPERATIONS_PER_BATCH}个,保持原始顺序")
|
|
3053
|
+
|
|
3054
|
+
# 按顺序分批执行
|
|
3055
|
+
for batch_idx in range(total_batches):
|
|
3056
|
+
start_idx = batch_idx * MAX_OPERATIONS_PER_BATCH
|
|
3057
|
+
end_idx = min(start_idx + MAX_OPERATIONS_PER_BATCH, len(operations))
|
|
3058
|
+
batch_operations = operations[start_idx:end_idx]
|
|
3059
|
+
|
|
3060
|
+
log(f"执行第{batch_idx + 1}/{total_batches}批操作({start_idx + 1}-{end_idx}),共{len(batch_operations)}个操作")
|
|
3061
|
+
|
|
3062
|
+
# 重试机制
|
|
3063
|
+
max_retries = 3
|
|
3064
|
+
for retry in range(max_retries):
|
|
3065
|
+
try:
|
|
3066
|
+
# 强制垃圾回收
|
|
3067
|
+
import gc
|
|
3068
|
+
gc.collect()
|
|
3069
|
+
|
|
3070
|
+
if _execute_operations_batch(excel_path, batch_operations):
|
|
3071
|
+
log(f"第{batch_idx + 1}批操作成功")
|
|
3072
|
+
break
|
|
3073
|
+
else:
|
|
3074
|
+
log(f"第{batch_idx + 1}批操作失败,重试 {retry + 1}/{max_retries}")
|
|
3075
|
+
if retry == max_retries - 1:
|
|
3076
|
+
log(f"第{batch_idx + 1}批操作最终失败")
|
|
3077
|
+
return False
|
|
3078
|
+
import time
|
|
3079
|
+
time.sleep(3)
|
|
3080
|
+
except Exception as e:
|
|
3081
|
+
log(f"第{batch_idx + 1}批操作异常: {e}")
|
|
3082
|
+
if retry == max_retries - 1:
|
|
3083
|
+
return False
|
|
3084
|
+
import time
|
|
3085
|
+
time.sleep(3)
|
|
3086
|
+
|
|
3087
|
+
# 批次间延迟
|
|
3088
|
+
if batch_idx < total_batches - 1:
|
|
3089
|
+
import time
|
|
3090
|
+
time.sleep(1)
|
|
3091
|
+
|
|
3092
|
+
log(f"所有批量操作完成: {excel_path}")
|
|
3093
|
+
return True
|
|
3094
|
+
|
|
3095
|
+
except Exception as e:
|
|
3096
|
+
log(f"批量操作过程异常: {e}")
|
|
3097
|
+
return False
|
|
3098
|
+
|
|
3099
|
+
def _execute_operations_batch(excel_path, operations):
|
|
3100
|
+
"""
|
|
3101
|
+
执行单个批次的操作
|
|
3102
|
+
"""
|
|
2511
3103
|
app, wb, sheet = open_excel_with_lock(excel_path)
|
|
2512
3104
|
if not app or not wb:
|
|
2513
3105
|
log(f"无法打开 Excel 文件: {excel_path}")
|
|
@@ -2515,46 +3107,101 @@ def batch_excel_operations(excel_path, operations):
|
|
|
2515
3107
|
|
|
2516
3108
|
try:
|
|
2517
3109
|
for sheet_name, operation_type, *args in operations:
|
|
2518
|
-
#
|
|
3110
|
+
# 根据操作类型决定是否需要获取或创建工作表
|
|
3111
|
+
sheet = None
|
|
3112
|
+
|
|
3113
|
+
# 删除操作不需要获取sheet对象
|
|
3114
|
+
if operation_type == 'delete':
|
|
3115
|
+
log(f'删除sheet: {sheet_name}')
|
|
3116
|
+
delete_sheet_if_exists(wb, sheet_name)
|
|
3117
|
+
continue
|
|
3118
|
+
|
|
3119
|
+
# 其他操作需要获取或创建工作表
|
|
2519
3120
|
if isinstance(sheet_name, str):
|
|
2520
3121
|
sheet_names = [s.name.strip().lower() for s in wb.sheets]
|
|
2521
3122
|
if sheet_name.strip().lower() in sheet_names:
|
|
2522
3123
|
sheet = wb.sheets[sheet_name]
|
|
2523
3124
|
else:
|
|
2524
|
-
sheet
|
|
3125
|
+
# 只有在需要操作sheet内容时才创建
|
|
3126
|
+
if operation_type in ['write', 'format']:
|
|
3127
|
+
sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
|
|
3128
|
+
else:
|
|
3129
|
+
log(f"警告: 操作 {operation_type} 需要的sheet {sheet_name} 不存在,跳过此操作")
|
|
3130
|
+
continue
|
|
2525
3131
|
else:
|
|
2526
3132
|
sheet = wb.sheets[sheet_name]
|
|
2527
3133
|
|
|
2528
|
-
sheet
|
|
3134
|
+
if sheet:
|
|
3135
|
+
sheet.activate()
|
|
2529
3136
|
|
|
2530
3137
|
if operation_type == 'write':
|
|
2531
|
-
data, format_to_text_colunm = args[:
|
|
3138
|
+
data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
|
|
2532
3139
|
# 清空工作表
|
|
2533
3140
|
sheet.clear()
|
|
2534
|
-
|
|
2535
|
-
|
|
3141
|
+
|
|
3142
|
+
# 先设置文本格式,再写入数据(确保格式生效)
|
|
3143
|
+
if format_to_text_colunm and format_to_text_colunm[0]:
|
|
3144
|
+
try:
|
|
3145
|
+
# 使用安全的预格式化方式
|
|
3146
|
+
pre_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
|
|
3147
|
+
except Exception as e:
|
|
3148
|
+
log(f"预格式化失败: {e},继续执行")
|
|
3149
|
+
|
|
2536
3150
|
# 写入数据
|
|
3151
|
+
log(f"批量操作,写入数据到: {sheet_name}")
|
|
2537
3152
|
sheet.range('A1').value = data
|
|
2538
|
-
|
|
3153
|
+
|
|
3154
|
+
# 写入后再次确认格式(双重保险)
|
|
3155
|
+
if format_to_text_colunm and format_to_text_colunm[0]:
|
|
3156
|
+
try:
|
|
3157
|
+
post_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
|
|
3158
|
+
except Exception as e:
|
|
3159
|
+
log(f"后格式化失败: {e}")
|
|
2539
3160
|
|
|
2540
3161
|
elif operation_type == 'format':
|
|
2541
3162
|
format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
|
|
2542
3163
|
# 执行格式化
|
|
2543
3164
|
format_func(sheet, *format_args)
|
|
2544
|
-
|
|
3165
|
+
|
|
3166
|
+
elif operation_type == 'move':
|
|
3167
|
+
log(f'移动sheet: {sheet_name}')
|
|
3168
|
+
position = args[0]
|
|
3169
|
+
move_sheet_to_position(wb, sheet_name, position)
|
|
3170
|
+
|
|
3171
|
+
elif operation_type == 'active':
|
|
3172
|
+
log(f'激活sheet: {sheet_name}')
|
|
3173
|
+
sheet.activate()
|
|
2545
3174
|
|
|
2546
3175
|
# 保存所有更改
|
|
2547
3176
|
wb.save()
|
|
2548
|
-
log(f"批量操作完成: {excel_path}")
|
|
2549
3177
|
return True
|
|
2550
3178
|
|
|
2551
3179
|
except Exception as e:
|
|
2552
|
-
log(f"
|
|
3180
|
+
log(f"单批次操作失败: {e}")
|
|
2553
3181
|
return False
|
|
2554
3182
|
finally:
|
|
2555
3183
|
# 释放锁但不关闭 Excel(保持复用)
|
|
2556
3184
|
excel_lock_manager.release_excel_lock(excel_path)
|
|
3185
|
+
close_excel_with_lock(excel_path, app, wb, True)
|
|
2557
3186
|
|
|
3187
|
+
def close_excel_file(file_path):
|
|
3188
|
+
file_path = os.path.abspath(file_path).lower()
|
|
3189
|
+
|
|
3190
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
3191
|
+
if proc.info['name'] and proc.info['name'].lower() in ['excel.exe', 'wps.exe']: # 只找 Excel
|
|
3192
|
+
try:
|
|
3193
|
+
for f in proc.open_files():
|
|
3194
|
+
if os.path.abspath(f.path).lower() == file_path:
|
|
3195
|
+
print(f"文件被 Excel 占用 (PID: {proc.pid}),正在关闭进程...")
|
|
3196
|
+
proc.terminate()
|
|
3197
|
+
proc.wait(timeout=3)
|
|
3198
|
+
print("已关闭。")
|
|
3199
|
+
return True
|
|
3200
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
3201
|
+
continue
|
|
3202
|
+
|
|
3203
|
+
print("文件没有被 Excel 占用。")
|
|
3204
|
+
return False
|
|
2558
3205
|
|
|
2559
3206
|
def force_close_excel_file(excel_path):
|
|
2560
3207
|
"""
|
|
@@ -2572,7 +3219,6 @@ def force_close_excel_file(excel_path):
|
|
|
2572
3219
|
except Exception as e:
|
|
2573
3220
|
log(f"强制关闭 Excel 文件失败: {e}")
|
|
2574
3221
|
|
|
2575
|
-
|
|
2576
3222
|
def wait_for_excel_available(excel_path, timeout=60, check_interval=1):
|
|
2577
3223
|
"""
|
|
2578
3224
|
等待 Excel 文件可用
|
|
@@ -2594,7 +3240,6 @@ def wait_for_excel_available(excel_path, timeout=60, check_interval=1):
|
|
|
2594
3240
|
log(f"等待 Excel 文件可用超时: {excel_path}")
|
|
2595
3241
|
return False
|
|
2596
3242
|
|
|
2597
|
-
|
|
2598
3243
|
def smart_excel_operation(excel_path, operation_func, priority=0, timeout=60, max_retries=3):
|
|
2599
3244
|
"""
|
|
2600
3245
|
智能 Excel 操作函数,支持优先级、重试和更好的错误处理
|
|
@@ -2661,7 +3306,6 @@ def smart_excel_operation(excel_path, operation_func, priority=0, timeout=60, ma
|
|
|
2661
3306
|
|
|
2662
3307
|
return False
|
|
2663
3308
|
|
|
2664
|
-
|
|
2665
3309
|
def batch_excel_operations_with_priority(excel_path, operations, priority=0, timeout=60):
|
|
2666
3310
|
"""
|
|
2667
3311
|
带优先级的批量 Excel 操作函数
|
|
@@ -2715,7 +3359,6 @@ def batch_excel_operations_with_priority(excel_path, operations, priority=0, tim
|
|
|
2715
3359
|
|
|
2716
3360
|
return smart_excel_operation(excel_path, batch_operation, priority, timeout)
|
|
2717
3361
|
|
|
2718
|
-
|
|
2719
3362
|
def wait_for_excel_available_with_priority(excel_path, timeout=60, check_interval=1, priority=0):
|
|
2720
3363
|
"""
|
|
2721
3364
|
等待 Excel 文件可用(带优先级)
|
|
@@ -2738,7 +3381,6 @@ def wait_for_excel_available_with_priority(excel_path, timeout=60, check_interva
|
|
|
2738
3381
|
log(f"等待 Excel 文件可用超时: {excel_path}")
|
|
2739
3382
|
return False
|
|
2740
3383
|
|
|
2741
|
-
|
|
2742
3384
|
def get_excel_status(excel_path):
|
|
2743
3385
|
"""
|
|
2744
3386
|
获取 Excel 文件状态信息
|
|
@@ -2750,8 +3392,11 @@ def get_excel_status(excel_path):
|
|
|
2750
3392
|
dict: 状态信息
|
|
2751
3393
|
"""
|
|
2752
3394
|
return {
|
|
2753
|
-
'is_open': excel_lock_manager.is_excel_open(excel_path),
|
|
2754
|
-
'waiting_count': excel_lock_manager.get_waiting_count(excel_path),
|
|
3395
|
+
'is_open' : excel_lock_manager.is_excel_open(excel_path),
|
|
3396
|
+
'waiting_count' : excel_lock_manager.get_waiting_count(excel_path),
|
|
2755
3397
|
'operation_count': excel_lock_manager.get_operation_count(excel_path),
|
|
2756
|
-
'has_lock': excel_lock_manager.get_file_lock(excel_path).locked()
|
|
3398
|
+
'has_lock' : excel_lock_manager.get_file_lock(excel_path).locked()
|
|
2757
3399
|
}
|
|
3400
|
+
|
|
3401
|
+
def get_last_used_row(sheet):
|
|
3402
|
+
return sheet.used_range.last_cell.row
|