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