qrpa 1.0.23__py3-none-any.whl → 1.0.25__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/__init__.py CHANGED
@@ -3,7 +3,8 @@ from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_
3
3
 
4
4
  from .shein_ziniao import ZiniaoRunner
5
5
 
6
- from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename
6
+ # from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename, get_file_size, calculate_star_symbols
7
+ from .fun_base import *
7
8
 
8
9
  from .time_utils import TimeUtils
9
10
 
@@ -18,3 +19,7 @@ from .shein_excel import SheinExcel
18
19
  from .shein_lib import SheinLib
19
20
 
20
21
  from .fun_excel import InsertImageV2
22
+
23
+ from .temu_lib import TemuLib
24
+ from .temu_excel import TemuExcel
25
+ from .temu_chrome import temu_chrome_excute
qrpa/fun_base.py CHANGED
@@ -105,3 +105,233 @@ def copy_file(source, destination):
105
105
  print(f"错误:没有权限复制到 '{destination}'")
106
106
  except Exception as e:
107
107
  print(f"错误:发生未知错误 - {e}")
108
+
109
+ def get_file_size(file_path, human_readable=False):
110
+ """
111
+ 获取文件大小
112
+
113
+ :param file_path: 文件路径
114
+ :param human_readable: 是否返回可读格式(KB, MB, GB)
115
+ :return: 文件大小(字节数或可读格式)
116
+ """
117
+ if not os.path.isfile(file_path):
118
+ raise FileNotFoundError(f"文件不存在: {file_path}")
119
+
120
+ size_bytes = os.path.getsize(file_path)
121
+
122
+ if not human_readable:
123
+ return size_bytes
124
+
125
+ # 转换为可读单位
126
+ units = ["B", "KB", "MB", "GB", "TB"]
127
+ size = float(size_bytes)
128
+ for unit in units:
129
+ if size < 1024:
130
+ return f"{size:.2f} {unit}"
131
+ size /= 1024
132
+
133
+ def calculate_star_symbols(rating):
134
+ """
135
+ 计算星级对应的符号组合(独立评分逻辑函数)
136
+ 参数:
137
+ rating (int): 标准化评分(0-5)
138
+ 返回:
139
+ str: 星级符号字符串(如★★★⭐☆)
140
+ """
141
+ full_stars = int(rating)
142
+ empty_stars = 5 - full_stars
143
+ star_string = '★' * full_stars
144
+ star_string += '☆' * empty_stars
145
+ return star_string
146
+
147
+ def remove_columns(matrix, indices):
148
+ """
149
+ 过滤二维列表,移除指定索引的列
150
+
151
+ 参数:
152
+ matrix: 二维列表
153
+ indices: 需要移除的列索引列表
154
+
155
+ 返回:
156
+ 过滤后的二维列表
157
+ """
158
+ # 创建要保留的索引集合(排除需要移除的索引)
159
+ indices_to_keep = set(range(len(matrix[0]))) - set(indices)
160
+
161
+ # 遍历每行,只保留不在indices中的列
162
+ return [[row[i] for i in indices_to_keep] for row in matrix]
163
+
164
+
165
+ def filter_columns(matrix, indices):
166
+ """
167
+ 过滤二维列表,只保留指定索引的列
168
+
169
+ 参数:
170
+ matrix: 二维列表
171
+ indices: 需要保留的列索引列表
172
+
173
+ 返回:
174
+ 过滤后的二维列表
175
+ """
176
+ # 转置矩阵,获取每一列
177
+ columns = list(zip(*matrix))
178
+
179
+ # 只保留指定索引的列
180
+ filtered_columns = [columns[i] for i in indices]
181
+
182
+ # 将过滤后的列转回二维列表
183
+ return [list(row) for row in zip(*filtered_columns)]
184
+
185
+
186
+ # # 示例使用
187
+ # matrix = [
188
+ # [1, 2, 3, 4],
189
+ # [5, 6, 7, 8],
190
+ # [9, 10, 11, 12]
191
+ # ]
192
+ #
193
+ # # 只保留索引为 0 和 2 的列
194
+ # filtered = filter_columns(matrix, [0, 2])
195
+ # print(filtered) # 输出: [[1, 3], [5, 7], [9, 11]]
196
+
197
+ def add_column_to_2d_list(data, new_col, index=None):
198
+ """
199
+ 给二维列表增加一列数据
200
+
201
+ :param data: 原始二维列表,例如 [[1, 2], [3, 4]]
202
+ :param new_col: 要添加的新列数据,例如 [10, 20]
203
+ :param index: 插入位置,默认为最后一列之后;支持负数索引
204
+ :return: 增加新列后的二维列表
205
+ """
206
+ if not data:
207
+ raise ValueError("原始数据为空")
208
+ if len(data) != len(new_col):
209
+ raise ValueError("新列长度必须与原始数据的行数相等")
210
+
211
+ new_data = []
212
+ for i, row in enumerate(data):
213
+ row = list(row) # 防止修改原始数据
214
+ insert_at = index if index is not None else len(row)
215
+ row.insert(insert_at, new_col[i])
216
+ new_data.append(row)
217
+ return new_data
218
+
219
+
220
+ def add_prefixed_column(data, header, value):
221
+ """
222
+ 给二维列表增加第一列,第一行为 header,后面为 value。
223
+
224
+ :param data: 原始二维列表
225
+ :param header: 新列的标题
226
+ :param value: 新列内容(相同值)
227
+ :return: 增加新列后的二维列表
228
+ """
229
+ if not data:
230
+ raise ValueError("原始数据不能为空")
231
+
232
+ new_col = [header] + [value] * (len(data) - 1)
233
+ return [[new_col[i]] + row for i, row in enumerate(data)]
234
+
235
+
236
+ def add_suffixed_column(data, header, value):
237
+ """
238
+ 给二维列表增加第一列,第一行为 header,后面为 value。
239
+
240
+ :param data: 原始二维列表
241
+ :param header: 新列的标题
242
+ :param value: 新列内容(相同值)
243
+ :return: 增加新列后的二维列表
244
+ """
245
+ if not data:
246
+ raise ValueError("原始数据不能为空")
247
+
248
+ new_col = [header] + [value] * (len(data) - 1)
249
+ return [row + [new_col[i]] for i, row in enumerate(data)]
250
+
251
+
252
+ def merge_2d_lists_keep_first_header(data1, data2):
253
+ """
254
+ 合并两个二维列表,只保留第一个列表的标题(即第一行)。
255
+
256
+ :param data1: 第一个二维列表(包含标题)
257
+ :param data2: 第二个二维列表(包含标题)
258
+ :return: 合并后的二维列表
259
+ """
260
+ if not data1 or not isinstance(data1, list):
261
+ raise ValueError("data1 不能为空并且必须是二维列表")
262
+ if not data2 or not isinstance(data2, list):
263
+ raise ValueError("data2 不能为空并且必须是二维列表")
264
+
265
+ header = data1[0]
266
+ rows1 = data1[1:]
267
+ rows2 = data2[1:]
268
+
269
+ return [header] + rows1 + rows2
270
+
271
+
272
+ def insert_total_row(data, row_index=1, label="合计"):
273
+ """
274
+ 在指定行插入一行,第一列为 label,其余为空字符串。
275
+
276
+ :param data: 原始二维列表
277
+ :param row_index: 插入位置,默认插在第二行(索引1)
278
+ :param label: 第一列的标签内容,默认为 "合计"
279
+ :return: 新的二维列表
280
+ """
281
+ if not data or not isinstance(data, list):
282
+ raise ValueError("data 不能为空并且必须是二维列表")
283
+
284
+ num_cols = len(data[0])
285
+ new_row = [label] + [""] * (num_cols - 1)
286
+
287
+ return data[:row_index] + [new_row] + data[row_index:]
288
+
289
+
290
+ def insert_empty_column_after(data, col_index, new_header="单价成本"):
291
+ """
292
+ 在二维列表中指定列的后面插入一个新列,标题为 new_header,其余内容为空字符串。
293
+
294
+ :param data: 原始二维列表
295
+ :param col_index: 要插入的位置(在该列后面插入)
296
+ :param new_header: 新列的标题
297
+ :return: 新的二维列表
298
+ """
299
+ if not data or not isinstance(data, list):
300
+ raise ValueError("data 不能为空且必须是二维列表")
301
+
302
+ new_data = []
303
+ for i, row in enumerate(data):
304
+ row = list(row) # 复制避免修改原数据
305
+ insert_value = new_header if i == 0 else ""
306
+ row.insert(col_index + 1, insert_value)
307
+ new_data.append(row)
308
+
309
+ return new_data
310
+
311
+
312
+ def insert_empty_column_after_column_name(data, target_col_name, new_header="单价成本"):
313
+ """
314
+ 在指定列名对应的列后面插入一个新列,标题为 new_header,其余行为空字符串。
315
+
316
+ :param data: 原始二维列表
317
+ :param target_col_name: 要在哪一列之后插入(通过列标题匹配)
318
+ :param new_header: 插入的新列标题
319
+ :return: 新的二维列表
320
+ """
321
+ if not data or not isinstance(data, list):
322
+ raise ValueError("data 不能为空且必须是二维列表")
323
+
324
+ header = data[0]
325
+ if target_col_name not in header:
326
+ raise ValueError(f"找不到列名:{target_col_name}")
327
+
328
+ col_index = header.index(target_col_name)
329
+
330
+ new_data = []
331
+ for i, row in enumerate(data):
332
+ row = list(row) # 防止修改原始数据
333
+ insert_value = new_header if i == 0 else ""
334
+ row.insert(col_index + 1, insert_value)
335
+ new_data.append(row)
336
+
337
+ return new_data
qrpa/fun_excel.py CHANGED
@@ -12,6 +12,7 @@ import concurrent.futures
12
12
  from collections import defaultdict
