qrpa 1.0.9__py3-none-any.whl → 1.0.11__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,2758 @@
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
+
16
+ from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
17
+
18
+ excel_color_index = {
19
+ "无色(自动)": 0, # 透明/默认
20
+ "黑色": 1, # #000000
21
+ "白色": 2, # #FFFFFF
22
+ "红色": 3, # #FF0000
23
+ "绿色": 4, # #00FF00
24
+ "蓝色": 5, # #0000FF
25
+ "黄色": 6, # #FFFF00
26
+ "粉红色": 7, # #FF00FF
27
+ "青绿色": 8, # #00FFFF
28
+ "深红色": 9, # #800000
29
+ "深绿色": 10, # #008000
30
+ "深蓝色": 11, # #000080
31
+ "橄榄色(深黄)": 12, # #808000
32
+ "紫色": 13, # #800080
33
+ "蓝绿色(水色)": 14, # #008080
34
+ "灰色(25%)": 15, # #808080
35
+ "浅灰色(12.5%)": 16, # #C0C0C0
36
+ # 17-19:系统保留(通常不可用)
37
+ "深玫瑰红": 20, # #FF99CC
38
+ "深金色": 21, # #FFCC99
39
+ "深橙红色": 22, # #FF6600
40
+ "深灰色(50%)": 23, # #666666
41
+ "深紫色": 24, # #660066
42
+ "蓝灰色": 25, # #3366FF
43
+ "浅蓝色": 26, # #99CCFF
44
+ "浅紫色": 27, # #CC99FF
45
+ "浅青绿色": 28, # #99FFFF
46
+ "浅绿色": 29, # #CCFFCC
47
+ "浅黄色": 30, # #FFFFCC
48
+ "浅橙红色": 31, # #FFCC99
49
+ "玫瑰红": 32, # #FF9999
50
+ "浅天蓝色": 33, # #99CCFF
51
+ "浅海绿色": 34, # #99FFCC
52
+ "浅草绿色": 35, # #CCFF99
53
+ "浅柠檬黄": 36, # #FFFF99
54
+ "浅珊瑚色": 37, # #FFCC99
55
+ "浅玫瑰红": 38, # #FF9999
56
+ "棕褐色": 39, # #CC9966
57
+ "浅棕褐色": 40, # #FFCC99
58
+ "浅橄榄色": 41, # #CCCC99
59
+ "浅蓝灰色": 42, # #9999FF
60
+ "浅灰绿色": 43, # #99CC99
61
+ "金色": 44, # #FFCC00
62
+ "浅橙黄色": 45, # #FFCC66
63
+ "橙红色": 46, # #FF6600
64
+ "深天蓝色": 47, # #0066CC
65
+ "深海绿色": 48, # #009966
66
+ "深草绿色": 49, # #669900
67
+ "深柠檬黄": 50, # #CCCC00
68
+ "深珊瑚色": 51, # #FF9933
69
+ "深玫瑰红(暗)": 52, # #CC6699
70
+ "深棕褐色": 53, # #996633
71
+ "深橄榄色": 54, # #666600
72
+ "深蓝灰色": 55, # #333399
73
+ }
74
+
75
+
76
+ def set_cell_prefix_red(cell, n, color_name):
77
+ """
78
+ 将指定 Excel 单元格内容的前 n 个字符设置为红色。
79
+ """
80
+ text = str(cell.value)
81
+
82
+ if not text or n <= 0:
83
+ return
84
+
85
+ n = min(n, len(text)) # 避免超出范围
86
+
87
+ try:
88
+ # 设置前n个字符为红色
89
+ cell.api.Characters(1, n).Font.ColorIndex = excel_color_index[color_name]
90
+ except Exception as e:
91
+ print(f"设置字体颜色失败: {e}")
92
+
93
+
94
+ def sort_by_column(data, col_index, start_row=2, reverse=True):
95
+ if not data or start_row >= len(data):
96
+ return data
97
+
98
+ try:
99
+ header = data[:start_row]
100
+ new_data_sorted = data[start_row:]
101
+
102
+ def get_key(row):
103
+ value = row[col_index]
104
+ if isinstance(value, (int, float)): # 已经是数字
105
+ return (0, value) # 用元组排序,数字优先
106
+ try:
107
+ return (0, float(value)) # 尝试转数字
108
+ except (ValueError, TypeError):
109
+ return (1, str(value)) # 转不了就按字符串排
110
+
111
+ new_data_sorted.sort(key=get_key, reverse=reverse)
112
+ return header + new_data_sorted
113
+ except IndexError:
114
+ print(f"Error: Column index {col_index} out of range")
115
+ return data
116
+
117
+
118
+ def column_exists(sheet, column_name, header_row=1):
119
+ """
120
+ 检查工作表中是否存在指定列名
121
+ :param sheet: xlwings Sheet 对象
122
+ :param column_name: 要查找的列名
123
+ :param header_row: 表头所在行号,默认为1
124
+ :return: 如果存在返回True,否则返回False
125
+ """
126
+ # 获取表头行所有值
127
+ header_values = sheet.range((header_row, 1), (header_row, sheet.used_range.last_cell.column)).value
128
+
129
+ return column_name in header_values
130
+
131
+
132
+ def merge_by_column_v2(sheet, column_name, other_columns):
133
+ log('正在处理合并单元格')
134
+ # 最好放到 open_excel 后面,不然容易出错
135
+ col_letter = find_column_by_data(sheet, 1, column_name)
136
+ if col_letter is None:
137
+ log(f'未找到合并的列名: {column_name}')
138
+ return
139
+
140
+ data = sheet.range(f'{col_letter}1').expand('table').value
141
+ # col_index = column_name_to_index(col_letter)
142
+ start_row = 1
143
+ merge_ranges = [] # 用来存储所有待合并的单元格范围
144
+
145
+ # 缓存其他列的列号
146
+ other_columns_index = {}
147
+ for col in other_columns:
148
+ col_name = find_column_by_data(sheet, 1, col)
149
+ if col_name:
150
+ other_columns_index[col_name] = column_name_to_index(col_name)
151
+
152
+ for row in range(2, len(data) + 1):
153
+ log(f'查找 {row}/{len(data)}')
154
+ if data[row - 1][0] != data[row - 2][0]:
155
+ if row - start_row > 1:
156
+ # 将合并范围加入列表
157
+ merge_ranges.append((col_letter, start_row, row - 1))
158
+ for col_name, col_index in other_columns_index.items():
159
+ merge_ranges.append((col_name, start_row, row - 1))
160
+ start_row = row
161
+
162
+ if len(data) - start_row > 1:
163
+ merge_ranges.append((col_letter, start_row, len(data)))
164
+ for col_name, col_index in other_columns_index.items():
165
+ merge_ranges.append((col_name, start_row, len(data)))
166
+
167
+ # 批量合并单元格
168
+ for col_name, start, end in merge_ranges:
169
+ log(f'处理 {col_name}{start}:{col_name}{end} merge')
170
+ sheet.range(f'{col_name}{start}:{col_name}{end}').merge()
171
+
172
+
173
+ def merge_by_column(sheet, column_name, other_columns):
174
+ log('正在处理合并单元格')
175
+ # 最好放到 open_excel 后面,不然容易出错
176
+ data = sheet.range('A1').expand('table').value
177
+ col_letter = find_column_by_data(sheet, 1, column_name)
178
+ if col_letter is None:
179
+ log(f'未找到合并的列名: {column_name}')
180
+ return
181
+ col_index = column_name_to_index(col_letter)
182
+ start_row = 1
183
+ for row in range(2, len(data) + 1):
184
+ log(f'{row}/{len(data)}')
185
+ if data[row - 1][col_index] != data[row - 2][col_index]:
186
+ if row - start_row > 1:
187
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{row - 1}').merge()
188
+ for col in other_columns:
189
+ col_name = find_column_by_data(sheet, 1, col)
190
+ if col_name is not None:
191
+ sheet.range(f'{col_name}{start_row}:{col_name}{row - 1}').merge()
192
+ start_row = row
193
+
194
+ if len(data) - start_row > 1:
195
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{len(data)}').merge()
196
+ for col in other_columns:
197
+ col_name = find_column_by_data(sheet, 1, col)
198
+ if col_name is not None:
199
+ sheet.range(f'{col_name}{start_row}:{col_name}{len(data)}').merge()
200
+
201
+
202
+ def merge_column_v2(sheet, columns):
203
+ if columns is None:
204
+ return
205
+
206
+ # 缓存所有列的字母
207
+ col_letters = {col: find_column_by_data(sheet, 1, col) for col in columns}
208
+ merge_ranges = [] # 用来存储所有待合并的单元格范围
209
+
210
+ for c, col_letter in col_letters.items():
211
+ if col_letter is None:
212
+ continue
213
+
214
+ data = sheet.range(f'{col_letter}1').expand('table').value
215
+ start_row = 1
216
+
217
+ for row in range(2, len(data) + 1):
218
+ log(f'查找 {row}/{len(data)}') # 如果数据量非常大,这里的日志会影响性能,可以考虑优化
219
+ if data[row - 1][0] != data[row - 2][0]:
220
+ if row - start_row > 1:
221
+ merge_ranges.append((col_letter, start_row, row - 1))
222
+ start_row = row
223
+
224
+ if len(data) - start_row > 1:
225
+ merge_ranges.append((col_letter, start_row, len(data)))
226
+
227
+ # 批量合并单元格
228
+ for col_letter, start, end in merge_ranges:
229
+ log(f'处理 {col_letter}{start}:{col_letter}{end} merge')
230
+ sheet.range(f'{col_letter}{start}:{col_letter}{end}').merge()
231
+
232
+
233
+ # 按列相同值合并
234
+ def merge_column(sheet, columns):
235
+ # 最后放到 open_excel 后面,不然容易出错
236
+ if columns is None:
237
+ return
238
+ for c in columns:
239
+ col_letter = find_column_by_data(sheet, 1, c)
240
+ if col_letter is None:
241
+ continue
242
+ data = sheet.range(f'{col_letter}1').expand('table').value
243
+ # col_index = column_name_to_index(col_letter)
244
+ col_index = 0
245
+ start_row = 1
246
+ for row in range(2, len(data) + 1):
247
+ if data[row - 1][col_index] != data[row - 2][col_index]:
248
+ if row - start_row > 1:
249
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{row - 1}').merge()
250
+ start_row = row
251
+
252
+ if len(data) - start_row > 1:
253
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{len(data)}').merge()
254
+
255
+
256
+ def remove_excel_columns(sheet, columns):
257
+ # 获取第一行(标题行)的所有值
258
+ header_row = sheet.range('1:1').value
259
+
260
+ # 获取要删除的列的索引(从1开始)
261
+ columns_to_remove = []
262
+ for i, header in enumerate(header_row, start=1):
263
+ if header in columns:
264
+ columns_to_remove.append(i)
265
+
266
+ # 如果没有找到要删除的列
267
+ if not columns_to_remove:
268
+ log("警告: 未找到任何匹配的列")
269
+ return False
270
+
271
+ # 按从右到左的顺序删除列(避免索引变化问题)
272
+ for col_idx in sorted(columns_to_remove, reverse=True):
273
+ col_letter = xw.utils.col_name(col_idx)
274
+ sheet.range(f'{col_letter}:{col_letter}').delete()
275
+
276
+ print(f"成功移除列: {columns_to_remove}")
277
+ return True
278
+
279
+
280
+ def delete_sheet_if_exists(wb, sheet_name):
281
+ """
282
+ 如果工作簿中存在指定名称的工作表,则将其删除。
283
+
284
+ 参数:
285
+ wb : xw.Book
286
+ xlwings 的工作簿对象。
287
+ sheet_name : str
288
+ 要检查并删除的工作表名称。
289
+ """
290
+ sheet_names = [s.name for s in wb.sheets]
291
+ if sheet_name in sheet_names:
292
+ wb.sheets[sheet_name].delete()
293
+ print(f"已删除 Sheet: {sheet_name}")
294
+ else:
295
+ print(f"Sheet 不存在: {sheet_name}")
296
+
297
+
298
+ # 水平对齐:
299
+ # -4108:居中
300
+ # -4131:左对齐
301
+ # -4152:右对齐
302
+ # 垂直对齐:
303
+ # -4108:居中
304
+ # -4160:顶部对齐
305
+ # -4107:底部对齐
306
+ def index_to_column_name(index):
307
+ """
308
+ 将列索引转换为Excel列名。
309
+ 例如:1 -> 'A', 2 -> 'B', 26 -> 'Z', 27 -> 'AA'
310
+ """
311
+ column_name = ''
312
+ while index > 0:
313
+ index -= 1
314
+ remainder = index % 26
315
+ column_name = chr(65 + remainder) + column_name
316
+ index = index // 26
317
+ return column_name
318
+
319
+
320
+ # # 示例:将列索引转换为列名
321
+ # log(index_to_column_name(1)) # 输出: 'A'
322
+ # log(index_to_column_name(26)) # 输出: 'Z'
323
+ # log(index_to_column_name(27)) # 输出: 'AA'
324
+ # log(index_to_column_name(52)) # 输出: 'AZ'
325
+
326
+ def column_name_to_index(column_name):
327
+ """
328
+ 将Excel列名转换为列索引。
329
+ 例如:'A' -> 1, 'B' -> 2, 'Z' -> 26, 'AA' -> 27
330
+ 例如:'A' -> 0, 'B' -> 1, 'Z' -> 25, 'AA' -> 26
331
+ """
332
+ index = 0
333
+ for char in column_name:
334
+ index = index * 26 + (ord(char.upper()) - 64)
335
+ return index - 1
336
+
337
+
338
+ # # 示例:将列名转换为列索引
339
+ # log(column_name_to_index('A')) # 输出: 1
340
+ # log(column_name_to_index('Z')) # 输出: 26
341
+ # log(column_name_to_index('AA')) # 输出: 27
342
+ # log(column_name_to_index('AZ')) # 输出: 52
343
+
344
+ def find_row_by_data(sheet, column, target_value):
345
+ """
346
+ 查找指定数据在某一列中第一次出现的行号。
347
+
348
+ :param sheet: xlwings 的 Sheet 对象。
349
+ :param column: 列名(如 'A', 'B', 'C')。
350
+ :param target_value: 要查找的数据。
351
+ :return: 数据所在的行号(从1开始),如果未找到返回 None。
352
+ """
353
+ # 获取指定列的所有数据
354
+ column_data = sheet.range(f'{column}1').expand('down').value
355
+
356
+ # 遍历数据,查找目标值
357
+ for i, value in enumerate(column_data, start=1):
358
+ if value == target_value:
359
+ return i
360
+
361
+ # 如果未找到,返回 None
362
+ return None
363
+
364
+
365
+ def find_column_by_data(sheet, row, target_value):
366
+ """
367
+ 查找指定数据在某一行中第一次出现的列名,包括隐藏的列。
368
+
369
+ :param sheet: xlwings 的 Sheet 对象。
370
+ :param row: 行号(如 1, 2, 3)。
371
+ :param target_value: 要查找的数据。
372
+ :return: 数据所在的列名(如 'A', 'B', 'C'),如果未找到返回 None。
373
+ """
374
+ last_col = sheet.used_range.last_cell.column # 获取最后一列索引
375
+
376
+ for col in range(1, last_col + 1): # 遍历所有列
377
+ cell = sheet.cells(row, col)
378
+
379
+ # 检查目标值是否匹配
380
+ if cell.value == target_value:
381
+ return xw.utils.col_name(col) # 返回列名
382
+
383
+ return None # 未找到返回 None
384
+
385
+
386
+ def find_column_by_data_old(sheet, row, target_value):
387
+ """
388
+ 查找指定数据在某一行中第一次出现的列名。
389
+
390
+ :param sheet: xlwings 的 Sheet 对象。
391
+ :param row: 行号(如 1, 2, 3)。
392
+ :param target_value: 要查找的数据。
393
+ :return: 数据所在的列名(如 'A', 'B', 'C'),如果未找到返回 None。
394
+ """
395
+ # 获取指定行的所有数据
396
+ row_data = sheet.range(f'A{row}').expand('right').value
397
+
398
+ # 遍历数据,查找目标值
399
+ for i, value in enumerate(row_data):
400
+ if value == target_value:
401
+ # 将列索引转换为列名
402
+ return xw.utils.col_name(i + 1)
403
+
404
+ # 如果未找到,返回 None
405
+ return None
406
+
407
+
408
+ def set_print_area(sheet, print_range, pdf_path=None, fit_to_width=True, landscape=False):
409
+ """
410
+ 设置指定sheet的打印区域和打印布局为适合A4宽度打印。
411
+
412
+ :param sheet: xlwings 的 Sheet 对象
413
+ :param print_range: 要设置为打印区域的字符串范围,比如 "A1:G50"
414
+ :param fit_to_width: 是否缩放以适应A4纸宽度
415
+ :param landscape: 是否横向打印(默认纵向)
416
+ """
417
+ # 设置打印区域
418
+ sheet.api.PageSetup.PrintArea = print_range
419
+
420
+ # 取消打印标题行/列
421
+ sheet.api.PageSetup.PrintHeadings = False
422
+
423
+ # 取消打印网格线
424
+ sheet.api.PageSetup.PrintGridlines = False
425
+
426
+ # 打印方向(横向或纵向)
427
+ sheet.api.PageSetup.Orientation = 2 if landscape else 1 # 2: Landscape, 1: Portrait
428
+
429
+ # 设置纸张大小为 A4
430
+ sheet.api.PageSetup.PaperSize = 9 # 9: xlPaperA4
431
+
432
+ # 设置页边距
433
+ sheet.api.PageSetup.LeftMargin = 20 # 上边距
434
+ sheet.api.PageSetup.RightMargin = 20 # 上边距
435
+ sheet.api.PageSetup.TopMargin = 20 # 上边距
436
+ sheet.api.PageSetup.BottomMargin = 20 # 上边距
437
+
438
+ if fit_to_width:
439
+ # 适应一页宽度,多页高度
440
+ sheet.api.PageSetup.Zoom = False
441
+ sheet.api.PageSetup.FitToPagesWide = 1
442
+ sheet.api.PageSetup.FitToPagesTall = False # 高度不限制,可以分页
443
+ else:
444
+ # 使用默认缩放(不建议用于A4布局控制)
445
+ sheet.api.PageSetup.Zoom = 100
446
+
447
+ # 可选:居中打印
448
+ sheet.api.PageSetup.CenterHorizontally = True
449
+ sheet.api.PageSetup.CenterVertically = False
450
+
451
+ # 导出打印区域为PDF
452
+ if pdf_path is not None:
453
+ sheet.to_pdf(path=pdf_path)
454
+ log(f"PDF已成功生成:{pdf_path}")
455
+
456
+
457
+ def minimize(app):
458
+ # 让 Excel 窗口最小化
459
+ app.api.WindowState = -4140 # -4140 对应 Excel 中的 xlMinimized 常量
460
+
461
+
462
+ def insert_fixed_scale_image_v2(sheet, cell, image_path):
463
+ """
464
+ 将图片插入到指定单元格中,自动缩放以适应单元格尺寸,但保持宽高比例不变。
465
+ - sheet: xlwings 工作表对象
466
+ - cell: 单元格地址,如 'C3'
467
+ - image_path: 图片路径
468
+ """
469
+ if not image_path:
470
+ return None
471
+
472
+ target_range = sheet.range(cell)
473
+ if target_range.merge_cells:
474
+ target_range = target_range.merge_area
475
+
476
+ cell_value = target_range.value
477
+
478
+ try:
479
+ # 获取单元格的宽高(单位是 points)
480
+ cell_width = target_range.width
481
+ cell_height = target_range.height
482
+
483
+ # 获取图片实际尺寸(像素),并计算比例
484
+ with Image.open(image_path) as img:
485
+ img_width_px, img_height_px = img.size
486
+
487
+ # 计算图片的实际宽高比(防止变形)
488
+ img_ratio = img_width_px / img_height_px
489
+ cell_ratio = cell_width / cell_height
490
+
491
+ # 设置缩放因子,留出空隙(例如,0.9 表示图片尺寸为原来的 90%)
492
+ padding_factor = 0.9
493
+
494
+ # 计算缩放倍数(确保图片不超过单元格大小)
495
+ if img_ratio > cell_ratio:
496
+ # 宽度限制
497
+ scale = cell_width / img_width_px * padding_factor
498
+ img_width_resized = cell_width * padding_factor
499
+ img_height_resized = img_height_px * scale
500
+ else:
501
+ # 高度限制
502
+ scale = cell_height / img_height_px * padding_factor
503
+ img_width_resized = img_width_px * scale
504
+ img_height_resized = cell_height * padding_factor
505
+
506
+ # 插入图片
507
+ pic = sheet.pictures.add(image_path, left=target_range.left, top=target_range.top, width=img_width_resized, height=img_height_resized)
508
+
509
+ # 居中对齐
510
+ pic.left = target_range.left + (target_range.width - pic.width) / 2
511
+ pic.top = target_range.top + (target_range.height - pic.height) / 2
512
+
513
+ # 清除单元格文字
514
+ target_range.value = None
515
+
516
+ return pic
517
+
518
+ except Exception as e:
519
+ target_range.value = cell_value
520
+ send_exception()
521
+
522
+ return None
523
+
524
+
525
+ def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
526
+ """
527
+ 按固定比例放大图片并插入到单元格
528
+ insert_fixed_scale_image(sheet, 'C1', img_path, 1.5)
529
+ 参数:
530
+ - sheet: xlwings工作表对象
531
+ - cell: 目标单元格地址
532
+ - image_path: 图片文件路径
533
+ - scale: 缩放倍数(2.0表示放大两倍)
534
+ """
535
+ if not image_path:
536
+ return None
537
+
538
+ # 获取目标单元格范围
539
+ target_range = sheet.range(cell)
540
+
541
+ if target_range.merge_cells:
542
+ target_range = target_range.merge_area
543
+
544
+ cell_value = target_range.value
545
+ try:
546
+ # 插入图片并缩放
547
+ pic = sheet.pictures.add(image_path, left=target_range.left, top=target_range.top, scale=scale)
548
+
549
+ # 调整位置使其居中(可选)
550
+ pic.left = target_range.left + (target_range.width - pic.width) / 2
551
+ pic.top = target_range.top + (target_range.height - pic.height) / 2
552
+
553
+ target_range.value = None
554
+
555
+ return pic
556
+ except Exception as e:
557
+ target_range.value = cell_value
558
+ send_exception()
559
+
560
+ return None
561
+
562
+
563
+ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150, img_save_key=None, dir_name=None):
564
+ if not columns:
565
+ return
566
+
567
+ minimize(app)
568
+
569
+ # 清空所有图片
570
+ clear_all_pictures(sheet)
571
+
572
+ # 获取每列图片列的列号,并设置列宽
573
+ col_letter_map = {}
574
+ for img_col in columns:
575
+ col_letter = find_column_by_data(sheet, 1, img_col)
576
+ if col_letter is not None:
577
+ col_letter_map[img_col] = col_letter
578
+ # 下载图片
579
+ log(f'批量下载图片: {img_col} => {col_letter}')
580
+ last_row = get_last_row(sheet, col_letter)
581
+ images = sheet.range(f'{col_letter}2:{col_letter}{last_row}').value
582
+ images = images if isinstance(images, list) else [images]
583
+ download_images_concurrently(images, platform)
584
+
585
+ # 任意一个列作为主参考列,用来确定行数
586
+ if not col_letter_map:
587
+ return
588
+
589
+ ref_col_letter = next(iter(col_letter_map.values()))
590
+ last_row = get_last_row(sheet, ref_col_letter)
591
+
592
+ img_key_letter = find_column_by_data(sheet, 1, img_save_key)
593
+
594
+ # 预计算所有单元格的合并区域信息 (优化点1)
595
+ area_map = {}
596
+ for row in range(2, last_row + 1):
597
+ log(f'计算 {row}/{last_row}') # 如果数据量非常大,这里的日志会影响性能,可以考虑优化
598
+ for col_letter in col_letter_map.values():
599
+ cell_ref = f'{col_letter}{row}'
600
+ cell_range = sheet.range(cell_ref)
601
+ cell_address = cell_range.address
602
+
603
+ if cell_range.merge_cells:
604
+ cell_range = cell_range.merge_area
605
+ cell_address = cell_range.address
606
+
607
+ if cell_address not in area_map:
608
+ # 计算合并区域的宽高和位置
609
+ cell_width = cell_range.width
610
+ cell_height = cell_range.height
611
+
612
+ # 调整列宽和行高
613
+ if cell_width < img_width:
614
+ cell_range.column_width = img_width / 6.1
615
+
616
+ # 这一行暂时先自动控制宽度
617
+ cell_range.column_width = img_width / 6.1
618
+
619
+ if cell_height < img_width:
620
+ cell_range.row_height = max(150 / 8, img_width / cell_range.rows.count)
621
+
622
+ # 计算居中位置
623
+ top = cell_range.top + (cell_range.height - img_width) / 2
624
+ left = cell_range.left + (cell_range.width - img_width) / 2
625
+
626
+ area_map[cell_address] = {
627
+ 'top': top,
628
+ 'left': left,
629
+ 'width': img_width,
630
+ 'cell_list': [c.address for c in cell_range]
631
+ }
632
+
633
+ # 处理图片插入 (优化点2)
634
+ for row in range(2, last_row + 1):
635
+ for img_col_name, col_letter in col_letter_map.items():
636
+ cell_ref = f'{col_letter}{row}'
637
+ cell_range = sheet.range(cell_ref)
638
+ cell_address = cell_range.address
639
+
640
+ # 检查合并单元格 (使用预计算的信息)
641
+ if cell_range.merge_cells and cell_address in area_map[cell_range.merge_area.address]['cell_list'][1:]:
642
+ continue
643
+
644
+ if cell_range.merge_cells:
645
+ cell_range = cell_range.merge_area
646
+ cell_address = cell_range.address
647
+
648
+ # 使用预计算的位置信息
649
+ top = area_map[cell_address]['top']
650
+ left = area_map[cell_address]['left']
651
+ width = area_map[cell_address]['width']
652
+
653
+ # 获取图片链接
654
+ if cell_range.merge_cells:
655
+ img_url = cell_range.value[0]
656
+ else:
657
+ img_url = cell_range.value
658
+
659
+ if img_url:
660
+ if img_key_letter is not None:
661
+ image_dir = Path(f'{os.getenv('auto_dir')}/image') / dir_name
662
+ extension = Path(img_url).suffix
663
+ filename = str(sheet.range(f'{img_key_letter}{row}').value)
664
+ img_save_path = image_dir / f"{sanitize_filename(filename)}{extension}"
665
+ else:
666
+ img_save_path = None
667
+
668
+ img_path = download_img_v2(img_url, platform, img_save_path)
669
+ log(f'插入图片 {sheet.name} [{img_col_name}] {row}/{last_row} {img_path}')
670
+ if not img_path:
671
+ log('跳过:', img_path, img_url)
672
+ continue
673
+ cell_value = cell_range.value
674
+
675
+ # 优化图片插入函数调用 (优化点3)
676
+ try:
677
+ # 使用预计算的位置直接插入图片
678
+ sheet.pictures.add(img_path, top=top + 2, left=left + 2, width=width - 4, height=width - 4)
679
+ cell_range.value = None
680
+ except Exception as e:
681
+ # 插入图片失败恢复链接地址
682
+ cell_range.value = cell_value
683
+ send_exception()
684
+ else:
685
+ log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
686
+
687
+
688
+ def download_images_concurrently(image_urls, platform='shein', img_save_dir=None):
689
+ # 使用线程池执行并发下载
690
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
691
+ # 使用 lambda 函数同时传递 url 和 img_save_path
692
+ results = list(executor.map(lambda url: download_img_v2(url, platform, img_save_path=img_save_dir), image_urls))
693
+ return results
694
+
695
+
696
+ def download_img_by_chrome(image_url, save_name):
697
+ with sync_playwright() as p:
698
+ browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
699
+ context = browser.new_context()
700
+ page = context.new_page()
701
+ # 直接通过Playwright下载图片
702
+ response = page.request.get(image_url)
703
+ with open(save_name, 'wb') as f:
704
+ f.write(response.body()) # 将下载的内容保存为文件
705
+ log(f"图片已通过chrome下载并保存为:{save_name}")
706
+ # 关闭浏览器
707
+ browser.close()
708
+ return save_name
709
+
710
+
711
+ def download_img_v2(image_url, platform='shein', img_save_path=None):
712
+ image_url = add_https(image_url)
713
+ if image_url is None or 'http' not in image_url:
714
+ return False
715
+
716
+ image_dir = Path(f'{os.getenv('auto_dir')}/image')
717
+ image_dir = os.path.join(image_dir, platform)
718
+
719
+ # 确保目录存在,如果不存在则创建
720
+ if not os.path.exists(image_dir):
721
+ os.makedirs(image_dir)
722
+
723
+ file_name = os.path.basename(urlparse(image_url).path) # 获取 URL 路径中的文件名
724
+ file_path = os.path.join(image_dir, file_name) # 拼接文件路径
725
+
726
+ if os.path.exists(file_path):
727
+ if img_save_path is not None:
728
+ create_file_path(img_save_path)
729
+ copy_file(file_path, img_save_path)
730
+ return file_path
731
+
732
+ # 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
733
+ # https://ssmp-spmp.oss-cn-shenzhen.aliyuncs.com/4136915/image/spec/wNgR5gFzOYYnu52Jkyez.jpg?x-image-process=image/resize,m_lfit,h_100,w_100
734
+ # 这个域名有浏览器指纹校验 无法通过脚本下载图片
735
+ if any(blocked in image_url for blocked in
736
+ ['myhuaweicloud.com', 'ssmp-spmp.oss-cn-shenzhen.aliyuncs.com', 'image.myqcloud.com', 'kj-img.pddpic.com']):
737
+ return download_img_by_chrome(image_url, file_path)
738
+
739
+ # if 'myhuaweicloud.com' in image_url:
740
+ # return False
741
+ # if 'ssmp-spmp.oss-cn-shenzhen.aliyuncs.com' in image_url:
742
+ # return False
743
+ # if 'image.myqcloud.com' in image_url:
744
+ # return False
745
+ # if 'kj-img.pddpic.com' in image_url:
746
+ # return False
747
+
748
+ headers = {
749
+ "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",
750
+ "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",
751
+ "Accept-Encoding": "gzip, deflate",
752
+ "Accept-Language": "zh-CN,zh;q=0.9"
753
+ }
754
+ # 下载图片
755
+ try:
756
+ response = requests.get(image_url, headers=headers, timeout=10)
757
+ response.raise_for_status() # 如果响应状态码不是 200,将引发 HTTPError
758
+ # # 成功处理
759
+ log(f"成功获取网络图片: {image_url}")
760
+ except requests.exceptions.HTTPError as e:
761
+ log(f"HTTP 错误: {e} {image_url}")
762
+ return False
763
+ except requests.exceptions.ConnectionError as e:
764
+ log(f"连接错误: {e} {image_url}")
765
+ return False
766
+ except requests.exceptions.Timeout as e:
767
+ log(f"请求超时: {e} {image_url}")
768
+ return False
769
+ except requests.exceptions.RequestException as e:
770
+ log(f"请求异常: {e} {image_url}")
771
+ return False
772
+
773
+ # 将图片保存到本地
774
+ with open(file_path, 'wb') as f:
775
+ f.write(response.content)
776
+
777
+ if img_save_path is not None:
778
+ create_file_path(img_save_path)
779
+ copy_file(file_path, img_save_path)
780
+
781
+ return file_path
782
+
783
+
784
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
785
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
786
+ def insert_cell_image(sheet, cell, file_path, img_width=120):
787
+ """
788
+ 从本地文件居中插入图片到 Excel 指定合并的单元格。
789
+ :param sheet: xlwings 的 Sheet 对象。
790
+ :param cell: 目标单元格地址(如 'A1')。
791
+ :param file_path: 本地文件路径。
792
+ :param img_width: 插入图片的宽高 图片为正方形 和 row_height 同值
793
+ """
794
+ try:
795
+ # 获取目标单元格区域
796
+ cell_range = sheet.range(cell)
797
+
798
+ # 如果是合并区域,获取合并区域
799
+ if cell_range.merge_cells:
800
+ merge_area = cell_range.merge_area
801
+ cell_range = merge_area # 更新为合并区域
802
+
803
+ # 获取合并区域的宽度和高度
804
+ cell_width = cell_range.width # 单元格宽度
805
+ cell_height = cell_range.height # 单元格高度
806
+
807
+ # 如果单元格的宽度小于图片宽度,增加单元格的宽度
808
+ if cell_width < img_width:
809
+ cell_range.column_width = img_width / 6.1 # 约12.86字符宽度
810
+
811
+ # 这一行暂时先自动控制宽度
812
+ cell_range.column_width = img_width / 6.1
813
+
814
+ # 如果单元格的高度小于图片高度,增加单元格的高度
815
+ if cell_height < img_width:
816
+ cell_range.row_height = max(150 / 8, img_width / cell_range.rows.count) # 设置为图片高度
817
+
818
+ # 获取合并区域的 top 和 left,计算图片居中的位置
819
+ top = cell_range.top + (cell_range.height - img_width) / 2
820
+ left = cell_range.left + (cell_range.width - img_width) / 2
821
+
822
+ # 将图片插入到指定单元格并填充满单元格
823
+ sheet.pictures.add(file_path, top=top + 2, left=left + 2, width=img_width - 4, height=img_width - 4)
824
+
825
+ except Exception as e:
826
+ log(f'插入图片失败: {e}, {file_path}')
827
+ send_exception()
828
+
829
+
830
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
831
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
832
+ def insert_image_from_local(sheet, cell, file_path, cell_width=90, cell_height=90):
833
+ """
834
+ 从本地文件插入图片到 Excel 指定单元格。
835
+ :param sheet: xlwings 的 Sheet 对象。
836
+ :param cell: 目标单元格地址(如 'A1')。
837
+ :param file_path: 本地文件路径。
838
+ """
839
+ try:
840
+ # 打印文件路径以确保正确
841
+ # log(f'插入图片的文件路径: {file_path}')
842
+
843
+ # if is_cell_has_image(sheet, cell):
844
+ # log(f'单元格 {cell} 已有图片,跳过插入。')
845
+ # return
846
+
847
+ # 获取单元格位置
848
+ cell_range = sheet.range(cell)
849
+ # cell_width = cell_range.width # 获取单元格的宽度
850
+ # cell_height = cell_range.height # 获取单元格的高度
851
+ # log(f'插入图片单元格:{cell} {cell_width} {cell_height}')
852
+
853
+ # 设置列宽为 90 磅(近似值)
854
+ cell_range.column_width = cell_width / 6.1 # 约 12.86 字符宽度
855
+ # 设置行高为 90 磅
856
+ cell_range.row_height = cell_height
857
+
858
+ # 将图片插入到指定单元格并填充满单元格
859
+ sheet.pictures.add(file_path,
860
+ top=cell_range.top + 5,
861
+ left=cell_range.left + 5,
862
+ width=cell_width - 10, height=cell_height - 10)
863
+
864
+ # log(f'图片已成功插入到单元格 {cell}')
865
+ except Exception as e:
866
+ log(f'插入图片失败: {e}, {file_path}')
867
+
868
+
869
+ # 插入图片函数 注意windows中这个路径反斜杠要是这样的才能插入成功
870
+ # C:\Users\Administrator/Desktop/auto/sku_img\K-CPYZB005-1_1734316546.png
871
+ def insert_skc_image_from_local(sheet, cell, file_path):
872
+ """
873
+ 从本地文件插入图片到 Excel 指定单元格。
874
+ :param sheet: xlwings 的 Sheet 对象。
875
+ :param cell: 目标单元格地址(如 'A1')。
876
+ :param file_path: 本地文件路径。
877
+ """
878
+ try:
879
+ # 打印文件路径以确保正确
880
+ log(f'插入图片的文件路径: {file_path}')
881
+
882
+ # if is_cell_has_image(sheet, cell):
883
+ # log(f'单元格 {cell} 已有图片,跳过插入。')
884
+ # return
885
+
886
+ # 获取单元格位置
887
+ cell_range = sheet.range(cell)
888
+ cell_width = cell_range.width # 获取单元格的宽度
889
+ cell_height = cell_range.height # 获取单元格的高度
890
+
891
+ # 将图片插入到指定单元格并填充满单元格
892
+ sheet.pictures.add(file_path,
893
+ top=cell_range.top + 2,
894
+ left=cell_range.left + 2,
895
+ width=86, height=88)
896
+
897
+ log(f'图片已成功插入到单元格 {cell}')
898
+ except Exception as e:
899
+ log(f'插入图片失败: {e}')
900
+
901
+
902
+ # # 设置 A 列和第 1 行为接近 100x100 的正方形
903
+ # set_square_cells(sheet, 'A', 1, 100)
904
+
905
+ def clear_all_pictures(sheet):
906
+ """
907
+ 清空 Excel Sheet 中的所有图片。
908
+
909
+ :param sheet: xlwings 的 Sheet 对象
910
+ """
911
+ try:
912
+ # 遍历并删除所有图片
913
+ for picture in sheet.pictures:
914
+ picture.delete()
915
+ log("已清空该 Sheet 上的所有图片!")
916
+ except Exception as e:
917
+ send_exception()
918
+ log(f"清空图片失败: {e}")
919
+
920
+
921
+ def get_excel_format(sheet, cell_range):
922
+ rng = sheet.range(cell_range)
923
+
924
+ format_settings = {
925
+ "numberFormat": rng.number_format,
926
+ "font": {
927
+ "name": rng.api.Font.Name,
928
+ "size": rng.api.Font.Size,
929
+ "bold": rng.api.Font.Bold,
930
+ "italic": rng.api.Font.Italic,
931
+ "color": rng.api.Font.Color
932
+ },
933
+ "alignment": {
934
+ "horizontalAlignment": rng.api.HorizontalAlignment,
935
+ "verticalAlignment": rng.api.VerticalAlignment,
936
+ "wrapText": rng.api.WrapText
937
+ },
938
+ "borders": []
939
+ }
940
+
941
+ # 获取所有边框设置(Excel 有 8 种边框)
942
+ for index in range(5, 13):
943
+ border = rng.api.Borders(index)
944
+ format_settings["borders"].append({
945
+ "index": index,
946
+ "lineStyle": border.LineStyle,
947
+ "color": border.Color,
948
+ "weight": border.Weight
949
+ })
950
+
951
+ # 获取背景色
952
+ format_settings["background"] = {
953
+ "color": rng.api.Interior.Color
954
+ }
955
+
956
+ # 获取锁定和公式隐藏
957
+ format_settings["locked"] = rng.api.Locked
958
+ format_settings["formulaHidden"] = rng.api.FormulaHidden
959
+
960
+ return json.dumps(format_settings, indent=2)
961
+
962
+
963
+ def set_excel_format(sheet, cell_range, json_setting):
964
+ settings = json.loads(json_setting)
965
+
966
+ # 解析并应用格式
967
+ rng = sheet.range(cell_range)
968
+
969
+ # 设置数字格式
970
+ if "numberFormat" in settings:
971
+ rng.number_format = settings["numberFormat"]
972
+
973
+ # 设置字体格式
974
+ if "font" in settings:
975
+ font = settings["font"]
976
+ if "name" in font:
977
+ rng.api.Font.Name = font["name"]
978
+ if "size" in font:
979
+ rng.api.Font.Size = font["size"]
980
+ if "bold" in font:
981
+ rng.api.Font.Bold = font["bold"]
982
+ if "italic" in font:
983
+ rng.api.Font.Italic = font["italic"]
984
+ if "color" in font:
985
+ rng.api.Font.Color = font["color"]
986
+
987
+ # 设置对齐方式
988
+ if "alignment" in settings:
989
+ alignment = settings["alignment"]
990
+ if "horizontalAlignment" in alignment:
991
+ rng.api.HorizontalAlignment = alignment["horizontalAlignment"]
992
+ if "verticalAlignment" in alignment:
993
+ rng.api.VerticalAlignment = alignment["verticalAlignment"]
994
+ if "wrapText" in alignment:
995
+ rng.api.WrapText = alignment["wrapText"]
996
+
997
+ # 设置边框
998
+ if "borders" in settings:
999
+ for border in settings["borders"]:
1000
+ index = border["index"]
1001
+ line_style = border["lineStyle"]
1002
+ color = border["color"]
1003
+ weight = border["weight"]
1004
+
1005
+ rng.api.Borders(index).LineStyle = line_style
1006
+ rng.api.Borders(index).Color = color
1007
+ rng.api.Borders(index).Weight = weight
1008
+
1009
+ # 设置背景
1010
+ if "background" in settings:
1011
+ bg = settings["background"]
1012
+ if "color" in bg:
1013
+ rng.api.Interior.Color = bg["color"]
1014
+
1015
+ # 设置锁定和隐藏公式
1016
+ if "locked" in settings:
1017
+ rng.api.Locked = settings["locked"]
1018
+ if "formulaHidden" in settings:
1019
+ rng.api.FormulaHidden = settings["formulaHidden"]
1020
+
1021
+
1022
+ # # 获取 A1 单元格格式
1023
+ # json_format = get_excel_format(sheet, "A1")
1024
+ # log("Original Format:", json_format)
1025
+ # # 将格式应用到 B1
1026
+ # set_excel_format(sheet, json_format, "B1")
1027
+ # log("Format copied from A1 to B1")
1028
+
1029
+ def get_unique_values(sheet, column, start_row, end_row=None):
1030
+ """
1031
+ 获取指定列从指定行开始的不重复值列表,确保读取的值与 Excel 中显示的内容完全一致。
1032
+
1033
+ 参数:
1034
+ sheet (xlwings.Sheet): Excel 工作表对象。
1035
+ column (str): 列字母(例如 'A', 'B' 等)。
1036
+ start_row (int): 开始行号。
1037
+ end_row (int, optional): 结束行号。如果未提供,则读取到列的最后一行。
1038
+
1039
+ 返回:
1040
+ list: 不重复的值列表。
1041
+ """
1042
+ # 获取指定列的区域
1043
+ if end_row:
1044
+ range_str = f"{column}{start_row}:{column}{end_row}"
1045
+ else:
1046
+ range_str = f"{column}{start_row}:{column}{sheet.range(f'{column}{start_row}').end('down').row}"
1047
+
1048
+ values = []
1049
+ for cell in sheet.range(range_str):
1050
+ # 使用 .api 获取底层 Excel 单元格的 Text 属性
1051
+ cell_value = cell.api.Text
1052
+ values.append(cell_value)
1053
+ # 将值转换为字符串并去重
1054
+ unique_values = list(set(str(value) if value is not None else "" for value in values))
1055
+ return unique_values
1056
+ # # 获取 A 列从第 2 行开始的不重复值
1057
+ # unique_values = get_unique_values(sheet, 'A', 2)
1058
+ # log(unique_values)
1059
+
1060
+
1061
+ def get_unique_values_by_row(sheet, row, start_col, end_col=None):
1062
+ """
1063
+ 获取指定行从指定列开始的不重复值列表,确保读取的值与 Excel 中显示的内容完全一致。
1064
+
1065
+ 参数:
1066
+ sheet (xlwings.Sheet): Excel 工作表对象。
1067
+ row (int): 行号。
1068
+ start_col (str): 开始列字母(例如 'A', 'B' 等)。
1069
+ end_col (str, optional): 结束列字母。如果未提供,则读取到行的最后一列。
1070
+
1071
+ 返回:
1072
+ list: 不重复的值列表。
1073
+ """
1074
+ # 获取指定行的区域
1075
+ if end_col:
1076
+ range_str = f"{start_col}{row}:{end_col}{row}"
1077
+ else:
1078
+ range_str = f"{start_col}{row}:{sheet.range(f'{start_col}{row}').end('right').column_letter}{row}"
1079
+
1080
+ values = []
1081
+ for cell in sheet.range(range_str):
1082
+ # 使用 .api 获取底层 Excel 单元格的 Text 属性
1083
+ cell_value = cell.api.Text
1084
+ values.append(cell_value)
1085
+
1086
+ # 将值转换为字符串并去重
1087
+ unique_values = list(set(str(value) if value is not None else "" for value in values))
1088
+ return unique_values
1089
+ # 获取第 2 行从 A 列开始的不重复值
1090
+ # unique_values = get_unique_values_by_row(sheet, 2, 'A')
1091
+
1092
+
1093
+ def find_rows_by_criteria(sheet, col, search_text, match_type='equals'):
1094
+ """
1095
+ 在指定列中查找符合条件的数据所在行。
1096
+
1097
+ 参数:
1098
+ sheet (xlwings.Sheet): Excel 工作表对象。
1099
+ col (str or int): 查找列号,支持列字母(如 'A')或列号(如 1),也支持负数(如 -1 表示倒数第一列)。
1100
+ search_text (str): 待查找的文本内容。
1101
+ match_type (str): 匹配方式,可选 'equals'(完全匹配)或 'contains'(包含匹配)。默认为 'equals'。
1102
+
1103
+ 返回:
1104
+ list: 包含所有符合查找标准的行号的列表。如果未找到匹配项,则返回空列表 []。
1105
+ """
1106
+ # 将列号转换为列字母
1107
+ if isinstance(col, int):
1108
+ if col < 0:
1109
+ # 处理负数列号(倒数第几列)
1110
+ col = sheet.range((1, 1)).end('right').column + col + 1
1111
+ col_letter = xw.utils.col_name(col)
1112
+ else:
1113
+ col_letter = col.upper()
1114
+
1115
+ # 获取指定列的区域
1116
+ start_cell = sheet.range(f"{col_letter}1")
1117
+ end_cell = start_cell.end('down')
1118
+ range_str = f"{col_letter}1:{col_letter}{end_cell.row}"
1119
+
1120
+ # 查找符合条件的行号
1121
+ matched_rows = []
1122
+ for cell in sheet.range(range_str):
1123
+ cell_value = cell.api.Text # 获取单元格的显示值
1124
+ # log('内部',cell_value,search_text,cell_value == search_text)
1125
+ if match_type == 'equals' and cell_value == search_text:
1126
+ matched_rows.append(cell.row)
1127
+ elif match_type == 'contains' and search_text in cell_value:
1128
+ matched_rows.append(cell.row)
1129
+
1130
+ return matched_rows
1131
+
1132
+ # # 示例 1:在 A 列中查找完全匹配 "123" 的行号
1133
+ # result_equals = find_rows_by_criteria(sheet, 'A', '123', match_type='equals')
1134
+ # log("完全匹配结果:", result_equals)
1135
+
1136
+ # # 示例 2:在 B 列中查找包含 "abc" 的行号
1137
+ # result_contains = find_rows_by_criteria(sheet, 2, 'abc', match_type='contains')
1138
+ # log("包含匹配结果:", result_contains)
1139
+
1140
+ # # 示例 3:在倒数第一列中查找完全匹配 "xyz" 的行号
1141
+ # result_negative_col = find_rows_by_criteria(sheet, -1, 'xyz', match_type='equals')
1142
+ # log("倒数第一列匹配结果:", result_negative_col)
1143
+
1144
+
1145
+ def find_columns_by_criteria(sheet, row, search_text, match_type='equals'):
1146
+ """
1147
+ 在指定行中查找符合条件的数据所在列。
1148
+
1149
+ 参数:
1150
+ sheet (xlwings.Sheet): Excel 工作表对象。
1151
+ row (int): 查找行号,支持正数(如 1)或负数(如 -1 表示倒数第一行)。
1152
+ search_text (str): 待查找的文本内容。
1153
+ match_type (str): 匹配方式,可选 'equals'(完全匹配)或 'contains'(包含匹配)。默认为 'equals'。
1154
+
1155
+ 返回:
1156
+ list: 包含所有符合查找标准的列字母的列表。如果未找到匹配项,则返回空列表 []。
1157
+ """
1158
+ # 处理负行号
1159
+ if row < 0:
1160
+ last_row = sheet.range('A1').end('down').row
1161
+ row = last_row + row + 1
1162
+
1163
+ # 获取指定行的区域
1164
+ start_cell = sheet.range(f"A{row}")
1165
+ end_cell = start_cell.end('right')
1166
+ range_str = f"A{row}:{end_cell.column_letter}{row}"
1167
+
1168
+ # 查找符合条件的列字母
1169
+ matched_columns = []
1170
+ for cell in sheet.range(range_str):
1171
+ cell_value = cell.api.Text # 获取单元格的显示值
1172
+ if match_type == 'equals' and cell_value == search_text:
1173
+ matched_columns.append(cell.column_letter)
1174
+ elif match_type == 'contains' and search_text in cell_value:
1175
+ matched_columns.append(cell.column_letter)
1176
+
1177
+ return matched_columns
1178
+ # # 示例 1:在第 1 行中查找完全匹配 "123" 的列字母
1179
+ # result_equals = find_columns_by_criteria(sheet, 1, '123', match_type='equals')
1180
+ # log("完全匹配结果:", result_equals)
1181
+
1182
+ # # 示例 2:在第 2 行中查找包含 "abc" 的列字母
1183
+ # result_contains = find_columns_by_criteria(sheet, 2, 'abc', match_type='contains')
1184
+ # log("包含匹配结果:", result_contains)
1185
+
1186
+ # # 示例 3:在倒数第一行中查找完全匹配 "xyz" 的列字母
1187
+ # result_negative_row = find_columns_by_criteria(sheet, -1, 'xyz', match_type='equals')
1188
+ # log("倒数第一行匹配结果:", result_negative_row)
1189
+
1190
+
1191
+ def check_data(data):
1192
+ for row in data:
1193
+ log(len(row), row)
1194
+
1195
+
1196
+ def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
1197
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1198
+ # 清空工作表中的所有数据
1199
+ sheet.clear()
1200
+ # 某些列以文本格式写入
1201
+ format_to_text_v2(sheet, format_to_text_colunm)
1202
+ # 写入数据
1203
+ # check_data(data)
1204
+ sheet.range('A1').value = data
1205
+ # 保存
1206
+ wb.save()
1207
+ close_excel(app, wb)
1208
+
1209
+
1210
+ def colorize_by_field(app, wb, sheet, field):
1211
+ minimize(app)
1212
+ # 读取数据
1213
+ field_column = find_column_by_data(sheet, 1, field) # 假设 SPU 在 C 列
1214
+ if field_column is None:
1215
+ return
1216
+ data_range = sheet.range(f"{field_column}1").expand("down") # 获取 SPU 列的所有数据
1217
+ spu_values = data_range.value[:]
1218
+ max_column_letter = get_max_column_letter(sheet)
1219
+ # 记录 SPU 对应的颜色
1220
+ spu_color_map = {}
1221
+ for i, spu in enumerate(spu_values): # 从 Excel 第 2 行开始(第 1 行是标题)
1222
+ row = i + 1
1223
+ if row < 2:
1224
+ continue
1225
+ if spu not in spu_color_map:
1226
+ spu_color_map[spu] = random_color() # 生成新的颜色
1227
+ bg_color = spu_color_map[spu]
1228
+ row_range = sheet.range(f"A{row}:{max_column_letter}{row}")
1229
+ row_range.color = bg_color # 应用背景色
1230
+ sheet.range(f"A{row}").api.Font.Bold = True # 让店铺名称加粗
1231
+
1232
+
1233
+ def add_borders(sheet, lineStyle=1):
1234
+ log('添加边框')
1235
+ # 获取工作表的整个范围(假设表格的数据是从A1开始)
1236
+ last_col = sheet.range('A1').end('right').column # 获取最后一列
1237
+ last_row = get_last_row(sheet, 'A')
1238
+ range_to_border = sheet.range((1, 1), (last_row, last_col)) # 定义范围
1239
+
1240
+ # 设置外部边框(所有边都为实线)
1241
+ range_to_border.api.Borders(7).LineStyle = lineStyle # 上边框
1242
+ range_to_border.api.Borders(8).LineStyle = lineStyle # 下边框
1243
+ range_to_border.api.Borders(9).LineStyle = lineStyle # 左边框
1244
+ range_to_border.api.Borders(10).LineStyle = lineStyle # 右边框
1245
+
1246
+ # 设置内部边框
1247
+ range_to_border.api.Borders(1).LineStyle = lineStyle # 内部上边框
1248
+ range_to_border.api.Borders(2).LineStyle = lineStyle # 内部下边框
1249
+ range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
1250
+ range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
1251
+
1252
+
1253
+ def add_range_border(sheet, coor_A=(1, 1), coor_B=(1, 1), lineStyle=1):
1254
+ range_to_border = sheet.range(coor_A, coor_B) # 定义范围
1255
+
1256
+ # 设置外部边框(所有边都为实线)
1257
+ range_to_border.api.Borders(7).LineStyle = lineStyle # 上边框
1258
+ range_to_border.api.Borders(8).LineStyle = lineStyle # 下边框
1259
+ range_to_border.api.Borders(9).LineStyle = lineStyle # 左边框
1260
+ range_to_border.api.Borders(10).LineStyle = lineStyle # 右边框
1261
+
1262
+ # 设置内部边框
1263
+ range_to_border.api.Borders(1).LineStyle = lineStyle # 内部上边框
1264
+ range_to_border.api.Borders(2).LineStyle = lineStyle # 内部下边框
1265
+ range_to_border.api.Borders(3).LineStyle = lineStyle # 内部左边框
1266
+ range_to_border.api.Borders(4).LineStyle = lineStyle # 内部右边框
1267
+
1268
+
1269
+ def open_excel(excel_path, sheet_name='Sheet1'):
1270
+ try:
1271
+ # 创建新实例
1272
+ app = xw.App(visible=True, add_book=False)
1273
+ app.display_alerts = False # 复用时仍然关闭警告
1274
+ app.screen_updating = True
1275
+
1276
+ # 打开或新建工作簿
1277
+ wb = None
1278
+ if os.path.exists(excel_path):
1279
+ for book in app.books:
1280
+ if book.fullname.lower() == os.path.abspath(excel_path).lower():
1281
+ wb = book
1282
+ break
1283
+ else:
1284
+ wb = app.books.open(excel_path, read_only=False)
1285
+ else:
1286
+ wb = app.books.add()
1287
+ os.makedirs(os.path.dirname(excel_path), exist_ok=True)
1288
+ wb.save(excel_path)
1289
+
1290
+ # 处理 sheet 选择逻辑(支持名称或索引)
1291
+ if isinstance(sheet_name, int): # 如果是整数,按索引获取
1292
+ if 0 <= sheet_name < len(wb.sheets): # 确保索引有效
1293
+ sheet = wb.sheets[sheet_name]
1294
+ else:
1295
+ log(f"索引 {sheet_name} 超出范围,创建新工作表。")
1296
+ sheet = wb.sheets.add(after=wb.sheets[-1])
1297
+ elif isinstance(sheet_name, str): # 如果是字符串,按名称获取
1298
+ sheet_name_clean = sheet_name.strip().lower()
1299
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
1300
+ if sheet_name_clean in sheet_names:
1301
+ sheet = wb.sheets[sheet_name]
1302
+ else:
1303
+ try:
1304
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
1305
+ except Exception as e:
1306
+ send_exception()
1307
+ return None, None, None
1308
+ else:
1309
+ send_exception(f"sheet_name 必须是字符串(名称)或整数(索引):{sheet_name}")
1310
+ raise
1311
+
1312
+ sheet.activate()
1313
+ file_name = os.path.basename(excel_path)
1314
+ log(f"open_excel {file_name} {sheet.name}")
1315
+ # 不能在这个地方最小化 容易导致错误
1316
+ # 让 Excel 窗口最小化
1317
+ # app.api.WindowState = -4140 # -4140 对应 Excel 中的 xlMinimized 常量
1318
+ return app, wb, sheet
1319
+
1320
+ except Exception as e:
1321
+ send_exception()
1322
+ # wxwork.notify_error_msg(f'打开 Excel 失败: {traceback.format_exc()}')
1323
+ return None, None, None
1324
+
1325
+
1326
+ def close_excel(app, wb):
1327
+ if wb is not None:
1328
+ wb.save()
1329
+ wb.close()
1330
+ if app is not None:
1331
+ app.quit()
1332
+
1333
+
1334
+ # 获取某列最后非空行
1335
+ def get_last_row(sheet, column):
1336
+ last_row = sheet.range(column + str(sheet.cells.last_cell.row)).end('up').row
1337
+ # 检查当前单元格是否在合并区域中
1338
+ cell = sheet.range(f'{column}{last_row}')
1339
+ # 如果当前单元格是合并单元格的一部分,获取合并区域的首行
1340
+ if cell.merge_cells:
1341
+ last_row = cell.merge_area.last_cell.row
1342
+ return last_row
1343
+
1344
+
1345
+ # 获取最后一列字母
1346
+ def get_last_col(sheet):
1347
+ # # 获取最后一行的索引
1348
+ last_col = index_to_column_name(sheet.range('A1').end('right').column) # 里面是索引 返回最后一列 如 C
1349
+ return last_col
1350
+
1351
+
1352
+ # 获取最大列名字母
1353
+ def get_max_column_letter(sheet):
1354
+ """获取当前 sheet 中最大有数据的列的列名(如 'A', 'B', ..., 'Z', 'AA', 'AB')"""
1355
+ last_col = sheet.used_range.last_cell.column # 获取最大列索引
1356
+ return xw.utils.col_name(last_col) # 将索引转换为列名
1357
+
1358
+
1359
+ # 随机生成颜色
1360
+ def random_color():
1361
+ return (random.randint(180, 255), random.randint(180, 255), random.randint(180, 255)) # 亮色背景
1362
+
1363
+
1364
+ def get_contrast_text_color(rgb):
1365
+ """根据背景色亮度返回适合的字体颜色(黑色或白色)"""
1366
+ r, g, b = rgb
1367
+ brightness = r * 0.299 + g * 0.587 + b * 0.114 # 亮度计算公式
1368
+ return (0, 0, 0) if brightness > 186 else (255, 255, 255) # 186 是经验值
1369
+
1370
+
1371
+ def rgb_to_long(r, g, b):
1372
+ """将 RGB 颜色转换为 Excel Long 类型"""
1373
+ return r + (g * 256) + (b * 256 * 256)
1374
+
1375
+
1376
+ def read_excel_to_json(file_path, sheet_name="Sheet1"):
1377
+ app, wb, sheet = open_excel(file_path, sheet_name)
1378
+
1379
+ used_range = sheet.used_range
1380
+ data = {}
1381
+ merged_cells = []
1382
+ column_widths = {} # 存储列宽度
1383
+ row_heights = {} # 存储行高度
1384
+
1385
+ # 记录列宽度
1386
+ for col in range(1, used_range.columns.count + 1):
1387
+ width = sheet.range((1, col)).column_width
1388
+ column_widths[col] = min(max(width, 1), 255) # ✅ 限制范围,防止错误
1389
+
1390
+ # 记录行高度
1391
+ for row in range(1, used_range.rows.count + 1):
1392
+ row_heights[row] = sheet.range((row, 1)).row_height # ✅ 修正行高获取方式
1393
+
1394
+ # 遍历所有单元格
1395
+ for row in range(1, used_range.rows.count + 1):
1396
+ for col in range(1, used_range.columns.count + 1):
1397
+ cell = sheet.cells(row, col)
1398
+
1399
+ # 处理对角线
1400
+ diagonal_up = cell.api.Borders(5) # 左上到右下
1401
+ diagonal_down = cell.api.Borders(6) # 右上到左下
1402
+
1403
+ diagonal_up_info = None
1404
+ diagonal_down_info = None
1405
+
1406
+ if diagonal_up.LineStyle == 1:
1407
+ diagonal_up_info = {"style": diagonal_up.LineStyle, "color": diagonal_up.Color}
1408
+
1409
+ if diagonal_down.LineStyle == 1:
1410
+ diagonal_down_info = {"style": diagonal_down.LineStyle, "color": diagonal_down.Color}
1411
+
1412
+ cell_info = {
1413
+ "value": cell.value,
1414
+ "color": cell.color,
1415
+ "font_name": cell.api.Font.Name,
1416
+ "font_size": cell.api.Font.Size,
1417
+ "bold": cell.api.Font.Bold,
1418
+ "italic": cell.api.Font.Italic,
1419
+ "font_color": cell.api.Font.Color,
1420
+ "horizontal_align": cell.api.HorizontalAlignment,
1421
+ "vertical_align": cell.api.VerticalAlignment,
1422
+ "number_format": cell.api.NumberFormat,
1423
+ "border": {
1424
+ "left": {"style": cell.api.Borders(1).LineStyle, "color": cell.api.Borders(1).Color},
1425
+ "right": {"style": cell.api.Borders(2).LineStyle, "color": cell.api.Borders(2).Color},
1426
+ "top": {"style": cell.api.Borders(3).LineStyle, "color": cell.api.Borders(3).Color},
1427
+ "bottom": {"style": cell.api.Borders(4).LineStyle, "color": cell.api.Borders(4).Color},
1428
+ }
1429
+ }
1430
+
1431
+ if diagonal_up_info:
1432
+ cell_info["border"]["diagonal_up"] = diagonal_up_info
1433
+ if diagonal_down_info:
1434
+ cell_info["border"]["diagonal_down"] = diagonal_down_info
1435
+
1436
+ data[f"{row},{col}"] = cell_info
1437
+
1438
+ # 处理合并单元格
1439
+ for merged_range in sheet.api.UsedRange.Cells:
1440
+ if merged_range.MergeCells:
1441
+ merged_cells.append({
1442
+ "merge_range": merged_range.MergeArea.Address.replace("$", "")
1443
+ })
1444
+
1445
+ wb.close()
1446
+ app.quit()
1447
+
1448
+ final_data = {
1449
+ "cells": data,
1450
+ "merged_cells": merged_cells,
1451
+ "column_widths": column_widths,
1452
+ "row_heights": row_heights
1453
+ }
1454
+
1455
+ with open("excel_data.json", "w", encoding="utf-8") as f:
1456
+ json.dump(final_data, f, indent=4, ensure_ascii=False)
1457
+
1458
+ print("✅ Excel 数据已存储为 JSON")
1459
+
1460
+
1461
+ def write_json_to_excel(json_file, new_excel="new_test.xlsx", sheet_name="Sheet1"):
1462
+ with open(json_file, "r", encoding="utf-8") as f:
1463
+ final_data = json.load(f)
1464
+
1465
+ data = final_data["cells"]
1466
+ merged_cells = final_data["merged_cells"]
1467
+ column_widths = final_data["column_widths"]
1468
+ row_heights = final_data["row_heights"]
1469
+
1470
+ app, wb, sheet = open_excel(new_excel, sheet_name)
1471
+
1472
+ for col, width in column_widths.items():
1473
+ col_name = xw.utils.col_name(int(col))
1474
+ sheet.range(f'{col_name}:{col_name}').column_width = int(width)
1475
+
1476
+ # 恢复行高度
1477
+ for row, height in row_heights.items():
1478
+ sheet.range((row, 1)).row_height = height # ✅ 修正行高恢复方式
1479
+
1480
+ for key, cell_info in data.items():
1481
+ row, col = map(int, key.split(","))
1482
+
1483
+ cell = sheet.cells(row, col)
1484
+ cell.value = cell_info["value"]
1485
+ cell.color = cell_info["color"]
1486
+ cell.api.Font.Name = cell_info["font_name"]
1487
+ cell.api.Font.Size = cell_info["font_size"]
1488
+ cell.api.Font.Bold = cell_info["bold"]
1489
+ cell.api.Font.Italic = cell_info["italic"]
1490
+ cell.api.Font.Color = cell_info["font_color"]
1491
+ cell.api.HorizontalAlignment = cell_info["horizontal_align"]
1492
+ cell.api.VerticalAlignment = cell_info["vertical_align"]
1493
+ cell.api.NumberFormat = cell_info["number_format"]
1494
+
1495
+ # 恢复边框
1496
+ for side, border_info in cell_info["border"].items():
1497
+ border_index = {"left": 1, "right": 2, "top": 3, "bottom": 4}.get(side)
1498
+ if border_index and border_info["style"] not in [None, 0]:
1499
+ cell.api.Borders(border_index).LineStyle = border_info["style"]
1500
+ cell.api.Borders(border_index).Color = border_info["color"]
1501
+
1502
+ # 恢复对角线
1503
+ if "diagonal_up" in cell_info["border"]:
1504
+ cell.api.Borders(5).LineStyle = cell_info["border"]["diagonal_up"]["style"]
1505
+ cell.api.Borders(5).Color = cell_info["border"]["diagonal_up"]["color"]
1506
+
1507
+ if "diagonal_down" in cell_info["border"]:
1508
+ cell.api.Borders(6).LineStyle = cell_info["border"]["diagonal_down"]["style"]
1509
+ cell.api.Borders(6).Color = cell_info["border"]["diagonal_down"]["color"]
1510
+
1511
+ wb.save(new_excel)
1512
+ # 恢复合并单元格
1513
+ for merge in merged_cells:
1514
+ merge_range = merge["merge_range"]
1515
+ sheet.range(merge_range).merge()
1516
+
1517
+ wb.save(new_excel)
1518
+ close_excel(app, wb)
1519
+
1520
+ print(f"✅ 数据已成功写入 {new_excel}")
1521
+ time.sleep(2) # 这里需要一个延时
1522
+
1523
+
1524
+ def safe_expand_down(sheet, start_cell='A2'):
1525
+ rng = sheet.range(start_cell)
1526
+ if not rng.value:
1527
+ return []
1528
+ try:
1529
+ return rng.expand('down')
1530
+ except Exception as e:
1531
+ log(f'safe_expand_down failed: {e}')
1532
+ return [rng] # 返回单元格本身
1533
+
1534
+
1535
+ # 初始化一个表格
1536
+ # data 需要是一个二维列表
1537
+ def init_progress_ex(key_id, excel_path, sheet_name='Sheet1'):
1538
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1539
+
1540
+ # 设置标题与格式
1541
+ expected_header = ["任务ID", "处理状态(未完成|已完成)"]
1542
+ # 只在首次或不一致时写入标题
1543
+ current_header = [sheet.range('A1').value, sheet.range('B1').value]
1544
+ if current_header != expected_header:
1545
+ sheet.range('A1').value = expected_header
1546
+ sheet.range('A:A').number_format = '@'
1547
+ log('初始化表头和格式')
1548
+ else:
1549
+ log('已存在正确表头,跳过初始化')
1550
+
1551
+ # 获取已存在的 keyID(从 A2 开始向下扩展)
1552
+ used_range = safe_expand_down(sheet, 'A2')
1553
+ existing_ids = [str(c.value) for c in used_range if c.value]
1554
+
1555
+ if str(key_id) in existing_ids:
1556
+ log(f'已存在相同任务跳过: {key_id}')
1557
+ else:
1558
+ # 找到第一列最后一个非空行
1559
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
1560
+ new_row = last_row + 1
1561
+ sheet.range(f'A{new_row}').value = [key_id, '']
1562
+ log(f'写入任务: {key_id}')
1563
+
1564
+ # 设置标题栏样式
1565
+ format_header_row(sheet, len(expected_header))
1566
+
1567
+ wb.save()
1568
+
1569
+
1570
+ def init_data_ex(key_id, excel_path, header, sheet_name='Sheet1'):
1571
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1572
+
1573
+ # 判断是否需要写入标题和设置格式
1574
+ current_header = [sheet.range(f'{index_to_column_name(i + 1)}1').value for i in range(len(header))]
1575
+ if current_header != header:
1576
+ sheet.range('A1').value = header
1577
+ sheet.range('A:A').number_format = '@'
1578
+ log('初始化表头和格式')
1579
+ else:
1580
+ log('表头已存在,跳过初始化')
1581
+
1582
+ # 检查是否已存在相同 key_id
1583
+ existing_ids = [str(cell.value) for cell in sheet.range('A2').expand('down') if cell.value]
1584
+ if str(key_id) in existing_ids:
1585
+ log(f'已初始化主键: {key_id}')
1586
+ else:
1587
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
1588
+ new_row = last_row + 1
1589
+ sheet.range(f'A{new_row}').value = [key_id, '']
1590
+ log(f'写入任务: {[key_id, ""]}')
1591
+
1592
+ # 格式化标题栏(如果是第一次设置标题)
1593
+ if current_header != header:
1594
+ format_header_row(sheet, len(header))
1595
+
1596
+ wb.save()
1597
+
1598
+
1599
+ def format_header_row(sheet, column_count):
1600
+ """
1601
+ 设置标题行样式和列对齐
1602
+ """
1603
+ for col_index in range(1, column_count + 1):
1604
+ col_letter = index_to_column_name(col_index)
1605
+ cell = sheet.range(f'{col_letter}1')
1606
+
1607
+ # 设置标题样式
1608
+ cell.color = (68, 114, 196)
1609
+ cell.font.size = 12
1610
+ cell.font.bold = True
1611
+ cell.font.color = (255, 255, 255)
1612
+
1613
+ # 设置列居中对齐
1614
+ sheet.range(f'{col_letter}:{col_letter}').api.HorizontalAlignment = -4108 # xlCenter
1615
+ sheet.range(f'{col_letter}:{col_letter}').api.VerticalAlignment = -4108 # xlCenter
1616
+
1617
+ # 自动调整列宽
1618
+ sheet.range(f'{col_letter}:{col_letter}').autofit()
1619
+
1620
+
1621
+ # 初始化一个表格
1622
+ # data 需要是一个二维列表
1623
+ def init_progress(excel_path, keyID, sheet_name='Sheet1'):
1624
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1625
+ # 覆盖写入标题
1626
+ sheet.range('A1').value = ["任务ID", "处理状态(未完成|已完成)"]
1627
+ # 覆盖写入数据
1628
+ sheet.range(f'A:A').number_format = '@' # 一般先设置格式再写入数据才起到效果 否则需要后触发格式
1629
+
1630
+ data = [[keyID, '']]
1631
+ for index, item in enumerate(data):
1632
+ keyID = item[0]
1633
+ status = item[1]
1634
+ flagRecord = True
1635
+ # 遍历可用行
1636
+ used_range_row = sheet.range('A1').expand('down')
1637
+ for i, cell in enumerate(used_range_row):
1638
+ row = i + 1
1639
+ if row < 2:
1640
+ continue
1641
+ rowKeyID = sheet.range(f'A{row}').value
1642
+ if str(rowKeyID) == str(keyID):
1643
+ log(f'已存在相同任务跳过: {keyID}')
1644
+ flagRecord = False
1645
+ break
1646
+ if flagRecord:
1647
+ # 获取第一列最后一个非空单元格的行号
1648
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
1649
+ sheet.range(f'A{last_row + 1}').value = item
1650
+ log(f'写入任务: {item}')
1651
+
1652
+ # 处理标题栏格式
1653
+ # 遍历可用列 这个要先遍历 因为要列宽自适应 会破坏前面设置好的宽度属性
1654
+ used_range_col = sheet.range('A1').expand('right')
1655
+ for j, cell in enumerate(used_range_col):
1656
+ col = j + 1
1657
+ col_name = index_to_column_name(col)
1658
+ col_val = sheet.range(f'{col_name}1').value
1659
+ # 设置标题栏字体颜色与背景色
1660
+ sheet.range(f'{col_name}1').color = (68, 114, 196)
1661
+ sheet.range(f'{col_name}1').font.size = 12
1662
+ sheet.range(f'{col_name}1').font.bold = True
1663
+ sheet.range(f'{col_name}1').font.color = (255, 255, 255)
1664
+ # 所有列水平居中和垂直居中
1665
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4108
1666
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
1667
+
1668
+ sheet.range(f'{col_name}:{col_name}').autofit()
1669
+
1670
+ wb.save()
1671
+
1672
+
1673
+ def get_progress(excel_path, keyID, sheet_name="Sheet1"):
1674
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1675
+ # 遍历可用行
1676
+ used_range_row = sheet.range('A1').expand('down')
1677
+ for i, cell in enumerate(used_range_row):
1678
+ row = i + 1
1679
+ if row < 2:
1680
+ continue
1681
+ rowKeyID = sheet.range(f'A{row}').value;
1682
+ if rowKeyID == keyID:
1683
+ result = sheet.range(f'B{row}').value;
1684
+ if result == "已完成":
1685
+ return True
1686
+ else:
1687
+ return False
1688
+
1689
+
1690
+ def get_progress_ex(keyID, excel_path, sheet_name="Sheet1"):
1691
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1692
+ # 遍历可用行
1693
+ used_range_row = sheet.range('A1').expand('down')
1694
+ for i, cell in enumerate(used_range_row):
1695
+ row = i + 1
1696
+ if row < 2:
1697
+ continue
1698
+ rowKeyID = sheet.range(f'A{row}').value;
1699
+ if rowKeyID == keyID:
1700
+ result = sheet.range(f'B{row}').value;
1701
+ if result == "已完成":
1702
+ return True
1703
+ else:
1704
+ return False
1705
+ close_excel(app, wb)
1706
+
1707
+
1708
+ def get_progress_data(excel_path, keyID, sheet_name="Sheet1"):
1709
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1710
+ # 遍历可用行
1711
+ used_range_row = sheet.range('A1').expand('down')
1712
+ for i, cell in enumerate(used_range_row):
1713
+ row = i + 1
1714
+ if row < 2:
1715
+ continue
1716
+ rowKeyID = sheet.range(f'A{row}').value
1717
+ if rowKeyID == keyID:
1718
+ result = sheet.range(f'C{row}').value
1719
+ return result
1720
+ return None
1721
+
1722
+
1723
+ def get_progress_data_ex(keyID, excel_path, sheet_name="Sheet1"):
1724
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1725
+ # 遍历可用行
1726
+ used_range_row = sheet.range('A1').expand('down')
1727
+ for i, cell in enumerate(used_range_row):
1728
+ row = i + 1
1729
+ if row < 2:
1730
+ continue
1731
+ rowKeyID = sheet.range(f'A{row}').value
1732
+ if rowKeyID == keyID:
1733
+ result = sheet.range(f'C{row}').value
1734
+ return result
1735
+ return None
1736
+
1737
+
1738
+ def set_progress(excel_path, keyID, status='已完成', sheet_name="Sheet1"):
1739
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1740
+ # 遍历可用行
1741
+ used_range_row = sheet.range('A1').expand('down')
1742
+ for i, cell in enumerate(used_range_row):
1743
+ row = i + 1
1744
+ if row < 2:
1745
+ continue
1746
+ rowKeyID = sheet.range(f'A{row}').value
1747
+ if str(rowKeyID) == str(keyID):
1748
+ sheet.range(f'B{row}').value = status
1749
+ wb.save()
1750
+ return
1751
+
1752
+
1753
+ def set_progress_ex(keyID, excel_path, status='已完成', sheet_name="Sheet1"):
1754
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1755
+ # 遍历可用行
1756
+ used_range_row = sheet.range('A1').expand('down')
1757
+ for i, cell in enumerate(used_range_row):
1758
+ row = i + 1
1759
+ if row < 2:
1760
+ continue
1761
+ rowKeyID = sheet.range(f'A{row}').value
1762
+ if str(rowKeyID) == str(keyID):
1763
+ sheet.range(f'B{row}').value = status
1764
+ wb.save()
1765
+ close_excel(app, wb)
1766
+ return
1767
+ close_excel(app, wb)
1768
+
1769
+
1770
+ def set_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
1771
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1772
+ # 遍历可用行
1773
+ used_range_row = sheet.range('A1').expand('down')
1774
+ for i, cell in enumerate(used_range_row):
1775
+ row = i + 1
1776
+ if row < 2:
1777
+ continue
1778
+ rowKeyID = sheet.range(f'A{row}').value
1779
+ if str(rowKeyID) == str(keyID):
1780
+ sheet.range(f'A{row}').value = data
1781
+ wb.save()
1782
+ return
1783
+
1784
+
1785
+ def set_progress_data(excel_path, keyID, data, sheet_name="Sheet1"):
1786
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1787
+ # 遍历可用行
1788
+ used_range_row = sheet.range('A1').expand('down')
1789
+ for i, cell in enumerate(used_range_row):
1790
+ row = i + 1
1791
+ if row < 2:
1792
+ continue
1793
+ rowKeyID = sheet.range(f'A{row}').value
1794
+ if str(rowKeyID) == str(keyID):
1795
+ log('设置数据', data)
1796
+ sheet.range(f'C{row}').value = data
1797
+ wb.save()
1798
+ return
1799
+
1800
+
1801
+ def set_progress_data_ex(keyID, data, excel_path, sheet_name="Sheet1"):
1802
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1803
+ # 遍历可用行
1804
+ used_range_row = sheet.range('A1').expand('down')
1805
+ for i, cell in enumerate(used_range_row):
1806
+ row = i + 1
1807
+ if row < 2:
1808
+ continue
1809
+ rowKeyID = sheet.range(f'A{row}').value
1810
+ if str(rowKeyID) == str(keyID):
1811
+ log('设置数据', data)
1812
+ sheet.range(f'C{row}').value = data
1813
+ wb.save()
1814
+ return
1815
+
1816
+
1817
+ def check_progress(excel_path, listKeyID, sheet_name="Sheet1"):
1818
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1819
+ # 读取整个任务表数据
1820
+ data = sheet.used_range.value
1821
+ data = [row for row in data if any(row)] # 过滤掉空行
1822
+ # 任务ID和状态列索引
1823
+ task_id_col = 0
1824
+ status_col = 1
1825
+ # 创建任务ID与状态的字典
1826
+ task_status_dict = {row[task_id_col]: row[status_col] for row in data[1:] if row[task_id_col]}
1827
+ # 找出未完成的任务
1828
+ incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
1829
+ return len(incomplete_tasks) == 0, incomplete_tasks
1830
+
1831
+
1832
+ def check_progress_ex(listKeyID, excel_path, sheet_name="Sheet1"):
1833
+ app, wb, sheet = open_excel(excel_path, sheet_name)
1834
+ # 读取整个任务表数据
1835
+ data = sheet.used_range.value
1836
+ data = [row for row in data if any(row)] # 过滤掉空行
1837
+ # 任务ID和状态列索引
1838
+ task_id_col = 0
1839
+ status_col = 1
1840
+ # 创建任务ID与状态的字典
1841
+ task_status_dict = {row[task_id_col]: row[status_col] for row in data[1:] if row[task_id_col]}
1842
+ # 找出未完成的任务
1843
+ incomplete_tasks = [task_id for task_id in listKeyID if task_status_dict.get(task_id) != "已完成"]
1844
+ return len(incomplete_tasks) == 0, incomplete_tasks
1845
+
1846
+
1847
+ def read_excel_sheet_to_list(file_path, sheet_name=None):
1848
+ """
1849
+ 使用 xlwings 读取 Excel 文件中指定工作表的数据,并返回为二维列表。
1850
+
1851
+ :param file_path: Excel 文件路径
1852
+ :param sheet_name: 要读取的 sheet 名称(默认读取第一个 sheet)
1853
+ :return: 二维列表形式的数据
1854
+ """
1855
+ app, wb, sheet = open_excel(file_path, sheet_name)
1856
+ used_range = sheet.used_range
1857
+ data = used_range.value # 返回为二维列表或一维列表(取决于数据)
1858
+ close_excel(app, wb)
1859
+ time.sleep(2)
1860
+ # 保证返回的是二维列表
1861
+ if not data:
1862
+ return []
1863
+ elif isinstance(data[0], list):
1864
+ return data
1865
+ else:
1866
+ return [data]
1867
+
1868
+
1869
+ def excel_to_dict(excel_path, column_key, column_value, sheet_name=None):
1870
+ """
1871
+ 从 Excel 文件中读取指定两列,生成字典返回(不受中间空行影响)
1872
+
1873
+ :param excel_path: Excel 文件路径
1874
+ :param column_key: 键所在列,比如 'A' 或 1(从1开始)
1875
+ :param column_value: 值所在列,比如 'B' 或 2
1876
+ :param sheet_name: 可选,指定sheet名称,默认第一个sheet
1877
+ :return: dict
1878
+ """
1879
+ app = xw.App(visible=False)
1880
+ wb = None
1881
+ try:
1882
+ wb = app.books.open(excel_path)
1883
+ sheet = wb.sheets[sheet_name] if sheet_name else wb.sheets[0]
1884
+
1885
+ # 如果列是数字,转为列字母
1886
+ if isinstance(column_key, int):
1887
+ column_key = xw.utils.col_name(column_key)
1888
+ if isinstance(column_value, int):
1889
+ column_value = xw.utils.col_name(column_value)
1890
+
1891
+ # 获取 used range 的总行数
1892
+ used_rows = sheet.used_range.last_cell.row
1893
+
1894
+ # 获取整列值(从第2行开始,跳过标题)
1895
+ keys = sheet.range(f'{column_key}2:{column_key}{used_rows}').value
1896
+ values = sheet.range(f'{column_value}2:{column_value}{used_rows}').value
1897
+
1898
+ # 容错:如果只有一个值会变成单个元素,需变成列表
1899
+ if not isinstance(keys, list):
1900
+ keys = [keys]
1901
+ if not isinstance(values, list):
1902
+ values = [values]
1903
+
1904
+ # 构建字典,忽略空键
1905
+ result = {
1906
+ str(k).strip().lower(): (str(v).strip() if v is not None else '-')
1907
+ for k, v in zip(keys, values)
1908
+ if k is not None and str(k).strip() != ""
1909
+ }
1910
+ return result
1911
+ finally:
1912
+ if wb is not None:
1913
+ wb.close()
1914
+ app.quit()
1915
+
1916
+
1917
+ def format_to_text_v2(sheet, columns=None):
1918
+ if columns is None or len(columns) == 0:
1919
+ return
1920
+ for col_name in columns:
1921
+ if isinstance(col_name, int):
1922
+ col_name = xw.utils.col_name(col_name)
1923
+ log(f'设置[{col_name}] 文本格式')
1924
+ sheet.range(f'{col_name}:{col_name}').number_format = '@'
1925
+
1926
+
1927
+ def format_to_text(sheet, columns=None):
1928
+ if columns is None:
1929
+ return
1930
+ used_range_col = sheet.range('A1').expand('right')
1931
+ for j, cell in enumerate(used_range_col):
1932
+ col = j + 1
1933
+ col_name = index_to_column_name(col)
1934
+ col_val = sheet.range(f'{col_name}1').value
1935
+ for c in columns:
1936
+ if str(c).lower() in str(col_val).lower():
1937
+ log(f'设置[{c}] 文本格式')
1938
+ sheet.range(f'{col_name}:{col_name}').number_format = '@'
1939
+
1940
+
1941
+ def format_to_date(sheet, columns=None):
1942
+ if columns is None:
1943
+ return
1944
+ used_range_col = sheet.range('A1').expand('right')
1945
+ for j, cell in enumerate(used_range_col):
1946
+ col = j + 1
1947
+ col_name = index_to_column_name(col)
1948
+ col_val = sheet.range(f'{col_name}1').value
1949
+ if col_val is None:
1950
+ continue
1951
+ for c in columns:
1952
+ if c in col_val:
1953
+ log(f'设置[{c}] 时间格式')
1954
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd'
1955
+
1956
+
1957
+ def format_to_datetime(sheet, columns=None):
1958
+ if columns is None:
1959
+ return
1960
+ used_range_col = sheet.range('A1').expand('right')
1961
+ for j, cell in enumerate(used_range_col):
1962
+ col = j + 1
1963
+ col_name = index_to_column_name(col)
1964
+ col_val = sheet.range(f'{col_name}1').value
1965
+ if col_val is None:
1966
+ continue
1967
+ for c in columns:
1968
+ if c in col_val:
1969
+ log(f'设置[{c}] 时间格式')
1970
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm-dd hh:mm:ss'
1971
+
1972
+
1973
+ def format_to_month(sheet, columns=None):
1974
+ if columns is None:
1975
+ return
1976
+ used_range_col = sheet.range('A1').expand('right')
1977
+ for j, cell in enumerate(used_range_col):
1978
+ col = j + 1
1979
+ col_name = index_to_column_name(col)
1980
+ col_val = sheet.range(f'{col_name}1').value
1981
+ for c in columns:
1982
+ if c in col_val:
1983
+ log(f'设置[{c}] 年月格式')
1984
+ sheet.range(f'{col_name}:{col_name}').number_format = 'yyyy-mm'
1985
+
1986
+
1987
+ def add_sum_for_cell(sheet, col_list, row=2):
1988
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
1989
+ for col_name in col_list:
1990
+ col_letter = find_column_by_data(sheet, 1, col_name)
1991
+ sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
1992
+ sheet.range(f'{col_letter}{row}').api.Font.Color = 255
1993
+ sheet.range(f'{col_letter}:{col_letter}').autofit()
1994
+
1995
+
1996
+ def clear_for_cell(sheet, col_list, row=2):
1997
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
1998
+ for col_name in col_list:
1999
+ col_letter = find_column_by_data(sheet, 1, col_name)
2000
+ sheet.range(f'{col_letter}{row}').value = ''
2001
+
2002
+
2003
+ def color_for_column(sheet, col_list, color_name, start_row=2):
2004
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2005
+ for col_name in col_list:
2006
+ col_letter = find_column_by_data(sheet, 1, col_name)
2007
+ if last_row > start_row:
2008
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api.Font.ColorIndex = excel_color_index[
2009
+ color_name]
2010
+
2011
+
2012
+ def add_formula_for_column(sheet, col_name, formula, start_row=2):
2013
+ last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
2014
+ col_letter = find_column_by_data(sheet, 1, col_name)
2015
+ if last_row >= start_row:
2016
+ # 第3行公式(填一次)
2017
+ sheet.range(f'{col_letter}{start_row}').formula = formula
2018
+ if '汇总' in col_name:
2019
+ sheet.range(f'{col_letter}{start_row}').api.Font.Color = 255
2020
+ if last_row > start_row:
2021
+ # AutoFill 快速填充到所有行(start_row 到 last_row)
2022
+ sheet.range(f'{col_letter}{start_row}').api.AutoFill(
2023
+ sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api)
2024
+
2025
+
2026
+ def autofit_column(sheet, columns=None):
2027
+ if columns is None:
2028
+ return
2029
+ used_range_col = sheet.range('A1').expand('right')
2030
+ for j, cell in enumerate(used_range_col):
2031
+ col = j + 1
2032
+ col_name = index_to_column_name(col)
2033
+ col_val = sheet.range(f'{col_name}1').value
2034
+ if col_val is None:
2035
+ continue
2036
+ for c in columns:
2037
+ if c in col_val:
2038
+ log(f'设置[{c}] 宽度自适应')
2039
+ sheet.range(f'{col_name}:{col_name}').api.WrapText = False
2040
+ sheet.range(f'{col_name}:{col_name}').autofit()
2041
+ sheet.range(f'{col_name}:{col_name}').api.WrapText = True
2042
+ sheet.range(f'{col_name}:{col_name}').autofit()
2043
+
2044
+
2045
+ def specify_column_width(sheet, columns=None, width=150):
2046
+ if columns is None:
2047
+ return
2048
+ used_range_col = sheet.range('A1').expand('right')
2049
+ for j, cell in enumerate(used_range_col):
2050
+ col = j + 1
2051
+ col_name = index_to_column_name(col)
2052
+ col_val = sheet.range(f'{col_name}1').value
2053
+ if col_val is None:
2054
+ continue
2055
+ for c in columns:
2056
+ if c in col_val:
2057
+ log(f'设置[{c}]宽度: {width}')
2058
+ sheet.range(f'{col_name}:{col_name}').column_width = width
2059
+
2060
+
2061
+ def format_to_money(sheet, columns=None):
2062
+ if columns is None:
2063
+ return
2064
+ used_range_col = sheet.range('A1').expand('right')
2065
+ for j, cell in enumerate(used_range_col):
2066
+ col = j + 1
2067
+ col_name = index_to_column_name(col)
2068
+ col_val = sheet.range(f'{col_name}1').value
2069
+ if col_val is None:
2070
+ continue
2071
+ for c in columns:
2072
+ if c in col_val:
2073
+ log(f'设置[{c}] 金额格式')
2074
+ sheet.range(f'{col_name}:{col_name}').number_format = '¥#,##0.00'
2075
+
2076
+
2077
+ def format_to_percent(sheet, columns=None, decimal_places=2):
2078
+ if columns is None:
2079
+ return
2080
+ used_range_col = sheet.range('A1').expand('right')
2081
+ for j, cell in enumerate(used_range_col):
2082
+ col = j + 1
2083
+ col_name = index_to_column_name(col)
2084
+ col_val = sheet.range(f'{col_name}1').value
2085
+ if col_val is None:
2086
+ continue
2087
+ for c in columns:
2088
+ if c in col_val:
2089
+ log(f'设置[{c}] 百分比格式')
2090
+ # 根据 decimal_places 决定格式
2091
+ if decimal_places == 0:
2092
+ sheet.range(f'{col_name}:{col_name}').number_format = '0%'
2093
+ else:
2094
+ sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}%'
2095
+
2096
+
2097
+ def format_to_number(sheet, columns=None, decimal_places=2):
2098
+ if not columns or not isinstance(columns, (list, tuple, set)):
2099
+ log(f'未提供有效列名列表({columns}),跳过格式转换')
2100
+ return
2101
+
2102
+ decimal_places = max(0, int(decimal_places)) # 确保非负整数
2103
+ used_range_col = sheet.range('A1').expand('right')
2104
+
2105
+ for j, cell in enumerate(used_range_col):
2106
+ col = j + 1
2107
+ col_name = index_to_column_name(col)
2108
+ col_val = sheet.range(f'{col_name}1').value
2109
+
2110
+ if col_val is None:
2111
+ continue
2112
+
2113
+ col_val = str(col_val) # 确保转为字符串比较
2114
+ for c in columns:
2115
+ if c in col_val:
2116
+ log(f'设置 [{c}] 列为数字格式,小数位 {decimal_places}')
2117
+ number_format = '0' if decimal_places == 0 else f'0.{"0" * decimal_places}'
2118
+ sheet.range(f'{col_name}:{col_name}').number_format = number_format
2119
+ break # 如果一列只匹配一个关键词可提前退出
2120
+
2121
+
2122
+ # def format_to_number(sheet, columns=None, decimal_places=2):
2123
+ # if columns is None or not isinstance(columns, list):
2124
+ # log('跳过格式化成数字', columns)
2125
+ # return
2126
+ # used_range_col = sheet.range('A1').expand('right')
2127
+ # for j, cell in enumerate(used_range_col):
2128
+ # col = j + 1
2129
+ # col_name = index_to_column_name(col)
2130
+ # col_val = sheet.range(f'{col_name}1').value
2131
+ # if col_val is None:
2132
+ # continue
2133
+ # for c in columns:
2134
+ # if c in col_val:
2135
+ # log(f'设置[{c}] 数字格式')
2136
+ # # 根据 decimal_places 决定格式
2137
+ # if decimal_places == 0:
2138
+ # sheet.range(f'{col_name}:{col_name}').number_format = '0'
2139
+ # else:
2140
+ # sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}'
2141
+
2142
+ def column_to_right(sheet, columns=None):
2143
+ if columns is None:
2144
+ return
2145
+ used_range_col = sheet.range('A1').expand('right')
2146
+ for j, cell in enumerate(used_range_col):
2147
+ col = j + 1
2148
+ col_name = index_to_column_name(col)
2149
+ col_val = sheet.range(f'{col_name}1').value
2150
+ if col_val is None:
2151
+ continue
2152
+ for c in columns:
2153
+ if c in col_val:
2154
+ # 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
2155
+ # 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
2156
+ # 所有列水平居中和垂直居中
2157
+ log(f'设置[{c}] 水平垂直居中')
2158
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4152
2159
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2160
+
2161
+
2162
+ def column_to_left(sheet, columns=None):
2163
+ if columns is None:
2164
+ return
2165
+ used_range_col = sheet.range('A1').expand('right')
2166
+ for j, cell in enumerate(used_range_col):
2167
+ col = j + 1
2168
+ col_name = index_to_column_name(col)
2169
+ col_val = sheet.range(f'{col_name}1').value
2170
+ if col_val is None:
2171
+ continue
2172
+ for c in columns:
2173
+ if c in col_val:
2174
+ # 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
2175
+ # 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
2176
+ # 所有列水平居中和垂直居中
2177
+ log(f'设置[{c}] 左对齐')
2178
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4131
2179
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2180
+
2181
+
2182
+ def beautify_title(sheet):
2183
+ log('美化标题')
2184
+ used_range_col = sheet.range('A1').expand('right')
2185
+ for j, cell in enumerate(used_range_col):
2186
+ col = j + 1
2187
+ col_name = index_to_column_name(col)
2188
+
2189
+ # 设置标题栏字体颜色与背景色
2190
+ sheet.range(f'{col_name}1').color = (68, 114, 196)
2191
+ sheet.range(f'{col_name}1').font.size = 12
2192
+ sheet.range(f'{col_name}1').font.bold = True
2193
+ sheet.range(f'{col_name}1').font.color = (255, 255, 255)
2194
+
2195
+ # 所有列水平居中和垂直居中
2196
+ sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4108
2197
+ sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2198
+ sheet.autofit()
2199
+
2200
+
2201
+ def set_title_style(sheet, rows=2):
2202
+ col = get_max_column_letter(sheet)
2203
+ range = sheet.range(f'A1:{col}{rows}')
2204
+ # 设置字体名称
2205
+ range.font.name = '微软雅黑'
2206
+ # 设置字体大小
2207
+ range.font.size = 11
2208
+ # 设置字体加粗
2209
+ range.font.bold = True
2210
+ # 设置标题栏字体颜色与背景色
2211
+ range.color = (252, 228, 214)
2212
+ # 设置行高
2213
+ range.row_height = 30
2214
+
2215
+ # 获取已使用范围
2216
+ used_range = sheet.used_range
2217
+ # 设置水平居中对齐
2218
+ used_range.api.HorizontalAlignment = xw.constants.HAlign.xlHAlignCenter
2219
+ used_range.api.VerticalAlignment = xw.constants.HAlign.xlHAlignCenter
2220
+
2221
+ sheet.autofit()
2222
+
2223
+
2224
+ def move_sheet_to_position(wb, sheet_name, position):
2225
+ # 获取要移动的工作表
2226
+ sheet = wb.sheets[sheet_name]
2227
+ # 获取目标位置的参考工作表
2228
+ if position == 1:
2229
+ # 如果目标位置是第一个,将其移至最前
2230
+ sheet.api.Move(Before=wb.sheets[0].api)
2231
+ else:
2232
+ # 如果目标位置不是第一个,将其移至目标位置之前
2233
+ sheet.api.Move(Before=wb.sheets[position - 1].api)
2234
+ # 保存工作簿
2235
+ wb.save()
2236
+
2237
+
2238
+ # Excel 文件锁管理器
2239
+ import threading
2240
+ import time
2241
+ from collections import defaultdict
2242
+
2243
+
2244
+ class ExcelFileLockManager:
2245
+ """Excel 文件锁管理器,用于管理不同 Excel 文件的并发访问"""
2246
+
2247
+ def __init__(self):
2248
+ self._locks = defaultdict(threading.Lock)
2249
+ self._excel_instances = {} # 存储已打开的 Excel 实例
2250
+ self._lock = threading.Lock() # 保护内部数据结构的锁
2251
+ self._waiting_queue = defaultdict(list) # 等待队列,按文件路径分组
2252
+ self._operation_count = defaultdict(int) # 记录每个文件的操作次数
2253
+ self._max_wait_time = 300 # 最大等待时间(秒)
2254
+
2255
+ def get_file_lock(self, excel_path):
2256
+ """获取指定 Excel 文件的锁"""
2257
+ return self._locks[excel_path]
2258
+
2259
+ def acquire_excel_lock(self, excel_path, timeout=30, priority=0):
2260
+ """
2261
+ 获取 Excel 文件锁,支持超时和优先级
2262
+
2263
+ Args:
2264
+ excel_path: Excel 文件路径
2265
+ timeout: 超时时间(秒)
2266
+ priority: 优先级,数字越小优先级越高
2267
+
2268
+ Returns:
2269
+ bool: 是否成功获取锁
2270
+ """
2271
+ lock = self.get_file_lock(excel_path)
2272
+
2273
+ # 记录等待请求
2274
+ with self._lock:
2275
+ self._waiting_queue[excel_path].append({
2276
+ 'priority': priority,
2277
+ 'timestamp': time.time(),
2278
+ 'thread_id': threading.get_ident()
2279
+ })
2280
+ # 按优先级排序
2281
+ self._waiting_queue[excel_path].sort(key=lambda x: (x['priority'], x['timestamp']))
2282
+
2283
+ try:
2284
+ acquired = lock.acquire(timeout=timeout)
2285
+ if acquired:
2286
+ # 记录操作次数
2287
+ with self._lock:
2288
+ self._operation_count[excel_path] += 1
2289
+ # 从等待队列中移除
2290
+ self._waiting_queue[excel_path] = [
2291
+ item for item in self._waiting_queue[excel_path]
2292
+ if item['thread_id'] != threading.get_ident()
2293
+ ]
2294
+ log(f"成功获取 Excel 文件锁: {os.path.basename(excel_path)} (优先级: {priority})")
2295
+ return True
2296
+ else:
2297
+ log(f"获取 Excel 文件锁超时: {excel_path} (优先级: {priority})")
2298
+ return False
2299
+ except Exception as e:
2300
+ log(f"获取 Excel 文件锁异常: {e}")
2301
+ return False
2302
+
2303
+ def release_excel_lock(self, excel_path):
2304
+ """释放 Excel 文件锁"""
2305
+ lock = self.get_file_lock(excel_path)
2306
+ if lock.locked():
2307
+ lock.release()
2308
+ log(f"释放 Excel 文件锁: {os.path.basename(excel_path)}")
2309
+
2310
+ def get_excel_instance(self, excel_path):
2311
+ """获取已打开的 Excel 实例"""
2312
+ with self._lock:
2313
+ return self._excel_instances.get(excel_path)
2314
+
2315
+ def set_excel_instance(self, excel_path, app, wb):
2316
+ """设置 Excel 实例"""
2317
+ with self._lock:
2318
+ self._excel_instances[excel_path] = (app, wb)
2319
+
2320
+ def remove_excel_instance(self, excel_path):
2321
+ """移除 Excel 实例"""
2322
+ with self._lock:
2323
+ self._excel_instances.pop(excel_path, None)
2324
+
2325
+ def is_excel_open(self, excel_path):
2326
+ """检查 Excel 文件是否已打开"""
2327
+ return excel_path in self._excel_instances
2328
+
2329
+ def get_waiting_count(self, excel_path):
2330
+ """获取等待该文件的线程数量"""
2331
+ with self._lock:
2332
+ return len(self._waiting_queue[excel_path])
2333
+
2334
+ def get_operation_count(self, excel_path):
2335
+ """获取该文件的操作次数"""
2336
+ with self._lock:
2337
+ return self._operation_count[excel_path]
2338
+
2339
+ def cleanup_old_instances(self, max_age=3600):
2340
+ """清理过期的 Excel 实例"""
2341
+ current_time = time.time()
2342
+ with self._lock:
2343
+ expired_files = []
2344
+ for excel_path, (app, wb) in self._excel_instances.items():
2345
+ # 这里可以添加更复杂的清理逻辑
2346
+ # 比如检查文件最后访问时间等
2347
+ pass
2348
+
2349
+
2350
+ # 全局 Excel 文件锁管理器实例
2351
+ excel_lock_manager = ExcelFileLockManager()
2352
+
2353
+
2354
+ def open_excel_with_lock(excel_path, sheet_name='Sheet1', timeout=30):
2355
+ """
2356
+ 带锁的 Excel 打开函数,支持复用已打开的实例
2357
+
2358
+ Args:
2359
+ excel_path: Excel 文件路径
2360
+ sheet_name: 工作表名称
2361
+ timeout: 获取锁的超时时间(秒)
2362
+
2363
+ Returns:
2364
+ tuple: (app, wb, sheet) 或 (None, None, None) 如果失败
2365
+ """
2366
+ if not excel_lock_manager.acquire_excel_lock(excel_path, timeout):
2367
+ return None, None, None
2368
+
2369
+ try:
2370
+ # 检查是否已有打开的实例
2371
+ existing_instance = excel_lock_manager.get_excel_instance(excel_path)
2372
+ if existing_instance:
2373
+ app, wb = existing_instance
2374
+ # 检查工作簿是否仍然有效
2375
+ try:
2376
+ if wb.name in [book.name for book in app.books]:
2377
+ # 获取指定的工作表
2378
+ if isinstance(sheet_name, int):
2379
+ if 0 <= sheet_name < len(wb.sheets):
2380
+ sheet = wb.sheets[sheet_name]
2381
+ else:
2382
+ sheet = wb.sheets.add(after=wb.sheets[-1])
2383
+ elif isinstance(sheet_name, str):
2384
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
2385
+ if sheet_name.strip().lower() in sheet_names:
2386
+ sheet = wb.sheets[sheet_name]
2387
+ else:
2388
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2389
+ else:
2390
+ raise ValueError(f"sheet_name 必须是字符串或整数: {sheet_name}")
2391
+
2392
+ sheet.activate()
2393
+ log(f"复用已打开的 Excel: {os.path.basename(excel_path)} {sheet.name}")
2394
+ return app, wb, sheet
2395
+ except Exception as e:
2396
+ log(f"复用 Excel 实例失败,重新打开: {e}")
2397
+ # 移除无效的实例
2398
+ excel_lock_manager.remove_excel_instance(excel_path)
2399
+
2400
+ # 打开新的 Excel 实例
2401
+ app, wb, sheet = open_excel(excel_path, sheet_name)
2402
+ if app and wb:
2403
+ excel_lock_manager.set_excel_instance(excel_path, app, wb)
2404
+ log(f"打开新的 Excel 实例: {os.path.basename(excel_path)} {sheet.name}")
2405
+
2406
+ return app, wb, sheet
2407
+
2408
+ except Exception as e:
2409
+ log(f"打开 Excel 失败: {e}")
2410
+ excel_lock_manager.release_excel_lock(excel_path)
2411
+ return None, None, None
2412
+
2413
+
2414
+ def close_excel_with_lock(excel_path, app, wb, force_close=False):
2415
+ """
2416
+ 带锁的 Excel 关闭函数
2417
+
2418
+ Args:
2419
+ excel_path: Excel 文件路径
2420
+ app: Excel 应用实例
2421
+ wb: 工作簿实例
2422
+ force_close: 是否强制关闭(即使有其他操作在进行)
2423
+ """
2424
+ try:
2425
+ if force_close:
2426
+ # 强制关闭,移除实例记录
2427
+ excel_lock_manager.remove_excel_instance(excel_path)
2428
+ close_excel(app, wb)
2429
+ else:
2430
+ # 只保存,不关闭
2431
+ if wb:
2432
+ wb.save()
2433
+ log(f"保存 Excel 文件: {os.path.basename(excel_path)}")
2434
+ except Exception as e:
2435
+ log(f"关闭 Excel 失败: {e}")
2436
+ finally:
2437
+ excel_lock_manager.release_excel_lock(excel_path)
2438
+
2439
+
2440
+ def write_data_with_lock(excel_path, sheet_name, data, format_to_text_colunm=None):
2441
+ """
2442
+ 带锁的数据写入函数,复用 Excel 实例
2443
+
2444
+ Args:
2445
+ excel_path: Excel 文件路径
2446
+ sheet_name: 工作表名称
2447
+ data: 要写入的数据
2448
+ format_to_text_colunm: 格式化为文本的列
2449
+ """
2450
+ app, wb, sheet = open_excel_with_lock(excel_path, sheet_name)
2451
+ if not app or not wb or not sheet:
2452
+ log(f"无法打开 Excel 文件: {excel_path}")
2453
+ return False
2454
+
2455
+ try:
2456
+ # 清空工作表中的所有数据
2457
+ sheet.clear()
2458
+ # 某些列以文本格式写入
2459
+ format_to_text_v2(sheet, format_to_text_colunm)
2460
+ # 写入数据
2461
+ sheet.range('A1').value = data
2462
+ # 保存
2463
+ wb.save()
2464
+ log(f"成功写入数据到 {sheet_name}")
2465
+ return True
2466
+ except Exception as e:
2467
+ log(f"写入数据失败: {e}")
2468
+ return False
2469
+
2470
+
2471
+ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs):
2472
+ """
2473
+ 带锁的 Excel 格式化函数
2474
+
2475
+ Args:
2476
+ excel_path: Excel 文件路径
2477
+ sheet_name: 工作表名称
2478
+ format_func: 格式化函数
2479
+ *args, **kwargs: 传递给格式化函数的参数
2480
+ """
2481
+ app, wb, sheet = open_excel_with_lock(excel_path, sheet_name)
2482
+ if not app or not wb or not sheet:
2483
+ log(f"无法打开 Excel 文件进行格式化: {excel_path}")
2484
+ return False
2485
+
2486
+ try:
2487
+ # 执行格式化函数
2488
+ format_func(sheet, *args, **kwargs)
2489
+ # 保存
2490
+ wb.save()
2491
+ log(f"成功格式化工作表: {sheet_name}")
2492
+ return True
2493
+ except Exception as e:
2494
+ log(f"格式化失败: {e}")
2495
+ return False
2496
+
2497
+
2498
+ def batch_excel_operations(excel_path, operations):
2499
+ """
2500
+ 批量 Excel 操作函数,一次性打开 Excel 执行多个操作
2501
+
2502
+ Args:
2503
+ excel_path: Excel 文件路径
2504
+ operations: 操作列表,每个操作是 (sheet_name, operation_type, data, format_func) 的元组
2505
+ operation_type: 'write' 或 'format'
2506
+ data: 写入的数据(仅 write 操作需要)
2507
+ format_func: 格式化函数(仅 format 操作需要)
2508
+
2509
+ Returns:
2510
+ bool: 是否全部操作成功
2511
+ """
2512
+ app, wb, sheet = open_excel_with_lock(excel_path)
2513
+ if not app or not wb:
2514
+ log(f"无法打开 Excel 文件: {excel_path}")
2515
+ return False
2516
+
2517
+ try:
2518
+ for sheet_name, operation_type, *args in operations:
2519
+ # 获取或创建工作表
2520
+ if isinstance(sheet_name, str):
2521
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
2522
+ if sheet_name.strip().lower() in sheet_names:
2523
+ sheet = wb.sheets[sheet_name]
2524
+ else:
2525
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2526
+ else:
2527
+ sheet = wb.sheets[sheet_name]
2528
+
2529
+ sheet.activate()
2530
+
2531
+ if operation_type == 'write':
2532
+ data, format_to_text_colunm = args[:2]
2533
+ # 清空工作表
2534
+ sheet.clear()
2535
+ # 格式化文本列
2536
+ format_to_text_v2(sheet, format_to_text_colunm)
2537
+ # 写入数据
2538
+ sheet.range('A1').value = data
2539
+ log(f"批量操作:写入数据到 {sheet_name}")
2540
+
2541
+ elif operation_type == 'format':
2542
+ format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
2543
+ # 执行格式化
2544
+ format_func(sheet, *format_args)
2545
+ log(f"批量操作:格式化工作表 {sheet_name}")
2546
+
2547
+ # 保存所有更改
2548
+ wb.save()
2549
+ log(f"批量操作完成: {excel_path}")
2550
+ return True
2551
+
2552
+ except Exception as e:
2553
+ log(f"批量操作失败: {e}")
2554
+ return False
2555
+ finally:
2556
+ # 释放锁但不关闭 Excel(保持复用)
2557
+ excel_lock_manager.release_excel_lock(excel_path)
2558
+
2559
+
2560
+ def force_close_excel_file(excel_path):
2561
+ """
2562
+ 强制关闭指定的 Excel 文件
2563
+
2564
+ Args:
2565
+ excel_path: Excel 文件路径
2566
+ """
2567
+ try:
2568
+ existing_instance = excel_lock_manager.get_excel_instance(excel_path)
2569
+ if existing_instance:
2570
+ app, wb = existing_instance
2571
+ close_excel_with_lock(excel_path, app, wb, force_close=True)
2572
+ log(f"强制关闭 Excel 文件: {excel_path}")
2573
+ except Exception as e:
2574
+ log(f"强制关闭 Excel 文件失败: {e}")
2575
+
2576
+
2577
+ def wait_for_excel_available(excel_path, timeout=60, check_interval=1):
2578
+ """
2579
+ 等待 Excel 文件可用
2580
+
2581
+ Args:
2582
+ excel_path: Excel 文件路径
2583
+ timeout: 超时时间(秒)
2584
+ check_interval: 检查间隔(秒)
2585
+
2586
+ Returns:
2587
+ bool: 是否成功获取锁
2588
+ """
2589
+ start_time = time.time()
2590
+ while time.time() - start_time < timeout:
2591
+ if excel_lock_manager.acquire_excel_lock(excel_path, timeout=0):
2592
+ return True
2593
+ time.sleep(check_interval)
2594
+
2595
+ log(f"等待 Excel 文件可用超时: {excel_path}")
2596
+ return False
2597
+
2598
+
2599
+ def smart_excel_operation(excel_path, operation_func, priority=0, timeout=60, max_retries=3):
2600
+ """
2601
+ 智能 Excel 操作函数,支持优先级、重试和更好的错误处理
2602
+
2603
+ Args:
2604
+ excel_path: Excel 文件路径
2605
+ operation_func: 要执行的操作函数,接收 (app, wb, sheet) 参数
2606
+ priority: 优先级,数字越小优先级越高
2607
+ timeout: 获取锁的超时时间(秒)
2608
+ max_retries: 最大重试次数
2609
+
2610
+ Returns:
2611
+ bool: 操作是否成功
2612
+ """
2613
+ for attempt in range(max_retries):
2614
+ try:
2615
+ # 检查是否有其他程序正在操作该文件
2616
+ waiting_count = excel_lock_manager.get_waiting_count(excel_path)
2617
+ if waiting_count > 0:
2618
+ log(f"等待其他程序完成操作: {os.path.basename(excel_path)} (等待队列: {waiting_count})")
2619
+
2620
+ # 尝试获取锁
2621
+ if not excel_lock_manager.acquire_excel_lock(excel_path, timeout, priority):
2622
+ if attempt < max_retries - 1:
2623
+ wait_time = (attempt + 1) * 5 # 递增等待时间
2624
+ log(f"获取锁失败,等待 {wait_time} 秒后重试 (尝试 {attempt + 1}/{max_retries})")
2625
+ time.sleep(wait_time)
2626
+ continue
2627
+ else:
2628
+ log(f"达到最大重试次数,操作失败: {excel_path}")
2629
+ return False
2630
+
2631
+ # 打开 Excel
2632
+ app, wb, sheet = open_excel_with_lock(excel_path)
2633
+ if not app or not wb:
2634
+ log(f"无法打开 Excel 文件: {excel_path}")
2635
+ return False
2636
+
2637
+ try:
2638
+ # 执行操作
2639
+ result = operation_func(app, wb, sheet)
2640
+
2641
+ # 保存更改
2642
+ if wb:
2643
+ wb.save()
2644
+ log(f"成功保存 Excel 文件: {os.path.basename(excel_path)}")
2645
+
2646
+ return result
2647
+
2648
+ except Exception as e:
2649
+ log(f"Excel 操作失败: {e}")
2650
+ return False
2651
+ finally:
2652
+ # 释放锁但不关闭 Excel(保持复用)
2653
+ excel_lock_manager.release_excel_lock(excel_path)
2654
+
2655
+ except Exception as e:
2656
+ log(f"智能 Excel 操作异常: {e}")
2657
+ if attempt < max_retries - 1:
2658
+ time.sleep(2)
2659
+ continue
2660
+ else:
2661
+ return False
2662
+
2663
+ return False
2664
+
2665
+
2666
+ def batch_excel_operations_with_priority(excel_path, operations, priority=0, timeout=60):
2667
+ """
2668
+ 带优先级的批量 Excel 操作函数
2669
+
2670
+ Args:
2671
+ excel_path: Excel 文件路径
2672
+ operations: 操作列表
2673
+ priority: 优先级
2674
+ timeout: 超时时间
2675
+
2676
+ Returns:
2677
+ bool: 是否全部操作成功
2678
+ """
2679
+
2680
+ def batch_operation(app, wb, sheet):
2681
+ try:
2682
+ for sheet_name, operation_type, *args in operations:
2683
+ # 获取或创建工作表
2684
+ if isinstance(sheet_name, str):
2685
+ sheet_names = [s.name.strip().lower() for s in wb.sheets]
2686
+ if sheet_name.strip().lower() in sheet_names:
2687
+ sheet = wb.sheets[sheet_name]
2688
+ else:
2689
+ sheet = wb.sheets.add(sheet_name, after=wb.sheets[-1])
2690
+ else:
2691
+ sheet = wb.sheets[sheet_name]
2692
+
2693
+ sheet.activate()
2694
+
2695
+ if operation_type == 'write':
2696
+ data, format_to_text_colunm = args[:2]
2697
+ # 清空工作表
2698
+ sheet.clear()
2699
+ # 格式化文本列
2700
+ format_to_text_v2(sheet, format_to_text_colunm)
2701
+ # 写入数据
2702
+ sheet.range('A1').value = data
2703
+ log(f"批量操作:写入数据到 {sheet_name}")
2704
+
2705
+ elif operation_type == 'format':
2706
+ format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
2707
+ # 执行格式化
2708
+ format_func(sheet, *format_args)
2709
+ log(f"批量操作:格式化工作表 {sheet_name}")
2710
+
2711
+ return True
2712
+
2713
+ except Exception as e:
2714
+ log(f"批量操作失败: {e}")
2715
+ return False
2716
+
2717
+ return smart_excel_operation(excel_path, batch_operation, priority, timeout)
2718
+
2719
+
2720
+ def wait_for_excel_available_with_priority(excel_path, timeout=60, check_interval=1, priority=0):
2721
+ """
2722
+ 等待 Excel 文件可用(带优先级)
2723
+
2724
+ Args:
2725
+ excel_path: Excel 文件路径
2726
+ timeout: 超时时间(秒)
2727
+ check_interval: 检查间隔(秒)
2728
+ priority: 优先级
2729
+
2730
+ Returns:
2731
+ bool: 是否成功获取锁
2732
+ """
2733
+ start_time = time.time()
2734
+ while time.time() - start_time < timeout:
2735
+ if excel_lock_manager.acquire_excel_lock(excel_path, timeout=0, priority=priority):
2736
+ return True
2737
+ time.sleep(check_interval)
2738
+
2739
+ log(f"等待 Excel 文件可用超时: {excel_path}")
2740
+ return False
2741
+
2742
+
2743
+ def get_excel_status(excel_path):
2744
+ """
2745
+ 获取 Excel 文件状态信息
2746
+
2747
+ Args:
2748
+ excel_path: Excel 文件路径
2749
+
2750
+ Returns:
2751
+ dict: 状态信息
2752
+ """
2753
+ return {
2754
+ 'is_open': excel_lock_manager.is_excel_open(excel_path),
2755
+ 'waiting_count': excel_lock_manager.get_waiting_count(excel_path),
2756
+ 'operation_count': excel_lock_manager.get_operation_count(excel_path),
2757
+ 'has_lock': excel_lock_manager.get_file_lock(excel_path).locked()
2758
+ }