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/RateLimitedSender.py +45 -0
- qrpa/__init__.py +4 -1
- qrpa/fun_base.py +70 -4
- qrpa/fun_excel.py +2758 -0
- qrpa/fun_web.py +148 -0
- qrpa/fun_win.py +168 -1
- qrpa/shein_ziniao.py +51 -42
- qrpa/time_utils.py +55 -55
- qrpa/wxwork.py +11 -13
- {qrpa-1.0.9.dist-info → qrpa-1.0.11.dist-info}/METADATA +1 -1
- qrpa-1.0.11.dist-info/RECORD +16 -0
- qrpa-1.0.9.dist-info/RECORD +0 -13
- {qrpa-1.0.9.dist-info → qrpa-1.0.11.dist-info}/WHEEL +0 -0
- {qrpa-1.0.9.dist-info → qrpa-1.0.11.dist-info}/top_level.txt +0 -0
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
|
+
}
|