# encoding utf-8 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 try: from config import proxy_url as CONFIG_PROXY_URL, proxy_mode as CONFIG_PROXY_MODE except ImportError: CONFIG_PROXY_URL = None CONFIG_PROXY_MODE = 1 # Forbidden 后 3 分钟内统一使用代理:记录代理生效截止时间与当前代理 _FORBIDDEN_PROXY_UNTIL_Similar = 0.0 _FORBIDDEN_PROXY_DICT_Similar = None _FORBIDDEN_PROXY_MINUTES_Similar = 0.5 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" 标题 : """ global _FORBIDDEN_PROXY_UNTIL_Similar, _FORBIDDEN_PROXY_DICT_Similar 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 } proxies = None # 若 3 分钟内曾出现 Forbidden,则直接使用当时保存的代理 now = time.time() if now < _FORBIDDEN_PROXY_UNTIL_Similar and _FORBIDDEN_PROXY_DICT_Similar: proxies = _FORBIDDEN_PROXY_DICT_Similar print("处于 Forbidden 代理窗口内,直接使用代理", proxies) # 发送第一次请求 try: response = requests_frp.post(url, headers=headers, params=params, json=data, impersonate="chrome101", cookies=cookie, proxies=proxies) response.encoding = "utf-8" # 检查响应状态码和数据有效性 need_retry = False if response.status_code != 200: print(f"请求失败,状态码: {response.status_code}") need_retry = True else: try: result_data = response.json() # 检查是否获取到有效数据 if not result_data or not isinstance(result_data, dict): print("返回数据为空或格式不正确") need_retry = True elif "Forbidden" in response.text or "Too Many Requests" in response.text: print("返回Forbidden或Too Many Requests") need_retry = True except Exception as e: print(f"解析响应JSON失败: {e}") need_retry = True # 如果需要重试且配置了代理URL if need_retry and CONFIG_PROXY_URL and not proxies: try: proxy_resp = requests.get(CONFIG_PROXY_URL, timeout=10) print("代理请求结果->:", proxy_resp.text) # 模式 2:账号密码代理,接口返回 JSON if CONFIG_PROXY_MODE == 2: resp_json = proxy_resp.json() proxy_list = resp_json.get("data", {}).get("list") or [] first_item = proxy_list[0] if proxy_list else None if first_item: ip = first_item.get("ip") port = first_item.get("port") account = first_item.get("account") password = first_item.get("password") if ip and port and account and password: auth_proxy = f"{account}:{password}@{ip}:{port}" print("获取到账号密码代理:", auth_proxy) proxies = { "http": f"http://{auth_proxy}", "https": f"http://{auth_proxy}", } # 默认模式 1:普通 IP:port 文本 if CONFIG_PROXY_MODE != 2: proxy_ip = (proxy_resp.text or "").strip() if proxy_ip: proxies = { "http": f"http://{proxy_ip}", "https": f"http://{proxy_ip}", } if proxies: print("使用代理重试请求") response = requests_frp.post(url, headers=headers, params=params, json=data, impersonate="chrome101", cookies=cookie, proxies=proxies) response.encoding = "utf-8" print("代理重试结果状态码:", response.status_code) # 记录 3 分钟内都使用该代理 _FORBIDDEN_PROXY_UNTIL_Similar = now + _FORBIDDEN_PROXY_MINUTES_Similar * 60 _FORBIDDEN_PROXY_DICT_Similar = proxies except Exception as e: print("获取代理或重试失败:", e) # 返回最终结果 result_data = response.json() return result_data except Exception as e: print(f"get_aliprice_data 请求异常: {e}") return {} def _scrape_data(self): """ 抓取商品数据 Returns: dict: 抓取到的数据 """ data = { 'image_url': "", 'title': "", 'category' : '', 'sku' : '', '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 # sku sku_ele_ls = self.tab.eles('xpath://ul[@class="a-unordered-list a-vertical a-spacing-mini"]',timeout=20) if len(sku_ele_ls) > 0: data["sku"] = sku_ele_ls[0].text 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}" self.log(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. 切换国家/设置邮编 self.log(f"正在检查并设置邮编: {zip_code},标识: {mark}") self._set_zip_code(zip_code, mark) # self.tab.wait.doc_loaded(timeout=5, raise_err=False) # 3. 抓取数据 self.log("正在抓取商品数据...") 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 fetch_parsed_payload(self, task_id, user_id=1): url = f"{DELETE_BRAND_API_BASE}/api/similar-asin/tasks/{task_id}/parsed-payload" response = requests.get(url, params={"user_id": user_id}, timeout=300, verify=False) response.raise_for_status() payload = response.json() if response.text else {} if isinstance(payload, dict) and payload.get("success"): return payload.get("data") or {} raise RuntimeError(f"获取货源查询解析载荷失败: {payload}") def _merge_parsed_payload(self, data, parsed_payload): payload_rows = parsed_payload.get("allItems") or parsed_payload.get("items") or [] merged = { **data, "groups": parsed_payload.get("groups") or [], "rows": payload_rows, } if parsed_payload.get("aiPrompt"): merged["prompt"] = parsed_payload.get("aiPrompt") if parsed_payload.get("apiKey") and not merged.get("api_key"): merged["api_key"] = parsed_payload.get("apiKey") return merged def process_task(self, task_data: dict): """处理审批任务主入口 Args: task_data: 任务数据 """ try: data = task_data.get("data", {}) task_id = data.get("taskId") parsed_payload = self.fetch_parsed_payload(task_id, data.get("user_id") or data.get("userId") or 1) data = self._merge_parsed_payload(data, parsed_payload) groups = self.normalize_groups(data) print(groups) 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") for _ in range(max_retry): try: return_data = chrome.run(country, asin,total_page=3) 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"), "sku" : return_data.get("sku"), "country": i.get("country"), "url": return_data.get("image_url"), "title": return_data.get("title"), "done": False, "urls" : [i.get("ori_picture") for i in return_data.get("similar_data")] } 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) > 5 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) # 更新已处理店铺数 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") try: chrome.close() except Exception as e: print("退出浏览器出错", e) # 更新任务状态 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/similar-asin/tasks/{task_id}/result" payload ={ "submissionId": f"{task_id}", "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="B0CJ8SNXXV", total_page=2 ) print(resp) # task_data = { # "type": "similar-asin-run", # "ts": 1778038688824, # "data": { # "taskId": 6850, # "api_key": "sk-EMcDFg36zCeWUtCbRzKUbrRZyNeC6M4KBhY6fAVcNP7GG4xI", # "groups": [ # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "groupKey": "similar-asin:6850", # "baseId": "", # "displayId": "1", # "items": [ # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::2", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::1@2", # "id": "1", # "asin": "B0D792ND9V", # "country": "英国", # "price": "12.29", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::3", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_1", # "asin": "B0D14L34S7", # "country": "英国", # "price": "14.88", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::4", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_2", # "asin": "B0D14N8FJY", # "country": "英国", # "price": "15.36", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::5", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_3", # "asin": "B0D14NBL5D", # "country": "英国", # "price": "14.87", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::6", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_4", # "asin": "B0D14M5X4J", # "country": "英国", # "price": "14.41", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::7", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7", # "id": "3_1", # "asin": "B0CWQFZMGY", # "country": "英国", # "price": "17.05", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::8", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7", # "id": "3_2", # "asin": "B0CX8D68L3", # "country": "英国", # "price": "16.46", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::9", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::4@9", # "id": "4_1", # "asin": "B0CX87XXWJ", # "country": "英国", # "price": "17.54", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::10", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::5@10", # "id": "5_1", # "asin": "B0CR3PMR8L", # "country": "英国", # "price": "16.74", # "url": "", # "title": "", # "target_urls": [] # } # ] # } # ], # "rows": [ # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::2", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::1@2", # "id": "1", # "asin": "B0D792ND9V", # "country": "英国", # "price": "12.29", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::3", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_1", # "asin": "B0D14L34S7", # "country": "英国", # "price": "14.88", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::4", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_2", # "asin": "B0D14N8FJY", # "country": "英国", # "price": "15.36", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::5", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_3", # "asin": "B0D14NBL5D", # "country": "英国", # "price": "14.87", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::6", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3", # "id": "2_4", # "asin": "B0D14M5X4J", # "country": "英国", # "price": "14.41", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::7", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7", # "id": "3_1", # "asin": "B0CWQFZMGY", # "country": "英国", # "price": "17.05", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::8", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7", # "id": "3_2", # "asin": "B0CX8D68L3", # "country": "英国", # "price": "16.46", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::9", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::4@9", # "id": "4_1", # "asin": "B0CX87XXWJ", # "country": "英国", # "price": "17.54", # "url": "", # "title": "", # "target_urls": [] # }, # { # "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3", # "sourceFilename": "17(1) - 副本1.xlsx", # "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::10", # "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::5@10", # "id": "5_1", # "asin": "B0CR3PMR8L", # "country": "英国", # "price": "16.74", # "url": "", # "title": "", # "target_urls": [] # } # ] # } # } # sim_asin = SimilarAsinTask({}) # sim_asin.process_task(task_data)