qrpa 1.0.34__py3-none-any.whl → 1.1.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qrpa/RateLimitedSender.py +45 -45
- qrpa/__init__.py +31 -26
- qrpa/db_migrator.py +600 -600
- qrpa/feishu_bot_app.py +267 -267
- qrpa/feishu_client.py +410 -0
- qrpa/feishu_logic.py +1443 -0
- qrpa/fun_base.py +339 -337
- qrpa/fun_excel.py +529 -61
- qrpa/fun_file.py +318 -318
- qrpa/fun_web.py +258 -148
- qrpa/fun_win.py +198 -198
- 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_product_model.py +495 -0
- qrpa/mysql_module/shein_return_order_model.py +569 -0
- qrpa/mysql_module/shein_store_model.py +595 -0
- qrpa/shein_daily_report_model.py +375 -375
- qrpa/shein_excel.py +1248 -109
- qrpa/shein_lib.py +2333 -143
- qrpa/shein_mysql.py +92 -0
- qrpa/shein_sqlite.py +153 -153
- qrpa/shein_ziniao.py +529 -450
- qrpa/temu_chrome.py +56 -56
- qrpa/temu_excel.py +139 -139
- qrpa/temu_lib.py +156 -154
- qrpa/time_utils.py +87 -50
- qrpa/time_utils_example.py +243 -243
- qrpa/wxwork.py +318 -318
- {qrpa-1.0.34.dist-info → qrpa-1.1.50.dist-info}/METADATA +1 -1
- qrpa-1.1.50.dist-info/RECORD +33 -0
- qrpa-1.0.34.dist-info/RECORD +0 -24
- {qrpa-1.0.34.dist-info → qrpa-1.1.50.dist-info}/WHEEL +0 -0
- {qrpa-1.0.34.dist-info → qrpa-1.1.50.dist-info}/top_level.txt +0 -0
qrpa/fun_excel.py
CHANGED
|
@@ -14,6 +14,9 @@ import threading
|
|
|
14
14
|
from playwright.sync_api import sync_playwright
|
|
15
15
|
import psutil
|
|
16
16
|
|
|
17
|
+
import os, sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
17
20
|
from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
|
|
18
21
|
|
|
19
22
|
excel_color_index = {
|
|
@@ -73,6 +76,49 @@ excel_color_index = {
|
|
|
73
76
|
"深蓝灰色" : 55, # #333399
|
|
74
77
|
}
|
|
75
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
|
+
|
|
76
122
|
def set_cell_prefix_red(cell, n, color_name):
|
|
77
123
|
"""
|
|
78
124
|
将指定 Excel 单元格内容的前 n 个字符设置为红色。
|
|
@@ -105,6 +151,34 @@ def wrap_column(sheet, columns=None, WrapText=True):
|
|
|
105
151
|
log(f'设置[{c}] 换行 {WrapText}')
|
|
106
152
|
sheet.range(f'{col_name}:{col_name}').api.WrapText = WrapText
|
|
107
153
|
|
|
154
|
+
def sort_by_column_excel(sheet, sort_col: str, has_header=True, order="desc"):
|
|
155
|
+
"""
|
|
156
|
+
对整个表格按照某一列排序
|
|
157
|
+
|
|
158
|
+
:param sheet: xlwings 的 sheet 对象
|
|
159
|
+
:param sort_col: 排序依据的列(如 'D')
|
|
160
|
+
:param has_header: 是否有表头(默认 True)
|
|
161
|
+
:param order: 'asc' 升序,'desc' 降序
|
|
162
|
+
"""
|
|
163
|
+
# 找到表格的最后一行和列
|
|
164
|
+
last_cell = sheet.used_range.last_cell
|
|
165
|
+
rng = sheet.range((1, 1), (last_cell.row, last_cell.column))
|
|
166
|
+
|
|
167
|
+
# 排序依据列
|
|
168
|
+
col_index = ord(sort_col.upper()) - ord('A') + 1
|
|
169
|
+
key = sheet.range((2 if has_header else 1, col_index)).api
|
|
170
|
+
|
|
171
|
+
# 排序顺序
|
|
172
|
+
order_val = 1 if order == "asc" else 2
|
|
173
|
+
|
|
174
|
+
# 调用 Excel 的 Sort 方法
|
|
175
|
+
rng.api.Sort(
|
|
176
|
+
Key1=key,
|
|
177
|
+
Order1=order_val,
|
|
178
|
+
Orientation=1,
|
|
179
|
+
Header=1 if has_header else 0
|
|
180
|
+
)
|
|
181
|
+
|
|
108
182
|
def sort_by_column(data, col_index, header_rows=2, reverse=True):
|
|
109
183
|
if not data or header_rows >= len(data):
|
|
110
184
|
return data
|
|
@@ -149,37 +223,88 @@ def merge_by_column_v2(sheet, column_name, other_columns):
|
|
|
149
223
|
log(f'未找到合并的列名: {column_name}')
|
|
150
224
|
return
|
|
151
225
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
226
|
+
# 更安全的数据获取方式,确保获取完整的数据范围
|
|
227
|
+
last_row = get_last_row(sheet, col_letter)
|
|
228
|
+
data = sheet.range(f'{col_letter}1:{col_letter}{last_row}').value
|
|
229
|
+
|
|
230
|
+
# 确保data是列表格式
|
|
231
|
+
if not isinstance(data, list):
|
|
232
|
+
data = [data]
|
|
156
233
|
|
|
157
|
-
|
|
158
|
-
|
|
234
|
+
log(f'数据范围: {col_letter}1:{col_letter}{last_row}, 数据长度: {len(data)}')
|
|
235
|
+
|
|
236
|
+
start_row = 2 # 从第2行开始,跳过表头
|
|
237
|
+
merge_row_ranges = [] # 用来存储需要合并的行范围 (start_row, end_row)
|
|
238
|
+
|
|
239
|
+
# 获取所有需要合并的列
|
|
240
|
+
all_columns = [col_letter] # 主列
|
|
159
241
|
for col in other_columns:
|
|
160
242
|
col_name = find_column_by_data(sheet, 1, col)
|
|
161
243
|
if col_name:
|
|
162
|
-
|
|
244
|
+
all_columns.append(col_name)
|
|
163
245
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
246
|
+
log(f'需要合并的列: {all_columns}')
|
|
247
|
+
|
|
248
|
+
# 遍历数据行,从第3行开始比较(因为第1行是表头,第2行是第一个数据行)
|
|
249
|
+
for row in range(3, len(data) + 1):
|
|
250
|
+
log(f'查找 {row}/{len(data)}, 当前值: {data[row - 1] if row - 1 < len(data) else "超出范围"}, 前一个值: {data[row - 2] if row - 2 < len(data) else "超出范围"}')
|
|
251
|
+
|
|
252
|
+
# 检查值是否发生变化
|
|
253
|
+
if row <= len(data) and data[row - 1] != data[row - 2]:
|
|
254
|
+
# 值发生变化,处理前一组
|
|
255
|
+
end_row = row - 1
|
|
256
|
+
log(f'添加合并范围: {start_row} 到 {end_row}')
|
|
257
|
+
merge_row_ranges.append((start_row, end_row))
|
|
172
258
|
start_row = row
|
|
173
259
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
260
|
+
# 处理最后一组数据(循环结束后,start_row 到数据末尾)
|
|
261
|
+
if start_row <= len(data):
|
|
262
|
+
end_row = len(data)
|
|
263
|
+
log(f'处理最后一组: {start_row} 到 {end_row}')
|
|
264
|
+
merge_row_ranges.append((start_row, end_row))
|
|
178
265
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
266
|
+
log(f'行合并范围: {merge_row_ranges}')
|
|
267
|
+
|
|
268
|
+
# 对每个行范围,在所有指定列中执行合并
|
|
269
|
+
for start_row, end_row in merge_row_ranges:
|
|
270
|
+
if start_row < end_row: # 只有当开始行小于结束行时才合并(多行)
|
|
271
|
+
for col_name in all_columns:
|
|
272
|
+
try:
|
|
273
|
+
cell_range = sheet.range(f'{col_name}{start_row}:{col_name}{end_row}')
|
|
274
|
+
|
|
275
|
+
# 验证:检查范围内的值是否都相同
|
|
276
|
+
values = cell_range.value
|
|
277
|
+
if not isinstance(values, list):
|
|
278
|
+
values = [values]
|
|
279
|
+
|
|
280
|
+
# 检查是否所有值都相同(忽略 None)
|
|
281
|
+
non_none_values = [v for v in values if v is not None]
|
|
282
|
+
if non_none_values and len(set(non_none_values)) > 1:
|
|
283
|
+
log(f'警告:{col_name}{start_row}:{col_name}{end_row} 包含不同的值,跳过合并: {set(non_none_values)}')
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
log(f'处理 {col_name}{start_row}:{col_name}{end_row} merge')
|
|
287
|
+
|
|
288
|
+
# 保存第一个单元格的值
|
|
289
|
+
first_cell_value = sheet.range(f'{col_name}{start_row}').value
|
|
290
|
+
|
|
291
|
+
# 先清空所有单元格(避免多行文本导致的合并问题)
|
|
292
|
+
cell_range.value = None
|
|
293
|
+
|
|
294
|
+
# 执行合并
|
|
295
|
+
cell_range.merge()
|
|
296
|
+
|
|
297
|
+
# 恢复第一个单元格的值
|
|
298
|
+
cell_range.value = first_cell_value
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
log(f'合并失败 {col_name}{start_row}:{col_name}{end_row}: {e}')
|
|
302
|
+
# 继续处理其他列
|
|
303
|
+
continue
|
|
304
|
+
elif start_row == end_row:
|
|
305
|
+
log(f'单行数据无需合并: {start_row} 到 {end_row}')
|
|
306
|
+
else:
|
|
307
|
+
log(f'跳过无效合并范围: {start_row} 到 {end_row}')
|
|
183
308
|
|
|
184
309
|
def merge_by_column(sheet, column_name, other_columns):
|
|
185
310
|
log('正在处理合并单元格')
|
|
@@ -589,10 +714,10 @@ def InsertImageV2(sheet, columns=None, platform='shein', img_width=150, img_save
|
|
|
589
714
|
|
|
590
715
|
img_key_letter = find_column_by_data(sheet, 1, img_save_key)
|
|
591
716
|
|
|
592
|
-
#
|
|
717
|
+
# 阶段1:调整所有单元格尺寸 (优化点1)
|
|
593
718
|
area_map = {}
|
|
594
719
|
for row in range(start_row, last_row + 1):
|
|
595
|
-
log(f'计算 {row}/{last_row}')
|
|
720
|
+
log(f'计算 {row}/{last_row}')
|
|
596
721
|
for col_letter in col_letter_map.values():
|
|
597
722
|
cell_ref = f'{col_letter}{row}'
|
|
598
723
|
cell_range = sheet.range(cell_ref)
|
|
@@ -603,32 +728,46 @@ def InsertImageV2(sheet, columns=None, platform='shein', img_width=150, img_save
|
|
|
603
728
|
cell_address = cell_range.address
|
|
604
729
|
|
|
605
730
|
if cell_address not in area_map:
|
|
606
|
-
#
|
|
607
|
-
cell_width = cell_range.width
|
|
608
|
-
cell_height = cell_range.height
|
|
609
|
-
|
|
610
|
-
# 调整列宽和行高
|
|
611
|
-
if cell_width < img_width:
|
|
612
|
-
cell_range.column_width = img_width / 6.1
|
|
613
|
-
|
|
614
|
-
# 这一行暂时先自动控制宽度
|
|
731
|
+
# 调整列宽
|
|
615
732
|
cell_range.column_width = img_width / 6.1
|
|
616
733
|
|
|
617
|
-
|
|
618
|
-
|
|
734
|
+
# 调整行高
|
|
735
|
+
if cell_range.height < img_width:
|
|
736
|
+
if cell_range.merge_cells:
|
|
737
|
+
# 合并单元格:为每一行设置高度
|
|
738
|
+
rows_count = cell_range.rows.count
|
|
739
|
+
per_row_height = img_width / rows_count
|
|
740
|
+
for single_row in cell_range.rows:
|
|
741
|
+
single_row.row_height = max(per_row_height, 150 / 8)
|
|
742
|
+
else:
|
|
743
|
+
cell_range.row_height = max(img_width, 150 / 8)
|
|
619
744
|
|
|
620
745
|
if cell_height_with_img:
|
|
621
|
-
cell_range.
|
|
622
|
-
|
|
746
|
+
if cell_range.merge_cells:
|
|
747
|
+
rows_count = cell_range.rows.count
|
|
748
|
+
for single_row in cell_range.rows:
|
|
749
|
+
single_row.row_height = img_width / rows_count
|
|
750
|
+
else:
|
|
751
|
+
cell_range.row_height = img_width
|
|
752
|
+
|
|
753
|
+
# 重新读取调整后的宽高
|
|
754
|
+
if cell_range.merge_cells:
|
|
755
|
+
cell_range = sheet.range(cell_ref).merge_area
|
|
756
|
+
else:
|
|
757
|
+
cell_range = sheet.range(cell_ref)
|
|
758
|
+
|
|
623
759
|
# 计算居中位置
|
|
624
|
-
|
|
625
|
-
|
|
760
|
+
actual_width = cell_range.width
|
|
761
|
+
actual_height = cell_range.height
|
|
762
|
+
actual_img_size = img_width - 4
|
|
763
|
+
top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
|
|
764
|
+
left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
|
|
626
765
|
|
|
627
766
|
area_map[cell_address] = {
|
|
628
767
|
'top' : top,
|
|
629
768
|
'left' : left,
|
|
630
769
|
'width' : img_width,
|
|
631
|
-
'cell_list': [c.address for c in cell_range]
|
|
770
|
+
'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
|
|
632
771
|
}
|
|
633
772
|
|
|
634
773
|
# 处理图片插入 (优化点2)
|
|
@@ -685,6 +824,238 @@ def InsertImageV2(sheet, columns=None, platform='shein', img_width=150, img_save
|
|
|
685
824
|
else:
|
|
686
825
|
log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
|
|
687
826
|
|
|
827
|
+
def InsertImageV3(sheet, columns=None, platform='shein', img_widths=None, img_save_key=None, dir_name=None, cell_height_with_img=False, start_row=2):
|
|
828
|
+
"""
|
|
829
|
+
V3版本:支持一次性插入多列图片,每列可以设置不同的宽度
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
sheet: Excel工作表对象
|
|
833
|
+
columns: 图片列名列表,如 ['SKC图片', 'SKU图片']
|
|
834
|
+
platform: 平台名称,如 'shein'
|
|
835
|
+
img_widths: 图片宽度列表,与columns对应,如 [90, 60]
|
|
836
|
+
img_save_key: 图片保存时的key列
|
|
837
|
+
dir_name: 图片保存目录名
|
|
838
|
+
cell_height_with_img: 是否根据图片调整单元格高度
|
|
839
|
+
start_row: 开始行号,默认为2
|
|
840
|
+
"""
|
|
841
|
+
if not columns:
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# 如果没有提供宽度列表,使用默认宽度150
|
|
845
|
+
if not img_widths:
|
|
846
|
+
img_widths = [150] * len(columns)
|
|
847
|
+
|
|
848
|
+
# 确保宽度列表长度与列名列表一致
|
|
849
|
+
if len(img_widths) != len(columns):
|
|
850
|
+
raise ValueError(f"img_widths长度({len(img_widths)})必须与columns长度({len(columns)})一致")
|
|
851
|
+
|
|
852
|
+
minimize(sheet.book.app)
|
|
853
|
+
|
|
854
|
+
# 只清空一次所有图片
|
|
855
|
+
clear_all_pictures(sheet)
|
|
856
|
+
|
|
857
|
+
# 获取每列图片列的列号,并批量下载图片
|
|
858
|
+
col_letter_map = {}
|
|
859
|
+
col_width_map = {} # 存储每列对应的宽度
|
|
860
|
+
|
|
861
|
+
for idx, img_col in enumerate(columns):
|
|
862
|
+
col_letter = find_column_by_data(sheet, 1, img_col)
|
|
863
|
+
if col_letter is not None:
|
|
864
|
+
col_letter_map[img_col] = col_letter
|
|
865
|
+
col_width_map[col_letter] = img_widths[idx]
|
|
866
|
+
# 下载图片
|
|
867
|
+
log(f'批量下载图片: {img_col} => {col_letter} (宽度: {img_widths[idx]})')
|
|
868
|
+
last_row = get_last_row(sheet, col_letter)
|
|
869
|
+
images = sheet.range(f'{col_letter}2:{col_letter}{last_row}').value
|
|
870
|
+
images = images if isinstance(images, list) else [images]
|
|
871
|
+
download_images_concurrently(images, platform)
|
|
872
|
+
|
|
873
|
+
# 任意一个列作为主参考列,用来确定行数
|
|
874
|
+
if not col_letter_map:
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
ref_col_letter = next(iter(col_letter_map.values()))
|
|
878
|
+
last_row = get_last_row(sheet, ref_col_letter)
|
|
879
|
+
|
|
880
|
+
img_key_letter = find_column_by_data(sheet, 1, img_save_key)
|
|
881
|
+
|
|
882
|
+
# 阶段1:收集每个单元格需要的尺寸要求
|
|
883
|
+
cell_size_requirements = {} # {cell_address: {'width': max_width, 'height': max_height, 'merge': is_merge}}
|
|
884
|
+
|
|
885
|
+
for row in range(start_row, last_row + 1):
|
|
886
|
+
for col_letter in col_letter_map.values():
|
|
887
|
+
cell_ref = f'{col_letter}{row}'
|
|
888
|
+
cell_range = sheet.range(cell_ref)
|
|
889
|
+
cell_address = cell_range.address
|
|
890
|
+
img_width = col_width_map[col_letter]
|
|
891
|
+
|
|
892
|
+
if cell_range.merge_cells:
|
|
893
|
+
cell_range = cell_range.merge_area
|
|
894
|
+
cell_address = cell_range.address
|
|
895
|
+
|
|
896
|
+
# 记录每个单元格需要的最大尺寸
|
|
897
|
+
if cell_address not in cell_size_requirements:
|
|
898
|
+
cell_size_requirements[cell_address] = {
|
|
899
|
+
'width': img_width,
|
|
900
|
+
'height': img_width,
|
|
901
|
+
'cell_range': cell_range,
|
|
902
|
+
'merge': cell_range.merge_cells
|
|
903
|
+
}
|
|
904
|
+
else:
|
|
905
|
+
# 取最大值
|
|
906
|
+
cell_size_requirements[cell_address]['width'] = max(
|
|
907
|
+
cell_size_requirements[cell_address]['width'], img_width
|
|
908
|
+
)
|
|
909
|
+
cell_size_requirements[cell_address]['height'] = max(
|
|
910
|
+
cell_size_requirements[cell_address]['height'], img_width
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# 阶段2:统一调整所有单元格的宽高(按列分别处理)
|
|
914
|
+
log(f'调整单元格尺寸...')
|
|
915
|
+
adjusted_cells = {} # 记录已调整的单元格,避免重复调整
|
|
916
|
+
|
|
917
|
+
for col_letter in col_letter_map.values():
|
|
918
|
+
img_width = col_width_map[col_letter]
|
|
919
|
+
|
|
920
|
+
for row in range(start_row, last_row + 1):
|
|
921
|
+
cell_ref = f'{col_letter}{row}'
|
|
922
|
+
cell_range = sheet.range(cell_ref)
|
|
923
|
+
cell_address = cell_range.address
|
|
924
|
+
|
|
925
|
+
if cell_range.merge_cells:
|
|
926
|
+
cell_range = cell_range.merge_area
|
|
927
|
+
cell_address = cell_range.address
|
|
928
|
+
|
|
929
|
+
# 调整列宽(按原来的逻辑,每列都调整)
|
|
930
|
+
if cell_range.width < img_width:
|
|
931
|
+
cell_range.column_width = img_width / 6.1
|
|
932
|
+
# 这一行暂时先自动控制宽度
|
|
933
|
+
cell_range.column_width = img_width / 6.1
|
|
934
|
+
|
|
935
|
+
# 行高只调整一次(使用最大需求)
|
|
936
|
+
if cell_address not in adjusted_cells:
|
|
937
|
+
adjusted_cells[cell_address] = True
|
|
938
|
+
required_height = cell_size_requirements[cell_address]['height']
|
|
939
|
+
|
|
940
|
+
# 调整行高
|
|
941
|
+
if cell_range.height < required_height:
|
|
942
|
+
if cell_range.merge_cells:
|
|
943
|
+
# 合并单元格:为每一行设置高度
|
|
944
|
+
rows_count = cell_range.rows.count
|
|
945
|
+
per_row_height = required_height / rows_count
|
|
946
|
+
for single_row in cell_range.rows:
|
|
947
|
+
single_row.row_height = max(per_row_height, 150 / 8)
|
|
948
|
+
else:
|
|
949
|
+
cell_range.row_height = max(required_height, 150 / 8)
|
|
950
|
+
|
|
951
|
+
if cell_height_with_img:
|
|
952
|
+
if cell_range.merge_cells:
|
|
953
|
+
rows_count = cell_range.rows.count
|
|
954
|
+
for single_row in cell_range.rows:
|
|
955
|
+
single_row.row_height = required_height / rows_count
|
|
956
|
+
else:
|
|
957
|
+
cell_range.row_height = required_height
|
|
958
|
+
|
|
959
|
+
# 阶段3:计算所有图片的位置
|
|
960
|
+
area_map = {}
|
|
961
|
+
for row in range(start_row, last_row + 1):
|
|
962
|
+
log(f'计算位置 {row}/{last_row}')
|
|
963
|
+
for col_letter in col_letter_map.values():
|
|
964
|
+
cell_ref = f'{col_letter}{row}'
|
|
965
|
+
cell_range = sheet.range(cell_ref)
|
|
966
|
+
cell_address = cell_range.address
|
|
967
|
+
img_width = col_width_map[col_letter]
|
|
968
|
+
|
|
969
|
+
if cell_range.merge_cells:
|
|
970
|
+
cell_range = cell_range.merge_area
|
|
971
|
+
cell_address = cell_range.address
|
|
972
|
+
|
|
973
|
+
# 重新读取调整后的宽高
|
|
974
|
+
actual_width = cell_range.width
|
|
975
|
+
actual_height = cell_range.height
|
|
976
|
+
|
|
977
|
+
# 计算该列图片的居中位置
|
|
978
|
+
# 图片实际大小是 img_width-4,插入时偏移+2,所以这里-2补偿
|
|
979
|
+
actual_img_size = img_width - 4
|
|
980
|
+
top = cell_range.top + (actual_height - actual_img_size) / 2 - 2
|
|
981
|
+
left = cell_range.left + (actual_width - actual_img_size) / 2 - 2
|
|
982
|
+
|
|
983
|
+
# 每个列都单独保存位置
|
|
984
|
+
area_map[f'{cell_address}_{col_letter}'] = {
|
|
985
|
+
'top': top,
|
|
986
|
+
'left': left,
|
|
987
|
+
'width': img_width,
|
|
988
|
+
'cell_address': cell_address,
|
|
989
|
+
'cell_list': [c.address for c in cell_range] if cell_range.merge_cells else [cell_address]
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
# 阶段4:插入图片
|
|
993
|
+
for row in range(start_row, last_row + 1):
|
|
994
|
+
for img_col_name, col_letter in col_letter_map.items():
|
|
995
|
+
cell_ref = f'{col_letter}{row}'
|
|
996
|
+
cell_range = sheet.range(cell_ref)
|
|
997
|
+
original_address = cell_range.address
|
|
998
|
+
|
|
999
|
+
if cell_range.merge_cells:
|
|
1000
|
+
cell_range = cell_range.merge_area
|
|
1001
|
+
cell_address = cell_range.address
|
|
1002
|
+
else:
|
|
1003
|
+
cell_address = original_address
|
|
1004
|
+
|
|
1005
|
+
# 检查是否是合并单元格的非首单元格(跳过)
|
|
1006
|
+
area_key = f'{cell_address}_{col_letter}'
|
|
1007
|
+
if area_key not in area_map:
|
|
1008
|
+
continue
|
|
1009
|
+
|
|
1010
|
+
area_info = area_map[area_key]
|
|
1011
|
+
|
|
1012
|
+
# 对于合并单元格,只在第一个单元格处理
|
|
1013
|
+
if cell_range.merge_cells:
|
|
1014
|
+
# 获取合并区域的第一个单元格地址
|
|
1015
|
+
first_cell_in_merge = area_info['cell_list'][0] if area_info['cell_list'] else cell_address
|
|
1016
|
+
# 如果当前单元格不是合并区域的第一个单元格,跳过
|
|
1017
|
+
if original_address != first_cell_in_merge:
|
|
1018
|
+
continue
|
|
1019
|
+
|
|
1020
|
+
# 使用预计算的位置信息
|
|
1021
|
+
top = area_info['top']
|
|
1022
|
+
left = area_info['left']
|
|
1023
|
+
width = area_info['width']
|
|
1024
|
+
|
|
1025
|
+
# 获取图片链接
|
|
1026
|
+
if cell_range.merge_cells:
|
|
1027
|
+
img_url = cell_range.value[0]
|
|
1028
|
+
else:
|
|
1029
|
+
img_url = cell_range.value
|
|
1030
|
+
|
|
1031
|
+
if img_url:
|
|
1032
|
+
if img_key_letter is not None:
|
|
1033
|
+
image_dir = Path(f'{os.getenv('auto_dir')}/image') / dir_name
|
|
1034
|
+
extension = Path(img_url).suffix
|
|
1035
|
+
filename = str(sheet.range(f'{img_key_letter}{row}').value)
|
|
1036
|
+
img_save_path = image_dir / f"{sanitize_filename(filename)}{extension}"
|
|
1037
|
+
else:
|
|
1038
|
+
img_save_path = None
|
|
1039
|
+
|
|
1040
|
+
img_path = download_img_v2(img_url, platform, img_save_path)
|
|
1041
|
+
log(f'插入图片 {sheet.name} [{img_col_name}] {row}/{last_row} {img_path}')
|
|
1042
|
+
if not img_path:
|
|
1043
|
+
log('跳过:', img_path, img_url)
|
|
1044
|
+
continue
|
|
1045
|
+
cell_value = cell_range.value
|
|
1046
|
+
|
|
1047
|
+
# 插入图片
|
|
1048
|
+
try:
|
|
1049
|
+
# 使用预计算的位置直接插入图片
|
|
1050
|
+
sheet.pictures.add(img_path, top=top + 2, left=left + 2, width=width - 4, height=width - 4)
|
|
1051
|
+
cell_range.value = None
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
# 插入图片失败恢复链接地址
|
|
1054
|
+
cell_range.value = cell_value
|
|
1055
|
+
send_exception()
|
|
1056
|
+
else:
|
|
1057
|
+
log(f'图片地址不存在 [{img_col_name}] : 第{row}行')
|
|
1058
|
+
|
|
688
1059
|
def download_images_concurrently(image_urls, platform='shein', img_save_dir=None):
|
|
689
1060
|
# 使用线程池执行并发下载
|
|
690
1061
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
@@ -693,18 +1064,22 @@ def download_images_concurrently(image_urls, platform='shein', img_save_dir=None
|
|
|
693
1064
|
return results
|
|
694
1065
|
|
|
695
1066
|
def download_img_by_chrome(image_url, save_name):
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1067
|
+
try:
|
|
1068
|
+
with sync_playwright() as p:
|
|
1069
|
+
browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
|
|
1070
|
+
context = browser.new_context()
|
|
1071
|
+
page = context.new_page()
|
|
1072
|
+
# 直接通过Playwright下载图片
|
|
1073
|
+
response = page.request.get(image_url)
|
|
1074
|
+
with open(save_name, 'wb') as f:
|
|
1075
|
+
f.write(response.body()) # 将下载的内容保存为文件
|
|
1076
|
+
log(f"图片已通过chrome下载并保存为:{save_name}")
|
|
1077
|
+
# 关闭浏览器
|
|
1078
|
+
browser.close()
|
|
1079
|
+
return save_name
|
|
1080
|
+
except:
|
|
1081
|
+
send_exception()
|
|
1082
|
+
return None
|
|
708
1083
|
|
|
709
1084
|
def download_img_v2(image_url, platform='shein', img_save_path=None):
|
|
710
1085
|
image_url = add_https(image_url)
|
|
@@ -1213,6 +1588,73 @@ def colorize_by_field(sheet, field):
|
|
|
1213
1588
|
row_range.color = bg_color # 应用背景色
|
|
1214
1589
|
sheet.range(f"A{row}").api.Font.Bold = True # 让店铺名称加粗
|
|
1215
1590
|
|
|
1591
|
+
def colorize_by_field_v2(sheet, field):
|
|
1592
|
+
"""
|
|
1593
|
+
改进版:按指定字段为行着色,正确处理合并单元格
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
sheet: Excel工作表对象
|
|
1597
|
+
field: 用于分组着色的字段名(列名)
|
|
1598
|
+
"""
|
|
1599
|
+
minimize(sheet.book.app)
|
|
1600
|
+
|
|
1601
|
+
# 查找字段所在的列
|
|
1602
|
+
field_column = find_column_by_data(sheet, 1, field)
|
|
1603
|
+
if field_column is None:
|
|
1604
|
+
log(f'未找到字段列: {field}')
|
|
1605
|
+
return
|
|
1606
|
+
|
|
1607
|
+
log(f'按字段 {field} (列 {field_column}) 着色')
|
|
1608
|
+
|
|
1609
|
+
# 获取最后一行和最后一列
|
|
1610
|
+
last_row = get_last_row(sheet, field_column)
|
|
1611
|
+
max_column_letter = get_max_column_letter(sheet)
|
|
1612
|
+
|
|
1613
|
+
# 记录字段值对应的颜色
|
|
1614
|
+
field_color_map = {}
|
|
1615
|
+
last_field_value = None # 记录上一个非空值
|
|
1616
|
+
|
|
1617
|
+
# 从第2行开始遍历(跳过表头)
|
|
1618
|
+
for row in range(2, last_row + 1):
|
|
1619
|
+
# 读取当前行的字段值
|
|
1620
|
+
cell = sheet.range(f'{field_column}{row}')
|
|
1621
|
+
current_value = cell.value
|
|
1622
|
+
|
|
1623
|
+
# 如果是合并单元格的非首单元格,值可能为 None,使用上一个非空值
|
|
1624
|
+
if current_value is None or current_value == '':
|
|
1625
|
+
# 检查是否是合并单元格
|
|
1626
|
+
if cell.merge_cells:
|
|
1627
|
+
# 使用合并区域的值
|
|
1628
|
+
merge_area = cell.merge_area
|
|
1629
|
+
current_value = merge_area.value
|
|
1630
|
+
if isinstance(current_value, (list, tuple)):
|
|
1631
|
+
current_value = current_value[0] if current_value else None
|
|
1632
|
+
|
|
1633
|
+
# 如果仍然为空,使用上一个非空值
|
|
1634
|
+
if current_value is None or current_value == '':
|
|
1635
|
+
current_value = last_field_value
|
|
1636
|
+
else:
|
|
1637
|
+
# 更新上一个非空值
|
|
1638
|
+
last_field_value = current_value
|
|
1639
|
+
|
|
1640
|
+
# 跳过空值
|
|
1641
|
+
if current_value is None or current_value == '':
|
|
1642
|
+
continue
|
|
1643
|
+
|
|
1644
|
+
# 为新的字段值分配颜色
|
|
1645
|
+
if current_value not in field_color_map:
|
|
1646
|
+
field_color_map[current_value] = random_color()
|
|
1647
|
+
|
|
1648
|
+
# 应用背景色到整行
|
|
1649
|
+
bg_color = field_color_map[current_value]
|
|
1650
|
+
row_range = sheet.range(f'A{row}:{max_column_letter}{row}')
|
|
1651
|
+
row_range.color = bg_color
|
|
1652
|
+
|
|
1653
|
+
# 可选:让第一列加粗(店铺信息等)
|
|
1654
|
+
# sheet.range(f'A{row}').api.Font.Bold = True
|
|
1655
|
+
|
|
1656
|
+
log(f'着色完成,共 {len(field_color_map)} 个不同的 {field} 值')
|
|
1657
|
+
|
|
1216
1658
|
def add_borders(sheet, lineStyle=1):
|
|
1217
1659
|
log('添加边框')
|
|
1218
1660
|
# 获取工作表的整个范围(假设表格的数据是从A1开始)
|
|
@@ -1961,7 +2403,7 @@ def pre_format_columns_safe(sheet, columns, data_rows):
|
|
|
1961
2403
|
|
|
1962
2404
|
def post_format_columns_safe(sheet, columns, data_rows):
|
|
1963
2405
|
"""
|
|
1964
|
-
|
|
2406
|
+
后格式化函数:在写入数据后确认列格式并强制转换为文本
|
|
1965
2407
|
|
|
1966
2408
|
Args:
|
|
1967
2409
|
sheet: Excel工作表对象
|
|
@@ -1981,8 +2423,32 @@ def post_format_columns_safe(sheet, columns, data_rows):
|
|
|
1981
2423
|
# 只对实际有数据的行进行格式化
|
|
1982
2424
|
if data_rows > 0:
|
|
1983
2425
|
range_str = f'{col_name}1:{col_name}{data_rows}'
|
|
1984
|
-
sheet.range(range_str)
|
|
1985
|
-
|
|
2426
|
+
target_range = sheet.range(range_str)
|
|
2427
|
+
|
|
2428
|
+
# 设置格式为文本
|
|
2429
|
+
target_range.number_format = '@'
|
|
2430
|
+
|
|
2431
|
+
# 关键步骤:读取数据并重新写入,触发文本转换
|
|
2432
|
+
# 这样可以将已经写入的数字转换为文本格式
|
|
2433
|
+
values = target_range.value
|
|
2434
|
+
if values is not None:
|
|
2435
|
+
# 处理单个值的情况
|
|
2436
|
+
if not isinstance(values, list):
|
|
2437
|
+
if values != '':
|
|
2438
|
+
target_range.value = str(values)
|
|
2439
|
+
# 处理列表的情况(单列多行)
|
|
2440
|
+
elif len(values) > 0:
|
|
2441
|
+
# 检查是否是二维数组(实际上单列应该是一维数组)
|
|
2442
|
+
if isinstance(values[0], list):
|
|
2443
|
+
# 二维数组,取第一列
|
|
2444
|
+
converted_values = [[str(row[0]) if row[0] is not None and row[0] != '' else row[0]] for row in values]
|
|
2445
|
+
else:
|
|
2446
|
+
# 一维数组
|
|
2447
|
+
converted_values = [[str(val)] if val is not None and val != '' else [val] for val in values]
|
|
2448
|
+
# 重新写入(这次会按照文本格式写入)
|
|
2449
|
+
target_range.value = converted_values
|
|
2450
|
+
|
|
2451
|
+
log(f'后格式化并转换成功: {range_str}')
|
|
1986
2452
|
|
|
1987
2453
|
except Exception as e:
|
|
1988
2454
|
log(f'后格式化列 {col_name} 失败: {e},继续处理其他列')
|
|
@@ -2045,11 +2511,12 @@ def format_to_month(sheet, columns=None):
|
|
|
2045
2511
|
|
|
2046
2512
|
def add_sum_for_cell(sheet, col_list, row=2):
|
|
2047
2513
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2514
|
+
if last_row > row:
|
|
2515
|
+
for col_name in col_list:
|
|
2516
|
+
col_letter = find_column_by_data(sheet, 1, col_name)
|
|
2517
|
+
sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
|
|
2518
|
+
sheet.range(f'{col_letter}{row}').api.Font.Color = 255
|
|
2519
|
+
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
2053
2520
|
|
|
2054
2521
|
def clear_for_cell(sheet, col_list, row=2):
|
|
2055
2522
|
last_row = sheet.range('A' + str(sheet.cells.last_cell.row)).end('up').row
|
|
@@ -2077,6 +2544,7 @@ def add_formula_for_column(sheet, col_name, formula, start_row=2):
|
|
|
2077
2544
|
# AutoFill 快速填充到所有行(start_row 到 last_row)
|
|
2078
2545
|
sheet.range(f'{col_letter}{start_row}').api.AutoFill(
|
|
2079
2546
|
sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api)
|
|
2547
|
+
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
2080
2548
|
|
|
2081
2549
|
def autofit_column(sheet, columns=None):
|
|
2082
2550
|
if columns is None:
|