13
13
  import threading
14
14
  from playwright.sync_api import sync_playwright
15
+ import psutil
15
16
 
16
17
  from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
17
18
 
@@ -89,13 +90,13 @@ def set_cell_prefix_red(cell, n, color_name):
89
90
  except Exception as e:
90
91
  print(f"设置字体颜色失败: {e}")
91
92
 
92
- def sort_by_column(data, col_index, start_row=2, reverse=True):
93
- if not data or start_row >= len(data):
93
+ def sort_by_column(data, col_index, header_rows=2, reverse=True):
94
+ if not data or header_rows >= len(data):
94
95
  return data
95
96
 
96
97
  try:
97
- header = data[:start_row]
98
- new_data_sorted = data[start_row:]
98
+ header = data[:header_rows]
99
+ new_data_sorted = data[header_rows:]
99
100
 
100
101
  def get_key(row):
101
102
  value = row[col_index]
@@ -541,11 +542,11 @@ def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
541
542
 
542
543
  return None
543
544
 
544
- def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150, img_save_key=None, dir_name=None, cell_height_with_img=False):
545
+ 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):
545
546
  if not columns:
546
547
  return
547
548
 
548
- minimize(app)
549
+ minimize(sheet.book.app)
549
550
 
550
551
  # 清空所有图片
