qrpa 1.0.95__py3-none-any.whl → 1.0.97__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/fun_excel.py +1 -0
- qrpa/fun_web.py +65 -7
- qrpa/shein_excel.py +110 -1
- qrpa/shein_lib.py +47 -0
- qrpa/shein_ziniao.py +2 -2
- qrpa/time_utils.py +30 -43
- {qrpa-1.0.95.dist-info → qrpa-1.0.97.dist-info}/METADATA +1 -1
- {qrpa-1.0.95.dist-info → qrpa-1.0.97.dist-info}/RECORD +10 -10
- {qrpa-1.0.95.dist-info → qrpa-1.0.97.dist-info}/WHEEL +0 -0
- {qrpa-1.0.95.dist-info → qrpa-1.0.97.dist-info}/top_level.txt +0 -0
qrpa/fun_excel.py
CHANGED
|
@@ -2149,6 +2149,7 @@ def add_formula_for_column(sheet, col_name, formula, start_row=2):
|
|
|
2149
2149
|
# AutoFill 快速填充到所有行(start_row 到 last_row)
|
|
2150
2150
|
sheet.range(f'{col_letter}{start_row}').api.AutoFill(
|
|
2151
2151
|
sheet.range(f'{col_letter}{start_row}:{col_letter}{last_row}').api)
|
|
2152
|
+
sheet.range(f'{col_letter}:{col_letter}').autofit()
|
|
2152
2153
|
|
|
2153
2154
|
def autofit_column(sheet, columns=None):
|
|
2154
2155
|
if columns is None:
|
qrpa/fun_web.py
CHANGED
|
@@ -10,12 +10,13 @@ import inspect
|
|
|
10
10
|
def fetch(page: Page, url: str, params: Optional[Union[dict, list, str]] = None, headers: Optional[dict] = None, config:
|
|
11
11
|
Optional[dict] = None) -> dict:
|
|
12
12
|
"""
|
|
13
|
-
发送 HTTP POST 请求,支持自定义 headers
|
|
13
|
+
发送 HTTP POST 请求,支持自定义 headers 和重定向处理。
|
|
14
14
|
|
|
15
15
|
:param page: Playwright 的 Page 对象
|
|
16
16
|
:param url: 请求地址
|
|
17
17
|
:param params: 请求参数(dict、list、str 或 None)
|
|
18
18
|
:param headers: 自定义 headers 字典
|
|
19
|
+
:param config: 请求配置字典
|
|
19
20
|
:return: 服务器返回的 JSON 响应(dict)
|
|
20
21
|
"""
|
|
21
22
|
if params is not None and not isinstance(params, (dict, list, str)):
|
|
@@ -26,7 +27,7 @@ Optional[dict] = None) -> dict:
|
|
|
26
27
|
try:
|
|
27
28
|
page.wait_for_load_state('load')
|
|
28
29
|
response = page.evaluate("""
|
|
29
|
-
async ({ url, params, extraHeaders }) => {
|
|
30
|
+
async ({ url, params, extraHeaders, config }) => {
|
|
30
31
|
try {
|
|
31
32
|
const defaultHeaders = {
|
|
32
33
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
@@ -37,9 +38,15 @@ Optional[dict] = None) -> dict:
|
|
|
37
38
|
const options = {
|
|
38
39
|
method: 'POST',
|
|
39
40
|
credentials: 'include',
|
|
41
|
+
redirect: 'follow', // 明确设置跟随重定向
|
|
40
42
|
headers: headers
|
|
41
43
|
};
|
|
42
44
|
|
|
45
|
+
// 应用额外配置
|
|
46
|
+
if (config) {
|
|
47
|
+
Object.assign(options, config);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
if (params !== null) {
|
|
44
51
|
if (typeof params === 'string') {
|
|
45
52
|
options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
@@ -51,15 +58,40 @@ Optional[dict] = None) -> dict:
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
const response = await fetch(url, options);
|
|
61
|
+
|
|
62
|
+
// 处理重定向
|
|
63
|
+
if (response.redirected) {
|
|
64
|
+
console.log(`请求被重定向到: ${response.url}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
if (!response.ok) {
|
|
68
|
+
// 如果是重定向相关的状态码,尝试获取响应内容
|
|
69
|
+
if (response.status >= 300 && response.status < 400) {
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
return {
|
|
72
|
+
"error": "redirect_error",
|
|
73
|
+
"message": `HTTP ${response.status} - ${response.statusText}`,
|
|
74
|
+
"redirect_url": response.url,
|
|
75
|
+
"response_text": text,
|
|
76
|
+
"status": response.status
|
|
77
|
+
};
|
|
78
|
+
}
|
|
55
79
|
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
56
80
|
}
|
|
57
|
-
|
|
81
|
+
|
|
82
|
+
// 尝试解析 JSON,如果失败则返回文本内容
|
|
83
|
+
const contentType = response.headers.get('content-type');
|
|
84
|
+
if (contentType && contentType.includes('application/json')) {
|
|
85
|
+
return await response.json();
|
|
86
|
+
} else {
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
return { "content": text, "content_type": contentType, "final_url": response.url };
|
|
89
|
+
}
|
|
58
90
|
} catch (error) {
|
|
59
91
|
return { "error": "fetch_failed", "message": error.message };
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
|
-
""", {"url": url, "params": params, "extraHeaders": headers})
|
|
94
|
+
""", {"url": url, "params": params, "extraHeaders": headers, "config": config})
|
|
63
95
|
|
|
64
96
|
return response
|
|
65
97
|
except Exception as e:
|
|
@@ -148,7 +180,7 @@ def full_screen_shot(web_page: Page, config):
|
|
|
148
180
|
|
|
149
181
|
def fetch_get(page: Page, url: str, headers: Optional[dict] = None, config: Optional[dict] = None) -> dict:
|
|
150
182
|
"""
|
|
151
|
-
发送 HTTP GET 请求,支持自定义 headers
|
|
183
|
+
发送 HTTP GET 请求,支持自定义 headers 和配置,支持重定向处理。
|
|
152
184
|
|
|
153
185
|
:param page: Playwright 的 Page 对象
|
|
154
186
|
:param url: 请求地址
|
|
@@ -173,17 +205,43 @@ def fetch_get(page: Page, url: str, headers: Optional[dict] = None, config: Opti
|
|
|
173
205
|
const defaultConfig = {
|
|
174
206
|
method: 'GET',
|
|
175
207
|
credentials: 'include',
|
|
176
|
-
mode: 'cors'
|
|
208
|
+
mode: 'cors',
|
|
209
|
+
redirect: 'follow' // 明确设置跟随重定向
|
|
177
210
|
};
|
|
178
211
|
|
|
179
212
|
const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
|
|
180
213
|
const options = Object.assign({}, defaultConfig, config || {}, { headers: headers });
|
|
181
214
|
|
|
182
215
|
const response = await fetch(url, options);
|
|
216
|
+
|
|
217
|
+
// 处理重定向
|
|
218
|
+
if (response.redirected) {
|
|
219
|
+
console.log(`请求被重定向到: ${response.url}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
183
222
|
if (!response.ok) {
|
|
223
|
+
// 如果是重定向相关的状态码,尝试获取响应内容
|
|
224
|
+
if (response.status >= 300 && response.status < 400) {
|
|
225
|
+
const text = await response.text();
|
|
226
|
+
return {
|
|
227
|
+
"error": "redirect_error",
|
|
228
|
+
"message": `HTTP ${response.status} - ${response.statusText}`,
|
|
229
|
+
"redirect_url": response.url,
|
|
230
|
+
"response_text": text,
|
|
231
|
+
"status": response.status
|
|
232
|
+
};
|
|
233
|
+
}
|
|
184
234
|
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
|
|
185
235
|
}
|
|
186
|
-
|
|
236
|
+
|
|
237
|
+
// 尝试解析 JSON,如果失败则返回文本内容
|
|
238
|
+
const contentType = response.headers.get('content-type');
|
|
239
|
+
if (contentType && contentType.includes('application/json')) {
|
|
240
|
+
return await response.json();
|
|
241
|
+
} else {
|
|
242
|
+
const text = await response.text();
|
|
243
|
+
return { "content": text, "content_type": contentType, "final_url": response.url };
|
|
244
|
+
}
|
|
187
245
|
} catch (error) {
|
|
188
246
|
return { "error": "fetch_failed", "message": error.message };
|
|
189
247
|
}
|
qrpa/shein_excel.py
CHANGED
|
@@ -2442,7 +2442,7 @@ class SheinExcel:
|
|
|
2442
2442
|
self.dealFormula(sheet) # 有空再封装优化
|
|
2443
2443
|
colorize_by_field(sheet, 'SPU')
|
|
2444
2444
|
autofit_column(sheet, ['商品信息', '店铺名称', 'SKC点击率/SKC转化率', '自主参与活动'])
|
|
2445
|
-
column_to_left(sheet, ['店铺名称', 'SKC点击率/SKC转化率', '自主参与活动','近7天SKU销量/SKC销量/SKC曝光'])
|
|
2445
|
+
column_to_left(sheet, ['店铺名称', 'SKC点击率/SKC转化率', '自主参与活动', '近7天SKU销量/SKC销量/SKC曝光'])
|
|
2446
2446
|
specify_column_width(sheet, ['商品标题'], 150 / 6)
|
|
2447
2447
|
add_borders(sheet)
|
|
2448
2448
|
InsertImageV2(sheet, ['SKC图片', 'SKU图片'], 'shein', 120, None, None, True)
|
|
@@ -2530,3 +2530,112 @@ class SheinExcel:
|
|
|
2530
2530
|
sheet.range(rangeF).number_format = '0.00'
|
|
2531
2531
|
sheet.range(rangeG).formula = f'=IF(ISNUMBER({rangeB}),{col_week_1}{row}*{col_gross_profit}{row},"")'
|
|
2532
2532
|
sheet.range(rangeG).number_format = '0.00'
|
|
2533
|
+
|
|
2534
|
+
def write_check_order(self, erp, start_date, end_date):
|
|
2535
|
+
header = ['店铺账号', '店铺别名', '店长', '报账单号', '货号', 'SKC', '平台SKU', '商家SKU', '属性集', '商品数量', '账单类型', '收支类型', '状态', '币种', '金额', 'ERP成本',
|
|
2536
|
+
'成本总额', '业务单号', '费用类型', '备注', '来源单号', '账单创建时间', '台账添加时间', '报账时间', '预计结算日期', '实际结算日期']
|
|
2537
|
+
excel_data = [header]
|
|
2538
|
+
|
|
2539
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
2540
|
+
|
|
2541
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/check_order_{start_date}_{end_date}.json'
|
|
2542
|
+
dict = read_dict_from_file(cache_file)
|
|
2543
|
+
for store_username, data_list in dict.items():
|
|
2544
|
+
for item in data_list:
|
|
2545
|
+
store_name = dict_store.get(store_username)
|
|
2546
|
+
store_manager = self.config.shein_store_manager.get(str(store_username).lower())
|
|
2547
|
+
|
|
2548
|
+
row_item = []
|
|
2549
|
+
row_item.append(store_username)
|
|
2550
|
+
row_item.append(store_name)
|
|
2551
|
+
row_item.append(store_manager)
|
|
2552
|
+
row_item.append(item['reportOrderNo'])
|
|
2553
|
+
row_item.append(item['goodsSn'])
|
|
2554
|
+
row_item.append(item['skcName'])
|
|
2555
|
+
row_item.append(item['skuCode'])
|
|
2556
|
+
row_item.append(item['skuSn'])
|
|
2557
|
+
row_item.append(item['suffix'])
|
|
2558
|
+
row_item.append(item['goodsCount'])
|
|
2559
|
+
row_item.append(item['secondOrderTypeName'])
|
|
2560
|
+
row_item.append(item['inAndOutName'])
|
|
2561
|
+
row_item.append(item['settlementStatusName'])
|
|
2562
|
+
row_item.append(item['settleCurrencyCode'])
|
|
2563
|
+
row_item.append(item['income'])
|
|
2564
|
+
row_item.append(self.bridge.get_sku_cost(item['skuSn'], erp))
|
|
2565
|
+
row_item.append('')
|
|
2566
|
+
row_item.append(item['bzOrderNo'])
|
|
2567
|
+
row_item.append(item['expenseTypeName'])
|
|
2568
|
+
row_item.append(item['remark'])
|
|
2569
|
+
row_item.append(item['sourceNo'])
|
|
2570
|
+
row_item.append(item['addTime'])
|
|
2571
|
+
row_item.append(item['businessCompletedTime'])
|
|
2572
|
+
row_item.append(item['reportTime'])
|
|
2573
|
+
row_item.append(item['estimatePayTime'])
|
|
2574
|
+
row_item.append(item['completedPayTime'])
|
|
2575
|
+
|
|
2576
|
+
excel_data.append(row_item)
|
|
2577
|
+
|
|
2578
|
+
cache_file_excel = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_excel_{start_date}_{end_date}.json'
|
|
2579
|
+
write_dict_to_file(cache_file_excel, excel_data)
|
|
2580
|
+
|
|
2581
|
+
sheet_name = '收支明细'
|
|
2582
|
+
batch_excel_operations(self.config.excel_shein_finance_month_report_pop, [
|
|
2583
|
+
(sheet_name, 'write', excel_data, ['R']),
|
|
2584
|
+
(sheet_name, 'format', self.format_check_order)
|
|
2585
|
+
])
|
|
2586
|
+
|
|
2587
|
+
header = ['店铺账号', '店铺别名', '店长', '出库金额', '出库成本', '备货作业费', '代收服务费', '订单履约服务费', '订单退货', '退货处理费', '退货单履约服务费', '利润']
|
|
2588
|
+
excel_data = [header]
|
|
2589
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/check_order_{start_date}_{end_date}.json'
|
|
2590
|
+
dict = read_dict_from_file(cache_file)
|
|
2591
|
+
for store_username, data_list in dict.items():
|
|
2592
|
+
store_name = dict_store.get(store_username)
|
|
2593
|
+
store_manager = self.config.shein_store_manager.get(str(store_username).lower())
|
|
2594
|
+
row_item = []
|
|
2595
|
+
row_item.append(store_username)
|
|
2596
|
+
row_item.append(store_name)
|
|
2597
|
+
row_item.append(store_manager)
|
|
2598
|
+
row_item.append('')
|
|
2599
|
+
row_item.append('')
|
|
2600
|
+
row_item.append('')
|
|
2601
|
+
row_item.append('')
|
|
2602
|
+
row_item.append('')
|
|
2603
|
+
row_item.append('')
|
|
2604
|
+
row_item.append('')
|
|
2605
|
+
row_item.append('')
|
|
2606
|
+
row_item.append('')
|
|
2607
|
+
excel_data.append(row_item)
|
|
2608
|
+
|
|
2609
|
+
sheet_name = '总表'
|
|
2610
|
+
batch_excel_operations(self.config.excel_shein_finance_month_report_pop, [
|
|
2611
|
+
(sheet_name, 'write', excel_data),
|
|
2612
|
+
(sheet_name, 'format', self.format_check_order),
|
|
2613
|
+
('Sheet1', 'delete'),
|
|
2614
|
+
(sheet_name, 'move', 1),
|
|
2615
|
+
])
|
|
2616
|
+
|
|
2617
|
+
def format_check_order(self, sheet):
|
|
2618
|
+
if sheet.name == '收支明细':
|
|
2619
|
+
beautify_title(sheet)
|
|
2620
|
+
add_borders(sheet)
|
|
2621
|
+
format_to_datetime(sheet, ['时间'])
|
|
2622
|
+
format_to_date(sheet, ['日期'])
|
|
2623
|
+
format_to_money(sheet, ['金额', '成本'])
|
|
2624
|
+
column_to_right(sheet, ['金额', '成本'])
|
|
2625
|
+
column_to_left(sheet, ['货号', '商家SKU'])
|
|
2626
|
+
add_formula_for_column(sheet, '成本总额', '=IF(ISNUMBER(O2),O2*J2,0)')
|
|
2627
|
+
|
|
2628
|
+
if sheet.name == '总表':
|
|
2629
|
+
beautify_title(sheet)
|
|
2630
|
+
add_borders(sheet)
|
|
2631
|
+
format_to_money(sheet, ['金额', '成本', '费', '订单退货', '利润'])
|
|
2632
|
+
column_to_right(sheet, ['金额', '成本', '费', '订单退货', '利润'])
|
|
2633
|
+
add_formula_for_column(sheet, '出库金额', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"收入")')
|
|
2634
|
+
add_formula_for_column(sheet, '出库成本', '=SUMIFS(收支明细!P:P,收支明细!A:A,A2,收支明细!L:L,"收入")')
|
|
2635
|
+
add_formula_for_column(sheet, '备货作业费', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"备货作业费")')
|
|
2636
|
+
add_formula_for_column(sheet, '代收服务费', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"代收服务费")')
|
|
2637
|
+
add_formula_for_column(sheet, '订单履约服务费', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"订单履约服务费")')
|
|
2638
|
+
add_formula_for_column(sheet, '订单退货', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"订单退货")')
|
|
2639
|
+
add_formula_for_column(sheet, '退货处理费', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"退货处理费")')
|
|
2640
|
+
add_formula_for_column(sheet, '退货单履约服务费', '=SUMIFS(收支明细!O:O,收支明细!A:A,A2,收支明细!L:L,"支出",收支明细!K:K,"退货单履约服务费")')
|
|
2641
|
+
add_formula_for_column(sheet, '利润', '=D2-E2-F2-G2-H2-I2-J2')
|
qrpa/shein_lib.py
CHANGED
|
@@ -26,6 +26,7 @@ class SheinLib:
|
|
|
26
26
|
self.DictQueryTime = {}
|
|
27
27
|
|
|
28
28
|
self.deal_auth()
|
|
29
|
+
# self.get_user()
|
|
29
30
|
|
|
30
31
|
# 处理鉴权
|
|
31
32
|
def deal_auth(self):
|
|
@@ -101,6 +102,10 @@ class SheinLib:
|
|
|
101
102
|
if "crashed" in str(status_error):
|
|
102
103
|
break
|
|
103
104
|
|
|
105
|
+
if web_page.locator('xpath=//div[text()="扫码登录"]').is_visible():
|
|
106
|
+
log('检查到扫码登录,切换至账号登录', self.store_username, self.store_name)
|
|
107
|
+
web_page.locator('xpath=//*[@id="container"]/div[2]/div[4]/img').click()
|
|
108
|
+
|
|
104
109
|
if web_page.locator('xpath=//div[@id="container" and @alita-name="gmpsso"]//button[@type="button" and @id]').nth(0).is_visible():
|
|
105
110
|
if 'https://sso.geiwohuo.com/#/home' not in web_page.url:
|
|
106
111
|
log("鉴权确定按钮可见 点击'确定'按钮", web_page.title(), web_page.url, self.store_username, self.store_name)
|
|
@@ -2889,3 +2894,45 @@ class SheinLib:
|
|
|
2889
2894
|
write_dict_to_file_ex(cache_file, {self.store_name: NotifyItem}, {self.store_name})
|
|
2890
2895
|
|
|
2891
2896
|
return excel_data
|
|
2897
|
+
|
|
2898
|
+
def check_order_list(self, source, first_day, last_day):
|
|
2899
|
+
page_num = 1
|
|
2900
|
+
page_size = 200 # 列表最多返回200条数据 大了没有用
|
|
2901
|
+
|
|
2902
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/check_order_{first_day}_{last_day}.json'
|
|
2903
|
+
list_item = read_dict_from_file_ex(cache_file, self.store_username)
|
|
2904
|
+
|
|
2905
|
+
url = f"https://sso.geiwohuo.com/gsfs/finance/reportOrder/dualMode/checkOrderList/item/union"
|
|
2906
|
+
payload = {
|
|
2907
|
+
"page" : page_num,
|
|
2908
|
+
"perPage" : page_size,
|
|
2909
|
+
"detailAddTimeStart": f"{first_day} 00:00:00",
|
|
2910
|
+
"detailAddTimeEnd" : f"{last_day} 23:59:59"
|
|
2911
|
+
}
|
|
2912
|
+
response_text = fetch(self.web_page, url, payload)
|
|
2913
|
+
error_code = response_text.get('code')
|
|
2914
|
+
if str(error_code) != '0':
|
|
2915
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
2916
|
+
list_item = response_text['info']['data']
|
|
2917
|
+
total = response_text['info']['meta']['count']
|
|
2918
|
+
totalPage = math.ceil(total / page_size)
|
|
2919
|
+
|
|
2920
|
+
if int(total) == len(list_item):
|
|
2921
|
+
log('总数与缓存数量相同 跳过剩余页抓取', total)
|
|
2922
|
+
return list_item
|
|
2923
|
+
|
|
2924
|
+
for page in range(2, totalPage + 1):
|
|
2925
|
+
log(f'获取收支明细列表 第{page}/{totalPage}页')
|
|
2926
|
+
payload['pageNumber'] = page
|
|
2927
|
+
response_text = fetch(self.web_page, url, payload)
|
|
2928
|
+
spu_list_new = response_text['info']['data']
|
|
2929
|
+
list_item += spu_list_new
|
|
2930
|
+
time.sleep(0.1)
|
|
2931
|
+
|
|
2932
|
+
for item in list_item:
|
|
2933
|
+
supplierSku = item['skuSn']
|
|
2934
|
+
item['cost_price'] = self.bridge.get_sku_cost(supplierSku, source)
|
|
2935
|
+
item['sku_img'] = self.bridge.get_sku_img(supplierSku, source)
|
|
2936
|
+
|
|
2937
|
+
write_dict_to_file_ex(cache_file, {self.store_username: list_item}, [self.store_username])
|
|
2938
|
+
return list_item
|
qrpa/shein_ziniao.py
CHANGED
|
@@ -376,7 +376,7 @@ class ZiniaoTaskManager:
|
|
|
376
376
|
return
|
|
377
377
|
|
|
378
378
|
# 打开店铺
|
|
379
|
-
print(f"=====打开店铺:{store_name}=====")
|
|
379
|
+
print(f"=====打开店铺:{store_name},{browser_id},{store_username}=====")
|
|
380
380
|
ret_json = self.browser.open_store(store_id)
|
|
381
381
|
print(ret_json)
|
|
382
382
|
store_id = ret_json.get("browserOauth") or ret_json.get("browserId")
|
|
@@ -488,7 +488,7 @@ class ZiniaoRunner:
|
|
|
488
488
|
raise RuntimeError("店铺列表为空")
|
|
489
489
|
|
|
490
490
|
# 多线程并发执行任务
|
|
491
|
-
max_threads =
|
|
491
|
+
max_threads = 3 if (hostname().lower() == 'krrpa' or hostname().lower() == 'jyrpa') else 3
|
|
492
492
|
log(f'当前启用线程数: {max_threads}')
|
|
493
493
|
self.task_manager.run_with_thread_pool(browser_list, max_threads, run, task_key, just_store_username, is_skip_store)
|
|
494
494
|
|
qrpa/time_utils.py
CHANGED
|
@@ -8,7 +8,6 @@ import calendar
|
|
|
8
8
|
from datetime import date, datetime, timedelta, timezone
|
|
9
9
|
from typing import Optional, Tuple, List, Union
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
class TimeUtils:
|
|
13
12
|
"""时间工具类,提供各种时间相关的静态方法"""
|
|
14
13
|
|
|
@@ -94,6 +93,17 @@ class TimeUtils:
|
|
|
94
93
|
last_month = today.month - 1 if today.month > 1 else 12
|
|
95
94
|
return last_month
|
|
96
95
|
|
|
96
|
+
@staticmethod
|
|
97
|
+
def get_last_two_month() -> int:
|
|
98
|
+
"""获取上上个月的月份"""
|
|
99
|
+
today = datetime.today()
|
|
100
|
+
# 计算上上个月:当前月份减2
|
|
101
|
+
last_two_month = today.month - 2
|
|
102
|
+
# 处理跨年情况
|
|
103
|
+
if last_two_month < 1:
|
|
104
|
+
last_two_month += 12
|
|
105
|
+
return last_two_month
|
|
106
|
+
|
|
97
107
|
# ==================== 日期范围计算 ====================
|
|
98
108
|
|
|
99
109
|
@staticmethod
|
|
@@ -285,6 +295,7 @@ class TimeUtils:
|
|
|
285
295
|
current_date += timedelta(days=1)
|
|
286
296
|
|
|
287
297
|
return date_list
|
|
298
|
+
|
|
288
299
|
# ==================== 月份相关 ====================
|
|
289
300
|
|
|
290
301
|
@staticmethod
|
|
@@ -299,6 +310,24 @@ class TimeUtils:
|
|
|
299
310
|
|
|
300
311
|
return first_day.strftime("%Y-%m-%d"), last_day.strftime("%Y-%m-%d")
|
|
301
312
|
|
|
313
|
+
@staticmethod
|
|
314
|
+
def get_last_two_month_range() -> Tuple[str, str]:
|
|
315
|
+
"""获取上上个月的第一天和最后一天"""
|
|
316
|
+
today = datetime.today()
|
|
317
|
+
# 计算上上个月
|
|
318
|
+
last_two_month = today.month - 2
|
|
319
|
+
year = today.year
|
|
320
|
+
|
|
321
|
+
# 处理跨年情况
|
|
322
|
+
if last_two_month < 1:
|
|
323
|
+
last_two_month += 12
|
|
324
|
+
year -= 1
|
|
325
|
+
|
|
326
|
+
first_day = datetime(year, last_two_month, 1)
|
|
327
|
+
last_day = datetime(year, last_two_month, calendar.monthrange(year, last_two_month)[1])
|
|
328
|
+
|
|
329
|
+
return first_day.strftime("%Y-%m-%d"), last_day.strftime("%Y-%m-%d")
|
|
330
|
+
|
|
302
331
|
@staticmethod
|
|
303
332
|
def get_last_month_range_time_str() -> Tuple[str, str]:
|
|
304
333
|
"""获取上个月第一天和最后一天的时间字符串"""
|
|
@@ -618,7 +647,6 @@ class TimeUtils:
|
|
|
618
647
|
|
|
619
648
|
return mtime_dt.strftime('%Y-%m-%d %H:%M:%S') if to_str else mtime_dt
|
|
620
649
|
|
|
621
|
-
|
|
622
650
|
# ==================== 便捷函数 ====================
|
|
623
651
|
# 为了保持向后兼容,提供一些便捷函数
|
|
624
652
|
|
|
@@ -626,197 +654,158 @@ def get_current_date() -> str:
|
|
|
626
654
|
"""获取当前日期,格式为 YYYYMMDD"""
|
|
627
655
|
return TimeUtils.get_current_date()
|
|
628
656
|
|
|
629
|
-
|
|
630
657
|
def get_current_datetime() -> str:
|
|
631
658
|
"""获取当前日期时间,格式为 YYYYMMDDHHMMSS"""
|
|
632
659
|
return TimeUtils.get_current_datetime()
|
|
633
660
|
|
|
634
|
-
|
|
635
661
|
def current_datetime() -> str:
|
|
636
662
|
"""获取当前日期时间,格式为 YYYY-MM-DD HH:MM:SS"""
|
|
637
663
|
return TimeUtils.current_datetime()
|
|
638
664
|
|
|
639
|
-
|
|
640
665
|
def today_date() -> str:
|
|
641
666
|
"""获取今天的日期,格式为 YYYY-MM-DD"""
|
|
642
667
|
return TimeUtils.today_date()
|
|
643
668
|
|
|
644
|
-
|
|
645
669
|
def today_date_hour() -> str:
|
|
646
670
|
"""获取今天的日期和小时,格式为 YYYY-MM-DD_HH"""
|
|
647
671
|
return TimeUtils.today_date_hour()
|
|
648
672
|
|
|
649
|
-
|
|
650
673
|
def get_yesterday(dt: Optional[str] = None) -> str:
|
|
651
674
|
"""获取昨天的日期"""
|
|
652
675
|
return TimeUtils.get_yesterday(dt)
|
|
653
676
|
|
|
654
|
-
|
|
655
677
|
def tomorrow_date() -> str:
|
|
656
678
|
"""获取明天的日期"""
|
|
657
679
|
return TimeUtils.tomorrow_date()
|
|
658
680
|
|
|
659
|
-
|
|
660
681
|
def before_yesterday() -> str:
|
|
661
682
|
"""获取前天的日期"""
|
|
662
683
|
return TimeUtils.before_yesterday()
|
|
663
684
|
|
|
664
|
-
|
|
665
685
|
def get_current_year() -> int:
|
|
666
686
|
"""获取当前年份"""
|
|
667
687
|
return TimeUtils.get_current_year()
|
|
668
688
|
|
|
669
|
-
|
|
670
689
|
def get_current_month() -> int:
|
|
671
690
|
"""获取当前月份"""
|
|
672
691
|
return TimeUtils.get_current_month()
|
|
673
692
|
|
|
674
|
-
|
|
675
693
|
def get_last_month() -> int:
|
|
676
694
|
"""获取上个月的月份"""
|
|
677
695
|
return TimeUtils.get_last_month()
|
|
678
696
|
|
|
679
|
-
|
|
680
697
|
def get_week_num() -> int:
|
|
681
698
|
"""获取当前是第几周"""
|
|
682
699
|
return TimeUtils.get_week_num()
|
|
683
700
|
|
|
684
|
-
|
|
685
701
|
def get_period() -> str:
|
|
686
702
|
"""获取当前时间段(上午/下午/晚上)"""
|
|
687
703
|
return TimeUtils.get_period()
|
|
688
704
|
|
|
689
|
-
|
|
690
705
|
def get_period2() -> str:
|
|
691
706
|
"""获取当前时间段(AM/PM)"""
|
|
692
707
|
return TimeUtils.get_period2()
|
|
693
708
|
|
|
694
|
-
|
|
695
709
|
def get_chinese_weekday(date_str: str) -> str:
|
|
696
710
|
"""根据输入的日期字符串返回中文星期几"""
|
|
697
711
|
return TimeUtils.get_chinese_weekday(date_str)
|
|
698
712
|
|
|
699
|
-
|
|
700
713
|
def get_weekday_name(date_str: str) -> str:
|
|
701
714
|
"""获取中文星期名称(简短格式)"""
|
|
702
715
|
return TimeUtils.get_weekday_name(date_str)
|
|
703
716
|
|
|
704
|
-
|
|
705
717
|
def is_in_month(time_str: str, month: int, fmt: str = "%Y-%m-%d") -> bool:
|
|
706
718
|
"""判断时间字符串是否在指定月份"""
|
|
707
719
|
return TimeUtils.is_in_month(time_str, month, fmt)
|
|
708
720
|
|
|
709
|
-
|
|
710
721
|
def date_trans(d_t: str) -> str:
|
|
711
722
|
"""无斜杠日期转成有斜杠日期"""
|
|
712
723
|
return TimeUtils.date_trans(d_t)
|
|
713
724
|
|
|
714
|
-
|
|
715
725
|
def is_date_greater_or_equal(date_str: str) -> bool:
|
|
716
726
|
"""比较指定日期是否大于或等于今天的日期"""
|
|
717
727
|
return TimeUtils.is_date_greater_or_equal(date_str)
|
|
718
728
|
|
|
719
|
-
|
|
720
729
|
def is_yesterday(create_time_str: str, dt: Optional[str] = None) -> bool:
|
|
721
730
|
"""判断给定的时间字符串是否是昨天"""
|
|
722
731
|
return TimeUtils.is_yesterday(create_time_str, dt)
|
|
723
732
|
|
|
724
|
-
|
|
725
733
|
def is_yesterday_date(date_str: str) -> bool:
|
|
726
734
|
"""判断给定的日期字符串是否是昨天"""
|
|
727
735
|
return TimeUtils.is_yesterday_date(date_str)
|
|
728
736
|
|
|
729
|
-
|
|
730
737
|
def get_file_mtime(file_path: str, to_str: bool = True, tz_offset: int = 8) -> Union[str, datetime]:
|
|
731
738
|
"""获取文件的修改时间"""
|
|
732
739
|
return TimeUtils.get_file_mtime(file_path, to_str, tz_offset)
|
|
733
740
|
|
|
734
|
-
|
|
735
741
|
def convert_timestamp_to_str(timestamp_ms: Optional[int]) -> str:
|
|
736
742
|
"""将毫秒时间戳转换为字符串"""
|
|
737
743
|
return TimeUtils.convert_timestamp_to_str(timestamp_ms)
|
|
738
744
|
|
|
739
|
-
|
|
740
745
|
def convert_timestamp_to_date(timestamp_ms: Union[int, float]) -> str:
|
|
741
746
|
"""将毫秒时间戳转换为日期字符串"""
|
|
742
747
|
return TimeUtils.convert_timestamp_to_date(timestamp_ms)
|
|
743
748
|
|
|
744
|
-
|
|
745
749
|
def convert_datetime_to_date(datetime_str: str) -> str:
|
|
746
750
|
"""将格式为 'YYYY-MM-DD HH:MM:SS' 的时间字符串转换为 'YYYY-MM-DD' 格式的日期字符串"""
|
|
747
751
|
return TimeUtils.convert_datetime_to_date(datetime_str)
|
|
748
752
|
|
|
749
|
-
|
|
750
753
|
def get_current_year_range() -> Tuple[str, str]:
|
|
751
754
|
"""获取当前年份的开始和结束日期"""
|
|
752
755
|
return TimeUtils.get_current_year_range()
|
|
753
756
|
|
|
754
|
-
|
|
755
757
|
def get_start_timestamps(date_str: str) -> int:
|
|
756
758
|
"""获取指定日期的开始毫秒时间戳(00:00:00.000)"""
|
|
757
759
|
return TimeUtils.get_start_timestamps(date_str)
|
|
758
760
|
|
|
759
|
-
|
|
760
761
|
def get_end_timestamps(date_str: str) -> int:
|
|
761
762
|
"""获取指定日期的结束毫秒时间戳(23:59:59.999)"""
|
|
762
763
|
return TimeUtils.get_end_timestamps(date_str)
|
|
763
764
|
|
|
764
|
-
|
|
765
765
|
def format_date_cross_platform(date_str: str) -> str:
|
|
766
766
|
"""跨平台格式化日期"""
|
|
767
767
|
return TimeUtils.format_date_cross_platform(date_str)
|
|
768
768
|
|
|
769
|
-
|
|
770
769
|
def get_past_7_days_range(start_from: Optional[str] = None) -> Tuple[str, str]:
|
|
771
770
|
"""获取过去7天的日期范围(包括结束日,共7天)"""
|
|
772
771
|
return TimeUtils.get_past_7_days_range(start_from)
|
|
773
772
|
|
|
774
|
-
|
|
775
773
|
def get_past_7_days_range_format(start_from: Optional[str] = None, format_str: str = '%Y-%m-%d') -> Tuple[str, str]:
|
|
776
774
|
"""获取过去7天的日期范围(包括结束日,共7天),支持自定义格式"""
|
|
777
775
|
return TimeUtils.get_past_7_days_range_format(start_from, format_str)
|
|
778
776
|
|
|
779
|
-
|
|
780
777
|
def get_past_nth_day(n: int, start_from: Optional[str] = None) -> str:
|
|
781
778
|
"""获取过去第n天的日期"""
|
|
782
779
|
return TimeUtils.get_past_nth_day(n, start_from)
|
|
783
780
|
|
|
784
|
-
|
|
785
781
|
def get_past_n_days_list(n: int, start_from: Optional[str] = None) -> List[str]:
|
|
786
782
|
"""获取过去n天的日期列表,从最旧到最近的日期"""
|
|
787
783
|
return TimeUtils.get_past_n_days_list(n, start_from)
|
|
788
784
|
|
|
789
|
-
|
|
790
785
|
def get_past_7_days_list(start_from: Optional[str] = None) -> List[str]:
|
|
791
786
|
"""获取过去7天的日期列表(不包含 start_from 当天),共7天"""
|
|
792
787
|
return TimeUtils.get_past_7_days_list(start_from)
|
|
793
788
|
|
|
794
|
-
|
|
795
789
|
def date_range(start_date: str, end_date: str) -> List[str]:
|
|
796
790
|
"""生成两个日期之间的日期列表"""
|
|
797
791
|
return TimeUtils.date_range(start_date, end_date)
|
|
798
792
|
|
|
799
|
-
|
|
800
793
|
def get_dates_from_first_of_month_to_yesterday() -> List[str]:
|
|
801
794
|
"""获取从本月第一天到昨天的日期列表"""
|
|
802
795
|
return TimeUtils.get_dates_from_first_of_month_to_yesterday()
|
|
803
796
|
|
|
804
|
-
|
|
805
797
|
def get_last_month_range() -> Tuple[str, str]:
|
|
806
798
|
"""获取上个月的第一天和最后一天"""
|
|
807
799
|
return TimeUtils.get_last_month_range()
|
|
808
800
|
|
|
809
|
-
|
|
810
801
|
def get_last_month_range_time_str() -> Tuple[str, str]:
|
|
811
802
|
"""获取上个月第一天和最后一天的时间字符串"""
|
|
812
803
|
return TimeUtils.get_last_month_range_time_str()
|
|
813
804
|
|
|
814
|
-
|
|
815
805
|
def get_last_month_range_time() -> Tuple[int, int]:
|
|
816
806
|
"""获取上个月第一天和最后一天的毫秒级时间戳"""
|
|
817
807
|
return TimeUtils.get_last_month_range_time()
|
|
818
808
|
|
|
819
|
-
|
|
820
809
|
# 为了向后兼容,保留一些旧函数名
|
|
821
810
|
def get_past_7_days_range_old() -> Tuple[str, str]:
|
|
822
811
|
"""获取过去7天的日期范围(旧版本)"""
|
|
@@ -824,7 +813,6 @@ def get_past_7_days_range_old() -> Tuple[str, str]:
|
|
|
824
813
|
start_date = end_date - timedelta(days=6) # 往前推6天为开始日期
|
|
825
814
|
return start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')
|
|
826
815
|
|
|
827
|
-
|
|
828
816
|
def get_past_7_days_list_old() -> List[str]:
|
|
829
817
|
"""获取过去7天的日期列表(旧版本)"""
|
|
830
818
|
today = datetime.today()
|
|
@@ -833,7 +821,6 @@ def get_past_7_days_list_old() -> List[str]:
|
|
|
833
821
|
for i in range(1, 8)
|
|
834
822
|
]
|
|
835
823
|
|
|
836
|
-
|
|
837
824
|
if __name__ == "__main__":
|
|
838
825
|
# 测试示例
|
|
839
826
|
print(f"当前日期: {today_date()}")
|
|
@@ -5,26 +5,26 @@ qrpa/feishu_bot_app.py,sha256=6r2YqCAMUN7y3F7onoABRmHC7S-UPOLHwbhsQnQ3IRc,9452
|
|
|
5
5
|
qrpa/feishu_client.py,sha256=gXvyhf7r-IqeDhPjM01SfsGf17t8g1ZwUAkhymBkBeg,17544
|
|
6
6
|
qrpa/feishu_logic.py,sha256=yuwb-LeZiHKGlz-W8JobinorHonVa8L-5h12WxnU7_Q,67508
|
|
7
7
|
qrpa/fun_base.py,sha256=lMzqPwsbVfe968CUR2MVNImzIskIUZqPCE2tWxYqb5o,10728
|
|
8
|
-
qrpa/fun_excel.py,sha256=
|
|
8
|
+
qrpa/fun_excel.py,sha256=58Fjr1bX1KvRIm0NwaXMDq8yIcjBv4c8e5Zar4Mxaaw,117592
|
|
9
9
|
qrpa/fun_file.py,sha256=yzjDV16WL5vRys7J4uQcNzIFkX4D5MAlSCwxcD-mwQo,11966
|
|
10
|
-
qrpa/fun_web.py,sha256=
|
|
10
|
+
qrpa/fun_web.py,sha256=2aYrtRlf-p-P6AyjrlIcmMVq8oSDePEY48YLm4ZRS1A,11857
|
|
11
11
|
qrpa/fun_win.py,sha256=-LnTeocdTt72NVH6VgLdpAT9_C5oV9okeudXG6CftMA,8034
|
|
12
12
|
qrpa/shein_daily_report_model.py,sha256=H8oZmIN5Pyqe306W1_xuz87lOqLQ_LI5RjXbaxDkIzE,12589
|
|
13
|
-
qrpa/shein_excel.py,sha256
|
|
14
|
-
qrpa/shein_lib.py,sha256=
|
|
13
|
+
qrpa/shein_excel.py,sha256=QMgXHELu7-zR9CujN1W66qj8S3tDVJPfToW9i90_WaI,137668
|
|
14
|
+
qrpa/shein_lib.py,sha256=kTA1CNITz99VnC3EqpHHkOk4B7dVdT1BDkUHjx3WrqI,136643
|
|
15
15
|
qrpa/shein_mysql.py,sha256=Sgz6U0_3f4cT5zPf1Ht1OjvSFhrVPLkMxt91NV-ZPCM,3005
|
|
16
16
|
qrpa/shein_sqlite.py,sha256=ZQwD0Gz81q9WY7tY2HMEYvSF9r3N_G_Aur3bYfST9WY,5707
|
|
17
|
-
qrpa/shein_ziniao.py,sha256=
|
|
17
|
+
qrpa/shein_ziniao.py,sha256=X7BupME0y80-MGMTbJ4C9EG2tGDJuVIymFV1MR61m_c,21339
|
|
18
18
|
qrpa/temu_chrome.py,sha256=CbtFy1QPan9xJdJcNZj-EsVGhUvv3ZTEPVDEA4-im40,2803
|
|
19
19
|
qrpa/temu_excel.py,sha256=2hGw76YWzkTZGyFCuuUAab4oHptYX9a6U6yjpNsL7FE,6990
|
|
20
20
|
qrpa/temu_lib.py,sha256=hYB59zsLS3m3NTic_duTwPMOTSxlHyQVa8OhHnHm-1g,7199
|
|
21
|
-
qrpa/time_utils.py,sha256=
|
|
21
|
+
qrpa/time_utils.py,sha256=BMaR5gUl9K2z1qOpKgwrK7a3_NQ5dqcRQCbxn0mXusY,30971
|
|
22
22
|
qrpa/time_utils_example.py,sha256=shHOXKKF3QSzb0SHsNc34M61wEkkLuM30U9X1THKNS8,8053
|
|
23
23
|
qrpa/wxwork.py,sha256=gIytG19DZ5g7Tsl0-W3EbjfSnpIqZw-ua24gcB78YEg,11264
|
|
24
24
|
qrpa/mysql_module/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
qrpa/mysql_module/shein_product_model.py,sha256=v21_juWsZqTHj2k08GUGNkoRZ4hkKZKjaE-wmRl2a2k,19257
|
|
26
26
|
qrpa/mysql_module/shein_return_order_model.py,sha256=Zt-bGOH_kCDbakW7uaTmqqo_qTT8v424yidcYSfWvWM,26562
|
|
27
|
-
qrpa-1.0.
|
|
28
|
-
qrpa-1.0.
|
|
29
|
-
qrpa-1.0.
|
|
30
|
-
qrpa-1.0.
|
|
27
|
+
qrpa-1.0.97.dist-info/METADATA,sha256=aptoNJ0O4Cl_wzSgwydE3RD11yfHm-_NRi9sdZWJs1g,231
|
|
28
|
+
qrpa-1.0.97.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
qrpa-1.0.97.dist-info/top_level.txt,sha256=F6T5igi0fhXDucPPUbmmSC0qFCDEsH5eVijfVF48OFU,5
|
|
30
|
+
qrpa-1.0.97.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|