From 3ca569ed1e4f5024bbcdf8d31eb31c479bb536ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=AD=E5=9D=A4?= <10372463+ming-kun@user.noreply.gitee.com> Date: Mon, 4 May 2026 19:15:50 +0800 Subject: [PATCH] new file: amazon/amazon_base.py modified: amazon/approve.py modified: amazon/asin_status.py new file: amazon/chrome_base.py modified: amazon/del_brand.py modified: amazon/detail_spider.py new file: "amazon/detail_spider_\345\244\207\344\273\275.py" modified: amazon/main.py modified: amazon/match_action.py modified: amazon/price_match.py new file: amazon/similar_asin.py modified: app.py new file: assets/appearance-patent-Br1dtmol.css new file: assets/appearance-patent-D1DgeYhe.css new file: assets/delete-brand-BfMLVSQU.css new file: assets/patrol-delete-BAzbYtgc.css new file: assets/price-track-Hozgt_em.css new file: assets/product-risk-CbX7bwZi.css new file: assets/query-asin-B4RsOiza.css new file: assets/shop-match-B2HWAgBY.css modified: main.py --- app/amazon/amazon_base.py | 847 +++ app/amazon/approve.py | 866 +-- app/amazon/asin_status.py | 174 +- app/amazon/chrome_base.py | 189 + app/amazon/del_brand.py | 994 ++-- app/amazon/detail_spider.py | 298 +- app/amazon/detail_spider_备份.py | 8384 ++++++++++++++++++++++++++++++ app/amazon/main.py | 595 +-- app/amazon/match_action.py | 549 +- app/amazon/price_match.py | 573 +- app/amazon/similar_asin.py | 505 ++ app/app.py | 53 +- app/main.py | 16 +- 13 files changed, 11073 insertions(+), 2970 deletions(-) create mode 100644 app/amazon/amazon_base.py create mode 100644 app/amazon/chrome_base.py create mode 100644 app/amazon/detail_spider_备份.py create mode 100644 app/amazon/similar_asin.py diff --git a/app/amazon/amazon_base.py b/app/amazon/amazon_base.py new file mode 100644 index 0000000..43ceaba --- /dev/null +++ b/app/amazon/amazon_base.py @@ -0,0 +1,847 @@ +import traceback +import winreg +import subprocess +import time +import uuid +import requests +import json +import os +import re +from datetime import datetime + +from DrissionPage import Chromium,ChromiumOptions +from DrissionPage.common import By +from DrissionPage._pages.chromium_tab import ChromiumTab +from typing import Literal,Dict,Any,TypeVar,Type + + +from amazon.tool import get_shop_info,show_notification +from config import runing_shop + +# 定义类型变量,用于泛型类型注解 +T = TypeVar('T', bound='AmamzonBase') + + +def kill_process(version: Literal["v5", "v6"]): + """杀紫鸟客户端进程(独立函数版本)""" + driver = ZiniaoDriver({}) + driver.kill_process(version) + + +class ZiniaoDriver: + """紫鸟浏览器自动化驱动类""" + + def __init__(self, user_info: dict, socket_port: int = 19890): + """ + 初始化紫鸟浏览器驱动 + + Args: + user_info: 用户信息字典,包含 company, username, password + socket_port: 客户端通信端口,默认 19890 + """ + self.user_info = user_info + self.socket_port = socket_port + self.client_path = None + self.browser = None + self.tab: ChromiumTab = None + self.store_id = None + + self.url = None #用于记录当前链接,实现断点续传 + + + + def get_zinaio_exe(self, protocol_name: str = "superbrowser"): + """ + 获取紫鸟安装目录 + + Args: + protocol_name: 协议名称,默认 "superbrowser" + + Returns: + exe_path: 紫鸟浏览器可执行文件路径 + """ + try: + key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command" + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) + command, _ = winreg.QueryValueEx(key, "") + winreg.CloseKey(key) + if isinstance(command, str): + sub = "ziniao.exe" + exe_path = command[0: command.find(sub) + len(sub) + 1] + else: + exe_path = command[0] + return exe_path + except FileNotFoundError: + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) + command, _ = winreg.QueryValueEx(key, "") + winreg.CloseKey(key) + if isinstance(command, str): + sub = "ziniao.exe" + exe_path = command[0: command.find(sub) + len(sub) + 1] + else: + exe_path = command[0] + return exe_path + except FileNotFoundError: + return None + + def update_core(self): + """ + 下载所有内核,打开店铺前调用,需客户端版本5.285.7以上 + 因为http有超时时间,所以这个action适合循环调用,直到返回成功 + """ + data = { + "action": "updateCore", + "requestId": str(uuid.uuid4()), + } + data.update(self.user_info) + while True: + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) + result = response.json() + print(result) + if result is None: + print("等待客户端启动...") + time.sleep(2) + continue + if result.get("statusCode") is None or result.get("statusCode") == -10003: + print("当前版本不支持此接口,请升级客户端") + return + elif result.get("statusCode") == 0: + print("更新内核完成") + return + else: + print(f"等待更新内核: {json.dumps(result)}") + time.sleep(2) + + def kill_process(self, version: Literal["v5", "v6"]): + """ + 杀紫鸟客户端进程 + + Args: + version: 客户端版本 + """ + if version == "v5": + process_name = 'SuperBrowser.exe' + os.system('taskkill /f /t /im ' + "starter.exe") + else: + process_name = 'ziniao.exe' + os.system('taskkill /f /t /im ' + process_name) + time.sleep(3) + + def get_browser_list(self) -> list: + """ + 获取浏览器列表 + + Returns: + list: 浏览器列表 + """ + request_id = str(uuid.uuid4()) + data = { + "action": "getBrowserList", + "requestId": request_id + } + data.update(self.user_info) + + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) + r = response.json() + if str(r.get("statusCode")) == "0": + print(r) + return r.get("browserList") + elif str(r.get("statusCode")) == "-10003": + print(f"【get_browser_list】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() + else: + print(f"【get_browser_list】失败 {json.dumps(r, ensure_ascii=False)} ") + # exit() + + def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0, + isHeadless=0, cookieTypeSave=0, jsInfo=""): + """ + 打开店铺 + + Args: + store_info: 店铺信息(browserId 或 browserOauth) + isWebDriverReadOnlyMode: 是否只读模式,默认 0 + isprivacy: 隐私模式,默认 0 + isHeadless: 无头模式,默认 0 + cookieTypeSave: cookie保存类型,默认 0 + jsInfo: 注入的JS信息,默认 "" + + Returns: + dict: 返回结果 + """ + request_id = str(uuid.uuid4()) + data = { + "action": "startBrowser", + "isWaitPluginUpdate": 0, + "isHeadless": isHeadless, + "requestId": request_id, + "isWebDriverReadOnlyMode": isWebDriverReadOnlyMode, + "cookieTypeLoad": 0, + "cookieTypeSave": cookieTypeSave, + "runMode": "1", + "isLoadUserPlugin": True, + "pluginIdType": 1, + "privacyMode": isprivacy + } + data.update(self.user_info) + + if store_info.isdigit(): + data["browserId"] = store_info + else: + data["browserOauth"] = store_info + + if len(str(jsInfo)) > 2: + data["injectJsInfo"] = json.dumps(jsInfo) + + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) + r = response.json() + if str(r.get("statusCode")) == "0": + return r + elif str(r.get("statusCode")) == "-10003": + raise RuntimeError(f"【open_store】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() + else: + raise RuntimeError(f"【open_store】失败 {json.dumps(r, ensure_ascii=False)} ") + # exit() + + def get_browser(self, port) -> Chromium: + """ + 获取浏览器实例 + + Args: + port: 调试端口 + + Returns: + Chromium: DrissionPage浏览器实例 + """ + co = ChromiumOptions() + # 设置不加载图片、静音 + co.set_local_port(port) + co.no_imgs(True).mute(True) + browser = Chromium(co) + return browser + + def start_client(self): + """启动紫鸟客户端""" + + # 检查端口是否已经启动 + try: + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.get(url, timeout=2) + print(f"端口 {self.socket_port} 已经启动,跳过启动客户端操作") + return + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + # 端口未启动,继续执行启动操作 + print(f"端口 {self.socket_port} 未启动,开始启动客户端") + + self.kill_process('v6') + time.sleep(5) + self.client_path = self.get_zinaio_exe("superbrowserv6").strip('"') + print(self.client_path) + cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http', + '--port=' + str(self.socket_port)] + print(" ".join(cmd)) + + # 最大重试次数 + max_retries = 3 + + for retry_count in range(max_retries): + print(f"第 {retry_count + 1} 次尝试启动客户端...") + + # 启动进程 + subprocess.Popen(cmd) + + # 循环检测10秒,每0.5秒检测一次 + start_check_time = time.time() + client_started = False + + while time.time() - start_check_time < 10: + try: + response = requests.get(url, timeout=2) + print(f"客户端启动成功!(第 {retry_count + 1} 次尝试)") + client_started = True + break + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + # 端口还未启动,继续等待 + time.sleep(0.5) + + if client_started: + time.sleep(5) # 等待客户端完全启动 + # 更新内核 + self.update_core() + return + else: + print(f"第 {retry_count + 1} 次尝试启动失败,10秒内未检测到客户端启动") + + # 超过最大重试次数,抛出异常 + raise RuntimeError(f"客户端启动失败:重试 {max_retries} 次后仍未成功启动") + + def open_shop(self, shop_name: str): + """ + 打开指定店铺 + + Args: + shop_name: 店铺名称 + + Returns: + Chromium or str: 成功返回浏览器实例,失败返回错误信息 + """ + print("=============打开指定店铺================") + # 启动客户端 + self.start_client() + + # 获取店铺列表 + shop_ls = self.get_browser_list() + self.store_id = None + print(shop_ls) + for shop in shop_ls: + if shop.get("browserName") == shop_name: + self.store_id = shop.get('browserOauth') + break + + if not self.store_id: + print("店铺不存在") + return "店铺不存在" + + # 打开店铺 + ret_json = self.open_store(self.store_id) + print(ret_json) + self.store_id = ret_json.get("browserOauth") + if self.store_id is None: + self.store_id = ret_json.get("browserId") + + # 获取drissionpage浏览器会话 + self.browser = self.get_browser(ret_json.get('debuggingPort')) + + ip_check_url = ret_json.get("ipDetectionPage") + if not ip_check_url: + print("ip检测页地址为空,请升级紫鸟浏览器到最新版") + print(f"=====关闭店铺:{shop_name}=====") + self.close_store(self.store_id) + # exit() + raise RuntimeError("没有IP检测地址,为了店铺安全不打开店铺") + ip_usable = self.open_ip_check(self.browser, ip_check_url) + if ip_usable: + print("ip检测通过,打开店铺平台主页") + self.open_launcher_page(ret_json.get("launcherPage"), self.browser) + else: + print("IP检测不通过") + raise RuntimeError("IP检测不通过,可能是因为网络环境变化导致的,为了店铺安全不打开店铺") + return self.browser + + def close_store(self, browser_oauth=None): + """ + 关闭店铺 + + Args: + browser_oauth: 店铺OAuth标识,如果不提供则使用当前打开的店铺 + + Returns: + dict: 返回结果 + """ + if browser_oauth is None: + browser_oauth = self.store_id + + request_id = str(uuid.uuid4()) + data = { + "action": "stopBrowser", + "requestId": request_id, + "duplicate": 0, + "browserOauth": browser_oauth + } + data.update(self.user_info) + + url = f'http://127.0.0.1:{self.socket_port}' + response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) + r = response.json() + if str(r.get("statusCode")) == "0": + return r + elif str(r.get("statusCode")) == "-10003": + raise RuntimeError(f"【close_store】登录失败 {json.dumps(r, ensure_ascii=False)}") + # exit() + else: + raise RuntimeError(f"【close_store】失败: {json.dumps(r, ensure_ascii=False)} ") + # exit() + + def open_launcher_page(self, launcher_page: str, browser: Chromium = None): + """ + 打开启动页面 + + Args: + launcher_page: 要打开的页面URL + browser: 浏览器实例,如果不提供则使用当前浏览器实例 + """ + if browser is None: + browser = self.browser + + tab = browser.new_tab(url=launcher_page) + self.tab = tab + return tab + + def open_ip_check(self, browser: Chromium, ip_check_url: str): + """ + 打开ip检测页检测ip是否正常 + :param browser: drissionpage浏览器会话 + :param ip_check_url ip检测页地址 + :return 检测结果 + """ + try: + tab = browser.latest_tab + tab.get(ip_check_url) + success_button = tab.ele((By.XPATH, '//button[contains(@class, "styles_btn--success")]'), + timeout=60) # 等待查找元素60秒 + if success_button: + print("ip检测成功") + return True + else: + print("ip检测超时") + return False + except Exception as e: + print("ip检测异常:" + traceback.format_exc()) + return False + + +class AmamzonBase(ZiniaoDriver): + """亚马逊操作基类,包含一些通用方法""" + mark_name = "操作基类" + + def log(self, message: str, level: str = "INFO"): + """日志输出 + Args: + message: 日志消息 + level: 日志级别 + """ + try: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if level == "ERROR": + show_notification(message, "error") + print(f"[{timestamp}] [{self.mark_name}] [{level}] {message}") + except Exception as e: + print(f"输出出错,{e}") + + + def SwitchingCountries(self, country_name: str): + """ + 切换国家 + 操作: + 1、//div[@class="dropdown-account-switcher-header-label"]/span[last()] 获取此元素文本,判断当前国家,如果与目标国家相同则不操作,否则执行下一步 + 2、点击 //div[@class="dropdown-account-switcher-header-label"] 打开下拉框 + 3、点击 //div[@class="dropdown-account-switcher-list-item"] 第一个展开国家列表 + 4、点击 //div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="国家名"] 切换到目标国家 + 5、等待页面加载完成,判断国家是否切换成功,成功则返回True,否则返回False + + Args: + country_name: 目标国家名称 + + Returns: + bool: 切换成功返回True,失败返回False + """ + try: + print("=============切换国家================") + if self.browser is None: + print("浏览器实例不存在,请先打开店铺") + return False + + # 获取当前标签页 + tab = self.tab + tab.wait.doc_loaded(timeout=120, raise_err=False) + + # 步骤1:获取当前国家名称 + self.log(f"正在检查当前国家...") + current_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]', + timeout=20) + if current_country_ele: + current_country = current_country_ele.text.strip() + self.log(f"当前国家:{current_country}") + + # 判断是否与目标国家相同 + if current_country == country_name: + self.log(f"当前已经是目标国家 {country_name},无需切换") + return True + else: + self.log("无法获取当前国家信息") + return False + + # 步骤2:点击打开下拉框 + self.log(f"正在打开国家切换下拉框...") + dropdown_header = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]', timeout=10) + if not dropdown_header: + self.log("找不到国家切换下拉框") + return False + dropdown_header.click() + time.sleep(1) # 等待下拉框展开 + + # 步骤3:点击第一个展开国家列表 + self.log(f"正在展开国家列表...") + first_item = tab.ele('xpath://div[@class="dropdown-account-switcher-list-item"]', timeout=10) + if not first_item: + self.log("找不到国家列表项") + return False + first_item.click() + time.sleep(1) # 等待国家列表展开 + + # 步骤4:点击目标国家 + self.log(f"正在切换到国家:{country_name}") + target_country_xpath = f'//div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="{country_name}"]' + target_country = tab.ele(f'xpath:{target_country_xpath}', timeout=10) + if not target_country: + self.log(f"找不到目标国家:{country_name}") + return False + target_country.click() + + # 步骤5:等待页面加载完成并验证切换结果 + self.log(f"等待页面加载...") + # time.sleep(3) # 等待页面加载 + self.tab.wait.doc_loaded() + + # 再次检查当前国家 + new_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]', + timeout=10) + if new_country_ele: + new_country = new_country_ele.text.strip() + if new_country == country_name: + self.log(f"国家切换成功:{new_country}") + return True + else: + self.log(f"国家切换失败,当前国家:{new_country},目标国家:{country_name}") + return False + else: + self.log("无法验证切换结果") + return False + + except Exception as e: + self.log(f"切换国家时发生异常:{traceback.format_exc()}") + return False + + def need_login(self): + """ + 判断是否需要登录,部分国家可能需要登录后才能切换国家 + 处理流程: + + """ + time.sleep(3) # 等待页面可能的登录元素加载,避免跳转等等 + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + need_login_ele = self.tab.eles('xpath://h1[@class="a-spacing-small"]|//span[contains(text(),"登录")]', timeout=5) + if len(need_login_ele) > 0: + self.log("检测到需要登录元素") + return True + return False + + def login(self, password, username=""): + try: + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + for _ in range(4): + switch_account = self.tab.eles( + 'xpath://div[@data-test-id="switchableAccounts"]//div[@class="a-fixed-left-grid"]', timeout=5) + if len(switch_account) > 0: + switch_account[0].click() + time.sleep(1) + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + + pwd_input = self.tab.eles('xpath://input[@type="password"]', timeout=5) + if len(pwd_input) > 0: + pwd_input[0].input(password, clear=True) + submit_btn = self.tab.eles('xpath://input[@id="signInSubmit"]', timeout=5) + if len(submit_btn) > 0: + submit_btn[0].click() + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + + send_code = self.tab.eles('xpath://span[@id="auth-send-code" and contains(string(.),"发送一次性密码")]', + timeout=5) + self.log(f"发送一次性密码:{len(send_code)}") + if len(send_code) > 0: + self.log("检测到 发送一次性密码") + send_code[0].click() + time.sleep(1) + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + else: + for j in range(5): + opt_code_input = self.tab.eles('xpath://input[@name="otpCode"]', timeout=30) + self.log(f"验证码输入框:{len(opt_code_input)}") + if len(opt_code_input) > 0: + opt_code_input[0].wait.displayed(timeout=10, raise_err=False) + for _ in range(30): + if opt_code_input[0].value is not None and opt_code_input[0].value.strip() != "": + print("检测到验证码输入完成") + submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]', timeout=10) + if len(submit_btn) > 0: + submit_btn[0].click() + self.tab.wait.doc_loaded(timeout=20, raise_err=False) + # return True + error_mes = self.tab.eles('xpath://div[@id="auth-error-message-box"]', + timeout=10) + if len(error_mes) > 0: + self.log("验证码输入错误") + self.log(f"验证码输入错误提示:{error_mes[0].text}") + self.tab.refresh() + else: + self.log("登入成功") + return True + + time.sleep(1) + + else: + break + + submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]', timeout=10) + if len(submit_btn) > 0: + submit_btn[0].click() + except Exception as e: + self.log(f"登录过程中发生异常:{traceback.format_exc()}") + return False + + def SwitchPage(self): + """ + 切换至 管理所有库存页面 + 1、等待 //navigation-favorites-bar[@class="hydrated"] 出现 + """ + navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]') + navigation.wait.displayed(raise_err=False) + page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr( + 'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]') + page_btn.wait.displayed(raise_err=False) + page_btn.click(timeout=5) + + self.tab.wait.doc_loaded() + # 等待搜索框出现 + search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') + search_region.wait.displayed(raise_err=False, timeout=60) + + def search(self,filter_type="ApprovalRequired"): + sku_ls = [] + + load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5) + if len(load_ele) > 0: + load_ele[0].wait.deleted(timeout=3, raise_err=False) + time.sleep(0.5) + + drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown') + drop_down.wait.displayed(raise_err=False) + drop_down.wait.enabled(raise_err=False) + time.sleep(0.6) + drop_down.click() + # //kat-option[@value="SearchSuppressed"] + xp = f'xpath://kat-option[@value="{filter_type}"]' + self.log(f"正在寻找筛选条件 {filter_type},xpath: {xp}") + approval_required = self.tab.eles(xp,timeout=5) + if len(approval_required) == 0: + self.log(f"没有需要{filter_type}选项】没有需要{filter_type}的商品了") + return sku_ls # "没有需要审批的商品了" + else: + approval_required = approval_required[0] + approval_required.wait.displayed(raise_err=False) + approval_required.click() + + approval_required_text = approval_required.text + self.log(f"已选择筛选条件: {approval_required_text}") + + count = re.findall(r'\d+', approval_required_text) + if count: + count = int(count[0]) + self.log(f"待审批的商品数量: {count}") + if count <= 0: + self.log(f"没有需要{filter_type}的商品了") + return sku_ls #"没有需要审批的商品了" + for _ in range(3): + # 等待加载完成 + load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]") + if len(load_ele) > 0: + load_ele[0].wait.deleted(timeout=3, raise_err=False) + time.sleep(0.5) + + sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3) + if len(sku_ls) > 0: + break + approval_required.click() + return sku_ls + + +class TaskBase: + task_name = "任务基类" + + country_info = { + "DE": "德国", + "FR": "法国", + "ES": "西班牙", + "IT": "意大利", + "UK": "英国" + } + + def __init__(self, user_info: dict = None): + """初始化审批任务处理器 + + Args: + user_info: 用户信息字典,包含 company, username, password + """ + self.user_info = user_info or {} + self.running = True + + + def log(self, message: str, level: str = "INFO"): + """日志输出 + Args: + message: 日志消息 + level: 日志级别 + """ + try: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if level == "ERROR": + show_notification(message, "error") + print(f"[{timestamp}] [{self.task_name}] [{level}] {message}") + except Exception as e: + print(f"输出出错,{e}") + + def process_task(self, task_data: dict): + pass + + + def open_shop(self, cls: Type[T], max_retries: int, company_name: str, shop_name: str, iskill: bool = False) -> T: + error_info = "" + driver = None + for retry in range(max_retries): + try: + self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") + + if iskill: + self.log("重试前先杀掉浏览器进程...") + kill_process("v6") + kill_process("v5") + time.sleep(2) + + # 组装用户信息并创建驱动 + user_info = { + **self.user_info, + "company": company_name + } + driver = cls(user_info) + browser = driver.open_shop(shop_name) + + if browser and browser != "店铺不存在": + self.log(f"成功打开店铺 {shop_name}") + else: + self.log(f"打开店铺失败: {browser}", "WARNING") + driver = None + continue + + # 判断是否需要登录 + need_login = driver.need_login() + print("【是否需要登录】:", need_login) + if need_login: + self.log(f"店铺 {shop_name} 需要登录,正在登录...") + # 获取店铺凭证 + response = get_shop_info(shop_name) + print("【获取店铺凭证返回】:", response.text) + shop_data = response.json() + if not shop_data: + mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" + self.log(mes, "ERROR") + show_notification(mes, "ERROR") + continue + + password = shop_data["data"]["password"] + + login_success = driver.login(password) + if login_success: + self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") + browser = driver.open_shop(shop_name) + if browser and browser != "店铺不存在": + self.log(f"成功打开店铺 {shop_name} 登录后") + break + else: + self.log(f"登录后打开店铺失败: {browser}", "WARNING") + driver = None + else: + self.log(f"店铺 {shop_name} 登录失败", "WARNING") + driver = None + else: + break + + except Exception as e: + import traceback + self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO") + driver = None + error_info = str(e) + time.sleep(10) + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(3) + + # 检查是否成功打开 + if not driver or not browser or browser == "店铺不存在": + error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}" + self.log(error_msg, "ERROR") + # 从执行列表中移除 + if shop_name in runing_shop: + del runing_shop[shop_name] + return driver + return driver + + + def process_shop(self, shop_data: Dict[str, Any], task_id: int): + pass + + def action_init(self, driver, country_name, shop_name, risk_listing_filter=None): + """切换到指定国际 -> 页面 -》 筛选好""" + max_retries = 5 + switch_success = False + switch_success_pg = False + search_success = False + sku_ls = [] + for retry in range(max_retries): + try: + self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") + if retry > 2: + # 刷新不行就重新打开店铺 + self.log("重试前重新打开店铺...") + try: + driver.close_store() + time.sleep(3) + driver.open_shop(shop_name) + except Exception as e: + self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") + # 如果不是第一次尝试,先刷新页面 + if retry > 0: + self.log("重试前刷新页面...") + try: + driver.tab.refresh() + time.sleep(3) + except Exception as e: + self.log(f"刷新页面失败: {str(e)}", "WARNING") + + switch_success = driver.SwitchingCountries(country_name) + if switch_success: + self.log(f"成功切换到国家 {country_name}") + + # 切换到库存管理页面 + driver.SwitchPage() + self.log(f"已切换到库存管理页面") + time.sleep(3) + switch_success_pg = True + + if risk_listing_filter is None: + self.log(f"risk_listing_filter 为空,不筛选, {risk_listing_filter}") + search_success = True + break + + sku_ls = driver.search(filter_type=risk_listing_filter) + search_success = True + self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...") + break + else: + self.log(f"切换到国家 {country_name} 失败", "WARNING") + + except Exception as e: + import traceback + self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + return switch_success, switch_success_pg, search_success, sku_ls diff --git a/app/amazon/approve.py b/app/amazon/approve.py index b18d407..390ebf4 100644 --- a/app/amazon/approve.py +++ b/app/amazon/approve.py @@ -1,88 +1,27 @@ import time import re import traceback +import requests + from DrissionPage._pages.chromium_tab import ChromiumTab -import pyautogui -from config import runing_task, runing_shop +from config import runing_task, runing_shop,DELETE_BRAND_API_BASE + from datetime import datetime -from amazon.del_brand import AmamzonBase, kill_process -from amazon.tool import get_shop_info,show_notification - -# 导入 webview 用于前端通知 -try: - import webview -except ImportError: - webview = None +from amazon.amazon_base import AmamzonBase,kill_process,TaskBase +from amazon.tool import show_notification class AmzoneApprove(AmamzonBase): mark_name = "产品风险审批" - def SwitchPage(self): - """ - 切换至 管理所有库存页面 - 1、等待 //navigation-favorites-bar[@class="hydrated"] 出现 - """ - navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]') - navigation.wait.displayed(raise_err=False) - page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr( - 'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]') - page_btn.wait.displayed(raise_err=False) - page_btn.click(timeout=5) + def __init__(self, user_info: dict, socket_port: int = 19890): + super().__init__(user_info,socket_port) + self.already_asin = set() - self.tab.wait.doc_loaded() - # 等待搜索框出现 - search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') - search_region.wait.displayed(raise_err=False) - - def search(self,filter_type="ApprovalRequired"): - sku_ls = [] - - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5) - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown') - drop_down.wait.displayed(raise_err=False) - drop_down.wait.enabled(raise_err=False) - time.sleep(0.6) - drop_down.click() - # //kat-option[@value="SearchSuppressed"] - xp = f'xpath://kat-option[@value="{filter_type}"]' - print(f"正在寻找筛选条件 {filter_type},xpath: {xp}") - approval_required = self.tab.eles(xp,timeout=5) - if len(approval_required) == 0: - print(f"【没有需要{filter_type}选项】没有需要{filter_type}的商品了") - return sku_ls # "没有需要审批的商品了" - else: - approval_required = approval_required[0] - approval_required.wait.displayed(raise_err=False) - approval_required.click() - - approval_required_text = approval_required.text - print(f"已选择筛选条件: {approval_required_text}") - - count = re.findall(r'\d+', approval_required_text) - if count: - count = int(count[0]) - print(f"待审批的商品数量: {count}") - if count <= 0: - print(f"没有需要{filter_type}的商品了") - return sku_ls #"没有需要审批的商品了" - for _ in range(3): - # 等待加载完成 - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]") - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3) - if len(sku_ls) > 0: - break - approval_required.click() - return sku_ls + def reset_already_asin(self): + self.already_asin = set() + self.log("already_asin 已重置") def wait_loaded(self): # 等待加载完成 @@ -108,104 +47,6 @@ class AmzoneApprove(AmamzonBase): print("当前操作的tab_id",self.tab.tab_id) self.browser.close_tabs(close_tab) - # def handle_repair_product_old(self,tab:ChromiumTab): - # def wait_loaded(): - # # 等待加载完成 - # try: - # load_ele = tab.ele( - # 'xpath://div[@class="contentWrapper"]//div[contains(@class,"==")]/span[not(node())]', - # timeout=3) - # load_ele.wait.deleted(timeout=5, raise_err=False) - # except Exception as e: - # print("等待加载中消失出错", e) - - # tab.wait.doc_loaded(timeout=60,raise_err=False) - # submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10) - # if len(submit_btn_ls) == 0: - # print("未找到提交按钮,可能页面未加载完成或者页面结构发生变化") - # # return - # for _ in range(3): - # try: - # # todo 遍历 - # for index in range(len(submit_btn_ls)): - # if index > len(submit_btn_ls)-1: - # print("提交按钮数量发生变化,停止处理") - # break - - # submit_btn = submit_btn_ls[index] - - # # 关闭按钮 - # close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5) - # if len(close_x_btn) > 0: - # close_x_btn[0].wait.displayed(timeout=5, raise_err=False) - # close_x_btn[0].click() - - # submit_btn.wait.enabled(timeout=5, raise_err=False) - # submit_btn.click() - # time.sleep(0.5) - # self.wait_loaded() - - # wait_loaded() - - # need_input = tab.eles('xpath://div[@class="contentWrapper"]//kat-input[@data-testid="kat-input-dew:ump_epr_resgitration_number_title"]',timeout=5) - # if len(need_input) > 0: - # print("需要输入注册号,不符合操作要求,跳过...") - # continue - - # target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]') - # if len(target_title) > 0: - # not_start = tab.eles('xpath://div[@class="contentWrapper"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]') - # if len(not_start) == 0: - # print("安全证明-不是未开始状态,不需要操作") - # continue - - # not_start.wait.displayed(timeout=5, raise_err=False) - # not_start.click() - - # wait_loaded() - - # check = tab.ele('xpath://div[@class="contentWrapper"]//kat-checkbox[@name="value"]') - # check.wait.displayed(timeout=5, raise_err=False) - # check.click() - - # save_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]') - # save_btn.wait.enabled(timeout=5, raise_err=False) - # save_btn.click() - - # wait_loaded() - - # close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10) - # close_btn.wait.displayed(timeout=5, raise_err=False) - # close_btn.click() - - # save_btn_ls = tab.eles('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]',timeout=5) - # if len(save_btn_ls) > 0: - # option_selection_ls = tab.eles('xpath://div[@class="contentWrapper"]//div[@role="option"]',timeout=5) - # for option in option_selection_ls: - # option.click() - # time.sleep(0.5) - - # save_btn_ls[0].wait.enabled(timeout=5, raise_err=False) - # save_btn_ls[0].click() - - # wait_loaded() - - # close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10) - # close_btn.wait.displayed(timeout=5, raise_err=False) - # close_btn.wait.enabled(timeout=5, raise_err=False) - # close_btn.click() - - # submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10) - # break - # except Exception as e: - # print(f"【handle_repair_product】处理修复商品信息异常", traceback.format_exc()) - # tab.refresh() - # tab.wait.doc_loaded(timeout=60,raise_err=False) - # time.sleep(2) - - # print("修复商品信息处理完成!") - # tab.close() - def handle_repair_product(self,tab:ChromiumTab,headle_type=""): def wait_loaded(wait_type="contentWrapper"): # 等待加载完成 @@ -244,12 +85,6 @@ class AmzoneApprove(AmamzonBase): def close_contentWrapper(): # 关闭按钮 - # close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5) - # if len(close_x_btn) > 0: - # close_x_btn[0].wait.displayed(timeout=5, raise_err=False) - # close_x_btn[0].click() - # time.sleep(1) - # close_x_btn[0].wait.deleted(timeout=5, raise_err=False) close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5) contentWrapper = tab.eles('xpath://div[@class="flyoutPanelContent"]/div[@class="contentWrapper"]',timeout=5) if len(close_x_btn) > 0 and len(contentWrapper)>0 : @@ -468,13 +303,12 @@ class AmzoneApprove(AmamzonBase): print("修复商品信息处理完成!") tab.close() - def run_page_action(self): - print("开始执行") + self.log(f"==================={self.mark_name}=======================") + self.log("开始执行...") num = 0 retry_num = 0 - already_asin = set() - now_page = None + max_retry_num = 5 while retry_num < max_retry_num: # 最多重试3次 # if num > 3: #测试 @@ -498,13 +332,21 @@ class AmzoneApprove(AmamzonBase): total_page = total_page[-1].text else: total_page = 0 - print(f"当前页码: {current_page} / 总页数: {total_page}") + self.log(f"当前页码: {current_page} / 总页数: {total_page}") except Exception as e: - print("获取页码失败", e) - print("==========商品风险解决==============") + self.log(f"获取页码失败:{e}") + + # 保存当前URL,用于失败重试时访问 + try: + self.url = self.tab.url + self.log(f"获取当前的链接:{self.url}") + except Exception as e: + self.url = None + self.log(f"获取当前的链接失败:{e}") sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10) - print(f"获取到 {len(sku_ls)}") + self.log(f"获取到 {len(sku_ls)}") + if len(sku_ls) == 0: retry_num += 1 print(f"没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}") @@ -512,44 +354,43 @@ class AmzoneApprove(AmamzonBase): self.tab.wait.doc_loaded(raise_err=False, timeout=120) continue # for sku_ele in sku_ls[0:2]: - for sku_ele in sku_ls[1:]: + for sku_ele in sku_ls: # solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]') - asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]').text - print(f"*************** {asin} *****************") - print(f"ASIN {asin} 存在问题,正在点击解决...") - if asin in already_asin: - print(f"{asin} 已经处理过了,跳过") - continue - xp_ls = [f'.//kat-link[@label="{i}"]' for i in ["解决商品信息问题","修复被禁止显示的商品","解决商品移除风险","查看冻结原因"]] - xp = "|".join(xp_ls) - # solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]') - solve_problem = sku_ele.eles(f'xpath:{xp}') - if len(solve_problem) == 0: - # //a[text0=”添加缺失的商品详情”] - lack_info = sku_ele.eles('xpath:.//a[text()="添加缺失的商品详情"]') - if len(lack_info) > 0: - print(f"{asin} 需要添加缺失的商品详情,跳过...") - yield (asin,"添加缺失的商品详情") + self.log(f"*************** {asin} *****************") + self.log(f"ASIN {asin} 存在问题,正在点击解决...") + try: + if asin in self.already_asin: + self.log(f"{asin} 已经处理过了,跳过") + continue + xp_ls = [f'.//kat-link[@label="{i}"]' for i in ["解决商品信息问题","修复被禁止显示的商品","解决商品移除风险","查看冻结原因"]] + xp = "|".join(xp_ls) + # solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]') + solve_problem = sku_ele.eles(f'xpath:{xp}') + if len(solve_problem) == 0: + # //a[text0=”添加缺失的商品详情”] + lack_info = sku_ele.eles('xpath:.//a[text()="添加缺失的商品详情"]') + if len(lack_info) > 0: + self.log(f"{asin} 需要添加缺失的商品详情,跳过...") + yield (asin,"添加缺失的商品详情") + continue + + self.log(f"{asin},没有处理入口,不处理") + yield (asin,"没有处理入口,不处理") + self.already_asin.add(asin) continue - print(f"{asin},没有处理入口,不处理") - yield (asin,"没有处理入口,不处理") - already_asin.add(asin) - continue + solve_problem[0].click() - solve_problem[0].click() + label = solve_problem[0].attr('label') + self.log(f"【当前类别】{label}") - label = solve_problem[0].attr('label') - print("【当前类别】",label) + action_panel = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]') + action_panel.wait.displayed(timeout=5, raise_err=False) - action_panel = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]') - action_panel.wait.displayed(timeout=5, raise_err=False) + self.wait_loaded() + time.sleep(1) - self.wait_loaded() - time.sleep(1) - - try: # 如果存在“请求批准”按钮,则直接返回跳过 request_approval_btn = self.tab.eles('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="section-header" and contains(string(.),"移除")]',timeout=5) if len(request_approval_btn) > 0: @@ -557,9 +398,9 @@ class AmzoneApprove(AmamzonBase): if "查看冻结原因" in label: repair_produ_btn_xp = '//kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//kat-button[@variant="secondary" and @class="action-button" and contains(string(.),"修复商品信息")]' repair_produ_btn = self.tab.eles(f'xpath:{repair_produ_btn_xp}',timeout=5) - print(f"{asin}【查看冻结原因】修复商品信息数量",len(repair_produ_btn)) + self.log(f"{asin}【查看冻结原因】修复商品信息数量,{len(repair_produ_btn)}") if len(repair_produ_btn) > 0: - print(f"{asin}【查看冻结原因】修复商品信息数量,开始操作") + self.log(f"{asin}【查看冻结原因】修复商品信息数量,开始操作") self.clear_tab() repair_produ_btn[0].click() time.sleep(3) @@ -570,24 +411,24 @@ class AmzoneApprove(AmamzonBase): self.handle_repair_product(new_tab,headle_type="查看冻结原因") else: - print(f"{i}【查看冻结原因】修复商品信息没有,不需要操作") + self.log(f"{i}【查看冻结原因】修复商品信息没有,不需要操作") else: - print(f"ASIN {asin} 存在请求批准按钮,不需要处理,跳过...") + self.log(f"ASIN {asin} 存在请求批准按钮,不需要处理,跳过...") panel_close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]', timeout=5).sr('xpath:.//button[@class="close"]') panel_close_btn.click() action_panel.wait.deleted(timeout=3, raise_err=False) yield (asin,"请求批准") - already_asin.add(asin) + self.already_asin.add(asin) continue # 无需采取任何操作 not_operate = self.tab.eles('xpath://kat-alert[contains(@description,"如果您之前已提交更改,则这些更改当前正在处理中") and not(@dismissed)]',timeout=3) if len(not_operate) > 0: - print(f"ASIN {asin} 无需采取任何操作,跳过...") + self.log(f"ASIN {asin} 无需采取任何操作,跳过...") yield (asin,"无需操作") - already_asin.add(asin) + self.already_asin.add(asin) continue # 解决商品信息违规问题 按钮 @@ -595,12 +436,12 @@ class AmzoneApprove(AmamzonBase): # if len(kat_box_ls) == 0: # print(f"ASIN {asin} 的操作面板中未找到选项,跳过...") # continue - print(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.") + self.log(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.") # 解决商品信息违规问题 按钮,有多少个都要处理 for i in range(len(kat_box_ls)): - print(f"开始处理第{i}个问题") + self.log(f"开始处理第{i}个问题") if len(kat_box_ls) <= i: - print(f"ASIN {asin} 的操作面板中选项数量发生变化,停止处理...") + self.log(f"ASIN {asin} 的操作面板中选项数量发生变化,停止处理...") break kat_box_ls[i].click() time.sleep(0.5) @@ -610,7 +451,7 @@ class AmzoneApprove(AmamzonBase): safe_handle_ls = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]', timeout=15) if len(safe_handle_ls) > 0: - print("存在安全证明,正在处理...") + self.log("存在安全证明,正在处理...") safe_handle_ls[0].click() time.sleep(0.5) self.wait_loaded() @@ -620,7 +461,7 @@ class AmzoneApprove(AmamzonBase): problem_selection_ls = self.tab.eles( 'xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="registry-list"]//div[@data-testid="registry"]', timeout=10) - print("待选择数量",len(problem_selection_ls)) + self.log(f"待选择数量,{len(problem_selection_ls)}") for problem_selection in problem_selection_ls: problem_selection.click() print("点击选择完成") @@ -641,14 +482,14 @@ class AmzoneApprove(AmamzonBase): if len(close_btn)>0: close_btn[0].click() except Exception as e: - print("点击关闭按钮失败",e) + self.log(f"点击关闭按钮失败,{e}") kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]', timeout=5) # 修复商品信息 repair_product = action_panel.eles('xpath:.//kat-button[@class="action-button" and not(@variant="secondary")]',timeout=5) if len(repair_product) > 0: - print(f"ASIN {asin} 存在修复商品信息按钮,正在点击...") + self.log(f"ASIN {asin} 存在修复商品信息按钮,正在点击...") self.clear_tab() repair_product[0].click() time.sleep(3) @@ -658,28 +499,26 @@ class AmzoneApprove(AmamzonBase): new_tab = self.browser.get_tab(id_or_num=new_tab) self.handle_repair_product(new_tab) - except Exception as e: - print("【asin】:",asin,"处理失败",traceback.format_exc()) - yield (asin,"处理失败") - already_asin.add(asin) - continue - yield (asin,"处理完成") - already_asin.add(asin) - print(f"*********************{asin} 处理完成*************************") + yield (asin,"处理完成") + self.already_asin.add(asin) + self.log(f"*********************{asin} 处理完成*************************") - try: - # 全部操作完成,关闭 - panel_close_btn= self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]', - timeout=5).sr('xpath:.//button[@class="close"]') - panel_close_btn.click() - - action_panel.wait.deleted(timeout=3, raise_err=False) - - self.clear_tab() + try: + # 全部操作完成,关闭 + panel_close_btn= self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]', + timeout=5).sr('xpath:.//button[@class="close"]') + panel_close_btn.click() + action_panel.wait.deleted(timeout=3, raise_err=False) + self.clear_tab() + except Exception as e: + self.log(f"关闭操作面板失败,{e}") except Exception as e: - print("关闭操作面板失败", e) + yield (asin, "处理失败") + self.already_asin.add(asin) + + self.log(f"{asin} 操作出错:{traceback.format_exc()}") # 判断是否存在需要翻页的情况 page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5) @@ -691,160 +530,21 @@ class AmzoneApprove(AmamzonBase): break next_page_btn.click() num += 1 - print(f"【程序计算】正在翻页,已翻 {num} 页...") + self.log(f"【程序计算】正在翻页,已翻 {num} 页...") - already_asin = set() + # already_asin = set() except Exception as e: - print("处理审批操作异常", e) + self.log(f"处理审批操作异常,{e}") traceback.print_exc() retry_num += 1 self.tab.refresh() self.tab.wait.doc_loaded(raise_err=False,timeout=120) - - - -class ApproveTask: +class ApproveTask(TaskBase): """审批任务处理类:负责处理产品风险审批任务""" - mark_name = "产品风险审批" - - country_info = { - "DE": "德国", - "FR": "法国", - "ES": "西班牙", - "IT": "意大利", - "UK": "英国" - } - - @staticmethod - def show_notification(message: str, message_type: str = "error"): - """显示 pywebview 顶层通知(5秒后自动消失) - - Args: - message: 通知消息 - message_type: 消息类型 (success/warning/error/info) - """ - if webview and webview.windows: - try: - # 转义单引号,防止 JavaScript 语法错误 - safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n') - - # 设置通知样式颜色 - color_map = { - 'success': '#67C23A', - 'warning': '#E6A23C', - 'error': '#F56C6C', - 'info': '#909399' - } - bg_color = color_map.get(message_type, '#F56C6C') - - # 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知 - js_code = f""" - (function() {{ - // 移除已存在的通知 - var existingNotif = document.getElementById('pywebview-notification'); - if (existingNotif) {{ - existingNotif.remove(); - }} - - // 创建通知容器 - var notif = document.createElement('div'); - notif.id = 'pywebview-notification'; - notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' + - 'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' + - 'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' + - 'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;'; - - // 添加消息内容 - var msgSpan = document.createElement('span'); - msgSpan.textContent = '{safe_message}'; - notif.appendChild(msgSpan); - - // 添加关闭按钮 - var closeBtn = document.createElement('span'); - closeBtn.innerHTML = '×'; - closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;'; - closeBtn.onclick = function() {{ notif.remove(); }}; - notif.appendChild(closeBtn); - - // 添加到页面 - document.body.appendChild(notif); - - // 5秒后自动消失 - setTimeout(function() {{ - if (notif && notif.parentNode) {{ - notif.style.transition = 'opacity 0.3s'; - notif.style.opacity = '0'; - setTimeout(function() {{ notif.remove(); }}, 300); - }} - }}, 5000); - }})(); - """ - # 在第一个窗口中执行 JavaScript - webview.windows[0].evaluate_js(js_code) - except Exception as e: - print(f"显示通知失败: {str(e)}") - - """ - 处理流程: - 1、解析任务json,提取对应的companyName 组组装user_info实例化 AmzoneApprove - 2、打开对应的店铺、遍历所有的国家站点处理 - 3、切换至指定国家,切换至 管理所有库存页面 - 4、调用search方法,查询出是否存在需要审批的商品,如果没有则结束;如果有则进入处理流程 - 5、进入处理流程后,调用run_page_action方法(注意是通过yield 返回数据的) - - 任务json示例: - { - "type": "product-risk-resolve-run", - "ts": 1775395996876, - "data": { - "taskId": 925, - "items": [ - { - "matched": true, - "platform": "亚马逊", - "shopName": "魏振峰", - "shopId": "27543917795757", - "companyName": "rongchuang123", - "openStoreUrl": null, - "matchedUserId": 17543915345493, - "matchStatus": "MATCHED", - "matchMessage": null - } - ], - "country_codes": [ - "UK", - "DE", - "FR", - "ES" - ] - } - } - """ - - def __init__(self, user_info: dict = None): - """初始化审批任务处理器 - - Args: - user_info: 用户信息字典,包含 company, username, password - """ - self.user_info = user_info or {} - self.running = True - - def log(self, message: str, level: str = "INFO"): - """日志输出 - - Args: - message: 日志消息 - level: 日志级别 - """ - from datetime import datetime - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if level == "ERROR": - self.show_notification(message, "error") - print(f"[{timestamp}] [ApproveTask] [{level}] {message}") - + task_name = "产品风险审批-TASK" + def process_task(self, task_data: dict): """处理审批任务主入口 @@ -872,7 +572,6 @@ class ApproveTask: self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家") - from config import runing_task runing_task[task_id] = { "status": "running", "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), @@ -897,7 +596,7 @@ class ApproveTask: shop_name = shop_item.get("shopName", "未知店铺") self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}") - self.show_notification(f"开始处理店铺: {shop_name}", "info") + show_notification(f"开始处理店铺: {shop_name}", "info") try: self.process_shop(shop_item, country_codes, task_id,risk_listing_filter) @@ -905,7 +604,6 @@ class ApproveTask: if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 except Exception as e: - import traceback self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -919,97 +617,12 @@ class ApproveTask: self.log(f"任务 {task_id} 处理完成!") except Exception as e: - import traceback self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") if task_id: - from config import runing_task if task_id in runing_task: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def open_shop(self,max_retries,company_name,shop_name,iskill=False): - error_info = "" - driver = None - for retry in range(max_retries): - try: - self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") - - if iskill: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) - - # 组装用户信息并创建驱动 - user_info = { - **self.user_info, - "company": company_name - } - driver = AmzoneApprove(user_info) - browser = driver.open_shop(shop_name) - - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name}") - else: - self.log(f"打开店铺失败: {browser}", "WARNING") - driver = None - continue - - # 判断是否需要登录 - need_login = driver.need_login() - print("【是否需要登录】:", need_login) - if need_login: - self.log(f"店铺 {shop_name} 需要登录,正在登录...") - # 获取店铺凭证 - response = get_shop_info(shop_name) - print("【获取店铺凭证返回】:", response.text) - shop_data = response.json() - if not shop_data: - mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" - self.log(mes, "ERROR") - show_notification(mes, "ERROR") - continue - - password = shop_data["data"]["password"] - - login_success = driver.login(password) - if login_success: - self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") - browser = driver.open_shop(shop_name) - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name} 登录后") - break - else: - self.log(f"登录后打开店铺失败: {browser}", "WARNING") - driver = None - else: - self.log(f"店铺 {shop_name} 登录失败", "WARNING") - driver = None - else: - break - - except Exception as e: - import traceback - self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO") - driver = None - error_info = str(e) - time.sleep(10) - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(3) - - # 检查是否成功打开 - if not driver or not browser or browser == "店铺不存在": - error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}" - self.log(error_msg, "ERROR") - # 从执行列表中移除 - if shop_name in runing_shop: - del runing_shop[shop_name] - return driver - return driver - - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str): """处理单个店铺 @@ -1049,22 +662,29 @@ class ApproveTask: break # 打开店铺 - driver = self.open_shop(max_retries=max_retries, company_name=company_name, + driver = self.open_shop(cls=AmzoneApprove,max_retries=max_retries, company_name=company_name, shop_name=shop_name, iskill=iskill) + if driver is None: self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR") for country_code in country_codes: self.post_result(task_id, shop_name, country_code, "", "", is_done=True) return - try: - self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter) - except Exception as e: - import traceback - self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - if "与页面的连接已断开" in str(e): - iskill = True + current_url = None + for _ in range(max_retries): + try: + self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,target_url=current_url) + driver.reset_already_asin() + driver.close_store() + + break + except Exception as e: + self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + if "与页面的连接已断开" in str(e): + iskill = True + current_url = driver.url # 更新已处理国家数 if task_id in runing_task: @@ -1074,6 +694,7 @@ class ApproveTask: # 最后回传,标记完成 try: self.post_result(task_id, shop_name, country_code, "", "", is_done=True) + driver.close_store() #完成退出店铺 except Exception as e: self.log(f"回传结果失败: {str(e)}", "ERROR") @@ -1082,168 +703,23 @@ class ApproveTask: del runing_shop[shop_name] self.log(f"店铺 {shop_name} 已从执行列表中移除") - def action_init(self,driver,country_name,shop_name,risk_listing_filter): - """切换到指定国际 -> 页面 -》 筛选好""" - max_retries = 5 - switch_success = False - switch_success_pg = False - search_success = False - sku_ls = [] - for retry in range(max_retries): - try: - self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") - if retry > 2: - # 刷新不行就重新打开店铺 - self.log("重试前重新打开店铺...") - try: - driver.close_store() - time.sleep(3) - driver.open_shop(shop_name) - except Exception as e: - self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") - # 如果不是第一次尝试,先刷新页面 - if retry > 0: - self.log("重试前刷新页面...") - try: - driver.tab.refresh() - time.sleep(3) - except Exception as e: - self.log(f"刷新页面失败: {str(e)}", "WARNING") - - switch_success = driver.SwitchingCountries(country_name) - if switch_success: - self.log(f"成功切换到国家 {country_name}") - - # 切换到库存管理页面 - driver.SwitchPage() - self.log(f"已切换到库存管理页面") - time.sleep(3) - switch_success_pg = True - - sku_ls = driver.search(filter_type=risk_listing_filter) - search_success = True - self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...") - break - else: - self.log(f"切换到国家 {country_name} 失败", "WARNING") - - except Exception as e: - import traceback - self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(2) - - # 如果切换失败,直接返回 - # if not switch_success or not switch_success_pg or not search_success: - # error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家" - # self.log(error_message, "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - return switch_success,switch_success_pg,search_success,sku_ls - - def process_country(self, driver: AmzoneApprove, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str): - """处理单个国家的审批任务 - - Args: - driver: AmzoneApprove驱动实例 - country_code: 国家代码(如 UK, DE, FR 等) - task_id: 任务ID - shop_name: 店铺名称 - risk_listing_filter: 风险商品筛选条件 + def process_country(self, driver: AmzoneApprove, country_code: str, task_id: int, + shop_name: str, risk_listing_filter: str,target_url:str=None): """ - from config import runing_task - + 处理单个国家的审批任务 + """ + # 转换国家代码为中文名称 country_name = self.country_info.get(country_code, country_code) info_mes = f"开始处理国家: {country_name} ({country_code})" self.log(info_mes) - self.show_notification(info_mes, "info") + show_notification(info_mes, "info") # 更新当前处理的国家 if task_id in runing_task: runing_task[task_id]["current_country"] = country_name # 切换国家,最多重试3次 - max_retries = 3 - switch_success = False - - # for retry in range(max_retries): - # try: - # self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") - # if retry > 1: - # # 刷新不行就重新打开店铺 - # self.log("重试前重新打开店铺...") - # try: - # driver.close_store() - # time.sleep(3) - # driver.open_shop(shop_name) - # except Exception as e: - # self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") - # - # # 如果不是第一次尝试,先刷新页面 - # if retry > 0: - # self.log("重试前刷新页面...") - # try: - # driver.tab.refresh() - # time.sleep(3) - # except Exception as e: - # self.log(f"刷新页面失败: {str(e)}", "WARNING") - # - # switch_success = driver.SwitchingCountries(country_name) - # if switch_success: - # self.log(f"成功切换到国家 {country_name}") - # break - # else: - # self.log(f"切换到国家 {country_name} 失败", "WARNING") - # - # except Exception as e: - # import traceback - # self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") - # self.log(traceback.format_exc(), "ERROR") - # - # # 如果还有重试机会,等待后继续 - # if retry < max_retries - 1: - # time.sleep(2) - # - # # 如果切换失败,直接返回 - # if not switch_success: - # error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家" - # self.log(error_message, "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - # - # # 切换到库存管理页面 - # try: - # driver.SwitchPage() - # self.log(f"已切换到库存管理页面") - # except Exception as e: - # import traceback - # self.log(f"切换页面失败: {str(e)}", "ERROR") - # self.log(traceback.format_exc(), "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - # - # # 搜索需要审批的商品,最多重试3次 - # sku_ls = [] - # for retry in range(max_retries): - # try: - # self.log(f"尝试搜索需要审批的商品 (第 {retry + 1}/{max_retries} 次)") - # sku_ls = driver.search(filter_type=risk_listing_filter) - # break - # except Exception as e: - # self.log(f"搜索商品异常: {str(e)}", "ERROR") - # if retry < max_retries - 1: - # try: - # driver.tab.refresh() - # time.sleep(3) - # except Exception as refresh_error: - # self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING") - # max_retries = 5 switch_success, switch_success_pg, search_success,sku_ls = self.action_init(driver, country_name, shop_name, risk_listing_filter) @@ -1263,7 +739,13 @@ class ApproveTask: return self.log(f"国家 {country_name} 有 {len(sku_ls)} 个需要审批的商品,开始处理...") - + + if target_url is not None: #从失败的链接继续 + self.log(f"开始访问:{target_url}") + driver.tab.get(url=target_url) + driver.tab.wait.doc_loaded(timeout=60,raise_err=False) + + result = [] # 处理所有需要审批的商品(通过yield获取结果) for asin, status in driver.run_page_action(): # 检查是否收到暂停请求 @@ -1273,26 +755,33 @@ class ApproveTask: self.log(f"ASIN {asin} 处理结果: {status}") - # 更新任务状态 - if task_id in runing_task: - runing_task[task_id]["current_asin"] = asin - runing_task[task_id]["processed_asins"] += 1 + if status in ["请求批准", "添加缺失的商品详情"]: + country_data = { + "status": "", + "shopName": shop_name, + "productAsinSku": "", + "done": False, + "removeAsin": asin, + "removeStatus": status + } + else: + country_data = { + "status": status, + "shopName": shop_name, + "productAsinSku": asin, + "done": False, + "removeAsin": "", + "removeStatus": "" + } + result.append(country_data) - if status == "处理完成": - runing_task[task_id]["success_count"] += 1 - elif status in ["请求批准", "添加缺失的商品详情"]: - # 请求批准的商品也算作成功(因为不需要处理) - runing_task[task_id]["success_count"] += 1 - else: - runing_task[task_id]["failed_count"] += 1 + if len(result) > 20: + self.post_result_batch(task_id, shop_name, country_code, result) + result = [] - # 回传结果到API - try: - self.post_result(task_id, shop_name, country_code, asin, status) - except Exception as e: - self.log(f"回传结果失败: {str(e)}", "ERROR") + if len(result) > 0: + self.post_result_batch(task_id, shop_name, country_code, result) - self.log(f"国家 {country_name} 处理完成") def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False): @@ -1305,9 +794,7 @@ class ApproveTask: asin: ASIN status: 处理状态 """ - import requests - from config import DELETE_BRAND_API_BASE - + url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result" if status in ["请求批准","添加缺失的商品详情"]: country_data = { @@ -1343,7 +830,7 @@ class ApproveTask: max_retries = 3 for retry in range(max_retries): try: - print("=====================================") + self.log("=====================================") self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") self.log(f"回传URL: {url}") self.log(f"回传数据: {payload}") @@ -1372,7 +859,60 @@ class ApproveTask: time.sleep(2) self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - + + def post_result_batch(self, task_id: int, shop_name: str, country_code: str, country_data_ls:list): + """ + 批量回传处理结果到API + """ + + url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result" + + + payload = { + "shops": [ + { + "error": "", + "countries": { + country_code: country_data_ls + }, + "shopName": shop_name + } + ] + } + + max_retries = 3 + for retry in range(max_retries): + try: + self.log("=====================================") + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + self.log(f"回传URL: {url}") + self.log(f"回传数据: {payload}") + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False + ) + self.log(f"回传结果: {response.text}") + + if response.status_code == 200: + self.log(f"结果回传成功") + return + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + print("=====================================") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + print("=====================================") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + if __name__ == '__main__': diff --git a/app/amazon/asin_status.py b/app/amazon/asin_status.py index 8a5ad46..fee9d83 100644 --- a/app/amazon/asin_status.py +++ b/app/amazon/asin_status.py @@ -1,42 +1,17 @@ -import json -import sys -import os -import io -# sys.stdout.reconfigure(encoding='utf-8') - import time -import re import traceback from datetime import datetime -from DrissionPage import Chromium, ChromiumOptions import requests -from amazon.del_brand import AmamzonBase, kill_process -from amazon.tool import show_notification, get_shop_info, remove_special_characters, split_currency_values +from amazon.amazon_base import AmamzonBase, kill_process,TaskBase +from amazon.tool import show_notification -from config import runing_task, runing_shop, base_dir, DELETE_BRAND_API_BASE +from config import runing_task, runing_shop, DELETE_BRAND_API_BASE class AmzonePStatus(AmamzonBase): mark_name = "状态查询" - def SwitchPage(self): - """ - 切换至 管理所有库存页面 - 1、等待 //navigation-favorites-bar[@class="hydrated"] 出现 - """ - navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]') - navigation.wait.displayed(raise_err=False) - page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr( - 'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]') - page_btn.wait.displayed(raise_err=False) - page_btn.click(timeout=5) - - self.tab.wait.doc_loaded() - # 等待搜索框出现 - search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') - search_region.wait.displayed(raise_err=False, timeout=60) - def search_asin(self, asin): search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') search_region.wait.displayed(raise_err=False) @@ -81,14 +56,8 @@ class AmzonePStatus(AmamzonBase): self.browser.close_tabs(close_tab) def run_page_action(self, appoint_asin: str = None): - """ - - miniprice_info : - { - "B0DSJGJBWV" : "19.81" # asin : 最低价 - } - """ - print(f"【{self.mark_name}】,开始执行") + self.log(f"==================={self.mark_name}=======================") + self.log("开始执行...") num = 0 retry_num = 0 already_asin = set() @@ -174,29 +143,8 @@ class AmzonePStatus(AmamzonBase): self.tab.wait.doc_loaded(raise_err=False, timeout=120) -class StatusTask: - country_info = { - "DE": "德国", - "FR": "法国", - "ES": "西班牙", - "IT": "意大利", - "UK": "英国" - } - - def __init__(self, user_info: dict = None): - """初始化审批任务处理器 - - Args: - user_info: 用户信息字典,包含 company, username, password - """ - self.user_info = user_info or {} - self.running = True - - def log(self, message: str, level: str = "INFO"): - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if level == "ERROR": - show_notification(message, "error") - print(f"[{timestamp}] [PriceTask] [{level}] {message}") +class StatusTask(TaskBase): + task_name = "状态查询-TASK" def process_task(self, task_data: dict): """处理审批任务主入口 @@ -218,21 +166,12 @@ class StatusTask: # 用于测试 limit = data.get("limit", None) - if not task_id: - self.log("任务ID为空,跳过", "WARNING") - return - - # if not items: - # self.log("店铺列表为空,跳过", "WARNING") - # return - - if not queryAsins: - self.log("queryAsins 列表为空,跳过", "WARNING") + if not task_id or not queryAsins: + self.log("任务ID / queryAsins列表 为空,跳过", "WARNING") return self.log(f"开始处理审批任务 {task_id},共 1 个店铺,{len(queryAsins)} 个国家") - from config import runing_task runing_task[task_id] = { "status": "running", "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), @@ -265,7 +204,6 @@ class StatusTask: if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 except Exception as e: - import traceback self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -279,96 +217,12 @@ class StatusTask: self.log(f"任务 {task_id} 处理完成!") except Exception as e: - import traceback self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") if task_id: - from config import runing_task if task_id in runing_task: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def open_shop(self, max_retries, company_name, shop_name, iskill=False): - error_info = "" - driver = None - for retry in range(max_retries): - try: - self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") - - if iskill: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) - - # 组装用户信息并创建驱动 - user_info = { - **self.user_info, - "company": company_name - } - driver = AmzonePStatus(user_info) - browser = driver.open_shop(shop_name) - - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name}") - else: - self.log(f"打开店铺失败: {browser}", "WARNING") - driver = None - continue - - # 判断是否需要登录 - need_login = driver.need_login() - print("【是否需要登录】:", need_login) - if need_login: - self.log(f"店铺 {shop_name} 需要登录,正在登录...") - # 获取店铺凭证 - response = get_shop_info(shop_name) - print("【获取店铺凭证返回】:", response.text) - shop_data = response.json() - if not shop_data: - mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" - self.log(mes, "ERROR") - show_notification(mes, "ERROR") - continue - - password = shop_data["data"]["password"] - - login_success = driver.login(password) - if login_success: - self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") - browser = driver.open_shop(shop_name) - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name} 登录后") - break - else: - self.log(f"登录后打开店铺失败: {browser}", "WARNING") - driver = None - else: - self.log(f"店铺 {shop_name} 登录失败", "WARNING") - driver = None - else: - break - - except Exception as e: - import traceback - self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO") - driver = None - error_info = str(e) - time.sleep(10) - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(3) - - # 检查是否成功打开 - if not driver or not browser or browser == "店铺不存在": - error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}" - self.log(error_msg, "ERROR") - # 从执行列表中移除 - if shop_name in runing_shop: - del runing_shop[shop_name] - return driver - return driver - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, user_id=None, stage_index=None, final_stage: bool = True, limit: str = None): """处理单个店铺 @@ -382,7 +236,6 @@ class StatusTask: shop_name = shop_item.get("shopName", "未知店铺") company_name = shop_item.get("companyName", "") - if not company_name: self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING") return @@ -403,7 +256,7 @@ class StatusTask: country_code = value.get("country") all_asin = value.get("asins") # 打开店铺 - driver = self.open_shop(max_retries=max_retries, company_name=company_name, + driver = self.open_shop(cls=AmzonePStatus,max_retries=max_retries, company_name=company_name, shop_name=shop_name, iskill=iskill) if driver is None: self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR") @@ -419,7 +272,6 @@ class StatusTask: try: self.process_country(driver, country_code, task_id, shop_name,all_asin=all_asin ) except Exception as e: - import traceback self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") if "与页面的连接已断开" in str(e): @@ -450,8 +302,7 @@ class StatusTask: self.log(f"店铺 {shop_name} 已从执行列表中移除") def process_country(self, driver: AmzonePStatus, country_code: str, task_id: int, shop_name: str, - all_asin:list, - limit: str = None): + all_asin:list,limit: str = None): """处理单个国家的审批任务 Args: @@ -505,7 +356,6 @@ class StatusTask: self.log(f"切换到国家 {country_name} 失败", "WARNING") except Exception as e: - import traceback self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -527,7 +377,6 @@ class StatusTask: driver.SwitchPage() self.log(f"已切换到库存管理页面") except Exception as e: - import traceback self.log(f"切换页面失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -677,6 +526,7 @@ class StatusTask: raise RuntimeError("已达到最大重试次数,结果回传最终失败") + if __name__ == '__main__': # 使用示例 user_info = { diff --git a/app/amazon/chrome_base.py b/app/amazon/chrome_base.py new file mode 100644 index 0000000..81b7f64 --- /dev/null +++ b/app/amazon/chrome_base.py @@ -0,0 +1,189 @@ +import json +import sys +import os +import io + +import time +import traceback +from datetime import datetime +from DrissionPage import Chromium, ChromiumOptions +from collections import defaultdict + +from config import base_dir +from amazon.tool import get_shop_info,show_notification + + + +class ChromeAmzoneBase: + mark_name = "谷歌浏览器基类" + + country_info = { + "英国": { + "url": "https://www.amazon.co.uk/dp/B0CJ8SNXXV", + "zip_code": "SW1A 1AA", + "mark": "SW1A 1" + }, + "德国": { + "url": "https://www.amazon.de/dp/B0CC8CW9G2?th=1", + "zip_code": "10115" + }, + "法国": { + "url": "https://www.amazon.fr/dp/B0FRG1MJ8H?th=1", + "zip_code": "75001" + }, + "西班牙": { + "url": "https://www.amazon.es/dp/B08ZXVNYNN", + "zip_code": "28001" + }, + "意大利": { + "url": "https://www.amazon.it/dp/B0D1P17T2Q", + "zip_code": "20121" + } + } + + def __init__(self): + """ + 杀死当前谷歌浏览器进程,并使用 drissionpage 启动谷歌浏览器,使用系统安装的浏览器默认用户文件夹 + """ + print("正在关闭现有的chromium浏览器进程...") + os.system('taskkill /f /t /im chrome.exe') + time.sleep(2) + + print("正在启动chromium浏览器...") + # 配置浏览器选项 + co = ChromiumOptions() + user_data_path = os.path.join(base_dir, "user_data", "chrome_data") + if not os.path.exists(user_data_path): + os.makedirs(user_data_path, exist_ok=True) + co.set_user_data_path(user_data_path) + co.set_local_port(port=19897) + self.browser = Chromium(co) + self.tab = self.browser.latest_tab + print("Chrome浏览器启动成功") + + def log(self, message: str, level: str = "INFO"): + """日志输出 + Args: + message: 日志消息 + level: 日志级别 + """ + try: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if level == "ERROR": + show_notification(message, "error") + print(f"[{timestamp}] [{self.mark_name}] [{level}] {message}") + except Exception as e: + print(f"输出出错,{e}") + + def close_init_popup(self): + """ + 关闭所有的初始化弹窗 + """ + self.tab.wait.doc_loaded(timeout=60, raise_err=True) + footbar = self.tab.eles('xpath://footer[@class="el-dialog__footer"]', timeout=5) + if len(footbar) > 0: + do_not_remind = footbar[0].eles('xpath:.//input[@class="el-checkbox__original"]') + if len(do_not_remind) > 0: + do_not_remind[0].check() + resume_immediately = footbar[0].eles('xpath:.//button') + if len(resume_immediately) > 0: + resume_immediately[0].click() + + self.tab.wait.doc_loaded(timeout=60, raise_err=True) + accept_btn = self.tab.eles('xpath://input[@id="sp-cc-accept"]', timeout=5) + if len(accept_btn) > 0: + accept_btn[0].click() + + def _set_zip_code(self, zip_code, mark=None): + """ + 设置邮编 + + Args: + zip_code: 目标邮编 + """ + try: + # 检查当前邮编 + zip_display = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) + + if zip_display: + current_text = zip_display.text + # print(f"当前地址信息: {current_text}") + # 先检查标识 + if mark is not None and mark in current_text: + print(f"邮编检测到标识: {mark},无需修改") + return True + # 检查是否已经包含目标邮编 + if zip_code in current_text: + print(f"邮编已经设置为: {zip_code},无需修改") + return True + + # 需要设置邮编 + print(f"正在设置邮编为: {zip_code}") + + # 点击地址选择按钮 + location_link = self.tab.ele('xpath://a[@id="nav-global-location-popover-link"]', timeout=10) + if not location_link: + print("找不到地址设置按钮") + return False + + location_link.click() + time.sleep(1) + + # 等待邮编输入框出现 + zip_input = self.tab.ele('xpath://input[@id="GLUXZipUpdateInput"]', timeout=10) + if not zip_input: + print("找不到邮编输入框") + return False + + # 输入邮编 + zip_input.input(zip_code, clear=True) + time.sleep(0.5) + + # 点击提交按钮 + submit_btn = self.tab.ele('xpath://input[@aria-labelledby="GLUXZipUpdate-announce"]', timeout=10) + if not submit_btn: + print("找不到提交按钮") + return False + + submit_btn.click() + + continue_btn = self.tab.eles('xpath://div[@class="a-popover-footer"]//input[@id="GLUXConfirmClose"]', + timeout=10) + if len(continue_btn) > 0: + continue_btn[0].click() + + # 等待提交按钮消失(表示请求已发送) + print("等待邮编更新...") + time.sleep(2) + + # 等待页面加载完成 + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + time.sleep(2) + + # 验证邮编是否设置成功 + zip_display_after = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) + if zip_display_after: + updated_text = zip_display_after.text + # print(f"更新后的地址信息: {updated_text}") + + if zip_code in updated_text: + print(f"邮编设置成功: {zip_code}") + return True + else: + # print(f"邮编设置可能失败,当前显示: {updated_text}") + return False + + return True + + except Exception as e: + print(f"设置邮编时出错: {traceback.format_exc()}") + return False + + def close(self): + """关闭浏览器""" + try: + if self.browser: + self.browser.quit() + print("浏览器已关闭") + except Exception as e: + print(f"关闭浏览器时出错: {str(e)}") \ No newline at end of file diff --git a/app/amazon/del_brand.py b/app/amazon/del_brand.py index 884b093..628c19a 100644 --- a/app/amazon/del_brand.py +++ b/app/amazon/del_brand.py @@ -6,560 +6,18 @@ import uuid import requests import json import os +from datetime import datetime -from typing import Literal -from DrissionPage import Chromium,ChromiumOptions -from DrissionPage.common import By -from DrissionPage._pages.chromium_tab import ChromiumTab +from amazon.amazon_base import AmamzonBase,kill_process,TaskBase +from amazon.tool import show_notification,get_shop_info +from config import runing_task,runing_shop,DELETE_BRAND_API_BASE -def kill_process(version: Literal["v5", "v6"]): - """杀紫鸟客户端进程(独立函数版本)""" - driver = ZiniaoDriver({}) - driver.kill_process(version) - - -class ZiniaoDriver: - """紫鸟浏览器自动化驱动类""" - - def __init__(self, user_info: dict, socket_port: int = 19890): - """ - 初始化紫鸟浏览器驱动 - - Args: - user_info: 用户信息字典,包含 company, username, password - socket_port: 客户端通信端口,默认 19890 - """ - self.user_info = user_info - self.socket_port = socket_port - self.client_path = None - self.browser = None - self.tab: ChromiumTab = None - self.store_id = None - - def get_zinaio_exe(self, protocol_name: str = "superbrowser"): - """ - 获取紫鸟安装目录 - - Args: - protocol_name: 协议名称,默认 "superbrowser" - - Returns: - exe_path: 紫鸟浏览器可执行文件路径 - """ - try: - key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command" - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) - command, _ = winreg.QueryValueEx(key, "") - winreg.CloseKey(key) - if isinstance(command, str): - sub = "ziniao.exe" - exe_path = command[0: command.find(sub)+len(sub)+1] - else: - exe_path = command[0] - return exe_path - except FileNotFoundError: - try: - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) - command, _ = winreg.QueryValueEx(key, "") - winreg.CloseKey(key) - if isinstance(command, str): - sub = "ziniao.exe" - exe_path = command[0: command.find(sub)+len(sub)+1] - else: - exe_path = command[0] - return exe_path - except FileNotFoundError: - return None - - def update_core(self): - """ - 下载所有内核,打开店铺前调用,需客户端版本5.285.7以上 - 因为http有超时时间,所以这个action适合循环调用,直到返回成功 - """ - data = { - "action": "updateCore", - "requestId": str(uuid.uuid4()), - } - data.update(self.user_info) - while True: - url = f'http://127.0.0.1:{self.socket_port}' - response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) - result = response.json() - print(result) - if result is None: - print("等待客户端启动...") - time.sleep(2) - continue - if result.get("statusCode") is None or result.get("statusCode") == -10003: - print("当前版本不支持此接口,请升级客户端") - return - elif result.get("statusCode") == 0: - print("更新内核完成") - return - else: - print(f"等待更新内核: {json.dumps(result)}") - time.sleep(2) - - def kill_process(self, version: Literal["v5", "v6"]): - """ - 杀紫鸟客户端进程 - - Args: - version: 客户端版本 - """ - if version == "v5": - process_name = 'SuperBrowser.exe' - os.system('taskkill /f /t /im ' + "starter.exe") - else: - process_name = 'ziniao.exe' - os.system('taskkill /f /t /im ' + process_name) - time.sleep(3) - - def get_browser_list(self) -> list: - """ - 获取浏览器列表 - - Returns: - list: 浏览器列表 - """ - request_id = str(uuid.uuid4()) - data = { - "action": "getBrowserList", - "requestId": request_id - } - data.update(self.user_info) - - url = f'http://127.0.0.1:{self.socket_port}' - response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) - r = response.json() - if str(r.get("statusCode")) == "0": - print(r) - return r.get("browserList") - elif str(r.get("statusCode")) == "-10003": - print(f"【get_browser_list】登录失败 {json.dumps(r, ensure_ascii=False)}") - # exit() - else: - print(f"【get_browser_list】失败 {json.dumps(r, ensure_ascii=False)} ") - # exit() - - def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0, - isHeadless=0, cookieTypeSave=0, jsInfo=""): - """ - 打开店铺 - - Args: - store_info: 店铺信息(browserId 或 browserOauth) - isWebDriverReadOnlyMode: 是否只读模式,默认 0 - isprivacy: 隐私模式,默认 0 - isHeadless: 无头模式,默认 0 - cookieTypeSave: cookie保存类型,默认 0 - jsInfo: 注入的JS信息,默认 "" - - Returns: - dict: 返回结果 - """ - request_id = str(uuid.uuid4()) - data = { - "action": "startBrowser", - "isWaitPluginUpdate": 0, - "isHeadless": isHeadless, - "requestId": request_id, - "isWebDriverReadOnlyMode": isWebDriverReadOnlyMode, - "cookieTypeLoad": 0, - "cookieTypeSave": cookieTypeSave, - "runMode": "1", - "isLoadUserPlugin": True, - "pluginIdType": 1, - "privacyMode": isprivacy - } - data.update(self.user_info) - - if store_info.isdigit(): - data["browserId"] = store_info - else: - data["browserOauth"] = store_info - - if len(str(jsInfo)) > 2: - data["injectJsInfo"] = json.dumps(jsInfo) - - url = f'http://127.0.0.1:{self.socket_port}' - response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) - r = response.json() - if str(r.get("statusCode")) == "0": - return r - elif str(r.get("statusCode")) == "-10003": - raise RuntimeError(f"【open_store】登录失败 {json.dumps(r, ensure_ascii=False)}") - # exit() - else: - raise RuntimeError(f"【open_store】失败 {json.dumps(r, ensure_ascii=False)} ") - # exit() - - def get_browser(self, port) -> Chromium: - """ - 获取浏览器实例 - - Args: - port: 调试端口 - - Returns: - Chromium: DrissionPage浏览器实例 - """ - co = ChromiumOptions() - # 设置不加载图片、静音 - co.set_local_port(port) - co.no_imgs(True).mute(True) - browser = Chromium(co) - return browser - - def start_client(self): - """启动紫鸟客户端""" - - # 检查端口是否已经启动 - try: - url = f'http://127.0.0.1:{self.socket_port}' - response = requests.get(url, timeout=2) - print(f"端口 {self.socket_port} 已经启动,跳过启动客户端操作") - return - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - # 端口未启动,继续执行启动操作 - print(f"端口 {self.socket_port} 未启动,开始启动客户端") - - self.kill_process('v6') - time.sleep(5) - self.client_path = self.get_zinaio_exe("superbrowserv6").strip('"') - print(self.client_path) - cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http', - '--port=' + str(self.socket_port)] - print(" ".join(cmd)) - - # 最大重试次数 - max_retries = 3 - - for retry_count in range(max_retries): - print(f"第 {retry_count + 1} 次尝试启动客户端...") - - # 启动进程 - subprocess.Popen(cmd) - - # 循环检测10秒,每0.5秒检测一次 - start_check_time = time.time() - client_started = False - - while time.time() - start_check_time < 10: - try: - response = requests.get(url, timeout=2) - print(f"客户端启动成功!(第 {retry_count + 1} 次尝试)") - client_started = True - break - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - # 端口还未启动,继续等待 - time.sleep(0.5) - - if client_started: - time.sleep(5) # 等待客户端完全启动 - # 更新内核 - self.update_core() - return - else: - print(f"第 {retry_count + 1} 次尝试启动失败,10秒内未检测到客户端启动") - - # 超过最大重试次数,抛出异常 - raise RuntimeError(f"客户端启动失败:重试 {max_retries} 次后仍未成功启动") - - def open_shop(self, shop_name: str): - """ - 打开指定店铺 - - Args: - shop_name: 店铺名称 - - Returns: - Chromium or str: 成功返回浏览器实例,失败返回错误信息 - """ - # 启动客户端 - self.start_client() - - - - # 获取店铺列表 - shop_ls = self.get_browser_list() - self.store_id = None - print(shop_ls) - for shop in shop_ls: - if shop.get("browserName") == shop_name: - self.store_id = shop.get('browserOauth') - break - - if not self.store_id: - print("店铺不存在") - return "店铺不存在" - - # 打开店铺 - ret_json = self.open_store(self.store_id) - print(ret_json) - self.store_id = ret_json.get("browserOauth") - if self.store_id is None: - self.store_id = ret_json.get("browserId") - - # 获取drissionpage浏览器会话 - self.browser = self.get_browser(ret_json.get('debuggingPort')) - - ip_check_url = ret_json.get("ipDetectionPage") - if not ip_check_url: - print("ip检测页地址为空,请升级紫鸟浏览器到最新版") - print(f"=====关闭店铺:{shop_name}=====") - self.close_store(self.store_id) - # exit() - raise RuntimeError("没有IP检测地址,为了店铺安全不打开店铺") - ip_usable = self.open_ip_check(self.browser, ip_check_url) - if ip_usable: - print("ip检测通过,打开店铺平台主页") - self.open_launcher_page(ret_json.get("launcherPage"), self.browser) - else: - print("IP检测不通过") - raise RuntimeError("IP检测不通过,可能是因为网络环境变化导致的,为了店铺安全不打开店铺") - return self.browser - - def close_store(self, browser_oauth=None): - """ - 关闭店铺 - - Args: - browser_oauth: 店铺OAuth标识,如果不提供则使用当前打开的店铺 - - Returns: - dict: 返回结果 - """ - if browser_oauth is None: - browser_oauth = self.store_id - - request_id = str(uuid.uuid4()) - data = { - "action": "stopBrowser", - "requestId": request_id, - "duplicate": 0, - "browserOauth": browser_oauth - } - data.update(self.user_info) - - url = f'http://127.0.0.1:{self.socket_port}' - response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120) - r = response.json() - if str(r.get("statusCode")) == "0": - return r - elif str(r.get("statusCode")) == "-10003": - raise RuntimeError(f"【close_store】登录失败 {json.dumps(r, ensure_ascii=False)}") - # exit() - else: - raise RuntimeError(f"【close_store】失败: {json.dumps(r, ensure_ascii=False)} ") - # exit() - - def open_launcher_page(self, launcher_page: str, browser: Chromium = None): - """ - 打开启动页面 - - Args: - launcher_page: 要打开的页面URL - browser: 浏览器实例,如果不提供则使用当前浏览器实例 - """ - if browser is None: - browser = self.browser - - tab = browser.new_tab(url=launcher_page) - self.tab = tab - return tab - - def open_ip_check(self, browser: Chromium, ip_check_url: str): - """ - 打开ip检测页检测ip是否正常 - :param browser: drissionpage浏览器会话 - :param ip_check_url ip检测页地址 - :return 检测结果 - """ - try: - tab = browser.latest_tab - tab.get(ip_check_url) - success_button = tab.ele((By.XPATH, '//button[contains(@class, "styles_btn--success")]'), - timeout=60) # 等待查找元素60秒 - if success_button: - print("ip检测成功") - return True - else: - print("ip检测超时") - return False - except Exception as e: - print("ip检测异常:" + traceback.format_exc()) - return False - - -class AmamzonBase(ZiniaoDriver): - """亚马逊操作基类,包含一些通用方法""" - - def SwitchingCountries(self, country_name: str): - """ - 切换国家 - 操作: - 1、//div[@class="dropdown-account-switcher-header-label"]/span[last()] 获取此元素文本,判断当前国家,如果与目标国家相同则不操作,否则执行下一步 - 2、点击 //div[@class="dropdown-account-switcher-header-label"] 打开下拉框 - 3、点击 //div[@class="dropdown-account-switcher-list-item"] 第一个展开国家列表 - 4、点击 //div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="国家名"] 切换到目标国家 - 5、等待页面加载完成,判断国家是否切换成功,成功则返回True,否则返回False - - Args: - country_name: 目标国家名称 - - Returns: - bool: 切换成功返回True,失败返回False - """ - try: - if self.browser is None: - print("浏览器实例不存在,请先打开店铺") - return False - - # 获取当前标签页 - tab = self.tab - tab.wait.doc_loaded(timeout=120,raise_err=False) - - # 步骤1:获取当前国家名称 - print(f"正在检查当前国家...") - current_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]', - timeout=20) - if current_country_ele: - current_country = current_country_ele.text.strip() - print(f"当前国家:{current_country}") - - # 判断是否与目标国家相同 - if current_country == country_name: - print(f"当前已经是目标国家 {country_name},无需切换") - return True - else: - print("无法获取当前国家信息") - return False - - # 步骤2:点击打开下拉框 - print(f"正在打开国家切换下拉框...") - dropdown_header = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]', timeout=10) - if not dropdown_header: - print("找不到国家切换下拉框") - return False - dropdown_header.click() - time.sleep(1) # 等待下拉框展开 - - # 步骤3:点击第一个展开国家列表 - print(f"正在展开国家列表...") - first_item = tab.ele('xpath://div[@class="dropdown-account-switcher-list-item"]', timeout=10) - if not first_item: - print("找不到国家列表项") - return False - first_item.click() - time.sleep(1) # 等待国家列表展开 - - # 步骤4:点击目标国家 - print(f"正在切换到国家:{country_name}") - target_country_xpath = f'//div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="{country_name}"]' - target_country = tab.ele(f'xpath:{target_country_xpath}', timeout=10) - if not target_country: - print(f"找不到目标国家:{country_name}") - return False - target_country.click() - - # 步骤5:等待页面加载完成并验证切换结果 - print(f"等待页面加载...") - # time.sleep(3) # 等待页面加载 - self.tab.wait.doc_loaded() - - # 再次检查当前国家 - new_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]', - timeout=10) - if new_country_ele: - new_country = new_country_ele.text.strip() - if new_country == country_name: - print(f"国家切换成功:{new_country}") - return True - else: - print(f"国家切换失败,当前国家:{new_country},目标国家:{country_name}") - return False - else: - print("无法验证切换结果") - return False - - except Exception as e: - print(f"切换国家时发生异常:{traceback.format_exc()}") - return False - - def need_login(self): - """ - 判断是否需要登录,部分国家可能需要登录后才能切换国家 - 处理流程: - - """ - time.sleep(3) # 等待页面可能的登录元素加载,避免跳转等等 - self.tab.wait.doc_loaded(timeout=30, raise_err=False) - need_login_ele = self.tab.eles('xpath://h1[@class="a-spacing-small"]|//span[contains(text(),"登录")]',timeout=5) - if len(need_login_ele) >0: - print("检测到需要登录元素") - return True - return False - - def login(self,password,username=""): - try: - self.tab.wait.doc_loaded(timeout=30,raise_err=False) - for _ in range(4): - pwd_input = self.tab.eles('xpath://input[@type="password"]',timeout=5) - if len(pwd_input) > 0: - pwd_input[0].input(password,clear=True) - submit_btn = self.tab.eles('xpath://input[@id="signInSubmit"]',timeout=5) - if len(submit_btn) > 0: - submit_btn[0].click() - self.tab.wait.doc_loaded(timeout=30,raise_err=False) - - send_code = self.tab.eles('xpath://span[@id="auth-send-code" and contains(string(.),"发送一次性密码")]',timeout=5) - print("发送一次性密码:",len(send_code)) - if len(send_code) > 0: - print("检测到 发送一次性密码") - send_code[0].click() - time.sleep(1) - self.tab.wait.doc_loaded(timeout=30,raise_err=False) - else: - for j in range(5): - opt_code_input = self.tab.eles('xpath://input[@name="otpCode"]',timeout=30) - print("验证码输入框:",len(opt_code_input)) - if len(opt_code_input) > 0: - opt_code_input[0].wait.displayed(timeout=10,raise_err=False) - for _ in range(30): - if opt_code_input[0].value is not None and opt_code_input[0].value.strip() != "": - print("检测到验证码输入完成") - submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]',timeout=10) - if len(submit_btn) > 0: - submit_btn[0].click() - self.tab.wait.doc_loaded(timeout=20,raise_err=False) - # return True - error_mes = self.tab.eles('xpath://div[@id="auth-error-message-box"]',timeout=10) - if len(error_mes)>0: - print("验证码输入错误") - print("验证码输入错误提示:",error_mes[0].text) - self.tab.refresh() - else: - print("登入成功") - return True - - time.sleep(1) - - else: - break - - - submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]',timeout=10) - if len(submit_btn) > 0: - submit_btn[0].click() - except Exception as e: - print("登录过程中发生异常:" + traceback.format_exc()) - return False class AmazoneDriver(AmamzonBase): """亚马逊专用驱动类,继承自ZiniaoDriver""" + mark_name = "删除品牌" def SwitchPage(self): """ @@ -619,6 +77,448 @@ class AmazoneDriver(AmamzonBase): return suc +class DelbramdTask(TaskBase): + task_name = "删除品牌-TASK" + + def update_task_status(self, task_id: int, **kwargs): + """更新任务状态 + + Args: + task_id: 任务ID + **kwargs: 要更新的字段 + """ + if task_id not in runing_task: + runing_task[task_id] = {} + runing_task[task_id].update(kwargs) + + def process_task(self,task_data): + try: + data = task_data.get("data", {}) + task_id = data.get("taskId") + items = data.get("items", []) + + if not task_id: + self.log("任务ID为空,跳过", "WARNING") + return + + # 初始化任务状态 + self.update_task_status( + task_id, + status="running", + start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + total_shops=len(items), + processed_shops=0, + total_asins=sum(shop.get("totalRows", 0) for shop in items), + processed_asins=0, + success_count=0, + failed_count=0, + stop_requested=False # 暂停请求标志 + ) + + self.log(f"开始处理任务 {task_id},共 {len(items)} 个店铺") + + # 遍历处理每个店铺 + for idx, shop_data in enumerate(items, 1): + shop_name = shop_data.get("shopName", "未知店铺") + self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}") + + try: + self.process_shop(shop_data, task_id) + # 更新已处理店铺数 + if task_id in runing_task: + runing_task[task_id]["processed_shops"] += 1 + except Exception as e: + self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + + # 检查任务最终状态 + if task_id in runing_task and runing_task[task_id].get("stop_requested", False): + self.update_task_status(task_id, status="stopped") + self.log(f"任务 {task_id} 已被暂停!") + else: + self.update_task_status(task_id, status="completed") + self.log(f"任务 {task_id} 处理完成!") + + except Exception as e: + self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") + if task_id: + self.update_task_status(task_id, status="failed", error=str(e)) + + def process_shop(self, shop_data, task_id): + """处理单个店铺(包含重试逻辑) + Args: + shop_data: 店铺数据 + task_id: 任务ID + """ + shop_name = shop_data.get("shopName", "未知店铺") + result_id = shop_data.get("resultId") + countries = shop_data.get("countries", []) + company_name = shop_data.get("companyName", "未知公司") + + if not countries: + self.log(f"店铺 {shop_name} 没有国家数据,跳过", "WARNING") + return + + # 更新当前处理的店铺 + self.update_task_status(task_id, current_shop=shop_name) + + # 将店铺添加到正在执行中的店铺列表 + start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + runing_shop[shop_name] = start_time + self.log(f"账号:{company_name},店铺 {shop_name} 已添加到执行列表,开始时间: {start_time}") + + # 店铺打开重试最多3次 + max_retries = 5 + iskill = False + + + # 处理每个国家 + chunk_index = 1 + for country_data in countries: + self.log(f"开始处理国家:{country_data}") + # 检查是否收到暂停请求 + if task_id in runing_task and runing_task[task_id].get("stop_requested", False): + self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING") + break # 跳出循环,进入finally关闭店铺 + + # 打开店铺 + driver = self.open_shop(AmazoneDriver,max_retries=max_retries, company_name=company_name, + shop_name=shop_name, iskill=iskill) + + # 检查是否成功打开 + if not driver: + self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR") + return + + try: + chunk_index = self.process_country(driver, country_data, task_id, result_id, shop_data, chunk_index) + except Exception as e: + country_name = country_data.get("country", "未知") + self.log(f"处理国家 {country_name} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + self.log(traceback.format_exc(), "ERROR") + if "与页面的连接已断开" in str(e): + iskill = True + + try: + driver.close_store() + except Exception as e: + iskill = True + self.log(f"关闭国家失败,{e}") + + # 从正在执行中的店铺列表中移除 + if shop_name in runing_shop: + del runing_shop[shop_name] + self.log(f"店铺 {shop_name} 已从执行列表中移除") + + def process_country(self, driver: AmazoneDriver, country_data, task_id: int, + result_id: int, shop_data, chunk_index: int): + """处理单个国家的所有ASIN + + Args: + driver: 亚马逊驱动实例 + country_data: 国家数据 + task_id: 任务ID + result_id: 结果ID + shop_data: 店铺数据 + """ + country = country_data.get("country", "未知") + items = country_data.get("items", []) + shop_name = shop_data.get("shopName", "未知店铺") + + + self.log(f"开始处理国家: {country},共 {len(items)} 个ASIN") + self.update_task_status(task_id, current_country=country) + + max_retries = 5 + switch_success, switch_success_pg, search_success, sku_ls = self.action_init(driver, country, shop_name, + None) + # 如果切换失败,直接返回 + if not switch_success or not switch_success_pg or not search_success: + error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country} 失败,已重试 {max_retries} 次,跳过该国家" + self.log(error_message, "ERROR") + if task_id in runing_task: + runing_task[task_id]["processed_countries"] += 1 + return chunk_index + + # 处理每个ASIN + file_key = shop_data.get("fileKey", "") + source_filename = shop_data.get("sourceFilename", "") + total_rows = shop_data.get("totalRows", 0) + + for idx, asin_item in enumerate(items, 1): + # 检查是否收到暂停请求 + if task_id in runing_task and runing_task[task_id].get("stop_requested", False): + self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING") + return # 返回到上层,会触发店铺关闭 + + try: + self.log(f"[{idx}/{len(items)}] 处理ASIN: {asin_item.get('asin', '')}") + self.process_asin(driver, asin_item, country, task_id, result_id, + file_key, source_filename, total_rows, chunk_index) + except Exception as e: + asin = asin_item.get("asin", "未知") + self.log(f"处理ASIN {asin} 失败: {str(e)}", "ERROR") + # 继续处理下一个ASIN + chunk_index += 1 + + return chunk_index + + def process_asin(self, driver: AmazoneDriver, asin_item, + country: str, task_id: int, result_id: int, file_key: str, + source_filename: str, total_rows: int, chunk_index: int): + """处理单个ASIN并回传结果 + + Args: + driver: 亚马逊驱动实例 + asin_item: ASIN数据项 + country: 国家名称 + task_id: 任务ID + result_id: 结果ID + file_key: 文件KEY + source_filename: 源文件名 + total_rows: 总行数 + chunk_index: 当前处理索引 + """ + asin = asin_item.get("asin", "") + row_index = asin_item.get("rowIndex", 0) + + # 更新当前处理的ASIN + self.update_task_status(task_id, current_asin=asin) + + status = "失败" + max_retries = 3 # 最多重试3次 + + for retry in range(max_retries): + try: + self.log(f"处理ASIN {asin} (第 {retry + 1}/{max_retries} 次)") + + # 如果不是第一次尝试,先刷新页面 + if retry > 0: + self.log("重试前刷新页面...") + try: + driver.tab.refresh() + time.sleep(3) + except Exception as e: + self.log(f"刷新页面失败: {str(e)}", "WARNING") + + # 搜索ASIN + sku_ls = driver.search(asin=asin) + self.log(f"搜索到 {len(sku_ls)} 个SKU") + + if len(sku_ls) == 0: + status = "查询不到" + self.log(f"ASIN {asin} 未找到商品", "WARNING") + break # 查询不到商品,无需重试 + else: + # 删除所有找到的SKU,如果任何一个失败则重新开始整个流程 + success_count = 0 + all_success = True # 标记是否所有SKU都删除成功 + total_sku_count = len(sku_ls) + + for sku in sku_ls: + try: + suc = driver.del_action(sku) + if suc: + success_count += 1 + self.log(f"SKU 删除成功 ({success_count}/{total_sku_count})") + else: + self.log(f"SKU 删除失败", "WARNING") + all_success = False + break # 任何一个失败,退出循环,准备重试整个流程 + except Exception as e: + self.log(f"删除SKU异常: {str(e)}", "ERROR") + all_success = False + break # 发生异常,退出循环,准备重试整个流程 + + # 判断删除结果 + if all_success and success_count == total_sku_count: + status = "成功" + self.log(f"ASIN {asin} 所有SKU删除成功 ({success_count}/{total_sku_count})") + # 更新成功计数 + if task_id in runing_task: + runing_task[task_id]["success_count"] += 1 + break # 全部成功,跳出重试循环 + + else: + # 有失败的SKU + if retry < max_retries - 1: + self.log(f"有SKU删除失败,准备重试整个流程... ({retry + 1}/{max_retries})") + time.sleep(2) + continue # 继续下一次重试 + else: + # 所有重试都用完了 + if success_count > 0: + status = "部分成功" + self.log(f"ASIN {asin} 部分SKU删除成功 ({success_count}/{total_sku_count})", "WARNING") + if task_id in runing_task: + runing_task[task_id]["success_count"] += 1 + else: + status = "失败" + self.log(f"ASIN {asin} 所有SKU删除失败", "ERROR") + if task_id in runing_task: + runing_task[task_id]["failed_count"] += 1 + + except Exception as e: + status = "删除异常" + self.log(f"处理ASIN {asin} 异常: {str(e)}", "ERROR") + + # 如果还有重试机会,继续重试 + if retry < max_retries - 1: + self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})") + time.sleep(2) + else: + # 所有重试都失败了,更新失败计数 + if task_id in runing_task: + runing_task[task_id]["failed_count"] += 1 + + # 更新已处理ASIN计数 + if task_id in runing_task: + runing_task[task_id]["processed_asins"] += 1 + + # 回传结果到API + try: + payload = { + "submissionId": "", + "files": [{ + "fileKey": file_key, + "sourceFilename": source_filename, + "chunkIndex": chunk_index, + "chunkTotal": total_rows, + "processedRows": chunk_index, + "totalRows": total_rows, + "currentCountry": country, + "currentAsin": asin, + "countries": [{ + "country": country, + "items": [{ + "asin": asin, + "status": status + }] + }] + }] + } + + self.post_result(task_id, payload) + self.log(f"ASIN {asin} 结果已回传,状态: {status}") + + except Exception as e: + self.log(f"回传结果失败: {str(e)}", "ERROR") + + def _report_all_asins_failed(self, country: str, items, task_id, shop_data, chunk_index): + """将国家下所有ASIN标记为失败并回传 + + Args: + country: 国家名称 + items: ASIN列表 + task_id: 任务ID + shop_data: 店铺数据 + """ + self.log(f"开始回传国家 {country} 下的 {len(items)} 个ASIN失败状态") + + file_key = shop_data.get("fileKey", "") + source_filename = shop_data.get("sourceFilename", "") + total_rows = shop_data.get("totalRows", 0) + + for asin_item in items: + asin = asin_item.get("asin", "") + + try: + # 更新失败计数 + if task_id in runing_task: + runing_task[task_id]["failed_count"] += 1 + runing_task[task_id]["processed_asins"] += 1 + + # 回传失败状态 + payload = { + "submissionId": "", + "files": [{ + "fileKey": file_key, + "sourceFilename": source_filename, + "chunkIndex": chunk_index, + "chunkTotal": total_rows, + "processedRows": chunk_index, + "totalRows": total_rows, + "currentCountry": country, + "currentAsin": asin, + "countries": [{ + "country": country, + "items": [{ + "asin": asin, + "status": "失败" + }] + }] + }] + } + + self.post_result(task_id, payload) + chunk_index += 1 + self.log(f"ASIN {asin} 失败状态已回传") + + except Exception as e: + self.log(f"回传ASIN {asin} 失败状态时出错: {str(e)}", "ERROR") + + return chunk_index + + def post_result(self, task_id: int, payload): + """回传结果到API(带重试机制) + + Args: + task_id: 任务ID + payload: 结果数据 + """ + url = f"{DELETE_BRAND_API_BASE}/api/delete-brand/tasks/{task_id}/result" + max_retries = 3 # 最多重试3次 + + for retry in range(max_retries): + try: + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False # 忽略SSL证书验证 + ) + print("【结果提交】:", payload) + print("【结果提交返回】:", response.text) + + if response.status_code == 200: + self.log(f"结果回传成功: {url}") + return # 成功后直接返回,不再重试 + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + # 如果还有重试机会,继续重试 + if retry < max_retries - 1: + self.log(f"准备重试... ({retry + 1}/{max_retries})") + time.sleep(2) # 等待2秒后重试 + else: + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + # 如果还有重试机会,继续重试 + if retry < max_retries - 1: + self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})") + time.sleep(2) # 等待2秒后重试 + else: + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + + def update_task_status(self, task_id: int, **kwargs): + """更新任务状态 + + Args: + task_id: 任务ID + **kwargs: 要更新的字段 + """ + if task_id not in runing_task: + runing_task[task_id] = {} + + runing_task[task_id].update(kwargs) + + + if __name__ == '__main__': # 使用示例 user_info = { diff --git a/app/amazon/detail_spider.py b/app/amazon/detail_spider.py index ab5027e..84fb739 100644 --- a/app/amazon/detail_spider.py +++ b/app/amazon/detail_spider.py @@ -1,88 +1,23 @@ import json -import sys import os -import io -# sys.stdout.reconfigure(encoding='utf-8') import time -import re import traceback from datetime import datetime from DrissionPage import Chromium, ChromiumOptions from collections import defaultdict import requests -from amazon.del_brand import AmamzonBase, kill_process from amazon.tool import show_notification,get_shop_info,remove_special_characters,split_currency_values +from amazon.amazon_base import TaskBase +from amazon.chrome_base import ChromeAmzoneBase + from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE -class ChromeAmzone: +class ChromeAmzone(ChromeAmzoneBase): mark_name = "亚马逊详情采集" - country_info = { - "英国": { - "url": "https://www.amazon.co.uk/dp/B0CJ8SNXXV", - "zip_code": "SW1A 1AA", - "mark": "SW1A 1" - }, - "德国": { - "url": "https://www.amazon.de/dp/B0CC8CW9G2?th=1", - "zip_code": "10115" - }, - "法国": { - "url": "https://www.amazon.fr/dp/B0FRG1MJ8H?th=1", - "zip_code": "75001" - }, - "西班牙": { - "url": "https://www.amazon.es/dp/B08ZXVNYNN", - "zip_code": "28001" - }, - "意大利": { - "url": "https://www.amazon.it/dp/B0D1P17T2Q", - "zip_code": "20121" - } - } - - def __init__(self): - """ - 杀死当前谷歌浏览器进程,并使用 drissionpage 启动谷歌浏览器,使用系统安装的浏览器默认用户文件夹 - """ - # 杀死现有的Chrome进程 - print("正在关闭现有的chromium浏览器进程...") - os.system('taskkill /f /t /im chrome.exe') - time.sleep(2) - - print("正在启动chromium浏览器...") - # 配置浏览器选项 - co = ChromiumOptions() - user_data_path = os.path.join(base_dir, "user_data", "chrome_data") - if not os.path.exists(user_data_path): - os.makedirs(user_data_path, exist_ok=True) - co.set_user_data_path(user_data_path) - co.set_local_port(port=19897) - self.browser = Chromium(co) - self.tab = self.browser.latest_tab - print("Chrome浏览器启动成功") - - def close_init_popup(self): - """ - 关闭所有的初始化弹窗 - """ - self.tab.wait.doc_loaded(timeout=60, raise_err=True) - footbar = self.tab.eles('xpath://footer[@class="el-dialog__footer"]', timeout=5) - if len(footbar) > 0: - do_not_remind = footbar[0].eles('xpath:.//input[@class="el-checkbox__original"]') - if len(do_not_remind) > 0: - do_not_remind[0].check() - resume_immediately = footbar[0].eles('xpath:.//button') - if len(resume_immediately) > 0: - resume_immediately[0].click() - - self.tab.wait.doc_loaded(timeout=60, raise_err=True) - accept_btn = self.tab.eles('xpath://input[@id="sp-cc-accept"]', timeout=5) - if len(accept_btn) > 0: - accept_btn[0].click() def run(self, country, asin): """ @@ -99,7 +34,7 @@ class ChromeAmzone: # 验证国家是否支持 if country not in self.country_info: error_msg = f"不支持的国家: {country},支持的国家有: {list(self.country_info.keys())}" - print(error_msg) + self.log(error_msg) show_notification(error_msg, "error") return None @@ -114,7 +49,7 @@ class ChromeAmzone: domain = base_url.split("/dp/")[0] # 拼接新的URL product_url = f"{domain}/dp/{asin}" - print(f"正在访问: {product_url}") + self.log(f"正在访问: {product_url}") # 打开链接 self.tab.get(product_url) @@ -123,13 +58,13 @@ class ChromeAmzone: self.close_init_popup() # 2. 切换国家/设置邮编 - print(f"正在检查并设置邮编: {zip_code},标识: {mark}") + self.log(f"正在检查并设置邮编: {zip_code},标识: {mark}") self._set_zip_code(zip_code, mark) # self.tab.wait.doc_loaded(timeout=5, raise_err=False) # 3. 抓取数据 - print("正在抓取商品数据...") + self.log("正在抓取商品数据...") data = self._scrape_data() # 添加基本信息 @@ -138,100 +73,15 @@ class ChromeAmzone: data['url'] = product_url data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - print(f"数据抓取完成: {json.dumps(data)}") + self.log(f"数据抓取完成: {json.dumps(data)}") return data except Exception as e: error_msg = f"运行出错: {traceback.format_exc()}" - print(error_msg) + self.log(error_msg) show_notification(f"采集失败: {str(e)}", "error") return {} - def _set_zip_code(self, zip_code, mark=None): - """ - 设置邮编 - - Args: - zip_code: 目标邮编 - """ - try: - # 检查当前邮编 - zip_display = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) - - if zip_display: - current_text = zip_display.text - # print(f"当前地址信息: {current_text}") - # 先检查标识 - if mark is not None and mark in current_text: - print(f"邮编检测到标识: {mark},无需修改") - return True - # 检查是否已经包含目标邮编 - if zip_code in current_text: - print(f"邮编已经设置为: {zip_code},无需修改") - return True - - # 需要设置邮编 - print(f"正在设置邮编为: {zip_code}") - - # 点击地址选择按钮 - location_link = self.tab.ele('xpath://a[@id="nav-global-location-popover-link"]', timeout=10) - if not location_link: - print("找不到地址设置按钮") - return False - - location_link.click() - time.sleep(1) - - # 等待邮编输入框出现 - zip_input = self.tab.ele('xpath://input[@id="GLUXZipUpdateInput"]', timeout=10) - if not zip_input: - print("找不到邮编输入框") - return False - - # 输入邮编 - zip_input.input(zip_code, clear=True) - time.sleep(0.5) - - # 点击提交按钮 - submit_btn = self.tab.ele('xpath://input[@aria-labelledby="GLUXZipUpdate-announce"]', timeout=10) - if not submit_btn: - print("找不到提交按钮") - return False - - submit_btn.click() - - continue_btn = self.tab.eles('xpath://div[@class="a-popover-footer"]//input[@id="GLUXConfirmClose"]', - timeout=10) - if len(continue_btn) > 0: - continue_btn[0].click() - - # 等待提交按钮消失(表示请求已发送) - print("等待邮编更新...") - time.sleep(2) - - # 等待页面加载完成 - self.tab.wait.doc_loaded(timeout=30, raise_err=False) - time.sleep(2) - - # 验证邮编是否设置成功 - zip_display_after = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) - if zip_display_after: - updated_text = zip_display_after.text - # print(f"更新后的地址信息: {updated_text}") - - if zip_code in updated_text: - print(f"邮编设置成功: {zip_code}") - return True - else: - # print(f"邮编设置可能失败,当前显示: {updated_text}") - return False - - return True - - except Exception as e: - print(f"设置邮编时出错: {traceback.format_exc()}") - return False - def _scrape_data(self): """ 抓取商品数据 @@ -256,39 +106,16 @@ class ChromeAmzone: return data except Exception as e: - print(f"抓取数据时出错: {traceback.format_exc()}") + self.log(f"抓取数据时出错: {traceback.format_exc()}") return data - def close(self): - """关闭浏览器""" - try: - if self.browser: - self.browser.quit() - print("浏览器已关闭") - except Exception as e: - print(f"关闭浏览器时出错: {str(e)}") -class SpiderTask: - mark_name = "亚马逊采集" - - def __init__(self, user_info: dict = None): - """初始化审批任务处理器 - - Args: - user_info: 用户信息字典,包含 company, username, password - """ - self.user_info = user_info or {} - self.running = True - - def log(self, message: str, level: str = "INFO"): - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if level == "ERROR": - show_notification(message, "error") - print(f"[{timestamp}] [PriceTask] [{level}] {message}") +class SpiderTask(TaskBase): + task_name = "亚马逊采集" @staticmethod - def group_by_id_prefix(data): + def group_by_id_prefix(data): """ 根据每条数据的 id 字段进行分组: - 如果 id 为 "2_1",则取 "_" 前面的 "2" 作为分组 key @@ -304,35 +131,35 @@ class SpiderTask: item_id = str(item.get("id", "")) group_key = item_id.split("_")[0] grouped[group_key].append(item) - - return list(grouped.values()) - - @staticmethod - def normalize_groups(data): - groups = data.get("groups") - if isinstance(groups, list) and len(groups) > 0: - return groups - - rows = data.get("rows") or data.get("items") or [] - if not isinstance(rows, list) or len(rows) == 0: - return [] - - normalized = [] - for items in SpiderTask.group_by_id_prefix(rows): - first = items[0] if items else {} - item_id = str(first.get("id") or first.get("displayId") or "") - base_id = item_id.split("_")[0] if item_id else "" - normalized.append({ - "sourceFileKey": first.get("sourceFileKey", ""), - "sourceFilename": first.get("sourceFilename", ""), - "groupKey": first.get("groupKey") or base_id, - "baseId": first.get("baseId") or base_id, - "displayId": first.get("displayId") or item_id, - "items": items, - }) - return normalized - - def process_task(self, task_data: dict): + + return list(grouped.values()) + + @staticmethod + def normalize_groups(data): + groups = data.get("groups") + if isinstance(groups, list) and len(groups) > 0: + return groups + + rows = data.get("rows") or data.get("items") or [] + if not isinstance(rows, list) or len(rows) == 0: + return [] + + normalized = [] + for items in SpiderTask.group_by_id_prefix(rows): + first = items[0] if items else {} + item_id = str(first.get("id") or first.get("displayId") or "") + base_id = item_id.split("_")[0] if item_id else "" + normalized.append({ + "sourceFileKey": first.get("sourceFileKey", ""), + "sourceFilename": first.get("sourceFilename", ""), + "groupKey": first.get("groupKey") or base_id, + "baseId": first.get("baseId") or base_id, + "displayId": first.get("displayId") or item_id, + "items": items, + }) + return normalized + + def process_task(self, task_data: dict): """处理审批任务主入口 Args: @@ -341,7 +168,7 @@ class SpiderTask: try: data = task_data.get("data", {}) task_id = data.get("taskId") - groups = self.normalize_groups(data) + groups = self.normalize_groups(data) # 用于测试 limit = data.get("limit", None) @@ -353,11 +180,10 @@ class SpiderTask: self.log(f"开始处理爬取任务 {task_id},{len(groups)} 个任务") - if not groups: - self.log("appearance-patent groups/rows is empty, skip", "WARNING") - return - - from config import runing_task + if not groups: + self.log("appearance-patent groups/rows is empty, skip", "WARNING") + return + runing_task[task_id] = { "status": "running", "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), @@ -383,12 +209,12 @@ class SpiderTask: try: # 数据整理 # new_items = self.group_by_id_prefix(items) - result = [] - for gp_index,gp in enumerate(groups): - items = gp.get("items", []) - return_data = {} - - for index,value in enumerate(items): + result = [] + for gp_index,gp in enumerate(groups): + items = gp.get("items", []) + return_data = {} + + for index,value in enumerate(items): return_data = { 'image_url': "", @@ -396,18 +222,17 @@ class SpiderTask: } asin = value.get("asin") country = value.get("country") - return_data = None for _ in range(max_retry): try: - return_data = chrome.run(country, asin) or {} + return_data = chrome.run(country, asin) or {} self.log(f"抓取结果->{return_data}") break except Exception as e: if "与页面的连接已断开" in str(e): chrome = ChromeAmzone() - if not isinstance(return_data, dict): - return_data = {} - if return_data.get("image_url"): + if not isinstance(return_data, dict): + return_data = {} + if return_data.get("image_url"): break group_item = [ @@ -457,7 +282,6 @@ class SpiderTask: if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 except Exception as e: - import traceback self.log(f"处理店铺 {task_id} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -471,10 +295,8 @@ class SpiderTask: self.log(f"任务 {task_id} 处理完成!") except Exception as e: - import traceback self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") if task_id: - from config import runing_task if task_id in runing_task: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) @@ -488,7 +310,7 @@ class SpiderTask: url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result" payload ={ - "submissionId": f"{int(time.time())}", + "submissionId": f"{task_id}", "chunkIndex": chunkIndex, "chunkTotal": chunkTotal, "error": error, @@ -8413,4 +8235,4 @@ if __name__ == '__main__': ] # spide.process_task(task_data) res = SpiderTask.group_by_id_prefix(task_data) - print(res) + print(res) diff --git a/app/amazon/detail_spider_备份.py b/app/amazon/detail_spider_备份.py new file mode 100644 index 0000000..39ad845 --- /dev/null +++ b/app/amazon/detail_spider_备份.py @@ -0,0 +1,8384 @@ +import json +import sys +import os +import io +# sys.stdout.reconfigure(encoding='utf-8') + +import time +import re +import traceback +from datetime import datetime +from DrissionPage import Chromium, ChromiumOptions +from collections import defaultdict +import requests + +from amazon.del_brand import AmamzonBase, kill_process +from amazon.tool import show_notification,get_shop_info,remove_special_characters,split_currency_values + +from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE + + +class ChromeAmzone: + mark_name = "亚马逊详情采集" + country_info = { + "英国": { + "url": "https://www.amazon.co.uk/dp/B0CJ8SNXXV", + "zip_code": "SW1A 1AA", + "mark": "SW1A 1" + }, + "德国": { + "url": "https://www.amazon.de/dp/B0CC8CW9G2?th=1", + "zip_code": "10115" + }, + "法国": { + "url": "https://www.amazon.fr/dp/B0FRG1MJ8H?th=1", + "zip_code": "75001" + }, + "西班牙": { + "url": "https://www.amazon.es/dp/B08ZXVNYNN", + "zip_code": "28001" + }, + "意大利": { + "url": "https://www.amazon.it/dp/B0D1P17T2Q", + "zip_code": "20121" + } + } + + def __init__(self): + """ + 杀死当前谷歌浏览器进程,并使用 drissionpage 启动谷歌浏览器,使用系统安装的浏览器默认用户文件夹 + """ + # 杀死现有的Chrome进程 + print("正在关闭现有的chromium浏览器进程...") + os.system('taskkill /f /t /im chrome.exe') + time.sleep(2) + + print("正在启动chromium浏览器...") + # 配置浏览器选项 + co = ChromiumOptions() + user_data_path = os.path.join(base_dir, "user_data", "chrome_data") + if not os.path.exists(user_data_path): + os.makedirs(user_data_path, exist_ok=True) + co.set_user_data_path(user_data_path) + co.set_local_port(port=19897) + self.browser = Chromium(co) + self.tab = self.browser.latest_tab + print("Chrome浏览器启动成功") + + def close_init_popup(self): + """ + 关闭所有的初始化弹窗 + """ + self.tab.wait.doc_loaded(timeout=60, raise_err=True) + footbar = self.tab.eles('xpath://footer[@class="el-dialog__footer"]', timeout=5) + if len(footbar) > 0: + do_not_remind = footbar[0].eles('xpath:.//input[@class="el-checkbox__original"]') + if len(do_not_remind) > 0: + do_not_remind[0].check() + resume_immediately = footbar[0].eles('xpath:.//button') + if len(resume_immediately) > 0: + resume_immediately[0].click() + + self.tab.wait.doc_loaded(timeout=60, raise_err=True) + accept_btn = self.tab.eles('xpath://input[@id="sp-cc-accept"]', timeout=5) + if len(accept_btn) > 0: + accept_btn[0].click() + + def run(self, country, asin): + """ + 运行亚马逊详情采集任务 + + Args: + country: 国家名称(如:英国、德国、法国、西班牙、意大利) + asin: 亚马逊商品ASIN码 + + Returns: + dict: 包含采集到的数据 + """ + try: + # 验证国家是否支持 + if country not in self.country_info: + error_msg = f"不支持的国家: {country},支持的国家有: {list(self.country_info.keys())}" + print(error_msg) + show_notification(error_msg, "error") + return None + + # 获取国家配置 + country_config = self.country_info[country] + zip_code = country_config["zip_code"] + mark = country_config.get("mark") + + # 1. 根据国家和ASIN拼接链接 + base_url = country_config["url"] + # 提取域名部分 + domain = base_url.split("/dp/")[0] + # 拼接新的URL + product_url = f"{domain}/dp/{asin}" + print(f"正在访问: {product_url}") + + # 打开链接 + self.tab.get(product_url) + time.sleep(3) # 等待页面初步加载 + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + + self.close_init_popup() + # 2. 切换国家/设置邮编 + print(f"正在检查并设置邮编: {zip_code},标识: {mark}") + self._set_zip_code(zip_code, mark) + + # self.tab.wait.doc_loaded(timeout=5, raise_err=False) + + # 3. 抓取数据 + print("正在抓取商品数据...") + data = self._scrape_data() + + # 添加基本信息 + data['country'] = country + data['asin'] = asin + data['url'] = product_url + data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + print(f"数据抓取完成: {json.dumps(data)}") + return data + + except Exception as e: + error_msg = f"运行出错: {traceback.format_exc()}" + print(error_msg) + show_notification(f"采集失败: {str(e)}", "error") + return {} + + def _set_zip_code(self, zip_code, mark=None): + """ + 设置邮编 + + Args: + zip_code: 目标邮编 + """ + try: + # 检查当前邮编 + zip_display = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) + + if zip_display: + current_text = zip_display.text + # print(f"当前地址信息: {current_text}") + # 先检查标识 + if mark is not None and mark in current_text: + print(f"邮编检测到标识: {mark},无需修改") + return True + # 检查是否已经包含目标邮编 + if zip_code in current_text: + print(f"邮编已经设置为: {zip_code},无需修改") + return True + + # 需要设置邮编 + print(f"正在设置邮编为: {zip_code}") + + # 点击地址选择按钮 + location_link = self.tab.ele('xpath://a[@id="nav-global-location-popover-link"]', timeout=10) + if not location_link: + print("找不到地址设置按钮") + return False + + location_link.click() + time.sleep(1) + + # 等待邮编输入框出现 + zip_input = self.tab.ele('xpath://input[@id="GLUXZipUpdateInput"]', timeout=10) + if not zip_input: + print("找不到邮编输入框") + return False + + # 输入邮编 + zip_input.input(zip_code, clear=True) + time.sleep(0.5) + + # 点击提交按钮 + submit_btn = self.tab.ele('xpath://input[@aria-labelledby="GLUXZipUpdate-announce"]', timeout=10) + if not submit_btn: + print("找不到提交按钮") + return False + + submit_btn.click() + + continue_btn = self.tab.eles('xpath://div[@class="a-popover-footer"]//input[@id="GLUXConfirmClose"]', + timeout=10) + if len(continue_btn) > 0: + continue_btn[0].click() + + # 等待提交按钮消失(表示请求已发送) + print("等待邮编更新...") + time.sleep(2) + + # 等待页面加载完成 + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + time.sleep(2) + + # 验证邮编是否设置成功 + zip_display_after = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10) + if zip_display_after: + updated_text = zip_display_after.text + # print(f"更新后的地址信息: {updated_text}") + + if zip_code in updated_text: + print(f"邮编设置成功: {zip_code}") + return True + else: + # print(f"邮编设置可能失败,当前显示: {updated_text}") + return False + + return True + + except Exception as e: + print(f"设置邮编时出错: {traceback.format_exc()}") + return False + + def _scrape_data(self): + """ + 抓取商品数据 + + Returns: + dict: 抓取到的数据 + """ + data = { + 'image_url': "", + 'title': "" + } + + try: + # 等待页面加载 + # time.sleep(3) + title_ele = self.tab.ele('xpath://h1[@id="title"]',timeout=30) + title = title_ele.text + data["title"] = title + imge_ele = self.tab.ele('xpath://div[@id="imgTagWrapperId"]//img',timeout=20) + image_url = imge_ele.attr("src") + data["image_url"] = image_url + return data + + except Exception as e: + print(f"抓取数据时出错: {traceback.format_exc()}") + return data + + def close(self): + """关闭浏览器""" + try: + if self.browser: + self.browser.quit() + print("浏览器已关闭") + except Exception as e: + print(f"关闭浏览器时出错: {str(e)}") + + +class SpiderTask: + mark_name = "亚马逊采集" + + def __init__(self, user_info: dict = None): + """初始化审批任务处理器 + + Args: + user_info: 用户信息字典,包含 company, username, password + """ + self.user_info = user_info or {} + self.running = True + + def log(self, message: str, level: str = "INFO"): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if level == "ERROR": + show_notification(message, "error") + print(f"[{timestamp}] [PriceTask] [{level}] {message}") + + @staticmethod + def group_by_id_prefix(data): + """ + 根据每条数据的 id 字段进行分组: + - 如果 id 为 "2_1",则取 "_" 前面的 "2" 作为分组 key + - 如果 id 为 "1",则分组 key 就是 "1" + - 相同 key 的数据归为同一组 + + :param data: 原始列表数据 + :return: 二维列表,按 id 前缀分组 + """ + grouped = defaultdict(list) + + for item in data: + item_id = str(item.get("id", "")) + group_key = item_id.split("_")[0] + grouped[group_key].append(item) + + return list(grouped.values()) + + def process_task(self, task_data: dict): + """处理审批任务主入口 + + Args: + task_data: 任务数据 + """ + try: + data = task_data.get("data", {}) + task_id = data.get("taskId") + groups = data.get("groups") + + # 用于测试 + limit = data.get("limit", None) + + if not task_id: + self.log("任务ID为空,跳过", "WARNING") + return + + + self.log(f"开始处理爬取任务 {task_id},{len(groups)} 个任务") + + from config import runing_task + runing_task[task_id] = { + "status": "running", + "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "total_shops": 1, + "processed_shops": 0, + "total_countries": len(groups) , + "processed_countries": 0, + "total_asins": 0, + "processed_asins": 0, + "success_count": 0, + "failed_count": 0, + "stop_requested": False + } + + # 检查是否收到暂停请求 + if task_id in runing_task and runing_task[task_id].get("stop_requested", False): + self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING") + runing_task[task_id]["status"] = "stopped" + return + max_retry = 3 + show_notification(f"开始爬取数据", "info") + chrome = ChromeAmzone() + try: + # 数据整理 + # new_items = self.group_by_id_prefix(items) + result = [] + for gp_index,gp in enumerate(groups): + items = gp.get("items", []) + + for index,value in enumerate(items): + + return_data = { + 'image_url': "", + 'title': "" + } + asin = value.get("asin") + country = value.get("country") + # return_data = None + for _ in range(max_retry): + try: + return_data = chrome.run(country, asin) + self.log(f"抓取结果->{return_data}") + break + except Exception as e: + if "与页面的连接已断开" in str(e): + chrome = ChromeAmzone() + if return_data.get("image_url"): + break + + group_item = [ + { + "sourceFileKey": i.get("sourceFileKey"), + "sourceFilename": i.get("sourceFilename"), + "rowToken": i.get("rowToken"), + "groupKey": i.get("groupKey"), + "id": i.get("id"), + "asin": i.get("asin"), + "country": i.get("country"), + "url": return_data.get("image_url"), + "title": return_data.get("title"), + } + for i in items + ] + # task_id: int, chunkIndex:int,chunkTotal: int, country_code: str, asin: str, status: dict,error:str="", + # item_data:dict={}, + res = { + "sourceFileKey": gp.get("sourceFileKey"), + "sourceFilename": gp.get("sourceFilename"), + "groupKey": gp.get("groupKey"), + "baseId": gp.get("baseId"), + "displayId": gp.get("displayId"), + "items": group_item + } + print("================") + result.append(res) + print("================") + is_done = gp_index == len(groups)-1 + if len(result) > 20 or is_done: + self.post_result(task_id=task_id,chunkIndex=gp_index+1,chunkTotal=len(groups), + asin=asin,item_data=result,is_done=is_done) + result = [] + + + if len(result) > 0: + is_done = True + self.post_result(task_id=task_id, chunkIndex=len(groups), chunkTotal=len(groups), + asin=asin, item_data=result, is_done=is_done) + + try: + chrome.close() + except Exception as e: + print("退出浏览器出错",e) + # 更新已处理店铺数 + if task_id in runing_task: + runing_task[task_id]["processed_shops"] += 1 + except Exception as e: + import traceback + self.log(f"处理店铺 {task_id} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + + # 更新任务状态 + if task_id in runing_task: + if runing_task[task_id].get("stop_requested", False): + runing_task[task_id]["status"] = "stopped" + self.log(f"任务 {task_id} 已被暂停!") + else: + runing_task[task_id]["status"] = "completed" + self.log(f"任务 {task_id} 处理完成!") + + except Exception as e: + import traceback + self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") + if task_id: + from config import runing_task + if task_id in runing_task: + runing_task[task_id]["status"] = "failed" + runing_task[task_id]["error"] = str(e) + + def post_result(self, task_id: int, chunkIndex:int,chunkTotal: int, asin: str, error:str="", + item_data:dict={}, + is_done: bool = False): + """回传处理结果到API + """ + + url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result" + + payload ={ + "submissionId": f"{int(time.time())}", + "chunkIndex": chunkIndex, + "chunkTotal": chunkTotal, + "error": error, + "groups": item_data, + "done": is_done + } + + max_retries = 3 + for retry in range(max_retries): + try: + request_timeout = 300 if is_done else 30 + print("================【详情采集】=====================") + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + self.log(f"回传URL: {url}") + self.log(f"回传数据: {payload}") + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=request_timeout, + verify=False + ) + self.log(f"回传结果: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"结果回传成功: {asin} - {item_data}") + return + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + print("=====================================") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + print("=====================================") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + raise RuntimeError("已达到最大重试次数,结果回传最终失败") + +if __name__ == '__main__': + # spide = SpiderTask() + task_data = [ + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0D792ND9V", + "country": "英国", + "url": "", + "title": "低" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0D14L34S7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0CWQFZMGY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0CX87XXWJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0CR3PMR8L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0DK3L6FD4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0D9QNV6FT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0DBZG716J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B0CX8K4F8Z", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0C4KXJWW1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0D62LNNKQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0DK3K3GF5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0C1HN8GY6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0B71WMDDF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0DSZNF73N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0897Y5QDC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0FNCLNS68", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B009U2SCMW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0FBLY2ZRD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B08LP7DW3M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0FPBZLD6R", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B09LV1TNHK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0DSKS214Y", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0BZBW8KL5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0CR3LS3R7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B097TS2MNQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B09LV4WGTR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0916ZQP1V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0C9WHZ67N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0F8QKVS1D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0D49974HV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0F2DXMKVF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B08YX48NZ2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0BGM3HF9C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0CJJM9KZV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B09HDSTGT4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0CX9HK5NL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0C2BV83D3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0D5WDK7GW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B09NR1PZYD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0DC45N1Y8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B01AVHJQY2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0F3VSJ9Q2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0CLXCXW6W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B07PSPN1XH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B09PHQYR6Z", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0CT2H94WV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0FRRK9V73", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0B3RNHXV8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B09FF115TW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0CYGXDWBT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0F9FT3ZPY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0F295JTQZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0FPJZ1N51", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B08B8SP7XT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B002P7HGEO", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B09X268FXC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0170OT3X8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0859793H1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0FP8KHCPX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B09H6KR87N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0D9778Q65", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0CPJCBZR8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0CZ63NGNQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0CR4795W6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0CR45ZB5Y", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0CYB255ST", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0BMYWFKTF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B0BMYTPRD6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0DK1MGDZM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B006FUXS30", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B00BLPUXCI", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B09N79DR4C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B006O74WTI", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B006O75DIC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B09323MXYP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B00K2NTEPC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B09C19Y6B3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B00L2HK0S2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B00666DQBW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B00666FLII", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B00666GVM8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B07KWJCVVN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B00F0KAGKO", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0C695NBMR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0F2FXYCG4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0FPGC8Z84", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B0D8HXBGGB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0DDG8GJP9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0FLCZVDDH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B07WZZW6V9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0GT3SJ6J1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B077R1GRW1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0D2TMYX23", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B00AXTKYCM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0D45CB5SS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0852JR7RD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0GR9F93JL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0CCJ3NY78", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0D1V26VBG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0CN5VVWB2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0DLNJTKFB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0CSD7S964", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0GLP8L4TT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0FGJB6LLV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0DZ67MYZN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0G3XP9VYB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0FNWMRMSZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0FNWP3K72", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0DQTMRNLC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DZ62KT5N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0DZ65VCDC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0DZ67P85P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B0F6LRCZXW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0DPYVJMLK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0FDKZJTY5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B0D9VFN9QW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0BW46XZ2G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0FLCBQ92M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0F6JW61SR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0CNZ27HCF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B001DKRB8A", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B00NH5DWIW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B01IOKEOGS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B08YJVVPBK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0FG29NPDK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0FRFHFXXZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0FMQ5G18V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0DPDKD5PB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0FS6JDGNW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0DZBM5VRX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B0D4Q2KB8X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B098B3XKRS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B09F9JM8F9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B08H5M3ZQ7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0DG3NXCCF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0FJX2FSM1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B089KHDH15", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0CPWP6W5M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0D4LXTCSZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0CK682F4T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0B9CGJDBV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0DV55H9DD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0FJ8B8RJS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0D426K41D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0CTD4BF7S", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0CGXGQ43L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B0BDM3B6LY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0FS1SJGJ5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0FJ1RH4JV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0B9MBTX28", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0FV2VLSX1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0BRYKW1GD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0CT5G9YWM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0D927DZ6B", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0DSDY3G29", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0CWRRSV9H", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DNDTT9Z4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0CP5VD5SG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0BV2Q24YK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0F3JD29KW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0C818XVYT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0FJRYQ3QV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0CP3NYG8J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0F6C7XH75", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0FXX9N5VH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0CFJPCSDP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0FQJHS1MZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0D5VJN99L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B07MCDWVJG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0CKYJCJGD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0CQXPR4H4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B08S2913J7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B0CXPL3GSW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0D7JVZWZF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0FKMXP3ZR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0FGP7YJC8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0F9PDZTCJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0CTLXM3C6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0CSYB8BPK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B01I1CEEXW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0G2Y49LCY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0F3HK55L6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0GWJ69JK1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B09Z877C17", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0GLDX9NB1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0FPC1Y19B", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0C65438CP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0F6MNZB6F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0BVPLLLRP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0FK2B5FK2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0BPNFMKQV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0G4BLRDW6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0F93QCQF1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0FHHPYP9K", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0F1K9KDX6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B0F52ZXD55", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B07GDGSKK6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B08BPHJ34D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B07ZFWRH75", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0G5PVP3Y3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0FXXLPC1T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0GQB1K38X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0C1TCDZ5N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0FHD3QCLW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0F1THT9XW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DYJ8743N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0FD3Q2RGP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0BV66T3PQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0CDVYLTB4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0CNKGFWKR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0D4VQF22F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0C5TBR8LB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0DDH3G9PG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0D8VRMWP2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0BFWXWNTC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B096ZCL4JX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0CFDXR5FC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0G6DN5QC7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0CNRL9R1F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0BJK6D2D4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0DPFDTL7W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0G5N9KGXC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B0F73X9X8D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0CJTJB8PV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0FBGD3QJ2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0F98TGSQ1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0F329ZXH5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0G191SX5F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0F7XT5TNY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0FVS7K78Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0FYFZBQLT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0FG2LHWB3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0F9WX88T1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0F7XNDBNN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0DT7GV9NS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0DKF26TYN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0D1FP339J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B0CNCBQCMG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0CHRR45X7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0C1RPB8BQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0BVPPBH7D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0BHQKQMG8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B0C7QQPNC9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0FHKT5CBC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B0DR5XVMN7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B08HPS68Q4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0C4KLQPDZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0F6TT21VN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B08MFB5W4Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B07C9Z5CG5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B07589LNBP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B08HRLZVX2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B08K7B3T6P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B09T9YYYTS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B081ZVNH98", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0F6T4XMXY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B07QH4X4Y5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B096S27HMH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0FB9DWYXF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B08R58FBXZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0CRK8YNNY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B09C8F7J2H", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0F6TK2KP3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0BYJHNBWM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0DJBZXCDP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0FMRGW63W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0D175WMJC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B07FLD65NM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0DRFWYNNV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0DZX3JWQP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0F5X5G34F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0BJDL1D9W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0BYJMMMLX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B092M4RJVV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B092M49F1Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0FMRFHRZR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0DZWVVM6F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0BCNZZ32X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0BRC8PXMW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B07RL77RM9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0FMRBN7H4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0FMRGHCPK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0G4BWPCK7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B0CJJ4Y8SB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0DYY436WG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0G64PMQ94", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0CST698J6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B00U7LUWR8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0DHRJX1VS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B0CY9NB8VS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B08ML38H24", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0BXVBVJ6G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B08HS69FQG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0F281WHDB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0007RN382", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0F2JDR3Y5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0FX9L2JJ4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0FD9NVNFT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0B72HD5B9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0F53FQ9Y4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0GHDHPQ7J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B07B9ZP8TK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0DPWW9P2V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0CR43DTKJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0G8K4C3D6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0F52TLKVM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0BGH6Y6KB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0D28VG1C3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0D3LLKF7C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B09KT2VBJL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B07F6P6C3F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0GCZH5V2P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0FH5RNFZQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0CSFMDTSR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0GR9TS4N8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0D9GWVPGJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0GF2BKHJK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0GQZ8SPZZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0D3LQF6P5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0GF2ML2NY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0C28BCVH6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0C1CRZPMS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0F8HKX897", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B0GSKSC1KR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0GSQGF76K", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0F9WTNVXP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0F47S987P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0F4Y4F5TC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0DYNW29NB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0GF29HX77", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0GM2WBVXF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0GGWW658M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0F32R7JTF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DZ67TBHQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0CLC6KSZH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0F8QW1FS1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0F6LHFVJ3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0DZH72GF6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B099N1FB4K", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0BS678GBK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0F5Y4KSQJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0D584KC9W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0GK1D75ZF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0DPWXNWV5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0G64ZCGTV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B07VHK16YV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0GS3QV5F2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0DC7TR9B5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FH634932", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0CY2XN6JF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0GKR5WMXK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0GH4KYZH5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0FD76KJ9P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0836NDGM2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0DYJWTS4N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B089X4VCPY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0CQZR1LZX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0GSVTDQBX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0D9TK3VMD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0GDZWHF6D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0C8N1LZT9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0GWHQHKJS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0BTTJKZDC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0DZPDJ7Q4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0D5W9F2DS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0CGCPLWV1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B09VT58BM4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B07XZT797H", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B0GW9TR89X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B09ZKT7QW6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0CJYRW7RL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0DSZ4V9PJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B09TKN8F9W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B09SW82GSP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B001F4OSJY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B091BK7ZF3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B07HSL53DY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B07HSDTYC8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0FKN7JZ6B", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B071XJDSBK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B006UITUJ8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0CBPWL6NZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0G3Q9B48L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0BYJV8P98", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0G8DTQP1F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0CYP192B6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0CCN8TJCD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0FWKJVMNK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0GD11NDH2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0BC2NFJBP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0D9RRCC93", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0DLSZFMM2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0GFBG4HSR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B07TBCFGPT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0GSZRFZGZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B008OB1AZ6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B09XLVPYPR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0FZK5NCWK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0DYJTRT6K", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0CL664TZX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0DN6XQ5W4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B002HNNWLM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0F5HHHR65", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0G5739C1J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0G11TTQPT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0G579K29G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0G57DKM85", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DGFW14PB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0DK3YXKS5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0FGTHRS1R", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0FP2FDZPZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B002HO7QQ8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B002HNS1TU", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B004KL50HS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B07QK3B4Q5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B08DVGHKLL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B08K3ZYRGC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0DZWVCVK1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0CT29V1D5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B01N7G9ALO", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0CQLN3T6N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FJKTYHSV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0FRFNVTKX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0DJQWP543", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0D7M543CT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0F21QQQL1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0CMZR1JJD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0F66NTL6T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0D976JCLF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0FP2DMFMG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0CTDM36PB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0DY76XFLY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0CQ4PS256", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12_1", + "asin": "B0C15RDVDK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0F5Y8Y58T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0B5CBKC17", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0FMXVRQM2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0DC2WX8Q2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0FVR6L7NP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0DFQMCBS7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B07TSMHP3T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B09DY2KCLB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0GRTXKMGZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B0BG4TTWWW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FC2QBR9T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B09Y2H27PS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B0CTGLQFCD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B01MCVO50E", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0FJXM3T1S", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0C8MG6NF7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0C7NK648Y", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0DYFDXD8N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0GGMLRZWF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0D45VVD1Y", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0CC6KP279", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0FRZYQYGT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0DK447HBK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0788GRQT1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0F93QPPVD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0BRQG6CKG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B09DCW6XN1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0FSQDHC9V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0D25DHV1T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0DJF4Z91G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0F3J3B1R2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0F3J6WJZJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FQTWP8GM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0FPQL1ZW9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0B77PQ12W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0FXWTDY16", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0D6ZV5F22", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0BF13CW1W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0F52CM3X1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0043RJSSG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B07V2G46J7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B07X4V22PW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0GQXDVW6C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0BK888623", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0BFWC5SZQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B0F1C9ZQKD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B09F5VV8DW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0BWMT9LFC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0FJLQLRNT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B08ZCNVYTK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0CMZLKY9T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0GJZXZ2F9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B07BMFCKCR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0DK46T977", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B08HCPX1MB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0D8L6WRCM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B09NDN8NV1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0GT2527XG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0FNRCLHNS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0DQS8GKLG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B003NGLFYI", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0CR4FTMGG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B0DK9ZBBKS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0D1XX28FN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B003KHEEY8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0F9WRKC84", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B08BHKGYJ7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0FT1562DX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0F8QRJ9JB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0070OARSO", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B07XJ4W6SW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0C1V86KMX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0CGM1Z2VG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B004699SWM", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0GT281VQK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B09BCSR7NQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B003YCKTIE", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B092CGQN2K", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30_1", + "asin": "B001WSDMBI", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B079C2CDLD", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B077WX43MH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0FQ2YC11V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0DRFQJ6CX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0BX65MF93", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0CT5ZR247", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0BFXLLR2F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B0FDQT4X7W", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0FQ2XD95Z", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B094RDMZ3Z", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0DMZHTJY2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0CS2TQGM5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0DG2XK945", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0G2LNGQPK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0G4R1RHYH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0GSRZM4XB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B0D926NJN9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B01LZBHEKJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B09LQL4S9Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29_1", + "asin": "B0CQTPF6BJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0DZVY2F7M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0GMYGQ2G6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0C5WQ6DFB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0DJVK3SC4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B000N324DU", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B074F6YQZW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B09MD5W6Z5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0F94GJX9D", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0DP298J3J", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B010W3V6Z2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11_1", + "asin": "B0F24BSV4V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0B9MKBPH9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0BHHKC5JQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B0B4RFNK72", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B01LXD3KE6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B010W3V1W0", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B095JNPP53", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0D7DSBNNH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0B935PV25", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0B67XDHPQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B072Z1PQM2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0CT1J5HWX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B097G2DXSQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B010W3VLE8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B00R39MP4A", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B095JMYSQ9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0F8F5H3MG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0DNM4R43P", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0FCFB3X78", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0CN4KXVWF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0D455HLWT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0B1HYLLYT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0F1TKQG14", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0F32CLT9F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B0DJXYDPFC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0B3RRLNCT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0F2DR94NT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0BL7HRF5R", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0B6PT6D7Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0FF4FLHM4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B07L2476DB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0CK4379VH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0BRFW6XF4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B07BZT7699", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0D8W4JRTY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0F9NQMWGC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B07CF8KD48", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B0F9NWBVD8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0DSZK41Z9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0DHP1TPQL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0DPHSPQRP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0BVZ3VKNX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B0FPX7363M", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0F9KCHB7F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B07L3H5DDW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B003K5S4NW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0BNVHXLDN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0FC2R299V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0BNVJPSDT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0CCRNRHYW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6_1", + "asin": "B0FP2NQM67", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7", + "asin": "B0DRSD66T3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0DM6CS4KG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0GQ2YGFTR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0F1LNXRSH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0GJ59NQTT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0GKY1T54N", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0FZNMHLDY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0DHWYL3JP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16_1", + "asin": "B0CW5DD6SC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B0CRH7T5WG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0CRR6F9JX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B09D3XG1PT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B00AFGCL42", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0CXP3619X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0CW5GFQX3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0CZDK561H", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FKLRMG14", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28_1", + "asin": "B0DFQWC297", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1_1", + "asin": "B0DGPV9QY8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B0GV3W42MJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0DK4JV5YY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0CV4K3HSW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0FN5YD344", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B01HY4XZE2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13", + "asin": "B0D485JG3T", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0GC584NTL", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0FDQLKJL3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18_1", + "asin": "B0D6RBQR4Q", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B0CSLMZ4XS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0FC64KYP9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0FH234TL6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0F6D4Z3WB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23_1", + "asin": "B0F8RCZ91V", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24_1", + "asin": "B0FP5Q7B7X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0FM88JBRX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0CKMT72MX", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0C4JT7PHB", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0C1RMVGTZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B07BFQJ3G5", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B074VGHYF8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B015AK9JXI", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2_1", + "asin": "B084WSGN9C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3_1", + "asin": "B0FHWJ42Q8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4_1", + "asin": "B0CZDTGFSQ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5_1", + "asin": "B0F7XKNY9L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B0DWLFKPNW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0CWDPSJG4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8_1", + "asin": "B0G488Z8JG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9_1", + "asin": "B0BYRQ5TBY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B0CWH1MRCV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0FSCXLBHZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0GH6YJX2L", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "13_1", + "asin": "B0GMQ29YQ9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14_1", + "asin": "B0BQLX4QP3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0FXVHQNQ4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B072DVNZYH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17_1", + "asin": "B0G198MZH3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0F7HGQHLP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0B9M83MZV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B074P9YRGW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21_1", + "asin": "B0G3YJ32Q4", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B0D8RXPCH6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0CBPKKT98", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0DGG82L25", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26", + "asin": "B0FXVNDFHC", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B0CYY2YJ2S", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B0FC34Z9P6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B0018MQIVO", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B018OQ66AE", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "1", + "asin": "B0FNMK6R7Z", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B0CTJ8WRWZ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0CTJ9522Y", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B0CTJ88CKR", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B07DL3PBL7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B0C96V7HFN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B079HQKBP3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0BZHW8VQ6", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10_1", + "asin": "B019RKRGWS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B0DCLSFGQS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B07FL8Y4BP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B01NBVCL86", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15_1", + "asin": "B0C8J5Z7VN", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0CP73M8ST", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0G2XVTF2R", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19_1", + "asin": "B08GCVHM9S", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20_1", + "asin": "B0B5X3YGMP", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22", + "asin": "B07VM7QS8C", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0F93G7KFF", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B098B8QTM1", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25_1", + "asin": "B0F4WSK73F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0DC5M4XT7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27", + "asin": "B07SD5DWFT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "28", + "asin": "B082ZNZG3X", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B084M4ZGC3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "30", + "asin": "B0FXH7SVWV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "3", + "asin": "B07RM4MB3H", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "4", + "asin": "B0154H395G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "5", + "asin": "B08CHFDRJT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "6", + "asin": "B09YQ3D7CW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "7_1", + "asin": "B087YTMRK8", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "8", + "asin": "B08WKDDV96", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "9", + "asin": "B0GFMG4SFT", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "10", + "asin": "B07KWY3XLW", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "11", + "asin": "B07RQTKPSK", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "12", + "asin": "B0CRHC42M2", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "14", + "asin": "B07RGG2TCV", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "15", + "asin": "B0CTJNQ5MH", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "16", + "asin": "B0GGZSVY7G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "17", + "asin": "B0G3WTPGMG", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "18", + "asin": "B0GH136M59", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "19", + "asin": "B0G3WJP9HJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "20", + "asin": "B0814RZNHY", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "21", + "asin": "B09ZXZH432", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "22_1", + "asin": "B0BWY99XXS", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "23", + "asin": "B0849Y6VYJ", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "24", + "asin": "B0FKBBNN2G", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "25", + "asin": "B07SBWB2X9", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "26_1", + "asin": "B0DK4JNBM3", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "27_1", + "asin": "B0FFZ3636F", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "29", + "asin": "B0BRGZKXN7", + "country": "英国", + "url": "", + "title": "" + }, + { + "sourceFileKey": "", + "sourceFilename": "", + "rowToken": "", + "groupKey": "", + "id": "2", + "asin": "B0GSTM85H3", + "country": "英国", + "url": "", + "title": "" + } +] + # spide.process_task(task_data) + res = SpiderTask.group_by_id_prefix(task_data) + print(res) \ No newline at end of file diff --git a/app/amazon/main.py b/app/amazon/main.py index fd5864c..c2c807b 100644 --- a/app/amazon/main.py +++ b/app/amazon/main.py @@ -1,12 +1,11 @@ -import time -import traceback -import requests -import threading -from datetime import datetime +import traceback +import requests +import threading +from datetime import datetime from typing import Dict, Any, List from concurrent.futures import ThreadPoolExecutor, as_completed from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_BASE, ZN_COMPANY, ZN_USERNAME, ZN_PASSWORD -from amazon.del_brand import AmazoneDriver, kill_process +from amazon.del_brand import AmazoneDriver, DelbramdTask from amazon.approve import ApproveTask from amazon.match_action import MatchTak from amazon.price_match import PriceTask @@ -14,8 +13,7 @@ from amazon.asin_status import StatusTask from amazon.patrol_delete import PatrolDeleteTask from amazon.detail_spider import SpiderTask - -from amazon.tool import get_shop_info,show_notification +from amazon.amazon_base import kill_process class TaskMonitor: @@ -32,10 +30,10 @@ class TaskMonitor: self.chunk_index = 1 # 当前处理的分块索引 self.max_workers = 5 # 线程池最大线程数 - self.executor = None # 线程池执行器 - self.serial_task_locks = { - "product-risk-resolve-run": threading.Lock(), - } + self.executor = None # 线程池执行器 + self.serial_task_locks = { + "product-risk-resolve-run": threading.Lock(), + } # 在提交新任务前杀掉旧进程(确保环境干净) kill_process("v6") @@ -60,6 +58,7 @@ class TaskMonitor: futures = [] # 保存所有提交的任务Future对象 task_type_info = { + "delete-brand-run" : "删除品牌", "product-risk-resolve-run" : "产品风险审批", "shop-match-run" : "匹配价格", "price-track-run" : "跟价", @@ -70,20 +69,13 @@ class TaskMonitor: try: while self.running: try: - # 使用较长超时时间等待任务,减少空等待异常 # 超时后继续循环检查self.running状态,避免卡死 task_data = JSON_TASK_QUEUE.get(block=True, timeout=30) # 检查任务类型 task_type = task_data.get("type", "") - - if task_type == "delete-brand-run": - # 提交删除品牌任务到线程池 - self.log(f"接收到【删除品牌】任务,提交到线程池处理...") - future = self.executor.submit(self._process_task_wrapper, task_data) - futures.append(future) - - elif task_type in task_type_info: + + if task_type in task_type_info: # 提交产品风险审批任务到线程池 self.log(f"接收到任务,提交到线程池处理...") future = self.executor.submit(self._process_approve_task_wrapper, task_data, task_type_info[task_type]) @@ -116,23 +108,23 @@ class TaskMonitor: task_data: 任务数据 """ try: - self.log(f"线程 {id(task_data)} 开始处理删除品牌任务...") + self.log(f"线程 {id(task_data)} 开始处理任务...") self.process_task(task_data) - self.log(f"线程 {id(task_data)} 删除品牌任务处理完成") + self.log(f"线程 {id(task_data)} 任务处理完成") except Exception as e: self.log(f"线程 {id(task_data)} 删除品牌任务处理异常: {traceback.format_exc()}", "ERROR") - def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str): - task_type = task_data.get("type", "") - task_lock = self.serial_task_locks.get(task_type) - if task_lock is not None: - self.log(f"线程 {id(task_data)} 等待 {task_type} 串行锁...") - with task_lock: - self.log(f"线程 {id(task_data)} 获取 {task_type} 串行锁,开始处理...") - return self._process_approve_task_unlocked(task_data, TASK_TYPE) - return self._process_approve_task_unlocked(task_data, TASK_TYPE) - - def _process_approve_task_unlocked(self, task_data: Dict[str, Any],TASK_TYPE:str): + def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str): + task_type = task_data.get("type", "") + task_lock = self.serial_task_locks.get(task_type) + if task_lock is not None: + self.log(f"线程 {id(task_data)} 等待 {task_type} 串行锁...") + with task_lock: + self.log(f"线程 {id(task_data)} 获取 {task_type} 串行锁,开始处理...") + return self._process_approve_task_unlocked(task_data, TASK_TYPE) + return self._process_approve_task_unlocked(task_data, TASK_TYPE) + + def _process_approve_task_unlocked(self, task_data: Dict[str, Any],TASK_TYPE:str): """审批任务处理包装器(用于线程池调用) Args: @@ -140,6 +132,7 @@ class TaskMonitor: """ try: TASK_INFO = { + "删除品牌": DelbramdTask , "产品风险审批" : ApproveTask, "匹配价格" : MatchTak, "跟价" : PriceTask, @@ -152,540 +145,10 @@ class TaskMonitor: TASK_CLS = TASK_INFO[TASK_TYPE] # 根据任务类型选择处理类,默认为ApproveTask approve_task = TASK_CLS(user_info=self.user_info) approve_task.process_task(task_data) - self.log(f"线程 {id(task_data)} 产品风险审批任务处理完成") + self.log(f"线程 {id(task_data)} {TASK_TYPE} 任务处理完成") except Exception as e: - self.log(f"线程 {id(task_data)} 产品风险审批任务处理异常: {traceback.format_exc()}", "ERROR") + self.log(f"线程 {id(task_data)} {TASK_TYPE} 任务处理异常: {traceback.format_exc()}", "ERROR") - def process_task(self, task_data: Dict[str, Any]): - """处理单个任务 - - Args: - task_data: 任务数据 - """ - try: - data = task_data.get("data", {}) - task_id = data.get("taskId") - items = data.get("items", []) - - if not task_id: - self.log("任务ID为空,跳过", "WARNING") - return - - # 初始化任务状态 - self.update_task_status( - task_id, - status="running", - start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - total_shops=len(items), - processed_shops=0, - total_asins=sum(shop.get("totalRows", 0) for shop in items), - processed_asins=0, - success_count=0, - failed_count=0, - stop_requested=False # 暂停请求标志 - ) - - self.log(f"开始处理任务 {task_id},共 {len(items)} 个店铺") - - # 遍历处理每个店铺 - for idx, shop_data in enumerate(items, 1): - shop_name = shop_data.get("shopName", "未知店铺") - self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}") - - try: - self.process_shop(shop_data, task_id) - # 更新已处理店铺数 - if task_id in runing_task: - runing_task[task_id]["processed_shops"] += 1 - except Exception as e: - self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - - # 检查任务最终状态 - if task_id in runing_task and runing_task[task_id].get("stop_requested", False): - self.update_task_status(task_id, status="stopped") - self.log(f"任务 {task_id} 已被暂停!") - else: - self.update_task_status(task_id, status="completed") - self.log(f"任务 {task_id} 处理完成!") - - except Exception as e: - self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") - if task_id: - self.update_task_status(task_id, status="failed", error=str(e)) - - def process_shop(self, shop_data: Dict[str, Any], task_id: int): - """处理单个店铺(包含重试逻辑) - - Args: - shop_data: 店铺数据 - task_id: 任务ID - """ - shop_name = shop_data.get("shopName", "未知店铺") - result_id = shop_data.get("resultId") - countries = shop_data.get("countries", []) - company_name = shop_data.get("companyName", "未知公司") - - if not countries: - self.log(f"店铺 {shop_name} 没有国家数据,跳过", "WARNING") - return - - # 更新当前处理的店铺 - self.update_task_status(task_id, current_shop=shop_name) - - # 将店铺添加到正在执行中的店铺列表 - start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - runing_shop[shop_name] = start_time - self.log(f"账号:{company_name},店铺 {shop_name} 已添加到执行列表,开始时间: {start_time}") - - # 店铺打开重试最多3次 - driver = None - browser = None - max_retries = 3 - - for retry in range(max_retries): - try: - self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") - - # 如果不是第一次尝试,先杀进程 - # if retry > 0: - # self.log("重试前先杀掉浏览器进程...") - # kill_process("v6") - # kill_process("v5") - # time.sleep(2) - - # 创建驱动并打开店铺 - user_info = { - **self.user_info, - "company": company_name - } - driver = AmazoneDriver(user_info) - browser = driver.open_shop(shop_name) - - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name}") - break - else: - self.log(f"打开店铺失败: {browser}", "WARNING") - driver = None - - # 判断是否需要登录 - driver.tab.wait.doc_loaded(timeout=30, raise_err=False) # 等待页面加载,避免过早判断登录状态 - need_login = driver.need_login() - print("【是否需要登录】:",need_login) - if need_login: - self.log(f"店铺 {shop_name} 需要登录,正在登录...") - # 获取店铺凭证 - response = get_shop_info(shop_name) - print("【获取店铺凭证返回】:",response.text) - shop_data = response.json() - if not shop_data: - mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" - self.log(mes, "ERROR") - show_notification(mes, "ERROR") - continue - - password = shop_data["data"]["password"] - - login_success = driver.login(password) - if login_success: - self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") - browser = driver.open_shop(shop_name) - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name} 登录后") - break - else: - self.log(f"登录后打开店铺失败: {browser}", "WARNING") - driver = None - else: - self.log(f"店铺 {shop_name} 登录失败", "WARNING") - driver = None - - - except Exception as e: - self.log(f"打开店铺异常: {str(e)}", "ERROR") - driver = None - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(3) - - # 检查是否成功打开 - if not driver or not browser or browser == "店铺不存在": - self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR") - return - - try: - # 处理每个国家 - chunk_index =1 - for country_data in countries: - # 检查是否收到暂停请求 - if task_id in runing_task and runing_task[task_id].get("stop_requested", False): - self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING") - break # 跳出循环,进入finally关闭店铺 - - try: - chunk_index = self.process_country(driver, country_data, task_id, result_id, shop_data,chunk_index) - except Exception as e: - country_name = country_data.get("country", "未知") - self.log(f"处理国家 {country_name} 失败: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - - finally: - # 关闭店铺 - try: - if driver: - self.log(f"关闭店铺 {shop_name}") - driver.close_store() - time.sleep(2) - except Exception as e: - self.log(f"关闭店铺失败: {str(e)}", "WARNING") - - # 从正在执行中的店铺列表中移除 - if shop_name in runing_shop: - del runing_shop[shop_name] - self.log(f"店铺 {shop_name} 已从执行列表中移除") - - def process_country(self, driver: AmazoneDriver, country_data: Dict[str, Any], - task_id: int, result_id: int, shop_data: Dict[str, Any],chunk_index:int): - """处理单个国家的所有ASIN - - Args: - driver: 亚马逊驱动实例 - country_data: 国家数据 - task_id: 任务ID - result_id: 结果ID - shop_data: 店铺数据 - """ - country = country_data.get("country", "未知") - items = country_data.get("items", []) - - self.log(f"开始处理国家: {country},共 {len(items)} 个ASIN") - self.update_task_status(task_id, current_country=country) - - # 切换国家,最多重试3次 - max_retries = 3 - switch_success = False - - for retry in range(max_retries): - try: - self.log(f"尝试切换到国家 {country} (第 {retry + 1}/{max_retries} 次)") - - # 如果不是第一次尝试,先刷新页面 - if retry > 0: - self.log("重试前刷新页面...") - try: - driver.tab.refresh() - time.sleep(3) - except Exception as e: - self.log(f"刷新页面失败: {str(e)}", "WARNING") - - switch_success = driver.SwitchingCountries(country) - if switch_success: - self.log(f"成功切换到国家 {country}") - break - else: - self.log(f"切换到国家 {country} 失败", "WARNING") - - except Exception as e: - self.log(f"切换国家 {country} 异常: {str(e)}", "ERROR") - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(2) - - # 如果切换失败,回传该国家所有ASIN为失败状态 - if not switch_success: - self.log(f"切换到国家 {country} 失败,已重试 {max_retries} 次,将所有ASIN标记为失败", "ERROR") - chunk_index = self._report_all_asins_failed(country, items, task_id, shop_data,chunk_index) - return chunk_index - - # 切换到库存管理页面 - try: - driver.SwitchPage() - self.log(f"已切换到库存管理页面") - except Exception as e: - self.log(f"切换页面失败: {str(e)}", "ERROR") - # 切换页面失败也回传所有ASIN为失败 - chunk_index = self._report_all_asins_failed(country, items, task_id, shop_data,chunk_index) - return chunk_index - - # 处理每个ASIN - file_key = shop_data.get("fileKey", "") - source_filename = shop_data.get("sourceFilename", "") - total_rows = shop_data.get("totalRows", 0) - - for idx, asin_item in enumerate(items, 1): - # 检查是否收到暂停请求 - if task_id in runing_task and runing_task[task_id].get("stop_requested", False): - self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING") - return # 返回到上层,会触发店铺关闭 - - try: - self.log(f"[{idx}/{len(items)}] 处理ASIN: {asin_item.get('asin', '')}") - self.process_asin(driver, asin_item, country, task_id, result_id, - file_key, source_filename, total_rows,chunk_index) - except Exception as e: - asin = asin_item.get("asin", "未知") - self.log(f"处理ASIN {asin} 失败: {str(e)}", "ERROR") - # 继续处理下一个ASIN - chunk_index += 1 - return chunk_index - - - def process_asin(self, driver: AmazoneDriver, asin_item: Dict[str, Any], - country: str, task_id: int, result_id: int, file_key: str, - source_filename: str, total_rows: int,chunk_index:int): - """处理单个ASIN并回传结果 - - Args: - driver: 亚马逊驱动实例 - asin_item: ASIN数据项 - country: 国家名称 - task_id: 任务ID - result_id: 结果ID - file_key: 文件KEY - source_filename: 源文件名 - total_rows: 总行数 - chunk_index: 当前处理索引 - """ - asin = asin_item.get("asin", "") - row_index = asin_item.get("rowIndex", 0) - - # 更新当前处理的ASIN - self.update_task_status(task_id, current_asin=asin) - - status = "失败" - max_retries = 3 # 最多重试3次 - - for retry in range(max_retries): - try: - self.log(f"处理ASIN {asin} (第 {retry + 1}/{max_retries} 次)") - - # 如果不是第一次尝试,先刷新页面 - if retry > 0: - self.log("重试前刷新页面...") - try: - driver.tab.refresh() - time.sleep(3) - except Exception as e: - self.log(f"刷新页面失败: {str(e)}", "WARNING") - - # 搜索ASIN - sku_ls = driver.search(asin=asin) - self.log(f"搜索到 {len(sku_ls)} 个SKU") - - if len(sku_ls) == 0: - status = "查询不到" - self.log(f"ASIN {asin} 未找到商品", "WARNING") - break # 查询不到商品,无需重试 - else: - # 删除所有找到的SKU,如果任何一个失败则重新开始整个流程 - success_count = 0 - all_success = True # 标记是否所有SKU都删除成功 - total_sku_count = len(sku_ls) - - for sku in sku_ls: - try: - suc = driver.del_action(sku) - if suc: - success_count += 1 - self.log(f"SKU 删除成功 ({success_count}/{total_sku_count})") - else: - self.log(f"SKU 删除失败", "WARNING") - all_success = False - break # 任何一个失败,退出循环,准备重试整个流程 - except Exception as e: - self.log(f"删除SKU异常: {str(e)}", "ERROR") - all_success = False - break # 发生异常,退出循环,准备重试整个流程 - - # 判断删除结果 - if all_success and success_count == total_sku_count: - status = "成功" - self.log(f"ASIN {asin} 所有SKU删除成功 ({success_count}/{total_sku_count})") - # 更新成功计数 - if task_id in runing_task: - runing_task[task_id]["success_count"] += 1 - break # 全部成功,跳出重试循环 - else: - # 有失败的SKU - if retry < max_retries - 1: - self.log(f"有SKU删除失败,准备重试整个流程... ({retry + 1}/{max_retries})") - time.sleep(2) - continue # 继续下一次重试 - else: - # 所有重试都用完了 - if success_count > 0: - status = "部分成功" - self.log(f"ASIN {asin} 部分SKU删除成功 ({success_count}/{total_sku_count})", "WARNING") - if task_id in runing_task: - runing_task[task_id]["success_count"] += 1 - else: - status = "失败" - self.log(f"ASIN {asin} 所有SKU删除失败", "ERROR") - if task_id in runing_task: - runing_task[task_id]["failed_count"] += 1 - - except Exception as e: - status = "删除异常" - self.log(f"处理ASIN {asin} 异常: {str(e)}", "ERROR") - - # 如果还有重试机会,继续重试 - if retry < max_retries - 1: - self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})") - time.sleep(2) - else: - # 所有重试都失败了,更新失败计数 - if task_id in runing_task: - runing_task[task_id]["failed_count"] += 1 - - # 更新已处理ASIN计数 - if task_id in runing_task: - runing_task[task_id]["processed_asins"] += 1 - - # 回传结果到API - try: - payload = { - "submissionId": "", - "files": [{ - "fileKey": file_key, - "sourceFilename": source_filename, - "chunkIndex": chunk_index, - "chunkTotal": total_rows, - "processedRows": chunk_index, - "totalRows": total_rows, - "currentCountry": country, - "currentAsin": asin, - "countries": [{ - "country": country, - "items": [{ - "asin": asin, - "status": status - }] - }] - }] - } - - self.post_result(task_id, payload) - self.log(f"ASIN {asin} 结果已回传,状态: {status}") - - except Exception as e: - self.log(f"回传结果失败: {str(e)}", "ERROR") - - def _report_all_asins_failed(self, country: str, items: List[Dict[str, Any]], - task_id: int, shop_data: Dict[str, Any],chunk_index:int): - """将国家下所有ASIN标记为失败并回传 - - Args: - country: 国家名称 - items: ASIN列表 - task_id: 任务ID - shop_data: 店铺数据 - """ - self.log(f"开始回传国家 {country} 下的 {len(items)} 个ASIN失败状态") - - file_key = shop_data.get("fileKey", "") - source_filename = shop_data.get("sourceFilename", "") - total_rows = shop_data.get("totalRows", 0) - - for asin_item in items: - asin = asin_item.get("asin", "") - - try: - # 更新失败计数 - if task_id in runing_task: - runing_task[task_id]["failed_count"] += 1 - runing_task[task_id]["processed_asins"] += 1 - - # 回传失败状态 - payload = { - "submissionId": "", - "files": [{ - "fileKey": file_key, - "sourceFilename": source_filename, - "chunkIndex": chunk_index, - "chunkTotal": total_rows, - "processedRows": chunk_index, - "totalRows": total_rows, - "currentCountry": country, - "currentAsin": asin, - "countries": [{ - "country": country, - "items": [{ - "asin": asin, - "status": "失败" - }] - }] - }] - } - - self.post_result(task_id, payload) - chunk_index += 1 - self.log(f"ASIN {asin} 失败状态已回传") - - except Exception as e: - self.log(f"回传ASIN {asin} 失败状态时出错: {str(e)}", "ERROR") - - return chunk_index - - def post_result(self, task_id: int, payload: Dict[str, Any]): - """回传结果到API(带重试机制) - - Args: - task_id: 任务ID - payload: 结果数据 - """ - url = f"{DELETE_BRAND_API_BASE}/api/delete-brand/tasks/{task_id}/result" - max_retries = 3 # 最多重试3次 - - for retry in range(max_retries): - try: - self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") - - response = requests.post( - url, - json=payload, - headers={"Content-Type": "application/json"}, - timeout=30, - verify=False # 忽略SSL证书验证 - ) - print("【结果提交】:",payload) - print("【结果提交返回】:",response.text) - - if response.status_code == 200: - self.log(f"结果回传成功: {url}") - return # 成功后直接返回,不再重试 - else: - self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") - # 如果还有重试机会,继续重试 - if retry < max_retries - 1: - self.log(f"准备重试... ({retry + 1}/{max_retries})") - time.sleep(2) # 等待2秒后重试 - else: - self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - - except Exception as e: - self.log(f"调用API异常: {str(e)}", "ERROR") - # 如果还有重试机会,继续重试 - if retry < max_retries - 1: - self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})") - time.sleep(2) # 等待2秒后重试 - else: - self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - - def update_task_status(self, task_id: int, **kwargs): - """更新任务状态 - - Args: - task_id: 任务ID - **kwargs: 要更新的字段 - """ - if task_id not in runing_task: - runing_task[task_id] = {} - - runing_task[task_id].update(kwargs) - def stop(self): """停止监控""" self.running = False diff --git a/app/amazon/match_action.py b/app/amazon/match_action.py index 07f2516..2d8da3f 100644 --- a/app/amazon/match_action.py +++ b/app/amazon/match_action.py @@ -1,82 +1,24 @@ import time import re import traceback -from DrissionPage._pages.chromium_tab import ChromiumTab -from config import runing_task, runing_shop +from config import runing_task, runing_shop,DELETE_BRAND_API_BASE from datetime import datetime -from amazon.del_brand import AmamzonBase, kill_process - -from amazon.tool import show_notification,get_shop_info +from amazon.amazon_base import AmamzonBase, kill_process,TaskBase +from amazon.tool import show_notification +import requests class AmzoneMatchAction(AmamzonBase): mark_name = "匹配价格" - def SwitchPage(self): - """ - 切换至 管理所有库存页面 - 1、等待 //navigation-favorites-bar[@class="hydrated"] 出现 - """ - navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]') - navigation.wait.displayed(raise_err=False) - page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr( - 'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]') - page_btn.wait.displayed(raise_err=False) - page_btn.click(timeout=5) + def __init__(self, user_info: dict, socket_port: int = 19890): + super().__init__(user_info,socket_port) + self.already_asin = set() - self.tab.wait.doc_loaded() - # 等待搜索框出现 - search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') - search_region.wait.displayed(raise_err=False) - - def search(self,filter_type="ApprovalRequired"): - sku_ls = [] - - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5) - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown') - drop_down.wait.displayed(raise_err=False) - drop_down.wait.enabled(raise_err=False) - time.sleep(0.6) - drop_down.click() - # //kat-option[@value="SearchSuppressed"] - xp = f'xpath://kat-option[@value="{filter_type}"]' - print(f"【{self.mark_name}】正在寻找筛选条件 {filter_type},xpath: {xp}") - approval_required = self.tab.eles(xp,timeout=5) - if len(approval_required) == 0: - print(f"【{self.mark_name}】没有需要{filter_type}选项】没有需要{filter_type}的商品了") - return sku_ls # "没有需要审批的商品了" - else: - approval_required = approval_required[0] - approval_required.wait.displayed(raise_err=False) - approval_required.click() - - approval_required_text = approval_required.text - print(f"【{self.mark_name}】已选择筛选条件: {approval_required_text}") - - count = re.findall(r'\d+', approval_required_text) - if count: - count = int(count[0]) - print(f"【{self.mark_name}】待审批的商品数量: {count}") - if count <= 0: - print(f"【{self.mark_name}】没有需要{filter_type}的商品了") - return sku_ls #"没有需要审批的商品了" - for _ in range(3): - # 等待加载完成 - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]") - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3) - if len(sku_ls) > 0: - break - approval_required.click() - return sku_ls + def reset_already_asin(self): + self.already_asin = set() + self.log("already_asin 已重置") def wait_loaded(self): # 等待加载完成 @@ -86,13 +28,13 @@ class AmzoneMatchAction(AmamzonBase): timeout=3) load_ele.wait.deleted(timeout=5, raise_err=False) except Exception as e: - print(f"【{self.mark_name}】等待加载中消失出错", e) - + print(f"【{self.mark_name}】等待加载中消失出错", e) + def run_page_action(self): - print(f"【{self.mark_name}】开始执行") + self.log(f"==================={self.mark_name}=======================") + self.log("开始执行...") num = 0 retry_num = 0 - already_asin = set() get_page_faild = 0 while retry_num < 3: # 最多重试3次 @@ -110,39 +52,50 @@ class AmzoneMatchAction(AmamzonBase): page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5) if len(page_pamel) > 0: current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text - + # 测试 + # if current_page > 1: + # break # 总页数 total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]') if len(total_page) > 0: total_page = total_page[-1].text else: total_page = 0 - print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}") + self.log(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}") get_page_faild = 0 except Exception as e: - print(f"【{self.mark_name}】获取页码失败", e) + self.log(f"【{self.mark_name}】获取页码失败", e) get_page_faild+= 1 if get_page_faild > 2: show_notification(f"【{self.mark_name}】获取页码失败超3次停止任务!") break + # 保存当前URL,用于失败重试时访问 + try: + self.url = self.tab.url + self.log(f"获取当前的链接:{self.url}") + except Exception as e: + self.url = None + self.log(f"获取当前的链接失败:{e}") + + sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10) - print(f"【{self.mark_name}】获取到 {len(sku_ls)}") + self.log(f"【{self.mark_name}】获取到 {len(sku_ls)}") # for sku_ele in sku_ls[0:2]: for sku_ele in sku_ls: # solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]') - + asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]',timeout=3).text - print(f"【{self.mark_name}】ASIN {asin} 找到....") - if asin in already_asin: - print(f"【{self.mark_name}】{asin} 已经处理过了,跳过") + self.log(f"【{self.mark_name}】ASIN {asin} 找到....") + if asin in self.already_asin: + self.log(f"【{self.mark_name}】{asin} 已经处理过了,跳过") continue price_match = sku_ele.eles('xpath:.//div[@data-test-id="FeaturedOfferPrice"]//a[text()="匹配"]',timeout=2) if len(price_match) == 0: - print(f"【{self.mark_name}】{asin},没有推荐价格匹配按钮") + self.log(f"【{self.mark_name}】{asin},没有推荐价格匹配按钮") yield (asin,"无需处理") - already_asin.add(asin) + self.already_asin.add(asin) continue try: price_match[0].click() @@ -154,14 +107,14 @@ class AmzoneMatchAction(AmamzonBase): save_all_btn.wait.deleted(timeout=10, raise_err=False) yield (asin,"处理完成") - already_asin.add(asin) + self.already_asin.add(asin) except Exception as e: print(f"{asin} 处理失败", e) yield (asin,"处理失败") - already_asin.add(asin) + self.already_asin.add(asin) continue - + # 判断是否存在需要翻页的情况 page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5) if len(page_pamel) == 0: @@ -172,56 +125,21 @@ class AmzoneMatchAction(AmamzonBase): break next_page_btn.click() num += 1 - print(f"【{self.mark_name}】【程序计算】正在翻页,已翻 {num} 页...") - - already_asin = set() + self.log(f"【{self.mark_name}】【程序计算】正在翻页,已翻 {num} 页...") except Exception as e: - print(f"【{self.mark_name}】处理匹配操作异常", e) + self.log(f"【{self.mark_name}】处理匹配操作异常", e) traceback.print_exc() retry_num += 1 self.tab.refresh() self.tab.wait.doc_loaded(raise_err=False,timeout=120) - - -class MatchTak: - - country_info = { - "DE": "德国", - "FR": "法国", - "ES": "西班牙", - "IT": "意大利", - "UK": "英国" - } - - - def __init__(self, user_info: dict = None): - """初始化审批任务处理器 - - Args: - user_info: 用户信息字典,包含 company, username, password - """ - self.user_info = user_info or {} - self.running = True - - def log(self, message: str, level: str = "INFO"): - """日志输出 - - Args: - message: 日志消息 - level: 日志级别 - """ - from datetime import datetime - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if level == "ERROR": - show_notification(message, "error") - print(f"[{timestamp}] [MatchTak] [{level}] {message}") +class MatchTak(TaskBase): + task_name = "匹配价格-TASK" def process_task(self, task_data: dict): """处理审批任务主入口 - Args: task_data: 任务数据 """ @@ -238,21 +156,12 @@ class MatchTak: # 用于测试 limit = data.get("limit",None) - if not task_id: - self.log("任务ID为空,跳过", "WARNING") + if not task_id or not items or not country_codes: + self.log(f"任务ID为空{task_id}/店铺列表为空{items}/国家列表为空{country_codes},跳过", "WARNING") return - - if not items: - self.log("店铺列表为空,跳过", "WARNING") - return - - if not country_codes: - self.log("国家列表为空,跳过", "WARNING") - return - + self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家") - from config import runing_task runing_task[task_id] = { "status": "running", "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), @@ -287,7 +196,6 @@ class MatchTak: if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 except Exception as e: - import traceback self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -301,98 +209,13 @@ class MatchTak: self.log(f"任务 {task_id} 处理完成!") except Exception as e: - import traceback self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") if task_id: - from config import runing_task if task_id in runing_task: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def open_shop(self,max_retries,company_name,shop_name,iskill=False): - error_info = "" - driver = None - for retry in range(max_retries): - try: - self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") - - if iskill: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) - - # 组装用户信息并创建驱动 - user_info = { - **self.user_info, - "company": company_name - } - driver = AmzoneMatchAction(user_info) - browser = driver.open_shop(shop_name) - - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name}") - else: - self.log(f"打开店铺失败: {browser}", "WARNING") - driver = None - continue - - # 判断是否需要登录 - need_login = driver.need_login() - print("【是否需要登录】:", need_login) - if need_login: - self.log(f"店铺 {shop_name} 需要登录,正在登录...") - # 获取店铺凭证 - response = get_shop_info(shop_name) - print("【获取店铺凭证返回】:", response.text) - shop_data = response.json() - if not shop_data: - mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" - self.log(mes, "ERROR") - show_notification(mes, "ERROR") - continue - - password = shop_data["data"]["password"] - - login_success = driver.login(password) - if login_success: - self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") - browser = driver.open_shop(shop_name) - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name} 登录后") - break - else: - self.log(f"登录后打开店铺失败: {browser}", "WARNING") - driver = None - else: - self.log(f"店铺 {shop_name} 登录失败", "WARNING") - driver = None - else: - break - - except Exception as e: - import traceback - self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO") - driver = None - error_info = str(e) - time.sleep(10) - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(3) - - # 检查是否成功打开 - if not driver or not browser or browser == "店铺不存在": - error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}" - self.log(error_msg, "ERROR") - # 从执行列表中移除 - if shop_name in runing_shop: - del runing_shop[shop_name] - return driver - return driver - - # def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str): - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, + def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, user_id=None, stage_index=None, final_stage: bool = True,limit:str=None): """处理单个店铺 @@ -408,7 +231,6 @@ class MatchTak: if not company_name: self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING") return - if task_id in runing_task: runing_task[task_id]["current_shop"] = shop_name @@ -431,7 +253,7 @@ class MatchTak: break # 打开店铺 - driver = self.open_shop(max_retries=max_retries, company_name=company_name, + driver = self.open_shop(cls=AmzoneMatchAction,max_retries=max_retries, company_name=company_name, shop_name=shop_name, iskill=iskill) if driver is None: self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR") @@ -439,18 +261,25 @@ class MatchTak: self.post_result(task_id, shop_name, country_code, "", "", is_done=True) return - try: - self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,limit) - except Exception as e: - import traceback - self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - if "与页面的连接已断开" in str(e): - iskill = True + current_url = None + for _ in range(max_retries): + try: + self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,limit, + target_url=current_url) + driver.reset_already_asin() + driver.close_store() + break + except Exception as e: + self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + if "与页面的连接已断开" in str(e): + iskill = True + current_url = driver.url # 更新已处理国家数 if task_id in runing_task: runing_task[task_id]["processed_countries"] += 1 + self.log(f"{task_id}任务处理完成") # 最后回传,标记完成 try: @@ -467,83 +296,11 @@ class MatchTak: if shop_name in runing_shop: del runing_shop[shop_name] self.log(f"店铺 {shop_name} 已从执行列表中移除") - - def action_init(self,driver,country_name,shop_name,risk_listing_filter): - """切换到指定国际 -> 页面 -》 筛选好""" - max_retries = 5 - switch_success = False - switch_success_pg = False - search_success = False - sku_ls = [] - for retry in range(max_retries): - try: - self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") - if retry > 2: - # 刷新不行就重新打开店铺 - self.log("重试前重新打开店铺...") - try: - driver.close_store() - time.sleep(3) - driver.open_shop(shop_name) - except Exception as e: - self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") - # 如果不是第一次尝试,先刷新页面 - if retry > 0: - self.log("重试前刷新页面...") - try: - driver.tab.refresh() - time.sleep(3) - except Exception as e: - self.log(f"刷新页面失败: {str(e)}", "WARNING") - switch_success = driver.SwitchingCountries(country_name) - if switch_success: - self.log(f"成功切换到国家 {country_name}") - - # 切换到库存管理页面 - driver.SwitchPage() - self.log(f"已切换到库存管理页面") - time.sleep(3) - switch_success_pg = True - - sku_ls = driver.search(filter_type=risk_listing_filter) - search_success = True - self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...") - break - else: - self.log(f"切换到国家 {country_name} 失败", "WARNING") - - except Exception as e: - import traceback - self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(2) - - # 如果切换失败,直接返回 - # if not switch_success or not switch_success_pg or not search_success: - # error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家" - # self.log(error_message, "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - return switch_success,switch_success_pg,search_success,sku_ls - - - - def process_country(self, driver: AmzoneMatchAction, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str,limit:str=None): + def process_country(self, driver, country_code, task_id, shop_name, risk_listing_filter,limit=None,target_url=None): """处理单个国家的审批任务 - - Args: - driver: AmzoneApprove驱动实例 - country_code: 国家代码(如 UK, DE, FR 等) - task_id: 任务ID - shop_name: 店铺名称 - risk_listing_filter: 风险商品筛选条件 """ - from config import runing_task - + # 转换国家代码为中文名称 country_name = self.country_info.get(country_code, country_code) info_mes = f"开始处理国家: {country_name} ({country_code})" @@ -554,91 +311,7 @@ class MatchTak: if task_id in runing_task: runing_task[task_id]["current_country"] = country_name - # 切换国家,最多重试3次 - # max_retries = 3 - # switch_success = False - # - # for retry in range(max_retries): - # try: - # self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") - # if retry > 1: - # # 刷新不行就重新打开店铺 - # self.log("重试前重新打开店铺...") - # try: - # driver.close_store() - # time.sleep(3) - # driver.open_shop(shop_name) - # except Exception as e: - # self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") - # - # # 如果不是第一次尝试,先刷新页面 - # if retry > 0: - # self.log("重试前刷新页面...") - # try: - # driver.tab.refresh() - # time.sleep(3) - # except Exception as e: - # self.log(f"刷新页面失败: {str(e)}", "WARNING") - # - # switch_success = driver.SwitchingCountries(country_name) - # if switch_success: - # self.log(f"成功切换到国家 {country_name}") - # break - # else: - # self.log(f"切换到国家 {country_name} 失败", "WARNING") - # - # except Exception as e: - # import traceback - # self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") - # self.log(traceback.format_exc(), "ERROR") - # - # # 如果还有重试机会,等待后继续 - # if retry < max_retries - 1: - # time.sleep(2) - # - # # 如果切换失败,直接返回 - # if not switch_success: - # error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家" - # self.log(error_message, "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - # - # # 切换到库存管理页面 - # try: - # driver.SwitchPage() - # self.log(f"已切换到库存管理页面") - # except Exception as e: - # import traceback - # self.log(f"切换页面失败: {str(e)}", "ERROR") - # self.log(traceback.format_exc(), "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - # - # # 搜索需要审批的商品,最多重试3次 - # sku_ls = [] - # for retry in range(max_retries): - # try: - # self.log(f"尝试搜索匹配操作商品 (第 {retry + 1}/{max_retries} 次)") - # sku_ls = driver.search(filter_type=risk_listing_filter) - # break - # except Exception as e: - # self.log(f"搜索商品异常: {str(e)}", "ERROR") - # if retry < max_retries - 1: - # try: - # driver.tab.refresh() - # time.sleep(3) - # except Exception as refresh_error: - # self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING") - # - # # 如果没有需要审批的商品,直接返回 - # if len(sku_ls) == 0: - # self.log(f"国家 {country_name} 没有搜索出的商品") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - # + max_retries = 5 switch_success, switch_success_pg, search_success, sku_ls = self.action_init(driver, country_name, shop_name, risk_listing_filter) @@ -658,7 +331,13 @@ class MatchTak: return self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...") - + + if target_url is not None: # 从失败的链接继续 + self.log(f"开始访问:{target_url}") + driver.tab.get(url=target_url) + driver.tab.wait.doc_loaded(timeout=60, raise_err=False) + + result = [] # 处理所有需要审批的商品(通过yield获取结果) for asin, status in driver.run_page_action(): # 检查是否收到暂停请求 @@ -675,19 +354,22 @@ class MatchTak: runing_task[task_id]["failed_count"] += 1 - # 回传结果到API - try: - self.post_result(task_id, shop_name, country_code, asin, status) - except Exception as e: - self.log(f"回传结果失败: {str(e)}", "ERROR") - + result.append({ + "asin": asin, + "status": status, + "done": False + }) + if len(result) > 20: + self.post_result_batch(task_id, shop_name, country_code,result) + result = [] + + if len(result) > 0: + self.post_result_batch(task_id, shop_name, country_code, result) self.log(f"国家 {country_name} 处理完成") def post_stage_finished(self, task_id: int, user_id, stage_index): - import requests - from config import DELETE_BRAND_API_BASE if user_id in (None, "", 0): raise ValueError("user_id is required for stage completion callback") @@ -733,9 +415,7 @@ class MatchTak: asin: ASIN status: 处理状态 """ - import requests - from config import DELETE_BRAND_API_BASE - + url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/result" payload = { @@ -789,7 +469,66 @@ class MatchTak: self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") raise RuntimeError("已达到最大重试次数,结果回传最终失败") - + + def post_result_batch(self, task_id: int, shop_name: str, country_code: str, country_data:list, + is_done: bool = False): + """回传处理结果到API + + Args: + task_id: 任务ID + shop_name: 店铺名称 + country_code: 国家代码 + asin: ASIN + status: 处理状态 + """ + + url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/result" + + payload = { + "shops": [ + { + "error": "", + "countries": { + country_code: country_data + }, + "shopName": shop_name + } + ] + } + + max_retries = 3 + for retry in range(max_retries): + try: + print("================【匹配】=====================") + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + self.log(f"回传URL: {url}") + self.log(f"回传数据: {payload}") + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False + ) + self.log(f"回传结果: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"结果回传成功") + return + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + print("=====================================") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + print("=====================================") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + raise RuntimeError("已达到最大重试次数,结果回传最终失败") if __name__ == "__main__": diff --git a/app/amazon/price_match.py b/app/amazon/price_match.py index c39b064..d0d6406 100644 --- a/app/amazon/price_match.py +++ b/app/amazon/price_match.py @@ -1,20 +1,14 @@ import json -import sys -import os -import io -# sys.stdout.reconfigure(encoding='utf-8') - import time import re import traceback from datetime import datetime -from DrissionPage import Chromium, ChromiumOptions import requests -from amazon.del_brand import AmamzonBase, kill_process +from amazon.amazon_base import AmamzonBase, kill_process,TaskBase from amazon.tool import show_notification,get_shop_info,remove_special_characters,split_currency_values -from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE +from config import runing_task, runing_shop,DELETE_BRAND_API_BASE class RepricingLogic: @@ -165,6 +159,9 @@ def calculate_target_price( # 分支 B:不是 Amazon US,且目前是自己的购物车 elif is_my_buybox: + if len(front_end_data.get("top_sellers")) < 2: + return None + price_2 = float(front_end_data.get("top_sellers")[1].get("price")) # 实际第二名价格 return RepricingLogic.situation_2_own_buybox(my_price,price_2) @@ -175,33 +172,6 @@ def calculate_target_price( return RepricingLogic.situation_3_not_own_buybox(my_price, price_1) -# ===================================================================== -# --- 辅助函数:封装“不是自己购物车”的阶梯降价逻辑 --- -# ===================================================================== -def calculate_standard_competitor_pricing(my_current_price, target_competitor_price): - # 基础策略:比目标价格低0.3 - base_target = target_competitor_price - 0.3 - - # 计算当前价格与目标价格的差值 - diff = abs(my_current_price - target_competitor_price) - - # 根据阶梯执行跟价扣减 - if 0 < diff < 0.5: - return base_target - 0.5 - elif 0.5 <= diff <= 0.8: - return base_target - 0.7 - elif 0.8 <= diff <= 1.5: - return base_target - 1.0 - elif 1.5 <= diff <= 2.5: - return base_target - 1.0 - elif 2.5 < diff <= 3 : - return None # 差距过大,跳过不跟价 - # elif diff > 3: - # return base_target - else: - return base_target - - class ChromeAmzone: mark_name = "亚马逊详情采集" @@ -486,70 +456,13 @@ class ChromeAmzone: class AmzonePriceMatch(AmamzonBase): mark_name = "跟价" - def SwitchPage(self): - """ - 切换至 管理所有库存页面 - 1、等待 //navigation-favorites-bar[@class="hydrated"] 出现 - """ - navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]') - navigation.wait.displayed(raise_err=False) - page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr( - 'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]') - page_btn.wait.displayed(raise_err=False) - page_btn.click(timeout=5) + def __init__(self, user_info: dict, socket_port: int = 19890): + super().__init__(user_info,socket_port) + self.already_asin = set() - self.tab.wait.doc_loaded() - # 等待搜索框出现 - search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') - search_region.wait.displayed(raise_err=False,timeout=60) - - def search(self,filter_type="ApprovalRequired"): - sku_ls = [] - - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5) - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown') - drop_down.wait.displayed(raise_err=False) - drop_down.wait.enabled(raise_err=False) - time.sleep(0.6) - drop_down.click() - # //kat-option[@value="SearchSuppressed"] - xp = f'xpath://kat-option[@value="{filter_type}"]' - print(f"【{self.mark_name}】正在寻找筛选条件 {filter_type},xpath: {xp}") - approval_required = self.tab.eles(xp,timeout=5) - if len(approval_required) == 0: - print(f"【{self.mark_name}】没有需要{filter_type}选项】没有需要{filter_type}的商品了") - return sku_ls # "没有需要审批的商品了" - else: - approval_required = approval_required[0] - approval_required.wait.displayed(raise_err=False) - approval_required.click() - - approval_required_text = approval_required.text - print(f"【{self.mark_name}】已选择筛选条件: {approval_required_text}") - - count = re.findall(r'\d+', approval_required_text) - if count: - count = int(count[0]) - print(f"【{self.mark_name}】待审批的商品数量: {count}") - if count <= 0: - print(f"【{self.mark_name}】没有需要{filter_type}的商品了") - return sku_ls #"没有需要审批的商品了" - for _ in range(3): - # 等待加载完成 - load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]") - if len(load_ele) > 0: - load_ele[0].wait.deleted(timeout=3, raise_err=False) - time.sleep(0.5) - - sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3) - if len(sku_ls) > 0: - break - approval_required.click() - return sku_ls + def reset_already_asin(self): + self.already_asin = set() + self.log("already_asin 已重置") def search_asin(self, asin): search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group') @@ -596,14 +509,8 @@ class AmzonePriceMatch(AmamzonBase): def run_page_action(self,current_shop_name:str,mode:str, appoint_asin:str=None,skip_asin:list=[],miniprice_info:dict={}): - """ - - miniprice_info : - { - "B0DSJGJBWV" : "19.81" # asin : 最低价 - } - """ - print(f"【{self.mark_name}】,开始执行") + self.log(f"==================={self.mark_name}=======================") + self.log("开始执行...") num = 0 retry_num = 0 already_asin = set() @@ -616,7 +523,7 @@ class AmzonePriceMatch(AmamzonBase): while retry_num < max_retry_num: # 最多重试3次 try: if appoint_asin is not None: - print(self.mark_name,"指定asin操作",appoint_asin) + self.log(f"指定asin操作,{appoint_asin}") self.search_asin_action(asin=appoint_asin) load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]", timeout=5) @@ -630,7 +537,7 @@ class AmzonePriceMatch(AmamzonBase): if len(page_pamel) > 0: current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text #测试===== - # if int(current_page) > 3:c + # if int(current_page) > 1: # break # 测试=========== @@ -642,28 +549,37 @@ class AmzonePriceMatch(AmamzonBase): total_page = total_page[-1].text else: total_page = 0 - print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}") + self.log(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}") get_page_faild = 0 except Exception as e: - print("【{self.mark_name}】","获取页码失败", e) + self.log(f"获取页码失败,{e}") get_page_faild += 1 if get_page_faild > 2: show_notification(f"【{self.mark_name}】获取页码失败超3次停止任务!") break + # 保存当前URL,用于失败重试时访问 + try: + self.url = self.tab.url + self.log(f"获取当前的链接:{self.url}") + except Exception as e: + self.url = None + self.log(f"获取当前的链接失败:{e}") + + sku_ls = self.tab.eles("xpath://div[@data-sku]", timeout=10) - print(f"【{self.mark_name}】获取到 {len(sku_ls)}") + self.log(f"【{self.mark_name}】获取到 {len(sku_ls)}") if len(sku_ls) == 0: if mode == "asin": - print(f"asin 模式查询不到,{asin}") - yield (asin, { + self.log(f"asin 模式查询不到,{appoint_asin}") + yield (appoint_asin, { "statu": "查询不到" }) break retry_num += 1 - print(f"没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}") + self.log(f"没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}") self.tab.refresh() self.tab.wait.doc_loaded(raise_err=False, timeout=120) continue @@ -673,7 +589,7 @@ class AmzonePriceMatch(AmamzonBase): asin = sku_ele.ele( 'xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]', timeout=3).text - print(f"【{self.mark_name}】ASIN {asin} 找到....") + self.log(f"【{self.mark_name}】ASIN {asin} 找到....") try: # if asin in already_asin: # print(f"【{self.mark_name}】{asin} 已经处理过了,跳过") @@ -691,12 +607,12 @@ class AmzonePriceMatch(AmamzonBase): # print("最低价格->>",bottom_price_text) bottom_price = f"{sum(split_currency_values(bottom_price_text))}" except Exception as e: - print("最低价提取失败",e) + self.log(f"最低价提取失败,{e}") current_price_ele = sku_ele.eles('xpath:.//b[text()="价格"]/../..//kat-input',timeout=5) if len(current_price_ele) > 0: current_price = current_price_ele[0].attr("value") - print("获取到当前价格为 ->",current_price) + self.log(f"获取到当前价格为 ->{current_price}") # 如果紫鸟里当前价格低于最低价则删除,否则跳过 # if mode == "status" and miniprice_info.get(asin): if miniprice_info.get(asin): @@ -705,12 +621,12 @@ class AmzonePriceMatch(AmamzonBase): yield (asin, { "statu": "跳过(当前价格低于或等于最低价)", "currentPrice": current_price, - "minimumPrice": bottom_price, # 最低价格 + "minimumPrice": backend_price, # 最低价格 }) continue else: - print(f"{self.mark_name} 没有获取到当前价格") + self.log(f"没有获取到当前价格") yield (asin, { "statu": "失败(没有获取到当前的价格)" }) @@ -720,7 +636,7 @@ class AmzonePriceMatch(AmamzonBase): detail_url_ele = sku_ele.ele('xpath:.//div[contains(@class,"ProductDetails-module__titleContainer")]//a') # detail_url_ele.click() detail_url = detail_url_ele.attr("href") - print("详情链接",detail_url) + self.log(f"详情链接,{detail_url}") # time.sleep(1) for _ in range(10): try: @@ -735,7 +651,7 @@ class AmzonePriceMatch(AmamzonBase): front_end_data = chrome.run(country=current_country) chrome.close() self.clear_tab() - print(self.mark_name,"亚马逊前台抓取到数据",front_end_data) + self.log(f"亚马逊前台抓取到数据,{front_end_data}") if front_end_data is None or len(front_end_data.get("top_sellers")) == 0: yield (asin, { "statu": "失败(第一名获取失败,请检查卖家精灵是否启用)", @@ -770,12 +686,12 @@ class AmzonePriceMatch(AmamzonBase): ) # adjust_prices = 236.56 - print(asin,"-->>",f"【逻辑计算后的价格】",adjust_prices) + self.log(f"{asin}【逻辑计算后的价格】{adjust_prices}") if adjust_prices is not None and adjust_prices != float(current_price): #修改价格 - print("准备填入的原始值:",adjust_prices) + self.log(f"准备填入的原始值:{adjust_prices}") adjust_prices = round(adjust_prices, 2) - print("保留两位小数后的::",adjust_prices) + self.log(f"保留两位小数后的::{adjust_prices}") self.tab.actions.click(current_price_ele[0]) time.sleep(1) current_price_ele[0].input(f"{adjust_prices}", clear=True) @@ -789,7 +705,7 @@ class AmzonePriceMatch(AmamzonBase): save_all_btn.click() save_all_btn.wait.deleted(timeout=10, raise_err=False) recommendedPrice = recommend_price + shipping_fee - price_1 = float(front_end_data.get("top_sellers")[0].get("price")) if len(front_end_data.get("top_sellers")) >= 2 else "" # 实际第一名价格 + price_1 = float(front_end_data.get("top_sellers")[0].get("price")) if len(front_end_data.get("top_sellers")) >= 1 else "" # 实际第一名价格 price_2 = float(front_end_data.get("top_sellers")[1].get("price")) if len(front_end_data.get("top_sellers")) >= 2 else "" # 实际第二名价格 cartShopName = front_end_data.get("cart_seller") yield (asin,{ @@ -825,7 +741,7 @@ class AmzonePriceMatch(AmamzonBase): "priceChangePrice": f"{adjust_prices}".replace("None",""), }) except Exception as e: - print(f"{asin},操作出错,{e}") + self.log(f"{asin},操作出错,{e}") continue already_asin.add(asin) @@ -839,40 +755,19 @@ class AmzonePriceMatch(AmamzonBase): break next_page_btn.click() num += 1 - print(f"【{self.mark_name}】【程序计算】正在翻页,已翻 {num} 页...") + self.log(f"【程序计算】正在翻页,已翻 {num} 页...") already_asin = set() retry_num = 0 except Exception as e: - print(f"【{self.mark_name}】处理跟价操作异常", e) + self.log(f"处理跟价操作异常", e) traceback.print_exc() retry_num += 1 self.tab.refresh() self.tab.wait.doc_loaded(raise_err=False, timeout=120) -class PriceTask: - country_info = { - "DE": "德国", - "FR": "法国", - "ES": "西班牙", - "IT": "意大利", - "UK": "英国" - } - - def __init__(self, user_info: dict = None): - """初始化审批任务处理器 - - Args: - user_info: 用户信息字典,包含 company, username, password - """ - self.user_info = user_info or {} - self.running = True - - def log(self, message: str, level: str = "INFO"): - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - if level == "ERROR": - show_notification(message, "error") - print(f"[{timestamp}] [PriceTask] [{level}] {message}") +class PriceTask(TaskBase): + task_name = "跟价-TASK" def process_task(self, task_data: dict): """处理审批任务主入口 @@ -941,7 +836,6 @@ class PriceTask: if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 except Exception as e: - import traceback self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") @@ -955,7 +849,6 @@ class PriceTask: self.log(f"任务 {task_id} 处理完成!") except Exception as e: - import traceback self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") if task_id: from config import runing_task @@ -963,151 +856,6 @@ class PriceTask: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def open_shop(self,max_retries,company_name,shop_name,iskill=False): - error_info = "" - driver = None - for retry in range(max_retries): - try: - self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)") - - if iskill: - self.log("重试前先杀掉浏览器进程...") - kill_process("v6") - kill_process("v5") - time.sleep(2) - - # 组装用户信息并创建驱动 - user_info = { - **self.user_info, - "company": company_name - } - driver = AmzonePriceMatch(user_info) - browser = driver.open_shop(shop_name) - - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name}") - else: - self.log(f"打开店铺失败: {browser}", "WARNING") - driver = None - continue - - # 判断是否需要登录 - need_login = driver.need_login() - print("【是否需要登录】:", need_login) - if need_login: - self.log(f"店铺 {shop_name} 需要登录,正在登录...") - # 获取店铺凭证 - response = get_shop_info(shop_name) - print("【获取店铺凭证返回】:", response.text) - shop_data = response.json() - if not shop_data: - mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}" - self.log(mes, "ERROR") - show_notification(mes, "ERROR") - continue - - password = shop_data["data"]["password"] - - login_success = driver.login(password) - if login_success: - self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...") - browser = driver.open_shop(shop_name) - if browser and browser != "店铺不存在": - self.log(f"成功打开店铺 {shop_name} 登录后") - break - else: - self.log(f"登录后打开店铺失败: {browser}", "WARNING") - driver = None - else: - self.log(f"店铺 {shop_name} 登录失败", "WARNING") - driver = None - else: - break - - except Exception as e: - import traceback - self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO") - driver = None - error_info = str(e) - time.sleep(10) - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(3) - - # 检查是否成功打开 - if not driver or not browser or browser == "店铺不存在": - error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}" - self.log(error_msg, "ERROR") - # 从执行列表中移除 - if shop_name in runing_shop: - del runing_shop[shop_name] - return driver - return driver - - def action_init(self,driver,country_name,shop_name,risk_listing_filter): - """切换到指定国际 -> 页面 -》 筛选好""" - max_retries = 5 - switch_success = False - switch_success_pg = False - search_success = False - sku_ls = [] - for retry in range(max_retries): - try: - self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)") - if retry > 2: - # 刷新不行就重新打开店铺 - self.log("重试前重新打开店铺...") - try: - driver.close_store() - time.sleep(3) - driver.open_shop(shop_name) - except Exception as e: - self.log(f"关闭重新打开店铺: {str(e)}", "WARNING") - # 如果不是第一次尝试,先刷新页面 - if retry > 0: - self.log("重试前刷新页面...") - try: - driver.tab.refresh() - time.sleep(3) - except Exception as e: - self.log(f"刷新页面失败: {str(e)}", "WARNING") - - switch_success = driver.SwitchingCountries(country_name) - if switch_success: - self.log(f"成功切换到国家 {country_name}") - - # 切换到库存管理页面 - driver.SwitchPage() - self.log(f"已切换到库存管理页面") - time.sleep(3) - switch_success_pg = True - - sku_ls = driver.search(filter_type=risk_listing_filter) - search_success = True - self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...") - break - else: - self.log(f"切换到国家 {country_name} 失败", "WARNING") - - except Exception as e: - import traceback - self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - - # 如果还有重试机会,等待后继续 - if retry < max_retries - 1: - time.sleep(2) - - # 如果切换失败,直接返回 - # if not switch_success or not switch_success_pg or not search_success: - # error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家" - # self.log(error_message, "ERROR") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - return switch_success,switch_success_pg,search_success - - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, user_id=None, stage_index=None, final_stage: bool = True, limit: str = None): """处理单个店铺 @@ -1150,7 +898,7 @@ class PriceTask: # 处理每个国家 for country_code in country_codes: # 打开店铺 - driver = self.open_shop(max_retries=max_retries, company_name=company_name, + driver = self.open_shop(cls=AmzonePriceMatch,max_retries=max_retries, company_name=company_name, shop_name=shop_name,iskill=iskill) if driver is None: self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR") @@ -1163,23 +911,32 @@ class PriceTask: self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "ERROR") driver.close_store() break + skip_asin = skip_asins_by_country.get(country_code,[]) #需要跳过的asin appoint_asin = asin_rows_by_country.get(country_code,[]) if mode == "status": miniprice_info = {i.get('asin'):i.get('minimumPrice') for i in skip_asin_details_by_country.get(country_code,[])} else: miniprice_info = minimum_price_by_country_and_asin.get(country_code,{}) - try: - self.process_country(driver, country_code, task_id, shop_name, risk_listing_filter, - shopMallName=shopMallName,skip_asin=skip_asin, limit=limit, - appoint_asin=appoint_asin,mode=mode,miniprice_info=miniprice_info, - ) - except Exception as e: - import traceback - self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") - self.log(traceback.format_exc(), "ERROR") - if "与页面的连接已断开" in str(e): - iskill = True + + current_url = None + for _ in range(max_retries): + try: + self.process_country(driver, country_code, task_id, shop_name, risk_listing_filter, + shopMallName=shopMallName,skip_asin=skip_asin, limit=limit, + appoint_asin=appoint_asin,mode=mode,miniprice_info=miniprice_info, + target_url=current_url + ) + driver.reset_already_asin() + driver.close_store() + break + except Exception as e: + self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + if "与页面的连接已断开" in str(e): + iskill = True + current_url = driver.url + # 更新已处理国家数 if task_id in runing_task: runing_task[task_id]["processed_countries"] += 1 @@ -1205,7 +962,7 @@ class PriceTask: def process_country(self, driver: AmzonePriceMatch, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str,shopMallName:str,skip_asin:list,appoint_asin:list,mode:str, - miniprice_info:dict={}, limit: str = None): + miniprice_info:dict={}, limit: str = None,target_url:str=None): """处理单个国家的审批任务 Args: @@ -1229,7 +986,7 @@ class PriceTask: # 切换国家,最多重试3次 max_retries = 5 - switch_success,switch_success_pg,search_success = self.action_init( driver, country_name, shop_name, risk_listing_filter) + switch_success,switch_success_pg,search_success,sku_ls = self.action_init( driver, country_name, shop_name, risk_listing_filter) # 如果切换失败,直接返回 if not switch_success or not switch_success_pg or not search_success: error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家" @@ -1238,34 +995,14 @@ class PriceTask: runing_task[task_id]["processed_countries"] += 1 return - # 搜索需要审批的商品,最多重试3次 - # sku_ls = [] - # for retry in range(max_retries): - # try: - # self.log(f"尝试搜索匹配操作商品 (第 {retry + 1}/{max_retries} 次)") - # sku_ls = driver.search(filter_type=risk_listing_filter) - # break - # except Exception as e: - # self.log(f"搜索商品异常: {str(e)}", "ERROR") - # if retry < max_retries - 1: - # try: - # driver.tab.refresh() - # driver.tab.wait.doc_loaded() - # time.sleep(3) - # except Exception as refresh_error: - # self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING") - # - # # 如果没有需要审批的商品,直接返回 - # if len(sku_ls) == 0: - # self.log(f"国家 {country_name} 没有搜索出的商品") - # if task_id in runing_task: - # runing_task[task_id]["processed_countries"] += 1 - # return - self.log(f"国家 {country_name} ,开始处理...") - # 处理所有需要审批的商品(通过yield获取结果) + if target_url is not None: #从失败的链接继续 + self.log(f"开始访问:{target_url}") + driver.tab.get(url=target_url) + driver.tab.wait.doc_loaded(timeout=60,raise_err=False) + result = [] # 指定 asin max_range = max(1,len(appoint_asin)) for i in range(max_range): @@ -1275,6 +1012,11 @@ class PriceTask: else: _shopMallName = shopMallName ap_asin = None + + if not _shopMallName: + self.log(f"没有店铺名,不执行,{_shopMallName}","ERROR") + return + for _ in range(max_retries): for asin, status in driver.run_page_action( current_shop_name=_shopMallName, @@ -1297,19 +1039,40 @@ class PriceTask: runing_task[task_id]["failed_count"] += 1 - # 回传结果到API - try: - self.post_result(task_id, shop_name, country_code, asin, status,shopMallName) - except Exception as e: - self.log(f"回传结果失败: {str(e)}", "ERROR") + result.append({ + "shopMallName": shopMallName, + "asin": asin, + "price": status.get("currentPrice") if status.get("currentPrice") else "", + "recommendedPrice": status.get("recommendedPrice") if status.get( + "recommendedPrice") else "", + "minimumPrice": status.get("minimumPrice") if status.get("minimumPrice") else "", + "firstPlace": status.get("firstPlace") if status.get("firstPlace") else "", + "secondPlace": status.get("secondPlace") if status.get("secondPlace") else "", + "cartShopName": status.get("cartShopName") if status.get("cartShopName") else "", + # "priceChangeStatus": "UPDATED", + "deleteSkipAsin": status.get("deleteSkipAsin", False), + "removeAsin": status.get("removeAsin") if status.get("removeAsin") else "", + "modifyCount": "1", + "shippingFee": status.get("shippingFee") if status.get("shippingFee") else "", + "status": status.get("statu"), + "priceChangeStatus": status.get("priceChangeStatus") if status.get( + "priceChangeStatus") else "", + "priceChangePrice": status.get("priceChangePrice") if status.get( + "priceChangePrice") else "", + } + ) + if len(result) > 20: + self.post_result_batch(task_id, shop_name, country_code, result) + result = [] break + if len(result) > 0: + self.post_result_batch(task_id, shop_name, country_code, result) + self.log(f"国家 {country_name} 处理完成") def post_stage_finished(self, task_id: int, user_id, stage_index): - import requests - from config import DELETE_BRAND_API_BASE if user_id in (None, "", 0): raise ValueError("user_id is required for stage completion callback") @@ -1426,8 +1189,62 @@ class PriceTask: time.sleep(2) self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - raise RuntimeError("已达到最大重试次数,结果回传最终失败") + # raise RuntimeError("已达到最大重试次数,结果回传最终失败") + def post_result_batch(self, task_id: int, shop_name: str, country_code: str, country_data,is_done: bool = False): + """回传处理结果到API + """ + + url = f"{DELETE_BRAND_API_BASE}/api/price-track/tasks/{task_id}/result" + country_aliases = {name: code for code, name in self.country_info.items()} + country_key = country_aliases.get(str(country_code).strip(), str(country_code).strip()) + + payload = { + "shops": [ + { + "shopName": shop_name, + "countries": { + country_key: country_data + }, + "error": "" + } + ] + } + if is_done: + payload["shops"][0]["success"] = is_done + + max_retries = 3 + for retry in range(max_retries): + try: + print("================【跟价】=====================") + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + self.log(f"回传URL: {url}") + self.log(f"回传数据: {payload}") + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False + ) + self.log(f"回传结果: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"结果回传成功") + return + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + print("=====================================") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + print("=====================================") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") if __name__ == '__main__': # 使用示例 @@ -1436,38 +1253,38 @@ if __name__ == '__main__': "username": "自动化_Robot", "password": "#20zsg25" } - shop_name = "刘建煌" - country = "法国" - kill_process('v6') - driver = AmzonePriceMatch(user_info) - browser = driver.open_shop(shop_name) - sw_suc = driver.SwitchingCountries(country) - driver.SwitchPage() - risk_listing_filter = "Active" - _shopMallName = "Jianhuang 888" - ap_asin ="B0F25Y52PH" - skip_asin = [] - for _ in range(3): - try: - sku_ls = driver.search(filter_type=risk_listing_filter) - break - except Exception as e: - print(e) - driver.tab.refresh() - if len(sku_ls) > 0: - print("有数据,开始操作") - for asin, status in driver.run_page_action( - current_shop_name=_shopMallName, - appoint_asin=ap_asin,skip_asin=skip_asin,mode="asin" - ): - print(f"ASIN {asin} 的处理结果: {status}") - print("已完成操作") + # shop_name = "周洪" + # country = "德国" + # kill_process('v6') + # driver = AmzonePriceMatch(user_info) + # browser = driver.open_shop(shop_name) + # sw_suc = driver.SwitchingCountries(country) + # driver.SwitchPage() + # risk_listing_filter = "Active" + # _shopMallName = "Zhou Hong 600" + # ap_asin ="B0FK574K91" + # skip_asin = [] + # for _ in range(3): + # try: + # sku_ls = driver.search(filter_type=risk_listing_filter) + # break + # except Exception as e: + # print(e) + # driver.tab.refresh() + # if len(sku_ls) > 0: + # print("有数据,开始操作") + # for asin, status in driver.run_page_action( + # current_shop_name=_shopMallName, + # appoint_asin=ap_asin,skip_asin=skip_asin,mode="asin" + # ): + # print(f"ASIN {asin} 的处理结果: {status}") + # print("已完成操作") - # front_end_data = {'top_sellers': [{'rank': 1, 'price': '52.51', 'stock': '100', 'shop_name': 'Amazon US'}, {'rank': 2, 'price': '62.58', 'stock': '30', 'shop_name': 'Amazon US'}], 'cart_seller': 'Jianhuang 888', 'timestamp': '2026-04-30 09:52:49'} + # front_end_data = {'top_sellers': [{'rank': 1, 'price': '30.14', 'stock': '100', 'shop_name': 'Zhou Hong 600'}], 'cart_seller': 'Zhou Hong 600', 'timestamp': '2026-04-30 23:47:02'} # - # current_Price = 52.51 - # current_shop_name = "Jianhuang 888" - # recommended_price = 52.51 + # current_Price = 30.14 + # current_shop_name = "Zhou Hong 600" + # recommended_price = 30.14 # recommended_shipping =0.0 # res = calculate_target_price( # front_end_data, current_Price, current_shop_name, @@ -1476,6 +1293,6 @@ if __name__ == '__main__': # print(res) - # task_data = {'type': 'price-track-run', 'ts': 1777388166302, 'data': {'task_id': 4892, 'result_id': 6685, 'shop_name': '魏振峰', 'shop_id': '27543917795757', 'platform': '亚马逊', 'company_name': 'rongchuang123', 'shop_mall_name': 'WEIZHENFENG168', 'matched': True, 'match_status': 'MATCHED', 'match_message': None, 'success': False, 'error': None, 'output_filename': None, 'download_url': None, 'task_status': 'RUNNING', 'loop_run_id': 168, 'round_index': 1, 'country_codes': ['DE', 'UK', 'FR', 'IT', 'ES'], 'mode': 'asin', 'skip_asins': {}, 'skip_asins_by_country': {}, 'skip_asin_details_by_country': {}, 'minimum_price_by_country_and_asin': {'DE': {'B0CFY5M3XJ': '38', 'B0DSJGJBWV': '9'}, 'UK': {'B0G2LPSGP5': '30', 'B0G1LY2K8L': '10'}}, 'skip_asin_delete_policy': {'enabled': False, 'mode': 'NONE', 'deleteWhenPriceBelowMinimum': False, 'compareField': 'price', 'thresholdField': 'minimumPrice', 'target': 'skip_asin', 'source': 'frontend-price-track'}, 'delete_skip_asin_when_price_below_minimum': False, 'deleteSkipAsinWhenPriceBelowMinimum': False, 'asin_rows_by_country': {'DE': [{'shopMallName': '', 'asin': 'B0CFY5M3XJ', 'price': '', 'recommendedPrice': '', 'minimumPrice': '38', 'firstPlace': '', 'secondPlace': '', 'cartShopName': '', 'priceChangeStatus': '', 'modifyCount': '', 'status': ''}, {'shopMallName': '', 'asin': 'B0DSJGJBWV', 'price': '', 'recommendedPrice': '', 'minimumPrice': '9', 'firstPlace': '', 'secondPlace': '', 'cartShopName': '', 'priceChangeStatus': '', 'modifyCount': '', 'status': ''}], 'UK': [{'shopMallName': '', 'asin': 'B0G2LPSGP5', 'price': '', 'recommendedPrice': '', 'minimumPrice': '30', 'firstPlace': '', 'secondPlace': '', 'cartShopName': '', 'priceChangeStatus': '', 'modifyCount': '', 'status': ''}, {'shopMallName': '', 'asin': 'B0G1LY2K8L', 'price': '', 'recommendedPrice': '', 'minimumPrice': '10', 'firstPlace': '', 'secondPlace': '', 'cartShopName': '', 'priceChangeStatus': '', 'modifyCount': '', 'status': ''}]}, 'taskId': 4892, 'resultId': 6685, 'shopName': '魏振峰', 'shopId': '27543917795757', 'platformName': '亚马逊', 'companyName': 'rongchuang123', 'shopMallName': 'WEIZHENFENG168', 'matchStatus': 'MATCHED', 'matchMessage': None, 'outputFilename': None, 'downloadUrl': None, 'taskStatus': 'RUNNING', 'loopRunId': 168, 'roundIndex': 1}} - # price_cls = PriceTask(user_info) - # price_cls.process_task(task_data) \ No newline at end of file + task_data = {'type': 'price-track-run', 'ts': 1777746140975, 'data': {'task_id': 5658, 'result_id': 7627, 'shop_name': '郭锋滨', 'shop_id': '27769139291498', 'platform': '亚马逊', 'company_name': '尾号5578的公司115', 'shop_mall_name': '', 'matched': True, 'match_status': 'MATCHED', 'match_message': None, 'success': False, 'error': None, 'output_filename': None, 'download_url': None, 'task_status': 'RUNNING', 'loop_run_id': 235, 'round_index': 1, 'country_codes': ['UK', 'DE', 'FR', 'ES', 'IT'], 'mode': 'status', 'skip_asins': {'DE': ['B0FMKM1BYH', 'B0DJT78FNR', 'B0DJLJ73RB', 'B0D6QL2JMY', 'B0CFKLPKF5', 'B09BMWX6Q1', 'B07C4MNWFL', 'B0FHDGLQK7', 'B0FHD9S7K7', 'B0FB2FB6G6', 'B0F9W8VXWQ', 'B0F9W81F2F', 'B0F9W7KLNM', 'B0F2ZJWJNC', 'B0D49F7LM4', 'B0CXSP3PDP', 'B0CLXZHM2G', 'B0CG8RNWL6', 'B0C4SZ8GZW', 'B0BVM53J3B', 'B0BNHN9N9X', 'B0BMXBNMP5', 'B0BLN7D2JZ', 'B09MYVN5YK', 'B09MBNG7P4', 'B0DXTSTZC6', 'B0D25871WL', 'B0CL69XRTY', 'B0CKXMDBN8', 'B0CGDF2G8V', 'B0CFTGXVYF', 'B0CFT6XPPD', 'B09YXTN8JP', 'B0DHX32XJH', 'B0DHL7FQJM', 'B0DHL63M79', 'B0CWXYY74V', 'B0CWXYRGZW', 'B0CPLR6YHB', 'B0BXP4RC25', 'B0BFRRZDCH', 'B0B65R3N47', 'B0F36ZSWTQ', 'B0DT1DYWC6', 'B0DNZ5WX1L', 'B0DNZ5N7HY', 'B0DCJJ3PCM', 'B0D8JV9MB9', 'B0D2KCRLJ1', 'B0CRVRDS8C', 'B0CQK7QWJB', 'B0CQK5SRXW', 'B0BCL2DLPC', 'B0B9JBHPFY', 'B09TVY4V8Z', 'B0FJBD6Q3D', 'B0DG8LQLXZ', 'B0CD7Q1BX3', 'B0BV1SR48Y', 'B0F29CFJJ9', 'B0DSL6PKR7', 'B0CPV21W4L', 'B0CNQZ7FXY', 'B0CLY9XT5M', 'B0CGTMZ8GP', 'B0CF881PG4', 'B0CF4K1G4R', 'B0CB5T79XG', 'B0C73PQ9KM', 'B0C28TBN79', 'B09T31ZRJF', 'B01LBOYWNO', 'B0DDTB25YZ', 'B0D1Q8GL1Y', 'B0CW9JC84B', 'B0CLGJG36H', 'B0CF9JWKHJ', 'B0C2J3V92Q', 'B0BVVYZ6TN', 'B0B8J8VF7C', 'B0B4BFVZD1', 'B09R1CPKVX', 'B0924BNVH2', 'B0F54M2147', 'B0F1BZNBY6', 'B0F1BXV9WG', 'B0DC957VHC', 'B0D8778SZB', 'B0CP9WVSRY', 'B0CKNQP1PT', 'B0CJ4PW758', 'B0BYFD87C6', 'B0B492K9KT', 'B09968XMC7', 'B0FGJ8RCW1', 'B0FGJ4WK8B', 'B0FFMYGWHP', 'B0FD3DLFKL', 'B0FBWRWXG2', 'B0F99RW38N', 'B0F62LZ4G3', 'B0F629FLVV', 'B0F2N88DS9', 'B0F23Y11GD', 'B0F1YV8T2S', 'B0DXK9PR5X', 'B0DRV7QZ85', 'B0DRV7LN4V', 'B0DR4QYC2M', 'B0D5QL869M', 'B0D41VK3YL', 'B0D3XBW4KT', 'B0D2KW1ZRL', 'B0CPPC6PDW', 'B0CHVPSJ61', 'B0C5C1V5JD', 'B0C232Z6C6', 'B0C22KBMJC', 'B0BZVL6Y9X', 'B0BX58MW7N', 'B0B9M86GMC', 'B08DQVJ2VB', 'B07WKYLJ3T', 'B092D7P9BP', 'B0F9W2MZJY', 'B09KXV2C3T', 'B092D7TM65', 'B0CR3P2F5L', 'B0BWCSCYFL', 'B08JLW8S3Y', 'B0D4H73BJH', 'B08D77WTYG', 'B08D73XW2J', 'B09H4RB4ZL', 'B09WMQ29GH', 'B0DJ2M2RYC', 'B0BGF2QCKT', 'B0DS6FC8M7', 'B098QFLVDZ', 'B0DMD16GTD', 'B0CNVLLQ3J', 'B0CP3JZ1P5', 'B0C1GR4K6W', 'B0CBM792D2', 'B09NJK6K5T', 'B0D8Q5MK31', 'B0CYSCV19T', 'B0C5D5XPLJ', 'B09R3NFGH2', 'B09BG1QD9M', 'B092QY1823', 'B0C55PGK51', 'B0DRN8MXRG', 'B08152JKFB', 'B09Y1P7GK7', 'B0CG62X5GT', 'B0DHJKKZDD', 'B0DYHZ59YL', 'B0D2D5WS2D', 'B0G6LHVRKG', 'B0DQ53F84K', 'ASDASD', 'B0CJTCGDB7232', 'B0CJTCGDB7213', 'B0CJTCGDB79', 'B0CJTCGDB78', 'B0CJTCGDB77', 'B0CJTCGDB76', 'B0CJTCGDB75', 'B0CJTCGDB74', 'B0CJTCGDB73', 'B0CJTCGDB72', 'B0CJTCGDB71', 'B0CJTCGDB7', '1'], 'UK': ['B0FGJD2P9Q', 'B0DKBSWJ93', 'B0DJT78FNR', 'B0DFCR79L5', 'B0DCBJ3WY5', 'B0D9LPW1YB', 'B0D7VDQG19', 'B0CP5K31TL', 'B0CG3RG7RQ', 'B0C8DSPMNC', 'B0B6H8RCHC', 'B08TVWB36K', 'B08FZMZQ11', 'B0G6LHVRKG', 'B0FF36SY6Q', 'B0DYTNBMB2', 'B0DQNLGSMQ', 'B0DNSNN7NH', 'B0DJWHHFBS', 'B0DJHSJ9LS', 'B0D2D5WS2D', 'B0CG8RNWL6', 'B0C5D5XPLJ', 'B09WHYGBSR', 'B09WHV9JCJ', 'B09P4VCL2V', 'B09MYVN5YK', 'B09MBNG7P4', 'B0DM63394K', 'B0D1Q4RR99', 'B0CZD1HLY7', 'B0CWD9R2FS', 'B0CS6WYRFW', 'B0CRVTSQGQ', 'B0CR8FTHYP', 'B0CCB5Q5ZY', 'B0CC94W5G7', 'B0CC948Q8Q', 'B0CC93R18S', 'B0CC92F33P', 'B0BZ4DNWPY', 'B0BZ4CMQDL', 'B07Q4RBT32', 'B0DHX32XJH', 'B0DHX1HR7X', 'B0DHTT1X71', 'B0DHTRLQ2L', 'B0DHL7FQJM', 'B0D3V1BJX9', 'B0D3TZ45C3', 'B0D2SDNYS5', 'B0CQY5SM94', 'B0CQLN2FC1', 'B0CQF6W2JT', 'B0BY85GDF5', 'B0BY82BRTZ', 'B0B73K6QZQ', 'B0B4MDDD7D', 'B0F1X5Q9Q6', 'B0DYHPWTVJ', 'B0DQNJPS5M', 'B0DQ4B5SF7', 'B0DNZ5WX1L', 'B0DKX28SNX', 'B0DHVTGXPD', 'B0DHDBN98L', 'B0DFXRP8X1', 'B0DFGPGXY6', 'B0D8JV9MB9', 'B0D2KCRLJ1', 'B0CW63X5XY', 'B0CRVRDS8C', 'B0CRHWLDY4', 'B0CLZBR5CM', 'B0CL67FJFL', 'B0CG2C89ZZ', 'B0BP7YMCXH', 'B0BM8Y94QC', 'B0BJKBRTK7', 'B0BCL2DLPC', 'B0B6PSLTPF', 'B0B6PS4RQ1', 'B0B5L4HC95', 'B08ZT1PJ2K', 'B0FGQ31VWT', 'B0FFT33WRF', 'B0FD3S6DRN', 'B0F2GBR7MP', 'B0F2G5TG4S', 'B0F2FZVS7X', 'B0F2FYW3HN', 'B0F2B6924H', 'B0DSJMMMXN', 'B0DJT68ZM3', 'B0DJT66VG3', 'B0DJT5YJXD', 'B0DCGSFTYX', 'B0D8C6KL4G', 'B0C9H8MJC3', 'B0C85JQNRC', 'B0C5QQFN1G', 'B0BZ8FXRZG', 'B0BS8SW341', 'B0FLVP9N4Y', 'B0F2MPWSCQ', 'B0F29CFJJ9', 'B0DSKC1GQB', 'B0DSCBZSGS', 'B0DSCB329P', 'B0DLNXW8W5', 'B0DJQHVBZZ', 'B0DGCW7WBY', 'B0DFTG85NG', 'B0D99N1DZY', 'B0D8B849BJ', 'B0D89YCGCR', 'B0D89WK6JF', 'B0D89PMKKP', 'B0D7X1634D', 'B0D7V3W7D1', 'B0CYT3R89Z', 'B0CY7P61VD', 'B0CQ8B9PZM', 'B0CNK1SV4P', 'B0CF4K1G4R', 'B0CF4JJXHY', 'B0CF4BJ42D', 'B0C6T346MQ', 'B0BX2KDVMY', 'B0BR4YWSF9', 'B0BR4XHP5Z', 'B09Z68FK4Q', 'B09SL3PTTM', 'B09JBQZQQL', 'B08PSSRS3C', 'B08G4P4F76', 'B0DRD7T6TQ', 'B0DQXT78DT', 'B0DQXRV47R', 'B0DQXRV18S', 'B0DQXRLK5X', 'B0DQXQNL6Z', 'B0D2KQVS2Y', 'B0CXJC74TK', 'B0CW9JC84B', 'B0CSYBNXWX', 'B0CRHSTGX6', 'B0CPPJTX5C', 'B0CJB9WFHD', 'B0CF9JWKHJ', 'B0BVVYZ6TN', 'B0BND2YKJF', 'B09TRCQNQH', 'B09H5J6YQX', 'B09CNRP5FJ', 'B08BZ9LW9Z', 'B0F54M2147', 'B0F388HQLY', 'B0F1BZNBY6', 'B0F1BXV9WG', 'B0DSD949P4', 'B0DFQ2FP96', 'B0D9YXGFQW', 'B0CP9WVSRY', 'B0CKZ35SJC', 'B0CHYNDX87', 'B0CH66SNFC', 'B0BHSWP8RD', 'B0BGRW6858', 'B0B6JKD25B', 'B0B1LJB3M5', 'B09P3S4SH5', 'B09NLSRHGV', 'B0FVDPV5NS', 'B0FQJ5CDL2', 'B0FLJLWTLV', 'B0FGQ1VP2J', 'B0FD6YL7MM', 'B0FBWP9GJL', 'B0F99RW38N', 'B0F3Y3M538', 'B0F27ZM2TG', 'B0F23Y11GD', 'B0F23WZ78P', 'B0F1MY4PF6', 'B0DXKC6H1G', 'B0DQLNTLF4', 'B0DM8DYG77', 'B0DHJNBFVH', 'B0DH1XKG95', 'B0DFYG7C4K', 'B0DFTR1CMN', 'B0DB8FM915', 'B0D5H65VW3', 'B0D3XBW4KT', 'B0CRKJWJ4X', 'B0CPPGX6FQ', 'B0CPPGSBHD', 'B0CL9PVZD1', 'B0C232Z6C6', 'B0C22KBMJC', 'B0BX58MW7N', 'B0B6T3N5MG', 'B0B4S9QYWM', 'B0B4S93T7Q', 'B09R1PMW3J', 'B08MLN92JM', 'B0BHN71FWM', 'B09NKRWXPC', 'B0C89TNVTN', 'B0D1VDMZR1', 'B0DS2QT1YZ', 'B09Y29P5YF', 'B0DBQDSHVZ', 'B0CFF62JY3', 'B0BHPGTWQR', 'B0D4F4D3W2', 'B0F13BRF4V', 'B0D1FXTY99', 'B00KY0LLFO', 'B0B7MJXDZC', 'B0B4DV8NLV', 'B0DJXRFCF7', 'B0BPHMFD7V', 'B0F281ZQWR', 'B0D7V2FNW3', 'B0D57X7KV9', 'B0CJ2SST5Z', 'B0CZ92LPP4', 'B0CG5Z2Z44', 'B0D6T9H5LX', 'B0CQJNR9MP', 'ZXCA', 'B0CJTGDB7232', 'B0CJTGDB7213', 'B0CJTGDB79', 'B0CJTGDB78', 'B0CJTGDB77', 'B0CJCGDB76', 'B0CJCGDB75', 'B0CJCGDB74', 'B0CJCGDB73', 'B0CJCGDB72', 'B0CJCGDB71', 'B0CJCGDB7', 'B0CTCGDB7', 'B0FGTNLTG3', 'WQDASDAS', 'SADAD', 'DFHFGH78979'], 'FR': ['B0FY6JY4BF', 'B0DZNRB5Q1', 'B0DNSKFLGK', 'B0DGLCQDTR', 'B0D9W93869', 'B0CG8RNWL6', 'B0BX94RYT1', 'B0BLN7D2JZ', 'B09WHV9JCJ', 'B0DKX28SNX', 'B0CZCVTMRJ', 'B0CC95H28M', 'B0CC942XSZ', 'B0CC93WW6S', 'B0CC93TDB9', 'B0816GGCFY', 'B0CQY5SM94', 'B0DQTKRNLH', 'B0DCB85F3J', 'B0D8JR3W53', 'B0D2KCRLJ1', 'B0CQK7QWJB', 'B0B9XM29C9', 'B0B5L4HC95', 'B0F9FCT8RD', 'B0BZ8J16JM', 'B0BV1TRMGN', 'B09G68LMB3', 'B0DCRJSMJP', 'B093Y447R2', 'B0D8Y5KWB8', 'B0BJK43Z6X', 'B0DXK7YZ5D', 'B0C232Z6C6', 'B0BZVL6Y9X', 'B0B4S98HLZ', 'B09VPMBMB8', 'B0D17L6YKN'], 'IT': ['B0DTK87KJ5', 'B0DNSNN7NH', 'B0D9W93869', 'B0CLXZHM2G', 'B0CG8RNWL6', 'B0BX94RYT1', 'B0BVM53J3B', 'B0BLN7D2JZ', 'B0F9KVS6Y2', 'B0F8QGHMLL', 'B0CY5KW8RY', 'B0CC94P4H1', 'B0CC94K2HN', 'B0CC922QN8', 'B0C7Q51PL1', 'B0D8JR3W53', 'B0B5L4HC95', 'B0C5C53444', 'B0F5PRVRLF', 'B0BJK43Z6X', 'B09WKFD3G8', 'B08NT2BNGH', 'B0DRV6D214', 'B08QRKHD3C', 'B09JMW3F1R', 'B0CKYHTDJ5', 'B098QFLVDZ', '32EGDFGS4', 'B087JP6PWL89'], 'ES': ['B0D2J11TKY', 'B0CLY2THTY', 'B0D8JS8NPH', 'B0CYGSBXSQ', 'B0CKXMDBN8', 'B0CJ5F9J4Z', 'B0CC95XMD4', 'B0CC93ZNH2', 'B07L281N1M', 'B0DNZ5N7HY', 'B0C7V4BFSZ', 'B0B5L4HC95', 'B0DZCC4B4T', 'B0DZBQ4ZNC', 'B0C5QQFN1G', 'B0BFXD4LYN', 'B0F3122Q81', 'B0BG73XTFC', 'B0CNM269LZ', 'B08QS37RHQ', 'B087JP6PWL89']}, 'skip_asins_by_country': {'DE': ['B0FMKM1BYH', 'B0DJT78FNR', 'B0DJLJ73RB', 'B0D6QL2JMY', 'B0CFKLPKF5', 'B09BMWX6Q1', 'B07C4MNWFL', 'B0FHDGLQK7', 'B0FHD9S7K7', 'B0FB2FB6G6', 'B0F9W8VXWQ', 'B0F9W81F2F', 'B0F9W7KLNM', 'B0F2ZJWJNC', 'B0D49F7LM4', 'B0CXSP3PDP', 'B0CLXZHM2G', 'B0CG8RNWL6', 'B0C4SZ8GZW', 'B0BVM53J3B', 'B0BNHN9N9X', 'B0BMXBNMP5', 'B0BLN7D2JZ', 'B09MYVN5YK', 'B09MBNG7P4', 'B0DXTSTZC6', 'B0D25871WL', 'B0CL69XRTY', 'B0CKXMDBN8', 'B0CGDF2G8V', 'B0CFTGXVYF', 'B0CFT6XPPD', 'B09YXTN8JP', 'B0DHX32XJH', 'B0DHL7FQJM', 'B0DHL63M79', 'B0CWXYY74V', 'B0CWXYRGZW', 'B0CPLR6YHB', 'B0BXP4RC25', 'B0BFRRZDCH', 'B0B65R3N47', 'B0F36ZSWTQ', 'B0DT1DYWC6', 'B0DNZ5WX1L', 'B0DNZ5N7HY', 'B0DCJJ3PCM', 'B0D8JV9MB9', 'B0D2KCRLJ1', 'B0CRVRDS8C', 'B0CQK7QWJB', 'B0CQK5SRXW', 'B0BCL2DLPC', 'B0B9JBHPFY', 'B09TVY4V8Z', 'B0FJBD6Q3D', 'B0DG8LQLXZ', 'B0CD7Q1BX3', 'B0BV1SR48Y', 'B0F29CFJJ9', 'B0DSL6PKR7', 'B0CPV21W4L', 'B0CNQZ7FXY', 'B0CLY9XT5M', 'B0CGTMZ8GP', 'B0CF881PG4', 'B0CF4K1G4R', 'B0CB5T79XG', 'B0C73PQ9KM', 'B0C28TBN79', 'B09T31ZRJF', 'B01LBOYWNO', 'B0DDTB25YZ', 'B0D1Q8GL1Y', 'B0CW9JC84B', 'B0CLGJG36H', 'B0CF9JWKHJ', 'B0C2J3V92Q', 'B0BVVYZ6TN', 'B0B8J8VF7C', 'B0B4BFVZD1', 'B09R1CPKVX', 'B0924BNVH2', 'B0F54M2147', 'B0F1BZNBY6', 'B0F1BXV9WG', 'B0DC957VHC', 'B0D8778SZB', 'B0CP9WVSRY', 'B0CKNQP1PT', 'B0CJ4PW758', 'B0BYFD87C6', 'B0B492K9KT', 'B09968XMC7', 'B0FGJ8RCW1', 'B0FGJ4WK8B', 'B0FFMYGWHP', 'B0FD3DLFKL', 'B0FBWRWXG2', 'B0F99RW38N', 'B0F62LZ4G3', 'B0F629FLVV', 'B0F2N88DS9', 'B0F23Y11GD', 'B0F1YV8T2S', 'B0DXK9PR5X', 'B0DRV7QZ85', 'B0DRV7LN4V', 'B0DR4QYC2M', 'B0D5QL869M', 'B0D41VK3YL', 'B0D3XBW4KT', 'B0D2KW1ZRL', 'B0CPPC6PDW', 'B0CHVPSJ61', 'B0C5C1V5JD', 'B0C232Z6C6', 'B0C22KBMJC', 'B0BZVL6Y9X', 'B0BX58MW7N', 'B0B9M86GMC', 'B08DQVJ2VB', 'B07WKYLJ3T', 'B092D7P9BP', 'B0F9W2MZJY', 'B09KXV2C3T', 'B092D7TM65', 'B0CR3P2F5L', 'B0BWCSCYFL', 'B08JLW8S3Y', 'B0D4H73BJH', 'B08D77WTYG', 'B08D73XW2J', 'B09H4RB4ZL', 'B09WMQ29GH', 'B0DJ2M2RYC', 'B0BGF2QCKT', 'B0DS6FC8M7', 'B098QFLVDZ', 'B0DMD16GTD', 'B0CNVLLQ3J', 'B0CP3JZ1P5', 'B0C1GR4K6W', 'B0CBM792D2', 'B09NJK6K5T', 'B0D8Q5MK31', 'B0CYSCV19T', 'B0C5D5XPLJ', 'B09R3NFGH2', 'B09BG1QD9M', 'B092QY1823', 'B0C55PGK51', 'B0DRN8MXRG', 'B08152JKFB', 'B09Y1P7GK7', 'B0CG62X5GT', 'B0DHJKKZDD', 'B0DYHZ59YL', 'B0D2D5WS2D', 'B0G6LHVRKG', 'B0DQ53F84K', 'ASDASD', 'B0CJTCGDB7232', 'B0CJTCGDB7213', 'B0CJTCGDB79', 'B0CJTCGDB78', 'B0CJTCGDB77', 'B0CJTCGDB76', 'B0CJTCGDB75', 'B0CJTCGDB74', 'B0CJTCGDB73', 'B0CJTCGDB72', 'B0CJTCGDB71', 'B0CJTCGDB7', '1'], 'UK': ['B0FGJD2P9Q', 'B0DKBSWJ93', 'B0DJT78FNR', 'B0DFCR79L5', 'B0DCBJ3WY5', 'B0D9LPW1YB', 'B0D7VDQG19', 'B0CP5K31TL', 'B0CG3RG7RQ', 'B0C8DSPMNC', 'B0B6H8RCHC', 'B08TVWB36K', 'B08FZMZQ11', 'B0G6LHVRKG', 'B0FF36SY6Q', 'B0DYTNBMB2', 'B0DQNLGSMQ', 'B0DNSNN7NH', 'B0DJWHHFBS', 'B0DJHSJ9LS', 'B0D2D5WS2D', 'B0CG8RNWL6', 'B0C5D5XPLJ', 'B09WHYGBSR', 'B09WHV9JCJ', 'B09P4VCL2V', 'B09MYVN5YK', 'B09MBNG7P4', 'B0DM63394K', 'B0D1Q4RR99', 'B0CZD1HLY7', 'B0CWD9R2FS', 'B0CS6WYRFW', 'B0CRVTSQGQ', 'B0CR8FTHYP', 'B0CCB5Q5ZY', 'B0CC94W5G7', 'B0CC948Q8Q', 'B0CC93R18S', 'B0CC92F33P', 'B0BZ4DNWPY', 'B0BZ4CMQDL', 'B07Q4RBT32', 'B0DHX32XJH', 'B0DHX1HR7X', 'B0DHTT1X71', 'B0DHTRLQ2L', 'B0DHL7FQJM', 'B0D3V1BJX9', 'B0D3TZ45C3', 'B0D2SDNYS5', 'B0CQY5SM94', 'B0CQLN2FC1', 'B0CQF6W2JT', 'B0BY85GDF5', 'B0BY82BRTZ', 'B0B73K6QZQ', 'B0B4MDDD7D', 'B0F1X5Q9Q6', 'B0DYHPWTVJ', 'B0DQNJPS5M', 'B0DQ4B5SF7', 'B0DNZ5WX1L', 'B0DKX28SNX', 'B0DHVTGXPD', 'B0DHDBN98L', 'B0DFXRP8X1', 'B0DFGPGXY6', 'B0D8JV9MB9', 'B0D2KCRLJ1', 'B0CW63X5XY', 'B0CRVRDS8C', 'B0CRHWLDY4', 'B0CLZBR5CM', 'B0CL67FJFL', 'B0CG2C89ZZ', 'B0BP7YMCXH', 'B0BM8Y94QC', 'B0BJKBRTK7', 'B0BCL2DLPC', 'B0B6PSLTPF', 'B0B6PS4RQ1', 'B0B5L4HC95', 'B08ZT1PJ2K', 'B0FGQ31VWT', 'B0FFT33WRF', 'B0FD3S6DRN', 'B0F2GBR7MP', 'B0F2G5TG4S', 'B0F2FZVS7X', 'B0F2FYW3HN', 'B0F2B6924H', 'B0DSJMMMXN', 'B0DJT68ZM3', 'B0DJT66VG3', 'B0DJT5YJXD', 'B0DCGSFTYX', 'B0D8C6KL4G', 'B0C9H8MJC3', 'B0C85JQNRC', 'B0C5QQFN1G', 'B0BZ8FXRZG', 'B0BS8SW341', 'B0FLVP9N4Y', 'B0F2MPWSCQ', 'B0F29CFJJ9', 'B0DSKC1GQB', 'B0DSCBZSGS', 'B0DSCB329P', 'B0DLNXW8W5', 'B0DJQHVBZZ', 'B0DGCW7WBY', 'B0DFTG85NG', 'B0D99N1DZY', 'B0D8B849BJ', 'B0D89YCGCR', 'B0D89WK6JF', 'B0D89PMKKP', 'B0D7X1634D', 'B0D7V3W7D1', 'B0CYT3R89Z', 'B0CY7P61VD', 'B0CQ8B9PZM', 'B0CNK1SV4P', 'B0CF4K1G4R', 'B0CF4JJXHY', 'B0CF4BJ42D', 'B0C6T346MQ', 'B0BX2KDVMY', 'B0BR4YWSF9', 'B0BR4XHP5Z', 'B09Z68FK4Q', 'B09SL3PTTM', 'B09JBQZQQL', 'B08PSSRS3C', 'B08G4P4F76', 'B0DRD7T6TQ', 'B0DQXT78DT', 'B0DQXRV47R', 'B0DQXRV18S', 'B0DQXRLK5X', 'B0DQXQNL6Z', 'B0D2KQVS2Y', 'B0CXJC74TK', 'B0CW9JC84B', 'B0CSYBNXWX', 'B0CRHSTGX6', 'B0CPPJTX5C', 'B0CJB9WFHD', 'B0CF9JWKHJ', 'B0BVVYZ6TN', 'B0BND2YKJF', 'B09TRCQNQH', 'B09H5J6YQX', 'B09CNRP5FJ', 'B08BZ9LW9Z', 'B0F54M2147', 'B0F388HQLY', 'B0F1BZNBY6', 'B0F1BXV9WG', 'B0DSD949P4', 'B0DFQ2FP96', 'B0D9YXGFQW', 'B0CP9WVSRY', 'B0CKZ35SJC', 'B0CHYNDX87', 'B0CH66SNFC', 'B0BHSWP8RD', 'B0BGRW6858', 'B0B6JKD25B', 'B0B1LJB3M5', 'B09P3S4SH5', 'B09NLSRHGV', 'B0FVDPV5NS', 'B0FQJ5CDL2', 'B0FLJLWTLV', 'B0FGQ1VP2J', 'B0FD6YL7MM', 'B0FBWP9GJL', 'B0F99RW38N', 'B0F3Y3M538', 'B0F27ZM2TG', 'B0F23Y11GD', 'B0F23WZ78P', 'B0F1MY4PF6', 'B0DXKC6H1G', 'B0DQLNTLF4', 'B0DM8DYG77', 'B0DHJNBFVH', 'B0DH1XKG95', 'B0DFYG7C4K', 'B0DFTR1CMN', 'B0DB8FM915', 'B0D5H65VW3', 'B0D3XBW4KT', 'B0CRKJWJ4X', 'B0CPPGX6FQ', 'B0CPPGSBHD', 'B0CL9PVZD1', 'B0C232Z6C6', 'B0C22KBMJC', 'B0BX58MW7N', 'B0B6T3N5MG', 'B0B4S9QYWM', 'B0B4S93T7Q', 'B09R1PMW3J', 'B08MLN92JM', 'B0BHN71FWM', 'B09NKRWXPC', 'B0C89TNVTN', 'B0D1VDMZR1', 'B0DS2QT1YZ', 'B09Y29P5YF', 'B0DBQDSHVZ', 'B0CFF62JY3', 'B0BHPGTWQR', 'B0D4F4D3W2', 'B0F13BRF4V', 'B0D1FXTY99', 'B00KY0LLFO', 'B0B7MJXDZC', 'B0B4DV8NLV', 'B0DJXRFCF7', 'B0BPHMFD7V', 'B0F281ZQWR', 'B0D7V2FNW3', 'B0D57X7KV9', 'B0CJ2SST5Z', 'B0CZ92LPP4', 'B0CG5Z2Z44', 'B0D6T9H5LX', 'B0CQJNR9MP', 'ZXCA', 'B0CJTGDB7232', 'B0CJTGDB7213', 'B0CJTGDB79', 'B0CJTGDB78', 'B0CJTGDB77', 'B0CJCGDB76', 'B0CJCGDB75', 'B0CJCGDB74', 'B0CJCGDB73', 'B0CJCGDB72', 'B0CJCGDB71', 'B0CJCGDB7', 'B0CTCGDB7', 'B0FGTNLTG3', 'WQDASDAS', 'SADAD', 'DFHFGH78979'], 'FR': ['B0FY6JY4BF', 'B0DZNRB5Q1', 'B0DNSKFLGK', 'B0DGLCQDTR', 'B0D9W93869', 'B0CG8RNWL6', 'B0BX94RYT1', 'B0BLN7D2JZ', 'B09WHV9JCJ', 'B0DKX28SNX', 'B0CZCVTMRJ', 'B0CC95H28M', 'B0CC942XSZ', 'B0CC93WW6S', 'B0CC93TDB9', 'B0816GGCFY', 'B0CQY5SM94', 'B0DQTKRNLH', 'B0DCB85F3J', 'B0D8JR3W53', 'B0D2KCRLJ1', 'B0CQK7QWJB', 'B0B9XM29C9', 'B0B5L4HC95', 'B0F9FCT8RD', 'B0BZ8J16JM', 'B0BV1TRMGN', 'B09G68LMB3', 'B0DCRJSMJP', 'B093Y447R2', 'B0D8Y5KWB8', 'B0BJK43Z6X', 'B0DXK7YZ5D', 'B0C232Z6C6', 'B0BZVL6Y9X', 'B0B4S98HLZ', 'B09VPMBMB8', 'B0D17L6YKN'], 'IT': ['B0DTK87KJ5', 'B0DNSNN7NH', 'B0D9W93869', 'B0CLXZHM2G', 'B0CG8RNWL6', 'B0BX94RYT1', 'B0BVM53J3B', 'B0BLN7D2JZ', 'B0F9KVS6Y2', 'B0F8QGHMLL', 'B0CY5KW8RY', 'B0CC94P4H1', 'B0CC94K2HN', 'B0CC922QN8', 'B0C7Q51PL1', 'B0D8JR3W53', 'B0B5L4HC95', 'B0C5C53444', 'B0F5PRVRLF', 'B0BJK43Z6X', 'B09WKFD3G8', 'B08NT2BNGH', 'B0DRV6D214', 'B08QRKHD3C', 'B09JMW3F1R', 'B0CKYHTDJ5', 'B098QFLVDZ', '32EGDFGS4', 'B087JP6PWL89'], 'ES': ['B0D2J11TKY', 'B0CLY2THTY', 'B0D8JS8NPH', 'B0CYGSBXSQ', 'B0CKXMDBN8', 'B0CJ5F9J4Z', 'B0CC95XMD4', 'B0CC93ZNH2', 'B07L281N1M', 'B0DNZ5N7HY', 'B0C7V4BFSZ', 'B0B5L4HC95', 'B0DZCC4B4T', 'B0DZBQ4ZNC', 'B0C5QQFN1G', 'B0BFXD4LYN', 'B0F3122Q81', 'B0BG73XTFC', 'B0CNM269LZ', 'B08QS37RHQ', 'B087JP6PWL89']}, 'skip_asin_details_by_country': {'DE': [{'asin': 'B0FMKM1BYH', 'minimumPrice': ''}, {'asin': 'B0DJT78FNR', 'minimumPrice': ''}, {'asin': 'B0DJLJ73RB', 'minimumPrice': ''}, {'asin': 'B0D6QL2JMY', 'minimumPrice': ''}, {'asin': 'B0CFKLPKF5', 'minimumPrice': ''}, {'asin': 'B09BMWX6Q1', 'minimumPrice': ''}, {'asin': 'B07C4MNWFL', 'minimumPrice': ''}, {'asin': 'B0FHDGLQK7', 'minimumPrice': ''}, {'asin': 'B0FHD9S7K7', 'minimumPrice': ''}, {'asin': 'B0FB2FB6G6', 'minimumPrice': ''}, {'asin': 'B0F9W8VXWQ', 'minimumPrice': ''}, {'asin': 'B0F9W81F2F', 'minimumPrice': ''}, {'asin': 'B0F9W7KLNM', 'minimumPrice': ''}, {'asin': 'B0F2ZJWJNC', 'minimumPrice': ''}, {'asin': 'B0D49F7LM4', 'minimumPrice': ''}, {'asin': 'B0CXSP3PDP', 'minimumPrice': ''}, {'asin': 'B0CLXZHM2G', 'minimumPrice': ''}, {'asin': 'B0CG8RNWL6', 'minimumPrice': ''}, {'asin': 'B0C4SZ8GZW', 'minimumPrice': ''}, {'asin': 'B0BVM53J3B', 'minimumPrice': ''}, {'asin': 'B0BNHN9N9X', 'minimumPrice': ''}, {'asin': 'B0BMXBNMP5', 'minimumPrice': ''}, {'asin': 'B0BLN7D2JZ', 'minimumPrice': ''}, {'asin': 'B09MYVN5YK', 'minimumPrice': ''}, {'asin': 'B09MBNG7P4', 'minimumPrice': ''}, {'asin': 'B09MBNG7P4', 'minimumPrice': '30.28'}, {'asin': 'B0DXTSTZC6', 'minimumPrice': ''}, {'asin': 'B0D25871WL', 'minimumPrice': ''}, {'asin': 'B0CL69XRTY', 'minimumPrice': ''}, {'asin': 'B0CKXMDBN8', 'minimumPrice': ''}, {'asin': 'B0CGDF2G8V', 'minimumPrice': ''}, {'asin': 'B0CFTGXVYF', 'minimumPrice': ''}, {'asin': 'B0CFT6XPPD', 'minimumPrice': ''}, {'asin': 'B09YXTN8JP', 'minimumPrice': ''}, {'asin': 'B0DHX32XJH', 'minimumPrice': ''}, {'asin': 'B0DHL7FQJM', 'minimumPrice': ''}, {'asin': 'B0DHL63M79', 'minimumPrice': ''}, {'asin': 'B0CWXYY74V', 'minimumPrice': ''}, {'asin': 'B0CWXYRGZW', 'minimumPrice': ''}, {'asin': 'B0CPLR6YHB', 'minimumPrice': ''}, {'asin': 'B0BXP4RC25', 'minimumPrice': ''}, {'asin': 'B0BFRRZDCH', 'minimumPrice': ''}, {'asin': 'B0B65R3N47', 'minimumPrice': ''}, {'asin': 'B0FB2FB6G6', 'minimumPrice': ''}, {'asin': 'B0F36ZSWTQ', 'minimumPrice': ''}, {'asin': 'B0DT1DYWC6', 'minimumPrice': ''}, {'asin': 'B0DNZ5WX1L', 'minimumPrice': ''}, {'asin': 'B0DNZ5N7HY', 'minimumPrice': ''}, {'asin': 'B0DCJJ3PCM', 'minimumPrice': ''}, {'asin': 'B0D8JV9MB9', 'minimumPrice': ''}, {'asin': 'B0D2KCRLJ1', 'minimumPrice': ''}, {'asin': 'B0CRVRDS8C', 'minimumPrice': ''}, {'asin': 'B0CQK7QWJB', 'minimumPrice': ''}, {'asin': 'B0CQK5SRXW', 'minimumPrice': ''}, {'asin': 'B0BCL2DLPC', 'minimumPrice': ''}, {'asin': 'B0B9JBHPFY', 'minimumPrice': ''}, {'asin': 'B09TVY4V8Z', 'minimumPrice': ''}, {'asin': 'B0FJBD6Q3D', 'minimumPrice': ''}, {'asin': 'B0DG8LQLXZ', 'minimumPrice': ''}, {'asin': 'B0CD7Q1BX3', 'minimumPrice': ''}, {'asin': 'B0BV1SR48Y', 'minimumPrice': ''}, {'asin': 'B0F29CFJJ9', 'minimumPrice': '26.70'}, {'asin': 'B0DSL6PKR7', 'minimumPrice': '8.80'}, {'asin': 'B0CPV21W4L', 'minimumPrice': '21.87'}, {'asin': 'B0CNQZ7FXY', 'minimumPrice': '74.46'}, {'asin': 'B0CLY9XT5M', 'minimumPrice': '19.72'}, {'asin': 'B0CGTMZ8GP', 'minimumPrice': '33.44'}, {'asin': 'B0CF881PG4', 'minimumPrice': '31.82'}, {'asin': 'B0CF4K1G4R', 'minimumPrice': '31.44'}, {'asin': 'B0CB5T79XG', 'minimumPrice': '16.59'}, {'asin': 'B0C73PQ9KM', 'minimumPrice': '20.92'}, {'asin': 'B0C28TBN79', 'minimumPrice': '12.70'}, {'asin': 'B09T31ZRJF', 'minimumPrice': '35.64'}, {'asin': 'B01LBOYWNO', 'minimumPrice': '11.21'}, {'asin': 'B0DDTB25YZ', 'minimumPrice': ''}, {'asin': 'B0D1Q8GL1Y', 'minimumPrice': ''}, {'asin': 'B0CW9JC84B', 'minimumPrice': ''}, {'asin': 'B0CLGJG36H', 'minimumPrice': ''}, {'asin': 'B0CF9JWKHJ', 'minimumPrice': ''}, {'asin': 'B0C2J3V92Q', 'minimumPrice': ''}, {'asin': 'B0BVVYZ6TN', 'minimumPrice': ''}, {'asin': 'B0B8J8VF7C', 'minimumPrice': ''}, {'asin': 'B0B4BFVZD1', 'minimumPrice': ''}, {'asin': 'B09R1CPKVX', 'minimumPrice': ''}, {'asin': 'B0924BNVH2', 'minimumPrice': ''}, {'asin': 'B0F54M2147', 'minimumPrice': ''}, {'asin': 'B0F1BZNBY6', 'minimumPrice': ''}, {'asin': 'B0F1BXV9WG', 'minimumPrice': ''}, {'asin': 'B0DC957VHC', 'minimumPrice': ''}, {'asin': 'B0D8778SZB', 'minimumPrice': ''}, {'asin': 'B0CP9WVSRY', 'minimumPrice': ''}, {'asin': 'B0CKNQP1PT', 'minimumPrice': ''}, {'asin': 'B0CJ4PW758', 'minimumPrice': ''}, {'asin': 'B0BYFD87C6', 'minimumPrice': ''}, {'asin': 'B0B492K9KT', 'minimumPrice': ''}, {'asin': 'B09968XMC7', 'minimumPrice': ''}, {'asin': 'B0FGJ8RCW1', 'minimumPrice': '12.09'}, {'asin': 'B0FGJ4WK8B', 'minimumPrice': '19.77'}, {'asin': 'B0FFMYGWHP', 'minimumPrice': '34.02'}, {'asin': 'B0FD3DLFKL', 'minimumPrice': '14.13'}, {'asin': 'B0FBWRWXG2', 'minimumPrice': '10.53'}, {'asin': 'B0F99RW38N', 'minimumPrice': '14.21'}, {'asin': 'B0F62LZ4G3', 'minimumPrice': '19.95'}, {'asin': 'B0F629FLVV', 'minimumPrice': '22.62'}, {'asin': 'B0F2N88DS9', 'minimumPrice': '18.43'}, {'asin': 'B0F23Y11GD', 'minimumPrice': '14.54'}, {'asin': 'B0F1YV8T2S', 'minimumPrice': '10.38'}, {'asin': 'B0DXK9PR5X', 'minimumPrice': '18.44'}, {'asin': 'B0DRV7QZ85', 'minimumPrice': '17.72'}, {'asin': 'B0DRV7LN4V', 'minimumPrice': '18.91'}, {'asin': 'B0DR4QYC2M', 'minimumPrice': '15.46'}, {'asin': 'B0D5QL869M', 'minimumPrice': '31.89'}, {'asin': 'B0D41VK3YL', 'minimumPrice': '15.94'}, {'asin': 'B0D3XBW4KT', 'minimumPrice': '26.18'}, {'asin': 'B0D2KW1ZRL', 'minimumPrice': '32.36'}, {'asin': 'B0CPPC6PDW', 'minimumPrice': '27.00'}, {'asin': 'B0CHVPSJ61', 'minimumPrice': '31.30'}, {'asin': 'B0C5C1V5JD', 'minimumPrice': '33.84'}, {'asin': 'B0C232Z6C6', 'minimumPrice': '42.55'}, {'asin': 'B0C22KBMJC', 'minimumPrice': '35.85'}, {'asin': 'B0BZVL6Y9X', 'minimumPrice': '50.46'}, {'asin': 'B0BX58MW7N', 'minimumPrice': '26.73'}, {'asin': 'B0B9M86GMC', 'minimumPrice': '15.93'}, {'asin': 'B08DQVJ2VB', 'minimumPrice': '37.30'}, {'asin': 'B07WKYLJ3T', 'minimumPrice': '64.75'}, {'asin': 'B092D7P9BP', 'minimumPrice': '28.12'}, {'asin': 'B0F9W2MZJY', 'minimumPrice': '77.28'}, {'asin': 'B09KXV2C3T', 'minimumPrice': '27.74'}, {'asin': 'B092D7TM65', 'minimumPrice': '29.44'}, {'asin': 'B0CR3P2F5L', 'minimumPrice': '47.54'}, {'asin': 'B0BWCSCYFL', 'minimumPrice': '45.18'}, {'asin': 'B08JLW8S3Y', 'minimumPrice': '28.56'}, {'asin': 'B0D4H73BJH', 'minimumPrice': '19.57'}, {'asin': 'B08D77WTYG', 'minimumPrice': '30.35'}, {'asin': 'B08D73XW2J', 'minimumPrice': '31.35'}, {'asin': 'B09H4RB4ZL', 'minimumPrice': '25.08'}, {'asin': 'B09WMQ29GH', 'minimumPrice': '29.45'}, {'asin': 'B0DJ2M2RYC', 'minimumPrice': '13.87'}, {'asin': 'B0BGF2QCKT', 'minimumPrice': '35.52'}, {'asin': 'B0DS6FC8M7', 'minimumPrice': '14.94'}, {'asin': 'B098QFLVDZ', 'minimumPrice': '66.04'}, {'asin': 'B0DMD16GTD', 'minimumPrice': '21.90'}, {'asin': 'B0CNVLLQ3J', 'minimumPrice': '42.27'}, {'asin': 'B0CP3JZ1P5', 'minimumPrice': '37.09'}, {'asin': 'B0C1GR4K6W', 'minimumPrice': '21.71'}, {'asin': 'B0CBM792D2', 'minimumPrice': '17.03'}, {'asin': 'B09NJK6K5T', 'minimumPrice': '22.51'}, {'asin': 'B0D8Q5MK31', 'minimumPrice': '21.37'}, {'asin': 'B0CYSCV19T', 'minimumPrice': '32.71'}, {'asin': 'B0C5D5XPLJ', 'minimumPrice': '50.72'}, {'asin': 'B09R3NFGH2', 'minimumPrice': '16.51'}, {'asin': 'B09BG1QD9M', 'minimumPrice': '23.84'}, {'asin': 'B092QY1823', 'minimumPrice': '16.86'}, {'asin': 'B0C55PGK51', 'minimumPrice': '13.77'}, {'asin': 'B0DRN8MXRG', 'minimumPrice': '29.13'}, {'asin': 'B08152JKFB', 'minimumPrice': '21.98'}, {'asin': 'B09Y1P7GK7', 'minimumPrice': '29.43'}, {'asin': 'B0CG62X5GT', 'minimumPrice': '16.88'}, {'asin': 'B0DHJKKZDD', 'minimumPrice': '17.96'}, {'asin': 'B0DYHZ59YL', 'minimumPrice': '20.74'}, {'asin': 'B0CG8RNWL6', 'minimumPrice': '28.08'}, {'asin': 'B0BLN7D2JZ', 'minimumPrice': '27.15'}, {'asin': 'B0D2D5WS2D', 'minimumPrice': '40.27'}, {'asin': 'B0G6LHVRKG', 'minimumPrice': '23.41'}, {'asin': 'B0DQ53F84K', 'minimumPrice': '53.79'}, {'asin': 'ASDASD', 'minimumPrice': '90.00'}, {'asin': 'B0CJTCGDB7232', 'minimumPrice': '32.00'}, {'asin': 'B0CJTCGDB7213', 'minimumPrice': '23.00'}, {'asin': 'B0CJTCGDB79', 'minimumPrice': '32.00'}, {'asin': 'B0CJTCGDB78', 'minimumPrice': '321.00'}, {'asin': 'B0CJTCGDB77', 'minimumPrice': '32.00'}, {'asin': 'B0CJTCGDB76', 'minimumPrice': '32.00'}, {'asin': 'B0CJTCGDB75', 'minimumPrice': '321.00'}, {'asin': 'B0CJTCGDB74', 'minimumPrice': '21.00'}, {'asin': 'B0CJTCGDB73', 'minimumPrice': '231.00'}, {'asin': 'B0CJTCGDB72', 'minimumPrice': '21.00'}, {'asin': 'B0CJTCGDB71', 'minimumPrice': '20.00'}, {'asin': 'B0CJTCGDB7', 'minimumPrice': '15.00'}, {'asin': '1', 'minimumPrice': '1.00'}, {'asin': 'ASDASD', 'minimumPrice': '50.00'}], 'UK': [{'asin': 'B0FGJD2P9Q', 'minimumPrice': ''}, {'asin': 'B0DKBSWJ93', 'minimumPrice': ''}, {'asin': 'B0DJT78FNR', 'minimumPrice': ''}, {'asin': 'B0DFCR79L5', 'minimumPrice': ''}, {'asin': 'B0DCBJ3WY5', 'minimumPrice': ''}, {'asin': 'B0D9LPW1YB', 'minimumPrice': ''}, {'asin': 'B0D7VDQG19', 'minimumPrice': ''}, {'asin': 'B0CP5K31TL', 'minimumPrice': ''}, {'asin': 'B0CG3RG7RQ', 'minimumPrice': ''}, {'asin': 'B0C8DSPMNC', 'minimumPrice': ''}, {'asin': 'B0B6H8RCHC', 'minimumPrice': ''}, {'asin': 'B08TVWB36K', 'minimumPrice': ''}, {'asin': 'B08FZMZQ11', 'minimumPrice': ''}, {'asin': 'B0G6LHVRKG', 'minimumPrice': ''}, {'asin': 'B0FF36SY6Q', 'minimumPrice': ''}, {'asin': 'B0DYTNBMB2', 'minimumPrice': ''}, {'asin': 'B0DQNLGSMQ', 'minimumPrice': ''}, {'asin': 'B0DNSNN7NH', 'minimumPrice': ''}, {'asin': 'B0DJWHHFBS', 'minimumPrice': ''}, {'asin': 'B0DJHSJ9LS', 'minimumPrice': ''}, {'asin': 'B0D2D5WS2D', 'minimumPrice': ''}, {'asin': 'B0CG8RNWL6', 'minimumPrice': ''}, {'asin': 'B0C5D5XPLJ', 'minimumPrice': ''}, {'asin': 'B09WHYGBSR', 'minimumPrice': ''}, {'asin': 'B09WHV9JCJ', 'minimumPrice': ''}, {'asin': 'B09P4VCL2V', 'minimumPrice': ''}, {'asin': 'B09MYVN5YK', 'minimumPrice': ''}, {'asin': 'B09MBNG7P4', 'minimumPrice': ''}, {'asin': 'B0DM63394K', 'minimumPrice': ''}, {'asin': 'B0D1Q4RR99', 'minimumPrice': ''}, {'asin': 'B0CZD1HLY7', 'minimumPrice': ''}, {'asin': 'B0CWD9R2FS', 'minimumPrice': ''}, {'asin': 'B0CS6WYRFW', 'minimumPrice': ''}, {'asin': 'B0CRVTSQGQ', 'minimumPrice': ''}, {'asin': 'B0CR8FTHYP', 'minimumPrice': ''}, {'asin': 'B0CCB5Q5ZY', 'minimumPrice': ''}, {'asin': 'B0CC94W5G7', 'minimumPrice': ''}, {'asin': 'B0CC948Q8Q', 'minimumPrice': ''}, {'asin': 'B0CC93R18S', 'minimumPrice': ''}, {'asin': 'B0CC92F33P', 'minimumPrice': ''}, {'asin': 'B0BZ4DNWPY', 'minimumPrice': ''}, {'asin': 'B0BZ4CMQDL', 'minimumPrice': ''}, {'asin': 'B07Q4RBT32', 'minimumPrice': ''}, {'asin': 'B0DHX32XJH', 'minimumPrice': ''}, {'asin': 'B0DHX1HR7X', 'minimumPrice': ''}, {'asin': 'B0DHTT1X71', 'minimumPrice': ''}, {'asin': 'B0DHTRLQ2L', 'minimumPrice': ''}, {'asin': 'B0DHL7FQJM', 'minimumPrice': ''}, {'asin': 'B0D3V1BJX9', 'minimumPrice': ''}, {'asin': 'B0D3TZ45C3', 'minimumPrice': ''}, {'asin': 'B0D2SDNYS5', 'minimumPrice': ''}, {'asin': 'B0CQY5SM94', 'minimumPrice': ''}, {'asin': 'B0CQLN2FC1', 'minimumPrice': ''}, {'asin': 'B0CQF6W2JT', 'minimumPrice': ''}, {'asin': 'B0BY85GDF5', 'minimumPrice': ''}, {'asin': 'B0BY82BRTZ', 'minimumPrice': ''}, {'asin': 'B0B73K6QZQ', 'minimumPrice': ''}, {'asin': 'B0B4MDDD7D', 'minimumPrice': ''}, {'asin': 'B0F1X5Q9Q6', 'minimumPrice': ''}, {'asin': 'B0DYHPWTVJ', 'minimumPrice': ''}, {'asin': 'B0DQNJPS5M', 'minimumPrice': ''}, {'asin': 'B0DQ4B5SF7', 'minimumPrice': ''}, {'asin': 'B0DNZ5WX1L', 'minimumPrice': ''}, {'asin': 'B0DKX28SNX', 'minimumPrice': ''}, {'asin': 'B0DHVTGXPD', 'minimumPrice': ''}, {'asin': 'B0DHDBN98L', 'minimumPrice': ''}, {'asin': 'B0DFXRP8X1', 'minimumPrice': ''}, {'asin': 'B0DFGPGXY6', 'minimumPrice': ''}, {'asin': 'B0D8JV9MB9', 'minimumPrice': ''}, {'asin': 'B0D2KCRLJ1', 'minimumPrice': ''}, {'asin': 'B0CW63X5XY', 'minimumPrice': ''}, {'asin': 'B0CRVRDS8C', 'minimumPrice': ''}, {'asin': 'B0CRHWLDY4', 'minimumPrice': ''}, {'asin': 'B0CLZBR5CM', 'minimumPrice': ''}, {'asin': 'B0CL67FJFL', 'minimumPrice': ''}, {'asin': 'B0CG2C89ZZ', 'minimumPrice': ''}, {'asin': 'B0BP7YMCXH', 'minimumPrice': ''}, {'asin': 'B0BM8Y94QC', 'minimumPrice': ''}, {'asin': 'B0BJKBRTK7', 'minimumPrice': ''}, {'asin': 'B0BCL2DLPC', 'minimumPrice': ''}, {'asin': 'B0B6PSLTPF', 'minimumPrice': ''}, {'asin': 'B0B6PS4RQ1', 'minimumPrice': ''}, {'asin': 'B0B5L4HC95', 'minimumPrice': ''}, {'asin': 'B08ZT1PJ2K', 'minimumPrice': ''}, {'asin': 'B0FGQ31VWT', 'minimumPrice': ''}, {'asin': 'B0FFT33WRF', 'minimumPrice': ''}, {'asin': 'B0FD3S6DRN', 'minimumPrice': ''}, {'asin': 'B0F2GBR7MP', 'minimumPrice': ''}, {'asin': 'B0F2G5TG4S', 'minimumPrice': ''}, {'asin': 'B0F2FZVS7X', 'minimumPrice': ''}, {'asin': 'B0F2FYW3HN', 'minimumPrice': ''}, {'asin': 'B0F2B6924H', 'minimumPrice': ''}, {'asin': 'B0DSJMMMXN', 'minimumPrice': ''}, {'asin': 'B0DJT68ZM3', 'minimumPrice': ''}, {'asin': 'B0DJT66VG3', 'minimumPrice': ''}, {'asin': 'B0DJT5YJXD', 'minimumPrice': ''}, {'asin': 'B0DCGSFTYX', 'minimumPrice': ''}, {'asin': 'B0D8C6KL4G', 'minimumPrice': ''}, {'asin': 'B0C9H8MJC3', 'minimumPrice': ''}, {'asin': 'B0C85JQNRC', 'minimumPrice': ''}, {'asin': 'B0C5QQFN1G', 'minimumPrice': ''}, {'asin': 'B0BZ8FXRZG', 'minimumPrice': ''}, {'asin': 'B0BS8SW341', 'minimumPrice': ''}, {'asin': 'B0FLVP9N4Y', 'minimumPrice': '6.16'}, {'asin': 'B0F2MPWSCQ', 'minimumPrice': '7.51'}, {'asin': 'B0F29CFJJ9', 'minimumPrice': '21.91'}, {'asin': 'B0DSKC1GQB', 'minimumPrice': '25.52'}, {'asin': 'B0DSCBZSGS', 'minimumPrice': '18.23'}, {'asin': 'B0DSCB329P', 'minimumPrice': '19.00'}, {'asin': 'B0DLNXW8W5', 'minimumPrice': '6.44'}, {'asin': 'B0DJQHVBZZ', 'minimumPrice': '17.62'}, {'asin': 'B0DGCW7WBY', 'minimumPrice': '24.73'}, {'asin': 'B0DFTG85NG', 'minimumPrice': '8.54'}, {'asin': 'B0D99N1DZY', 'minimumPrice': '37.69'}, {'asin': 'B0D8B849BJ', 'minimumPrice': '38.90'}, {'asin': 'B0D89YCGCR', 'minimumPrice': '38.97'}, {'asin': 'B0D89WK6JF', 'minimumPrice': '37.82'}, {'asin': 'B0D89PMKKP', 'minimumPrice': '36.47'}, {'asin': 'B0D7X1634D', 'minimumPrice': '15.56'}, {'asin': 'B0D7V3W7D1', 'minimumPrice': '76.14'}, {'asin': 'B0CYT3R89Z', 'minimumPrice': '68.79'}, {'asin': 'B0CY7P61VD', 'minimumPrice': '32.79'}, {'asin': 'B0CQ8B9PZM', 'minimumPrice': '31.31'}, {'asin': 'B0CNK1SV4P', 'minimumPrice': '16.61'}, {'asin': 'B0CF4K1G4R', 'minimumPrice': '48.07'}, {'asin': 'B0CF4JJXHY', 'minimumPrice': '34.72'}, {'asin': 'B0CF4BJ42D', 'minimumPrice': '24.82'}, {'asin': 'B0C6T346MQ', 'minimumPrice': '11.52'}, {'asin': 'B0BX2KDVMY', 'minimumPrice': '23.14'}, {'asin': 'B0BR4YWSF9', 'minimumPrice': '24.32'}, {'asin': 'B0BR4XHP5Z', 'minimumPrice': '24.71'}, {'asin': 'B09Z68FK4Q', 'minimumPrice': '17.34'}, {'asin': 'B09SL3PTTM', 'minimumPrice': '21.86'}, {'asin': 'B09JBQZQQL', 'minimumPrice': '13.87'}, {'asin': 'B08PSSRS3C', 'minimumPrice': '8.06'}, {'asin': 'B08G4P4F76', 'minimumPrice': '11.61'}, {'asin': 'B0DRD7T6TQ', 'minimumPrice': ''}, {'asin': 'B0DQXT78DT', 'minimumPrice': ''}, {'asin': 'B0DQXRV47R', 'minimumPrice': ''}, {'asin': 'B0DQXRV18S', 'minimumPrice': ''}, {'asin': 'B0DQXRLK5X', 'minimumPrice': ''}, {'asin': 'B0DQXQNL6Z', 'minimumPrice': ''}, {'asin': 'B0D2KQVS2Y', 'minimumPrice': ''}, {'asin': 'B0CXJC74TK', 'minimumPrice': ''}, {'asin': 'B0CW9JC84B', 'minimumPrice': ''}, {'asin': 'B0CSYBNXWX', 'minimumPrice': ''}, {'asin': 'B0CRHSTGX6', 'minimumPrice': ''}, {'asin': 'B0CPPJTX5C', 'minimumPrice': ''}, {'asin': 'B0CJB9WFHD', 'minimumPrice': ''}, {'asin': 'B0CF9JWKHJ', 'minimumPrice': ''}, {'asin': 'B0BVVYZ6TN', 'minimumPrice': ''}, {'asin': 'B0BND2YKJF', 'minimumPrice': ''}, {'asin': 'B09TRCQNQH', 'minimumPrice': ''}, {'asin': 'B09H5J6YQX', 'minimumPrice': ''}, {'asin': 'B09CNRP5FJ', 'minimumPrice': ''}, {'asin': 'B08BZ9LW9Z', 'minimumPrice': ''}, {'asin': 'B0F54M2147', 'minimumPrice': ''}, {'asin': 'B0F388HQLY', 'minimumPrice': ''}, {'asin': 'B0F1BZNBY6', 'minimumPrice': ''}, {'asin': 'B0F1BXV9WG', 'minimumPrice': ''}, {'asin': 'B0DSD949P4', 'minimumPrice': ''}, {'asin': 'B0DFQ2FP96', 'minimumPrice': ''}, {'asin': 'B0D9YXGFQW', 'minimumPrice': ''}, {'asin': 'B0CP9WVSRY', 'minimumPrice': ''}, {'asin': 'B0CKZ35SJC', 'minimumPrice': ''}, {'asin': 'B0CHYNDX87', 'minimumPrice': ''}, {'asin': 'B0CH66SNFC', 'minimumPrice': ''}, {'asin': 'B0BHSWP8RD', 'minimumPrice': ''}, {'asin': 'B0BGRW6858', 'minimumPrice': ''}, {'asin': 'B0B6JKD25B', 'minimumPrice': ''}, {'asin': 'B0B1LJB3M5', 'minimumPrice': ''}, {'asin': 'B09P3S4SH5', 'minimumPrice': ''}, {'asin': 'B09NLSRHGV', 'minimumPrice': ''}, {'asin': 'B0FVDPV5NS', 'minimumPrice': '16.54'}, {'asin': 'B0FQJ5CDL2', 'minimumPrice': '16.44'}, {'asin': 'B0FLJLWTLV', 'minimumPrice': '14.91'}, {'asin': 'B0FGQ1VP2J', 'minimumPrice': '18.19'}, {'asin': 'B0FD6YL7MM', 'minimumPrice': '10.10'}, {'asin': 'B0FBWP9GJL', 'minimumPrice': '11.34'}, {'asin': 'B0F99RW38N', 'minimumPrice': '21.47'}, {'asin': 'B0F3Y3M538', 'minimumPrice': '16.85'}, {'asin': 'B0F27ZM2TG', 'minimumPrice': '15.78'}, {'asin': 'B0F23Y11GD', 'minimumPrice': '16.44'}, {'asin': 'B0F23WZ78P', 'minimumPrice': '12.64'}, {'asin': 'B0F1MY4PF6', 'minimumPrice': '5.96'}, {'asin': 'B0DXKC6H1G', 'minimumPrice': '16.49'}, {'asin': 'B0DQLNTLF4', 'minimumPrice': '7.20'}, {'asin': 'B0DM8DYG77', 'minimumPrice': '13.80'}, {'asin': 'B0DHJNBFVH', 'minimumPrice': '11.19'}, {'asin': 'B0DH1XKG95', 'minimumPrice': '19.34'}, {'asin': 'B0DFYG7C4K', 'minimumPrice': '10.85'}, {'asin': 'B0DFTR1CMN', 'minimumPrice': '15.79'}, {'asin': 'B0DB8FM915', 'minimumPrice': '24.17'}, {'asin': 'B0D5H65VW3', 'minimumPrice': '9.83'}, {'asin': 'B0D3XBW4KT', 'minimumPrice': '19.21'}, {'asin': 'B0CRKJWJ4X', 'minimumPrice': '17.66'}, {'asin': 'B0CPPGX6FQ', 'minimumPrice': '40.53'}, {'asin': 'B0CPPGSBHD', 'minimumPrice': '32.58'}, {'asin': 'B0CL9PVZD1', 'minimumPrice': '24.79'}, {'asin': 'B0C232Z6C6', 'minimumPrice': '32.87'}, {'asin': 'B0C22KBMJC', 'minimumPrice': '25.72'}, {'asin': 'B0BX58MW7N', 'minimumPrice': '22.04'}, {'asin': 'B0B6T3N5MG', 'minimumPrice': '15.33'}, {'asin': 'B0B4S9QYWM', 'minimumPrice': '14.36'}, {'asin': 'B0B4S93T7Q', 'minimumPrice': '14.57'}, {'asin': 'B09R1PMW3J', 'minimumPrice': '14.07'}, {'asin': 'B08MLN92JM', 'minimumPrice': '9.63'}, {'asin': 'B0BHN71FWM', 'minimumPrice': '18.09'}, {'asin': 'B09NKRWXPC', 'minimumPrice': '12.53'}, {'asin': 'B0C89TNVTN', 'minimumPrice': '39.64'}, {'asin': 'B0D1VDMZR1', 'minimumPrice': '21.46'}, {'asin': 'B0DS2QT1YZ', 'minimumPrice': '23.49'}, {'asin': 'B09Y29P5YF', 'minimumPrice': '20.23'}, {'asin': 'B0DBQDSHVZ', 'minimumPrice': '31.03'}, {'asin': 'B0CFF62JY3', 'minimumPrice': '21.88'}, {'asin': 'B0BHPGTWQR', 'minimumPrice': '28.06'}, {'asin': 'B0D4F4D3W2', 'minimumPrice': '18.17'}, {'asin': 'B0F13BRF4V', 'minimumPrice': '14.89'}, {'asin': 'B0D1FXTY99', 'minimumPrice': '14.36'}, {'asin': 'B00KY0LLFO', 'minimumPrice': '16.48'}, {'asin': 'B0B7MJXDZC', 'minimumPrice': '18.28'}, {'asin': 'B0B4DV8NLV', 'minimumPrice': '18.09'}, {'asin': 'B0DJXRFCF7', 'minimumPrice': '34.13'}, {'asin': 'B0BPHMFD7V', 'minimumPrice': '17.16'}, {'asin': 'B0F281ZQWR', 'minimumPrice': '24.51'}, {'asin': 'B0D7V2FNW3', 'minimumPrice': '49.89'}, {'asin': 'B0D57X7KV9', 'minimumPrice': '15.04'}, {'asin': 'B0CJ2SST5Z', 'minimumPrice': '21.25'}, {'asin': 'B0CZ92LPP4', 'minimumPrice': '8.74'}, {'asin': 'B0CG5Z2Z44', 'minimumPrice': '13.24'}, {'asin': 'B0D6T9H5LX', 'minimumPrice': '14.42'}, {'asin': 'B0CQJNR9MP', 'minimumPrice': '10.64'}, {'asin': 'ZXCA', 'minimumPrice': '60.00'}, {'asin': 'B0CJTGDB7232', 'minimumPrice': '854.00'}, {'asin': 'B0CJTGDB7213', 'minimumPrice': '54.00'}, {'asin': 'B0CJTGDB79', 'minimumPrice': '54.00'}, {'asin': 'B0CJTGDB78', 'minimumPrice': '54.00'}, {'asin': 'B0CJTGDB77', 'minimumPrice': '4.00'}, {'asin': 'B0CJCGDB76', 'minimumPrice': '458.00'}, {'asin': 'B0CJCGDB75', 'minimumPrice': '45.00'}, {'asin': 'B0CJCGDB74', 'minimumPrice': '45.00'}, {'asin': 'B0CJCGDB73', 'minimumPrice': '485.00'}, {'asin': 'B0CJCGDB72', 'minimumPrice': '545.00'}, {'asin': 'B0CJCGDB71', 'minimumPrice': '545.00'}, {'asin': 'B0CJCGDB7', 'minimumPrice': '854.00'}, {'asin': 'B0CTCGDB7', 'minimumPrice': '84.00'}, {'asin': 'B0FGTNLTG3', 'minimumPrice': '56.00'}, {'asin': 'B0D57X7KV9', 'minimumPrice': '11.07'}, {'asin': 'B0D1VDMZR1', 'minimumPrice': '23.73'}, {'asin': 'B0DS2QT1YZ', 'minimumPrice': '19.36'}, {'asin': 'WQDASDAS', 'minimumPrice': '60.00'}, {'asin': 'B0BHPGTWQR', 'minimumPrice': '28.48'}, {'asin': 'SADAD', 'minimumPrice': '50.00'}, {'asin': 'DFHFGH78979', 'minimumPrice': '34.00'}], 'FR': [{'asin': 'B0FY6JY4BF', 'minimumPrice': ''}, {'asin': 'B0DZNRB5Q1', 'minimumPrice': ''}, {'asin': 'B0DNSKFLGK', 'minimumPrice': ''}, {'asin': 'B0DGLCQDTR', 'minimumPrice': ''}, {'asin': 'B0D9W93869', 'minimumPrice': ''}, {'asin': 'B0CG8RNWL6', 'minimumPrice': ''}, {'asin': 'B0BX94RYT1', 'minimumPrice': ''}, {'asin': 'B0BLN7D2JZ', 'minimumPrice': ''}, {'asin': 'B09WHV9JCJ', 'minimumPrice': ''}, {'asin': 'B0DKX28SNX', 'minimumPrice': ''}, {'asin': 'B0CZCVTMRJ', 'minimumPrice': ''}, {'asin': 'B0CC95H28M', 'minimumPrice': ''}, {'asin': 'B0CC942XSZ', 'minimumPrice': ''}, {'asin': 'B0CC93WW6S', 'minimumPrice': ''}, {'asin': 'B0CC93TDB9', 'minimumPrice': ''}, {'asin': 'B0816GGCFY', 'minimumPrice': ''}, {'asin': 'B0CQY5SM94', 'minimumPrice': ''}, {'asin': 'B0DQTKRNLH', 'minimumPrice': ''}, {'asin': 'B0DCB85F3J', 'minimumPrice': ''}, {'asin': 'B0D8JR3W53', 'minimumPrice': ''}, {'asin': 'B0D2KCRLJ1', 'minimumPrice': ''}, {'asin': 'B0CQK7QWJB', 'minimumPrice': ''}, {'asin': 'B0B9XM29C9', 'minimumPrice': ''}, {'asin': 'B0B5L4HC95', 'minimumPrice': ''}, {'asin': 'B0F9FCT8RD', 'minimumPrice': ''}, {'asin': 'B0BZ8J16JM', 'minimumPrice': ''}, {'asin': 'B0BV1TRMGN', 'minimumPrice': ''}, {'asin': 'B09G68LMB3', 'minimumPrice': ''}, {'asin': 'B0DCRJSMJP', 'minimumPrice': '10.72'}, {'asin': 'B093Y447R2', 'minimumPrice': '79.17'}, {'asin': 'B0D8Y5KWB8', 'minimumPrice': ''}, {'asin': 'B0BJK43Z6X', 'minimumPrice': ''}, {'asin': 'B0DXK7YZ5D', 'minimumPrice': '15.06'}, {'asin': 'B0C232Z6C6', 'minimumPrice': '48.25'}, {'asin': 'B0BZVL6Y9X', 'minimumPrice': '54.37'}, {'asin': 'B0B4S98HLZ', 'minimumPrice': '14.61'}, {'asin': 'B09VPMBMB8', 'minimumPrice': '18.01'}, {'asin': 'B0D17L6YKN', 'minimumPrice': '16.95'}], 'IT': [{'asin': 'B0DTK87KJ5', 'minimumPrice': ''}, {'asin': 'B0DNSNN7NH', 'minimumPrice': ''}, {'asin': 'B0D9W93869', 'minimumPrice': ''}, {'asin': 'B0CLXZHM2G', 'minimumPrice': ''}, {'asin': 'B0CG8RNWL6', 'minimumPrice': ''}, {'asin': 'B0BX94RYT1', 'minimumPrice': ''}, {'asin': 'B0BVM53J3B', 'minimumPrice': ''}, {'asin': 'B0BLN7D2JZ', 'minimumPrice': ''}, {'asin': 'B0F9KVS6Y2', 'minimumPrice': ''}, {'asin': 'B0F8QGHMLL', 'minimumPrice': ''}, {'asin': 'B0CY5KW8RY', 'minimumPrice': ''}, {'asin': 'B0CC94P4H1', 'minimumPrice': ''}, {'asin': 'B0CC94K2HN', 'minimumPrice': ''}, {'asin': 'B0CC922QN8', 'minimumPrice': ''}, {'asin': 'B0C7Q51PL1', 'minimumPrice': ''}, {'asin': 'B0D8JR3W53', 'minimumPrice': ''}, {'asin': 'B0B5L4HC95', 'minimumPrice': ''}, {'asin': 'B0C5C53444', 'minimumPrice': ''}, {'asin': 'B0F5PRVRLF', 'minimumPrice': ''}, {'asin': 'B0BJK43Z6X', 'minimumPrice': ''}, {'asin': 'B09WKFD3G8', 'minimumPrice': ''}, {'asin': 'B08NT2BNGH', 'minimumPrice': ''}, {'asin': 'B0DRV6D214', 'minimumPrice': '20.50'}, {'asin': 'B08QRKHD3C', 'minimumPrice': '38.36'}, {'asin': 'B09JMW3F1R', 'minimumPrice': '28.64'}, {'asin': 'B0CKYHTDJ5', 'minimumPrice': '24.05'}, {'asin': 'B098QFLVDZ', 'minimumPrice': '77.89'}, {'asin': '32EGDFGS4', 'minimumPrice': '56.00'}, {'asin': 'B087JP6PWL89', 'minimumPrice': ''}], 'ES': [{'asin': 'B0D2J11TKY', 'minimumPrice': ''}, {'asin': 'B0CLY2THTY', 'minimumPrice': ''}, {'asin': 'B0D8JS8NPH', 'minimumPrice': ''}, {'asin': 'B0CYGSBXSQ', 'minimumPrice': ''}, {'asin': 'B0CKXMDBN8', 'minimumPrice': ''}, {'asin': 'B0CJ5F9J4Z', 'minimumPrice': ''}, {'asin': 'B0CC95XMD4', 'minimumPrice': ''}, {'asin': 'B0CC93ZNH2', 'minimumPrice': ''}, {'asin': 'B07L281N1M', 'minimumPrice': ''}, {'asin': 'B0DNZ5N7HY', 'minimumPrice': ''}, {'asin': 'B0C7V4BFSZ', 'minimumPrice': ''}, {'asin': 'B0B5L4HC95', 'minimumPrice': ''}, {'asin': 'B0DZCC4B4T', 'minimumPrice': ''}, {'asin': 'B0DZBQ4ZNC', 'minimumPrice': ''}, {'asin': 'B0C5QQFN1G', 'minimumPrice': ''}, {'asin': 'B0BFXD4LYN', 'minimumPrice': '33.02'}, {'asin': 'B0F3122Q81', 'minimumPrice': ''}, {'asin': 'B0BG73XTFC', 'minimumPrice': ''}, {'asin': 'B0CNM269LZ', 'minimumPrice': '9.24'}, {'asin': 'B08QS37RHQ', 'minimumPrice': '42.40'}, {'asin': 'B087JP6PWL89', 'minimumPrice': ''}]}, 'minimum_price_by_country_and_asin': {}, 'skip_asin_delete_policy': {'enabled': True, 'mode': 'DELETE_WHEN_PRICE_BELOW_MINIMUM', 'deleteWhenPriceBelowMinimum': True, 'compareField': 'price', 'thresholdField': 'minimumPrice', 'target': 'skip_asin', 'source': 'frontend-price-track'}, 'delete_skip_asin_when_price_below_minimum': True, 'deleteSkipAsinWhenPriceBelowMinimum': True, 'asin_rows_by_country': {}, 'taskId': 5658, 'resultId': 7627, 'shopName': '郭锋滨', 'shopId': '27769139291498', 'platformName': '亚马逊', 'companyName': '尾号5578的公司115', 'shopMallName': '', 'matchStatus': 'MATCHED', 'matchMessage': None, 'outputFilename': None, 'downloadUrl': None, 'taskStatus': 'RUNNING', 'loopRunId': 235, 'roundIndex': 1}} + price_cls = PriceTask(user_info) + price_cls.process_task(task_data) \ No newline at end of file diff --git a/app/amazon/similar_asin.py b/app/amazon/similar_asin.py new file mode 100644 index 0000000..4cc81eb --- /dev/null +++ b/app/amazon/similar_asin.py @@ -0,0 +1,505 @@ +import json +from pathlib import Path +import base64 +import time +import re +import traceback +from datetime import datetime +from collections import defaultdict +import requests +from urllib.parse import quote + +from curl_cffi import requests as requests_frp + + + +from amazon.tool import show_notification,get_shop_info,remove_special_characters,split_currency_values +from amazon.amazon_base import TaskBase + +from amazon.chrome_base import ChromeAmzoneBase + +from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE + + +class ChromeAmzone(ChromeAmzoneBase): + mark_name = "亚马逊相似ASIN" + + def wait_load_complete(self): + try: + for _ in range(3): + content_area = self.tab.eles('xpath://div[@class="ap-sbi-aside__main"]', timeout=20) + if len(content_area) > 0: + pass + + except Exception as e: + self.log(f"等待加载完成失败,{e},等待5秒") + time.sleep(5) + + def encode_custom(self,input_): + if not input_ or not isinstance(input_, str): + return "" + + transformed = "" + + # 第一个字符:字符编码 + 字符串长度 + transformed += chr((ord(input_[0]) + len(input_)) & 0xFFFF) + + # 后续字符:当前字符编码 + 前一个字符编码 + for i in range(1, len(input_)): + current_code = ord(input_[i]) + prev_code = ord(input_[i - 1]) + transformed += chr((current_code + prev_code) & 0xFFFF) + + # 对应 JavaScript 的 encodeURIComponent + encoded = quote(transformed, safe="-_.!~*'()") + + # 对应 replace(/[!'()*]/g, ...) + for ch in ["!", "'", "(", ")", "*"]: + encoded = encoded.replace(ch, "%" + format(ord(ch), "x")) + return encoded + + def image_to_base64(self,image_source: str) -> str: + """ + 将图片链接(URL 或本地文件路径)转换为 Base64 编码的字符串。 + """ + if image_source.startswith(('http://', 'https://')): + response = requests.get(image_source, timeout=10) + response.raise_for_status() # 非 2xx 状态码将抛出异常 + image_data = response.content + else: + path = Path(image_source) + if not path.is_file(): + raise ValueError(f"本地文件不存在: {image_source}") + with open(path, 'rb') as f: + image_data = f.read() + base64_str = base64.b64encode(image_data).decode('utf-8') + return base64_str + + def get_aliprice_data(self,page=1,title="",domain="",category="",imageBase64=""): + """ + "fishkeeper Quick Aquarium Siphon Pump Gravel Cleaner - 256GPH Adjustable Powerful Fish Tank Vacuum Gravel Cleaning Kit for Aquarium Water Changer, Sand Cleaner, Dirt Removal : Amazon.co.uk: Pet Supplies" + 标题 : + + """ + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", + "browser": "chrome", + "cache-control": "no-cache", + "channel": "chrome_offline", + "content-type": "application/json;charset=UTF-8", + "ext-id": "10100", + "ext_id": "10100", + "origin": "chrome-extension://ephkdklmdkaakeleplfpahjphaokcllh", + "platform": "1688", + "pragma": "no-cache", + "priority": "u=1, i", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "none", + "sec-fetch-storage-access": "active", + "version": "3.7.4", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36" + } + cookie = { + "e-info": "[{\"e-name\":\"1688\",\"adid\":\"100\",\"version\":\"3.7.4\",\"ext_id\":\"10100\"}]", + "language": "chinese", + "province_code": "Guangdong", + "_ct": "czg", + "_e_ct": "czg", + "is_reto": "1", + "crossborder": "1", + "agent": "0", + "first_view": "1", + "currency": "USD", + "is_coo": "1", + "plugin": "chrome", + "plugin_ext": "100,129", + "m-info": "[{\"platform\":\"1688\",\"version\":\"3.7.4\",\"browser\":\"chrome\",\"m\":\"uni\",\"t\":1777902599578}]" + } + url = "https://api.aliprice.com/index.php/chrome/items/imageAnalysis" + itemTitle = f"{title} : {domain}: {category}" + + params = {"page": page, "size": 20, "website": "1688_lite2", "language": "zh-CN", "currency": "USD", "from": "", + "itemTitle": itemTitle, "domain": domain } + params['sign'] = self.encode_custom(json.dumps(params, ensure_ascii=False)) + data = { + "imageBase64" : imageBase64 + } + response = requests.post(url, headers=headers, params=params, json=data, impersonate="chrome101", + cookies=cookie) + response.encoding = "utf-8" + data = response.json() + return data + + def _scrape_data(self): + """ + 抓取商品数据 + + Returns: + dict: 抓取到的数据 + """ + data = { + 'image_url': "", + 'title': "", + 'category' : '', + 'success' : True + } + + try: + # 等待页面加载 + # time.sleep(3) + title_ele = self.tab.ele('xpath://h1[@id="title"]',timeout=30) + title = title_ele.text + data["title"] = title + imge_ele = self.tab.ele('xpath://div[@id="imgTagWrapperId"]//img',timeout=20) + image_url = imge_ele.attr("src") + data["image_url"] = image_url + category = self.tab.eles('xpath://div[@id="wayfinding-breadcrumbs_feature_div"]//span[@class="a-list-item"]//a[@class="a-link-normal a-color-tertiary"]',timeout=20) + if len(category) > 0: + data["category"] = category[0].text + return data + + except Exception as e: + print(f"抓取数据时出错: {traceback.format_exc()}") + data["success"] = False + return data + + def run(self, country, asin,total_page=2): + """ + 运行亚马逊详情采集任务 + Args: + country: 国家名称(如:英国、德国、法国、西班牙、意大利) + asin: 亚马逊商品ASIN码 + Returns: + dict: 包含采集到的数据 + """ + try: + # 验证国家是否支持 + if country not in self.country_info: + error_msg = f"不支持的国家: {country},支持的国家有: {list(self.country_info.keys())}" + print(error_msg) + show_notification(error_msg, "error") + return None + + # 获取国家配置 + country_config = self.country_info[country] + zip_code = country_config["zip_code"] + mark = country_config.get("mark") + + # 1. 根据国家和ASIN拼接链接 + base_url = country_config["url"] + # 提取域名部分 + domain = base_url.split("/dp/")[0] + # 拼接新的URL + product_url = f"{domain}/dp/{asin}" + print(f"正在访问: {product_url}") + + # 打开链接 + self.tab.get(product_url) + time.sleep(3) # 等待页面初步加载 + self.tab.wait.doc_loaded(timeout=30, raise_err=False) + + self.close_init_popup() + # 2. 切换国家/设置邮编 + print(f"正在检查并设置邮编: {zip_code},标识: {mark}") + self._set_zip_code(zip_code, mark) + + # self.tab.wait.doc_loaded(timeout=5, raise_err=False) + + # 3. 抓取数据 + print("正在抓取商品数据...") + data = self._scrape_data() + + # 判断是否采集标题出错 + new_size = "220,220" + pattern = r"\._(?:[A-Z]+)?(\d+)_\." + replacement = f"._{new_size}_.jpg" + # 使用正则替换 + image_new_url = re.sub(pattern, lambda m: replacement, data["image_url"]) + + iamge_base64 = self.image_to_base64(image_new_url) + + similar_data = [] + # 4、请求获取插件的数据 + for page_num in range(total_page): + resp_data = self.get_aliprice_data( + page=page_num, + title = data["title"], + domain=domain, + category=data["category"], + imageBase64 = iamge_base64 + ) + self.log(f"Aliprice 扩展数据获取:{resp_data}") + if len(resp_data.get("data",[])) > 0: + for i in resp_data.get("data"): + similar_data.append(i) + + data["similar_data"] = similar_data + + # 添加基本信息 + data['country'] = country + data['asin'] = asin + data['url'] = product_url + data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + print(f"数据抓取完成: {json.dumps(data)}") + return data + + except Exception as e: + error_msg = f"运行出错: {traceback.format_exc()}" + print(error_msg) + show_notification(f"采集失败: {str(e)}", "error") + return {} + + +class SimilarAsinTask(TaskBase): + mark_name = "相似ASIN采集" + + @staticmethod + def group_by_id_prefix(data): + """ + 根据每条数据的 id 字段进行分组: + - 如果 id 为 "2_1",则取 "_" 前面的 "2" 作为分组 key + - 如果 id 为 "1",则分组 key 就是 "1" + - 相同 key 的数据归为同一组 + + :param data: 原始列表数据 + :return: 二维列表,按 id 前缀分组 + """ + grouped = defaultdict(list) + + for item in data: + item_id = str(item.get("id", "")) + group_key = item_id.split("_")[0] + grouped[group_key].append(item) + + return list(grouped.values()) + + @staticmethod + def normalize_groups(data): + groups = data.get("groups") + if isinstance(groups, list) and len(groups) > 0: + return groups + + rows = data.get("rows") or data.get("items") or [] + if not isinstance(rows, list) or len(rows) == 0: + return [] + + normalized = [] + for items in SimilarAsinTask.group_by_id_prefix(rows): + first = items[0] if items else {} + item_id = str(first.get("id") or first.get("displayId") or "") + base_id = item_id.split("_")[0] if item_id else "" + normalized.append({ + "sourceFileKey": first.get("sourceFileKey", ""), + "sourceFilename": first.get("sourceFilename", ""), + "groupKey": first.get("groupKey") or base_id, + "baseId": first.get("baseId") or base_id, + "displayId": first.get("displayId") or item_id, + "items": items, + }) + return normalized + + def process_task(self, task_data: dict): + """处理审批任务主入口 + + Args: + task_data: 任务数据 + """ + try: + data = task_data.get("data", {}) + task_id = data.get("taskId") + groups = self.normalize_groups(data) + + if not task_id: + self.log("任务ID为空,跳过", "WARNING") + return + + + self.log(f"开始处理爬取任务 {task_id},{len(groups)} 个任务") + + if not groups: + self.log("appearance-patent groups/rows is empty, skip", "WARNING") + return + + runing_task[task_id] = { + "status": "running", + "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "total_shops": 1, + "processed_shops": 0, + "total_countries": len(groups) , + "processed_countries": 0, + "total_asins": 0, + "processed_asins": 0, + "success_count": 0, + "failed_count": 0, + "stop_requested": False + } + + # 检查是否收到暂停请求 + if task_id in runing_task and runing_task[task_id].get("stop_requested", False): + self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING") + runing_task[task_id]["status"] = "stopped" + return + max_retry = 3 + show_notification(f"开始爬取数据", "info") + chrome = ChromeAmzone() + try: + # 数据整理 + # new_items = self.group_by_id_prefix(items) + result = [] + for gp_index,gp in enumerate(groups): + items = gp.get("items", []) + return_data = {} + + for index,value in enumerate(items): + + return_data = { + 'image_url': "", + 'title': "" + } + asin = value.get("asin") + country = value.get("country") + return_data = None + for _ in range(max_retry): + try: + return_data = chrome.run(country, asin) or {} + self.log(f"抓取结果->{return_data}") + break + except Exception as e: + if "与页面的连接已断开" in str(e): + chrome = ChromeAmzone() + if not isinstance(return_data, dict): + return_data = {} + if return_data.get("image_url"): + break + + group_item = [ + { + "sourceFileKey": i.get("sourceFileKey"), + "sourceFilename": i.get("sourceFilename"), + "rowToken": i.get("rowToken"), + "groupKey": i.get("groupKey"), + "id": i.get("id"), + "asin": i.get("asin"), + "country": i.get("country"), + "url": return_data.get("image_url"), + "title": return_data.get("title"), + } + for i in items + ] + # task_id: int, chunkIndex:int,chunkTotal: int, country_code: str, asin: str, status: dict,error:str="", + # item_data:dict={}, + res = { + "sourceFileKey": gp.get("sourceFileKey"), + "sourceFilename": gp.get("sourceFilename"), + "groupKey": gp.get("groupKey"), + "baseId": gp.get("baseId"), + "displayId": gp.get("displayId"), + "items": group_item + } + print("================") + result.append(res) + print("================") + is_done = gp_index == len(groups)-1 + if len(result) > 20 or is_done: + self.post_result(task_id=task_id,chunkIndex=gp_index+1,chunkTotal=len(groups), + asin=asin,item_data=result,is_done=is_done) + result = [] + + + if len(result) > 0: + is_done = True + self.post_result(task_id=task_id, chunkIndex=len(groups), chunkTotal=len(groups), + asin=asin, item_data=result, is_done=is_done) + + try: + chrome.close() + except Exception as e: + print("退出浏览器出错",e) + # 更新已处理店铺数 + if task_id in runing_task: + runing_task[task_id]["processed_shops"] += 1 + except Exception as e: + self.log(f"处理店铺 {task_id} 失败: {str(e)}", "ERROR") + self.log(traceback.format_exc(), "ERROR") + + # 更新任务状态 + if task_id in runing_task: + if runing_task[task_id].get("stop_requested", False): + runing_task[task_id]["status"] = "stopped" + self.log(f"任务 {task_id} 已被暂停!") + else: + runing_task[task_id]["status"] = "completed" + self.log(f"任务 {task_id} 处理完成!") + + except Exception as e: + self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR") + if task_id: + if task_id in runing_task: + runing_task[task_id]["status"] = "failed" + runing_task[task_id]["error"] = str(e) + + def post_result(self, task_id: int, chunkIndex:int,chunkTotal: int, asin: str, error:str="", + item_data:dict={}, + is_done: bool = False): + """回传处理结果到API + """ + + url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result" + + payload ={ + "submissionId": f"{int(time.time())}", + "chunkIndex": chunkIndex, + "chunkTotal": chunkTotal, + "error": error, + "groups": item_data, + "done": is_done + } + + max_retries = 3 + for retry in range(max_retries): + try: + request_timeout = 300 if is_done else 30 + print("================【详情采集】=====================") + self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + self.log(f"回传URL: {url}") + self.log(f"回传数据: {payload}") + response = requests.post( + url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=request_timeout, + verify=False + ) + self.log(f"回传结果: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"结果回传成功: {asin} - {item_data}") + return + else: + self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + print("=====================================") + + except Exception as e: + self.log(f"调用API异常: {str(e)}", "ERROR") + print("=====================================") + + # 如果还有重试机会,等待后继续 + if retry < max_retries - 1: + time.sleep(2) + + self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") + raise RuntimeError("已达到最大重试次数,结果回传最终失败") + +if __name__ == '__main__': + spide = ChromeAmzone() + + resp = spide.run( + country="英国", asin="B0F79LCB3X", total_page=2 + ) + print(resp) + + + diff --git a/app/app.py b/app/app.py index 237ee51..56f5e7c 100644 --- a/app/app.py +++ b/app/app.py @@ -4,7 +4,9 @@ """ import os import secrets -from datetime import timedelta +import logging +from datetime import timedelta, datetime +from logging.handlers import RotatingFileHandler from flask import Flask from flask_cors import CORS @@ -16,6 +18,52 @@ from blueprints.admin import admin_bp from blueprints.image import image_bp from blueprints.brand import brand_bp from blueprints.communication import communication_bp + +from config import cache_path, debug + + +def setup_flask_logging(app): + """配置Flask访问日志,输出到独立的日志文件""" + + api_cache_path = os.path.join(cache_path, "api_logs") + os.makedirs(api_cache_path, exist_ok=True) + + # 只在非debug模式下配置文件日志 + if not debug: + # 获取当前日期作为日志文件名的一部分 + today = datetime.now().strftime("%Y_%m_%d") + log_file = os.path.join(api_cache_path, f'api_{today}.log') + + # 创建文件处理器,使用RotatingFileHandler避免日志文件过大 + file_handler = RotatingFileHandler( + log_file, + maxBytes=10 * 1024 * 1024, # 10MB + backupCount=5, + encoding='utf-8' + ) + + # 设置日志格式 + formatter = logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.INFO) + + # 配置Flask应用日志 + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + + # 配置Werkzeug日志(HTTP访问日志) + werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger.addHandler(file_handler) + werkzeug_logger.setLevel(logging.INFO) + + print(f"API 日志已配置,输出到: {log_file}") + else: + print("Debug模式下,API 日志输出到控制台") + + def create_app(): app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR) frontend_origin = os.environ.get('FRONTEND_ORIGIN', '*') @@ -39,6 +87,9 @@ def create_app(): app.register_blueprint(image_bp) app.register_blueprint(brand_bp) app.register_blueprint(communication_bp) + + # 配置Flask访问日志 + setup_flask_logging(app) return app diff --git a/app/main.py b/app/main.py index 365b576..fcd1d10 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,3 @@ -""" -基于 pywebview 实现的跨平台 UI,需先登录方可使用 -""" from config import cache_path,debug,version,JAVA_API_BASE,JSON_TASK_QUEUE,runing_task,runing_shop import datetime import json @@ -21,7 +18,8 @@ import requests import subprocess from amazon.del_brand import kill_process - +from app import run_app +from generate_api import generate with open("version.txt","w",encoding="utf-8") as file: @@ -31,7 +29,6 @@ print(f""" ======================================================== 版本: {version} ======================================================== - """) @@ -43,7 +40,6 @@ PORT = 5123 def generate_images(params): """供前端调用的生成图片接口""" - from generate_api import generate return generate(params) @@ -67,7 +63,8 @@ class WindowAPI: finally: time.sleep(0.5) os._exit(0) - + + print("===============程序退出================") # 先销毁窗口,让用户看到立即响应 try: self._window.destroy() @@ -368,7 +365,6 @@ class WindowAPI: def start_flask(): - from app import run_app run_app(host='127.0.0.1', port=PORT) @@ -395,11 +391,11 @@ def on_window_closing(): # 给一点时间让清理完成,然后强制退出 time.sleep(0.5) os._exit(0) - + + print("=================程序退出=====================") # 立即在后台线程执行清理,不阻塞窗口关闭 cleanup_thread = threading.Thread(target=cleanup_and_exit, daemon=True) cleanup_thread.start() - # 立即返回,让窗口快速关闭,用户不会感觉卡顿 return True