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/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
- data = sheet.range(f'{col_letter}1').expand('table').value
153
- # col_index = column_name_to_index(col_letter)
154
- start_row = 1
155
- merge_ranges = [] # 用来存储所有待合并的单元格范围
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
- other_columns_index = {}
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
- other_columns_index[col_name] = column_name_to_index(col_name)
244
+ all_columns.append(col_name)
163
245
 
164
- for row in range(2, len(data) + 1):
165
- log(f'查找 {row}/{len(data)}')
166
- if data[row - 1][0] != data[row - 2][0]:
167
- if row - start_row > 1:
168
- # 将合并范围加入列表
169
- merge_ranges.append((col_letter, start_row, row - 1))
170
- for col_name, col_index in other_columns_index.items():
171
- merge_ranges.append((col_name, start_row, row - 1))
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
- if len(data) - start_row > 1:
175
- merge_ranges.append((col_letter, start_row, len(data)))
176
- for col_name, col_index in other_columns_index.items():
177
- merge_ranges.append((col_name, start_row, len(data)))
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
- for col_name, start, end in merge_ranges:
181
- log(f'处理 {col_name}{start}:{col_name}{end} merge')
182
- sheet.range(f'{col_name}{start}:{col_name}{end}').merge()
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
- # 预计算所有单元格的合并区域信息 (优化点1)
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
- if cell_height < img_width:
618
- cell_range.row_height = max(150 / 8, img_width / cell_range.rows.count)
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.row_height = img_width
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
- top = cell_range.top + (cell_range.height - img_width) / 2
625
- left = cell_range.left + (cell_range.width - img_width) / 2
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
- with sync_playwright() as p:
697
- browser = p.chromium.launch(headless=True) # 运行时可以看到浏览器
698
- context = browser.new_context()
699
- page = context.new_page()
700
- # 直接通过Playwright下载图片
701
- response = page.request.get(image_url)
702
- with open(save_name, 'wb') as f:
703
- f.write(response.body()) # 将下载的内容保存为文件
704
- log(f"图片已通过chrome下载并保存为:{save_name}")
705
- # 关闭浏览器
706
- browser.close()
707
- return save_name
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).number_format = '@'
1985
- log(f'后格式化成功: {range_str}')
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
- for col_name in col_list:
2049
- col_letter = find_column_by_data(sheet, 1, col_name)
2050
- sheet.range(f'{col_letter}{row}').formula = f'=SUM({col_letter}{row + 1}:{col_letter}{last_row})'
2051
- sheet.range(f'{col_letter}{row}').api.Font.Color = 255
2052
- sheet.range(f'{col_letter}:{col_letter}').autofit()
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: