qrpa 1.1.79__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.

Potentially problematic release.


This version of qrpa might be problematic. Click here for more details.

qrpa/fun_excel.py ADDED
@@ -0,0 +1,3470 @@
1
+ from pathlib import Path
2
+ import xlwings as xw
3
+ import requests
4
+ from PIL import Image # 需要安装 Pillow 库:pip install pillow
5
+ import os
6
+ import json
7
+ from urllib.parse import urlparse
8
+ import time
9
+ import random
10
+ import traceback
11
+ import concurrent.futures
12
+ from collections import defaultdict
13
+ import threading
14
+ from playwright.sync_api import sync_playwright
15
+ import psutil
16
+
17
+ import os, sys
18
+ from pathlib import Path
19
+
20
+ from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
21
+
22
+ excel_color_index = {
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
39
+ "浅灰色(12.5%)": 16, # #C0C0C0
40
+ # 17-19:系统保留(通常不可用)
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
77
+ }
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
121
+
122
+
123
+ def aggregate_by_column_v2(data, group_by_col_name, as_str_columns=None, data_start_row=2):
124
+ """
125
+ 根据指定列名对二维表数据聚合:
126
+ - 数字列求和(除非指定为按字符串处理)
127
+ - 字符串列用换行符拼接
128
+
129
+ :param data: 二维列表,第一行为表头
130
+ :param group_by_col_name: 要聚合的列名,如 "店长"
131
+ :param as_str_columns: 可选,指定即使是数字也按字符串拼接的列名列表
132
+ :param data_start_row: 从哪一行开始是有效数据,默认2表示跳过表头和汇总行
133
+ :return: 聚合后的二维列表
134
+ """
135
+ if as_str_columns is None:
136
+ as_str_columns = []
137
+
138
+ headers = data[0]
139
+ group_index = headers.index(group_by_col_name)
140
+ grouped = defaultdict(list)
141
+
142
+ # 按 group_by 列聚合行(跳过前 data_start_row 行)
143
+ for row in data[data_start_row:]:
144
+ key = str(row[group_index]).strip()
145
+ grouped[key].append(row)
146
+
147
+ result = [headers]
148
+
149
+ for key, rows in grouped.items():
150
+ agg_row = []
151
+ for col_idx in range(len(headers)):
152
+ col_name = headers[col_idx]
153
+ col_values = [r[col_idx] for r in rows]
154
+
155
+ if col_idx == group_index:
156
+ agg_value = key
157
+ elif col_name in as_str_columns:
158
+ strings = [str(v).strip() for v in col_values if str(v).strip()]
159
+ agg_value = '\n'.join(strings)
160
+ else:
161
+ try:
162
+ nums = [float(v) for v in col_values if
163
+ isinstance(v, (int, float)) or (isinstance(v, str) and v.strip() != '')]
164
+ agg_value = sum(nums)
165
+ except ValueError:
166
+ strings = [str(v).strip() for v in col_values if str(v).strip()]
167
+ agg_value = '\n'.join(strings)
168
+
169
+ agg_row.append(agg_value)
170
+ result.append(agg_row)
171
+
172
+ return result
173
+
174
+
175
+ def set_cell_prefix_red(cell, n, color_name):
176
+ """
177
+ 将指定 Excel 单元格内容的前 n 个字符设置为红色。
178
+ """
179
+ text = str(cell.value)
180
+
181
+ if not text or n <= 0:
182
+ return
183
+
184
+ n = min(n, len(text)) # 避免超出范围
185
+
186
+ try:
187
+ # 设置前n个字符为红色
188
+ cell.api.Characters(1, n).Font.ColorIndex = excel_color_index[color_name]
189
+ except Exception as e:
190
+ print(f"设置字体颜色失败: {e}")
191
+
192
+ def wrap_column(sheet, columns=None, WrapText=True):
193
+ if columns is None:
194
+ return
195
+ used_range_col = sheet.range('A1').expand('right')
196
+ for j, cell in enumerate(used_range_col):
197
+ col = j + 1
198
+ col_name = index_to_column_name(col)
199
+ col_val = sheet.range(f'{col_name}1').value
200
+ if col_val is None:
201
+ continue
202
+ for c in columns:
203
+ if c in col_val:
204
+ log(f'设置[{c}] 换行 {WrapText}')
205
+ sheet.range(f'{col_name}:{col_name}').api.WrapText = WrapText
206
+
207
+ def sort_by_column_excel(sheet, sort_col: str, has_header=True, order="desc"):
208
+ """
209
+ 对整个表格按照某一列排序
210
+
211
+ :param sheet: xlwings 的 sheet 对象
212
+ :param sort_col: 排序依据的列(如 'D')
213
+ :param has_header: 是否有表头(默认 True)
214
+ :param order: 'asc' 升序,'desc' 降序
215
+ """
216
+ # 找到表格的最后一行和列
217
+ last_cell = sheet.used_range.last_cell
218
+ rng = sheet.range((1, 1), (last_cell.row, last_cell.column))
219
+
220
+ # 排序依据列
221
+ col_index = ord(sort_col.upper()) - ord('A') + 1
222
+ key = sheet.range((2 if has_header else 1, col_index)).api
223
+
224
+ # 排序顺序
225
+ order_val = 1 if order == "asc" else 2
226
+
227
+ # 调用 Excel 的 Sort 方法
228
+ rng.api.Sort(
229
+ Key1=key,
230
+ Order1=order_val,
231
+ Orientation=1,
232
+ Header=1 if has_header else 0
233
+ )
234
+
235
+ def sort_by_column(data, col_index, header_rows=2, reverse=True):
236
+ if not data or header_rows >= len(data):
237
+ return data
238
+
239
+ try:
240
+ header = data[:header_rows]
241
+ new_data_sorted = data[header_rows:]
242
+
243
+ def get_key(row):
244
+ value = row[col_index]
245
+ if isinstance(value, (int, float)): # 已经是数字
246
+ return (0, value) # 用元组排序,数字优先
247
+ try:
248
+ return (0, float(value)) # 尝试转数字
249
+ except (ValueError, TypeError):
250
+ return (1, str(value)) # 转不了就按字符串排
251
+
252
+ new_data_sorted.sort(key=get_key, reverse=reverse)
253
+ return header + new_data_sorted
254
+ except IndexError:
255
+ print(f"Error: Column index {col_index} out of range")
256
+ return data
257
+
258
+ def column_exists(sheet, column_name, header_row=1):
259
+ """
260
+ 检查工作表中是否存在指定列名
261
+ :param sheet: xlwings Sheet 对象
262
+ :param column_name: 要查找的列名
263
+ :param header_row: 表头所在行号,默认为1
264
+ :return: 如果存在返回True,否则返回False
265
+ """
266
+ # 获取表头行所有值
267
+ header_values = sheet.range((header_row, 1), (header_row, sheet.used_range.last_cell.column)).value
268
+
269
+ return column_name in header_values
270
+
271
+ def merge_by_column_v2(sheet, column_name, other_columns):
272
+ log('正在处理合并单元格')
273
+ # 最好放到 open_excel 后面,不然容易出错
274
+ col_letter = find_column_by_data(sheet, 1, column_name)
275
+ if col_letter is None:
276
+ log(f'未找到合并的列名: {column_name}')
277
+ return
278
+
279
+ # 更安全的数据获取方式,确保获取完整的数据范围
280
+ last_row = get_last_row(sheet, col_letter)
281
+ data = sheet.range(f'{col_letter}1:{col_letter}{last_row}').value
282
+
283
+ # 确保data是列表格式
284
+ if not isinstance(data, list):
285
+ data = [data]
286
+
287
+ log(f'数据范围: {col_letter}1:{col_letter}{last_row}, 数据长度: {len(data)}')
288
+
289
+ start_row = 2 # 从第2行开始,跳过表头
290
+ merge_row_ranges = [] # 用来存储需要合并的行范围 (start_row, end_row)
291
+
292
+ # 获取所有需要合并的列
293
+ all_columns = [col_letter] # 主列
294
+ for col in other_columns:
295
+ col_name = find_column_by_data(sheet, 1, col)
296
+ if col_name:
297
+ all_columns.append(col_name)
298
+
299
+ log(f'需要合并的列: {all_columns}')
300
+
301
+ # 遍历数据行,从第3行开始比较(因为第1行是表头,第2行是第一个数据行)
302
+ for row in range(3, len(data) + 1):
303
+ log(f'查找 {row}/{len(data)}, 当前值: {data[row - 1] if row - 1 < len(data) else "超出范围"}, 前一个值: {data[row - 2] if row - 2 < len(data) else "超出范围"}')
304
+
305
+ # 检查值是否发生变化
306
+ if row <= len(data) and data[row - 1] != data[row - 2]:
307
+ # 值发生变化,处理前一组
308
+ end_row = row - 1
309
+ log(f'添加合并范围: {start_row} 到 {end_row}')
310
+ merge_row_ranges.append((start_row, end_row))
311
+ start_row = row
312
+
313
+ # 处理最后一组数据(循环结束后,start_row 到数据末尾)
314
+ if start_row <= len(data):
315
+ end_row = len(data)
316
+ log(f'处理最后一组: {start_row} 到 {end_row}')
317
+ merge_row_ranges.append((start_row, end_row))
318
+
319
+ log(f'行合并范围: {merge_row_ranges}')
320
+
321
+ # 对每个行范围,在所有指定列中执行合并
322
+ for start_row, end_row in merge_row_ranges:
323
+ if start_row < end_row: # 只有当开始行小于结束行时才合并(多行)
324
+ for col_name in all_columns:
325
+ try:
326
+ cell_range = sheet.range(f'{col_name}{start_row}:{col_name}{end_row}')
327
+
328
+ # 验证:检查范围内的值是否都相同
329
+ values = cell_range.value
330
+ if not isinstance(values, list):
331
+ values = [values]
332
+
333
+ # 检查是否所有值都相同(忽略 None)
334
+ non_none_values = [v for v in values if v is not None]
335
+ if non_none_values and len(set(non_none_values)) > 1:
336
+ log(f'警告:{col_name}{start_row}:{col_name}{end_row} 包含不同的值,跳过合并: {set(non_none_values)}')
337
+ continue
338
+
339
+ log(f'处理 {col_name}{start_row}:{col_name}{end_row} merge')
340
+
341
+ # 保存第一个单元格的值
342
+ first_cell_value = sheet.range(f'{col_name}{start_row}').value
343
+
344
+ # 先清空所有单元格(避免多行文本导致的合并问题)
345
+ cell_range.value = None
346
+
347
+ # 执行合并
348
+ cell_range.merge()
349
+
350
+ # 恢复第一个单元格的值
351
+ cell_range.value = first_cell_value
352
+
353
+ except Exception as e:
354
+ log(f'合并失败 {col_name}{start_row}:{col_name}{end_row}: {e}')
355
+ # 继续处理其他列
356
+ continue
357
+ elif start_row == end_row:
358
+ log(f'单行数据无需合并: {start_row} 到 {end_row}')
359
+ else:
360
+ log(f'跳过无效合并范围: {start_row} 到 {end_row}')
361
+
362
+ def merge_by_column(sheet, column_name, other_columns):
363
+ log('正在处理合并单元格')
364
+ # 最好放到 open_excel 后面,不然容易出错
365
+ data = sheet.range('A1').expand('table').value
366
+ col_letter = find_column_by_data(sheet, 1, column_name)
367
+ if col_letter is None:
368
+ log(f'未找到合并的列名: {column_name}')
369
+ return
370
+ col_index = column_name_to_index(col_letter)
371
+ start_row = 1
372
+ for row in range(2, len(data) + 1):
373
+ log(f'{row}/{len(data)}')
374
+ if data[row - 1][col_index] != data[row - 2][col_index]:
375
+ if row - start_row > 1:
376
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{row - 1}').merge()
377
+ for col in other_columns:
378
+ col_name = find_column_by_data(sheet, 1, col)
379
+ if col_name is not None:
380
+ sheet.range(f'{col_name}{start_row}:{col_name}{row - 1}').merge()
381
+ start_row = row
382
+
383
+ if len(data) - start_row > 1:
384
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{len(data)}').merge()
385
+ for col in other_columns:
386
+ col_name = find_column_by_data(sheet, 1, col)
387
+ if col_name is not None:
388
+ sheet.range(f'{col_name}{start_row}:{col_name}{len(data)}').merge()
389
+
390
+ def merge_column_v2(sheet, columns):
391
+ if columns is None:
392
+ return
393
+
394
+ # 缓存所有列的字母
395
+ col_letters = {col: find_column_by_data(sheet, 1, col) for col in columns}
396
+ merge_ranges = [] # 用来存储所有待合并的单元格范围
397
+
398
+ for c, col_letter in col_letters.items():
399
+ if col_letter is None:
400
+ continue
401
+
402
+ data = sheet.range(f'{col_letter}1').expand('table').value
403
+ start_row = 1
404
+
405
+ for row in range(2, len(data) + 1):
406
+ log(f'查找 {row}/{len(data)}') # 如果数据量非常大,这里的日志会影响性能,可以考虑优化
407
+ if data[row - 1][0] != data[row - 2][0]:
408
+ if row - start_row > 1:
409
+ merge_ranges.append((col_letter, start_row, row - 1))
410
+ start_row = row
411
+
412
+ if len(data) - start_row > 1:
413
+ merge_ranges.append((col_letter, start_row, len(data)))
414
+
415
+ # 批量合并单元格
416
+ for col_letter, start, end in merge_ranges:
417
+ log(f'处理 {col_letter}{start}:{col_letter}{end} merge')
418
+ sheet.range(f'{col_letter}{start}:{col_letter}{end}').merge()
419
+
420
+ # 按列相同值合并
421
+ def merge_column(sheet, columns):
422
+ # 最后放到 open_excel 后面,不然容易出错
423
+ if columns is None:
424
+ return
425
+ for c in columns:
426
+ col_letter = find_column_by_data(sheet, 1, c)
427
+ if col_letter is None:
428
+ continue
429
+ data = sheet.range(f'{col_letter}1').expand('table').value
430
+ # col_index = column_name_to_index(col_letter)
431
+ col_index = 0
432
+ start_row = 1
433
+ for row in range(2, len(data) + 1):
434
+ if data[row - 1][col_index] != data[row - 2][col_index]:
435
+ if row - start_row > 1:
436
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{row - 1}').merge()
437
+ start_row = row
438
+
439
+ if len(data) - start_row > 1:
440
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{len(data)}').merge()
441
+
442
+ def remove_excel_columns(sheet, columns):
443
+ # 获取第一行(标题行)的所有值
444
+ header_row = sheet.range('1:1').value
445
+
446
+ # 获取要删除的列的索引(从1开始)
447
+ columns_to_remove = []
448
+ for i, header in enumerate(header_row, start=1):
449
+ if header in columns:
450
+ columns_to_remove.append(i)
451
+
452
+ # 如果没有找到要删除的列
453
+ if not columns_to_remove:
454
+ log("警告: 未找到任何匹配的列")
455
+ return False
456
+
457
+ # 按从右到左的顺序删除列(避免索引变化问题)
458
+ for col_idx in sorted(columns_to_remove, reverse=True):
459
+ col_letter = xw.utils.col_name(col_idx)
460
+ sheet.range(f'{col_letter}:{col_letter}').delete()
461
+
462
+ print(f"成功移除列: {columns_to_remove}")
463
+ return True
464
+
465
+ def delete_sheet_if_exists(wb, sheet_name):
466
+ """
467
+ 如果工作簿中存在指定名称的工作表,则将其删除。
468
+
469
+ 参数:
470
+ wb : xw.Book
471
+ xlwings 的工作簿对象。
472
+ sheet_name : str
473
+ 要检查并删除的工作表名称。
474
+ """
475
+ sheet_names = [s.name for s in wb.sheets]
476
+ if sheet_name in sheet_names:
477
+ wb.sheets[sheet_name].delete()
478
+ wb.save()
479
+ print(f"已删除 Sheet: {sheet_name}")
480
+ else:
481
+ print(f"Sheet 不存在: {sheet_name}")
482
+
483
+ # 水平对齐:
484
+ # -4108:居中
485
+ # -4131:左对齐
486
+ # -4152:右对齐
487
+ # 垂直对齐:
488
+ # -4108:居中
489
+ # -4160:顶部对齐
490
+ # -4107:底部对齐
491
+ def index_to_column_name(index):
492
+ """
493
+ 将列索引转换为Excel列名。
494
+ 例如:1 -> 'A', 2 -> 'B', 26 -> 'Z', 27 -> 'AA'
495
+ """
496
+ column_name = ''
497
+ while index > 0:
498
+ index -= 1
499
+ remainder = index % 26
500
+ column_name = chr(65 + remainder) + column_name
501
+ index = index // 26
502
+ return column_name
503
+
504
+ # # 示例:将列索引转换为列名
505
+ # log(index_to_column_name(1)) # 输出: 'A'
506
+ # log(index_to_column_name(26)) # 输出: 'Z'
507
+ # log(index_to_column_name(27)) # 输出: 'AA'
508
+ # log(index_to_column_name(52)) # 输出: 'AZ'
509
+
510
+ def column_name_to_index(column_name):
511
+ """
512
+ 将Excel列名转换为列索引。
513
+ 例如:'A' -> 1, 'B' -> 2, 'Z' -> 26, 'AA' -> 27
514
+ 例如:'A' -> 0, 'B' -> 1, 'Z' -> 25, 'AA' -> 26
515
+ """
516
+ index = 0
517
+ for char in column_name:
518
+ index = index * 26 + (ord(char.upper()) - 64)
519
+ return index - 1
520
+
521
+ # # 示例:将列名转换为列索引
522
+ # log(column_name_to_index('A')) # 输出: 1
523
+ # log(column_name_to_index('Z')) # 输出: 26
524
+ # log(column_name_to_index('AA')) # 输出: 27
525
+ # log(column_name_to_index('AZ')) # 输出: 52
526
+
527
+ def find_row_by_data(sheet, column, target_value):
528
+ """
529
+ 查找指定数据在某一列中第一次出现的行号。
530
+
531
+ :param sheet: xlwings 的 Sheet 对象。
532
+ :param column: 列名(如 'A', 'B', 'C')。
533
+ :param target_value: 要查找的数据。
534
+ :return: 数据所在的行号(从1开始),如果未找到返回 None。
535
+ """
536
+ # 获取指定列的所有数据
537
+ column_data = sheet.range(f'{column}1').expand('down').value
538
+
539
+ # 遍历数据,查找目标值
540
+ for i, value in enumerate(column_data, start=1):
541
+ if value == target_value:
542
+ return i
543
+
544
+ # 如果未找到,返回 None
545
+ return None
546
+
547
+ def find_column_by_data(sheet, row, target_value):
548
+ """
549
+ 查找指定数据在某一行中第一次出现的列名,包括隐藏的列。
550
+
551
+ :param sheet: xlwings 的 Sheet 对象。
552
+ :param row: 行号(如 1, 2, 3)。
553
+ :param target_value: 要查找的数据。
554
+ :return: 数据所在的列名(如 'A', 'B', 'C'),如果未找到返回 None。
555
+ """
556
+ last_col = sheet.used_range.last_cell.column # 获取最后一列索引
557
+
558
+ for col in range(1, last_col + 1): # 遍历所有列
559
+ cell = sheet.cells(row, col)
560
+
561
+ # 检查目标值是否匹配
562
+ if cell.value == target_value:
563
+ return xw.utils.col_name(col) # 返回列名
564
+
565
+ return None # 未找到返回 None
566
+
567
+ def find_column_by_data_old(sheet, row, target_value):
568
+ """
569
+ 查找指定数据在某一行中第一次出现的列名。
570
+
571
+ :param sheet: xlwings 的 Sheet 对象。
572
+ :param row: 行号(如 1, 2, 3)。
573
+ :param target_value: 要查找的数据。
574
+ :return: 数据所在的列名(如 'A', 'B', 'C'),如果未找到返回 None。
575
+ """
576
+ # 获取指定行的所有数据
577
+ row_data = sheet.range(f'A{row}').expand('right').value
578
+
579
+ # 遍历数据,查找目标值
580
+ for i, value in enumerate(row_data):
581
+ if value == target_value:
582
+ # 将列索引转换为列名
583
+ return xw.utils.col_name(i + 1)
584
+
585
+ # 如果未找到,返回 None
586
+ return None
587
+
588
+ def set_print_area(sheet, print_range, pdf_path=None, fit_to_width=True, landscape=False):
589
+ """
590
+ 设置指定sheet的打印区域和打印布局为适合A4宽度打印。
591
+
592
+ :param sheet: xlwings 的 Sheet 对象
593
+ :param print_range: 要设置为打印区域的字符串范围,比如 "A1:G50"
594
+ :param fit_to_width: 是否缩放以适应A4纸宽度
595
+ :param landscape: 是否横向打印(默认纵向)
596
+ """
597
+ # 设置打印区域
598
+ sheet.api.PageSetup.PrintArea = print_range
599
+
600
+ # 取消打印标题行/列
601
+ sheet.api.PageSetup.PrintHeadings = False
602
+
603
+ # 取消打印网格线
604
+ sheet.api.PageSetup.PrintGridlines = False
605
+
606
+ # 打印方向(横向或纵向)
607
+ sheet.api.PageSetup.Orientation = 2 if landscape else 1 # 2: Landscape, 1: Portrait
608
+
609
+ # 设置纸张大小为 A4
610
+ sheet.api.PageSetup.PaperSize = 9 # 9: xlPaperA4
611
+
612
+ # 设置页边距
613
+ sheet.api.PageSetup.LeftMargin = 20 # 上边距
614
+ sheet.api.PageSetup.RightMargin = 20 # 上边距
615
+ sheet.api.PageSetup.TopMargin = 20 # 上边距
616
+ sheet.api.PageSetup.BottomMargin = 20 # 上边距
617
+
618
+ if fit_to_width:
619
+ # 适应一页宽度,多页高度
620
+ sheet.api.PageSetup.Zoom = False
621
+ sheet.api.PageSetup.FitToPagesWide = 1
622
+ sheet.api.PageSetup.FitToPagesTall = False # 高度不限制,可以分页
623
+ else:
624
+ # 使用默认缩放(不建议用于A4布局控制)
625
+ sheet.api.PageSetup.Zoom = 100
626
+
627
+ # 可选:居中打印
628
+ sheet.api.PageSetup.CenterHorizontally = True
629
+ sheet.api.PageSetup.CenterVertically = False
630
+
631
+ # 导出打印区域为PDF
632
+ if pdf_path is not None:
633
+ sheet.to_pdf(path=pdf_path)
634
+ log(f"PDF已成功生成:{pdf_path}")
635
+
636
+ def minimize(app):
637
+ # 让 Excel 窗口最小化
638
+ app.api.WindowState = -4140 # -4140 对应 Excel 中的 xlMinimized 常量
639
+
640
+ def insert_fixed_scale_image_v2(sheet, cell, image_path):
641
+ """
642
+ 将图片插入到指定单元格中,自动缩放以适应单元格尺寸,但保持宽高比例不变。
643
+ - sheet: xlwings 工作表对象
644
+ - cell: 单元格地址,如 'C3'
645
+ - image_path: 图片路径
646
+ """
647
+ if not image_path:
648
+ return None
649
+
650
+ target_range = sheet.range(cell)
651
+ if target_range.merge_cells:
652
+ target_range = target_range.merge_area
653
+
654
+ cell_value = target_range.value
655
+
656
+ try:
657
+ # 获取单元格的宽高(单位是 points)
658
+ cell_width = target_range.width
659
+ cell_height = target_range.height
660
+
661
+ # 获取图片实际尺寸(像素),并计算比例
662
+ with Image.open(image_path) as img:
663
+ img_width_px, img_height_px = img.size
664
+
665
+ # 计算图片的实际宽高比(防止变形)
666
+ img_ratio = img_width_px / img_height_px
667
+ cell_ratio = cell_width / cell_height
668
+
669
+ # 设置缩放因子,留出空隙(例如,0.9 表示图片尺寸为原来的 90%)
670
+ padding_factor = 0.9
671
+
672
+ # 计算缩放倍数(确保图片不超过单元格大小)
673
+ if img_ratio > cell_ratio:
674
+ # 宽度限制
675
+ scale = cell_width / img_width_px * padding_factor
676
+ img_width_resized = cell_width * padding_factor
677
+ img_height_resized = img_height_px * scale
678
+ else:
679
+ # 高度限制
680
+ scale = cell_height / img_height_px * padding_factor
681
+ img_width_resized = img_width_px * scale
682
+ img_height_resized = cell_height * padding_factor
683
+
684
+ # 插入图片
685
+ pic = sheet.pictures.add(image_path, left=target_range.left, top=target_range.top, width=img_width_resized, height=img_height_resized)
686
+
687
+ # 居中对齐
688
+ pic.left = target_range.left + (target_range.width - pic.width) / 2
689
+ pic.top = target_range.top + (target_range.height - pic.height) / 2
690
+
691
+ # 清除单元格文字
692
+ target_range.value = None
693
+
694
+ return pic
695
+
696
+ except Exception as e:
697
+ target_range.value = cell_value
698
+ send_exception()
699
+
700
+ return None
701
+
702
+ def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
703
+ """
704
+ 按固定比例放大图片并插入到单元格
705
+ insert_fixed_scale_image(sheet, 'C1', img_path, 1.5)
706
+ 参数:
707
+ - sheet: xlwings工作表对象
708
+ - cell: 目标单元格地址
709
+ - image_path: 图片文件路径
710
+ - scale: 缩放倍数(2.0表示放大两倍)
711
+ """
712
+ if not image_path:
713
+ return None
714
+
715
+ # 获取目标单元格范围
716
+ target_range = sheet.range(cell)
717
+
718
+ if target_range.merge_cells:
719
+ target_range = target_range.merge_area
720
+
721
+ cell_value = target_range.value
722
+ try:
723
+ # 插入图片并缩放
724
+ pic = sheet.pictures.add(image_path, left=target_range.left, top=target_range.top, scale=scale)
725
+
726
+ # 调整位置使其居中(可选)
727
+ pic.left = target_range.left + (target_range.width - pic.width) / 2
728
+ pic.top = target_range.top + (target_range.height - pic.height) / 2
729
+
730
+ target_range.value = None
731
+
732
+ return pic
733
+ except Exception as e:
734
+ target_range.value = cell_value
735
+ send_exception()
736
+
737
+ return None
738
+
739
+ 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):
740
+ if not columns:
741
+ return
742
+
743
+ minimize(sheet.book.app)
744
+
745
+ # 清空所有图片
746
+ clear_all_pictures(sheet)
747
+
748
+ # 获取每列图片列的列号,并设置列宽
749
+ col_letter_map = {}
750
+ for img_col in columns:
751
+ col_letter = find_column_by_data(sheet, 1, img_col)
752
+ if col_letter is not None:
753
+ col_letter_map[img_col] = col_letter
754
+ # 下载图片
755
+ log(f'批量下载图片: {img_col} => {col_letter}')
756
+ last_row = get_last_row(sheet, col_letter)
757
+ images = sheet.range(f'{col_letter}2:{col_letter}{last_row}').value
758
+ images = images if isinstance(images, list) else [images]
759
+ download_images_concurrently(images, platform)
760
+
761
+ # 任意一个列作为主参考列,用来确定行数
762
+ if not col_letter_map:
763
+ return
764
+
765
+ ref_col_letter = next(iter(col_letter_map.values()))
766
+ last_row = get_last_row(sheet, ref_col_letter)
767
+
768
+ img_key_letter = find_column_by_data(sheet, 1, img_save_key)
769
+
770
+ # 阶段1:调整所有单元格尺寸 (优化点1)
771
+ area_map = {}
772
+ for row in range(start_row, last_row + 1):
773
+ log(f'计算 {row}/{last_row}')
774
+ for col_letter in col_letter_map.values():
775
+ cell_ref = f'{col_letter}{row}'
776
+ cell_range = sheet.range(cell_ref)
777
+ cell_address = cell_range.address
778
+
779
+ if cell_range.merge_cells:
780
+ cell_range = cell_range.merge_area
781
+ cell_address = cell_range.address
782
+
783
+ if cell_address not in area_map:
784
+ # 调整列宽
785
+ cell_range.column_width = img_width / 6.1
786
+
787
+ # 调整行高
788
+ if cell_range.height < img_width:
789
+ if cell_range.merge_cells:
790
+ # 合并单元格:为每一行设置高度
791
+ rows_count = cell_range.rows.count
792
+ per_row_height = img_width / rows_count
793
+ for single_row in cell_range.rows:
794
+ single_row.row_height = max(per_row_height, 150 / 8)
795
+ else:
796
+ cell_range.row_height = max(img_width, 150 / 8)
797
+
798
+ if cell_height_with_img:
799
+ if cell_range.merge_cells:
800
+ rows_count = cell_range.rows.count
801
+ for single_row in cell_range.rows:
802
+ single_row.row_height = img_width / rows_count
803
+ else:
804
+ cell_range.row_height = img_width
805
+
806
+ # 重新读取调整后的宽高
807
+ if cell_range.merge_cells:
808
+ cell_range = sheet.range(cell_ref).merge_area
809
+ else:
810
+ cell_range = sheet.range(cell_ref)
811
+
812
+ # 计算居中位置
813
+ actual_width = cell_range.width
814
+ actual_height = cell_range.height
815
+ actual_img_size = img_width - 4
816
+ top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
817
+ left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
818
+
819
+ area_map[cell_address] = {
820
+ 'top' : top,
821
+ 'left' : left,
822
+ 'width' : img_width,
823
+ 'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
824
+ }
825
+
826
+ # 处理图片插入 (优化点2)
827
+ for row in range(start_row, last_row + 1):
828
+ for img_col_name, col_letter in col_letter_map.items():
829
+ cell_ref = f'{col_letter}{row}'
830
+ cell_range = sheet.range(cell_ref)
831
+ cell_address = cell_range.address
832
+
833
+ # 检查合并单元格 (使用预计算的信息)
834
+ if cell_range.merge_cells and cell_address in area_map[cell_range.merge_area.address]['cell_list'][1:]:
835
+ continue
836
+
837
+ if cell_range.merge_cells:
838
+ cell_range = cell_range.merge_area
839
+ cell_address = cell_range.address
840
+
841
+ # 使用预计算的位置信息
842
+ top = area_map[cell_address]['top']
843
+ left = area_map[cell_address]['left']
844
+ width = area_map[cell_address]['width']
845
+
846
+ # 获取图片链接
847
+ if cell_range.merge_cells:
848
+ img_url = cell_range.value[0]
849
+ else:
850
+ img_url = cell_range.value
851
+
852
+ if img_url:
853
+ if img_key_letter is not None:
854
+ image_dir = Path(f'{os.getenv('auto_dir')}/image') / dir_name
855
+ extension = Path(img_url).suffix
856
+ filename = str(sheet.range(f'{img_key_letter}{row}').value)
857
+ img_save_path = image_dir / f"{sanitize_filename(filename)}{extension}"
858
+ else:
859
+ img_save_path = None
860
+
861
+ img_path = download_img_v2(img_url, platform, img_save_path)
862
+ log(f'插入图片 {sheet.name} [{img_col_name}] {row}/{last_row} {img_path}')
863
+ if not img_path:
864
+ log('跳过:', img_path, img_url)
865
+ continue
866
+ cell_value = cell_range.value
867
+
868
+ # 优化图片插入函数调用 (优化点3)
869
+ try:
870
+ # 使用预计算的位置直接插入图片
871
+ sheet.pictures.add(img_path, top=top + 2, left=left + 2, width=width - 4, height=width - 4)
872
+ cell_range.value = None
873
+ except Exception as e:
874
+ # 插入图片失败恢复链接地址
875
+ cell_range.value = cell_value
876
+ send_exception()
877
+ else:
878
+ log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
879
+
880
+ 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):
881
+ """
882
+ V3版本:支持一次性插入多列图片,每列可以设置不同的宽度
883
+
884
+ Args:
885
+ sheet: Excel工作表对象
886
+ columns: 图片列名列表,如 ['SKC图片', 'SKU图片']
887
+ platform: 平台名称,如 'shein'
888
+ img_widths: 图片宽度列表,与columns对应,如 [90, 60]
889
+ img_save_key: 图片保存时的key列
890
+ dir_name: 图片保存目录名
891
+ cell_height_with_img: 是否根据图片调整单元格高度
892
+ start_row: 开始行号,默认为2
893
+ """
894
+ if not columns:
895
+ return
896
+
897
+ # 如果没有提供宽度列表,使用默认宽度150
898
+ if not img_widths:
899
+ img_widths = [150] * len(columns)
900
+
901
+ # 确保宽度列表长度与列名列表一致
902
+ if len(img_widths) != len(columns):
903
+ raise ValueError(f"img_widths长度({len(img_widths)})必须与columns长度({len(columns)})一致")
904
+
905
+ minimize(sheet.book.app)
906
+
907
+ # 只清空一次所有图片
908
+ clear_all_pictures(sheet)
909
+
910
+ # 获取每列图片列的列号,并批量下载图片
911
+ col_letter_map = {}
912
+ col_width_map = {} # 存储每列对应的宽度
913
+
914
+ for idx, img_col in enumerate(columns):
915
+ col_letter = find_column_by_data(sheet, 1, img_col)
916
+ if col_letter is not None:
917
+ col_letter_map[img_col] = col_letter
918
+ col_width_map[col_letter] = img_widths[idx]
919
+ # 下载图片
920
+ log(f'批量下载图片: {img_col} => {col_letter} (宽度: {img_widths[idx]})')
921
+ last_row = get_last_row(sheet, col_letter)
922
+ images = sheet.range(f'{col_letter}2:{col_letter}{last_row}').value
923
+ images = images if isinstance(images, list) else [images]
924
+ download_images_concurrently(images, platform)
925
+
926
+ # 任意一个列作为主参考列,用来确定行数
927
+ if not col_letter_map:
928
+ return
929
+
930
+ ref_col_letter = next(iter(col_letter_map.values()))
931
+ last_row = get_last_row(sheet, ref_col_letter)
932
+
933
+ img_key_letter = find_column_by_data(sheet, 1, img_save_key)
934
+
935
+ # 阶段1:收集每个单元格需要的尺寸要求
936
+ cell_size_requirements = {} # {cell_address: {'width': max_width, 'height': max_height, 'merge': is_merge}}
937
+
938
+ for row in range(start_row, last_row + 1):
939
+ for col_letter in col_letter_map.values():
940
+ cell_ref = f'{col_letter}{row}'
941
+ cell_range = sheet.range(cell_ref)
942
+ cell_address = cell_range.address
943
+ img_width = col_width_map[col_letter]
944
+
945
+ if cell_range.merge_cells:
946
+ cell_range = cell_range.merge_area
947
+ cell_address = cell_range.address
948
+
949
+ # 记录每个单元格需要的最大尺寸
950
+ if cell_address not in cell_size_requirements:
951
+ cell_size_requirements[cell_address] = {
952
+ 'width': img_width,
953
+ 'height': img_width,
954
+ 'cell_range': cell_range,
955
+ 'merge': cell_range.merge_cells
956
+ }
957
+ else:
958
+ # 取最大值
959
+ cell_size_requirements[cell_address]['width'] = max(
960
+ cell_size_requirements[cell_address]['width'], img_width
961
+ )
962
+ cell_size_requirements[cell_address]['height'] = max(
963
+ cell_size_requirements[cell_address]['height'], img_width
964
+ )
965
+
966
+ # 阶段2:统一调整所有单元格的宽高(按列分别处理)
967
+ log(f'调整单元格尺寸...')
968
+ adjusted_cells = {} # 记录已调整的单元格,避免重复调整
969
+
970
+ for col_letter in col_letter_map.values():
971
+ img_width = col_width_map[col_letter]
972
+
973
+ for row in range(start_row, last_row + 1):
974
+ cell_ref = f'{col_letter}{row}'
975
+ cell_range = sheet.range(cell_ref)
976
+ cell_address = cell_range.address
977
+
978
+ if cell_range.merge_cells:
979
+ cell_range = cell_range.merge_area
980
+ cell_address = cell_range.address
981
+
982
+ # 调整列宽(按原来的逻辑,每列都调整)
983
+ if cell_range.width < img_width:
984
+ cell_range.column_width = img_width / 6.1
985
+ # 这一行暂时先自动控制宽度
986
+ cell_range.column_width = img_width / 6.1
987
+
988
+ # 行高只调整一次(使用最大需求)
989
+ if cell_address not in adjusted_cells:
990
+ adjusted_cells[cell_address] = True
991
+ required_height = cell_size_requirements[cell_address]['height']
992
+
993
+ # 调整行高
994
+ if cell_range.height < required_height:
995
+ if cell_range.merge_cells:
996
+ # 合并单元格:为每一行设置高度
997
+ rows_count = cell_range.rows.count
998
+ per_row_height = required_height / rows_count
999
+ for single_row in cell_range.rows:
1000
+ single_row.row_height = max(per_row_height, 150 / 8)
1001
+ else:
1002
+ cell_range.row_height = max(required_height, 150 / 8)
1003
+
1004
+ if cell_height_with_img:
1005
+ if cell_range.merge_cells:
1006
+ rows_count = cell_range.rows.count
1007
+ for single_row in cell_range.rows:
1008
+ single_row.row_height = required_height / rows_count
1009
+ else:
1010
+ cell_range.row_height = required_height
1011
+
1012
+ # 阶段3:计算所有图片的位置
1013
+ area_map = {}
1014
+ for row in range(start_row, last_row + 1):
1015
+ log(f'计算位置 {row}/{last_row}')
1016
+ for col_letter in col_letter_map.values():
1017
+ cell_ref = f'{col_letter}{row}'
1018
+ cell_range = sheet.range(cell_ref)
1019
+ cell_address = cell_range.address
1020
+ img_width = col_width_map[col_letter]
1021
+
1022
+ if cell_range.merge_cells:
1023
+ cell_range = cell_range.merge_area
1024
+ cell_address = cell_range.address
1025
+
1026
+ # 重新读取调整后的宽高
1027
+ actual_width = cell_range.width
1028
+ actual_height = cell_range.height
1029
+
1030
+ # 计算该列图片的居中位置
1031
+ # 图片实际大小是 img_width-4,插入时偏移+2,所以这里-2补偿
1032
+ actual_img_size = img_width - 4
1033
+ top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
1034
+ left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
1035
+
1036
+ # 每个列都单独保存位置
1037
+ area_map[f'{cell_address}_{col_letter}'] = {
1038
+ 'top': top,
1039
+ 'left': left,
1040
+ 'width': img_width,
1041
+ 'cell_address': cell_address,
1042
+ 'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
1043
+ }
1044
+
1045
+ # 阶段4:插入图片
1046
+ for row in range(start_row, last_row + 1):
1047
+ for img_col_name, col_letter in col_letter_map.items():
1048
+ cell_ref = f'{col_letter}{row}'
1049
+ cell_range = sheet.range(cell_ref)
1050
+ original_address = cell_range.address
1051
+
1052
+ if cell_range.merge_cells:
1053
+ cell_range = cell_range.merge_area
1054
+ cell_address = cell_range.address
1055
+ else:
1056
+ cell_address = original_address
1057
+
1058
+ # 检查是否是合并单元格的非首单元格(跳过)
1059
+ area_key = f'{cell_address}_{col_letter}'
1060
+ if area_key not in area_map:
1061
+ continue
1062
+
1063
+ area_info = area_map[area_key]
1064
+
1065
+ # 对于合并单元格,只在第一个单元格处理
1066
+ if cell_range.merge_cells:
1067
+ # 获取合并区域的第一个单元格地址
1068
+ first_cell_in_merge = area_info['cell_list'][0] if area_info['cell_list'] else cell_address
1069
+ # 如果当前单元格不是合并区域的第一个单元格,跳过
1070
+ if original_address != first_cell_in_merge:
1071
+ continue
1072
+
1073
+ # 使用预计算的位置信息
1074
+ top = area_info['top']
1075
+ left = area_info['left']
1076
+ width = area_info['width']
1077
+
1078
+ # 获取图片链接
1079
+ if cell_range.merge_cells:
1080
+ img_url = cell_range.value[0]
1081
+ else:
1082
+ img_url = cell_range.value
1083
+
1084
+ if img_url:
1085
+ if img_key_letter is not None:
1086
+ image_dir = Path(f'{os.getenv('auto_dir')}/image') / dir_name
1087
+ extension = Path(img_url).suffix
1088
+ filename = str(sheet.range(f'{img_key_letter}{row}').value)
1089
+ img_save_path = image_dir / f"{sanitize_filename(filename)}{extension}"
1090
+ else:
1091
+ img_save_path = None
1092
+
1093
+ img_path = download_img_v2(img_url, platform, img_save_path)
1094
+ log(f'插入图片 {sheet.name} [{img_col_name}] {row}/{last_row} {img_path}')
1095
+ if not img_path:
1096
+ log('跳过:', img_path, img_url)
1097
+ continue
1098
+ cell_value = cell_range.value
1099
+
1100
+ # 插入图片
1101
+ try:
1102
+ # 使用预计算的位置直接插入图片
1103
+ sheet.pictures.add(img_path, top=top + 2, left=left + 2, width=width - 4, height=width - 4)
1104
+ cell_range.value = None
1105
+ except Exception as e:
1106
+ # 插入图片失败恢复链接地址
1107
+ cell_range.value = cell_value
1108
+ send_exception()
1109
+ else:
1110
+ log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
1111
+
1112
+ def download_images_concurrently(image_urls, platform='shein', img_save_dir=None):
1113
+ # 使用线程池执行并发下载
1114
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
1115
+ # 使用 lambda 函数同时传递 url 和 img_save_path
1116
+ results = list(executor.map(lambda url: download_img_v2(url, platform, img_save_path=img_save_dir), image_urls))
1117
+ return results
1118
+
1119
+ def download_img_by_chrome(image_url, save_name):
1120
+ try:
1121
+ with sync_playwright() as p:
1122
+ browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
1123
+ context = browser.new_context()
1124
+ page = context.new_page()
1125
+ # 直接通过Playwright下载图片
1126
+ response = page.request.get(image_url)
1127
+ with open(save_name, 'wb') as f:
1128
+ f.write(response.body()) # 将下载的内容保存为文件
1129
+ log(f"图片已通过chrome下载并保存为:{save_name}")
1130
+ # 关闭浏览器
1131
+ browser.close()
1132
+ return save_name
1133
+ except:
1134
+ send_exception()
1135
+ return None
1136
+
1137
+ def download_img_v2(image_url, platform='shein', img_save_path=None):
1138
+ image_url = add_https(image_url)
1139
+ if image_url is None or 'http' not in image_url:
1140
+ return False
1141
+
1142
+ image_dir = Path(f'{os.getenv('auto_dir')}/image')
1143
+ image_dir = os.path.join(image_dir, platform)
1144
+
1145
+ # 确保目录存在,如果不存在则创建(线程安全)
1146
+ os.makedirs(image_dir, exist_ok=True)
1147
+
1148
+ file_name = os.path.basename(urlparse(image_url).path) # 获取 URL 路径中的文件名
1149
+ file_path = os.path.join(image_dir, file_name) # 拼接文件路径
1150
+
1151
+ if os.path.exists(file_path):
1152
+ if img_save_path is not None:
1153
+ create_file_path(img_save_path)
1154
+ copy_file(file_path, img_save_path)
1155
+ return file_path
1156
+
1157
+ # http://yituo.obs.cn-south-1.myhuaweicloud.com:80//UPLOAD/100743/2025-05/1747213019M9ujHVHG.jpg?x-image-process=image/resize,m_lfit,h_100,w_100
1158
+ # https://ssmp-spmp.oss-cn-shenzhen.aliyuncs.com/4136915/image/spec/wNgR5gFzOYYnu52Jkyez.jpg?x-image-process=image/resize,m_lfit,h_100,w_100
1159
+ # 这个域名有浏览器指纹校验 无法通过脚本下载图片
1160
+ if any(blocked in image_url for blocked in
1161
+ ['myhuaweicloud.com', 'ssmp-spmp.oss-cn-shenzhen.aliyuncs.com', 'image.myqcloud.com', 'kj-img.pddpic.com']):
1162
+ return download_img_by_chrome(image_url, file_path)
1163
+
1164
+ # if 'myhuaweicloud.com' in image_url:
1165
+ # return False
1166
+ # if 'ssmp-spmp.oss-cn-shenzhen.aliyuncs.com' in image_url:
1167
+ # return False
1168
+ # if 'image.myqcloud.com' in image_url:
1169
+ # return False
1170
+ # if 'kj-img.pddpic.com' in image_url:
1171
+ # return False
1172
+
1173
+ headers = {
1174
+ "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",
1175
+ "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",
1176
+ "Accept-Encoding": "gzip, deflate",
1177
+ "Accept-Language": "zh-CN,zh;q=0.9"
1178
+ }
1179
+ # 下载图片
1180
+ try:
1181
+ response = requests.get(image_url, headers=headers, timeout=10)
1182
+ response.raise_for_status() # 如果响应状态码不是 200,将引发 HTTPError
1183
+ # # 成功处理
1184
+ log(f"成功获取网络图片: {image_url}")
1185
+ except requests.exceptions.HTTPError as e:
1186
+ log(f"HTTP 错误: {e} {image_url}")
1187
+ return False
1188
+ except requests.exceptions.ConnectionError as e:
1189
+ log(f"连接错误: {e} {image_url}")
1190
+ return False
1191
+ except requests.exceptions.Timeout as e:
1192
+ log(f"请求超时: {e} {image_url}")
1193
+ return False
1194
+ except requests.exceptions.RequestException as e:
1195
+ log(f"请求异常: {e} {image_url}")
1196
+ return False
1197
+
1198
+ # 将图片保存到本地
1199
+ with open(file_path, 'wb') as f:
1200
+ f.write(response.content)
1201
+
1202
+ if img_save_path is not None:
1203
+ create_file_path(img_save_path)
1204
+ copy_file(file_path, img_save_path)
1205
+
1206
+ return file_path
1207
+
1208
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
1209
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
1210
+ def insert_cell_image(sheet, cell, file_path, img_width=120):
1211
+ """
1212
+ 从本地文件居中插入图片到 Excel 指定合并的单元格。
1213
+ :param sheet: xlwings 的 Sheet 对象。
1214
+ :param cell: 目标单元格地址(如 'A1')。
1215
+ :param file_path: 本地文件路径。
1216
+ :param img_width: 插入图片的宽高 图片为正方形 和 row_height 同值
1217
+ """
1218
+ try:
1219
+ # 获取目标单元格区域
1220
+ cell_range = sheet.range(cell)
1221
+
1222
+ # 如果是合并区域,获取合并区域
1223
+ if cell_range.merge_cells:
1224
+ merge_area = cell_range.merge_area
1225
+ cell_range = merge_area # 更新为合并区域
1226
+
1227
+ # 获取合并区域的宽度和高度
1228
+ cell_width = cell_range.width # 单元格宽度
1229
+ cell_height = cell_range.height # 单元格高度
1230
+
1231
+ # 如果单元格的宽度小于图片宽度,增加单元格的宽度
1232
+ if cell_width < img_width:
1233
+ cell_range.column_width = img_width / 6.1 # 约12.86字符宽度
1234
+
1235
+ # 这一行暂时先自动控制宽度
1236
+ cell_range.column_width = img_width / 6.1
1237
+
1238
+ # 如果单元格的高度小于图片高度,增加单元格的高度
1239
+ if cell_height < img_width:
1240
+ cell_range.row_height = max(150 / 8, img_width / cell_range.rows.count) # 设置为图片高度
1241
+
1242
+ # 获取合并区域的 top 和 left,计算图片居中的位置
1243
+ top = cell_range.top + (cell_range.height - img_width) / 2
1244
+ left = cell_range.left + (cell_range.width - img_width) / 2
1245
+
1246
+ # 将图片插入到指定单元格并填充满单元格
1247
+ sheet.pictures.add(file_path, top=top + 2, left=left + 2, width=img_width - 4, height=img_width - 4)
1248
+
1249
+ except Exception as e:
1250
+ log(f'插入图片失败: {e}, {file_path}')
1251
+ send_exception()
1252
+
1253
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
1254
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
1255
+ def insert_image_from_local(sheet, cell, file_path, cell_width=90, cell_height=90):
1256
+ """
1257
+ 从本地文件插入图片到 Excel 指定单元格。
1258
+ :param sheet: xlwings 的 Sheet 对象。
1259
+ :param cell: 目标单元格地址(如 'A1')。
1260
+ :param file_path: 本地文件路径。
1261
+ """
1262
+ try:
1263
+ # 打印文件路径以确保正确
1264
+ # log(f'插入图片的文件路径: {file_path}')
1265
+
1266
+ # if is_cell_has_image(sheet, cell):
1267
+ # log(f'单元格 {cell} 已有图片,跳过插入。')
1268
+ # return
1269
+
1270
+ # 获取单元格位置
1271
+ cell_range = sheet.range(cell)
1272
+ # cell_width = cell_range.width # 获取单元格的宽度
1273
+ # cell_height = cell_range.height # 获取单元格的高度
1274
+ # log(f'插入图片单元格:{cell} {cell_width} {cell_height}')
1275
+
1276
+ # 设置列宽为 90 磅(近似值)
1277
+ cell_range.column_width = cell_width / 6.1 # 约 12.86 字符宽度
1278
+ # 设置行高为 90 磅
1279
+ cell_range.row_height = cell_height
1280
+
1281
+ # 将图片插入到指定单元格并填充满单元格
1282
+ sheet.pictures.add(file_path,
1283
+ top=cell_range.top + 5,
1284
+ left=cell_range.left + 5,
1285
+ width=cell_width - 10, height=cell_height - 10)
1286
+
1287
+ # log(f'图片已成功插入到单元格 {cell}')
1288
+ except Exception as e:
1289
+ log(f'插入图片失败: {e}, {file_path}')
1290
+
1291
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
1292
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
1293
+ def insert_skc_image_from_local(sheet, cell, file_path):
1294
+ """
1295
+ 从本地文件插入图片到 Excel 指定单元格。
1296
+ :param sheet: xlwings 的 Sheet 对象。
1297
+ :param cell: 目标单元格地址(如 'A1')。
1298
+ :param file_path: 本地文件路径。
1299
+ """
1300
+ try:
1301
+ # 打印文件路径以确保正确
1302
+ log(f'插入图片的文件路径: {file_path}')
1303
+
1304
+ # if is_cell_has_image(sheet, cell):
1305
+ # log(f'单元格 {cell} 已有图片,跳过插入。')
1306
+ # return
1307
+
1308
+ # 获取单元格位置
1309
+ cell_range = sheet.range(cell)
1310
+ cell_width = cell_range.width # 获取单元格的宽度
1311
+ cell_height = cell_range.height # 获取单元格的高度
1312
+
1313
+ # 将图片插入到指定单元格并填充满单元格
1314
+ sheet.pictures.add(file_path,
1315
+ top=cell_range.top + 2,
1316
+ left=cell_range.left + 2,
1317
+ width=86, height=88)
1318
+
1319
+ log(f'图片已成功插入到单元格 {cell}')
1320
+ except Exception as e:
1321
+ log(f'插入图片失败: {e}')
1322
+
1323
+ # # 设置 A 列和第 1 行为接近 100x100 的正方形
1324
+ # set_square_cells(sheet, 'A', 1, 100)
1325
+
1326
+ def clear_all_pictures(sheet):
1327
+ """
1328
+ 清空 Excel Sheet 中的所有图片。
1329
+
1330
+ :param sheet: xlwings 的 Sheet 对象
1331
+ """
1332
+ try:
1333
+ # 遍历并删除所有图片
1334
+ for picture in sheet.pictures:
1335
+ picture.delete()
1336
+ log("已清空该 Sheet 上的所有图片!")
1337
+ except Exception as e:
1338
+ send_exception()
1339
+ log(f"清空图片失败: {e}")
1340
+
1341
+ def get_excel_format(sheet, cell_range):
1342
+ rng = sheet.range(cell_range)
1343
+
1344
+ format_settings = {
1345
+ "numberFormat": rng.number_format,
1346
+ "font" : {
1347
+ "name" : rng.api.Font.Name,
1348
+ "size" : rng.api.Font.Size,
1349
+ "bold" : rng.api.Font.Bold,
1350
+ "italic": rng.api.Font.Italic,
1351
+ "color" : rng.api.Font.Color
1352
+ },
1353
+ "alignment" : {
1354
+ "horizontalAlignment": rng.api.HorizontalAlignment,
1355
+ "verticalAlignment" : rng.api.VerticalAlignment,
1356
+ "wrapText" : rng.api.WrapText
1357
+ },
1358
+ "borders" : []
1359
+ }
1360
+
1361
+ # 获取所有边框设置(Excel 有 8 种边框)
1362
+ for index in range(5, 13):
1363
+ border = rng.api.Borders(index)
1364
+ format_settings["borders"].append({
1365
+ "index" : index,
1366
+ "lineStyle": border.LineStyle,
1367
+ "color" : border.Color,
1368
+ "weight" : border.Weight
1369
+ })
1370
+
1371
+ # 获取背景色
1372
+ format_settings["background"] = {
1373
+ "color": rng.api.Interior.Color
1374
+ }
1375
+
1376
+ # 获取锁定和公式隐藏
1377
+ format_settings["locked"] = rng.api.Locked
1378
+ format_settings["formulaHidden"] = rng.api.FormulaHidden
1379
+
1380
+ return json.dumps(format_settings, indent=2)
1381
+
1382
+ def set_excel_format(sheet, cell_range, json_setting):
1383
+ settings = json.loads(json_setting)
1384
+
1385
+ # 解析并应用格式
1386
+ rng = sheet.range(cell_range)
1387
+
1388
+ # 设置数字格式
1389
+ if "numberFormat" in settings:
1390
+ rng.number_format = settings["numberFormat"]
1391
+
1392
+ # 设置字体格式
1393
+ if "font" in settings:
1394
+ font = settings["font"]
1395
+ if "name" in font:
1396
+ rng.api.Font.Name = font["name"]
1397
+ if "size" in font:
1398
+ rng.api.Font.Size = font["size"]
1399
+ if "bold" in font:
1400
+ rng.api.Font.Bold = font["bold"]
1401
+ if "italic" in font:
1402
+ rng.api.Font.Italic = font["italic"]
1403
+ if "color" in font:
1404
+ rng.api.Font.Color = font["color"]
1405
+
1406
+ # 设置对齐方式
1407
+ if "alignment" in settings:
1408
+ alignment = settings["alignment"]
1409
+ if "horizontalAlignment" in alignment:
1410
+ rng.api.HorizontalAlignment = alignment["horizontalAlignment"]
1411
+ if "verticalAlignment" in alignment:
1412
+ rng.api.VerticalAlignment = alignment["verticalAlignment"]
1413
+ if "wrapText" in alignment:
1414
+ rng.api.WrapText = alignment["wrapText"]
1415
+
1416
+ # 设置边框
1417
+ if "borders" in settings:
1418
+ for border in settings["borders"]:
1419
+ index = border["index"]
1420
+ line_style = border["lineStyle"]
1421
+ color = border["color"]
1422
+ weight = border["weight"]
1423
+
1424
+ rng.api.Borders(index).LineStyle = line_style
1425
+ rng.api.Borders(index).Color = color
1426
+ rng.api.Borders(index).Weight = weight
1427
+
1428
+ # 设置背景
1429
+ if "background" in settings:
1430
+ bg = settings["background"]
1431
+ if "color" in bg:
1432
+ rng.api.Interior.Color = bg["color"]
1433
+
1434
+ # 设置锁定和隐藏公式
1435
+ if "locked" in settings:
1436
+ rng.api.Locked = settings["locked"]
1437
+ if "formulaHidden" in settings:
1438
+ rng.api.FormulaHidden = settings["formulaHidden"]
1439
+
1440
+ # # 获取 A1 单元格格式
1441
+ # json_format = get_excel_format(sheet, "A1")
1442
+ # log("Original Format:", json_format)
1443
+ # # 将格式应用到 B1
1444
+ # set_excel_format(sheet, json_format, "B1")
1445
+ # log("Format copied from A1 to B1")
1446
+
1447
+ def get_unique_values(sheet, column, start_row, end_row=None):
1448
+ """
1449
+ 获取指定列从指定行开始的不重复值列表,确保读取的值与 Excel 中显示的内容完全一致。
1450
+
1451
+ 参数:
1452
+ sheet (xlwings.Sheet): Excel 工作表对象。
1453
+ column (str): 列字母(例如 'A', 'B' 等)。
1454
+ start_row (int): 开始行号。
1455
+ end_row (int, optional): 结束行号。如果未提供,则读取到列的最后一行。
1456
+
1457
+ 返回:
1458
+ list: 不重复的值列表。
1459
+ """
1460
+ # 获取指定列的区域
1461
+ if end_row:
1462
+ range_str = f"{column}{start_row}:{column}{end_row}"
1463
+ else:
1464
+ range_str = f"{column}{start_row}:{column}{sheet.range(f'{column}{start_row}').end('down').row}"
1465
+
1466
+ values = []
1467
+ for cell in sheet.range(range_str):
1468
+ # 使用 .api 获取底层 Excel 单元格的 Text 属性
1469
+ cell_value = cell.api.Text
1470
+ values.append(cell_value)
1471
+ # 将值转换为字符串并去重
1472
+ unique_values = list(set(str(value) if value is not None else "" for value in values))
1473
+ return unique_values
1474
+ # # 获取 A 列从第 2 行开始的不重复值
1475
+ # unique_values = get_unique_values(sheet, 'A', 2)
1476
+ # log(unique_values)
1477
+
1478
+ def get_unique_values_by_row(sheet, row, start_col, end_col=None):
1479
+ """
1480
+ 获取指定行从指定列开始的不重复值列表,确保读取的值与 Excel 中显示的内容完全一致。
1481
+
1482
+ 参数:
1483
+ sheet (xlwings.Sheet): Excel 工作表对象。
1484
+ row (int): 行号。
1485
+ start_col (str): 开始列字母(例如 'A', 'B' 等)。
1486
+ end_col (str, optional): 结束列字母。如果未提供,则读取到行的最后一列。
1487
+
1488
+ 返回:
1489
+ list: 不重复的值列表。
1490
+ """
1491
+ # 获取指定行的区域
1492
+ if end_col:
1493
+ range_str = f"{start_col}{row}:{end_col}{row}"
1494
+ else:
1495
+ range_str = f"{start_col}{row}:{sheet.range(f'{start_col}{row}').end('right').column_letter}{row}"
1496
+
1497
+ values = []
1498
+ for cell in sheet.range(range_str):
1499
+ # 使用 .api 获取底层 Excel 单元格的 Text 属性
1500
+ cell_value = cell.api.Text
1501
+ values.append(cell_value)
1502
+
1503
+ # 将值转换为字符串并去重
1504
+ unique_values = list(set(str(value) if value is not None else "" for value in values))
1505
+ return unique_values
1506
+ # 获取第 2 行从 A 列开始的不重复值
1507
+ # unique_values = get_unique_values_by_row(sheet, 2, 'A')
1508
+
1509
+ def find_rows_by_criteria(sheet, col, search_text, match_type='equals'):
1510
+ """
1511
+ 在指定列中查找符合条件的数据所在行。
1512
+
1513
+ 参数:
1514
+ sheet (xlwings.Sheet): Excel 工作表对象。
1515
+ col (str or int): 查找列号,支持列字母(如 'A')或列号(如 1),也支持负数(如 -1 表示倒数第一列)。
1516
+ search_text (str): 待查找的文本内容。
1517
+ match_type (str): 匹配方式,可选 'equals'(完全匹配)或 'contains'(包含匹配)。默认为 'equals'。
1518
+
1519
+ 返回:
1520
+ list: 包含所有符合查找标准的行号的列表。如果未找到匹配项,则返回空列表 []。
1521
+ """
1522
+ # 将列号转换为列字母
1523
+ if isinstance(col, int):
1524
+ if col < 0:
1525
+ # 处理负数列号(倒数第几列)
1526
+ col = sheet.range((1, 1)).end('right').column + col + 1
1527
+ col_letter = xw.utils.col_name(col)
1528
+ else:
1529
+ col_letter = col.upper()
1530
+
1531
+ # 获取指定列的区域
1532
+ start_cell = sheet.range(f"{col_letter}1")
1533
+ end_cell = start_cell.end('down')
1534
+ range_str = f"{col_letter}1:{col_letter}{end_cell.row}"
1535
+
1536
+ # 查找符合条件的行号
1537
+ matched_rows = []
1538
+ for cell in sheet.range(range_str):
1539
+ cell_value = cell.api.Text # 获取单元格的显示值
1540
+ # log('内部',cell_value,search_text,cell_value == search_text)
1541
+ if match_type == 'equals' and cell_value == search_text:
1542
+ matched_rows.append(cell.row)
1543
+ elif match_type == 'contains' and search_text in cell_value:
1544
+ matched_rows.append(cell.row)
1545
+
1546
+ return matched_rows
1547
+
1548
+ # # 示例 1:在 A 列中查找完全匹配 "123" 的行号
1549
+ # result_equals = find_rows_by_criteria(sheet, 'A', '123', match_type='equals')
1550
+ # log("完全匹配结果:", result_equals)
1551
+
1552
+ # # 示例 2:在 B 列中查找包含 "abc" 的行号
1553
+ # result_contains = find_rows_by_criteria(sheet, 2, 'abc', match_type='contains')
1554
+ # log("包含匹配结果:", result_contains)
1555
+
1556
+ # # 示例 3:在倒数第一列中查找完全匹配 "xyz" 的行号
1557
+ # result_negative_col = find_rows_by_criteria(sheet, -1, 'xyz', match_type='equals')
1558
+ # log("倒数第一列匹配结果:", result_negative_col)
1559
+
1560
+ def find_columns_by_criteria(sheet, row, search_text, match_type='equals'):
1561
+ """
1562
+ 在指定行中查找符合条件的数据所在列。
1563
+
1564
+ 参数:
1565
+ sheet (xlwings.Sheet): Excel 工作表对象。
1566
+ row (int): 查找行号,支持正数(如 1)或负数(如 -1 表示倒数第一行)。
1567
+ search_text (str): 待查找的文本内容。
1568
+ match_type (str): 匹配方式,可选 'equals'(完全匹配)或 'contains'(包含匹配)。默认为 'equals'。
1569
+
1570
+ 返回:
1571
+ list: 包含所有符合查找标准的列字母的列表。如果未找到匹配项,则返回空列表 []。
1572
+ """
1573
+ # 处理负行号
1574
+ if row < 0:
1575
+ last_row = sheet.range('A1').end('down').row
1576
+ row = last_row + row + 1
1577
+
1578
+ # 获取指定行的区域
1579
+ start_cell = sheet.range(f"A{row}")
1580
+ end_cell = start_cell.end('right')
1581
+ range_str = f"A{row}:{end_cell.column_letter}{row}"
1582
+
1583
+ # 查找符合条件的列字母
1584
+ matched_columns = []
1585
+ for cell in sheet.range(range_str):
1586
+ cell_value = cell.api.Text # 获取单元格的显示值
1587
+ if match_type == 'equals' and cell_value == search_text:
1588
+ matched_columns.append(cell.column_letter)
1589
+ elif match_type == 'contains' and search_text in cell_value:
1590
+ matched_columns.append(cell.column_letter)
1591
+
1592
+ return matched_columns
1593
+ # # 示例 1:在第 1 行中查找完全匹配 "123" 的列字母
1594
+ # result_equals = find_columns_by_criteria(sheet, 1, '123', match_type='equals')
1595
+ # log("完全匹配结果:", result_equals)
1596
+
1597
+ # # 示例 2:在第 2 行中查找包含 "abc" 的列字母
1598
+ # result_contains = find_columns_by_criteria(sheet, 2, 'abc', match_type='contains')
1599
+ # log("包含匹配结果:", result_contains)
1600
+
1601
+ # # 示例 3:在倒数第一行中查找完全匹配 "xyz" 的列字母
1602
+ # result_negative_row = find_columns_by_criteria(sheet, -1, 'xyz', match_type='equals')
1603
+ # log("倒数第一行匹配结果:", result_negative_row)
1604
+
1605
+ def check_data(data):
1606
+ for row in data:
1607
+ log(len(row), row)
1608
+
1609
+ def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
1610
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1611
+ # 清空工作表中的所有数据
1612
+ sheet.clear()
1613
+ # 某些列以文本格式写入(从data表头获取列索引)
1614
+ if format_to_text_colunm and data and len(data) > 0:
1615
+ headers = data[0]
1616
+ for col_name in format_to_text_colunm:
1617
+ if col_name in headers:
1618
+ col_idx = headers.index(col_name) + 1 # Excel列索引从1开始
1619
+ col_letter = xw.utils.col_name(col_idx)
1620
+ log(f'设置[{col_name}] => [{col_letter}] 文本格式')
1621
+ sheet.range(f'{col_letter}:{col_letter}').number_format = '@'
1622
+ else:
1623
+ log(f'未找到列名[{col_name}],跳过文本格式设置')
1624
+ # 写入数据
1625
+ # check_data(data)
1626
+ sheet.range('A1').value = data
1627
+ # 保存
1628
+ wb.save()
1629
+ close_excel(app, wb)
1630
+
1631
+ def colorize_by_field(sheet, field):
1632
+ minimize(sheet.book.app)
1633
+ # 读取数据
1634
+ field_column = find_column_by_data(sheet, 1, field) # 假设 SPU 在 C 列
1635
+ if field_column is None:
1636
+ return
1637
+ data_range = sheet.range(f"{field_column}1").expand("down") # 获取 SPU 列的所有数据
1638
+ spu_values = data_range.value[:]
1639
+ max_column_letter = get_max_column_letter(sheet)
1640
+ # 记录 SPU 对应的颜色
1641
+ spu_color_map = {}
1642
+ for i, spu in enumerate(spu_values): # 从 Excel 第 2 行开始(第 1 行是标题)
1643
+ row = i + 1
1644
+ if row < 2:
1645
+ continue
1646
+ if spu not in spu_color_map:
1647
+ spu_color_map[spu] = random_color() # 生成新的颜色
1648
+ bg_color = spu_color_map[spu]
1649
+ row_range = sheet.range(f"A{row}:{max_column_letter}{row}")
1650
+ row_range.color = bg_color # 应用背景色
1651
+ sheet.range(f"A{row}").api.Font.Bold = True # 让店铺名称加粗
1652
+
1653
+ def colorize_by_field_v2(sheet, field):
1654
+ """
1655
+ 改进版:按指定字段为行着色,正确处理合并单元格
1656
+
1657
+ Args:
1658
+ sheet: Excel工作表对象
1659
+ field: 用于分组着色的字段名(列名)
1660
+ """
1661
+ minimize(sheet.book.app)
1662
+
1663
+ # 查找字段所在的列
1664
+ field_column = find_column_by_data(sheet, 1, field)
1665
+ if field_column is None:
1666
+ log(f'未找到字段列: {field}')
1667
+ return
1668
+
1669
+ log(f'按字段 {field} (列 {field_column}) 着色')
1670
+
1671
+ # 获取最后一行和最后一列
1672
+ last_row = get_last_row(sheet, field_column)
1673
+ max_column_letter = get_max_column_letter(sheet)
1674
+
1675
+ # 记录字段值对应的颜色
1676
+ field_color_map = {}
1677
+ last_field_value = None # 记录上一个非空值
1678
+
1679
+ # 从第2行开始遍历(跳过表头)
1680
+ for row in range(2, last_row + 1):
1681
+ # 读取当前行的字段值
1682
+ cell = sheet.range(f'{field_column}{row}')
1683
+ current_value = cell.value
1684
+
1685
+ # 如果是合并单元格的非首单元格,值可能为 None,使用上一个非空值
1686
+ if current_value is None or current_value == '':
1687
+ # 检查是否是合并单元格
1688
+ if cell.merge_cells:
1689
+ # 使用合并区域的值
1690
+ merge_area = cell.merge_area
1691
+ current_value = merge_area.value
1692
+ if isinstance(current_value, (list, tuple)):
1693
+ current_value = current_value[0] if current_value else None
1694
+
1695
+ # 如果仍然为空,使用上一个非空值
1696
+ if current_value is None or current_value == '':
1697
+ current_value = last_field_value
1698
+ else:
1699
+ # 更新上一个非空值
1700
+ last_field_value = current_value
1701
+
1702
+ # 跳过空值
1703
+ if current_value is None or current_value == '':
1704
+ continue
1705
+
1706
+ # 为新的字段值分配颜色
1707
+ if current_value not in field_color_map:
1708
+ field_color_map[current_value] = random_color()
1709
+
1710
+ # 应用背景色到整行
1711
+ bg_color = field_color_map[current_value]
1712
+ row_range = sheet.range(f'A{row}:{max_column_letter}{row}')
1713
+ row_range.color = bg_color
1714
+
1715
+ # 可选:让第一列加粗(店铺信息等)
1716
+ # sheet.range(f'A{row}').api.Font.Bold = True
1717
+
1718
+ log(f'着色完成,共 {len(field_color_map)} 个不同的 {field} 值')
1719
+
1720
+ def add_borders(sheet, lineStyle=1):
1721
+ log('添加边框')
1722
+ # 获取工作表的整个范围(假设表格的数据是从A1开始)
1723
+ last_col = sheet.range('A1').end('right').column # 获取最后一列
1724
+ last_row = get_last_row(sheet, 'A')
1725
+ range_to_border = sheet.range((1, 1), (last_row, last_col)) # 定义范围
1726
+
1727
+ # 设置外部边框(所有边都为实线)
1728
+ range_to_border.api.Borders(7).LineStyle = lineStyle # 上边框
1729
+ range_to_border.api.Borders(8).LineStyle = lineStyle # 下边框
1730
+ range_to_border.api.Borders(9).LineStyle = lineStyle # 左边框
1731
+ range_to_border.api.Borders(10).LineStyle = lineStyle # 右边框
1732
+
1733
+ # 设置内部边框
1734
+ range_to_border.api.Borders(1).LineStyle = lineStyle # 内部上边框
1735
+ range_to_border.api.Borders(2).LineStyle = lineStyle # 内部下边框
1736
+ range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
1737
+ range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
1738
+
1739
+ def add_range_border(sheet, coor_A=(1, 1), coor_B=(1, 1), lineStyle=1):
1740
+ range_to_border = sheet.range(coor_A, coor_B) # 定义范围
1741
+
1742
+ # 设置外部边框(所有边都为实线)
1743
+ range_to_border.api.Borders(7).LineStyle = lineStyle # 上边框
1744
+ range_to_border.api.Borders(8).LineStyle = lineStyle # 下边框
1745
+ range_to_border.api.Borders(9).LineStyle = lineStyle # 左边框
1746
+ range_to_border.api.Borders(10).LineStyle = lineStyle # 右边框
1747
+
1748
+ # 设置内部边框
1749
+ range_to_border.api.Borders(1).LineStyle = lineStyle # 内部上边框
1750
+ range_to_border.api.Borders(2).LineStyle = lineStyle # 内部下边框
1751
+ range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
1752
+ range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
1753
+
1754
+ def open_excel(excel_path, sheet_name='Sheet1'):
1755
+ try:
1756
+ # 创建新实例
1757
+ app = xw.App(visible=True, add_book=False)
1758
+ app.display_alerts = False # 复用时仍然关闭警告
1759
+ app.screen_updating = True
1760
+
1761
+ # 打开或新建工作簿
1762
+ wb = None
1763
+ if os.path.exists(excel_path):
1764
+ for book in app.books:
1765
+ if book.fullname.lower() == os.path.abspath(excel_path).lower():
1766
+ wb = book
1767
+ break
1768
+ else:
1769
+ wb = app.books.open(excel_path, read_only=False)
1770
+ else:
1771
+ wb = app.books.add()
1772
+ os.makedirs(os.path.dirname(excel_path), exist_ok=True)
1773
+ wb.save(excel_path)
1774
+
1775
+ # 处理 sheet 选择逻辑(支持名称或索引)
1776
+ if isinstance(sheet_name, int): # 如果是整数,按索引获取
1777
+ if 0 <= sheet_name < len(wb.sheets): # 确保索引有效
1778
+ sheet = wb.sheets[sheet_name]
1779
+ else:
1780
+ log(f"索引 {sheet_name} 超出范围,创建新工作表。")
1781
+ sheet = wb.sheets.add(after=wb.sheets[-1])
1782
+ elif isinstance(sheet_name, str): # 如果是字符串,按名称获取
1783
+ sheet_name_clean = sheet_name.strip().lower()
1784
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
1785
+ if sheet_name_clean in sheet_names:
1786
+ sheet = wb.sheets[sheet_name]
1787
+ else:
1788
+ try:
1789
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
1790
+ except Exception as e:
1791
+ send_exception()
1792
+ return None, None, None
1793
+ else:
1794
+ send_exception(f"sheet_name 必须是字符串(名称)或整数(索引):{sheet_name}")
1795
+ raise
1796
+
1797
+ sheet.activate()
1798
+ file_name = os.path.basename(excel_path)
1799
+ log(f"open_excel {file_name} {sheet.name}")
1800
+ # 不能在这个地方最小化 容易导致错误
1801
+ # 让 Excel 窗口最小化
1802
+ # app.api.WindowState = -4140 # -4140 对应 Excel 中的 xlMinimized 常量
1803
+ return app, wb, sheet
1804
+
1805
+ except Exception as e:
1806
+ send_exception()
1807
+ # wxwork.notify_error_msg(f'打开 Excel 失败: {traceback.format_exc()}')
1808
+ return None, None, None
1809
+
1810
+ def close_excel(app, wb):
1811
+ if wb is not None:
1812
+ wb.save()
1813
+ wb.close()
1814
+ if app is not None:
1815
+ app.quit()
1816
+
1817
+ # 获取某列最后非空行
1818
+ def get_last_row(sheet, column):
1819
+ last_row = sheet.range(column + str(sheet.cells.last_cell.row)).end('up').row
1820
+ # 检查当前单元格是否在合并区域中
1821
+ cell = sheet.range(f'{column}{last_row}')
1822
+ # 如果当前单元格是合并单元格的一部分,获取合并区域的首行
1823
+ if cell.merge_cells:
1824
+ last_row = cell.merge_area.last_cell.row
1825
+ return last_row
1826
+
1827
+ # 获取最后一列字母
1828
+ def get_last_col(sheet):
1829
+ # # 获取最后一行的索引
1830
+ last_col = index_to_column_name(sheet.range('A1').end('right').column) # 里面是索引 返回最后一列 如 C
1831
+ return last_col
1832
+
1833
+ # 获取最大列名字母
1834
+ def get_max_column_letter(sheet):
1835
+ """获取当前 sheet 中最大有数据的列的列名(如 'A', 'B', ..., 'Z', 'AA', 'AB')"""
1836
+ last_col = sheet.used_range.last_cell.column # 获取最大列索引
1837
+ return xw.utils.col_name(last_col) # 将索引转换为列名
1838
+
1839
+ # 随机生成颜色
1840
+ def random_color():
1841
+ return (random.randint(180, 255), random.randint(180, 255), random.randint(180, 255)) # 亮色背景
1842
+
1843
+ def get_contrast_text_color(rgb):
1844
+ """根据背景色亮度返回适合的字体颜色(黑色或白色)"""
1845
+ r, g, b = rgb
1846
+ brightness = r * 0.299 + g * 0.587 + b * 0.114 # 亮度计算公式
1847
+ return (0, 0, 0) if brightness > 186 else (255, 255, 255) # 186 是经验值
1848
+
1849
+ def rgb_to_long(r, g, b):
1850
+ """将 RGB 颜色转换为 Excel Long 类型"""
1851
+ return r + (g * 256) + (b * 256 * 256)
1852
+
1853
+ def read_excel_to_json(file_path, sheet_name="Sheet1"):
1854
+ app, wb, sheet = open_excel(file_path, sheet_name)
1855
+
1856
+ used_range = sheet.used_range
1857
+ data = {}
1858
+ merged_cells = []
1859
+ column_widths = {} # 存储列宽度
1860
+ row_heights = {} # 存储行高度
1861
+
1862
+ # 记录列宽度
1863
+ for col in range(1, used_range.columns.count + 1):
1864
+ width = sheet.range((1, col)).column_width
1865
+ column_widths[col] = min(max(width, 1), 255) # ✅ 限制范围,防止错误
1866
+
1867
+ # 记录行高度
1868
+ for row in range(1, used_range.rows.count + 1):
1869
+ row_heights[row] = sheet.range((row, 1)).row_height # ✅ 修正行高获取方式
1870
+
1871
+ # 遍历所有单元格
1872
+ for row in range(1, used_range.rows.count + 1):
1873
+ for col in range(1, used_range.columns.count + 1):
1874
+ cell = sheet.cells(row, col)
1875
+
1876
+ # 处理对角线
1877
+ diagonal_up = cell.api.Borders(5) # 左上到右下
1878
+ diagonal_down = cell.api.Borders(6) # 右上到左下
1879
+
1880
+ diagonal_up_info = None
1881
+ diagonal_down_info = None
1882
+
1883
+ if diagonal_up.LineStyle == 1:
1884
+ diagonal_up_info = {"style": diagonal_up.LineStyle, "color": diagonal_up.Color}
1885
+
1886
+ if diagonal_down.LineStyle == 1:
1887
+ diagonal_down_info = {"style": diagonal_down.LineStyle, "color": diagonal_down.Color}
1888
+
1889
+ cell_info = {
1890
+ "value" : cell.value,
1891
+ "color" : cell.color,
1892
+ "font_name" : cell.api.Font.Name,
1893
+ "font_size" : cell.api.Font.Size,
1894
+ "bold" : cell.api.Font.Bold,
1895
+ "italic" : cell.api.Font.Italic,
1896
+ "font_color" : cell.api.Font.Color,
1897
+ "horizontal_align": cell.api.HorizontalAlignment,
1898
+ "vertical_align" : cell.api.VerticalAlignment,
1899
+ "number_format" : cell.api.NumberFormat,
1900
+ "border" : {
1901
+ "left" : {"style": cell.api.Borders(1).LineStyle, "color": cell.api.Borders(1).Color},
1902
+ "right" : {"style": cell.api.Borders(2).LineStyle, "color": cell.api.Borders(2).Color},
1903
+ "top" : {"style": cell.api.Borders(3).LineStyle, "color": cell.api.Borders(3).Color},
1904
+ "bottom": {"style": cell.api.Borders(4).LineStyle, "color": cell.api.Borders(4).Color},
1905
+ }
1906
+ }
1907
+
1908
+ if diagonal_up_info:
1909
+ cell_info["border"]["diagonal_up"] = diagonal_up_info
1910
+ if diagonal_down_info:
1911
+ cell_info["border"]["diagonal_down"] = diagonal_down_info
1912
+
1913
+ data[f"{row},{col}"] = cell_info
1914
+
1915
+ # 处理合并单元格
1916
+ for merged_range in sheet.api.UsedRange.Cells:
1917
+ if merged_range.MergeCells:
1918
+ merged_cells.append({
1919
+ "merge_range": merged_range.MergeArea.Address.replace("$", "")
1920
+ })
1921
+
1922
+ wb.close()
1923
+ app.quit()
1924
+
1925
+ final_data = {
1926
+ "cells" : data,
1927
+ "merged_cells" : merged_cells,
1928
+ "column_widths": column_widths,
1929
+ "row_heights" : row_heights
1930
+ }
1931
+
1932
+ with open("excel_data.json", "w", encoding="utf-8") as f:
1933
+ json.dump(final_data, f, indent=4, ensure_ascii=False)
1934
+
1935
+ print("✅ Excel 数据已存储为 JSON")
1936
+
1937
+ def write_json_to_excel(json_file, new_excel="new_test.xlsx", sheet_name="Sheet1"):
1938
+ with open(json_file, "r", encoding="utf-8") as f:
1939
+ final_data = json.load(f)
1940
+
1941
+ data = final_data["cells"]
1942
+ merged_cells = final_data["merged_cells"]
1943
+ column_widths = final_data["column_widths"]
1944
+ row_heights = final_data["row_heights"]
1945
+
1946
+ app, wb, sheet = open_excel(new_excel, sheet_name)
1947
+
1948
+ for col, width in column_widths.items():
1949
+ col_name = xw.utils.col_name(int(col))
1950
+ sheet.range(f'{col_name}:{col_name}').column_width = int(width)
1951
+
1952
+ # 恢复行高度
1953
+ for row, height in row_heights.items():
1954
+ sheet.range((row, 1)).row_height = height # ✅ 修正行高恢复方式
1955
+
1956
+ for key, cell_info in data.items():
1957
+ row, col = map(int, key.split(","))
1958
+
1959
+ cell = sheet.cells(row, col)
1960
+ cell.value = cell_info["value"]
1961
+ cell.color = cell_info["color"]
1962
+ cell.api.Font.Name = cell_info["font_name"]
1963
+ cell.api.Font.Size = cell_info["font_size"]
1964
+ cell.api.Font.Bold = cell_info["bold"]
1965
+ cell.api.Font.Italic = cell_info["italic"]
1966
+ cell.api.Font.Color = cell_info["font_color"]
1967
+ cell.api.HorizontalAlignment = cell_info["horizontal_align"]
1968
+ cell.api.VerticalAlignment = cell_info["vertical_align"]
1969
+ cell.api.NumberFormat = cell_info["number_format"]
1970
+
1971
+ # 恢复边框
1972
+ for side, border_info in cell_info["border"].items():
1973
+ border_index = {"left": 1, "right": 2, "top": 3, "bottom": 4}.get(side)
1974
+ if border_index and border_info["style"] not in [None, 0]:
1975
+ cell.api.Borders(border_index).LineStyle = border_info["style"]
1976
+ cell.api.Borders(border_index).Color = border_info["color"]
1977
+
1978
+ # 恢复对角线
1979
+ if "diagonal_up" in cell_info["border"]:
1980
+ cell.api.Borders(5).LineStyle = cell_info["border"]["diagonal_up"]["style"]
1981
+ cell.api.Borders(5).Color = cell_info["border"]["diagonal_up"]["color"]
1982
+
1983
+ if "diagonal_down" in cell_info["border"]:
1984
+ cell.api.Borders(6).LineStyle = cell_info["border"]["diagonal_down"]["style"]
1985
+ cell.api.Borders(6).Color = cell_info["border"]["diagonal_down"]["color"]
1986
+
1987
+ wb.save(new_excel)
1988
+ # 恢复合并单元格
1989
+ for merge in merged_cells:
1990
+ merge_range = merge["merge_range"]
1991
+ sheet.range(merge_range).merge()
1992
+
1993
+ wb.save(new_excel)
1994
+ close_excel(app, wb)
1995
+
1996
+ print(f"✅ 数据已成功写入 {new_excel}")
1997
+ time.sleep(2) # 这里需要一个延时
1998
+
1999
+ def safe_expand_down(sheet, start_cell='A2'):
2000
+ rng = sheet.range(start_cell)
2001
+ if not rng.value:
2002
+ return []
2003
+ try:
2004
+ return rng.expand('down')
2005
+ except Exception as e:
2006
+ log(f'safe_expand_down failed: {e}')
2007
+ return [rng] # 返回单元格本身
2008
+
2009
+ # 初始化一个表格
2010
+ # data 需要是一个二维列表
2011
+ def init_progress_ex(key_id, excel_path, sheet_name='Sheet1'):
2012
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2013
+
2014
+ # 设置标题与格式
2015
+ expected_header = ["任务ID", "处理状态(未完成|已完成)"]
2016
+ # 只在首次或不一致时写入标题
2017
+ current_header = [sheet.range('A1').value, sheet.range('B1').value]
2018
+ if current_header != expected_header:
2019
+ sheet.range('A1').value = expected_header
2020
+ sheet.range('A:A').number_format = '@'
2021
+ log('初始化表头和格式')
2022
+ else:
2023
+ log('已存在正确表头,跳过初始化')
2024
+
2025
+ # 获取已存在的 keyID(从 A2 开始向下扩展)
2026
+ used_range = safe_expand_down(sheet, 'A2')
2027
+ existing_ids = [str(c.value) for c in used_range if c.value]
2028
+
2029
+ if str(key_id) in existing_ids:
2030
+ log(f'已存在相同任务跳过: {key_id}')
2031
+ else:
2032
+ # 找到第一列最后一个非空行
2033
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2034
+ new_row = last_row + 1
2035
+ sheet.range(f'A{new_row}').value = [key_id, '']
2036
+ log(f'写入任务: {key_id}')
2037
+
2038
+ # 设置标题栏样式
2039
+ format_header_row(sheet, len(expected_header))
2040
+
2041
+ wb.save()
2042
+
2043
+ def init_data_ex(key_id, excel_path, header, sheet_name='Sheet1'):
2044
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2045
+
2046
+ # 判断是否需要写入标题和设置格式
2047
+ current_header = [sheet.range(f'{index_to_column_name(i + 1)}1').value for i in range(len(header))]
2048
+ if current_header != header:
2049
+ sheet.range('A1').value = header
2050
+ sheet.range('A:A').number_format = '@'
2051
+ log('初始化表头和格式')
2052
+ else:
2053
+ log('表头已存在,跳过初始化')
2054
+
2055
+ # 检查是否已存在相同 key_id
2056
+ existing_ids = [str(cell.value) for cell in sheet.range('A2').expand('down') if cell.value]
2057
+ if str(key_id) in existing_ids:
2058
+ log(f'已初始化主键: {key_id}')
2059
+ else:
2060
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2061
+ new_row = last_row + 1
2062
+ sheet.range(f'A{new_row}').value = [key_id, '']
2063
+ log(f'写入任务: {[key_id, ""]}')
2064
+
2065
+ # 格式化标题栏(如果是第一次设置标题)
2066
+ if current_header != header:
2067
+ format_header_row(sheet, len(header))
2068
+
2069
+ wb.save()
2070
+
2071
+ def format_header_row(sheet, column_count):
2072
+ """
2073
+ 设置标题行样式和列对齐
2074
+ """
2075
+ for col_index in range(1, column_count + 1):
2076
+ col_letter = index_to_column_name(col_index)
2077
+ cell = sheet.range(f'{col_letter}1')
2078
+
2079
+ # 设置标题样式
2080
+ cell.color = (68, 114, 196)
2081
+ cell.font.size = 12
2082
+ cell.font.bold = True
2083
+ cell.font.color = (255, 255, 255)
2084
+
2085
+ # 设置列居中对齐
2086
+ sheet.range(f'{col_letter}:{col_letter}').api.HorizontalAlignment = -4108 # xlCenter
2087
+ sheet.range(f'{col_letter}:{col_letter}').api.VerticalAlignment = -4108 # xlCenter
2088
+
2089
+ # 自动调整列宽
2090
+ sheet.range(f'{col_letter}:{col_letter}').autofit()
2091
+
2092
+ # 初始化一个表格
2093
+ # data 需要是一个二维列表
2094
+ def init_progress(excel_path, keyID, sheet_name='Sheet1'):
2095
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2096
+ # 覆盖写入标题
2097
+ sheet.range('A1').value = ["任务ID", "处理状态(未完成|已完成)"]
2098
+ # 覆盖写入数据
2099
+ sheet.range(f'A:A').number_format = '@' # 一般先设置格式再写入数据才起到效果 否则需要后触发格式
2100
+
2101
+ data = [[keyID, '']]
2102
+ for index, item in enumerate(data):
2103
+ keyID = item[0]
2104
+ status = item[1]
2105
+ flagRecord = True
2106
+ # 遍历可用行
2107
+ used_range_row = sheet.range('A1').expand('down')
2108
+ for i, cell in enumerate(used_range_row):
2109
+ row = i + 1
2110
+ if row < 2:
2111
+ continue
2112
+ rowKeyID = sheet.range(f'A{row}').value
2113
+ if str(rowKeyID) == str(keyID):
2114
+ log(f'已存在相同任务跳过: {keyID}')
2115
+ flagRecord = False
2116
+ break
2117
+ if flagRecord:
2118
+ # 获取第一列最后一个非空单元格的行号
2119
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2120
+ sheet.range(f'A{last_row + 1}').value = item
2121
+ log(f'写入任务: {item}')
2122
+
2123
+ # 处理标题栏格式
2124
+ # 遍历可用列 这个要先遍历 因为要列宽自适应 会破坏前面设置好的宽度属性
2125
+ used_range_col = sheet.range('A1').expand('right')
2126
+ for j, cell in enumerate(used_range_col):
2127
+ col = j + 1
2128
+ col_name = index_to_column_name(col)
2129
+ col_val = sheet.range(f'{col_name}1').value
2130
+ # 设置标题栏字体颜色与背景色
2131
+ sheet.range(f'{col_name}1').color = (68, 114, 196)
2132
+ sheet.range(f'{col_name}1').font.size = 12
2133
+ sheet.range(f'{col_name}1').font.bold = True
2134
+ sheet.range(f'{col_name}1').font.color = (255, 255, 255)
2135
+ # 所有列水平居中和垂直居中
2136
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4108
2137
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2138
+
2139
+ sheet.range(f'{col_name}:{col_name}').autofit()
2140
+
2141
+ wb.save()
2142
+
2143
+ def get_progress(excel_path, keyID, sheet_name="Sheet1"):
2144
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2145
+ # 遍历可用行
2146
+ used_range_row = sheet.range('A1').expand('down')
2147
+ for i, cell in enumerate(used_range_row):
2148
+ row = i + 1
2149
+ if row < 2:
2150
+ continue
2151
+ rowKeyID = sheet.range(f'A{row}').value;
2152
+ if rowKeyID == keyID:
2153
+ result = sheet.range(f'B{row}').value;
2154
+ if result == "已完成":
2155
+ return True
2156
+ else:
2157
+ return False
2158
+
2159
+ def get_progress_ex(keyID, excel_path, sheet_name="Sheet1"):
2160
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2161
+ # 遍历可用行
2162
+ used_range_row = sheet.range('A1').expand('down')
2163
+ for i, cell in enumerate(used_range_row):
2164
+ row = i + 1
2165
+ if row < 2:
2166
+ continue
2167
+ rowKeyID = sheet.range(f'A{row}').value;
2168
+ if rowKeyID == keyID:
2169
+ result = sheet.range(f'B{row}').value;
2170
+ if result == "已完成":
2171
+ return True
2172
+ else:
2173
+ return False
2174
+ close_excel(app, wb)
2175
+
2176
+ def get_progress_data(excel_path, keyID, sheet_name="Sheet1"):
2177
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2178
+ # 遍历可用行
2179
+ used_range_row = sheet.range('A1').expand('down')
2180
+ for i, cell in enumerate(used_range_row):
2181
+ row = i + 1
2182
+ if row < 2:
2183
+ continue
2184
+ rowKeyID = sheet.range(f'A{row}').value
2185
+ if rowKeyID == keyID:
2186
+ result = sheet.range(f'C{row}').value
2187
+ return result
2188
+ return None
2189
+
2190
+ def get_progress_data_ex(keyID, excel_path, sheet_name="Sheet1"):
2191
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2192
+ # 遍历可用行
2193
+ used_range_row = sheet.range('A1').expand('down')
2194
+ for i, cell in enumerate(used_range_row):
2195
+ row = i + 1
2196
+ if row < 2:
2197
+ continue
2198
+ rowKeyID = sheet.range(f'A{row}').value
2199
+ if rowKeyID == keyID:
2200
+ result = sheet.range(f'C{row}').value
2201
+ return result
2202
+ return None
2203
+
2204
+ def set_progress(excel_path, keyID, status='已完成', sheet_name="Sheet1"):
2205
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2206
+ # 遍历可用行
2207
+ used_range_row = sheet.range('A1').expand('down')
2208
+ for i, cell in enumerate(used_range_row):
2209
+ row = i + 1
2210
+ if row < 2:
2211
+ continue
2212
+ rowKeyID = sheet.range(f'A{row}').value
2213
+ if str(rowKeyID) == str(keyID):
2214
+ sheet.range(f'B{row}').value = status
2215
+ wb.save()
2216
+ return
2217
+
2218
+ def set_progress_ex(keyID, excel_path, status='已完成', sheet_name="Sheet1"):
2219
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2220
+ # 遍历可用行
2221
+ used_range_row = sheet.range('A1').expand('down')
2222
+ for i, cell in enumerate(used_range_row):
2223
+ row = i + 1
2224
+ if row < 2:
2225
+ continue
2226
+ rowKeyID = sheet.range(f'A{row}').value
2227
+ if str(rowKeyID) == str(keyID):
2228
+ sheet.range(f'B{row}').value = status
2229
+ wb.save()
2230
+ close_excel(app, wb)
2231
+ return
2232
+ close_excel(app, wb)
2233
+
2234
+ def set_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
2235
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2236
+ # 遍历可用行
2237
+ used_range_row = sheet.range('A1').expand('down')
2238
+ for i, cell in enumerate(used_range_row):
2239
+ row = i + 1
2240
+ if row < 2:
2241
+ continue
2242
+ rowKeyID = sheet.range(f'A{row}').value
2243
+ if str(rowKeyID) == str(keyID):
2244
+ sheet.range(f'A{row}').value = data
2245
+ wb.save()
2246
+ return
2247
+
2248
+ def set_progress_data(excel_path, keyID, data, sheet_name="Sheet1"):
2249
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2250
+ # 遍历可用行
2251
+ used_range_row = sheet.range('A1').expand('down')
2252
+ for i, cell in enumerate(used_range_row):
2253
+ row = i + 1
2254
+ if row < 2:
2255
+ continue
2256
+ rowKeyID = sheet.range(f'A{row}').value
2257
+ if str(rowKeyID) == str(keyID):
2258
+ log('设置数据', data)
2259
+ sheet.range(f'C{row}').value = data
2260
+ wb.save()
2261
+ return
2262
+
2263
+ def set_progress_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
2264
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2265
+ # 遍历可用行
2266
+ used_range_row = sheet.range('A1').expand('down')
2267
+ for i, cell in enumerate(used_range_row):
2268
+ row = i + 1
2269
+ if row < 2:
2270
+ continue
2271
+ rowKeyID = sheet.range(f'A{row}').value
2272
+ if str(rowKeyID) == str(keyID):
2273
+ log('设置数据', data)
2274
+ sheet.range(f'C{row}').value = data
2275
+ wb.save()
2276
+ return
2277
+
2278
+ def check_progress(excel_path, listKeyID, sheet_name="Sheet1"):
2279
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2280
+ # 读取整个任务表数据
2281
+ data = sheet.used_range.value
2282
+ data = [row for row in data if any(row)] # 过滤掉空行
2283
+ # 任务ID和状态列索引
2284
+ task_id_col = 0
2285
+ status_col = 1
2286
+ # 创建任务ID与状态的字典
2287
+ task_status_dict = {row[task_id_col]: row[status_col] for row in data[1:] if row[task_id_col]}
2288
+ # 找出未完成的任务
2289
+ incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
2290
+ return len(incomplete_tasks) == 0, incomplete_tasks
2291
+
2292
+ def check_progress_ex(listKeyID, excel_path, sheet_name="Sheet1"):
2293
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2294
+ # 读取整个任务表数据
2295
+ data = sheet.used_range.value
2296
+ data = [row for row in data if any(row)] # 过滤掉空行
2297
+ # 任务ID和状态列索引
2298
+ task_id_col = 0
2299
+ status_col = 1
2300
+ # 创建任务ID与状态的字典
2301
+ task_status_dict = {row[task_id_col]: row[status_col] for row in data[1:] if row[task_id_col]}
2302
+ # 找出未完成的任务
2303
+ incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
2304
+ return len(incomplete_tasks) == 0, incomplete_tasks
2305
+
2306
+ def read_excel_sheet_to_list(file_path, sheet_name=None):
2307
+ """
2308
+ 使用 xlwings 读取 Excel 文件中指定工作表的数据,并返回为二维列表。
2309
+
2310
+ :param file_path: Excel 文件路径
2311
+ :param sheet_name: 要读取的 sheet 名称(默认读取第一个 sheet)
2312
+ :return: 二维列表形式的数据
2313
+ """
2314
+ app, wb, sheet = open_excel(file_path, sheet_name)
2315
+ used_range = sheet.used_range
2316
+ data = used_range.value # 返回为二维列表或一维列表(取决于数据)
2317
+ close_excel(app, wb)
2318
+ time.sleep(2)
2319
+ # 保证返回的是二维列表
2320
+ if not data:
2321
+ return []
2322
+ elif isinstance(data[0], list):
2323
+ return data
2324
+ else:
2325
+ return [data]
2326
+
2327
+ def excel_to_dict(excel_path, column_key, column_value, sheet_name=None):
2328
+ """
2329
+ 从 Excel 文件中读取指定两列,生成字典返回(不受中间空行影响)
2330
+
2331
+ :param excel_path: Excel 文件路径
2332
+ :param column_key: 键所在列,比如 'A' 或 1(从1开始)
2333
+ :param column_value: 值所在列,比如 'B' 或 2
2334
+ :param sheet_name: 可选,指定sheet名称,默认第一个sheet
2335
+ :return: dict
2336
+ """
2337
+ app = xw.App(visible=False)
2338
+ wb = None
2339
+ try:
2340
+ wb = app.books.open(excel_path)
2341
+ sheet = wb.sheets[sheet_name] if sheet_name else wb.sheets[0]
2342
+
2343
+ # 如果列是数字,转为列字母
2344
+ if isinstance(column_key, int):
2345
+ column_key = xw.utils.col_name(column_key)
2346
+ if isinstance(column_value, int):
2347
+ column_value = xw.utils.col_name(column_value)
2348
+
2349
+ # 获取 used range 的总行数
2350
+ used_rows = sheet.used_range.last_cell.row
2351
+
2352
+ # 获取整列值(从第2行开始,跳过标题)
2353
+ keys = sheet.range(f'{column_key}2:{column_key}{used_rows}').value
2354
+ values = sheet.range(f'{column_value}2:{column_value}{used_rows}').value
2355
+
2356
+ # 容错:如果只有一个值会变成单个元素,需变成列表
2357
+ if not isinstance(keys, list):
2358
+ keys = [keys]
2359
+ if not isinstance(values, list):
2360
+ values = [values]
2361
+
2362
+ # 构建字典,忽略空键
2363
+ result = {
2364
+ str(k).strip().lower(): (str(v).strip() if v is not None else '-')
2365
+ for k, v in zip(keys, values)
2366
+ if k is not None and str(k).strip() != ""
2367
+ }
2368
+ return result
2369
+ finally:
2370
+ if wb is not None:
2371
+ wb.close()
2372
+ app.quit()
2373
+
2374
+ def format_to_text_v2(sheet, columns=None):
2375
+ if columns is None or len(columns) == 0:
2376
+ return
2377
+ for col_name in columns:
2378
+ if isinstance(col_name, int):
2379
+ col_letter = xw.utils.col_name(col_name)
2380
+ else:
2381
+ # 尝试通过列名查找列字母
2382
+ col_letter = find_column_by_data(sheet, 1, col_name)
2383
+ if col_letter is None:
2384
+ log(f'未找到列名[{col_name}],跳过文本格式设置')
2385
+ continue
2386
+ log(f'设置[{col_name}] => [{col_letter}] 文本格式')
2387
+ sheet.range(f'{col_letter}:{col_letter}').number_format = '@'
2388
+
2389
+ def format_to_text_v2_safe(sheet, columns=None, data_rows=None):
2390
+ """
2391
+ 更安全的文本格式化函数,避免COM异常
2392
+
2393
+ Args:
2394
+ sheet: Excel工作表对象
2395
+ columns: 要格式化的列名列表
2396
+ data_rows: 数据行数,用于限制格式化范围
2397
+ """
2398
+ if columns is None or len(columns) == 0:
2399
+ return
2400
+
2401
+ # 确保columns是列表
2402
+ if not isinstance(columns, list):
2403
+ columns = [columns]
2404
+
2405
+ for col_name in columns:
2406
+ try:
2407
+ if isinstance(col_name, int):
2408
+ col_name = xw.utils.col_name(col_name)
2409
+
2410
+ log(f'安全设置[{col_name}] 文本格式')
2411
+
2412
+ # 如果指定了数据行数,只格式化有数据的范围
2413
+ if data_rows and data_rows > 0:
2414
+ # 格式化从第1行到数据行数的范围
2415
+ range_str = f'{col_name}1:{col_name}{data_rows}'
2416
+ sheet.range(range_str).number_format = '@'
2417
+ else:
2418
+ # 检查列是否有数据,如果没有则跳过
2419
+ try:
2420
+ # 先检查第一个单元格是否存在
2421
+ test_range = sheet.range(f'{col_name}1')
2422
+ if test_range.value is not None or sheet.used_range.last_cell.column >= column_name_to_index(col_name) + 1:
2423
+ sheet.range(f'{col_name}:{col_name}').number_format = '@'
2424
+ else:
2425
+ log(f'列 {col_name} 没有数据,跳过格式化')
2426
+ except:
2427
+ log(f'列 {col_name} 格式化失败,跳过')
2428
+
2429
+ except Exception as e:
2430
+ log(f'设置列 {col_name} 文本格式失败: {e},继续处理其他列')
2431
+
2432
+ def pre_format_columns_safe(sheet, columns, data_rows):
2433
+ """
2434
+ 预格式化函数:在写入数据前安全地设置列格式
2435
+
2436
+ Args:
2437
+ sheet: Excel工作表对象
2438
+ columns: 要格式化的列名列表
2439
+ data_rows: 预期数据行数
2440
+ """
2441
+ if not columns or not isinstance(columns, list):
2442
+ return
2443
+
2444
+ for col_name in columns:
2445
+ try:
2446
+ if isinstance(col_name, int):
2447
+ col_name = xw.utils.col_name(col_name)
2448
+
2449
+ log(f'预格式化列 [{col_name}] 为文本格式')
2450
+
2451
+ # 方法1:先创建最小范围,避免整列操作
2452
+ try:
2453
+ # 创建足够大的范围来覆盖预期数据
2454
+ range_str = f'{col_name}1:{col_name}{max(data_rows, 1000)}'
2455
+ sheet.range(range_str).number_format = '@'
2456
+ log(f'预格式化成功: {range_str}')
2457
+ except Exception as e1:
2458
+ log(f'预格式化方法1失败: {e1}')
2459
+
2460
+ # 方法2:逐行设置格式,更安全但稍慢
2461
+ try:
2462
+ for row in range(1, data_rows + 1):
2463
+ cell = sheet.range(f'{col_name}{row}')
2464
+ cell.number_format = '@'
2465
+ log(f'逐行预格式化成功: {col_name}')
2466
+ except Exception as e2:
2467
+ log(f'逐行预格式化也失败: {e2}')
2468
+
2469
+ except Exception as e:
2470
+ log(f'预格式化列 {col_name} 失败: {e},继续处理其他列')
2471
+
2472
+ def post_format_columns_safe(sheet, columns, data_rows):
2473
+ """
2474
+ 后格式化函数:在写入数据后确认列格式并强制转换为文本
2475
+
2476
+ Args:
2477
+ sheet: Excel工作表对象
2478
+ columns: 要格式化的列名列表
2479
+ data_rows: 实际数据行数
2480
+ """
2481
+ if not columns or not isinstance(columns, list):
2482
+ return
2483
+
2484
+ for col_name in columns:
2485
+ try:
2486
+ if isinstance(col_name, int):
2487
+ col_name = xw.utils.col_name(col_name)
2488
+
2489
+ log(f'后格式化列 [{col_name}] 为文本格式')
2490
+
2491
+ # 只对实际有数据的行进行格式化
2492
+ if data_rows > 0:
2493
+ range_str = f'{col_name}1:{col_name}{data_rows}'
2494
+ target_range = sheet.range(range_str)
2495
+
2496
+ # 设置格式为文本
2497
+ target_range.number_format = '@'
2498
+
2499
+ # 关键步骤:读取数据并重新写入,触发文本转换
2500
+ # 这样可以将已经写入的数字转换为文本格式
2501
+ values = target_range.value
2502
+ if values is not None:
2503
+ # 处理单个值的情况
2504
+ if not isinstance(values, list):
2505
+ if values != '':
2506
+ target_range.value = str(values)
2507
+ # 处理列表的情况(单列多行)
2508
+ elif len(values) > 0:
2509
+ # 检查是否是二维数组(实际上单列应该是一维数组)
2510
+ if isinstance(values[0], list):
2511
+ # 二维数组,取第一列
2512
+ converted_values = [[str(row[0]) if row[0] is not None and row[0] != '' else row[0]] for row in values]
2513
+ else:
2514
+ # 一维数组
2515
+ converted_values = [[str(val)] if val is not None and val != '' else [val] for val in values]
2516
+ # 重新写入(这次会按照文本格式写入)
2517
+ target_range.value = converted_values
2518
+
2519
+ log(f'后格式化并转换成功: {range_str}')
2520
+
2521
+ except Exception as e:
2522
+ log(f'后格式化列 {col_name} 失败: {e},继续处理其他列')
2523
+
2524
+ def format_to_text(sheet, columns=None):
2525
+ if columns is None:
2526
+ return
2527
+ used_range_col = sheet.range('A1').expand('right')
2528
+ for j, cell in enumerate(used_range_col):
2529
+ col = j + 1
2530
+ col_name = index_to_column_name(col)
2531
+ col_val = sheet.range(f'{col_name}1').value
2532
+ for c in columns:
2533
+ if str(c).lower() in str(col_val).lower():
2534
+ log(f'设置[{c}] 文本格式')
2535
+ sheet.range(f'{col_name}:{col_name}').number_format = '@'
2536
+
2537
+ def format_to_date(sheet, columns=None):
2538
+ if columns is None:
2539
+ return
2540
+ used_range_col = sheet.range('A1').expand('right')
2541
+ for j, cell in enumerate(used_range_col):
2542
+ col = j + 1
2543
+ col_name = index_to_column_name(col)
2544
+ col_val = sheet.range(f'{col_name}1').value
2545
+ if col_val is None:
2546
+ continue
2547
+ for c in columns:
2548
+ if c in col_val:
2549
+ log(f'设置[{c}] 时间格式')
2550
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd'
2551
+
2552
+ def format_to_datetime(sheet, columns=None):
2553
+ if columns is None:
2554
+ return
2555
+ used_range_col = sheet.range('A1').expand('right')
2556
+ for j, cell in enumerate(used_range_col):
2557
+ col = j + 1
2558
+ col_name = index_to_column_name(col)
2559
+ col_val = sheet.range(f'{col_name}1').value
2560
+ if col_val is None:
2561
+ continue
2562
+ for c in columns:
2563
+ if c in col_val:
2564
+ log(f'设置[{c}] 时间格式')
2565
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd hh:mm:ss'
2566
+
2567
+ def format_to_month(sheet, columns=None):
2568
+ if columns is None:
2569
+ return
2570
+ used_range_col = sheet.range('A1').expand('right')
2571
+ for j, cell in enumerate(used_range_col):
2572
+ col = j + 1
2573
+ col_name = index_to_column_name(col)
2574
+ col_val = sheet.range(f'{col_name}1').value
2575
+ for c in columns:
2576
+ if c in col_val:
2577
+ log(f'设置[{c}] 年月格式')
2578
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm'
2579
+
2580
+ def add_sum_for_cell(sheet, col_list, row=2):
2581
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2582
+ if last_row > row:
2583
+ for col_name in col_list:
2584
+ col_letter = find_column_by_data(sheet, 1, col_name)
2585
+ sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
2586
+ sheet.range(f'{col_letter}{row}').api.Font.Color = 255
2587
+ sheet.range(f'{col_letter}:{col_letter}').autofit()
2588
+
2589
+ def clear_for_cell(sheet, col_list, row=2):
2590
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2591
+ for col_name in col_list:
2592
+ col_letter = find_column_by_data(sheet, 1, col_name)
2593
+ sheet.range(f'{col_letter}{row}').value = ''
2594
+
2595
+ def color_for_column(sheet, col_list, color_name, start_row=2):
2596
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2597
+ for col_name in col_list:
2598
+ col_letter = find_column_by_data(sheet, 1, col_name)
2599
+ if last_row > start_row:
2600
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api.Font.ColorIndex = excel_color_index[
2601
+ color_name]
2602
+
2603
+ def add_formula_for_column(sheet, col_name, formula, start_row=2):
2604
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2605
+ col_letter = find_column_by_data(sheet, 1, col_name)
2606
+ if last_row >= start_row:
2607
+ # 第3行公式(填一次)
2608
+ sheet.range(f'{col_letter}{start_row}').formula = formula
2609
+ if '汇总' in col_name:
2610
+ sheet.range(f'{col_letter}{start_row}').api.Font.Color = 255
2611
+ if last_row > start_row:
2612
+ # AutoFill 快速填充到所有行(start_row 到 last_row)
2613
+ sheet.range(f'{col_letter}{start_row}').api.AutoFill(
2614
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api)
2615
+ sheet.range(f'{col_letter}:{col_letter}').autofit()
2616
+
2617
+ def autofit_column(sheet, columns=None):
2618
+ if columns is None:
2619
+ return
2620
+ used_range_col = sheet.range('A1').expand('right')
2621
+ for j, cell in enumerate(used_range_col):
2622
+ col = j + 1
2623
+ col_name = index_to_column_name(col)
2624
+ col_val = sheet.range(f'{col_name}1').value
2625
+ if col_val is None:
2626
+ continue
2627
+ for c in columns:
2628
+ if c in col_val:
2629
+ log(f'设置[{c}] 宽度自适应')
2630
+ sheet.range(f'{col_name}:{col_name}').api.WrapText = False
2631
+ sheet.range(f'{col_name}:{col_name}').autofit()
2632
+ sheet.range(f'{col_name}:{col_name}').api.WrapText = True
2633
+ sheet.range(f'{col_name}:{col_name}').autofit()
2634
+
2635
+ def specify_column_width(sheet, columns=None, width=150):
2636
+ if columns is None:
2637
+ return
2638
+ used_range_col = sheet.range('A1').expand('right')
2639
+ for j, cell in enumerate(used_range_col):
2640
+ col = j + 1
2641
+ col_name = index_to_column_name(col)
2642
+ col_val = sheet.range(f'{col_name}1').value
2643
+ if col_val is None:
2644
+ continue
2645
+ for c in columns:
2646
+ if c in col_val:
2647
+ log(f'设置[{c}]宽度: {width}')
2648
+ sheet.range(f'{col_name}:{col_name}').column_width = width
2649
+
2650
+ def format_to_money(sheet, columns=None):
2651
+ if columns is None:
2652
+ return
2653
+ used_range_col = sheet.range('A1').expand('right')
2654
+ for j, cell in enumerate(used_range_col):
2655
+ col = j + 1
2656
+ col_name = index_to_column_name(col)
2657
+ col_val = sheet.range(f'{col_name}1').value
2658
+ if col_val is None:
2659
+ continue
2660
+ for c in columns:
2661
+ if c in col_val:
2662
+ log(f'设置[{c}] 金额格式')
2663
+ sheet.range(f'{col_name}:{col_name}').number_format = '¥#,##0.00'
2664
+
2665
+ def format_to_percent(sheet, columns=None, decimal_places=2):
2666
+ if columns is None:
2667
+ return
2668
+ used_range_col = sheet.range('A1').expand('right')
2669
+ for j, cell in enumerate(used_range_col):
2670
+ col = j + 1
2671
+ col_name = index_to_column_name(col)
2672
+ col_val = sheet.range(f'{col_name}1').value
2673
+ if col_val is None:
2674
+ continue
2675
+ for c in columns:
2676
+ if c in col_val:
2677
+ log(f'设置[{c}] 百分比格式')
2678
+ # 根据 decimal_places 决定格式
2679
+ if decimal_places == 0:
2680
+ sheet.range(f'{col_name}:{col_name}').number_format = '0%'
2681
+ else:
2682
+ sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}%'
2683
+
2684
+ def format_to_number(sheet, columns=None, decimal_places=2):
2685
+ if not columns or not isinstance(columns, (list, tuple, set)):
2686
+ log(f'未提供有效列名列表({columns}),跳过格式转换')
2687
+ return
2688
+
2689
+ decimal_places = max(0, int(decimal_places)) # 确保非负整数
2690
+ used_range_col = sheet.range('A1').expand('right')
2691
+
2692
+ for j, cell in enumerate(used_range_col):
2693
+ col = j + 1
2694
+ col_name = index_to_column_name(col)
2695
+ col_val = sheet.range(f'{col_name}1').value
2696
+
2697
+ if col_val is None:
2698
+ continue
2699
+
2700
+ col_val = str(col_val) # 确保转为字符串比较
2701
+ for c in columns:
2702
+ if c in col_val:
2703
+ log(f'设置 [{c}] 列为数字格式,小数位 {decimal_places}')
2704
+ number_format = '0' if decimal_places == 0 else f'0.{"0" * decimal_places}'
2705
+ sheet.range(f'{col_name}:{col_name}').number_format = number_format
2706
+ break # 如果一列只匹配一个关键词可提前退出
2707
+
2708
+ # def format_to_number(sheet, columns=None, decimal_places=2):
2709
+ # if columns is None or not isinstance(columns, list):
2710
+ # log('跳过格式化成数字', columns)
2711
+ # return
2712
+ # used_range_col = sheet.range('A1').expand('right')
2713
+ # for j, cell in enumerate(used_range_col):
2714
+ # col = j + 1
2715
+ # col_name = index_to_column_name(col)
2716
+ # col_val = sheet.range(f'{col_name}1').value
2717
+ # if col_val is None:
2718
+ # continue
2719
+ # for c in columns:
2720
+ # if c in col_val:
2721
+ # log(f'设置[{c}] 数字格式')
2722
+ # # 根据 decimal_places 决定格式
2723
+ # if decimal_places == 0:
2724
+ # sheet.range(f'{col_name}:{col_name}').number_format = '0'
2725
+ # else:
2726
+ # sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}'
2727
+
2728
+ def hidden_columns(sheet, columns=None):
2729
+ if columns is None:
2730
+ return
2731
+ used_range_col = sheet.range('A1').expand('right')
2732
+ for j, cell in enumerate(used_range_col):
2733
+ col = j + 1
2734
+ col_name = index_to_column_name(col)
2735
+ col_val = sheet.range(f'{col_name}1').value
2736
+ if col_val is None:
2737
+ continue
2738
+ for c in columns:
2739
+ if c in col_val:
2740
+ log(f'设置[{c}] 隐藏')
2741
+ sheet.range(f'{col_name}:{col_name}').column_width = 0
2742
+
2743
+ def column_to_right(sheet, columns=None):
2744
+ if columns is None:
2745
+ return
2746
+ used_range_col = sheet.range('A1').expand('right')
2747
+ for j, cell in enumerate(used_range_col):
2748
+ col = j + 1
2749
+ col_name = index_to_column_name(col)
2750
+ col_val = sheet.range(f'{col_name}1').value
2751
+ if col_val is None:
2752
+ continue
2753
+ for c in columns:
2754
+ if c in col_val:
2755
+ # 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
2756
+ # 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
2757
+ # 所有列水平居中和垂直居中
2758
+ log(f'设置[{c}] 水平右对齐')
2759
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4152
2760
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2761
+
2762
+ def column_to_left(sheet, columns=None):
2763
+ if columns is None:
2764
+ return
2765
+ used_range_col = sheet.range('A1').expand('right')
2766
+ for j, cell in enumerate(used_range_col):
2767
+ col = j + 1
2768
+ col_name = index_to_column_name(col)
2769
+ col_val = sheet.range(f'{col_name}1').value
2770
+ if col_val is None:
2771
+ continue
2772
+ for c in columns:
2773
+ if c in col_val:
2774
+ # 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
2775
+ # 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
2776
+ # 所有列水平居中和垂直居中
2777
+ log(f'设置[{c}] 左对齐')
2778
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4131
2779
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2780
+
2781
+ def beautify_title(sheet):
2782
+ log('美化标题')
2783
+ used_range_col = sheet.range('A1').expand('right')
2784
+ for j, cell in enumerate(used_range_col):
2785
+ col = j + 1
2786
+ col_name = index_to_column_name(col)
2787
+
2788
+ # 设置标题栏字体颜色与背景色
2789
+ sheet.range(f'{col_name}1').color = (68, 114, 196)
2790
+ sheet.range(f'{col_name}1').font.size = 12
2791
+ sheet.range(f'{col_name}1').font.bold = True
2792
+ sheet.range(f'{col_name}1').font.color = (255, 255, 255)
2793
+
2794
+ # 所有列水平居中和垂直居中
2795
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4108
2796
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2797
+ sheet.autofit()
2798
+
2799
+ def set_body_style(sheet, row_start, row_end=None):
2800
+ if row_end is None:
2801
+ row_end = get_last_used_row(sheet)
2802
+
2803
+ range = sheet.range(f'{row_start}:{row_end}')
2804
+ # 设置字体名称
2805
+ range.font.name = 'Calibri'
2806
+ # 设置字体大小
2807
+ range.font.size = 11
2808
+
2809
+ def set_title_style(sheet, rows=2):
2810
+ col = get_max_column_letter(sheet)
2811
+ range = sheet.range(f'A1:{col}{rows}')
2812
+ # 设置字体名称
2813
+ range.font.name = '微软雅黑'
2814
+ # 设置字体大小
2815
+ range.font.size = 11
2816
+ # 设置字体加粗
2817
+ range.font.bold = True
2818
+ # 设置标题栏字体颜色与背景色
2819
+ range.color = (252, 228, 214)
2820
+ # 设置行高
2821
+ range.row_height = 30
2822
+
2823
+ # 获取已使用范围
2824
+ used_range = sheet.used_range
2825
+ # 设置水平居中对齐
2826
+ used_range.api.HorizontalAlignment = xw.constants.HAlign.xlHAlignCenter
2827
+ used_range.api.VerticalAlignment = xw.constants.HAlign.xlHAlignCenter
2828
+
2829
+ sheet.autofit()
2830
+
2831
+ def move_sheet_to_position(wb, sheet_name, position):
2832
+ # 获取要移动的工作表
2833
+ sheet = wb.sheets[sheet_name]
2834
+ # 获取目标位置的参考工作表
2835
+ if position == 1:
2836
+ # 如果目标位置是第一个,将其移至最前
2837
+ sheet.api.Move(Before=wb.sheets[0].api)
2838
+ else:
2839
+ # 如果目标位置不是第一个,将其移至目标位置之前
2840
+ sheet.api.Move(Before=wb.sheets[position - 1].api)
2841
+ # 保存工作簿
2842
+ wb.save()
2843
+
2844
+ # Excel 文件锁管理器
2845
+ import threading
2846
+ import time
2847
+ from collections import defaultdict
2848
+
2849
+ class ExcelFileLockManager:
2850
+ """Excel 文件锁管理器,用于管理不同 Excel 文件的并发访问"""
2851
+
2852
+ def __init__(self):
2853
+ self._locks = defaultdict(threading.Lock)
2854
+ self._excel_instances = {} # 存储已打开的 Excel 实例
2855
+ self._lock = threading.Lock() # 保护内部数据结构的锁
2856
+ self._waiting_queue = defaultdict(list) # 等待队列,按文件路径分组
2857
+ self._operation_count = defaultdict(int) # 记录每个文件的操作次数
2858
+ self._max_wait_time = 300 # 最大等待时间(秒)
2859
+
2860
+ def get_file_lock(self, excel_path):
2861
+ """获取指定 Excel 文件的锁"""
2862
+ return self._locks[excel_path]
2863
+
2864
+ def acquire_excel_lock(self, excel_path, timeout=30, priority=0):
2865
+ """
2866
+ 获取 Excel 文件锁,支持超时和优先级
2867
+
2868
+ Args:
2869
+ excel_path: Excel 文件路径
2870
+ timeout: 超时时间(秒)
2871
+ priority: 优先级,数字越小优先级越高
2872
+
2873
+ Returns:
2874
+ bool: 是否成功获取锁
2875
+ """
2876
+ lock = self.get_file_lock(excel_path)
2877
+
2878
+ # 记录等待请求
2879
+ with self._lock:
2880
+ self._waiting_queue[excel_path].append({
2881
+ 'priority' : priority,
2882
+ 'timestamp': time.time(),
2883
+ 'thread_id': threading.get_ident()
2884
+ })
2885
+ # 按优先级排序
2886
+ self._waiting_queue[excel_path].sort(key=lambda x: (x['priority'], x['timestamp']))
2887
+
2888
+ try:
2889
+ acquired = lock.acquire(timeout=timeout)
2890
+ if acquired:
2891
+ # 记录操作次数
2892
+ with self._lock:
2893
+ self._operation_count[excel_path] += 1
2894
+ # 从等待队列中移除
2895
+ self._waiting_queue[excel_path] = [
2896
+ item for item in self._waiting_queue[excel_path]
2897
+ if item['thread_id'] != threading.get_ident()
2898
+ ]
2899
+ log(f"成功获取 Excel 文件锁: {os.path.basename(excel_path)} (优先级: {priority})")
2900
+ return True
2901
+ else:
2902
+ log(f"获取 Excel 文件锁超时: {excel_path} (优先级: {priority})")
2903
+ return False
2904
+ except Exception as e:
2905
+ log(f"获取 Excel 文件锁异常: {e}")
2906
+ return False
2907
+
2908
+ def release_excel_lock(self, excel_path):
2909
+ """释放 Excel 文件锁"""
2910
+ lock = self.get_file_lock(excel_path)
2911
+ if lock.locked():
2912
+ lock.release()
2913
+ log(f"释放 Excel 文件锁: {os.path.basename(excel_path)}")
2914
+
2915
+ def get_excel_instance(self, excel_path):
2916
+ """获取已打开的 Excel 实例"""
2917
+ with self._lock:
2918
+ return self._excel_instances.get(excel_path)
2919
+
2920
+ def set_excel_instance(self, excel_path, app, wb):
2921
+ """设置 Excel 实例"""
2922
+ with self._lock:
2923
+ self._excel_instances[excel_path] = (app, wb)
2924
+
2925
+ def remove_excel_instance(self, excel_path):
2926
+ """移除 Excel 实例"""
2927
+ with self._lock:
2928
+ self._excel_instances.pop(excel_path, None)
2929
+
2930
+ def is_excel_open(self, excel_path):
2931
+ """检查 Excel 文件是否已打开"""
2932
+ return excel_path in self._excel_instances
2933
+
2934
+ def get_waiting_count(self, excel_path):
2935
+ """获取等待该文件的线程数量"""
2936
+ with self._lock:
2937
+ return len(self._waiting_queue[excel_path])
2938
+
2939
+ def get_operation_count(self, excel_path):
2940
+ """获取该文件的操作次数"""
2941
+ with self._lock:
2942
+ return self._operation_count[excel_path]
2943
+
2944
+ def cleanup_old_instances(self, max_age=3600):
2945
+ """清理过期的 Excel 实例"""
2946
+ current_time = time.time()
2947
+ with self._lock:
2948
+ expired_files = []
2949
+ for excel_path, (app, wb) in self._excel_instances.items():
2950
+ # 这里可以添加更复杂的清理逻辑
2951
+ # 比如检查文件最后访问时间等
2952
+ pass
2953
+
2954
+ # 全局 Excel 文件锁管理器实例
2955
+ excel_lock_manager = ExcelFileLockManager()
2956
+
2957
+ def open_excel_with_lock(excel_path, sheet_name='Sheet1', timeout=30):
2958
+ """
2959
+ 带锁的 Excel 打开函数,支持复用已打开的实例
2960
+
2961
+ Args:
2962
+ excel_path: Excel 文件路径
2963
+ sheet_name: 工作表名称
2964
+ timeout: 获取锁的超时时间(秒)
2965
+
2966
+ Returns:
2967
+ tuple: (app, wb, sheet) 或 (None, None, None) 如果失败
2968
+ """
2969
+ if not excel_lock_manager.acquire_excel_lock(excel_path, timeout):
2970
+ return None, None, None
2971
+
2972
+ try:
2973
+ # 检查是否已有打开的实例
2974
+ existing_instance = excel_lock_manager.get_excel_instance(excel_path)
2975
+ if existing_instance:
2976
+ app, wb = existing_instance
2977
+ # 检查工作簿是否仍然有效
2978
+ try:
2979
+ if wb.name in [book.name for book in app.books]:
2980
+ # 获取指定的工作表
2981
+ if isinstance(sheet_name, int):
2982
+ if 0 <= sheet_name < len(wb.sheets):
2983
+ sheet = wb.sheets[sheet_name]
2984
+ else:
2985
+ sheet = wb.sheets.add(after=wb.sheets[-1])
2986
+ elif isinstance(sheet_name, str):
2987
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
2988
+ if sheet_name.strip().lower() in sheet_names:
2989
+ sheet = wb.sheets[sheet_name]
2990
+ else:
2991
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2992
+ else:
2993
+ raise ValueError(f"sheet_name 必须是字符串或整数: {sheet_name}")
2994
+
2995
+ sheet.activate()
2996
+ log(f"复用已打开的 Excel: {os.path.basename(excel_path)} {sheet.name}")
2997
+ return app, wb, sheet
2998
+ except Exception as e:
2999
+ log(f"复用 Excel 实例失败,重新打开: {e}")
3000
+ # 移除无效的实例
3001
+ excel_lock_manager.remove_excel_instance(excel_path)
3002
+
3003
+ # 打开新的 Excel 实例
3004
+ app, wb, sheet = open_excel(excel_path, sheet_name)
3005
+ if app and wb:
3006
+ excel_lock_manager.set_excel_instance(excel_path, app, wb)
3007
+ log(f"打开新的 Excel 实例: {os.path.basename(excel_path)} {sheet.name}")
3008
+
3009
+ return app, wb, sheet
3010
+
3011
+ except Exception as e:
3012
+ log(f"打开 Excel 失败: {e}")
3013
+ excel_lock_manager.release_excel_lock(excel_path)
3014
+ return None, None, None
3015
+
3016
+ def close_excel_with_lock(excel_path, app, wb, force_close=False):
3017
+ """
3018
+ 带锁的 Excel 关闭函数
3019
+
3020
+ Args:
3021
+ excel_path: Excel 文件路径
3022
+ app: Excel 应用实例
3023
+ wb: 工作簿实例
3024
+ force_close: 是否强制关闭(即使有其他操作在进行)
3025
+ """
3026
+ try:
3027
+ if force_close:
3028
+ # 强制关闭,移除实例记录
3029
+ excel_lock_manager.remove_excel_instance(excel_path)
3030
+ close_excel(app, wb)
3031
+ else:
3032
+ # 只保存,不关闭
3033
+ if wb:
3034
+ wb.save()
3035
+ log(f"保存 Excel 文件: {os.path.basename(excel_path)}")
3036
+ except Exception as e:
3037
+ log(f"关闭 Excel 失败: {e}")
3038
+ finally:
3039
+ excel_lock_manager.release_excel_lock(excel_path)
3040
+
3041
+ def write_data_with_lock(excel_path, sheet_name, data, format_to_text_colunm=None):
3042
+ """
3043
+ 带锁的数据写入函数,复用 Excel 实例
3044
+
3045
+ Args:
3046
+ excel_path: Excel 文件路径
3047
+ sheet_name: 工作表名称
3048
+ data: 要写入的数据
3049
+ format_to_text_colunm: 格式化为文本的列
3050
+ """
3051
+ app, wb, sheet = open_excel_with_lock(excel_path, sheet_name)
3052
+ if not app or not wb or not sheet:
3053
+ log(f"无法打开 Excel 文件: {excel_path}")
3054
+ return False
3055
+
3056
+ try:
3057
+ # 清空工作表中的所有数据
3058
+ sheet.clear()
3059
+ # 某些列以文本格式写入
3060
+ format_to_text_v2(sheet, format_to_text_colunm)
3061
+ # 写入数据
3062
+ sheet.range('A1').value = data
3063
+ # 保存
3064
+ wb.save()
3065
+ log(f"成功写入数据到 {sheet_name}")
3066
+ return True
3067
+ except Exception as e:
3068
+ log(f"写入数据失败: {e}")
3069
+ return False
3070
+
3071
+ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs):
3072
+ """
3073
+ 带锁的 Excel 格式化函数
3074
+
3075
+ Args:
3076
+ excel_path: Excel 文件路径
3077
+ sheet_name: 工作表名称
3078
+ format_func: 格式化函数
3079
+ *args, **kwargs: 传递给格式化函数的参数
3080
+ """
3081
+ app, wb, sheet = open_excel_with_lock(excel_path, sheet_name)
3082
+ if not app or not wb or not sheet:
3083
+ log(f"无法打开 Excel 文件进行格式化: {excel_path}")
3084
+ return False
3085
+
3086
+ try:
3087
+ # 执行格式化函数
3088
+ format_func(sheet, *args, **kwargs)
3089
+ # 保存
3090
+ wb.save()
3091
+ log(f"成功格式化工作表: {sheet_name}")
3092
+ return True
3093
+ except Exception as e:
3094
+ log(f"格式化失败: {e}")
3095
+ return False
3096
+
3097
+ # 经过观察 fortmat时 传入函数需要为类函数且第二个参数必须是 sheet
3098
+ def batch_excel_operations(excel_path, operations):
3099
+ """
3100
+ 批量 Excel 操作函数,自动分批处理,避免一次操作过多sheet导致Excel COM错误
3101
+ 保持操作的原始顺序执行
3102
+
3103
+ Args:
3104
+ excel_path: Excel 文件路径
3105
+ operations: 操作列表,每个操作是 (sheet_name, operation_type, data, format_func) 的元组
3106
+ operation_type: 'write', 'format', 'delete', 'move', 'active'
3107
+
3108
+ Returns:
3109
+ bool: 是否全部操作成功
3110
+ """
3111
+ if not operations:
3112
+ return True
3113
+
3114
+ # 批处理大小设置:每批最多处理8个操作
3115
+ MAX_OPERATIONS_PER_BATCH = 8
3116
+
3117
+ try:
3118
+ # 计算需要分几批
3119
+ total_batches = (len(operations) + MAX_OPERATIONS_PER_BATCH - 1) // MAX_OPERATIONS_PER_BATCH
3120
+ log(f"分{total_batches}批执行{len(operations)}个操作,每批最多{MAX_OPERATIONS_PER_BATCH}个,保持原始顺序")
3121
+
3122
+ # 按顺序分批执行
3123
+ for batch_idx in range(total_batches):
3124
+ start_idx = batch_idx * MAX_OPERATIONS_PER_BATCH
3125
+ end_idx = min(start_idx + MAX_OPERATIONS_PER_BATCH, len(operations))
3126
+ batch_operations = operations[start_idx:end_idx]
3127
+
3128
+ log(f"执行第{batch_idx + 1}/{total_batches}批操作({start_idx + 1}-{end_idx}),共{len(batch_operations)}个操作")
3129
+
3130
+ # 重试机制
3131
+ max_retries = 3
3132
+ for retry in range(max_retries):
3133
+ try:
3134
+ # 强制垃圾回收
3135
+ import gc
3136
+ gc.collect()
3137
+
3138
+ if _execute_operations_batch(excel_path, batch_operations):
3139
+ log(f"第{batch_idx + 1}批操作成功")
3140
+ break
3141
+ else:
3142
+ log(f"第{batch_idx + 1}批操作失败,重试 {retry + 1}/{max_retries}")
3143
+ if retry == max_retries - 1:
3144
+ log(f"第{batch_idx + 1}批操作最终失败")
3145
+ return False
3146
+ import time
3147
+ time.sleep(3)
3148
+ except Exception as e:
3149
+ log(f"第{batch_idx + 1}批操作异常: {e}")
3150
+ if retry == max_retries - 1:
3151
+ return False
3152
+ import time
3153
+ time.sleep(3)
3154
+
3155
+ # 批次间延迟
3156
+ if batch_idx < total_batches - 1:
3157
+ import time
3158
+ time.sleep(1)
3159
+
3160
+ log(f"所有批量操作完成: {excel_path}")
3161
+ return True
3162
+
3163
+ except Exception as e:
3164
+ log(f"批量操作过程异常: {e}")
3165
+ return False
3166
+
3167
+ def _execute_operations_batch(excel_path, operations):
3168
+ """
3169
+ 执行单个批次的操作
3170
+ """
3171
+ app, wb, sheet = open_excel_with_lock(excel_path)
3172
+ if not app or not wb:
3173
+ log(f"无法打开 Excel 文件: {excel_path}")
3174
+ return False
3175
+
3176
+ try:
3177
+ for sheet_name, operation_type, *args in operations:
3178
+ # 根据操作类型决定是否需要获取或创建工作表
3179
+ sheet = None
3180
+
3181
+ # 删除操作不需要获取sheet对象
3182
+ if operation_type == 'delete':
3183
+ log(f'删除sheet: {sheet_name}')
3184
+ delete_sheet_if_exists(wb, sheet_name)
3185
+ continue
3186
+
3187
+ # 其他操作需要获取或创建工作表
3188
+ if isinstance(sheet_name, str):
3189
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
3190
+ if sheet_name.strip().lower() in sheet_names:
3191
+ sheet = wb.sheets[sheet_name]
3192
+ else:
3193
+ # 只有在需要操作sheet内容时才创建
3194
+ if operation_type in ['write', 'format']:
3195
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
3196
+ else:
3197
+ log(f"警告: 操作 {operation_type} 需要的sheet {sheet_name} 不存在,跳过此操作")
3198
+ continue
3199
+ else:
3200
+ sheet = wb.sheets[sheet_name]
3201
+
3202
+ if sheet:
3203
+ sheet.activate()
3204
+
3205
+ if operation_type == 'write':
3206
+ data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
3207
+ # 清空工作表
3208
+ sheet.clear()
3209
+
3210
+ # 先设置文本格式,再写入数据(确保格式生效)
3211
+ if format_to_text_colunm and format_to_text_colunm[0]:
3212
+ try:
3213
+ # 使用安全的预格式化方式
3214
+ pre_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
3215
+ except Exception as e:
3216
+ log(f"预格式化失败: {e},继续执行")
3217
+
3218
+ # 写入数据
3219
+ log(f"批量操作,写入数据到: {sheet_name}")
3220
+ sheet.range('A1').value = data
3221
+
3222
+ # 写入后再次确认格式(双重保险)
3223
+ if format_to_text_colunm and format_to_text_colunm[0]:
3224
+ try:
3225
+ post_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
3226
+ except Exception as e:
3227
+ log(f"后格式化失败: {e}")
3228
+
3229
+ elif operation_type == 'format':
3230
+ format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
3231
+ # 执行格式化
3232
+ format_func(sheet, *format_args)
3233
+
3234
+ elif operation_type == 'move':
3235
+ log(f'移动sheet: {sheet_name}')
3236
+ position = args[0]
3237
+ move_sheet_to_position(wb, sheet_name, position)
3238
+
3239
+ elif operation_type == 'active':
3240
+ log(f'激活sheet: {sheet_name}')
3241
+ sheet.activate()
3242
+
3243
+ # 保存所有更改
3244
+ wb.save()
3245
+ return True
3246
+
3247
+ except Exception as e:
3248
+ log(f"单批次操作失败: {e}")
3249
+ return False
3250
+ finally:
3251
+ # 释放锁但不关闭 Excel(保持复用)
3252
+ excel_lock_manager.release_excel_lock(excel_path)
3253
+ close_excel_with_lock(excel_path, app, wb, True)
3254
+
3255
+ def close_excel_file(file_path):
3256
+ file_path = os.path.abspath(file_path).lower()
3257
+
3258
+ for proc in psutil.process_iter(['pid', 'name']):
3259
+ if proc.info['name'] and proc.info['name'].lower() in ['excel.exe', 'wps.exe']: # 只找 Excel
3260
+ try:
3261
+ for f in proc.open_files():
3262
+ if os.path.abspath(f.path).lower() == file_path:
3263
+ print(f"文件被 Excel 占用 (PID: {proc.pid}),正在关闭进程...")
3264
+ proc.terminate()
3265
+ proc.wait(timeout=3)
3266
+ print("已关闭。")
3267
+ return True
3268
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
3269
+ continue
3270
+
3271
+ print("文件没有被 Excel 占用。")
3272
+ return False
3273
+
3274
+ def force_close_excel_file(excel_path):
3275
+ """
3276
+ 强制关闭指定的 Excel 文件
3277
+
3278
+ Args:
3279
+ excel_path: Excel 文件路径
3280
+ """
3281
+ try:
3282
+ existing_instance = excel_lock_manager.get_excel_instance(excel_path)
3283
+ if existing_instance:
3284
+ app, wb = existing_instance
3285
+ close_excel_with_lock(excel_path, app, wb, force_close=True)
3286
+ log(f"强制关闭 Excel 文件: {excel_path}")
3287
+ except Exception as e:
3288
+ log(f"强制关闭 Excel 文件失败: {e}")
3289
+
3290
+ def wait_for_excel_available(excel_path, timeout=60, check_interval=1):
3291
+ """
3292
+ 等待 Excel 文件可用
3293
+
3294
+ Args:
3295
+ excel_path: Excel 文件路径
3296
+ timeout: 超时时间(秒)
3297
+ check_interval: 检查间隔(秒)
3298
+
3299
+ Returns:
3300
+ bool: 是否成功获取锁
3301
+ """
3302
+ start_time = time.time()
3303
+ while time.time() - start_time < timeout:
3304
+ if excel_lock_manager.acquire_excel_lock(excel_path, timeout=0):
3305
+ return True
3306
+ time.sleep(check_interval)
3307
+
3308
+ log(f"等待 Excel 文件可用超时: {excel_path}")
3309
+ return False
3310
+
3311
+ def smart_excel_operation(excel_path, operation_func, priority=0, timeout=60, max_retries=3):
3312
+ """
3313
+ 智能 Excel 操作函数,支持优先级、重试和更好的错误处理
3314
+
3315
+ Args:
3316
+ excel_path: Excel 文件路径
3317
+ operation_func: 要执行的操作函数,接收 (app, wb, sheet) 参数
3318
+ priority: 优先级,数字越小优先级越高
3319
+ timeout: 获取锁的超时时间(秒)
3320
+ max_retries: 最大重试次数
3321
+
3322
+ Returns:
3323
+ bool: 操作是否成功
3324
+ """
3325
+ for attempt in range(max_retries):
3326
+ try:
3327
+ # 检查是否有其他程序正在操作该文件
3328
+ waiting_count = excel_lock_manager.get_waiting_count(excel_path)
3329
+ if waiting_count > 0:
3330
+ log(f"等待其他程序完成操作: {os.path.basename(excel_path)} (等待队列: {waiting_count})")
3331
+
3332
+ # 尝试获取锁
3333
+ if not excel_lock_manager.acquire_excel_lock(excel_path, timeout, priority):
3334
+ if attempt < max_retries - 1:
3335
+ wait_time = (attempt + 1) * 5 # 递增等待时间
3336
+ log(f"获取锁失败,等待 {wait_time} 秒后重试 (尝试 {attempt + 1}/{max_retries})")
3337
+ time.sleep(wait_time)
3338
+ continue
3339
+ else:
3340
+ log(f"达到最大重试次数,操作失败: {excel_path}")
3341
+ return False
3342
+
3343
+ # 打开 Excel
3344
+ app, wb, sheet = open_excel_with_lock(excel_path)
3345
+ if not app or not wb:
3346
+ log(f"无法打开 Excel 文件: {excel_path}")
3347
+ return False
3348
+
3349
+ try:
3350
+ # 执行操作
3351
+ result = operation_func(app, wb, sheet)
3352
+
3353
+ # 保存更改
3354
+ if wb:
3355
+ wb.save()
3356
+ log(f"成功保存 Excel 文件: {os.path.basename(excel_path)}")
3357
+
3358
+ return result
3359
+
3360
+ except Exception as e:
3361
+ log(f"Excel 操作失败: {e}")
3362
+ return False
3363
+ finally:
3364
+ # 释放锁但不关闭 Excel(保持复用)
3365
+ excel_lock_manager.release_excel_lock(excel_path)
3366
+
3367
+ except Exception as e:
3368
+ log(f"智能 Excel 操作异常: {e}")
3369
+ if attempt < max_retries - 1:
3370
+ time.sleep(2)
3371
+ continue
3372
+ else:
3373
+ return False
3374
+
3375
+ return False
3376
+
3377
+ def batch_excel_operations_with_priority(excel_path, operations, priority=0, timeout=60):
3378
+ """
3379
+ 带优先级的批量 Excel 操作函数
3380
+
3381
+ Args:
3382
+ excel_path: Excel 文件路径
3383
+ operations: 操作列表
3384
+ priority: 优先级
3385
+ timeout: 超时时间
3386
+
3387
+ Returns:
3388
+ bool: 是否全部操作成功
3389
+ """
3390
+
3391
+ def batch_operation(app, wb, sheet):
3392
+ try:
3393
+ for sheet_name, operation_type, *args in operations:
3394
+ # 获取或创建工作表
3395
+ if isinstance(sheet_name, str):
3396
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
3397
+ if sheet_name.strip().lower() in sheet_names:
3398
+ sheet = wb.sheets[sheet_name]
3399
+ else:
3400
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
3401
+ else:
3402
+ sheet = wb.sheets[sheet_name]
3403
+
3404
+ sheet.activate()
3405
+
3406
+ if operation_type == 'write':
3407
+ data, format_to_text_colunm = args[:2]
3408
+ # 清空工作表
3409
+ sheet.clear()
3410
+ # 格式化文本列
3411
+ format_to_text_v2(sheet, format_to_text_colunm)
3412
+ # 写入数据
3413
+ sheet.range('A1').value = data
3414
+ log(f"批量操作:写入数据到 {sheet_name}")
3415
+
3416
+ elif operation_type == 'format':
3417
+ format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
3418
+ # 执行格式化
3419
+ format_func(sheet, *format_args)
3420
+ log(f"批量操作:格式化工作表 {sheet_name}")
3421
+
3422
+ return True
3423
+
3424
+ except Exception as e:
3425
+ log(f"批量操作失败: {e}")
3426
+ return False
3427
+
3428
+ return smart_excel_operation(excel_path, batch_operation, priority, timeout)
3429
+
3430
+ def wait_for_excel_available_with_priority(excel_path, timeout=60, check_interval=1, priority=0):
3431
+ """
3432
+ 等待 Excel 文件可用(带优先级)
3433
+
3434
+ Args:
3435
+ excel_path: Excel 文件路径
3436
+ timeout: 超时时间(秒)
3437
+ check_interval: 检查间隔(秒)
3438
+ priority: 优先级
3439
+
3440
+ Returns:
3441
+ bool: 是否成功获取锁
3442
+ """
3443
+ start_time = time.time()
3444
+ while time.time() - start_time < timeout:
3445
+ if excel_lock_manager.acquire_excel_lock(excel_path, timeout=0, priority=priority):
3446
+ return True
3447
+ time.sleep(check_interval)
3448
+
3449
+ log(f"等待 Excel 文件可用超时: {excel_path}")
3450
+ return False
3451
+
3452
+ def get_excel_status(excel_path):
3453
+ """
3454
+ 获取 Excel 文件状态信息
3455
+
3456
+ Args:
3457
+ excel_path: Excel 文件路径
3458
+
3459
+ Returns:
3460
+ dict: 状态信息
3461
+ """
3462
+ return {
3463
+ 'is_open' : excel_lock_manager.is_excel_open(excel_path),
3464
+ 'waiting_count' : excel_lock_manager.get_waiting_count(excel_path),
3465
+ 'operation_count': excel_lock_manager.get_operation_count(excel_path),
3466
+ 'has_lock' : excel_lock_manager.get_file_lock(excel_path).locked()
3467
+ }
3468
+
3469
+ def get_last_used_row(sheet):
3470
+ return sheet.used_range.last_cell.row