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/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
- def sort_by_column(data, col_index, start_row=2, reverse=True):
95
- if not data or start_row >= len(data):
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[:start_row]
100
- new_data_sorted = data[start_row:]
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
- data = sheet.range(f'{col_letter}1').expand('table').value
141
- # col_index = column_name_to_index(col_letter)
142
- start_row = 1
143
- merge_ranges = [] # 用来存储所有待合并的单元格范围
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
- other_columns_index = {}
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
- other_columns_index[col_name] = column_name_to_index(col_name)
244
+ all_columns.append(col_name)
151
245
 
152
- for row in range(2, len(data) + 1):
153
- log(f'查找 {row}/{len(data)}')
154
- if data[row - 1][0] != data[row - 2][0]:
155
- if row - start_row > 1:
156
- # 将合并范围加入列表
157
- merge_ranges.append((col_letter, start_row, row - 1))
158
- for col_name, col_index in other_columns_index.items():
159
- merge_ranges.append((col_name, start_row, row - 1))
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
- if len(data) - start_row > 1:
163
- merge_ranges.append((col_letter, start_row, len(data)))
164
- for col_name, col_index in other_columns_index.items():
165
- merge_ranges.append((col_name, start_row, len(data)))
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
- # 预计算所有单元格的合并区域信息 (优化点1)
717
+ # 阶段1:调整所有单元格尺寸 (优化点1)
595
718
  area_map = {}
596
- for row in range(2, last_row + 1):
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
- if cell_height < img_width:
620
- cell_range.row_height = max(150 / 8, img_width / cell_range.rows.count)
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
- top = cell_range.top + (cell_range.height - img_width) / 2
624
- left = cell_range.left + (cell_range.width - img_width) / 2
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(2, last_row + 1):
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
- with sync_playwright() as p:
698
- browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
699
- context = browser.new_context()
700
- page = context.new_page()
701
- # 直接通过Playwright下载图片
702
- response = page.request.get(image_url)
703
- with open(save_name, 'wb') as f:
704
- f.write(response.body()) # 将下载的内容保存为文件
705
- log(f"图片已通过chrome下载并保存为:{save_name}")
706
- # 关闭浏览器
707
- browser.close()
708
- return save_name
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
- def colorize_by_field(app, wb, sheet, field):
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
- for col_name in col_list:
1989
- col_letter = find_column_by_data(sheet, 1, col_name)
1990
- sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
1991
- sheet.range(f'{col_letter}{row}').api.Font.Color = 255
1992
- sheet.range(f'{col_letter}:{col_letter}').autofit()
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 操作函数,一次性打开 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' 'format'
2505
- data: 写入的数据(仅 write 操作需要)
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 = wb.sheets.add(sheet_name, after=wb.sheets[-1])
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.activate()
3134
+ if sheet:
3135
+ sheet.activate()
2529
3136
 
2530
3137
  if operation_type == 'write':
2531
- data, format_to_text_colunm = args[:2]
3138
+ data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
2532
3139
  # 清空工作表
2533
3140
  sheet.clear()
2534
- # 格式化文本列
2535
- format_to_text_v2(sheet, format_to_text_colunm)
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
- log(f"批量操作:写入数据到 {sheet_name}")
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
- log(f"批量操作:格式化工作表 {sheet_name}")
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"批量操作失败: {e}")
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