551
552
  clear_all_pictures(sheet)
@@ -574,7 +575,7 @@ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150,
574
575
 
575
576
  # 预计算所有单元格的合并区域信息 (优化点1)
576
577
  area_map = {}
577
- for row in range(2, last_row + 1):
578
+ for row in range(start_row, last_row + 1):
578
579
  log(f'计算 {row}/{last_row}') # 如果数据量非常大,这里的日志会影响性能,可以考虑优化
579
580
  for col_letter in col_letter_map.values():
580
581
  cell_ref = f'{col_letter}{row}'
@@ -615,7 +616,7 @@ def InsertImageV2(app, wb, sheet, columns=None, platform='shein', img_width=150,
615
616
  }
616
617
 
617
618
  # 处理图片插入 (优化点2)
618
- for row in range(2, last_row + 1):
619
+ for row in range(start_row, last_row + 1):
619
620
  for img_col_name, col_letter in col_letter_map.items():
620
621
  cell_ref = f'{col_letter}{row}'
621
622
  cell_range = sheet.range(cell_ref)
@@ -1174,8 +1175,8 @@ def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
1174
1175
  wb.save()
1175
1176
  close_excel(app, wb)
1176
1177
 
1177
- def colorize_by_field(app, wb, sheet, field):
1178
- minimize(app)
1178
+ def colorize_by_field(sheet, field):
1179
+ minimize(sheet.book.app)
1179
1180
  # 读取数据
1180
1181
  field_column = find_column_by_data(sheet, 1, field) # 假设 SPU 在 C 列
1181
1182
  if field_column is None:
@@ -2061,6 +2062,21 @@ def format_to_number(sheet, columns=None, decimal_places=2):
2061
2062
  # else:
2062
2063
  # sheet.range(f'{col_name}:{col_name}').number_format = f'0.{"0" * decimal_places}'
2063
2064
 
2065
+ def hidden_columns(sheet, columns=None):
2066
+ if columns is None:
2067
+ return
2068
+ used_range_col = sheet.range('A1').expand('right')
2069
+ for j, cell in enumerate(used_range_col):
2070
+ col = j + 1
2071
+ col_name = index_to_column_name(col)
2072
+ col_val = sheet.range(f'{col_name}1').value
2073
+ if col_val is None:
2074
+ continue
2075
+ for c in columns:
2076
+ if c in col_val:
2077
+ log(f'设置[{c}] 隐藏')
2078
+ sheet.range(f'{col_name}:{col_name}').column_width = 0
2079
+
2064
2080
  def column_to_right(sheet, columns=None):
2065
2081
  if columns is None:
2066
2082
  return
@@ -2076,7 +2092,7 @@ def column_to_right(sheet, columns=None):
2076
2092
  # 水平对齐: # -4108:居中 # -4131:左对齐 # -4152:右对齐
2077
2093
  # 垂直对齐: # -4108:居中 # -4160:顶部对齐 # -4107:底部对齐
2078
2094
  # 所有列水平居中和垂直居中
2079
- log(f'设置[{c}] 水平垂直居中')
2095
+ log(f'设置[{c}] 水平右对齐')
2080
2096
  sheet.range(f'{col_name}:{col_name}').api.HorizontalAlignment = -4152
2081
2097
  sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2082
2098
 
@@ -2117,6 +2133,16 @@ def beautify_title(sheet):
2117
2133
  sheet.range(f'{col_name}:{col_name}').api.VerticalAlignment = -4108
2118
2134
  sheet.autofit()
2119
2135
 
2136
+ def set_body_style(sheet, row_start, row_end=None):
2137
+ if row_end is None:
2138
+ row_end = get_last_used_row(sheet)
2139
+
2140
+ range = sheet.range(f'{row_start}:{row_end}')
2141
+ # 设置字体名称
2142
+ range.font.name = 'Calibri'
2143
+ # 设置字体大小
2144
+ range.font.size = 11
2145
+
2120
2146
  def set_title_style(sheet, rows=2):
2121
2147
  col = get_max_column_letter(sheet)
2122
2148
  range = sheet.range(f'A1:{col}{rows}')
@@ -2405,6 +2431,7 @@ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs)
2405
2431
  log(f"格式化失败: {e}")
2406
2432
  return False
2407
2433
 
2434
+ # 经过观察 fortmat时 传入函数需要为类函数且第二个参数必须是 sheet
2408
2435
  def batch_excel_operations(excel_path, operations):
2409
2436
  """
2410
2437
  批量 Excel 操作函数,一次性打开 Excel 执行多个操作
@@ -2439,17 +2466,19 @@ def batch_excel_operations(excel_path, operations):
2439
2466
  sheet.activate()
2440
2467
 
2441
2468
  if operation_type == 'write':
2442
- data, format_to_text_colunm = args[:2]
2469
+ data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
2443
2470
  # 清空工作表
2444
2471
  sheet.clear()
2445
2472
  # 格式化文本列
2446
- format_to_text_v2(sheet, format_to_text_colunm)
2473
+ if format_to_text_colunm:
2474
+ format_to_text_v2(sheet, format_to_text_colunm)
2447
2475
  # 写入数据
2448
2476
  sheet.range('A1').value = data
2449
2477
  log(f"批量操作:写入数据到 {sheet_name}")
2450
2478
 
2451
2479
  elif operation_type == 'format':
2452
2480
  format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
2481
+ log('格式化入参', *format_args)
2453
2482
  # 执行格式化
2454
2483
  format_func(sheet, *format_args)
2455
2484
  log(f"批量操作:格式化工作表 {sheet_name}")
@@ -2478,6 +2507,26 @@ def batch_excel_operations(excel_path, operations):
2478
2507
  finally:
2479
2508
  # 释放锁但不关闭 Excel(保持复用)
2480
2509
  excel_lock_manager.release_excel_lock(excel_path)
2510
+ close_excel_with_lock(excel_path, app, wb, True)
2511
+
2512
+ def close_excel_file(file_path):
2513
+ file_path = os.path.abspath(file_path).lower()
2514
+
2515
+ for proc in psutil.process_iter(['pid', 'name']):
2516
+ if proc.info['name'] and proc.info['name'].lower() in ['excel.exe', 'wps.exe']: # 只找 Excel
2517
+ try:
2518
+ for f in proc.open_files():
2519
+ if os.path.abspath(f.path).lower() == file_path:
2520
+ print(f"文件被 Excel 占用 (PID: {proc.pid}),正在关闭进程...")
2521
+ proc.terminate()
2522
+ proc.wait(timeout=3)
2523
+ print("已关闭。")
2524
+ return True
2525
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
2526
+ continue
2527
+
2528
+ print("文件没有被 Excel 占用。")
2529
+ return False
2481
2530
 
2482
2531
  def force_close_excel_file(excel_path):
2483
2532
  """
@@ -2673,3 +2722,6 @@ def get_excel_status(excel_path):
2673
2722
  'operation_count': excel_lock_manager.get_operation_count(excel_path),
2674
2723
  'has_lock' : excel_lock_manager.get_file_lock(excel_path).locked()
2675
2724
  }
2725
+
2726
+ def get_last_used_row(sheet):
2727
+ return sheet.used_range.last_cell.row