From a19a4682986de43dbde1665f6019b0785494a0e3 Mon Sep 17 00:00:00 2001 From: Cerys Date: Fri, 10 Apr 2026 10:57:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + build | 1 + core/__init__.py | 3 + core/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 221 bytes core/__pycache__/browsers.cpython-310.pyc | Bin 0 -> 872 bytes core/__pycache__/browsers.cpython-312.pyc | Bin 0 -> 1374 bytes core/__pycache__/excels.cpython-312.pyc | Bin 0 -> 5861 bytes core/__pycache__/messages.cpython-312.pyc | Bin 0 -> 1255 bytes core/__pycache__/spiders.cpython-312.pyc | Bin 0 -> 2475 bytes core/__pycache__/types.cpython-312.pyc | Bin 0 -> 10850 bytes core/browsers.py | 25 + core/excels.py | 84 ++ core/spiders.py | 31 + core/types.py | 263 ++++++ lib/__init__.py | 4 + lib/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 294 bytes lib/__pycache__/get_goods.cpython-312.pyc | Bin 0 -> 5291 bytes lib/__pycache__/gether.cpython-312.pyc | Bin 0 -> 7528 bytes lib/__pycache__/shopyy.cpython-312.pyc | Bin 0 -> 13413 bytes lib/__pycache__/shopyy_verify.cpython-312.pyc | Bin 0 -> 4020 bytes lib/gether.py | 131 +++ lib/model/__pycache__/goods.cpython-312.pyc | Bin 0 -> 17087 bytes lib/model/goods.py | 417 +++++++++ .../__pycache__/agatameble.cpython-312.pyc | Bin 0 -> 9012 bytes .../ancientnutrition.cpython-312.pyc | Bin 0 -> 16042 bytes .../__pycache__/bricomarche.cpython-312.pyc | Bin 0 -> 8314 bytes .../__pycache__/condom69.cpython-312.pyc | Bin 0 -> 6494 bytes lib/module/__pycache__/dk_one.cpython-312.pyc | Bin 0 -> 9556 bytes .../__pycache__/example.cpython-312.pyc | Bin 0 -> 3169 bytes .../friskybusiness.cpython-312.pyc | Bin 0 -> 7272 bytes .../peek_cloppenburg.cpython-312.pyc | Bin 0 -> 10669 bytes .../peek_cloppenburg_nl.cpython-312.pyc | Bin 0 -> 10508 bytes .../__pycache__/ssfshop.cpython-312.pyc | Bin 0 -> 15858 bytes .../__pycache__/taketoys.cpython-312.pyc | Bin 0 -> 6105 bytes .../__pycache__/tfashion.cpython-312.pyc | Bin 0 -> 6840 bytes lib/module/agatameble.py | 176 ++++ lib/module/ancientnutrition.py | 325 +++++++ lib/module/bricomarche.py | 201 +++++ lib/module/condom69.py | 136 +++ lib/module/dk_one.py | 210 +++++ lib/module/example.py | 86 ++ lib/module/friskybusiness.py | 141 +++ lib/module/peek_cloppenburg.py | 217 +++++ lib/module/peek_cloppenburg_nl.py | 213 +++++ lib/module/ssfshop.py | 371 ++++++++ lib/module/taketoys.py | 131 +++ lib/module/tfashion.py | 140 +++ lib/shopyy.py | 371 ++++++++ main.py | 49 ++ server.py | 205 +++++ sources/amazon商品模板.xlsx | Bin 0 -> 8747 bytes sources/shopyy商品模板.xlsx | Bin 0 -> 9969 bytes sources/woo商品模板.csv | 1 + sources/专辑模板.xlsx | Bin 0 -> 9236 bytes sources/导航模板.xlsx | Bin 0 -> 9620 bytes utils/__pycache__/clash.cpython-312.pyc | Bin 0 -> 2458 bytes utils/__pycache__/db.cpython-312.pyc | Bin 0 -> 32356 bytes utils/__pycache__/files.cpython-312.pyc | Bin 0 -> 2003 bytes utils/__pycache__/formats.cpython-312.pyc | Bin 0 -> 5390 bytes utils/__pycache__/messages.cpython-312.pyc | Bin 0 -> 1404 bytes utils/__pycache__/redis.cpython-312.pyc | Bin 0 -> 2768 bytes utils/__pycache__/uploads.cpython-312.pyc | Bin 0 -> 1293 bytes utils/__pycache__/wpdata.cpython-312.pyc | Bin 0 -> 32369 bytes utils/clash.py | 64 ++ utils/db.py | 672 ++++++++++++++ utils/files.py | 24 + utils/formats.py | 104 +++ utils/messages.py | 18 + utils/uploads.py | 27 + utils/wpdata.py | 828 ++++++++++++++++++ 70 files changed, 5676 insertions(+) create mode 100644 .gitignore create mode 100644 build create mode 100644 core/__init__.py create mode 100644 core/__pycache__/__init__.cpython-312.pyc create mode 100644 core/__pycache__/browsers.cpython-310.pyc create mode 100644 core/__pycache__/browsers.cpython-312.pyc create mode 100644 core/__pycache__/excels.cpython-312.pyc create mode 100644 core/__pycache__/messages.cpython-312.pyc create mode 100644 core/__pycache__/spiders.cpython-312.pyc create mode 100644 core/__pycache__/types.cpython-312.pyc create mode 100644 core/browsers.py create mode 100644 core/excels.py create mode 100644 core/spiders.py create mode 100644 core/types.py create mode 100644 lib/__init__.py create mode 100644 lib/__pycache__/__init__.cpython-312.pyc create mode 100644 lib/__pycache__/get_goods.cpython-312.pyc create mode 100644 lib/__pycache__/gether.cpython-312.pyc create mode 100644 lib/__pycache__/shopyy.cpython-312.pyc create mode 100644 lib/__pycache__/shopyy_verify.cpython-312.pyc create mode 100644 lib/gether.py create mode 100644 lib/model/__pycache__/goods.cpython-312.pyc create mode 100644 lib/model/goods.py create mode 100644 lib/module/__pycache__/agatameble.cpython-312.pyc create mode 100644 lib/module/__pycache__/ancientnutrition.cpython-312.pyc create mode 100644 lib/module/__pycache__/bricomarche.cpython-312.pyc create mode 100644 lib/module/__pycache__/condom69.cpython-312.pyc create mode 100644 lib/module/__pycache__/dk_one.cpython-312.pyc create mode 100644 lib/module/__pycache__/example.cpython-312.pyc create mode 100644 lib/module/__pycache__/friskybusiness.cpython-312.pyc create mode 100644 lib/module/__pycache__/peek_cloppenburg.cpython-312.pyc create mode 100644 lib/module/__pycache__/peek_cloppenburg_nl.cpython-312.pyc create mode 100644 lib/module/__pycache__/ssfshop.cpython-312.pyc create mode 100644 lib/module/__pycache__/taketoys.cpython-312.pyc create mode 100644 lib/module/__pycache__/tfashion.cpython-312.pyc create mode 100644 lib/module/agatameble.py create mode 100644 lib/module/ancientnutrition.py create mode 100644 lib/module/bricomarche.py create mode 100644 lib/module/condom69.py create mode 100644 lib/module/dk_one.py create mode 100644 lib/module/example.py create mode 100644 lib/module/friskybusiness.py create mode 100644 lib/module/peek_cloppenburg.py create mode 100644 lib/module/peek_cloppenburg_nl.py create mode 100644 lib/module/ssfshop.py create mode 100644 lib/module/taketoys.py create mode 100644 lib/module/tfashion.py create mode 100644 lib/shopyy.py create mode 100644 main.py create mode 100644 server.py create mode 100644 sources/amazon商品模板.xlsx create mode 100644 sources/shopyy商品模板.xlsx create mode 100644 sources/woo商品模板.csv create mode 100644 sources/专辑模板.xlsx create mode 100644 sources/导航模板.xlsx create mode 100644 utils/__pycache__/clash.cpython-312.pyc create mode 100644 utils/__pycache__/db.cpython-312.pyc create mode 100644 utils/__pycache__/files.cpython-312.pyc create mode 100644 utils/__pycache__/formats.cpython-312.pyc create mode 100644 utils/__pycache__/messages.cpython-312.pyc create mode 100644 utils/__pycache__/redis.cpython-312.pyc create mode 100644 utils/__pycache__/uploads.cpython-312.pyc create mode 100644 utils/__pycache__/wpdata.cpython-312.pyc create mode 100644 utils/clash.py create mode 100644 utils/db.py create mode 100644 utils/files.py create mode 100644 utils/formats.py create mode 100644 utils/messages.py create mode 100644 utils/uploads.py create mode 100644 utils/wpdata.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..058a64e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/data/* +/database/* +/release/ +/test/ +/.vscode/ +/main.build/ +/main.dist/ \ No newline at end of file diff --git a/build b/build new file mode 100644 index 0000000..c9d3d61 --- /dev/null +++ b/build @@ -0,0 +1 @@ +nuitka --mingw64 --standalone main.py \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..65cdb26 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,3 @@ +from core import types + +Config = types.Config() diff --git a/core/__pycache__/__init__.cpython-312.pyc b/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e31eae69c600791bdd5aa7b273913f439824175 GIT binary patch literal 221 zcmX@j%ge<81lvFMWf}wN#~=<2FhLogjev~l3@HpLj5!Rsj8Tk?3``8Ej9Dx&g$Q~z zlo`cT$)w5r62#MFyv15lS&&-nr^$SaB{{z+wTKxge2dLFKQApaeI>(Zkg8t_E>c0Nyz;Z2$lO literal 0 HcmV?d00001 diff --git a/core/__pycache__/browsers.cpython-310.pyc b/core/__pycache__/browsers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3e0a4acdb3a04dfed7867e01494aaf7472147af GIT binary patch literal 872 zcmY*YOK%e~5VpPVJld+_0D=qR00E^UTtEmYB@~3JK17NTR;1OkcA9Oo>(q8yT5V3@ z&RO^GfyV-)!&(#7Ifr^fJIE-|^a3$9xnZ;xtF0BjJ2W^9@FJJ- zO{Spx7N|WVpRSCp+?qPf&Pk8VFmnzpc|AaHp$r>jVqtXnAIkg|Qg@CqMn>L&1}Mk! zv34hD>gB*O^>aMKV>@%owP1$Ec5Y8A%+=K#P4JAEoKsKNa(jZu&Rdi_os!M)9d0zd zt=;6-4TO@jGlWiq*1h{HcR^ao=1ss?o6Vk(F>f+{lz>xlG~mr%6l)==FK{V209{;Ym^57%Q6n?Y2_GbN?Y)At8PNLxf*S~pZx)U;|Uq?lA@+1)sHY>zcN zu9M2HazH4NBIQUbA;GaI9D3wf6b?vSVvH0sL;?gCF32cIk;YpOWWK3c`sX2m+xk`~mJ?>hW=5Wbnnrq&6GG&v_F+Xu=g_JC* zn_GaN|9Ne&`xg>*Xh1o3DF38iS7#$z+UV-*67_ZgdBt0Gi1a|07g?U7C#*eBnYifX zcnj)sP0k0O^|0ldG%Ia%tF@K+(*Pu(i4)|*TaREh|Ex}f8X_?o_M&cdnS9jK9AfPt zN51b+>v1*YRZv$-gZ~q;-}5{~FI!!yDfxjzy~o);`n<>2z>DwgsIF+@mR>mUP&|*i zkMGMj1qLL*MO6vGG$1}!7Z&7cs3L>&(@>H-(y9^jPv(vr0J+DH0q$st(bFeS@sY?~ zJ;vo12%@i;Xnki#KnJ(mt>yV6XQnS;qnpvT^ zV-5+kNwSrq!*j_lB}8;>%+G?BXniMh2je0U{m7L4fXHPk?!E!_BZ8|uwk!JZm3?}2 z{L=k+BLGZrS!jJM!qd65OP4!zh7K*YI=EH2~I60`!5o#?>Y z-IcrR_7~&L=xfW$PPl*V*2=BV#y(HpOMW%Bed1i>#JL~C$%n(OXzyBWrMCXT{a3eM z8fivPJsAHXdbZU&xaWh&`K@rW6+OErbGGdVE%KY%yQ2>`bz@r}ZRn#9-fZe;w)KgI zKJnd^rvCnl`bXCqZ|L!+KC-N~A_Lozqm9VXE#uv9jfeBii#M8)sin)m1|n@q(fW3d z46Rq2M~*FDZiV`Ij!^&7Zz^36# zy{>MKNU1mEn3UyKPVMsyVk!||IU*i03tb{P*X7@x>t@#3zoGrxCc5wtNB1S{@)?K- y&wAb|V!Kjsl4OXB;w8Jod}_-Gp`Rhlf4{)!-^z2S|Gw1*4qu=7M*w-5d;JGgUNPJN literal 0 HcmV?d00001 diff --git a/core/__pycache__/excels.cpython-312.pyc b/core/__pycache__/excels.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4787924484d6efdc7464131747635552b555067 GIT binary patch literal 5861 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 Chromium: + while True: + port = random.randint(9000, 65535) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + if s.connect_ex(('127.0.0.1', port)) != 10061: + continue + break + + if save_path: + options = ChromiumOptions().set_local_port(port).set_user_data_path(save_path) + else: + options = ChromiumOptions().auto_port() + options = options.set_argument('--force-device-scale-factor', str(chrome_zoom)) + # options = options.set_user_agent('Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)') + while True: + try: + browser = Chromium(options) + break + except: + pass + return browser diff --git a/core/excels.py b/core/excels.py new file mode 100644 index 0000000..b2a19e7 --- /dev/null +++ b/core/excels.py @@ -0,0 +1,84 @@ +import csv +import traceback +from openpyxl import load_workbook +from core import types +from utils import messages + +def read_excel_to_dict(excel_path: str, sheet_name: str = 'Sheet1') -> dict[str, types.GoodsInfo]: + """ + 从 Excel 文件中读取商品数据,返回 {url: GoodsInfo} 的字典。 + + :param excel_path: Excel 文件路径 + :param sheet_name: 工作表名称,默认 'Sheet1' + :return: dict{GoodsInfo.url: GoodsInfo} + """ + wb = load_workbook(excel_path, data_only=True) + ws = wb[sheet_name] + + goods_dict = {} + + # 从第3行开始读取(假设第1、2行为标题/说明) + for row in ws.iter_rows(min_row=3, values_only=True): + # 过滤空行(如果整行都为空则跳过) + if all(cell is None or str(cell).strip() == '' for cell in row): + continue + + # 转为字符串列表,None 转为空字符串 + row_data = [str(cell) if cell is not None else '' for cell in row] + + try: + goods = types.GoodsInfo.from_row_data(row_data) + if goods.url and (goods.attribute == "M" or goods.attribute == "S"): # 只有 url 非空才加入字典 + goods_dict[goods.url] = goods + elif goods.url and goods.attribute == "P": + if goods.url in goods_dict: + goods_dict[goods.url].p_lists.append(goods) + except Exception as e: + messages.sendError(f"解析行失败(跳过): {row_data[:5]}... 错误: {e}\n{traceback.format_exc()}") + continue + wb.close() + + return goods_dict + +def save_lists_to_csv(data: list, save_path: str, template_path: str = ''): + header = [] + if template_path: + with open(template_path, 'r', encoding='utf-8-sig') as f: + template = csv.reader(f) + for row in template: + header = list(row) + data.insert(0, header) + with open(save_path, 'w', encoding='utf-8', newline="") as f: + writer = csv.writer(f) + writer.writerows(data) + +class WorkBook: + def __init__(self, template: str, save_path: str, sheet_name: str = 'Sheet1'): + wb = load_workbook(template) + ws = wb[sheet_name] + last_row = ws.max_row + if ws.cell(row=last_row, column=1).value is None and "此行导入时不可删除" in str(ws.cell(row=2, column=1).value): + start_row = last_row + 1 + else: + while ws.cell(row=last_row, column=1).value is not None or any(ws.cell(row=last_row, column=c).value for c in range(2, 30)): + last_row += 1 + start_row = last_row + + self.wb = wb + self.ws = ws + self.start_row = start_row + self.save_path = save_path + + def add_row_of_list(self, data: list): + for col_idx, value in enumerate(data, start=1): + try: + self.ws.cell(row=self.start_row, column=col_idx, value=value) + except: + pass + self.start_row += 1 + + def save(self): + self.wb.save(self.save_path) + + def close(self): + self.wb.close() diff --git a/core/spiders.py b/core/spiders.py new file mode 100644 index 0000000..cdf4df1 --- /dev/null +++ b/core/spiders.py @@ -0,0 +1,31 @@ +import os +from core import browsers + +class Spiders: + def __init__(self, project_name: str, bitch: int = 5, worker_num: int = 1, switch_clash: bool = False, reflush_browser: bool = False): + self.bitch = bitch + self.worker_num = worker_num + self.switch_clash = switch_clash + self.reflush_browsers = reflush_browser + + self.__data_path__ = f"data/{project_name}" + self.__database_path__ = f"database/{project_name}.db" + self.__urls_folder__ = f"{self.__data_path__}/urls" + self.__excels_folder__ = f"{self.__data_path__}/excels" + self.__errors_folder__ = f"{self.__data_path__}/errors" + + if os.path.exists(self.__data_path__) == False: + os.makedirs(self.__data_path__) + os.makedirs(self.__urls_folder__) + os.makedirs(self.__excels_folder__) + os.makedirs(self.__errors_folder__) + + self.browser = browsers.getBrowsers() + self.browser.set.timeouts(3) + self.tab = self.browser.get_tab(0) + self.tab.set.window.max() + + def reflush_browser(self): + self.browser.quit() + self.browser = browsers.getBrowsers() + self.browser.set.timeouts(3) diff --git a/core/types.py b/core/types.py new file mode 100644 index 0000000..7ac1e4a --- /dev/null +++ b/core/types.py @@ -0,0 +1,263 @@ +import orjson +from bs4 import BeautifulSoup +from pydantic import BaseModel + +class Config(BaseModel): + home_mode: bool = True + skip_category: bool = True + +class GoodsInfo(BaseModel): + """ + 商品类(包含处理) + + :param spu: [必填] + :param title: 商品标题 [必填] + :param brand: 品牌 + :param desc: 简介 + :param attr: 属性 S|M + :param attr_items: 变体[名称]列表 [Color, Size] + :param price: 价格 + :param old_price: 原价 + :param images: 图片列表 + :param url: 主商品链接 + :param p_urls: 变体链接列表 + :param p_lists: 变体类列表 + :param other_data: 其他参数 + + """ + class GoodsInfoP(BaseModel): + """ + 变体类 + + :param attr_items: 变体[值]列表 [red, 30] + + """ + attr_items: list = [] # 变体名称1,2,3 + price: float = 0.00 + old_price: float = 0.00 + images: list[str] = [] + url: str = '' # 商品链接 + other_data: dict = {} # 其他参数 + + spu: str + title: str + brand: str = '' # 品牌 + desc: str = '' # 简介 + attr: str = 'S' # 属性 S M + attr_items: list = [] # 变体名称1,2,3 + price: float = 0.00 + old_price: float = 0.00 + images: list[str] = [] + url: str = '' # 商品链接 + p_urls: list[str] = [] # 变体链接列表(筛选用,可略) + p_lists: list[GoodsInfoP] = [] # 变体列表 + other_data: dict = {} # 其他参数 + + def to_db_data(self): + """转为goods_info.add_goods()所需格式""" + + main_info = { + 'spu': self.spu, + 'title': self.title, + 'brand': self.brand, + 'attr': self.attr, + 'desc': self.desc, + 'price': self.price, + 'old_price': self.old_price, + 'images': orjson.dumps(self.images), + 'url': self.url, + 'other_data': orjson.dumps(self.other_data) + } + for index, item_name in enumerate(self.attr_items): + if item_name: + main_info[f"item{index+1}"] = item_name + + son_infos = [] + for p_info in self.p_lists: + item = { + 'spu': self.spu, + 'price': p_info.price, + 'old_price': p_info.old_price, + 'images': orjson.dumps(p_info.images), + 'url': p_info.url, + 'other_data': orjson.dumps(p_info.other_data) + } + p_info.attr_items += [None, None] + for index, item_name in enumerate(p_info.attr_items[:3]): + if item_name: + item[f"item{index+1}"] = item_name + son_infos.append(item) + + return main_info, son_infos + + def to_shopyy_row_data(self): + """转为shopyy行数据""" + prices = [self.price, self.old_price] + for p_info in self.p_lists: + prices.append(p_info.price) + prices.append(p_info.old_price) + old_price = max(prices) + + rows_data = [] + rows_data.append([ + "", # 商品ID(系统生成,留空) + "", + f"{self.brand} {self.title}" if self.brand != "" else self.title, + self.attr, + "", + "", + self.desc, + "", + "", + "", + "", + "", + "", + "Y", + "Y", + self.spu, + 0, + "N", + 2, + self.brand, # 专辑名称 + "", + "", + *(self.attr_items[:3] + [''] * (3 - len(self.attr_items)))[:3], # 款式1/2/3 + self.price, + old_price, + "", + "", + "", + 50, + '', # 变体备注1(未在模型中定义,可扩展) + '', # 变体备注2 + ','.join(self.images) if self.images else '', + self.url, + ','.join(self.p_urls) if self.p_urls else '' + ]) + + p_index = [] + for p_info in self.p_lists: + if p_info.attr_items in p_index: + continue + p_index.append(p_info.attr_items) + rows_data.append([ + "", # 商品ID(系统生成,留空) + "", + f"{self.brand} {self.title}" if self.brand != "" else self.title, + "P", + "", + "", + self.desc, + "", + "", + "", + "", + "", + "", + "Y", + "Y", + self.spu, + 0, + "N", + 2, + self.brand, # 专辑名称 + "", + "", + *(p_info.attr_items[:3] + [''] * (3 - len(p_info.attr_items)))[:3], # 款式1/2/3 + p_info.price, + old_price, + "", + "", + "", + 50, + '', # 变体备注1(未在模型中定义,可扩展) + '', # 变体备注2 + ','.join(p_info.images) if p_info.images else '', + p_info.url, + "" + ]) + return rows_data + + def to_woo_row_data(self, category_name): + """转为woo行数据""" + def get_lists(attr = 'variable',spu = '', price = '', old_price = '', item_name1: str = '', item_value1: str = '', item_name2: str = '', item_value2: str = '', item_name3: str = '', item_value3: str = '', images: list = []): + return [ + attr, + self.spu if attr == 'variable' or attr == 'simple' else "", + self.title if attr == 'variable' or attr == 'simple' else "", + 1, + desc if attr == 'variable' or attr == 'simple' else "", + 1, + 100, + price, + old_price, + category_name if attr == 'variable' or attr == 'simple' else "", + ','.join(images) if images else '', + self.spu if attr == 'variation' else "", + item_name1, + item_value1, + 1, + item_name2, + item_value2, + 1, + item_name3, + item_value3, + 1, + ] + + desc = BeautifulSoup(self.desc, 'html.parser').get_text() + rows_data = [] + if self.attr == 'S': + rows_data.append(get_lists( + attr='simple', + spu=self.spu, + price=self.price, + old_price=self.old_price, + images=self.images + )) + else: + + item_values1 = set() + item_values2 = set() + item_values3 = set() + for p_info in self.p_lists: + item_value1 = p_info.attr_items[0] if len(p_info.attr_items) >= 1 else "" + item_value2 = p_info.attr_items[1] if len(p_info.attr_items) >= 2 else "" + item_value3 = p_info.attr_items[2] if len(p_info.attr_items) >= 3 else "" + + item_value1 = item_value1.replace(',', ',').replace('|', ' ') + item_values1.add(item_value1) + if item_value2: + item_value2 = item_value2.replace(',', ',').replace('|', ' ') + item_values2.add(item_value2) + if item_value3: + item_value3 = item_value3.replace(',', ',').replace('|', ' ') + item_values3.add(item_value3) + + rows_data.append(get_lists( + attr='variation', + spu='', + price=p_info.price, + old_price=p_info.old_price, + images=p_info.images, + item_name1=self.attr_items[0] if len(self.attr_items) >= 1 else "", + item_value1=item_value1, + item_name2=self.attr_items[1] if len(self.attr_items) >= 2 else "", + item_value2=item_value2, + item_name3=self.attr_items[2] if len(self.attr_items) >= 3 else "", + item_value3=item_value3 + )) + rows_data.insert(0, get_lists( + spu=self.spu, + price='', + old_price='', + images=self.images, + item_name1=self.attr_items[0] if len(self.attr_items) >= 1 else "", + item_value1='|'.join(item_values1), + item_name2=self.attr_items[1] if len(self.attr_items) >= 2 else "", + item_value2='|'.join(item_values2), + item_name3=self.attr_items[2] if len(self.attr_items) >= 3 else "", + item_value3='|'.join(item_values3) + )) + return rows_data diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..8297ea0 --- /dev/null +++ b/lib/__init__.py @@ -0,0 +1,4 @@ +from core import browsers +from lib.module import example + +Server: example.SpiderModule diff --git a/lib/__pycache__/__init__.cpython-312.pyc b/lib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..278bf31054986c6190286a86b492569532dd2743 GIT binary patch literal 294 zcmX@j%ge<81lOMRWkv((#~=<2Fhd!iZGepF4517u3@MB`47rR^jEo>QQw~!ua}+Zp zLn?C?2TT)!PGwDD2I;J1(PVuI64qqA#gSB$UtXMAR19RZr&c897UZPfVhc_!DoZW$ z(`38FlAK?ZS_HEA7FSMYl3s3pN@-4N5erCwC%7OpCAG*GEOd)MK0YxoFTW(QBr`v+ zI6i(Q!)K5kzvNx4V#@Q2vSW(Ds*7WQ#>B+OXXa&=#K-FuRNmsSff#C6#0yl<2*kzW iK;i>4BO~KI2Fbe&at}E8uXD&;#WkIs^N%$Y?ER8E`zQeqcL-9Mc@ z@{Wu{M95LPo!gzA-JPBJ_S?TWoi+r|mhqYRo*IPyK|Zv{P!jGP2H_%N5lbb}K(V9- z4DfA88b?e6rV)C89x)G?M=S#ts@Tp9FwkyHT1RXHHmEbbj0WtinWbN*1{@Pq(DErU zemH1S=#%jz$1BVT$Mex4kmy)4$`31+lW9B>75E^f&_n4o%g0L7Q40&ajqrc(kO`r) z*0Z*YXxch$J8594mzgXQC{0-)TI$0Zu9a%FGD}s}GivQxnKcNsW+h?Pq;e@Rnw(I* zseKj8gyXB<?g62{4zM5AbepJ5U-8&TFM)M^$IE=?+zEUj6i z&>EWN3Qdn@A>q}ea=FHe>dTs%lxWbTa>-gm>M~+2uNkhDVo4kSoOQ;U*ihEgq-!N1 zORupKMzchrA(SLqP*1KF@E5# zYSkxEsi)QHFj1<2LeK#tp*yVHS`9F+Q`#iAp{xPI+bALkky!!_Gq!}Y+*^}Glhy;m z-S_QBWDTcat8w!hJ$2V=<04cZIZLNPq9Nffw`tNER^ixr_yMaW?`CPyn9!lBtS450 z-s~%;G6?JkFVSm^SOug@9x#e@){=6Gbi!92OOv!91CTLSol_;C^b)e|2pJ5# zpzgV0%thHF>2$JxoQq|IG**ns_@H7w#^KW(R%}H}kyK_xvBvob=0>@wpwtX;LPTW{ zk$CE4S~2sP!I8M2n4_bkT#8l9U_6r)6l;`ai+YMpNJrSg2pbiminV_{#*GT`bP5C2 zKnlY{r{|+MRg|6rlR<}KD-MvsNgy!s6w8e(3|E>ZCK0Y!bq&Q_T#{lbj?CkAM4@Ct zog>95wu6N9Jey{@q+%g{N^&WlP>E*!nVNdgb)fGAS@wz131K*$I)c*)E++K$o&d+3 z&==k_I-yufex7X7t5tvWJf2L)qDg*NPf3BG6n_eKjaITY`O_H-oc5aoNTmQg+ zF!|o3y!$z6_j8NRt;^2ii!Vg;_3Nh(-u8Fqta*=L_H;^~&IRkTXKUWOam7=AvF}2k z?CFv`T?>0}db$h#<|_l224w$s$-n*Wi6#Hu3*kHd_A8M~k?Y6a8hCR+?hZ@c;g76K z{_p3)cUoB^&!aF}V@u9l_?q#&zm-^Pz4GFv7cWP0;e2Ix zJK7MqSA&{5ayFRfyzi!`17^5tFuHs4-mcss+1n*~yB0!nXtxyFy%gFjdH3BnBX@)B zYLQ$m^T+28F7z%uzwp#zVEeLbN8aU|b6>Dspg(hi%`YB7H9w$MA4e|VwByVBRhH2n`Ru*jCZw0lyub{`>vGaD(yBZT|jl(}z?G z$RBoF!}XRAx7UZ=mKzQlYHqk`sJT&ZBys~CZnaF>$-XtKG1pi67>osH=9_sR9EcYiha$<<#@8hhBm3<A67_#e#(n2;OAmP|+4NHU({cucq} zCg+3ViIftOm^Gh zW8*Oa6I=yY;8F^Wqp2ZIp;c!n4t|U{C8Ay~Fgd3cM{JmAL`HFXd_t+k+{t8yAC3&- z^cc@!Oq>+7V>lRL8N|nsE+nsuvTC*L3B}FA43!EyT6Y@b-q`R&pol^3of}sQ-=y}Z{_ET zyIyv;NbZ)`Ti*y>4atE$Qee-Ld+*etyv3b6cGI#x?`oXulU(i7bl%rAPtS7;hop|} zi@qJxwt~qan|zYVw^-k~Z0cGme}CSS2X5K}00B;4!R3=(0m&6ucC|s)J8FyV?Kkc1h1$CFlQWZYZJSivHvi+B zwOs|5M|QPJuGVE&dx3GuOoPNU%sq08>G;ftY99M?)kZmeAG^E{RJ^{?>3wpO;iFB4 zCj)eh9CF%2h#^Umd)pzeE&}(*+t#3$<#^HM%J3_jF86AVDX8)$XaHPL;DpmVz4(IZ4&0e0w7l5{TF{K!MDx zK{Ugi&@7r*N+m_6S*M`iToXD8(yS1I3pZGxg0WmSr&`-XzM%LEFV539(m&;+D;Oq>n8$~v4L;UXiz9l}9(1@3N$L}F2a z8%pDeh^kV7zsh1S z)+)|21%h_`G_-#Y{`r5yYYKgAap^Q=zvSP)$TSuj{Z~SlLUQ91QsWadwJREn*(LdR zRW|RG8h1|D7A)4YV=s-JJ@e9;Tzc8kk!NZO-Uiv*CVATy+jlH^cTPQfM@2EfwC<&v z=Bb0X*SAkQ^7gv9&62$%?{Mdi&K#QV2TUyE%kjB`^S$%W&p)-;*t5)RDKPf)wi%nu zv`9?L{N{Iqi%iQhb0E(+bM6`2487_^4IQgBh;iroZ(14(Os&jpl9)|%Ke@$hSv4Z2 z@gA%eq9q)3;(Z{2%XQY>QzG4%Ts9RtBgA352v!w$lN_Jw?y0e*Sb=QzkZ4O5k8Uq% zCQf}5VMsevbUO1msAQ7dE_@W~2oUnE@B%tSQMZxxHfs1g>Rm#;w~_5{Xv62GFhzCV MKVtAvkE%xg4?nw8RsaA1 literal 0 HcmV?d00001 diff --git a/lib/__pycache__/gether.cpython-312.pyc b/lib/__pycache__/gether.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8283edf4ec88b09d5d68a9c84b25e16294f541d GIT binary patch literal 7528 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 D4h5)T literal 0 HcmV?d00001 diff --git a/lib/__pycache__/shopyy_verify.cpython-312.pyc b/lib/__pycache__/shopyy_verify.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87a63019c6627ee483e8cc002b79ecfdac4ff2c4 GIT binary patch literal 4020 zcmahMZEV}t@sT1W>cf&Ef5dj}npQq)IhN#)BuQ#CSCryO@I#lwR@B) zDM5--(DB~Ad-v|$-Mc&fv#!pA;OTn!zBm*@=s$R1Khy^DKmueL@rWlRG=blwM5M?G zGUb?X5Qs#F5$|{p@zhnaykde9Xr7+!LP%-hofA$H36$WxN|a}30vmE`URO780?iZi zM8tI;kH;gV$sC+k*PvuDa zxC@S)W5#9Et8bpLIdUjrn;PZN3{&N?u^F$8Ro}VFdIGPitIpV%trr*-MaXdlA2i}p z@Lr~y%W85WBP*JMPh`5L1TCR)CryV1U}VygJSCL%=5wpqIkm`#7&Y;_&&ehPCo1tha6kQF)^RC;W*1{l=@-Cykb$S2!{VRQ|_1p5) zS;k;|=a@z2oNv*$bZm`j!DG*&=U2W5APFTa-h=_x+w$~vK$cO}(^bC^$c-&q8y~=n zpP7oE%VP}rs>g{Op=_y<=&(Hvo+6$Y3lvY=m!~!w=6`7aC&U9^xnKl3dm`c*&-y^C zum7}o_p@Joeeo}M|8@bc(1D+h92gmUX*_ZK=#fJk9M;^u{LiUhCgI9G%t^yAHCVoU25nq?s#ssVK{&2R-oZ_uyHB9 z7TjjEhcCZ*@y#m(*T~iOfhFqK9wXRj_=4v~7e~*HEshmB*L-d3btq7;)Pw9tobCjI z<#b3|=mT(j9!(OEB$x%F#$6Rh5@-r7kR&*buecCjXP#;SK|GD&0+l0y;>5TP=Z7Rv z2{Yc6qb{M})X2+08k!veo9xsoiG*h`RfqL<2#laN!yt*qJLhOScwpxr+$l$2LcH7B zt)eM}!Bz^Nz?maKVlPN^ltREvOI9IY$!c4X6@j`f0}-XpV@~k3SL|I2>`rJoo~@1p*2l!IL~Vo2Pjv@8TKW zy-3ek2Y3|l9^U)07ec7w(p<%GypON@n5?Y~GdKtzdw4&%miQ!K8^o#^6?0XY?TijV zNAW?+k(*`*A)NeLB_qI0sMd8qCxB9qDaTNxVO;OzG8susa=3`bXH+@e!)n55Ee;o2 z1iU-mt;hB0=)v(GeR}Vy;ojJ@Jwn<-_5cz+{DOrKV*J%(7QS0|W{3NFSW!O^9+$PQ z14Az;T$&H>eP=klC)~q^?aFOY`gZQbv@tQM$f`W4v4{96p)#YB`+IeF_;oSOLwyOG zw2;bZ^Cp#)6;=1DTuO+_iU@T{Ul@~hQIfd$u2?VIwXuL5KhE~`#(MX%K-j%=FME3T z&Io$|PAt4GoIE0G@m&Lhv4P!e*OAfVV=woxk~k}{hlS*<9AParg*Ye}0}&wg7&pl& zqP+z4tjL%I~PerKhquJ2H?sK}l_1cdJ4&nw*YiIXzfGqbd2MC<%I~ zN&(wzld_W1{g^f>Xt0JCRIPTLl6j$atcsdo*xlO;Z4;F#D*{jz zbwQ6$Yg$GfipP_p%1M))iYtFi7t**e$CBwdml5MhS(2b+S8yS0M8s z0Paz0l+=eFsVR6?7ZkacCH93v<@3vjvH-T@3g;dZ0$0(_Zi`c5$-a=aigXEa_hy`brN;FMSj!b z-|WvHei%kAZH19bZlkU98`{z2&%gN4gPPgp-1%H_>#N1L_+mP{w)vg>D2Ti~c5&>| z(fpVZXe+!{3T(YXTn|KUH+8JkT@gy`&ef(}#o#WZo?U4w)pr$rT~^J?HiKe!|9a-` zAAWi7cW3Th{{2^<*QHs+?#F1YFv2#?nghX63eQ@w9}eN z%;|Jin~d&>5GEs}Aw0zNlu3A?$HeKzq#+Byb#Ow$5z@TrkZ`9U18&lDa6JSF5E&Xw zce!oo$4i1WiULPURd!&q2cI#a;X_&>WCkQzt&IgU@Yq6=m{f5BecK8k0Ob&Bcv6t) zoj)2!O(05*Lt%$-c}TXpBMyp09o!+p1#+Y8)E>y-n)ZTq4* zwnW|9ynRJm-Q2T889{bstHE3SPZhz2NfC$PT=+wa>b^e8^P$h3$?`8J?N?S zeBjHDc>bsnc)HjzR0{0<6jXZN2zJ~IZZ8G5ue^J0uJp|QwcztuNXM1UrIx;8bN{s+ z#io5Xg8M-X+Y6!W3*_Hj^>(auUiWUfy{WO-)PL>Z-=Dd$=_mi-jNqna*Lhcgy6nE_ zUWu(~?ETQ*LLn2Qk zOiv;KX)P;Z+?Pn4%5qY9MuBh9a^t9s0`^B_-HUQNiPM6D0dC<{++n_lz9nh8@moJb zw?AkYpc~5Em 1: + lib.Server.reflush_browser() + messages.sendInfo("已保存") diff --git a/lib/model/__pycache__/goods.cpython-312.pyc b/lib/model/__pycache__/goods.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db30e534311632694f64ada5f9d27570089bef3f GIT binary patch literal 17087 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 literal 0 HcmV?d00001 diff --git a/lib/model/goods.py b/lib/model/goods.py new file mode 100644 index 0000000..f303304 --- /dev/null +++ b/lib/model/goods.py @@ -0,0 +1,417 @@ +############################################################################# + +# Author: Cerys +# Update: 2026-02-06 + +############################################################################# + +import os +import orjson +from core import excels, types +from utils import db, messages, files, formats + +class GoodsModel: + def __init__(self, db_path: str): + """ + 商品模型 + + :param db_path: 数据库文件路径(不存在则自动创建并初始化表) + + """ + self.__source_folder__ = "sources" + + create_table = False + if os.path.exists(db_path) == False: + create_table = True + + self.Db = db.Db(db_path) + if create_table: + self.__create_table__() + + def __create_table__(self): + table_sql = ''' + PRAGMA foreign_keys = false; + + DROP TABLE IF EXISTS "goods"; + CREATE TABLE "goods" ( + "spu" TEXT NOT NULL, + "title" TEXT NOT NULL, + "brand" TEXT, + "attr" TEXT NOT NULL, + "desc" TEXT, + "item1" TEXT, + "item2" TEXT, + "item3" TEXT, + "price" NUMBER NOT NULL, + "old_price" NUMBER NOT NULL, + "images" blob NOT NULL, + "url" TEXT NOT NULL, + "other_data" blob DEFAULT "{}", + PRIMARY KEY ("spu") + ); + + DROP TABLE IF EXISTS "goods_son"; + CREATE TABLE "goods_son" ( + "spu" TEXT NOT NULL, + "item1" TEXT, + "item2" TEXT, + "item3" TEXT, + "price" NUMBER NOT NULL, + "old_price" NUMBER NOT NULL, + "images" blob NOT NULL, + "url" TEXT, + "other_data" blob DEFAULT "{}" + ); + + CREATE INDEX "search_spu" + ON "goods_son" ( + "spu" ASC + ); + + PRAGMA foreign_keys = true; + ''' + self.Db.execute_script(table_sql) + + def add_goods(self, main_info: dict, son_infos: list = []): + """ + 添加商品 + + :param main_info: 主表信息 + :param son_infos: 变体信息列表 + + """ + if main_info['attr'] == 'M' and len(son_infos) == 0: + raise Exception(f"变体丢失 {main_info}") + if len(main_info['images']) == 0: + raise Exception('无图片') + + self.Db.table('goods').insert(main_info, True) + self.Db.table('goods_son').insert_all(son_infos) + + def is_repeat(self, url): + """ + 判断是否需要合并 + + :param url: 商品链接 + + """ + count = self.Db.table('goods_son').where('url', url).count() + if count > 1: + return True + return False + + def select_goods_to_lists(self) -> list[dict]: + """获取商品列表(包含变体)""" + messages.sendInfo('开始查询') + db_data = self.Db.table('goods').join('goods_son', 'goods.spu = goods_son.spu').select( + ''' + goods.spu, + goods.title, + goods.brand, + goods.attr, + goods.desc, + goods.item1 as main_item1, + goods.item2 as main_item2, + goods.item3 as main_item3, + goods.price, + goods.old_price, + goods.images, + goods.url, + goods.other_data, + goods_son.item1 as son_item1, + goods_son.item2 as son_item2, + goods_son.item3 as son_item3, + goods_son.price as son_price, + goods_son.old_price as son_old_price, + goods_son.images as son_images, + goods_son.url as son_url, + goods_son.other_data as son_other_data + ''' + ) + messages.sendInfo('查询完成') + return db_data + + def select_goods_to_dict(self, key: str = 'url') -> dict: + """ + 获取商品列表并转换为字典格式 + + :param key: 字典的键 + + """ + db_data = self.select_goods_to_lists() + dict_data = {} + for item in db_data: + if item[key] in dict_data: + dict_data[item[key]]['son_lists'].append({ + 'attr_items': [item['son_item1'], item['son_item2'], item['son_item3']], + 'price': item['son_price'], + 'old_price': item['son_old_price'], + 'images': orjson.loads(item['son_images']), + 'url': item['son_url'], + 'son_other_data': orjson.loads(item['son_other_data']) + }) + else: + dict_data[item[key]] = { + 'spu': item['spu'], + 'title': item['title'], + 'brand': item['brand'], + 'desc': item['desc'], + 'attr': item['attr'], + 'attr_items': [item['main_item1'], item['main_item2'], item['main_item3']], + 'price': item['price'], + 'old_price': item['old_price'], + 'images': orjson.loads(item['images']), + 'url': item['url'], + 'other_data': orjson.loads(item['other_data']) + } + if item['attr'] == 'M': + if item['son_price'] == None: + continue + dict_data[item[key]]['son_lists'] = [ + { + 'attr_items': [item['son_item1'], item['son_item2'], item['son_item3']], + 'price': item['son_price'], + 'old_price': item['son_old_price'], + 'images': orjson.loads(item['son_images']), + 'url': item['son_url'], + 'son_other_data': orjson.loads(item['son_other_data']) + } + ] + return dict_data + + def select_goods_to_goodsinfo(self) -> list[types.GoodsInfo]: + """获取商品列表并转换为GoodsInfo对象列表""" + db_data = self.select_goods_to_lists() + index_data = {} + for item in db_data: + if item['spu'] in index_data: + index_data[item['spu']].p_lists.append( + types.GoodsInfo.GoodsInfoP( + attr_items = [item['son_item1'], item['son_item2'], item['son_item3']], + price = item['son_price'], + old_price = item['son_old_price'], + images = orjson.loads(item['son_images']), + url = item['son_url'], + other_data = orjson.loads(item['son_other_data']) + ) + ) + else: + try: + index_data[item['spu']] = types.GoodsInfo( + spu = item['spu'], + title = item['title'], + brand = item['brand'], + attr = item['attr'], + desc = item['desc'], + attr_items = [item['main_item1'], item['main_item2'], item['main_item3']], + price = item['price'], + old_price = item['old_price'], + images = orjson.loads(item['images']), + url = item['url'], + other_data = orjson.loads(item['other_data']) + ) + if item['attr'] == 'M': + index_data[item['spu']].p_lists.append( + types.GoodsInfo.GoodsInfoP( + attr_items = [item['son_item1'], item['son_item2'], item['son_item3']], + price = item['son_price'], + old_price = item['son_old_price'], + images = orjson.loads(item['son_images']), + url = item['son_url'], + other_data = orjson.loads(item['son_other_data']) + ) + ) + except Exception as e: + messages.sendError(e) + return list(index_data.values()) + + def to_amazon_xlsx(self, goods_url_path: str, save_path: str): + """ + 整数据库导出为亚马逊导入表格 + + :param save_path: 保存路径 + :param goods_url_path: 未去重商品链接路径 + + """ + goods_data = self.select_goods_to_dict() + goods_urls = files.load_lines(goods_url_path) + WookBook = excels.WorkBook( + f'{self.__source_folder__}/amazon商品模板.xlsx', + save_path + ) + + for goods_url in goods_urls: + key = '#'.join(goods_url.split('#')[:-1]) + if key not in goods_data: + continue + + spu = formats.url_to_spu(key) + category = goods_url.split('#')[-1] + + WookBook.add_row_of_list([ + goods_data[key]['title'], + spu, + goods_data[key]['other_data']['popover'], + goods_data[key]['other_data']['popover_num'], + 0, + goods_data[key]['price'], + goods_data[key]['old_price'], + orjson.dumps(goods_data[key]['images']), + goods_data[key]['url'], + category + ]) + + # for goods_info in goods_info_data: + # WookBook.add_row_of_list([ + # goods_info.title, + # goods_info.price, + # goods_info.images[0], + # goods_info.other_data['popover'], + # goods_info.other_data['popover_num'], + # goods_info.url, + # orjson.dumps(goods_info.images) + # ]) + + WookBook.save() + WookBook.close() + + def to_shopyy_xlsx(self, save_path: str, save_bitch: int = 150000, shift_urls_path: str = ''): + """ + 整数据库导出为Shopyy导入表格 + + :param save_path: 保存路径 + :param repeat: 是否合并商品 + :param save_bitch: 超过多少行时自动分割 + + """ + def get_wookbook(bitch_index: int): + return excels.WorkBook( + f'{self.__source_folder__}/shopyy商品模板.xlsx', + save_path.replace('.xlsx', f'_{bitch_index}.xlsx') + ) + + def get_shift_url(): + shift_urls = {} + if shift_urls_path: + urls = files.load_lines(shift_urls_path) + for url in urls: + if url == '': + continue + shift_urls[url] = None + return shift_urls + + goods_info_data = self.select_goods_to_goodsinfo() + bitch = 1 + shift_urls = get_shift_url() + WookBook = get_wookbook(bitch) + + count = len(goods_info_data) + for index, goods_info in enumerate(goods_info_data): + messages.sendInfo(f"{index}/{count}") + if shift_urls_path and goods_info.url not in shift_urls: + continue + + rows_data = goods_info.to_shopyy_row_data() + for row_data in rows_data: + WookBook.add_row_of_list(row_data) + if WookBook.start_row > save_bitch: + WookBook.save() + WookBook.close() + bitch += 1 + WookBook = get_wookbook(bitch) + + WookBook.save() + WookBook.close() + + def to_woo_csv(self, goods_url_path: str, save_path: str): + """ + 整数据库导出为woo商品CSV表格 + + :param goods_url_path: 未去重链接txt路径 + :param save_path: 保存路径 + + """ + goods_urls = files.load_lines(goods_url_path) + category_data = {} + for goods_url in goods_urls: + category_name = goods_url.split('#')[-1].replace('/', '>') + goods_url = '#'.join(goods_url.split('#')[:-1]) + category_data[goods_url] = category_name + goods_info_data = self.select_goods_to_goodsinfo() + + bitch = 0 + save_data = [] + for goods_info in goods_info_data: + rows_data = goods_info.to_woo_row_data(category_data[goods_info.url]) + save_data += rows_data + if len(save_data) > 10000: + excels.save_lists_to_csv(save_data, save_path.replace('.csv', f"{bitch}.csv"), r'sources\woo商品模板.csv') + save_data = [] + bitch += 1 + excels.save_lists_to_csv(save_data, save_path.replace('.csv', f"{bitch}.csv"), r'sources\woo商品模板.csv') + + def import_shopyy_xlsx(self, excel_path: str): + """ + 导入shopyy表格为数据库数据 + + :param excel_path: 表格路径 + """ + shopyy_data = excels.read_excel_to_dict(excel_path) + for url, main_goods_info in shopyy_data.items(): + main_info = { + 'spu': main_goods_info.spu, + 'title': main_goods_info.title, + 'brand': main_goods_info.category_name, + 'attr': main_goods_info.attribute, + 'desc': main_goods_info.desc, + 'price': main_goods_info.price, + 'old_price': main_goods_info.old_price, + 'images': orjson.dumps(main_goods_info.images), + 'url': main_goods_info.url, + 'other_data': orjson.dumps({}) + } + for index, item_name in enumerate(main_goods_info.attribute_items): + if item_name: + main_info[f"item{index+1}"] = item_name + + son_infos = [] + for p_info in main_goods_info.p_lists: + item = { + 'spu': p_info.spu, + 'price': p_info.price, + 'old_price': p_info.old_price, + 'images': orjson.dumps(p_info.images), + 'url': p_info.url, + 'other_data': orjson.dumps({}), + 'item1': None, + 'item2': None, + 'item3': None + } + for index, item_name in enumerate(p_info.attribute_items): + if item_name: + item[f"item{index+1}"] = item_name + son_infos.append(item) + + self.add_goods(main_info, son_infos) + + def splice_db(self, db_folder: str): + """ + 拼接指定文件夹下所有数据库文件 + + :param db_folder: 数据库文件夹路径 + + """ + db_lists = os.listdir(db_folder) + db_index = self.select_goods_to_dict() + for db_name in db_lists: + splice_db = GoodsModel(f"{db_folder}/{db_name}") + goods_lists = splice_db.select_goods_to_goodsinfo() + for goods_info in goods_lists: + if goods_info.url in db_index: + continue + try: + self.add_goods(*goods_info.to_db_data()) + except Exception as e: + messages.sendError(f"{e} {goods_info.spu}") + db_index[goods_info.url] = None + messages.sendInfo(db_name) diff --git a/lib/module/__pycache__/agatameble.cpython-312.pyc b/lib/module/__pycache__/agatameble.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c884674880286d231e524d16fc6a2db77875753 GIT binary patch literal 9012 zcmd5iTWlLwc6Vm@7GJVRiK0kJq~5YD*_K_|j_t&bWyMY`r|V7a$W|CyGm=R2q0WpV zTNye*ks{Pqg=y?=C2zK{jDCcv1FYIFek=lpNCmKa|)?v zlJtCxkuBkwFdvRHmt)KnQ3Y>=I$r7H#oX%oHC=zzTrpI*y4=%jS@Rf(wN> z628{{_BXvJ4qd_@x>VYwOR>o1OL2t(mn!QYoV&*E0DwhzHHQYZuVIdG{g8eAHu~IT zTQZ6!k7RWgtbWnzmt2ifvsY?qms&fDb;#WOM`SQt9>Z)T8(4;)XA_&)iv>$zuMhGS zG=oyWR4fWxQxwnIl4#;?X{!jl30p>SGpdPK)fi1`P$^O%PoNAjc?O{rB~Vekv@MCI z%CmKXE^6K~3{6^Bo}q^smhaAp+P>RI>uJMXye*JY27!n=)OvL;=-)%MQ82uR{;^C2 zf%tlDP89FbmLwQ7YJewH&GnnK87ZS+Jb@;E3lx|H(^FP&7R*msy#;3cnAO*9vwDlR zdRi~k0T1*V-LNuK&6Sz887Zq^g_T``mDvQ_Q&wgd>`z&l17`e~l{vRrnPc?nbx0sUVOho(y|X+&#~m6PT3A>ZtUj-?p~c--=feE#k)fgf zi_b;qBSAKqNWp5Y zZpun47D-&egR&8i0g!CM6|DJW8^4gOhOTbPR@|yUAP-M+a)F15=z z%<4GCVo|FY4~NfZ`_{xZ;?>pz!$O-@yx6))$$n?t31Hmcf|`39Ngi+7_Q+(tVOlcX zur671?mZi(y^_aw+jPrx+jh&A>%NfpyqdOs?(0jxB(?MuT6T&pJF}F%D z#KxYiA>TNZ9+R3n3QfIYQ*WVZx7f6MyiE^!4QtOMUvI&;OZ4r^4W7vRM$=~=w(rVVrM8`gwjr@?Xx)-;dj{Ix z?t*ux=-rt;nfDH7j0#Z$V#`3GWuMrxZ~ff8j(p3}j7f6(3(ii_*_k!2U0WyCd)K}9 z_T0O0?{uzfY{U7&qu^j6I4lN-*U#jG$1{$H-TO1+QrB>yYeeiCxo63DJrC{n!9x46 z*gm{|GT;78=9Cl|Dg^e4fqjL*VKH#{-noxD@`00?(~>JtaP^9=-t4LDGwb1druD^- z>OVU0QU52$K8b#EDc3jgz;*E}GwS(^KR8feuvm}${l$Ld^yf@Iwh8Z|wR#7qN=h6C z5W+I9n6rO?9b4JXDAh1!0`VTI9Dgu_q#2pFgRkLj)KYPG6_OCz);QKOW@VEatt)|7 z6%~xjXFCAzt0+NTrCvn~+ZpXzhOc6N!Q_IH9tI`7ooOwby_8-cYn#Yu*&J2~t(fEr zPnY+DA|*kQ>cV0`DC08;C}s7EHZV4 zZO0S4a2ABD&!Fg5_5jpn%SXWxc% zr`Bh^Y0dfRypt(7uZYeo8_syqh#U=H7fpn%@pn%5H;>%SD;I8FSed*znd{*5?)kLk zVRJ|Nl;rQeJ$Y*~dob_cpB|U2jU|rF3QlU#{V0!X-ubpxZ3Achez=Oap8~HtWg$UvY5Sh4<6AVU zqCTxwDFwY?5Oe||P=Z;od}zB{y3?erU{q}IZx@IeBIVeIt9FgDlv8lh4jOq;Nk!KS z^);o(S%%mlIHFitZArq8C{|Tl5?!ysX(f2QprZK>ZKB}NMrg;C6CisOy#pw>ea{S8 zZ~(OrVE-C~x|B<35Qr-y0Qpl`E!K`(lF*=i0Ziook%VvBTi5>=iQ86JlsQ&Q;!|$8 zC)JXG2Ejc;V0jc=wCmSZ_qgT4rX4R!AMIAOjh}85tf8_-nnBPi>jI$;TIJKNjJdT9 zPTZiIsv!m2CP+I-NdlwS|FZG*w`iy>NG&17~Lwe*_0`0DG@W zQuY@dudPYbGH@OsNT#}Y8EP4+PF|@^EdQ(4=67otIuW}!<#{?@evbwrcr=>-hEOrr z9#g+Xqm<{mXMtGQL<^=;9q(%$pVBbn601EdDvy2OiQ~8(s6Mo$i$ZH_s)Uhh1(_-= z0qE}!ClLYx8+Hyh3<66vzf|#ODR3TYgqt8_Q_CwahnQ-4<<+62n&ZYws7YfvDBD*+ zL=cXIjxU~mx+9CDxQZhT;eai!>qT&iFVBNdnz*=Ua`Dm@T%|w+_NS}hFpT12a}X|8 z@K$-&cmVzZn=+@=#Pvm|iXC@F;xm9nrZ{$L(XbGqA^IVsMdK_JnVIE}fDeqV4toS= z?KmsD$1FGra1D?xkvN2-OP;Y>S3To$UAC)L%`;XUat;dtc8}k#@{TL07KgX-jw2BD z7(5(HPK9IK(LwA|SAd=cl-v?z5L1`7e{_1zCVwUy_1Xkf53+;viBwS=NHF+ z04y+}B~Jg&zR1KmnK+|NWG~cMz%n#Ae;!z^6?miRB^7K zjwQo9tHXfiF?IgooUgoj7 zk`3_?4bNfVC3uFH0{s_9GFTn2%lnF6iun{3AWpoU$|<$Zx(O(V+$ zqbgf0SXkNCOwUzm;0o2-#wYNSjsiYz6m|qO?Gw^$ZY?%KVBtn=DORutMSE~fGMBw=oY7q9eak)&*H$bdHhd zDKIN<5f>N*;gu)PH(=^WOgSi(fRi-fp|qCN;K5 z?q;03n(!Y~yd6&*Ca*nxy68fUp7cq@zDZvHsyltUWYBWK%lYOhX!?7yM3#K-wX#9W zUdsm#*0N{Q`QVWU&ZF374ZvER4VgoK_tskdXO5uc4Xie23|}~Ql)D}Vc0xS3V=(iQ z)YQKA{AW!)rHxoKZ+QCQwQ}wFubz=Qd)^7eaH?w*=cD9FT#S*YF}uge zaXbMR(0P_rm^ntij@dm%GML@CfU%S0G%h-3FuVP;RAy9a@5-Di>L_=hOzG8p=e0uT zb+PmMsu^U=-BxgQiLNfGtyc=}Tx*d6-BQ<}6de4j&d}2cFR|{XVh0KiJlU)9EAW}4 zLkf0fEWdC_z5N->hOgvf@9yo)KwBbQzSY@TFwczR&UEKxOF448?L*_Th z50Zt!7sSCABwyDWBl-rUj)ClXv17l~+LJYkt-FDs?2BUX0MM58h;4h}ad_wy@faC~ z8lNbH*-`gU_UcC0fmO5A(wTknvz8Ib*HiEfioU_@wFka~U+GczL5Ri&2jIwd?N*8G zRHzCR?UcD*0-x#B(y8Sa@o~d}y|0TYlCZgDr|a9|EJ3&fvZwvqCx?-<`432MZc`7f zvth-2)BKA%5TXX(>am{>zdMibhQ_Bv2hO{kXZ7gQ5%SDo>eGV;-196s zuG8NqaQ(i~e3qo{*E`1t$oql1@gvmz0doA1{{El?*N^C7^gs0ESqs%?!w>)Xp$Q)T zEuoP5e2;$w5DNYEd^o0#unj<%@`^9(ur&b>fM?ke+<6>l*d(#jI1?a~ZKajkN?aNYk4n9UuEgo>{ zz1iU+f?|E&J?dYe8>+=lme70GUU@%sC-f(T>#CcH5s$-9GIUc(mSM5KrP@sWJRga1 zis4nC4~?pC%_*dOfK+JZWzBS0WyDeTT>!_FbMONgRHO)k_&sX-J?eO(i;;vkGqUpR M&1e6Ja9siaAJSkZs{jB1 literal 0 HcmV?d00001 diff --git a/lib/module/__pycache__/ancientnutrition.cpython-312.pyc b/lib/module/__pycache__/ancientnutrition.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ea323ff8e23d21116ad2b09e2c21c3f7fe51b89 GIT binary patch literal 16042 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 Z0y91kcS&&FQp@t=Hy-~rhG?$%{{u452x9;M literal 0 HcmV?d00001 diff --git a/lib/module/__pycache__/bricomarche.cpython-312.pyc b/lib/module/__pycache__/bricomarche.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b171261952f402320779d6c13e461579b97eb580 GIT binary patch literal 8314 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 H!r{LG6}1f- literal 0 HcmV?d00001 diff --git a/lib/module/__pycache__/condom69.cpython-312.pyc b/lib/module/__pycache__/condom69.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..401330274ac0f5bc4050f47bda0dcdab00600b26 GIT binary patch literal 6494 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 Pb?f-e8Llo?D6tbr|B1e*d(?6koKXnXDq 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 literal 0 HcmV?d00001 diff --git a/lib/module/__pycache__/ssfshop.cpython-312.pyc b/lib/module/__pycache__/ssfshop.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc05670d0656731d3adfdf702b3f0f9fa3d6a7c3 GIT binary patch literal 15858 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 U{fr>oi%%~fSvvAhgn3o{zfN9MaR2}S literal 0 HcmV?d00001 diff --git a/lib/module/__pycache__/tfashion.cpython-312.pyc b/lib/module/__pycache__/tfashion.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efab6ad742486e6ae5e6e183a49990b2ffc505d8 GIT binary patch literal 6840 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 Ul!5SMU%xr=;l$Sn(<=M_07jV|yZ`_I literal 0 HcmV?d00001 diff --git a/lib/module/agatameble.py b/lib/module/agatameble.py new file mode 100644 index 0000000..47bf346 --- /dev/null +++ b/lib/module/agatameble.py @@ -0,0 +1,176 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "agatameble" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 1, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.agatameble.pl/') + one_menu_eles = self.tab.eles('xpath=//*[@id="root"]/main/div[5]/div[3]/nav/div') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele.ele('@tag()=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/div[1]/div') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + three_menu_eles = two_menu_ele.eles('xpath=ul/li') + for three_menu_ele in three_menu_eles: + three_link_ele = three_menu_ele.ele('@tag()=a') + three_url = three_link_ele.attr('href') + three_name = three_link_ele.text.replace('/', '-') + category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}") + goods_urls = [] + + while True: + try: + self.tab.scroll.to_bottom() + self.tab.ele('xpath=//*[@id="contentContainer"]/div/article/div[2]/div[2]/div/div/a').click() + except: + break + + goods_eles = self.tab.eles('xpath=//*[@id="contentContainer"]/div/article/div[2]/div[2]/section/div/div/div') + for goods_ele in goods_eles: + goods_link_ele = goods_ele.ele('xpath=a') + goods_urls.append(goods_link_ele.attr('href')) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + + tab.ele('xpath=//*[@id="contentContainer"]/form/div[1]/section/div/div[2]/button[1]').click() + image_eles = tab.eles('xpath=//*[@id="root"]/div/section/div[2]/div/aside/div[2]/div/div/div/div[1]/div/div/div/div') + for image_ele in image_eles: + image_link_ele = image_ele.ele('xpath=div/img[2]') + image_urls.append(image_link_ele.attr('src').replace('width=', '').replace('height=', '')) + + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = '' + try: + brand = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[1]', timeout=10).text + except: + pass + if brand == '': + title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1').text + else: + title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[2]').text + + desc = "" + desc_eles = tab.eles('xpath=//*[@id="contentContainer"]/form/div[1]/div/div')[:3] + for desc_ele in desc_eles: + desc += formats.clean_html(desc_ele.html) + + price = float(f"{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[1]').text}.{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[3]').text}") + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + color_urls = [url] + color_eles = tab.eles('xpath=//*[@id="contentContainer"]/form/div[2]/div[2]/div/ul/li') + for color_ele in color_eles[1:]: + color_link_ele = color_ele.ele('xpath=a') + color_urls.append(color_link_ele.attr('href')) + if len(color_urls) > 1: + m_data = ["Color"] + + if len(color_urls) > 1: + for color_url in color_urls: + color_images = main_images + if color_url != url: + tab.get(color_url) + if brand == '': + title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1').text + else: + title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[2]').text + color_images = get_image_urls() + + price = float(f"{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[1]').text}.{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[3]').text}") + old_price = price + + try: + item_name = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[2]/div/ul/li[1]/div[2]/p').text + except: + continue + attr_items.append({ + 'url': color_url, + 'items': [item_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + goods_info.p_urls = color_urls + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/ancientnutrition.py b/lib/module/ancientnutrition.py new file mode 100644 index 0000000..41970d2 --- /dev/null +++ b/lib/module/ancientnutrition.py @@ -0,0 +1,325 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "ancientnutrition" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 1, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://ancientnutrition.com/') + one_menu_eles = self.tab.eles('xpath=//*[@id="__next"]/div[4]/div[1]/div[1]/div') + for index, one_menu_ele in enumerate(one_menu_eles[1:]): + one_link_ele = one_menu_ele.ele('xpath=div/a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + print(index+2) + two_menu_eles = self.tab.eles(f'xpath=//*[@id="__next"]/div[4]/div[{index+2}]/div/div/div[2]/div/a') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + # three_menu_eles = two_menu_ele.eles('xpath=ul/li') + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}") + goods_urls = [] + + while True: + try: + self.tab.scroll.to_bottom() + self.tab.ele('text=Load More').click() + except: + break + + goods_eles = self.tab.eles('xpath=//*[@id="__next"]/div[9]/div/div[2]/div[2]/div') + for goods_ele in goods_eles: + try: + goods_link_ele = goods_ele.ele('@tag()=a') + except Exception as e: + continue + goods_urls.append(goods_link_ele.attr('href')) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + + image_eles = tab.eles('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[1]/div/div[2]/div/div[1]/div') + for image_ele in image_eles: + image_link_ele = image_ele.ele('xpath=div/span/img') + image_urls.append(image_link_ele.attr('src').replace('_150x150', '_1450x1450')) + + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = '' + # try: + # brand = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[1]', timeout=10).text + # except: + # pass + # if brand == '': + # title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1').text + # else: + title = tab.ele('@tag()=h1').text + + desc = formats.clean_html(tab.ele('#productDescription').html) + + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + try: + tab.ele('text=Select Flavor').click() + m_data.append('Flavor') + except: + pass + try: + tab.ele('text=Select Size').click() + m_data.append('Size') + except: + pass + try: + tab.ele('text=Select Quantity').click() + m_data.append('Quantity') + except: + pass + + if 'Flavor' in m_data: + tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[3]/div/div').click() + color_eles = tab.eles('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[3]/div/div[2]/div') + color_xpaths = [] + for color_ele in color_eles: + color_xpaths.append(color_ele.xpath) + + for color_xpath in color_xpaths: + while True: + tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[3]/div/div').click() + try: + color_ele = tab.ele(f"xpath={color_xpath}") + color_name = color_ele.text + break + except: + tab.get(url) + color_ele.click() + tab.wait(3) + color_images = get_image_urls() + if 'Size' in m_data: + try: + size_eles = tab.eles(f"xpath={tab.ele('text=Select Size').xpath.replace('/span[1]', '/div/div')}") + except: + size_name = 'Default' + if 'Quantity' in m_data: + quantity_eles = tab.eles(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', '/div/div')}") + for quantity_index, quantity_ele in enumerate(quantity_eles): + try: + quantity_ele = tab.ele(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', f'/div/div[{quantity_index+1}]')}") + quantity_name = quantity_ele.text + except: + continue + quantity_ele.click() + tab.wait(2) + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [color_name, size_name, quantity_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [color_name, size_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + size_xpaths = [] + for size_ele in size_eles: + try: + size_xpaths.append(size_ele.xpath) + except: + continue + + for size_xpath in size_xpaths: + try: + size_ele = tab.ele(f"xpath={size_xpath}") + size_name = size_ele.text + except: + continue + size_ele.click() + tab.wait(3) + color_images = get_image_urls() + if 'Quantity' in m_data: + quantity_eles = tab.eles(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', '/div/div')}") + for quantity_index, quantity_ele in enumerate(quantity_eles): + try: + quantity_ele = tab.ele(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', f'/div/div[{quantity_index+1}]')}") + quantity_name = quantity_ele.text + except: + continue + quantity_ele.click() + tab.wait(2) + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [color_name, size_name, quantity_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [color_name, size_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [color_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + color_images = [] + if 'Size' in m_data: + size_eles = tab.eles(f"xpath={tab.ele('text=Select Size').xpath.replace('/span[1]', '/div/div')}") + size_xpaths = [] + for size_ele in size_eles: + try: + size_xpaths.append(size_ele.xpath) + except: + continue + + for size_xpath in size_xpaths: + try: + size_ele = tab.ele(f"xpath={size_xpath}") + size_name = size_ele.text + except: + continue + size_ele.click() + tab.wait(2) + color_images = get_image_urls() + if 'Quantity' in m_data: + quantity_eles = tab.eles(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', '/div/div')}") + for quantity_index, quantity_ele in enumerate(quantity_eles): + try: + quantity_ele = tab.ele(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', f'/div/div[{quantity_index+1}]')}") + quantity_name = quantity_ele.text + except: + continue + quantity_ele.click() + tab.wait(2) + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [size_name, quantity_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + if 'Quantity' in m_data: + quantity_eles = tab.eles(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', '/div/div')}") + for quantity_index, quantity_ele in enumerate(quantity_eles): + try: + quantity_ele = tab.ele(f"xpath={tab.ele('text=Select Quantity').xpath.replace('/span[1]', f'/div/div[{quantity_index+1}]')}") + quantity_name = quantity_ele.text + except: + continue + quantity_ele.click() + tab.wait(2) + price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[1]').text.replace('$', '')) + old_price = float(tab.ele('xpath=//*[@id="__next"]/div[8]/div[1]/div/div[2]/div[2]/div/div[1]/div/span/span[2]/span[1]').text.replace('$', '')) + attr_items.append({ + 'url': tab.url, + 'items': [quantity_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/bricomarche.py b/lib/module/bricomarche.py new file mode 100644 index 0000000..97c616d --- /dev/null +++ b/lib/module/bricomarche.py @@ -0,0 +1,201 @@ +from core import spiders, types +from utils import formats, messages +from DrissionPage.common import Keys + +class SpiderModule(spiders.Spiders): + project_name = "bricomarche" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 5, + worker_num = 1, + switch_clash = False, + reflush_browser = False + ) + self.tab.get('https://www.bricomarche.pl/') + # input('wait:') + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.bricomarche.pl/') + one_menu_eles = self.tab.eles('xpath=//*[@id="root"]/main/div[5]/div[3]/nav/div') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele.ele('@tag()=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/div[1]/div') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + three_menu_eles = two_menu_ele.eles('xpath=ul/li') + for three_menu_ele in three_menu_eles: + three_link_ele = three_menu_ele.ele('@tag()=a') + three_url = three_link_ele.attr('href') + three_name = three_link_ele.text.replace('/', '-') + category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + page = 1 + limit = 100 + self.tab.get(f"{category_url}?page={page}&limit={limit}") + if self.tab.ele('@tag()=h2').text == '执行安全验证': + self.tab.wait(10) + self.tab.actions.type((Keys.TAB, Keys.SPACE)) + goods_urls = [] + + while True: + try: + self.tab.scroll.to_bottom() + goods_eles = self.tab.eles('xpath=//*[@id="spark"]/div[2]/div/section/div/div/main/div[2]/section/div') + for goods_ele in goods_eles: + goods_link_ele = goods_ele.ele('@tag()=a') + goods_urls.append(goods_link_ele.attr('href')) + if len(goods_eles) == 0: + break + except: + break + page += 1 + self.tab.get(f"{category_url}?page={page}&limit={limit}") + if self.tab.ele('@tag()=h2').text == '执行安全验证': + self.tab.wait(10) + self.tab.actions.type((Keys.TAB, Keys.SPACE)) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + + image_eles = tab.eles('xpath=//*[@id="spark"]/div[2]/div/main/div/div[2]/section[1]/div/div[1]/div/div/div/div/div') + for image_ele in image_eles: + image_link_ele = image_ele.ele('xpath=button/div/img') + image_urls.append(image_link_ele.attr('src').replace('gallery_100_91', 'gallery')) + + return image_urls + + tab = self.browser.new_tab(url) + while True: + try: + if tab.ele('@tag()=h2').text == '执行安全验证': + tab.wait(10) + tab.actions.type((Keys.TAB, Keys.SPACE)) + tab.wait(5) + else: + break + except: + break + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = '' + # try: + # brand = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[1]', timeout=10).text + # except: + # pass + + title = tab.ele('@tag()=h1', timeout=10).text + + desc = "" + try: + desc = formats.clean_html(tab.ele('.attributes').html) + except: + pass + + # try: + price_ele = tab.ele('.main-price') + price = float(f"{price_ele.ele('.whole').text}{price_ele.ele('.cents').text.replace(' zł', '')}") + # except: + # price = 0.0 + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + # color_urls = [url] + # color_eles = tab.eles('xpath=//*[@id="contentContainer"]/form/div[2]/div[2]/div/ul/li') + # for color_ele in color_eles[1:]: + # color_link_ele = color_ele.ele('xpath=a') + # color_urls.append(color_link_ele.attr('href')) + # if len(color_urls) > 1: + # m_data = ["Color"] + + # if len(color_urls) > 1: + # for color_url in color_urls: + # color_images = main_images + # if color_url != url: + # tab.get(color_url) + # if brand == '': + # title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1').text + # else: + # title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[2]').text + # color_images = get_image_urls() + + # price = float(f"{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[1]').text}.{tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/section[2]/div[1]/div[2]/span[3]').text}") + # old_price = price + + # try: + # item_name = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[2]/div/ul/li[1]/div[2]/p').text + # except: + # continue + # attr_items.append({ + # 'url': color_url, + # 'items': [item_name], + # 'price': price, + # 'old_price': old_price, + # 'images': color_images + # }) + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + # goods_info.p_urls = color_urls + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/condom69.py b/lib/module/condom69.py new file mode 100644 index 0000000..51a6893 --- /dev/null +++ b/lib/module/condom69.py @@ -0,0 +1,136 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "condom69" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 5, + switch_clash = False, + reflush_browser = False + ) + self.tab.get('https://www.condom69.net/') + # input('wait:') + + def get_category_urls(self) -> list[str]: + category_urls = [] + + one_menu_eles = self.tab.eles('xpath=//*[@id="nav-one"]/li') + for one_menu_ele in one_menu_eles[:-1]: + one_link_ele = one_menu_ele.ele('xpath=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/ul/li', timeout=1) + for two_index, two_menu_ele in enumerate(two_menu_eles): + two_link_ele = two_menu_ele.ele('xpath=div/a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-').replace(',', ',').replace('#', ' ') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + # try: + # three_menu_eles = self.tab.ele(f'xpath=//*[@id="__next"]/div[1]/header/div/div[2]/nav/div[{two_index+1}]/div/div[1]').eles('@tag()=li') + # except: + # continue + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + page = 1 + self.tab.get(f"{category_url}&pg={page}") + + #####获取页数#### + + ################# + + goods_urls = [] + url_index = {} + while True: + self.tab.scroll.to_bottom() + goods_eles = self.tab.eles('.divProdItemImg2Inner') + page_urls = [] + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('xpath=a').attr('href')}" + if goods_url not in url_index: + page_urls.append(goods_url) + url_index[goods_url] = None + if len(page_urls) > 0: + goods_urls += page_urls + else: + break + page += 1 + self.tab.get(f"{category_url}&pg={page}") + + messages.sendInfo(f"{len(goods_urls)} 个") + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + image_eles = tab.eles('xpath=/html/body/form/div[3]/div[2]/div/div[2]/div/div/div[2]/div[2]/div[3]/div/div[2]/div/div/div[1]/div[1]/div[2]/div') + + for image_ele in image_eles: + image_url = image_ele.ele('xpath=div/div/a/img').attr('src') + image_urls.append(image_url) + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + h1_ele = tab.ele('xpath=/html/body/form/div[3]/div[2]/div/div[2]/div/div/div[2]/div[2]/div[3]/div/div[2]/div/div/div[1]/div[2]/div/div[1]') + brand = '' + try: + brand = tab.ele('xpath=/html/body/form/div[3]/div[2]/div/div[2]/div/div/div[2]/div[2]/div[3]/div/div[2]/div/div/div[1]/div[2]/div/div[2]/div[1]/table/tbody/tr/td[3]/a').text + except: + pass + title = h1_ele.text + + desc = "" + try: + desc = formats.clean_html(tab.ele('.ctl00_cphContent_ucUsrIndProduct_pnlIndProdDesc').html) + except: + pass + + price = tab.ele('xpath=/html/body/form/div[3]/div[2]/div/div[2]/div/div/div[2]/div[2]/div[3]/div/div[2]/div/div/div[1]/div[2]/div/div[2]/div[3]/div[1]/div[2]/span').text.replace(',', '') + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/dk_one.py b/lib/module/dk_one.py new file mode 100644 index 0000000..e037a4f --- /dev/null +++ b/lib/module/dk_one.py @@ -0,0 +1,210 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "dk_one" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 1, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.dk_one.com/') + one_menu_eles = self.tab.eles('xpath=/html/body/div[5]/header/div[2]/div[2]/div/div[1]/div[1]/ul/li') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele.ele('@tag()=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + if one_name == '라이프': + continue + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/div/div[1]/ul/li') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + if two_name == '메인': + continue + category_urls.append(f"{two_url}#{one_name}/{two_name}") + two_menu_eles = one_menu_ele.eles('xpath=div/div/div[2]/ul/li') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + if two_name == '메인': + continue + category_urls.append(f"{two_url}#{one_name}/Top Brand/{two_name}") + + # three_menu_eles = two_menu_ele.eles('xpath=ul/li') + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(category_url) + goods_urls = [] + goods_eles = self.tab.eles('xpath=//*[@id="prodList"]/div') + for goods_ele in goods_eles: + goods_link_ele = goods_ele.ele('.prod-link') + goods_urls.append(goods_link_ele.attr('href')) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(tab): + image_urls = [] + image_eles = tab.ele('#prod-img').eles('@tag()=img') + for image_ele in image_eles: + image_urls.append(image_ele.attr('src')) + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = '' + # try: + # brand = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1/div[1]', timeout=10).text + # except: + # pass + # if brand == '': + # title = tab.ele('xpath=//*[@id="contentContainer"]/form/div[2]/div[1]/h1').text + # else: + title = tab.ele('.page-title').text + + try: + desc = formats.clean_html(tab.ele('@@class=tab-content@@id=tabContent0202').html) + except: + desc = '' + + price = float(tab.ele('.price-group').ele('.val').text.replace(',', '')) + try: + old_price = float(tab.ele('.price-group origin').text.replace(',', '')) + except: + old_price = price + + main_images = get_image_urls(tab) + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + images=main_images + ) + + m_data = [] + color_data = {} + color_eles = [] + # 颜色 + tab.ele('.opt-color') + m_data.append('컬러') + color_eles = tab.ele('.color-list-wrap').eles('.prod-color-rdo') + + m_data.append('사이즈') + + for color_ele in color_eles: + color_input_onclick = color_ele.ele('@tag()=input').attr('onclick') + have_url = False + if 'location.href=' in color_input_onclick: + color_url = 'https://dk-on.com' + formats.re_search(color_input_onclick, r"location\.href\s*=\s*'([^']*)'") + color_name = color_url.split('/')[-1] + have_url = True + elif 'fnColorClicked' in color_input_onclick: + color_url = goods_info.url + color_name = color_ele.ele('@tag()=input').attr('value') + color_ele.click() + else: + color_url = goods_info.url + color_name = goods_info.title.split(' / ')[-1] + + color_data[color_name] = {'url': color_url, 'have_url': have_url, 'ele': color_ele} + goods_info.p_urls.append(color_url) + + real_color_data = {} + + + for color_name, color_item in color_data.items(): + color_tab = tab + if color_item['have_url']: + color_tab = self.browser.new_tab() + + size_data = [] + if color_item['have_url']: + color_tab.get(color_item['url']) + else: + color_item['ele'].click() + color_tab.wait(1) + color_item['price'] = str(color_tab.ele('.price-group').ele('.val').text.replace(',', '')) + try: + color_item['old_price'] = str(color_tab.ele('.price-group origin').text.replace(',', '')) + except: + color_item['old_price'] = color_item['price'] + + # 颜色对应商品图 + color_item['images'] = get_image_urls(tab = color_tab) + + # 颜色对应尺码 + radio_eles = color_tab.ele('.prod-size-list').eles('.prod-size') + for radio_ele in radio_eles: + value = radio_ele.ele('@tag()=label').text + if value == None: + continue + size_data.append(value) + color_item['size'] = size_data + + real_color_data[color_tab.ele('.page-title').text.split(' / ')[-1]] = color_item + color_data = real_color_data + + goods_info.attr = 'M' + goods_info.title = goods_info.title.split(' / ')[0] + goods_info.attr_items = m_data + for color_name, color_item in color_data.items(): + for color_image in color_item['images']: + if color_image in goods_info.images: + continue + goods_info.images.append(color_image) + + # if color_data: + item_data = [] + for color_name, color_item in color_data.items(): + for size in color_item['size']: + item_data.append([color_name, size]) + for attribute_items in item_data: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + attr_items=attribute_items, + price=color_data[attribute_items[0]]['price'], + old_price=color_data[attribute_items[0]]['price'], + images=color_data[attribute_items[0]]['images'], + )) + + except Exception as e: + try: + tab.close() + color_tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + color_tab.close() + except: + pass + return goods_info diff --git a/lib/module/example.py b/lib/module/example.py new file mode 100644 index 0000000..e8d6b45 --- /dev/null +++ b/lib/module/example.py @@ -0,0 +1,86 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "example" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 10, + worker_num = 1, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + pass + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}") + goods_urls = [] + + pass + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + pass + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + desc = "简介" + brand = "品牌" + title = "标题" + price = 0.00 + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'] + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/friskybusiness.py b/lib/module/friskybusiness.py new file mode 100644 index 0000000..9a1575c --- /dev/null +++ b/lib/module/friskybusiness.py @@ -0,0 +1,141 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "friskybusiness" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 5, + switch_clash = False, + reflush_browser = False + ) + self.tab.get('https://friskybusiness.sg/') + # input('wait:') + + def get_category_urls(self) -> list[str]: + category_urls = [] + + one_menu_eles = self.tab.eles('xpath=/html/body/header/height-observer/x-header/nav[2]/ul/li') + for one_index, one_menu_ele in enumerate(one_menu_eles[:-1]): + + one_link_ele = one_menu_ele.ele('@tag()=summary') + one_url = one_link_ele.attr('data-follow-link') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + if one_index == 0 or one_index == 6: + two_menu_eles = one_menu_ele.eles('xpath=mega-menu-disclosure/details/div/div/a') + else: + two_menu_eles = one_menu_ele.eles('xpath=mega-menu-disclosure/details/div/ul/li', timeout=1) + for two_index, two_menu_ele in enumerate(two_menu_eles): + if one_index == 0 or one_index == 6: + two_link_ele = two_menu_ele + else: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-').replace(',', ',').replace('#', ' ') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + try: + three_menu_eles = two_menu_ele.eles(f'xpath=div/ul/li') + except: + continue + for three_menu_ele in three_menu_eles: + three_link_ele = three_menu_ele.ele('@tag()=a') + three_url = three_link_ele.attr('href') + three_name = three_link_ele.text.replace('/', '-').replace(',', ',').replace('#', ' ') + category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + page = 1 + self.tab.get(f"{category_url}?page={page}") + goods_urls = [] + + #####获取页数#### + + ################# + + goods_urls = [] + while True: + self.tab.scroll.to_bottom() + try: + goods_eles = self.tab.eles('#:snize-product-', timeout=5) + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + if len(goods_eles) == 0: + break + except: + break + page += 1 + self.tab.get(f"{category_url}?page={page}") + + messages.sendInfo(f"{len(goods_urls)} 个") + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + image_eles = tab.eles('xpath=/html/body/main/section[1]/div/div/product-rerender/div/product-gallery/safe-sticky/product-gallery-navigation/button') + + for image_ele in image_eles[-1:]: + image_url = image_ele.ele('xpath=img').attr('src') + image_urls.append(image_url) + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + h1_ele = tab.ele('@tag()=h1') + brand = '' + title = h1_ele.text + + desc = "" + try: + desc = formats.clean_html(tab.ele('.liquid').html) + except: + pass + + price_list = tab.ele('.price-list price-list--product') + price = price_list.ele('@tag()=sale-price').ele('.money').text.replace('$', '').replace(',', '') + try: + old_price = price_list.ele('@tag()=compare-at-price').ele('.money').text.replace('$', '').replace(',', '') + except: + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/peek_cloppenburg.py b/lib/module/peek_cloppenburg.py new file mode 100644 index 0000000..ca21863 --- /dev/null +++ b/lib/module/peek_cloppenburg.py @@ -0,0 +1,217 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "peek-cloppenburg" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 5, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.peek-cloppenburg.at/') + one_menu_eles = self.tab.ele('@data-testid=header-links').eles('@tag()=a') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + one_link_ele.click() + self.tab.wait(3) + + two_menu_eles = self.tab.eles('xpath=//*[@id="__next"]/div[1]/header/div[2]/div[2]/nav/a', timeout=1) + for two_index, two_menu_ele in enumerate(two_menu_eles): + two_link_ele = two_menu_ele + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + try: + three_menu_eles = self.tab.ele(f'xpath=//*[@id="__next"]/div[1]/header/div[2]/div[2]/nav/div[{two_index+1}]/div/div[1]').eles('@tag()=li') + except: + continue + for three_menu_ele in three_menu_eles: + three_link_ele = three_menu_ele.ele('@tag()=a') + three_url = three_link_ele.attr('href') + three_name = three_link_ele.text.replace('/', '-') + category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}?page=1") + goods_urls = [] + + max_page = 1 + #####获取页数#### + try: + max_page = int(self.tab.eles('@data-testid=pagination-item')[-1].text) + except: + pass + ################# + + goods_urls = [] + if max_page > 1: + for page in range(1, max_page+1): + if page > 1: + self.tab.get(f"{category_url}?page={page}&userId=123456") + + goods_eles = self.tab.eles('xpath=//*[@id="__next"]/div[2]/div/section/div/div[2]/div[5]/div/section[1]/section/div/div') + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + messages.sendInfo(f"{page}/{max_page}") + else: + goods_eles = self.tab.eles('xpath=//*[@id="__next"]/div[2]/div/section/div/div[2]/div[5]/div/section[1]/section/div/div') + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + image_eles = tab.ele('#photoswipe-gallery').eles('@tag()=a') + + for image_ele in image_eles: + image_url = image_ele.attr('href') + image_urls.append(image_url) + return image_urls + + def get_size_eles(): + try: + eles = tab.ele('.option-picker mt-2 ').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 long-label ').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 out-of-stock').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 long-label out-of-stock').eles('@tag()=li') + except: + pass + return eles + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + h1_eles = tab.ele('@tag()=h1', timeout=10).eles('@tag()=span') + brand = '' + try: + brand = h1_eles[0].text + title = h1_eles[1].text + except: + title = tab.ele('@tag()=h1').text + + desc = "" + try: + desc = formats.clean_html(f"{tab.ele('#accordion-panel-material-care').html}{tab.ele('#accordion-panel-details').html}") + except: + pass + + price = tab.ele('.:text-pdp-price-primary-xs font-bold').text.split(' ')[0].replace(',', '.') + try: + old_price = tab.ele('.line-through').text.split(' ')[0].replace(',', '.') + except: + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + color_urls = [] + try: + color_eles = tab.ele('.pt-6 xl:pt-8 -mr-6 md:mr-0').eles('@tag()=li') + m_data.append('Farbe') + for color_ele in color_eles: + color_url = color_ele.ele('@tag()=a').attr('href') + color_urls.append(color_url) + except: + pass + m_data.append('Größe') + + if len(color_urls) > 0: + for color_url in color_urls: + color_images = main_images + if color_url != url: + tab.get(color_url) + color_images = get_image_urls() + + price = tab.ele('.:text-pdp-price-primary-xs font-bold').text.split(' ')[0].replace(',', '') + try: + old_price = tab.ele('.line-through').text.split(' ')[0].replace(',', '') + except: + old_price = price + + color_name = tab.ele('.text-cp-md font-bold').ele('xpath=text()[3]') + + size_eles = get_size_eles() + for size_ele in size_eles: + size_name = size_ele.ele('.text-cp-md truncate').text + + attr_items.append({ + 'url': color_url, + 'items': [color_name, size_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + goods_info.p_urls = color_urls + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/peek_cloppenburg_nl.py b/lib/module/peek_cloppenburg_nl.py new file mode 100644 index 0000000..6be9f3b --- /dev/null +++ b/lib/module/peek_cloppenburg_nl.py @@ -0,0 +1,213 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "peek-cloppenburg-nl" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 5, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.peek-cloppenburg.nl/') + one_menu_eles = self.tab.ele('@data-testid=header-links').eles('@tag()=a') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + one_link_ele.click() + self.tab.wait(3) + + two_menu_eles = self.tab.eles('xpath=//*[@id="__next"]/div[1]/header/div/div[2]/nav/a', timeout=1) + for two_index, two_menu_ele in enumerate(two_menu_eles): + two_link_ele = two_menu_ele + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + try: + three_menu_eles = self.tab.ele(f'xpath=//*[@id="__next"]/div[1]/header/div/div[2]/nav/div[{two_index+1}]/div/div[1]').eles('@tag()=li') + except: + continue + for three_menu_ele in three_menu_eles: + three_link_ele = three_menu_ele.ele('@tag()=a') + three_url = three_link_ele.attr('href') + three_name = three_link_ele.text.replace('/', '-') + category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}?page=1") + goods_urls = [] + + max_page = 1 + #####获取页数#### + try: + max_page = int(self.tab.eles('@data-testid=pagination-item')[-1].text) + except: + pass + ################# + + goods_urls = [] + if max_page > 1: + for page in range(1, max_page+1): + if page > 1: + self.tab.get(f"{category_url}?page={page}&userId=123456") + + goods_eles = self.tab.eles('xpath=//*[@id="__next"]/div[2]/div/section/div/div[2]/div[5]/div/section[1]/section/div/div') + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + messages.sendInfo(f"{page}/{max_page}") + else: + goods_eles = self.tab.eles('xpath=//*[@id="__next"]/div[2]/div/section/div/div[2]/div[5]/div/section[1]/section/div/div') + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + image_eles = tab.ele('#photoswipe-gallery').eles('@tag()=a') + + for image_ele in image_eles: + image_url = image_ele.attr('href') + image_urls.append(image_url) + return image_urls + + def get_size_eles(): + try: + eles = tab.ele('.option-picker mt-2 ').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 long-label ').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 out-of-stock').eles('@tag()=li') + except: + pass + try: + eles = tab.ele('.option-picker mt-2 long-label out-of-stock').eles('@tag()=li') + except: + pass + return eles + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + h1_eles = tab.ele('@tag()=h1').eles('@tag()=span') + brand = h1_eles[0].text + title = h1_eles[1].text + + desc = "" + try: + desc = formats.clean_html(f"{tab.ele('#accordion-panel-material-care').html}{tab.ele('#accordion-panel-details').html}") + except: + pass + + price = tab.ele('.:text-pdp-price-primary-xs font-bold').text.split(' ')[1].replace(',', '.') + try: + old_price = tab.ele('.line-through').text.split(' ')[1].replace(',', '.') + except: + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + color_urls = [] + try: + color_eles = tab.ele('.pt-6 xl:pt-8 -mr-6 md:mr-0').eles('@tag()=li') + m_data.append('Kleur') + for color_ele in color_eles: + color_url = color_ele.ele('@tag()=a').attr('href') + color_urls.append(color_url) + except: + pass + m_data.append('Maat') + + if len(color_urls) > 0: + for color_url in color_urls: + color_images = main_images + if color_url != url: + tab.get(color_url) + color_images = get_image_urls() + + price = tab.ele('.:text-pdp-price-primary-xs font-bold').text.split(' ')[1].replace(',', '.') + try: + old_price = tab.ele('.line-through').text.split(' ')[1].replace(',', '.') + except: + old_price = price + + color_name = tab.ele('.text-cp-md font-bold').ele('xpath=text()[3]') + + size_eles = get_size_eles() + for size_ele in size_eles: + size_name = size_ele.ele('.text-cp-md truncate').text + + attr_items.append({ + 'url': color_url, + 'items': [color_name, size_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + + if len(attr_items) == 0: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + goods_info.p_urls = color_urls + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/ssfshop.py b/lib/module/ssfshop.py new file mode 100644 index 0000000..7dd4674 --- /dev/null +++ b/lib/module/ssfshop.py @@ -0,0 +1,371 @@ +from core import spiders, types +from utils import formats, messages, files +import requests + +class SpiderModule(spiders.Spiders): + project_name = "ssfshop" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 300, + worker_num = 24, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.ssfshop.com/') + one_menu_eles = self.tab.eles('xpath=/html/body/div[5]/header/div[2]/div[2]/div/div[1]/div[1]/ul/li') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele.ele('@tag()=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + if one_name == '라이프': + continue + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/div/div[1]/ul/li') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + if two_name == '메인': + continue + category_urls.append(f"{two_url}#{one_name}/{two_name}") + two_menu_eles = one_menu_ele.eles('xpath=div/div/div[2]/ul/li') + for two_menu_ele in two_menu_eles: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + if two_name == '메인': + continue + category_urls.append(f"{two_url}#{one_name}/Top Brand/{two_name}") + + # three_menu_eles = two_menu_ele.eles('xpath=ul/li') + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + page = 1 + self.tab.get(f"{category_url}¤tPage={page}&sortColumn=SALE_QTY_SEQ&serviceType=DSP&ctgrySectCd=GNRL_CTGRY&fitPsbYn=N") + goods_urls = [] + + url_index = {} + while True: + self.tab.scroll.to_bottom() + page_urls = [] + goods_eles = self.tab.eles('.god-item') + for goods_ele in goods_eles: + goods_link_ele = goods_ele.ele('xpath=a') + if goods_link_ele.attr('href') in url_index: + continue + page_urls.append(goods_link_ele.attr('href')) + url_index[goods_link_ele.attr('href')] = None + + if len(page_urls) == 0: + break + if len(goods_urls) > 5000: + break + goods_urls += page_urls + page += 1 + self.tab.get(f"{category_url}¤tPage={page}&sortColumn=SALE_QTY_SEQ&serviceType=DSP&ctgrySectCd=GNRL_CTGRY&fitPsbYn=N") + self.tab.wait(1) + + return goods_urls + + def get_goods_info(self, url: str) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + + for _ in range(10): + image_eles = tab.eles('xpath=//*[@id="godImgThumb"]/div') + if len(image_eles) > 0: + break + tab.wait(3) + for image_ele in image_eles: + image_link_ele = image_ele + image_urls.append('https://img.ssfshop.com'+image_link_ele.attr('data')) + + return image_urls + + def get_options(godNo, optValCd1, optValCd2 = ''): + headers = { + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Origin': 'https://www.ssfshop.com', + 'Referer': url, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0', + 'X-CSRF-TOKEN': 'e8e4bcd2-b976-4ca3-89b0-c85de0d1f3b0', + 'X-Requested-With': 'XMLHttpRequest', + 'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + # 'Cookie': 'PCID=17748347492661999807244; UTID=17748347492661999807244; MAI="eyJsb2dpbiI6Ik4ifQ=="; WMONID=mUUUMLseCyQ; AWSALBAPP-1=_remove_; AWSALBAPP-2=_remove_; AWSALBAPP-3=_remove_; visid_incap_2173964=lpNXES71Q5iAc3f2rzl/jTzUyWkAAAAAQUIPAAAAAABeBQZ8XijRe/SDlNUYlcCm; nlbi_2173964_3216183=TbQ1Ku6HAX5AN79F2g4AdQAAAADFBJVdMHKyRlRjq5BE8+sZ; storedUrl=Y; visid_incap_2794548=/x/uA+w/Rna/+lXitrmX20DUyWkAAAAAQUIPAAAAAACOgO5Lt68U4l70FJ35r3uf; incap_ses_725_2173964=A1WxV+agRSxFFqDq6bcPCtPnyWkAAAAAl1pNcoghbBUv0Idr250rQA==; visid_incap_2898463=WYviBGzFSwWHcJFJUGirBdXnyWkAAAAAQUIPAAAAAAAtfVuBlsqIe97ZjwN6QI2j; nlbi_2898463=wXQSYet0xyoFwAnd4MbCSgAAAAArzXmJlyqxxSh3+i0S9GbO; incap_ses_725_2898463=s8pzSCxlllS0GaDq6bcPCtXnyWkAAAAAu3cfqwOGa86YepyhOT6TvQ==; nth_pcid=3d5ffbba-60dd-a49a-6747-a542beef7538-1774839765504; incap_ses_725_2794548=D/rjK2Wq+zSjGqDq6bcPCtXnyWkAAAAAwfdIptRC0JfZMv8gf0dFHw==; incap_ses_724_2173964=dLR3AfGiVBRg7b2bbSoMCuEsymkAAAAAo4viBM5iG3asWiau6QX32w==; incap_ses_724_2898463=/V2oJwzj6BLV8L2bbSoMCuMsymkAAAAA9Ybn1njwVQ3r7KT6QLCZcg==; incap_ses_724_2794548=3i9aHRkHkTdh872bbSoMCugsymkAAAAAt9AH6HTki4ssrvCcPmSpIA==; AWSALBTG=AIb2slX0AEp+Mv3KJalsspDk7579UU9Jv2/3kjqEGNWjsIrDM+27+/1Ke7bKfPJDQPkvjZ2Sxz50ZoGgs7wax+6xPQZc22NzAPVhfCwSibc44xWGCNEUT9ixCDWhVrmeHJTF4AGiXACsmVs+73qXWgynz4bNZxFvwGcomdcZ09QX; AWSALB=1Lc2nZgyDqlHzKH2Ht9zUCbVzO3nmyjbvR6L8TbjmEzgIJXLWPHd2TKRrA4NweRQ4J3aqEC0O7/FVTTgdHmYYm6M1K3NeLkZbyrFTXyTGk7GFDh5nYLYl/aBRLKe; AWSALBCORS=1Lc2nZgyDqlHzKH2Ht9zUCbVzO3nmyjbvR6L8TbjmEzgIJXLWPHd2TKRrA4NweRQ4J3aqEC0O7/FVTTgdHmYYm6M1K3NeLkZbyrFTXyTGk7GFDh5nYLYl/aBRLKe; AWSALBAPP-0=AAAAAAAAAACeq+TZG7CFWkFz6UEyoFzfgzCso6p2one/fqTkDd1D8aJwmBMDcXt1z/4SFXLjp7cnG/VXtUeFI5VsG2vzUNb7CeolT4ZG4OzG2MYZTDISyFXF0Ftu3Y5EnlA+ClLy5QvoVKs=; nlbi_2173964_2029297=AebNbzFaqQMN0dyi2g4AdQAAAADw44LnFE5aKd0nWF3BiMTo; STID=1774941024138741600844420260331161024; INFLOW_SN=99993; N_INFLOW_SN=Y; PC_JSESSIONID=054FCD4B9E7ABB3B015CF732C022A41B; incap_ses_138_2173964=rIZHCDf5DCYIKM99lkbqAV9zy2kAAAAAhshd7O+yiHEnrcRAybEDaw==; incap_ses_138_2898463=zRn2E3KmY3qUMM99lkbqAWBzy2kAAAAAT0zfogyh0Cs4exDxqH0C5w==; nlbi_2794548=gojpVZcccHOhC/svvoS1vwAAAAB8htUpn549dG0/xu4xckFq; incap_ses_138_2794548=A1yKCZEQdFLyMc99lkbqAWBzy2kAAAAAcNViNS39K56YSoYGpK3p0A==; _TODAYALLLIST="[{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GM0026011527285\\",\\"regDt\\":\\"1774840170633\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GQ3A26021060165\\",\\"regDt\\":\\"1774920131600\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GM0025122296270\\",\\"regDt\\":\\"1774920840210\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GQJ924040149659\\",\\"regDt\\":\\"1774921134062\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GR4H25042395931\\",\\"regDt\\":\\"1774921515277\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GQZG26012945089\\",\\"regDt\\":\\"1774921868451\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GQZG26012945090\\",\\"regDt\\":\\"1774921870125\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GQZG26012945092\\",\\"regDt\\":\\"1774921872189\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GRRI26030602949\\",\\"regDt\\":\\"1774941024251\\"},{\\"todayGodSectCd\\":\\"GOD\\",\\"godNo\\":\\"GRRI26030602950\\",\\"regDt\\":1774941046056}]"; godClickInfo=GQY026032636412||GM0026011527285||GQ3A26021060165||GM0025122296270||GR4H25042395931||GR4H25042395931||GQJ924040149659||GR4H25042395931||GR4H25042395931||GQZG26012945092||GQZG26012945089||GQZG26012945090||GQZG26012945092||GQZG26012945089||GQZG26012945090||GQZG26012945092||GRRI26030602949||GRRI26030602950', + } + + data = { + 'optValCd1': optValCd1, + 'optValCd2': optValCd2, + 'optValCd3': '', + 'godNo': godNo, + } + + response = requests.post('https://www.ssfshop.com/public/goods/selectGodDetailOpt', headers=headers, data=data, proxies={'http': 'http://127.0.0.1:7890','https': 'http://127.0.0.1:7890'}) + json_data = response.json() + + options = {} + if 'opt' not in json_data: + return options + + for option in json_data['opt']: + for i in range(1, 6): + key = f"optValNm{i}" + if option[key]: + options[option[key]] = {} + break + return options + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = url.split('/')[3] + godNo = url.split('/')[4] + + title = tab.ele('#goodDtlTitle').text + + desc = "" + try: + desc = tab.ele('#godsTabView').html + except: + pass + + price = float(tab.ele('xpath=//*[@id="content"]/section/div[2]/div[2]/div[3]/span').ele('.price').text.replace(',', '')) + try: + old_price = float(tab.ele('xpath=//*[@id="content"]/section/div[2]/div[2]/div[3]/span/span[1]/del').text.replace(',', '')) + except: + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + color_urls = [] + try: + tab.ele('.gods-option').ele('text=색상').click() + color_eles = tab.eles('xpath=//*[@id="content"]/section/div[2]/div[2]/div[6]/div[1]/div/span/label') + for color_ele in color_eles: + color_url = color_ele.attr('onclick') + if color_url: + color_url = color_url.split("'")[1] + else: + color_url = url + if 'https://' not in color_url: + color_url = 'https://www.ssfshop.com'+color_url + color_urls.append(color_url) + except: + pass + + if len(color_urls) > 1: + m_data = ["색상"] + + try: + option_eles = tab.ele('#prdInfoOptionArea').eles('#:optSelectPartmalDiv') + except: + option_eles = [] + for option_ele in option_eles: + m_data.append(option_ele.ele('@tag()=input').attr('opt-nm')) + m_data = m_data[:3] + + option_data = {} + if len(option_eles) > 0: + try: + option_eles[0].click() + except: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + try: + tab.close() + except: + pass + return goods_info + tab.wait(0.5) + option1_eles = option_eles[0].eles('@tag()=li') + for option1_ele in option1_eles: + option_data[option1_ele.text] = get_options(godNo, option1_ele.text) + for option2_name, items in option_data[option1_ele.text].items(): + option_data[option1_ele.text][option2_name] = get_options(godNo, option1_ele.text, option2_name) + + if len(color_urls) > 1: + for color_url in color_urls: + color_images = main_images + if color_url != url: + tab.get(color_url) + title = tab.ele('#goodDtlTitle').text + color_images = get_image_urls() + + godNo = color_url.split('/')[4] + try: + option_eles = tab.ele('#prdInfoOptionArea', timeout=5).eles('.select lg unapplied') + except: + option_eles = [] + option_data = {} + if len(option_eles) > 0: + option1_eles = option_eles[0].eles('xpath=div/ul/li') + for option1_ele in option1_eles: + option_data[option1_ele.text] = get_options(godNo, option1_ele.text) + for option2_name, items in option_data[option1_ele.text].items(): + option_data[option1_ele.text][option2_name] = get_options(godNo, option1_ele.text, option2_name) + + try: + color_name = title.split('-')[1] + except: + color_name = tab.ele('.buy-txts').text + + price = float(tab.ele('xpath=//*[@id="content"]/section/div[2]/div[2]/div[3]/span').ele('.price').text.replace(',', '')) + try: + old_price = float(tab.ele('xpath=//*[@id="content"]/section/div[2]/div[2]/div[3]/span/span[1]/del').text.replace(',', '')) + except: + old_price = price + + if len(option_data) > 0: + for option1_name, items1 in option_data.items(): + if len(items1) > 0: + for option2_name, items2 in items1.items(): + attr_items.append({ + 'url': color_url, + 'items': [color_name, option1_name, option2_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + attr_items.append({ + 'url': color_url, + 'items': [color_name, option1_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + attr_items.append({ + 'url': color_url, + 'items': [color_name], + 'price': price, + 'old_price': old_price, + 'images': color_images + }) + else: + for option1_name, items1 in option_data.items(): + if len(items1) > 0: + for option2_name, items2 in items1.items(): + if len(items2) > 0: + for option3_name, items3 in items2.items(): + attr_items.append({ + 'url': url, + 'items': [option1_name, option2_name, option3_name], + 'price': price, + 'old_price': old_price, + 'images': [] + }) + else: + attr_items.append({ + 'url': url, + 'items': [option1_name, option2_name], + 'price': price, + 'old_price': old_price, + 'images': [] + }) + else: + attr_items.append({ + 'url': url, + 'items': [option1_name], + 'price': price, + 'old_price': old_price, + 'images': [] + }) + + if len(attr_items) == 0: + try: + tab.ele('.gods-option').ele('text=사이즈').click() + size_eles = tab.eles('xpath=/html/body/div[5]/main/section/div[2]/div[2]/div[6]/div[1]/div/ul/li') + goods_info.attr = 'M' + goods_info.attr_items = ['사이즈'] + goods_info.images = main_images + for size_ele in size_eles: + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + attr_items=[size_ele.text], + price=price, + old_price=old_price, + images=[], + )) + except: + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + else: + goods_info.attr = 'M' + goods_info.attr_items = m_data + for attr_item in attr_items: + if len(attr_item['items']) != len(m_data): + attr_item['items'] += m_data[len(attr_item['items']):] + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + url=attr_item['url'], + attr_items=attr_item['items'], + price=attr_item['price'], + old_price=attr_item['old_price'], + images=attr_item['images'], + )) + for image_url in attr_item['images']: + if image_url not in main_images: + main_images.append(image_url) + goods_info.images = main_images + goods_info.p_urls = color_urls + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info + + def get_home(self): + self.tab.get('https://www.ssfshop.com/main') + li_eles = self.tab.ele('xpath=/html/body/div[5]/main/div[2]/section[2]/div/div[1]').eles('@tag()=li') + categorys = [] + for li_ele in li_eles: + a_ele = li_ele.ele('@tag()=a') + categorys.append(f"{a_ele.attr('href')}#{a_ele.text}") + files.save_list(f"{self.__data_path__}/categorys(home).txt", categorys) diff --git a/lib/module/taketoys.py b/lib/module/taketoys.py new file mode 100644 index 0000000..286744e --- /dev/null +++ b/lib/module/taketoys.py @@ -0,0 +1,131 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "taketoys" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 30, + worker_num = 5, + switch_clash = False, + reflush_browser = False + ) + self.tab.get('https://taketoys.sg/') + # input('wait:') + + def get_category_urls(self) -> list[str]: + category_urls = [] + + one_menu_eles = self.tab.eles('xpath=/html/body/header/div[2]/div/nav/ul/li') + for one_menu_ele in one_menu_eles[1:-1]: + one_link_ele = one_menu_ele.ele('xpath=a') + one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=ul/li', timeout=1) + for two_index, two_menu_ele in enumerate(two_menu_eles): + two_link_ele = two_menu_ele.ele('xpath=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-').replace(',', ',').replace('#', ' ') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + # try: + # three_menu_eles = self.tab.ele(f'xpath=//*[@id="__next"]/div[1]/header/div/div[2]/nav/div[{two_index+1}]/div/div[1]').eles('@tag()=li') + # except: + # continue + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + self.tab.get(f"{category_url}?page=1") + goods_urls = [] + + #####获取页数#### + + ################# + + goods_urls = [] + while True: + self.tab.scroll.to_bottom() + try: + self.tab.ele('text=Show more', timeout=5).click() + self.tab.wait(1) + except: + break + + goods_eles = self.tab.eles('.col-xs-6 col-sm-4 col-lg-3 item-wrapper') + for goods_ele in goods_eles: + goods_url = f"{goods_ele.ele('@tag()=a').attr('href')}" + goods_urls.append(goods_url) + messages.sendInfo(f"{len(goods_urls)} 个") + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + image_eles = tab.eles('xpath=/html/body/main/div[2]/div[2]/div[2]/div[2]/div/div/div[3]/div[2]/div/div') + + for image_ele in image_eles: + image_url = image_ele.ele('xpath=img').attr('src') + image_urls.append(image_url) + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + h1_ele = tab.ele('@tag()=h1') + brand = '' + try: + brand = h1_ele.ele('@tag()=strong').text + except: + pass + title = h1_ele.ele('@tag()=span').text + + desc = "" + try: + desc = formats.clean_html(tab.ele('#product-info-tab').html) + except: + pass + + price = tab.ele('.price').attr('title').replace('SGD', '').replace(',', '') + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + goods_info.attr = 'S' + goods_info.attr_items = [] + goods_info.images = main_images + goods_info.p_lists = [] + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/module/tfashion.py b/lib/module/tfashion.py new file mode 100644 index 0000000..31e668c --- /dev/null +++ b/lib/module/tfashion.py @@ -0,0 +1,140 @@ +from core import spiders, types +from utils import formats, messages + +class SpiderModule(spiders.Spiders): + project_name = "t-fashion" + + def __init__(self): + super().__init__( + self.project_name, + bitch = 300, + worker_num = 24, + switch_clash = False, + reflush_browser = False + ) + + def get_category_urls(self) -> list[str]: + category_urls = [] + + self.tab.get('https://www.t-fashion.jp/') + one_menu_eles = self.tab.ele('.m-aside-menu-body').eles('.js-collest-li') + for one_menu_ele in one_menu_eles: + one_link_ele = one_menu_ele.ele('@tag()=a') + # one_url = one_link_ele.attr('href') + one_name = one_link_ele.text.replace('/', '-') + # category_urls.append(f"{one_url}#{one_name}") + messages.sendInfo(one_name) + + two_menu_eles = one_menu_ele.eles('xpath=div/ul/li') + for two_menu_ele in two_menu_eles[1:]: + two_link_ele = two_menu_ele.ele('@tag()=a') + two_url = two_link_ele.attr('href') + two_name = two_link_ele.text.replace('/', '-') + category_urls.append(f"{two_url}#{one_name}/{two_name}") + + # three_menu_eles = two_menu_ele.eles('xpath=ul/li') + # for three_menu_ele in three_menu_eles: + # three_link_ele = three_menu_ele.ele('@tag()=a') + # three_url = three_link_ele.attr('href') + # three_name = three_link_ele.text.replace('/', '-') + # category_urls.append(f"{three_url}#{one_name}/{two_name}/{three_name}") + + return category_urls + + def get_goods_urls(self, category_url: str) -> list[str]: + page = 1 + self.tab.get(f"{category_url}?page={page}") + goods_urls = [] + + while True: + try: + goods_eles = self.tab.eles('xpath=/html/body/div[1]/div[3]/div/section[1]/div/div[1]/div[1]/div[3]/ol/li') + for goods_ele in goods_eles: + goods_link_ele = goods_ele.ele('xpath=a') + goods_urls.append(goods_link_ele.attr('href')) + if len(goods_eles) == 0: + break + except: + break + page += 1 + self.tab.get(f"{category_url}?page={page}") + + return goods_urls + + def get_goods_info(self, url) -> types.GoodsInfo: + def get_image_urls(): + image_urls = [] + + image_eles = tab.eles('xpath=//*[@id="item-gallery-parent"]/li') + for image_ele in image_eles: + image_link_ele = image_ele.ele('xpath=div/img') + image_urls.append(image_link_ele.attr('src')) + + return image_urls + + tab = self.browser.new_tab(url) + tab.set.window.max() + spu = formats.url_to_spu(url) + + try: + brand = '' + try: + brand = tab.ele('xpath=//*[@id="current-tgoods-section"]/div/div[2]/div/div[1]/a[2]', timeout=10).text + except: + pass + + title = tab.ele('@tag()=h1').text + + desc = "" + try: + desc = formats.clean_html(tab.ele('xpath=//*[@id="slide-down-contents-parent"]')) + except: + pass + + try: + price = float(tab.ele('.is-fz28-36f is-fw-b ').text.replace(',', '').replace('¥', '').replace('(税込)', '')) + except: + price = float(tab.ele('.is-fz28-36f is-fw-b is-color-notice').text.replace(',', '').replace('¥', '').replace('(税込)', '')) + old_price = price + + main_images = get_image_urls() + + goods_info = types.GoodsInfo( + title=title, + desc=desc, + brand=brand, + url=url, + spu=spu, + price=price, + old_price=old_price, + ) + + m_data = [] + attr_items = [] + goods_info.attr = 'M' + goods_info.attr_items = ['Color', 'Size'] + goods_info.images = main_images + color_eles = tab.eles('xpath=//*[@id="sku-list-box"]/div') + for color_ele in color_eles: + color_image = color_ele.ele('xpath=div/a').attr('href') + color_name = color_ele.ele('xpath=div/a/span').text + size_eles = color_ele.eles('xpath=ol/li') + for size_ele in size_eles: + size_name = size_ele.ele('xpath=div[1]/text()') + goods_info.p_lists.append(types.GoodsInfo.GoodsInfoP( + attr_items=[color_name, size_name], + price=price, + old_price=old_price, + images=[color_image] + )) + except Exception as e: + try: + tab.close() + except: + pass + raise Exception(e) + try: + tab.close() + except: + pass + return goods_info diff --git a/lib/shopyy.py b/lib/shopyy.py new file mode 100644 index 0000000..a44c802 --- /dev/null +++ b/lib/shopyy.py @@ -0,0 +1,371 @@ +import requests +import lib +from core import excels, browsers +from utils import files, formats + +class Shopyy: + def __init__(self, save_folder): + self.save_folder = save_folder + + self.__album_template_path__ = 'sources/专辑模板.xlsx' + self.__navigation_template_path__ = 'sources/导航模板.xlsx' + + # 导航 + def generate_navigation(self, category_path): + index = {} + category_urls = files.load_lines(category_path) + + WorkBook = excels.WorkBook( + template=self.__navigation_template_path__, + save_path=f"{self.save_folder}/导航表格.xlsx" + ) + for category_url in category_urls: + category = category_url.split('#')[-1] + if category in index: + continue + category_lists = category.split('/') + category_lists += ['', ''] + row_data = [ + '顶部导航菜单', + category_lists[0], + category_lists[1], + category_lists[2], + 'collection', + category + ] + WorkBook.add_row_of_list(row_data) + index[category] = None + WorkBook.save() + + return self + + # 专辑 + def generate_album(self, goods_url_path): + def open_workbook(bitch_index) -> excels.WorkBook: + WorkBook = excels.WorkBook( + template=self.__album_template_path__, + save_path=f"{self.save_folder}/专辑表格({bitch_index}).xlsx" + ) + return WorkBook + + album_data = {} + count = 0 + bitch_index = 1 + WorkBook = open_workbook(bitch_index) + + goods_urls = files.load_lines(goods_url_path) + for goods_url in goods_urls: + if goods_url == '': + continue + goods_url_split = goods_url.split('#') + goods_categorys = goods_url_split[-1].split('/') + goods_url = '#'.join(goods_url_split[:-1]) + goods_url = goods_url.split('?')[0] + + for category_index, goods_category in enumerate(goods_categorys): + goods_category = '/'.join(goods_categorys[:category_index + 1]) + + if goods_category in album_data: + mode = '' + sort = '' + else: + mode = 'add' + sort = 'collections_position' + album_data[goods_category] = [] + + row_data = [ + '', + goods_category, + mode, + sort, + '', + formats.url_to_spu(goods_url) + ] + album_data[goods_category].append(row_data) + count += 1 + + save_bitch = count/3+1 + for goods_category, row_datas in album_data.items(): + for row_data in row_datas: + WorkBook.add_row_of_list(row_data) + + if WorkBook.start_row > save_bitch and bitch_index <= 2: + WorkBook.save() + WorkBook.close() + + bitch_index += 1 + WorkBook = open_workbook(bitch_index) + WorkBook.save() + WorkBook.close() + + return self + +class ShopyyVerify: + def __init__(self, url): + self.url = url + browser = browsers.getBrowsers() + self.tab = browser.get_tab(0) + self.login() + + def login(self): + self.tab.get(f"{self.url}/admin/login") + print('等待登录') + while True: + token = self.tab.local_storage('CARDADMIN_TOKEN') + if token: + print('登录成功') + self.token = token.replace('"', '') + break + self.tab.wait(1) + + def get_zero_collections(self, delete: bool = False): + def get_collections(page): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'priority': 'u=1, i', + # 'referer': 'https://cisalfasport.zenshop.cn/admin/collections?page=1&pagesize=50', + 'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0', + # 'cookie': '__stripe_mid=89b6ec3f-bbac-483d-b8ab-9938859a0b340b72d8; _aiihbe_vid=18FC7E3E-1CA7-FDC2-C52D-37A4BB0E1DF0; _aiihbe_cvid=75CFD325-E408-2C25-C126-07A349ED46C5; utm_source=direct; utm_medium=default; order_utm_history=%5B%7B%22utm_source%22%3A%22direct%22%2C%22utm_medium%22%3A%22default%22%2C%22utm_term%22%3A%22%22%2C%22utm_campaign%22%3A%22%22%2C%22utm_content%22%3A%22%22%2C%22source_device%22%3A%22computer%22%2C%22create_time%22%3A1770185710%2C%22expire_time%22%3A1772777710%7D%5D; landing_page=aHR0cHM6Ly9jaXNhbGZhc3BvcnQuemVuc2hvcC5jbi9wcm9kdWN0cy90b3VyLWRlLWZyYW5jZS1qcj9wcmV2aWV3PTE%3D; first_http_referer=https%3A%2F%2Fcisalfasport.zenshop.cn%2Fadmin%2Fproducts; first_visit_time=1770185710; PHPSESSID=6329e9a44018771ce40064bc50fbe189; _aiihbe_vs=36B35E5C-874E-4D6C-8945-F5CF6F323F99; theme_id=411278; _AIIHBE_ua=Mozilla%252F5.0%2520(Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64)%2520AppleWebKit%252F537.36%2520(KHTML%252C%2520like%2520Gecko)%2520Chrome%252F144.0.0.0%2520Safari%252F537.36%2520Edg%252F144.0.0.0; _AIIHBE_pu=https%253A%252F%252Fcisalfasport.zenshop.cn%252Fadmin%252Ftheme; _AIIHBE_tz=Etc%2FGMT-8; _AIIHBE_ss=1920X1080; _AIIHBE_lang=zh-CN; _AIIHBE_vs=1874X930; _AIIHBE_dt=2026-02-06%2016%3A30%3A44; acw_tc=a3b54ee217704477374193466e23fb58e387e54f5b84504527a4beab6b; cdn_sec_tc=a3b54ee217704477374193466e23fb58e387e54f5b84504527a4beab6b; __stripe_sid=49bcd8c1-4ae4-46ba-8e95-a29205606cb2980c18; visit_token=4be69765c90de228978e9738878367fbkj083fsy; _aiihbe_ka=A298A3E1-379E-4DA0-2B28-A5961B741FA9; is_theme_edit=1', + } + + params = { + 'page': str(page), + 'pagesize': '600', + } + + response = requests.get(f'{self.url}/api/collections', params=params, headers=headers) + response_data = response.json() + if response_data['code'] != 0: + raise Exception(response_data['msg']) + return response_data['data'] + + def delete_collections(ids: list): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'content-type': 'application/json', + 'origin': 'https://peekcloppenburgat.zenshop.cn', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + # 'referer': 'https://peekcloppenburgat.zenshop.cn/admin/collections?page=1&pagesize=600', + 'sec-ch-ua': '"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0', + # 'cookie': '__stripe_mid=49d8f368-8d82-4b14-9c42-63e0d33542ef6372ca; _bzcece_vid=E823CDA9-7EDE-3617-71BB-80F879AD878E; _bzcece_cvid=36F627EC-095F-9323-5047-43325149C808; utm_source=direct; utm_medium=default; order_utm_history=%5B%7B%22utm_source%22%3A%22direct%22%2C%22utm_medium%22%3A%22default%22%2C%22utm_term%22%3A%22%22%2C%22utm_campaign%22%3A%22%22%2C%22utm_content%22%3A%22%22%2C%22source_device%22%3A%22computer%22%2C%22create_time%22%3A1773370701%2C%22expire_time%22%3A1775962701%7D%5D; landing_page=aHR0cHM6Ly9wZWVrY2xvcHBlbmJ1cmdhdC56ZW5zaG9wLmNuL3Byb2R1Y3RzL3Zlcm8tbW9kYS13aWRlLWxlZy1qZWFucy1hdXMtYmF1bXdvbGwtbWl4LW1vZGVsbC10ZXNzYS1pbi1oZWxsZ3JhdT9wcmV2aWV3PTE%3D; first_http_referer=https%3A%2F%2Fpeekcloppenburgat.zenshop.cn%2Fadmin%2Fproducts; first_visit_time=1773370701; PHPSESSID=fba65fe62a66071f174801bb432b942c; theme_id=418468; __stripe_sid=323c1341-4108-4c33-b331-ea6b782f9a2349679f; visit_token=f1d5170d234867f2ca490821f454239bmwnlrqr7; _bzcece_vs=5F572333-803E-D038-11EC-6CFCBA6F32D3; _bzcece_ka=B2503B21-4B44-A5A0-F261-71EC86D52A6E; is_theme_edit=1; acw_tc=9b66b49a17734819180038754ede11478ee763b95bb1c9acff317f521f; cdn_sec_tc=9b66b49a17734819180038754ede11478ee763b95bb1c9acff317f521f', + } + + json_data = { + 'ids': ids, + } + + response = requests.post( + f'{self.url}/api/collections/batchdelete', + headers=headers, + json=json_data, + ) + print(response.json()) + + print('开始检查专辑数量') + collection_items = [] + data = get_collections(1) + page_total = data['paginate']['pageTotal'] + collection_items += data['collections'] + print(f"1/{page_total}") + + if page_total > 1: + for page in range(2, page_total+1): + data = get_collections(page) + collection_items += data['collections'] + print(f"{page}/{page_total}") + + lost_collection = [] + zero_ids = [] + for collection_item in collection_items: + if collection_item['collection_products_count'] == 0: + lost_collection.append(collection_item['title']) + print(f"{collection_item['title']} 数量为0") + zero_ids.append(collection_item['id']) + if delete: + delete_collections(zero_ids) + + if len(lost_collection) > 0: + with open('商品数量为0专辑.txt', 'w', encoding='utf-8') as f: + f.write('\n'.join(lost_collection)) + return self + + def get_error_images(self, delete: bool = False): + def get_errors(page): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'dnt': '1', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + # 'referer': 'https://vanuffelenmode.zenshop.cn/admin/resources', + 'sec-ch-ua': '"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0', + # 'cookie': 'acw_tc=a3b54dab17734824723112557e921e5eb8672de1b4bf2e0bfb7de57fe1; cdn_sec_tc=a3b54dab17734824723112557e921e5eb8672de1b4bf2e0bfb7de57fe1; __stripe_mid=cc09d025-360b-466e-bef4-ba92d045fdc94448e2; __stripe_sid=511d0972-d39d-4aa1-a031-5258db651ad1d4124e; visit_token=9b8172c220d5be292045835470d38951px0lh404', + } + + params = { + 'file_name': '', + 'page_type': 'fail', + 'page': str(page), + 'pagesize': '600', + } + + response = requests.get(f'{self.url}/api/resources', params=params, headers=headers) + response_data = response.json() + if response_data['code'] != 0: + raise Exception(response_data['msg']) + return response_data['data'] + + def post_retry(ids: list): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/json', + # 'origin': 'https://ssfshop.zenshop.cn', + 'priority': 'u=1, i', + # 'referer': 'https://ssfshop.zenshop.cn/admin/resources', + 'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0', + # 'cookie': '__stripe_mid=a8059a05-4d7e-476d-b376-9e54f222efbe3c23c7; _bzgigi_vid=85F94DA2-7BD9-525E-E4A5-8D686E51B2C6; _bzgigi_cvid=EEFE28A2-FDC4-DC54-E76A-2BC1B2929243; utm_source=direct; utm_medium=default; order_utm_history=%5B%7B%22utm_source%22%3A%22direct%22%2C%22utm_medium%22%3A%22default%22%2C%22utm_term%22%3A%22%22%2C%22utm_campaign%22%3A%22%22%2C%22utm_content%22%3A%22%22%2C%22source_device%22%3A%22computer%22%2C%22create_time%22%3A1775195861%2C%22expire_time%22%3A1777787861%7D%5D; landing_page=aHR0cHM6Ly9zc2ZzaG9wLnplbnNob3AuY24vcHJvZHVjdHMvJUU4JTg3JUFBJUU3JTk0JUIxJUU1JThDJUJBLWwtMTklRTUlOEYlQjclRTMlODIlQjUlRTMlODIlQTQlRTMlODIlQkElRTMlODElQkUlRTMlODElQTclRTUlQjElOTUlRTklOTYlOEItJUUzJTgyJUFCJUUzJTgyJUJGJUUzJTgzJUFEJUUzJTgyJUIwJUU2JThFJUIyJUU4JUJDJTg5LSVFMyU4MiVCQiVFMyU4MyU4MyVFMyU4MyU4OCVFMyU4MiVBMiVFMyU4MyU4MyVFMyU4MyU5NyVFNSVBRiVCRSVFNSVCRiU5Qy0lRTMlODIlQkIlRTMlODMlQUMlRTMlODMlQTIlRTMlODMlOEIlRTMlODMlQkMtJUU5JTgwJTlBJUU1JThCJUE0LSVFMyU4MyU4OCVFMyU4MyVBQSVFMyU4MiVBMiVFMyU4MiVCQiVFMyU4MyU4MCVFMyU4MyU5NiVFMyU4MyVBQiVFMyU4MiVBRiVFMyU4MyVBRCVFMyU4MiVCOSVFMyU4MyU4NiVFMyU4MyVCQyVFMyU4MyVBOSVFMyU4MyVCQyVFMyU4MyU4OS0lRTMlODIlQjglRTMlODMlQTMlRTMlODIlQjElRTMlODMlODMlRTMlODMlODhfOGNhZjQ4Y2U%2FcHJldmlldz0x; first_http_referer=https%3A%2F%2Fssfshop.zenshop.cn%2Fadmin%2Fproducts; first_visit_time=1775195861; visit_token=41be25876fa245bb99018d68f7338d89y49l7utp; _bzgigi_vs=E5BE5924-EEF9-E277-6AFC-5F70BB548D0F; PHPSESSID=b08ff5f29b9469e1e92740efef87c09e; _BZGIGI_ua=Mozilla%252F5.0%2520(Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64)%2520AppleWebKit%252F537.36%2520(KHTML%252C%2520like%2520Gecko)%2520Chrome%252F146.0.0.0%2520Safari%252F537.36%2520Edg%252F146.0.0.0; _BZGIGI_pu=https%253A%252F%252Fssfshop.zenshop.cn%252Fadmin%252Fproducts; _BZGIGI_tz=Etc%2FGMT-8; _BZGIGI_ss=2560X1440; _BZGIGI_lang=zh-CN; _BZGIGI_vs=2514X1316; _BZGIGI_dt=2026-04-07%2014%3A48%3A20; theme_id=429104; is_theme_edit=1; acw_tc=9b66b4a617756120379694001e5c7efed3faa08e9cf1c621bbfeb27c77; cdn_sec_tc=9b66b4a617756120379694001e5c7efed3faa08e9cf1c621bbfeb27c77; __stripe_sid=a0f6f277-4a95-4d43-bd6f-c38f108e17f52fc752', + } + response = requests.post(f'{self.url}/api/resources/retry', headers=headers, json={'ids':ids}) + print(response.json()) + + def delete_images(ids: list): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'content-type': 'application/json', + # 'origin': 'https://ssfshop.zenshop.cn', + 'priority': 'u=1, i', + # 'referer': 'https://ssfshop.zenshop.cn/admin/resources', + 'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0', + # 'cookie': '__stripe_mid=a8059a05-4d7e-476d-b376-9e54f222efbe3c23c7; _bzgigi_vid=85F94DA2-7BD9-525E-E4A5-8D686E51B2C6; _bzgigi_cvid=EEFE28A2-FDC4-DC54-E76A-2BC1B2929243; utm_source=direct; utm_medium=default; order_utm_history=%5B%7B%22utm_source%22%3A%22direct%22%2C%22utm_medium%22%3A%22default%22%2C%22utm_term%22%3A%22%22%2C%22utm_campaign%22%3A%22%22%2C%22utm_content%22%3A%22%22%2C%22source_device%22%3A%22computer%22%2C%22create_time%22%3A1775195861%2C%22expire_time%22%3A1777787861%7D%5D; landing_page=aHR0cHM6Ly9zc2ZzaG9wLnplbnNob3AuY24vcHJvZHVjdHMvJUU4JTg3JUFBJUU3JTk0JUIxJUU1JThDJUJBLWwtMTklRTUlOEYlQjclRTMlODIlQjUlRTMlODIlQTQlRTMlODIlQkElRTMlODElQkUlRTMlODElQTclRTUlQjElOTUlRTklOTYlOEItJUUzJTgyJUFCJUUzJTgyJUJGJUUzJTgzJUFEJUUzJTgyJUIwJUU2JThFJUIyJUU4JUJDJTg5LSVFMyU4MiVCQiVFMyU4MyU4MyVFMyU4MyU4OCVFMyU4MiVBMiVFMyU4MyU4MyVFMyU4MyU5NyVFNSVBRiVCRSVFNSVCRiU5Qy0lRTMlODIlQkIlRTMlODMlQUMlRTMlODMlQTIlRTMlODMlOEIlRTMlODMlQkMtJUU5JTgwJTlBJUU1JThCJUE0LSVFMyU4MyU4OCVFMyU4MyVBQSVFMyU4MiVBMiVFMyU4MiVCQiVFMyU4MyU4MCVFMyU4MyU5NiVFMyU4MyVBQiVFMyU4MiVBRiVFMyU4MyVBRCVFMyU4MiVCOSVFMyU4MyU4NiVFMyU4MyVCQyVFMyU4MyVBOSVFMyU4MyVCQyVFMyU4MyU4OS0lRTMlODIlQjglRTMlODMlQTMlRTMlODIlQjElRTMlODMlODMlRTMlODMlODhfOGNhZjQ4Y2U%2FcHJldmlldz0x; first_http_referer=https%3A%2F%2Fssfshop.zenshop.cn%2Fadmin%2Fproducts; first_visit_time=1775195861; visit_token=41be25876fa245bb99018d68f7338d89y49l7utp; _bzgigi_vs=E5BE5924-EEF9-E277-6AFC-5F70BB548D0F; PHPSESSID=b08ff5f29b9469e1e92740efef87c09e; _BZGIGI_ua=Mozilla%252F5.0%2520(Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64)%2520AppleWebKit%252F537.36%2520(KHTML%252C%2520like%2520Gecko)%2520Chrome%252F146.0.0.0%2520Safari%252F537.36%2520Edg%252F146.0.0.0; _BZGIGI_pu=https%253A%252F%252Fssfshop.zenshop.cn%252Fadmin%252Fproducts; _BZGIGI_tz=Etc%2FGMT-8; _BZGIGI_ss=2560X1440; _BZGIGI_lang=zh-CN; _BZGIGI_vs=2514X1316; _BZGIGI_dt=2026-04-07%2014%3A48%3A20; theme_id=429104; is_theme_edit=1; acw_tc=9b66b4a617756120379694001e5c7efed3faa08e9cf1c621bbfeb27c77; cdn_sec_tc=9b66b4a617756120379694001e5c7efed3faa08e9cf1c621bbfeb27c77; __stripe_sid=60ef73cf-e5ed-483a-a342-9e581a0387d64df267', + } + json_data = { + 'ids': ids, + 'delete_product': 0 + } + response = requests.delete('https://ssfshop.zenshop.cn/api/resources/batchdelete', headers=headers, json=json_data) + print(response.json()) + + print('开始检查失败图片') + image_items = [] + data = get_errors(1) + page_total = data['paginate']['pageTotal'] + image_items += data['list'] + print(f"1/{page_total}") + + if page_total > 1: + for page in range(2, page_total+1): + ids = [] + data = get_errors(1) + for item in data['list']: + ids.append(item['id']) + if delete: + delete_images(ids) + else: + post_retry(ids) + page_total = data['paginate']['pageTotal'] + print(f"{page}/{page_total}") + + # lost_image_urls = [] + # for image_item in image_items: + # lost_image_urls.append(image_item['remote_url']) + + # if len(lost_image_urls) > 0: + # files.save_list('采集失败图片链接.txt', lost_image_urls) + # print(f"共有: {len(lost_image_urls)} 条失败链接") + + def get_all_images(self): + def get_images(page): + headers = { + 'accept': 'application/json, text/plain, */*', + 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', + 'cache-control': 'no-cache', + 'dnt': '1', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + # 'referer': 'https://vanuffelenmode.zenshop.cn/admin/resources', + 'sec-ch-ua': '"Not:A-Brand";v="99", "Microsoft Edge";v="145", "Chromium";v="145"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'token': self.token, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0', + # 'cookie': 'acw_tc=a3b54dab17734824723112557e921e5eb8672de1b4bf2e0bfb7de57fe1; cdn_sec_tc=a3b54dab17734824723112557e921e5eb8672de1b4bf2e0bfb7de57fe1; __stripe_mid=cc09d025-360b-466e-bef4-ba92d045fdc94448e2; __stripe_sid=511d0972-d39d-4aa1-a031-5258db651ad1d4124e; visit_token=9b8172c220d5be292045835470d38951px0lh404', + } + + params = { + 'file_name': '', + 'page_type': 'all', + 'page': str(page), + 'pagesize': '600', + } + + response = requests.get(f'{self.url}/api/resources', params=params, headers=headers) + response_data = response.json() + if response_data['code'] != 0: + raise Exception(response_data['msg']) + return response_data['data'] + + print('开始获取图片url') + image_items = [] + data = get_images(1) + page_total = data['paginate']['pageTotal'] + image_items += data['list'] + print(f"1/{page_total}") + + if page_total > 1: + for page in range(2, page_total+1): + data = get_images(page) + image_items += data['list'] + print(f"{page}/{page_total}") + + image_urls = [] + for image_item in image_items: + image_urls.append(image_item['remote_url']) + + if len(image_urls) > 0: + files.save_list('图片链接.txt', image_urls) + print(f"共有: {len(image_urls)} 张图片") diff --git a/main.py b/main.py new file mode 100644 index 0000000..b109ed6 --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +import lib +from lib import gether, shopyy +from lib.model import goods +from utils import formats, files, wpdata + +if __name__ == '__main__': + from lib.module import example, ssfshop, tfashion + + Server:example.SpiderModule + + Server = ssfshop.SpiderModule() + lib.Server = Server + + # category_urls = Server.get_category_urls() + # files.save_list(f"{Server.__urls_folder__}/categorys.txt", category_urls) + # Server.get_home() + # gether.get_goods_urls(2) + # formats.de_repeat_urls(r'data\bricomarche\urls\goods(home).txt') + # gether.get_goods_info() + + GoodsModel = goods.GoodsModel(Server.__database_path__) + # GoodsModel.to_shopyy_xlsx(f"{Server.__excels_folder__}/goods.xlsx") + # GoodsModel.to_woo_csv(r'data\fs\urls\goods_2026-01-23.txt_old.txt', f"data/fs/goods.csv") + # shopyy.Shopyy(Server.__data_path__).generate_album( + # f"{Server.__urls_folder__}/goods.txt_old.txt" + # ) + # shopyy.Shopyy(Server.__data_path__).generate_navigation( + # f"{Server.__urls_folder__}/categorys.txt" + # ) + # shopyy.Shopyy(Server.__data_path__).generate_album( + # f"{Server.__urls_folder__}/goods(home).txt" + # ).generate_navigation( + # f"{Server.__urls_folder__}/categorys(home).txt" + # ) + + # wpdata.Wpdata( + # 'data/condom69', + # r'data\condom69\excels\goods_1.xlsx', + # r'data\condom69\urls\goods.txt_old.txt', + # ).run() + + # Verify = shopyy.ShopyyVerify('https://ssfshop.zenshop.cn') + # Verify.get_zero_collections(True) + # Verify.get_error_images(True) + + # GoodsModel.splice_db('D:/Workers/Cerys/spiders/database/ssfshop') + GoodsModel.Db.close() + + Server.browser.quit() diff --git a/server.py b/server.py new file mode 100644 index 0000000..d94bf16 --- /dev/null +++ b/server.py @@ -0,0 +1,205 @@ +import os +import re +import platform +import subprocess +import psutil +import winreg +from flask import Flask, jsonify, request +from flask_cors import CORS + +app = Flask(__name__) + +# <--- 2. 启用全局跨域支持 +# 默认允许所有域名跨域访问所有接口。 +# 如果想只允许特定前端域名,可以改成 CORS(app, origins=["http://localhost:8080", "http://你的域名.com"]) +CORS(app, supports_credentials=True) + +# Windows 下分离进程的标志位,确保 exe 独立运行 +DETACHED_PROCESS = 0x00000008 + +def get_cpu_model(): + """ + 通过读取 Windows 注册表获取准确的 CPU 型号 + """ + try: + # 打开注册表路径 + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0") + # 读取 ProcessorNameString 的值 + cpu_model, _ = winreg.QueryValueEx(key, "ProcessorNameString") + return cpu_model.strip() + except Exception: + return "Unknown CPU" + +def is_valid_task_id(task_id): + """ + 校验 task_id,只允许字母、数字、下划线和连字符,防止目录穿越攻击 + """ + return re.match(r'^[\w\-]+$', task_id) is not None + +@app.route('/api/system_info', methods=['GET']) +def get_system_info(): + """ + 接口 1:获取当前设备(Windows)的负载情况和硬件信息(附带字段说明) + """ + try: + # 获取基础监控数据 + cpu_usage = psutil.cpu_percent(interval=0.5) + cpu_cores_physical = psutil.cpu_count(logical=False) + cpu_cores_logical = psutil.cpu_count(logical=True) + cpu_freq = psutil.cpu_freq() + virtual_mem = psutil.virtual_memory() + disk_usage = psutil.disk_usage('C:\\') + + # 构建返回数据,为每个返回值添加 desc 注释说明 + info = { + # "os": { + # "system": platform.system(), # 操作系统名称 (如 Windows) + # "release": platform.release(), # 操作系统主要版本号 (如 10, 11) + # "version": platform.version(), # 操作系统的详细内部构建版本号 + # "machine": platform.machine() # 系统架构 (如 AMD64 代表 64位) + # }, + "os": f"{platform.system()} {platform.version()} ({platform.machine()})", # 综合描述操作系统的字符串 + "hardware": { + "cpu": f"{get_cpu_model()} ({cpu_cores_physical}P/{cpu_cores_logical}L) {cpu_freq.max if cpu_freq else None}GHz", # CPU 型号和核心数的综合描述字符串 + # "cpu_model": get_cpu_model(), # CPU 具体型号名称 + # "cpu_cores_physical": cpu_cores_physical, # CPU 物理核心数 (真实核心) + # "cpu_cores_logical": cpu_cores_logical, # CPU 逻辑核心数 (包含超线程) + # "cpu_max_freq_mhz": cpu_freq.max if cpu_freq else None, # CPU 最大设计频率 (单位: MHz) + "ram_total_gb": round(virtual_mem.total / (1024 ** 3), 2), # 物理内存 (RAM) 总容量 (单位: GB) + "disk_c_total_gb": round(disk_usage.total / (1024 ** 3), 2) # 系统盘 (C盘) 总容量 (单位: GB) + }, + "load": { + "cpu_usage_percent": cpu_usage, # 当前 CPU 总利用率 (单位: %) + "ram_usage_percent": virtual_mem.percent, # 当前物理内存使用率 (单位: %) + "ram_available_gb": round(virtual_mem.available / (1024 ** 3), 2), # 当前可用的物理内存大小 (单位: GB) + "disk_c_usage_percent": disk_usage.percent, # 系统盘 (C盘) 存储空间使用率 (单位: %) + "disk_c_free_gb": round(disk_usage.free / (1024 ** 3), 2) # 系统盘 (C盘) 剩余可用空间 (单位: GB) + } + } + return jsonify({"code": 200, "status": "success", "data": info}), 200 + + except Exception as e: + return jsonify({"code": 500, "status": "error", "message": str(e)}), 500 + +@app.route('/api/run_exe', methods=['POST']) +def run_exe(): + """ + 接口 2:非阻塞地运行一个 exe 程序 + """ + data = request.get_json() + if not data or 'exe_path' not in data: + return jsonify({"status": "error", "message": "Missing 'exe_path' in request body."}), 400 + + exe_path = data['exe_path'] + args = data.get('args', []) + + if not os.path.exists(exe_path): + return jsonify({"status": "error", "message": f"File not found: {exe_path}"}), 404 + + try: + command = [exe_path] + args + process = subprocess.Popen( + command, + creationflags=DETACHED_PROCESS, + close_fds=True, + shell=False + ) + + return jsonify({ + "code": 200, + "status": "success", + "message": "Executable started successfully.", + "pid": process.pid + }), 200 + + except Exception as e: + return jsonify({"code": 500, "status": "error", "message": str(e)}), 500 + +@app.route('/api/upload', methods=['POST']) +def upload_files(): + """ + 接口 3:上传文件 + 接收 form-data 格式数据: + - task_id: 任务ID (字符串) + - file: 上传的文件 (可以同时上传多个文件) + """ + # 1. 获取并校验 task_id + task_id = request.form.get('task_id') + if not task_id: + return jsonify({"status": "error", "message": "Missing 'task_id' in form data."}), 400 + + if not is_valid_task_id(task_id): + return jsonify({"status": "error", "message": "Invalid 'task_id'. Only alphanumeric, dash, and underscore are allowed."}), 400 + + # 2. 准备目录 + # os.path.join 会自动根据当前系统(Windows)处理路径分隔符 + data_dir = os.path.join("data", task_id) + lib_dir = "lib/module" + + # 自动创建不存在的目录 (exist_ok=True 避免目录已存在时报错) + os.makedirs(data_dir, exist_ok=True) + os.makedirs(lib_dir, exist_ok=True) + + saved_files = [] + + # 3. 遍历并保存文件 + # request.files 是一个字典,通过 .getlist('file') 获取所有上传的文件(假设表单键名为 file) + # 为了兼容不同的前端传法,我们遍历 request.files 中的所有文件对象 + for key in request.files: + for file in request.files.getlist(key): + if file and file.filename: + # 获取文件后缀,转换为小写 + ext = os.path.splitext(file.filename)[1].lower() + + if ext == '.txt': + # .txt 储存至 data/{task_id}/goods.txt + save_path = os.path.join(data_dir, "goods.txt") + file.save(save_path) + saved_files.append({"original_name": file.filename, "saved_path": save_path}) + + elif ext == '.py': + # .py 储存至 lib/{task_id}.py + save_path = os.path.join(lib_dir, f"{task_id}.py") + file.save(save_path) + saved_files.append({"original_name": file.filename, "saved_path": save_path}) + else: + # 忽略不支持的文件格式 + pass + + if not saved_files: + return jsonify({"status": "error", "message": "No valid .txt or .py files were uploaded."}), 400 + + return jsonify({ + "status": "success", + "message": "Files uploaded successfully.", + "data": saved_files + }), 200 + +@app.route('/api/get_status', methods=['GET']) +def get_status(): + params = request.args + task_id = params.get('task_id') + + progress = 0 + status = 0 + if os.path.exists(f"data/{task_id}/progress"): + with open(f"data/{task_id}/progress", "r", encoding="utf-8") as f: + progress = f.read().strip() + if os.path.exists(f"data/{task_id}/status"): + with open(f"data/{task_id}/status", "r", encoding="utf-8") as f: + status = f.read().strip() + + return jsonify({ + "status": "success", + "message": f"Task '{task_id}' is ready.", + "data": { + "task_id": task_id, + "progress": progress, + "status": status + } + }), 200 + +if __name__ == '__main__': + # 解决 jsonify 返回中文字符时变成 Unicode 编码的问题 + app.config['JSON_AS_ASCII'] = False + app.run(host='127.0.0.1', port=5000, debug=True) \ No newline at end of file diff --git a/sources/amazon商品模板.xlsx b/sources/amazon商品模板.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f48e106f974a2b1c5311642d12478e5760de59a8 GIT binary patch literal 8747 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 literal 0 HcmV?d00001 diff --git a/sources/shopyy商品模板.xlsx b/sources/shopyy商品模板.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4c870d77c2b8ab2e5d5954519ee2c7a5cec47bb7 GIT binary patch literal 9969 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 YJfxZu0(5l&095EN1RBVLsDAtRf9HVzVE_OC literal 0 HcmV?d00001 diff --git a/sources/导航模板.xlsx b/sources/导航模板.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e22e85d51584deac45eb6778bd3deaafad0bf2f2 GIT binary patch literal 9620 zcmeHN1y>x~vL4)haCg@bG`KUtAy^3R?hsspI|NA}K@x%v?jZyV?hrgU1j!8U@+LXw zzMR87@BM*$d)AunUVG|W-FsK<`o5}B2O%H=0muMU002M>kP-|Xae@N?5Rm`?0stz! zp^USWhozH;v9_;^rMnTAkD~)kJ|a9zE&v|(`~M#Q!81^%-RIQ7gWIaRBO&|gNpxOh zNeL=NGle0;ca%(@6g`Ie4N{it0A?!#$`u$N5@F?uonHnR{NpqwsdKf zgbZUV+hz&;#muf)Uxz+Ch}DzCOvooplaoyOo9@St8hj6)FjTyDr_|v}JBw!pR;f4D zUQ#RV^p68`jjC)&MP#TYZOh<_9Qmj*A4H(xR`CK4X{>{hx#<_1`y2VtJLi1-O?DMX znse5`^`6GpqM>6+aXPtbQ`O#0cI$~4M_KMid;Goc%cD8_4J`Lm&(LmtNG$zNoH^bd z5r>R?r+LGhbl}?>j_MwPYr6>e*8BtSn8Ljr{yW;i@e@Jfed4P$?1V`np~UPtK!&#| zaWYus1QRg?b5F+2X67`G;-YjOdod?pih4Qy0Gs?47H4;N2mtlJg=w7*5B)LB_a4JW zF<@b8>}KiU&dv4n`@iA&9~_f^dG)ds6}3(t%m}FBb!6|^)M65@G{{>@v4vJUFj#2; zr!KyLfqb!znF3dvEDS**s5S7qe}3^%{7x_J=_-F&EIv?_zTUe$BIDB46NQb*EnVKV zY^4j&YwBd`G)qC*m&L0!k*&0`C=b-PL@z&aAX|ky!lg@wM(~~@f=DbY#NdmH{x`F8 zFx-^1){pXt$_ByQoz#)nLDT7l+gK7&!jJdHvj}_L&8%m^{=E*gCl_QoS~kMAm1b|; zMCg2tEgU`{$-ZtQxbf#y&Ftr8%kOmzl-j$p#d1KMqre}c3l^yo%6GmFk&M&Wr>cQE5;MydW znBJ^Nif=snQJVsC#^T%uK;gO3!rYe*J~32{^3Gh(+C-KVJ-z8Sau5LVsv5SBUpIoS z7%Pg!Q#qYKSErX0B1i0eoho0g9cA1q1V3FmVscWE3quZ<6+@8$Plp~nt_E+$lS%?B zir_Arf#p+%Oq#Qdmn$WC5!@E$l6ImycWk9pej3WSRY%Epd*Q1tUXs$w*tKbl z$MO|LtU0PWto-%_v<*rn6%RdUNNo#H?m>J+WSFy()|v&PhI?J1V7%9 zA}$xDXxOqBQ24a59AZc1Li;56m~s8eT%1wLUpR5!P5i(`@gvDd ze)clQ@rN#)UcLdVw$G#wl``MuuzK6pjk2QCMlu(kd59WBWq9NZjrtsW;nd$y57}H< zQr08wl`fM9;09BQ%sc0^t~1GHo-a>!9=J@9w?%iP<>qD)cchp?IEfn=d6>o~RYONP zYk-uz{mAklpGwCBk|s5lllS;VC#@65hqo33%ptEFuEcw3wz?pBVDxcj*Xy{knDR99 zmcW&+x5=Nu%Z5j)uRoz=q2($_L-|gvXipDmI#xV3*KA47eekRTsk(jBN9bQu8}6ju zY3x6*V_u^)(@)pV8i}bEZS)mCJju4hom|G<4^S4MUm3(W1W_Qy;zl0ueVKSqyh0P# zDF+;OPnr&Ks53p|E)@v;o`6WK#vPR-@FTizxes#c!7nH}f#>Rtc}DePRS*^?{~kyo z;%OO2u#sk1wjl$c!odRRkA(BraQavBfrD{FSUCP?A7z>8y&P|VLYwC-jHHnTg>M;Hcc6AX_%d8;uROH2p@HTVtJTI`6D*LC--h~9j~$VsDLumS1&n>4q-8&E zmpes0{CI7p;yY~LUT}%IK~V~#DtRd8AK^h0*{eVu#G2y1k)PF~16}cuzUpUfn?K|j zIEGrS_w`II!B+a;U1n2SC$1LeE{ZT6K?3{5pL?&njisfBJNK^>@6Sy*JEc#xlLr@; zr9Osj_^mOdaoQQs;*OG=wX_VstJ-0lfiQKoJW#cbodiv>R53sM6!hg5rQqcg3%FsB zz49wYRz{QX@^@M3$7?rL-ZIfR3))Zh5g3iaJ`}86%$nFP4hjN7Xxh(H1ZZoK%%Ab+ zHmNyaAY0q*^5dNE77jLG`%F2G$86X>@8-{la6)|{8ezI?fbLUi25t|>>IzwfJ35PZ zxJfVU<`o)Rboh1}KE+ALq}f*q$xXA-i2LwGL|jAfjBVFl>IwD|MO$q7W5K}Ockf-k z&WkW8TD{K38ECU?h>NA%uDmVa(SIwf*XtAFryXdZJiC$b5^74!+yVBg^mz)-94L9W zw7oRZDITlR>|F0I;UQGS@Xe(xMyLq%JcCe;)Us1*>#Z6Z=#HByMD3eo2x>4HDdX51 z0()!M6_Q9zhD#?Y1~&>JsjwWJdn;6#6sbpR4$5DYZv6&^vfAk(W!;C_aPT+2YE0bWbSW z6ZyZYPTO{B!<0>JU|70KLflSvN2`)eoH%kU#HJv5-kscNr)DA!jxKKHrq${<-_D(z zTIO{edsFj>7GiCjmm@ZJTvx;Y!7=HHJ;wOy9M?FRqpmHAqo8fiTm4U&g12oZFkkue z8Tb>V&^shCbpcTUR| zD$qPcYiB8(=)~+dMFg&)`Cx{)8DvUU6-Y#wuU>`Ma@vL>EAuXuOfJO8o0KP)+>5kw9=kIbe%?0YMv$`}k~l-pXlU_R@Fp34ag+p= zVaG3#ddctdTxQz&k!q29!~<2mIamAXN7L=w;VMcZ(8e=`PUQj5M#*iUM*3<(OUFWM z)!l{l^w^sy*HFqzwF|19#wy&{R=byKxugg74k21T0_OUmhznLnJNz7)5YQWnm#a;sYY4W<$cfNJ#mbcsjSr%%hDq}&i7&IN zkMIIHo<`DfElc&BOujW-Y_X3Yl)Zt-E-X;4Udf2wD(Mi@&&ui5HiTzH4X~;9!6vs6UtGZbVV>n@liflwjY6cmIZ zK_QPT2fG#)uk3oKpXmSVa-l=8)oIC@%m*okRY|R3^ck)|{)vrX5@|u?6Cct2$f4&e zA7!N;I+TFCc+HDvbnMZdcU98@7?S9uG!@E-d?J|CyYccZvhlW}2F+PMA}NzbT2(-> zT9L0aB#e!UqjJUK`0e{JGWW}w`g&GU<2Yn{TG5^vVI8_`Q}E!WDvg!8ndwpFdw>l; zUS7-x!5H<$Rm0n7Vpm+yW4brH=NWB*H;0K5##>Zh43Jm4&qj=IZ?C&?mab0*9>ktG zMSKzQm&G5>cJ;@^+JNm+S!^n29K9R9rF_>7zI~|(#P*b5!Q646)LqwF1NsP z`(bkX3Bz!DsNVBWgv`kMnY?r6Sq}jX7RCnQ3IiTa&_{+ljee*(vzH0#b__n1PlGG* zUk|W+K^|%WzK0)jjd?-;wn{6Gv%pU=3UAt+uaCs~!O9~BRUABfMR^Mcpr);dB7W~c zzdWv{W7p<+$b|W%z>c{1gXDb!8arBvj!D!tI0N*wnWqWr(kH>xw0QH1Z_28p6UbfQ zvfh~YXSW00oobp=+kNWZEx|iheYnX4;}c||x!E20hLcPr2Y8Wnv*_yyr}BTTGTzr> z(O6%w>_C>V+9aog(&DfzWYi_4wV5TwBJIdP|bwU~84%AQ)v zk6S3C-*k)riL{a3gI=#Daoy4ri!HMNc@{A|_L#&*g9`V|hTFzj_x#uyoKJFlD}Yi)Bk9r2e7->zy8#=&>JarhY+wE z>4}t<?t!Qv`x}dg+qIk?Ym?bcaqP5}_u#_mKG3D9tk2bxh&W zFalVXSvM>kHCs51fl?PcNDGT!hb+HwYKI1unzKoux4%aoJH?5Kbi~Ss?zq;TlW+Wj zlf@rQ`1M2o^4c>hj#M<#&@0U4G#8=6;*GIve4)+-RDJy*Vx~fA zuM-(r9T{KI{8$pg2Qgfm!cU{WMht3c_V5o)l zoAcO<#uw1V%z-|NKDZhZY*SLTZy!sy>r8v>>Vt10Yni9al))P4o#SU77spu5@Kov5 zD&@m-e!mMkUay|*sTj>zfb=V1MVXOWz!H$1ElOC!o=r-Nl{4x^>9b3#a#q%OKZZtX zr1oCoaHR#h$8rY}G@;*L56!Ws=@ASN#XV@TujIk+J~m^0$%KKez3ZZgt{q8FnP&ja z;U7l{L}ah!E2(A<-YqRLLkdvzURCqgq(}l>*eaUtp^Rzl)dkXxk4}3GJiTy9*H$(N zL$6B~=*GkKWX=4hcp|l_ibM!0tM2lbHSCbWH0boA)(OS{7!77G6<(}4q|yR&B2|BYHGi_q1={DYAhz9EA_{LH1| zmC9HTVh4rW9U71;bbQ?TInd^;NiAPvUj!S0n;J1skRrKCkP6F;JF(M7oNRb_?GeN^ z@oi%~yaeWa>t=6{)&SzhV~BlSg}O1iTFNueVjYtS?YhJYol}FwfnFU3z$X) z{^|KPrf!xNS{`n;PS(Eypt`D}cSwT;#K|^1I8vj-H~BzaQ*j71tkyROYS2N)a-FDC z*JR;R8U!f~_Kkq*bV?p`KB;b4+o)#c;?e-@46)bi^s}(CR*p||s}3s;sc?K9;8q+$ z=zq6Cyup+XXp-B$>_(C+j%Mv9EiPtk626p2Mz*?aiq7&?M(?|L@5@-Pys3=--Z$&T z`{H4qPdO^Cg*htGi?O%DYJMNYb4IWHFObP^@I}c3l7wU-eF+g;E$I?QZCi3ZY?w_( zVvQSK9;<17bIliWYiV2L-V!;}@lH&#*e!;+knC3IY^Ybbcz_AZ>`$5g@6t`wkq^`k z8EQUCRHW0vj9Zne3WE5 z!S(4Cf~u-=bWf5VAx6~;4Tc+%&cLqOssyy+B=OBzrD$KCzKdA$(XtXd$3tV3T5i=k zD93t$ZkULervB|-LICw6r-HU&#lw+sCE+9!@q9($Ub!EK!Ddps-luQu1F#Vb9R~-F zPSO5p2EZ6M(JipI=O6?CnE#jo7npN+Sh{IhdU*Ww67GBVyj*6sPJ3xX%4_Pto;%y? zqG2-*R3|H5pspDyXst&TG$HcxOL!rnU=53abS@L4N-`;_)Fx*aO4bI4(Iue9i%~q7 z{AOu}ZWzeADla6M?I#Xt+SoN-Cogx@bwq8Czb8G&+>-dx#%MKzN(vW zDDYMdbN6J!uUQ(fA7fqm?%=n=KOulJvws}`>zC${o+nRhrBCcUPu{)TSLZiT_(a@Q zK7)fGb>pUx?G+`aw3_;j$!s>9HBQqcP`*i6yxm?yeKxNDYEO{r zDlUwuoGPJ-DsN~#=z${`$$T?iZ%wp$?Z;&F`BK zSJX#%?kC$u=TfxWk@_&z01PcT;0vt#hu&S+0F9L7U+M=T(GJ0Pcc>t;{kRr zsa#{fgQ}xDxnfnGGyAzkN0Y=mtN1lM9G9MBn{R0N9PvFa@rUr)w7b(Q&a)D9@pe7^ zU5aEDc{rPA7{v|3&$)D09-^0-8&sBjO)5aptL5P0=KH4pjNFIc6XAg1uw;^)E3 zBy8%rIo%9&=kvRtS+2GuD;{SnH_NPvQm(1ahT^P#|7IvmHeVk!CGsAY*WA3vXR3nQ_(IirEoX+H)oBII zRsZ>!vKN_g-bk-i-O@uQKJhzgRi5nZtP>32v+1(!QUV=NqH_k;=&Pc{C1nv<=`k|U zpU_8;ZYfQV7V*>0CTm{Iu)O1UYOG)@SQ+2FLZ&#JcqFk_Dd)nN#%$``i`=6h3w_Ah z3OOGivLi;-i;2Z88$-8MzL@&{J+*@8>f@9N5_N3o_w-z7L3_?167>hO;+HRkNe7f? z5kn=2yB-!f*E03-K%XG$ABEzTZu#~9hlX&`rf&#^^ zZw~IZoZFSC?E-|HD0?;wvQM#q6arqz2q|v1-wA<^K8$T(-AEW{fo)bo~{`sFJ|60F)o&Vu~ChDNS z1N^<`{V&6x=MS(T`AY}qwv8xoUm89UwXU`a9{ZS2H1j;9T>oG8F=6HK6CzUx&o{4 z|1`bNqwgc!XO_Pa5MagAKNsTv70e?SaB>j_DD_(f3&=q9`gR14buf3@+;=AOi_QGQsG(vBge# zS;DRqc7vFVGlnH?fMz2TqM9eSx(Q8M;1VI>SB+N+RdyQ}PBclYMkW~3(zzs^<}d4C zkWK%;vl%1?aVL0t z1Bzr}95N-5NJI0Nt&&x;yblCy(8LtU6b8Z=0VImY21_J!w?)gjz$ME<{6P=0HOodb z5UrX;w@s54xMb9PWSc}VPPHP*maDTkN0;U3a+S(<$36_O3z69-fJqT9rjUDU}%i{|H8K&?rVUUEo9SKU_%cS?3H!c@K_CTF)(^IqT@ zK8<5QTGEcG$URl=MOiAB%(6t~F%ScumF-aPYiiuGxp!?-fUP@H*WmLqjkM3(@NVN) zFWuBo=iS;^x7oM3sm|BjL`RDd#_XjFO4Z|mld~s(m{ZPVKDi97uSCTzjt} znqOsUp5?1f$i40^QH*S>t@YJ4)oex|-?kHSZG`7O2(p3)gT^kFW>{Y6t^5zW+7mLw z=}>4+ZR*@#^Yil%4|B((%aA(z+4#)GvDpi!9^LwScKGAW$h8x4v=|-ydj8gviJ_{; zr@x&U7|X+q+R^@_TOVbR2x^6DI+KEemXA~Yc2Y?y%to(?ly z4>Sb2xL|;VcqA%9iy+csRPYBlhV>LfBEs>=Ltq0ui-HPlhz+2gL0e&v{=&&JRHGlw z%wK4(KNvp3LHl#N(89BVW&-W1ZOpGW(;q^kfMzu1+-VHzg|6Tc5!%&$2E$P{zb!y= z!3SD`D$M*jv}sZy0F^H;ukq@O2NDb|(zy{p9N`euYg6^__2>GX+YTS+_+y77F?7qr z`+2S#747pKj*7vMa5xa6g|3=N3{rtEI()PMjjut`)bR?xpkE^eq%BTmXWCk?w59E< zl|2tFc2zA_+Oh^PP+Cf9gm!4=c>DOP3xp{EPw%m6t z&lI^m9dCydpT@FgtXI-GATzOPoqO z){Y)ZIBLhKgk$?eFyYv9$C+^KyK^w%cr&g~TO7ly2Ab1UQPvDB_CH9VCnw4EDbkxD zySI=+P0=_I})}Xx66~Z_Isv| zEDlVS&(NF!H2M8KbTI7qdk9{AH}NY}qTU1bD0i*>mjPd^?s{{(Ej=6)4Y4ot^+;9! svJD>JplFx!1}wX8QYI#c?=L#y!S3%dY^E7)Q)lzWw|(i13Ivp6951J literal 0 HcmV?d00001 diff --git a/utils/__pycache__/db.cpython-312.pyc b/utils/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d451f96dc8dc9c9dd8b5394770a29828720c955 GIT binary patch literal 32356 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 S{T*+4P@_q|$&t&}%l`qk&l3Lt literal 0 HcmV?d00001 diff --git a/utils/__pycache__/files.cpython-312.pyc b/utils/__pycache__/files.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea5ca0e04c4eb7b7fe86593d4c87e2802f169809 GIT binary patch literal 2003 zcmc&!%WD%s7@wJ)ZFbYfR`G$>N2=BuAFaU$T0|(~wTO>HB_JWQwOi68%xs%RB6{#( zRY*NG$6jLRd4DtC@B4l8Iu?r(C?|Jc zaqEnb&)5kI^mS>Rf@+Qka-ZBFf&x;(0GTiV4Z$5g%7l5J-!Pl)HfXn%A_*%K+CW2X z@EhWj5aI1!Do0so=MvxzOB8;a2!9G|!==L=KQ7 zQMAY?86gzD5}7teNRige;oB^IblIHrMvtGZQQvZMnY?gww|p9{F`wrPPR@@OrRzCa zH|MCD;aj6_)+waDF`s+RwCB@Ng|~{wMxXhTcxL*n|Pk=0?X)E;~2m_^3jex7_0L6 z;yZibMe>7XS2|`oX0KHbJ{eiE_I)!*^u&96V9_2}ZdkCPvhBF>+&mM>tk4RBE0jNk z&IXjzGHpfq0_?h(?XM{Tk0%8$euBO>z&>IqUS?rS7mBu z>e2KAj>HXoYVWKJ&J50qx9#0;c=tCG#@^Wd%L;E$*?i7;X%4YSYxQPf$p%!etKN{B zztL@i16jo%WF=U@lc|*|+tKHyC<9gkNuge4H%wxJt3H5VNC8PjW+JovaZHo)-tJ#S z4z!xK95J4nC)c6i|0J+PFD2fth0|7Ah@0ByJ+MIQWizha0^pljNKe%|oPUrR>x5JA zR{Af~Fvwy3SHU)i~5(|lE)<^@8 z1PN=MpU8{JtaCvoU<}`H9jh+}rL@i%jn^%M?yg4}-Cnn#`vqWs)oT!_u-#V`U!mP6 H55v~)9g}PQ literal 0 HcmV?d00001 diff --git a/utils/__pycache__/formats.cpython-312.pyc b/utils/__pycache__/formats.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cd613718a69e2e8c67baea0176df87a6912b8bc GIT binary patch literal 5390 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/messages.cpython-312.pyc b/utils/__pycache__/messages.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f586847327d72fa1ccf4538c46ccc735c6c3ab8 GIT binary patch literal 1404 zcmdT^-Afcv6u)=o?z%gZiPe^(;uLK;N`VCt8iwIUmS3qLZOXFjjJE5}j@=oP6$KxB zP!co<+|Zzh9ugn)wFn{zdgx^r5oHP@g3341o_gxs+pgvM7j$6Hz2|p6_MG26yB`V* zB?NNPc@W*k2z_NnPK4}bw-YY&NJAQqqaZtlI5vc!V2DA{AVGqWfON8)4aZ0mCsEL) z5#8M{Xs$^TEYRG5c(Oavq@-67FL)Q)6}@k0d7KD!#I zd1P~tso37_?3*pEq$kvTy2V)>F$BDcI1`xUD)3Ciq zTaK&+c;_*+fS!^ZIT$68Dz-WJ?x@hGz|5{88co=|(3rfX6s7jG@JrmnuIUhP?J zPM0)fq=t9C_myueGg8Mox&6Ozwe|SPkCl}?uEfqj(_f>ieYNQalL_7LJr$;>oTZq5o7!j6@5l$GU&_~ kL?+!kM8wCpC5)@*BD)BV9XH0EcpG#(p0j=e)Q)-O-(o`y3IG5A literal 0 HcmV?d00001 diff --git a/utils/__pycache__/redis.cpython-312.pyc b/utils/__pycache__/redis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..124663383549638a9987fb26ef8eddf58308fd69 GIT binary patch literal 2768 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 yrt!MpkrutH)rE856lrI;pywlnEF List[dict]: + """查询多条:返回字典列表""" + cursor = self.conn.cursor() + try: + cursor.execute(sql, params) + rows = cursor.fetchall() + if not rows: + return [] + # 兼容性处理:如果是对象(SQLite Row)则转dict,如果是dict(MySQL)则直接用 + return [dict(row) for row in rows] if rows else [] + finally: + cursor.close() + + @db_execute_wrapper + def fetch_one(self, sql: str, params: tuple = ()) -> Optional[dict]: + """查询单条:返回字典 或 None""" + cursor = self.conn.cursor() + try: + cursor.execute(sql, params) + row = cursor.fetchone() + return dict(row) if row else None + finally: + cursor.close() + + @db_execute_wrapper + def execute(self, sql: str, params: tuple = ()) -> int: + """增删改:返回影响行数""" + cursor = self.conn.cursor() + try: + cursor.execute(sql, params) + if not self._in_transaction: + self.conn.commit() + return cursor.rowcount + finally: + cursor.close() + + @db_execute_wrapper + def execute_insert(self, sql: str, params: tuple = ()) -> int: + """插入:返回自增ID""" + cursor = self.conn.cursor() + try: + cursor.execute(sql, params) + if not self._in_transaction: + self.conn.commit() + return cursor.lastrowid + finally: + cursor.close() + + @db_execute_wrapper + def execute_many(self, sql: str, params_list: List[tuple]) -> int: + """批量执行:返回影响行数""" + cursor = self.conn.cursor() + try: + cursor.executemany(sql, params_list) + if not self._in_transaction: + self.conn.commit() + return cursor.rowcount + finally: + cursor.close() + + @db_execute_wrapper + def execute_raw(self, sql: str, params: tuple = ()) -> Union[List[dict], int]: + """混合执行:根据是否返回结果集自动判断""" + cursor = self.conn.cursor() + try: + cursor.execute(sql, params) + # 如果有 description 说明是 SELECT 类查询 + if cursor.description: + rows = cursor.fetchall() + return [dict(row) for row in rows] if rows else [] + else: + if not self._in_transaction: + self.conn.commit() + return cursor.rowcount + finally: + cursor.close() + + @db_execute_wrapper + def execute_script(self, sql_script: str) -> None: + """简单的脚本执行,不支持复杂的存储过程分隔符""" + self.__connect__() + cursor = self.conn.cursor() + try: + # 开启多语句支持通常需要在 connect 时指定 client_flag + # 或者手动分割 + statements = [s.strip() for s in sql_script.split(';') if s.strip()] + for sql in statements: + cursor.execute(sql) + if not self._in_transaction: + self.conn.commit() + finally: + cursor.close() + +class MysqlDriver(BaseDriver): + def __init__(self, host, port, user, password, database, charset='utf8mb4'): + super().__init__() + self.param_mark = '%s' # MySQL 占位符 + self.quote_mark = '`' + self.config = { + 'host': host, + 'port': port, + 'user': user, + 'password': password, + 'database': database, + 'charset': charset, + 'autocommit': False, + 'cursorclass': DictCursor, + 'client_flag': CLIENT.MULTI_STATEMENTS + } + self._is_mysql = True + self.__connect__() + + @property + def _in_transaction(self) -> bool: + """判断当前是否处于事务中""" + return self._transaction_depth > 0 + + def __connect__(self) -> None: + if self.conn is None: + self.conn = pymysql.connect(**self.config) + else: + try: + # 【修复】去除已在 PyMySQL 新版废弃的 reconnect=True 参数 + self.conn.ping(reconnect=False) + except Exception: + self.conn = pymysql.connect(**self.config) + + # =============== 事务控制方法 =============== + def begin(self) -> None: + self.__connect__() + if self._transaction_depth == 0: + self.conn.begin() + self._transaction_depth += 1 + + def commit(self) -> None: + if self._transaction_depth > 0: + self._transaction_depth -= 1 + if self._transaction_depth == 0 and self.conn: + self.conn.commit() + + def rollback(self) -> None: + # 安全清理嵌套层级,防止局部回滚后引发后续误提交 + if self._transaction_depth > 0: + self._transaction_depth = 0 + if self.conn: + self.conn.rollback() + # ============================================ + + def close(self) -> None: + if self.conn: + self.conn.close() + self.conn = None + + +class SqliteDriver(BaseDriver): + def __init__(self, db_path: str): + super().__init__() + self.param_mark = '?' # SQLite 占位符 + self.quote_mark = '"' + self.db_path = db_path + self._is_mysql = False + self.__connect__() + + @property + def _in_transaction(self) -> bool: + return self._transaction_depth > 0 + + def __connect__(self) -> None: + if not self.conn: + self.conn = sqlite3.connect(self.db_path, check_same_thread=False, timeout=10.0) + self.conn.row_factory = sqlite3.Row + self.conn.execute('PRAGMA journal_mode=WAL;') + self.conn.execute('PRAGMA synchronous=NORMAL;') + + @db_execute_wrapper + def execute_script(self, sql_script: str) -> None: + """SQLite 原生支持脚本执行,更安全且无需手动分割""" + # 注意:sqlite3.Cursor.executescript 不遵循 commit 逻辑,它会直接提交 + # 所以这里不需要 conn.commit(),但为了保持一致性逻辑,还是走装饰器 + cursor = self.conn.cursor() + try: + cursor.executescript(sql_script) + # SQLite executescript 会自动 commit,不需要手动再 commit + finally: + cursor.close() + + # =============== 事务控制方法 =============== + def begin(self) -> None: + self.__connect__() + self._transaction_depth += 1 + + def commit(self) -> None: + if self._transaction_depth > 0: + self._transaction_depth -= 1 + if self._transaction_depth == 0 and self.conn: + self.conn.commit() + + def rollback(self) -> None: + if self._transaction_depth > 0: + self._transaction_depth = 0 + if self.conn: + self.conn.rollback() + # ============================================ + + def close(self) -> None: + if self.conn: + self.conn.close() + self.conn = None + + +class Query: + """查询构建器:负责拼装 SQL,且支持链式调用不可变对象""" + def __init__(self, driver, table_name: str): + self.driver = driver + self.table_name = table_name + self._wheres: List[tuple] = [] # 【修改】存为 tuple: (逻辑符, 语句) 支持 OR + self._params: List[Any] = [] + self._orders: List[str] = [] + self._joins: List[str] = [] + self._limit: Optional[int] = None + self._offset: Optional[int] = None + + self.mark = getattr(driver, 'param_mark', '?') + self.quote = getattr(driver, 'quote_mark', '') + + def __copy_instance(self) -> 'Query': + """【核心】创建当前对象的深拷贝,用于链式调用不污染原对象""" + new_query = copy.copy(self) + new_query._wheres = self._wheres[:] + new_query._params = self._params[:] + new_query._orders = self._orders[:] + new_query._joins = self._joins[:] + return new_query + + def _q(self, field: str) -> str: + field = field.strip() + if field == '*' or '(' in field or ')' in field: + return field + + # 简单的防止注入校验 + if re.search(r'[;\'"\s\-]', field): + raise ValueError(f"非法字段名: {field}") + + # 支持 table.column 格式 + if '.' in field: + table, col = field.split('.', 1) + return f"{self.quote}{table}{self.quote}.{self.quote}{col}{self.quote}" + + return f"{self.quote}{field}{self.quote}" + + # ================= 链式构建方法 (返回新对象) ================= + + def where(self, key: str, value: Any, operator: str = '=') -> 'Query': + """ + 单条件判断 + + :param key: 字段名 + :param value: 值 + :param operator: 运算符 + + """ + new_q = self.__copy_instance() + condition = f"{self._q(key)} {operator} {self.mark}" + new_q._wheres.append(("AND", condition)) + new_q._params.append(value) + return new_q + + def or_where(self, key: str, value: Any, operator: str = '=') -> 'Query': + """ + OR 条件判断 + 方法同where + """ + new_q = self.__copy_instance() + condition = f"{self._q(key)} {operator} {self.mark}" + new_q._wheres.append(("OR", condition)) + new_q._params.append(value) + return new_q + + def where_all(self, condition: Dict[str, Any], operator: str = '=', logic: str = 'AND') -> 'Query': + """ + 多条件判断 + + :param condition:param condition: { 字段名: 值 } + :param operator: 运算符 (传入字典建议保持默认 `=`) + :param logic: 逻辑运算符内部连接(AND/OR) + + """ + new_q = self.__copy_instance() + clauses = [] + for key, value in condition.items(): + clauses.append(f"{self._q(key)} {operator} {self.mark}") + new_q._params.append(value) + + if clauses: + full_condition = f"({f' {logic.upper()} '.join(clauses)})" + # 作为一个整体条件加入,默认外部通过 AND 连接 + new_q._wheres.append(("AND", full_condition)) + return new_q + + def where_in(self, key: str, values: List[Any]) -> 'Query': + """ + IN 语句支持 + + :param key: 字段名 + :param values: 匹配值列表 + """ + if not values: + raise ValueError("whereIn 的 values 参数不能为空列表") + new_q = self.__copy_instance() + placeholders = ", ".join([self.mark] * len(values)) + condition = f"{self._q(key)} IN ({placeholders})" + new_q._wheres.append(("AND", condition)) + new_q._params.extend(values) + return new_q + + def where_raw(self, sql: str, params: tuple = ()) -> 'Query': + """ + 原生条件语句 (支持复杂逻辑查询) + 注意:调用方需自行通过 params 传递参数以防注入 + + :param sql: SQL 字符串,如 "age > ? AND (status = ? OR role = ?)" + :param params: 对应占位符的参数元组 + """ + new_q = self.__copy_instance() + new_q._wheres.append(("AND", f"({sql})")) + new_q._params.extend(params) + return new_q + + def join(self, table: str, on: str, join_type: str = 'LEFT') -> 'Query': + """ + 链表查询 + + :param table: 要链接的表名 + :param on: 链接条件 + :param join_type: 链接类型(LEFT/INNER/RIGHT) + + """ + new_q = self.__copy_instance() + # 注意: join 的 on 条件由开发者自己写 raw sql,需自行注意安全 + new_q._joins.append(f"{join_type.upper()} JOIN {self._q(table)} ON {on}") + return new_q + + def order(self, field: str, direction: str = 'ASC') -> 'Query': + """ + 排序 + + :param field: 排序字段 + :param direction: 排序方向(ASC/DESC) + + """ + direction = direction.upper() + if direction not in ('ASC', 'DESC'): + raise ValueError("排序方向只能是 ASC 或 DESC") + + new_q = self.__copy_instance() + # _q() 内部已经加上了正则校验,防止 order by 注入 + new_q._orders.append(f"{self._q(field)} {direction}") + return new_q + + def limit(self, limit: int, offset: int = 0) -> 'Query': + """ + 分页 + + :param limit: 每页数量 + :param offset: 第几页 (从0开始) + + """ + new_q = self.__copy_instance() + new_q._limit = int(limit) + new_q._offset = int(offset) + return new_q + + # ================= 终结方法 (执行并返回结果) ================= + + def select(self, fields: str = '*') -> List[dict]: + """ + 执行查询返回列表 + + :param fields: 查询字段 + + """ + sql = self._build_select_sql(fields) + return self.driver.fetch_all(sql, tuple(self._params)) + + def find(self, fields: str = '*') -> Optional[dict]: + """ + 执行查询返回单条 + + :param fields: 查询字段 + + """ + temp_q = self.__copy_instance() + temp_q._limit = 1 + temp_q._offset = 0 + + sql = temp_q._build_select_sql(fields) + return self.driver.fetch_one(sql, tuple(temp_q._params)) + + def count(self) -> int: + """统计数量""" + sql = self._build_select_sql("COUNT(*) AS cnt") + res = self.driver.fetch_one(sql, tuple(self._params)) + return res.get('cnt', 0) if res else 0 + + def insert(self, data: Dict[str, Any], replace: bool = False) -> int: + """ + 插入单条数据 + + :param data: { 字段名: 值 } + :param replace: 是否使用 REPLACE INTO(存在则覆盖) + :return: 影响行数/自增主键ID + + """ + keys = [self._q(k) for k in data.keys()] + values = list(data.values()) + placeholders = ",".join([self.mark] * len(values)) + columns = ",".join(keys) + + action = "REPLACE" if replace else "INSERT" + sql = f"{action} INTO {self._q(self.table_name)} ({columns}) VALUES ({placeholders})" + return self.driver.execute_insert(sql, tuple(values)) + + def insert_all(self, data: List[Dict[str, Any]], replace: bool = False) -> int: + """ + 批量插入 + 每条数据key结构必须相同 + + :param data: [{ 字段名: 值 }, ...] + :param replace: 是否使用 REPLACE INTO(存在则覆盖) + :return: 影响行数 + + """ + if not data: return 0 + + raw_keys = list(data[0].keys()) + keys = [self._q(k) for k in raw_keys] + columns = ",".join(keys) + placeholders = ",".join([self.mark] * len(keys)) + + params_list = [tuple(d.get(k) for k in raw_keys) for d in data] + + action = "REPLACE" if replace else "INSERT" + sql = f"{action} INTO {self._q(self.table_name)} ({columns}) VALUES ({placeholders})" + + return self.driver.execute_many(sql, params_list) + + def update(self, data: Dict[str, Any]) -> int: + """ + 更新符合条件的所有行数据 + + :param data: { 字段名: 值 } + :return: 影响行数 + + """ + if not data: + return 0 # 避免生成无效 SQL + + set_clauses = [] + set_params = [] + + for key, value in data.items(): + set_clauses.append(f"{self._q(key)} = {self.mark}") + set_params.append(value) + + set_sql = ", ".join(set_clauses) + where_sql = self._build_where_sql() + if not where_sql: + raise ValueError("WHERE 条件不存在,为防止全表更新操作已被拦截!") + + full_params = set_params + self._params + sql = f"UPDATE {self._q(self.table_name)} SET {set_sql} {where_sql}" + return self.driver.execute(sql, tuple(full_params)) + + def delete(self) -> int: + """ + 删除符合条件的所有行数据 + :return: 影响行数 + + """ + where_sql = self._build_where_sql() + if not where_sql: + raise ValueError("WHERE 条件不存在,为防止全表删除操作已被拦截!") + sql = f"DELETE FROM {self._q(self.table_name)} {where_sql}" + return self.driver.execute(sql, tuple(self._params)) + + # ================= 内部辅助方法 ================= + + def _build_where_sql(self) -> str: + if not self._wheres: + return "" + + # 【修改】适配新的 Tuple(logic, condition) 结构以支持 OR + where_parts = [] + for i, (logic, condition) in enumerate(self._wheres): + if i == 0: + where_parts.append(condition) # 首个条件不需要 AND/OR 前缀 + else: + where_parts.append(f"{logic} {condition}") + + return "WHERE " + " ".join(where_parts) + + def _build_select_sql(self, fields: str) -> str: + parts = [f"SELECT {fields} FROM {self._q(self.table_name)}"] + + if self._joins: + parts.extend(self._joins) + + where_sql = self._build_where_sql() + if where_sql: + parts.append(where_sql) + + if self._orders: + parts.append("ORDER BY " + ", ".join(self._orders)) + + if self._limit is not None: + parts.append(f"LIMIT {self._limit}") + + if self._offset is not None: + parts.append(f"OFFSET {self._offset}") + + return " ".join(parts) + + +class Db: + """数据库入口""" + def __init__(self, db_path: str, config: dict = None, db_type: str = None): + config = config or {} + + if db_type is None: + if db_path.lower() == 'mysql' or 'host' in config: + db_type = 'mysql' + else: + db_type = 'sqlite' + + if db_type == 'mysql': + self.driver = MysqlDriver(**config) + elif db_type == 'sqlite': + self.driver = SqliteDriver(db_path) # 【修复】采用正确的命名 + else: + raise ValueError("db_type 参数错误: 应为 'mysql' 或 'sqlite'") + + @contextmanager + def transaction(self): + """ + 事务上下文管理器 + with db.transaction(): + db.table('A').insert(...) + db.table('B').update(...) + """ + self.driver.begin() + try: + yield self + self.driver.commit() + except Exception: + self.driver.rollback() + raise # 【修复】直接 raise 避免截断真实报错的 Traceback + + def table(self, table_name: str) -> Query: + """ + 获取表操作对象 + + :param table_name: 表名 + + """ + return Query(self.driver, table_name) + + def execute_raw(self, sql: str, params: tuple = ()) -> Union[List[dict], int]: + """ + 执行原生 SQL + + :param sql: SQL 语句 + :param params: 参数 + + """ + return self.driver.execute_raw(sql, params) + + def execute_many_raw(self, sql: str, params_list: List[tuple]) -> int: + """ + 批量执行原生 SQL + + :param sql: SQL 语句 + :param params_list: 参数列表 + + """ + return self.driver.execute_many(sql, params_list) + + def execute_script(self, sql_script: str) -> None: + """ + 执行多条 SQL 语句(如建表) + + :param sql_script: SQL 脚本 + + """ + self.driver.execute_script(sql_script) + + def close(self) -> None: + self.driver.close() + + # Context Manager 支持 + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() \ No newline at end of file diff --git a/utils/files.py b/utils/files.py new file mode 100644 index 0000000..57e79f2 --- /dev/null +++ b/utils/files.py @@ -0,0 +1,24 @@ +def save_line(filepath, text): + with open(filepath, 'a+', encoding='utf-8') as f: + f.writelines(text+'\n') + +def load_lines(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + text = f.read() + return text.split('\n') + +def save_text(filepath, text): + with open(filepath, 'w', encoding='utf-8') as f: + f.write(text) + +def add_text(filepath, text): + with open(filepath, 'a+', encoding='utf-8') as f: + f.writelines(text+'\n') + +def save_list(filepath, lists: list): + with open(filepath, 'w', encoding='utf-8') as f: + f.write('\n'.join(lists)) + +def add_list(filepath, lists: list): + with open(filepath, 'a+', encoding='utf-8') as f: + f.write('\n'.join(lists)+'\n') diff --git a/utils/formats.py b/utils/formats.py new file mode 100644 index 0000000..41a93f0 --- /dev/null +++ b/utils/formats.py @@ -0,0 +1,104 @@ +import hashlib +from bs4 import BeautifulSoup +import time +import random +import datetime +import re + +def url_to_spu(url: str, length: int = 36) -> str: + h = hashlib.sha256(url.encode('utf-8')).digest() + int_val = int.from_bytes(h, 'big') + chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + base36 = '' + while int_val > 0: + int_val, idx = divmod(int_val, 36) + base36 = chars[idx] + base36 + base36 = base36.zfill(50) + return base36[:length] + +# 工具函数,几乎每个站点的采集都会用到 +def remove_unwanted_tags(html_content): + html_content = str(html_content) + soup = BeautifulSoup(html_content, 'html.parser') + for tag in soup(['button', 'img', 'a', 'script', 'svg', 'video']): + tag.decompose() + return str(soup) + +def clean_html(html): + soup = BeautifulSoup(html, 'html.parser') + + # 遍历所有标签 + for tag in soup(['button', 'a', 'script']): + tag.attrs = {} # 直接清空所有属性 + + return str(soup) + +def de_repeat_urls(filepath): + index = {} + with open(filepath, 'r', encoding='utf-8') as f: + text = f.read() + old_data = text.split('\n') + with open(f"{filepath}_old.txt", 'w', encoding='utf-8') as f: + f.write('\n'.join(old_data)) + + new_data = [] + for line in old_data: + line = '#'.join(line.split('#')[:-1]) + if line in index: + continue + new_data.append(line) + index[line] = None + with open(filepath, 'w', encoding='utf-8') as f: + f.write('\n'.join(new_data)) + +def getTime(timeStamp = False, format: str = '%Y-%m-%d %H:%M:%S'): + """ + 获取时间 + Author Cerys + ChangeTime 2023-11-10 + + @param timeStamp 指定时间戳 + @param fotmat 指定格式 + + return str + """ + + if timeStamp != False: + time = datetime.datetime.fromtimestamp(timeStamp) + else: + time = datetime.datetime.now() + formatTime = time.strftime(format) + + return formatTime + +def formatName(name: str, types: bool = False): + """ + 下划线字符串命名转大驼峰或小驼峰 + Author Cerys + ChangeTime 2023-11-10 + + @param name 要转换的字符串 + @param types 是否转为大驼峰 + + return str + """ + + strLists = name.split('_') + if types: + return strLists[0].title() + ''.join(x.title() for x in strLists[1:]) + else: + return strLists[0] + ''.join(x.title() for x in strLists[1:]) + +def formatMd5(text: str = '', is_random: bool = False, attach: str = ''): + if is_random: + text = f"{time.time()}_{random.randint(10000, 99999)}" + md5 = hashlib.md5(f"{text}_{attach}".encode()).hexdigest() + return md5 + +def re_search(text: str, rule: str) -> str: + match = re.search(rule, text) + if match: + url_path = match.group(1) + return url_path + else: + return "" diff --git a/utils/messages.py b/utils/messages.py new file mode 100644 index 0000000..a70292d --- /dev/null +++ b/utils/messages.py @@ -0,0 +1,18 @@ +import datetime +from colorama import Fore, Style, init + +init() + +def __getTime__(): + time = datetime.datetime.now() + formatTime = time.strftime('%Y-%m-%d %H:%M:%S') + return formatTime + +def sendInfo(message: str): + print(f"[{__getTime__()}] {message}") + +def sendWarn(message): + print(Fore.YELLOW + f"[{__getTime__()}] 提示: {message}" + Style.RESET_ALL) + +def sendError(message: str): + print(Fore.RED + f"[{__getTime__()}] 错误: {message}" + Style.RESET_ALL) diff --git a/utils/uploads.py b/utils/uploads.py new file mode 100644 index 0000000..4a8a73f --- /dev/null +++ b/utils/uploads.py @@ -0,0 +1,27 @@ +import os +import io +import time +import random +import requests + +def upload_image(image2_data, domain: str): + image_stream = io.BytesIO(image2_data) + headers = { + 'Token': 'fangzhouxinghe' + } + data = { + 'domain': domain + } + files = { + 'file': (f'{time.time()}_{random.randint(10000, 99999)}.jpg', image_stream, 'image/jpeg') + } + + response = requests.post('https://fangzhouxinghe.com/uploads.php', data=data, headers=headers, files=files) + response_data = response.json() + + if response_data['code'] != 200: + raise Exception(response_data['msg']) + return response_data['data'] + +if __name__ == '__main__': + print(upload_image(open(r"test\50137.jpg", 'rb').read(), 'test')) diff --git a/utils/wpdata.py b/utils/wpdata.py new file mode 100644 index 0000000..d6a73e9 --- /dev/null +++ b/utils/wpdata.py @@ -0,0 +1,828 @@ +import pandas as pd +from bs4 import BeautifulSoup +import os +import re +import requests +from io import BytesIO +import hashlib +import concurrent.futures +import time + +class Wpdata: + def __init__(self, data_path: str, input_excel: str, goods_url_path: str, upload_workers: int = 10, upload_image_domain:str = "https://www.fzcaiji.com/upload.php", column_name:str = "商品图片*"): + self.__data_path__ = data_path + self.__input_excel__ = input_excel + self.__goods_url_path__ = goods_url_path + + self.__upload_workers__ = upload_workers + self.__upload_image_domain__ = upload_image_domain + self.__column_name__ = column_name + + self.__output_csv__ = f"{input_excel}.csv" + self.__images_txt__ = f"{data_path}_images.txt" + + # 原始数据转换 + def transform_excel_to_csv(self): + def match_variant_count(number, group_df, variant_1, title, text, spu, data_rows, variant_2=None,variant_3=None): + # 提取当前spu分组的价格 + sale_prices = group_df[(group_df['商品属性*'] == 'P') | (group_df['商品属性*'] == 'S')]['商品售价*'].tolist() + origin_prices = group_df[(group_df['商品属性*'] == 'P') | (group_df['商品属性*'] == 'S')]['商品原价'].tolist() + + # 提取对应属性的图片,并统计 + M_images = group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['商品图片*'].tolist() + image_list = [url.strip() for url in M_images[0].split(',')] + + group_df + # 获取当前分组的URL + if 'url' in group_df: + url = group_df['url'].tolist()[0] + else: + url = group_df['URL'].tolist()[0] + + match number: + case 3: + # 提取当前spu分组属性为P的值 + all_variants_first = group_df[(group_df['商品属性*'] == 'P')]['款式1'].tolist() + all_variants_second = group_df[(group_df['商品属性*'] == 'P')]['款式2'].tolist() + all_variants_three = group_df[(group_df['商品属性*'] == 'P')]['款式3'].tolist() + + all_variants_count = max(len(all_variants_first), len(all_variants_second), len(all_variants_three)) + + if all_variants_count >= len(image_list): + + # 计算需要扩大的长度,并进行扩大 + expand_length = all_variants_count - len(image_list) + expanded_image_list = image_list + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + for k, variant in enumerate(all_variants_first): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1, + "Option1 Value": all_variants_first[k], + "Option2 Name": variant_2, + "Option2 Value": all_variants_second[k], + "Option3 Name": variant_3, + "Option3 Value": all_variants_second[k], + "Variant SKU": f"{spu}{str(k)}", + "Variant Grams": "0", + "Variant Inventory Tracker": "shopify", + "Variant Inventory Qty": "1000", + "Variant Inventory Policy": "continue", + "Variant Fulfillment Service": "manual", + "Variant Price": sale_prices[k], + "Variant Compare At Price": origin_prices[k], + "Variant Requires Shipping": "TRUE", + "Variant Taxable": "TRUE", + "Variant Barcode": "", + "Image Src": expanded_image_list[k], + "Image Position": (image_counter := image_counter + 1) if expanded_image_list[ + k] is not None else "", # 海象运算符 + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if all_variants_first[k] else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + else: # 变体少于图片数量 + # 计算扩大长度 + expand_length = len(image_list) - all_variants_count + + expanded_all_variants_first_count = all_variants_first + [None] * expand_length + expanded_all_variants_two_count = all_variants_second + [None] * expand_length + expanded_all_variants_three_count = all_variants_three + [None] * expand_length + # 扩大价格长度(为了统一价格) + expanded_sale_prices = sale_prices + [None] * expand_length + expanded_origin_prices = origin_prices + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + + for k, variant in enumerate(image_list): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1 if expanded_all_variants_first_count[k] is not None else "", + "Option1 Value": expanded_all_variants_first_count[k], + "Option2 Name": variant_2 if expanded_all_variants_two_count[k] is not None else "", + "Option2 Value": expanded_all_variants_two_count[k], + "Option3 Name": variant_3 if expanded_all_variants_three_count[k] is not None else "", + "Option3 Value": expanded_all_variants_three_count[k], + "Variant SKU": f"{spu}{str(k)}" if expanded_all_variants_first_count[k] is not None else "", + "Variant Grams": "0" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Tracker": "shopify" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Inventory Qty": "1000" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Policy": "continue" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Fulfillment Service": "manual" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Price": expanded_sale_prices[k], + "Variant Compare At Price": expanded_origin_prices[k], + "Variant Requires Shipping": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Taxable": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Barcode": "", + "Image Src": image_list[k], + "Image Position": (image_counter := image_counter + 1) if image_list[k] is not None else "", + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if k == 0 else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + case 2: + # 提取当前spu分组属性为P的值 + all_variants_first = group_df[(group_df['商品属性*'] == 'P')]['款式1'].tolist() + all_variants_second = group_df[(group_df['商品属性*'] == 'P')]['款式2'].tolist() + + all_variants_count = max(len(all_variants_first), len(all_variants_second)) + + if all_variants_count >= len(image_list): + + # 计算需要扩大的长度,并进行扩大 + expand_length = all_variants_count - len(image_list) + expanded_image_list = image_list + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + for k, variant in enumerate(all_variants_first): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1, + "Option1 Value": all_variants_first[k], + "Option2 Name": variant_2, + "Option2 Value": all_variants_second[k], + "Option3 Name": "", + "Option3 Value": "", + "Variant SKU": f"{spu}{str(k)}", + "Variant Grams": "0", + "Variant Inventory Tracker": "shopify", + "Variant Inventory Qty": "1000", + "Variant Inventory Policy": "continue", + "Variant Fulfillment Service": "manual", + "Variant Price": sale_prices[k], + "Variant Compare At Price": origin_prices[k], + "Variant Requires Shipping": "TRUE", + "Variant Taxable": "TRUE", + "Variant Barcode": "", + "Image Src": expanded_image_list[k], + "Image Position": (image_counter := image_counter + 1) if expanded_image_list[ + k] is not None else "", # 海象运算符 + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if all_variants_first[k] else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + else: # 变体少于图片数量 + # 计算扩大长度 + expand_length = len(image_list) - all_variants_count + + expanded_all_variants_first_count = all_variants_first + [None] * expand_length + expanded_all_variants_two_count = all_variants_second + [None] * expand_length + + # 扩大价格长度(为了统一价格) + expanded_sale_prices = sale_prices + [None] * expand_length + expanded_origin_prices = origin_prices + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + + for k, variant in enumerate(image_list): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1 if expanded_all_variants_first_count[k] is not None else "", + "Option1 Value": expanded_all_variants_first_count[k], + "Option2 Name": variant_2 if expanded_all_variants_two_count[k] is not None else "", + "Option2 Value": expanded_all_variants_two_count[k], + "Option3 Name": "", + "Option3 Value": "", + "Variant SKU": f"{spu}{str(k)}" if expanded_all_variants_first_count[k] is not None else "", + "Variant Grams": "0" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Tracker": "shopify" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Inventory Qty": "1000" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Policy": "continue" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Fulfillment Service": "manual" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Price": expanded_sale_prices[k], + "Variant Compare At Price": expanded_origin_prices[k], + "Variant Requires Shipping": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Taxable": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Barcode": "", + "Image Src": image_list[k], + "Image Position": (image_counter := image_counter + 1) if image_list[k] is not None else "", + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if k == 0 else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + case 1: + all_variants = group_df[(group_df['商品属性*'] == 'P')]['款式1'].tolist() + + all_variants_count = len(all_variants) + + if all_variants_count >= len(image_list): + # 计算扩大长度 + expand_length = all_variants_count - len(image_list) + expanded_list = image_list + [None] * expand_length + + # 扩大价格 + expanded_sale_prices = sale_prices + [None] * expand_length + expanded_origin_prices = origin_prices + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + for k, variant in enumerate(all_variants): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1, + "Option1 Value": all_variants[k], + "Option2 Name": "", + "Option2 Value": "", + "Option3 Name": "", + "Option3 Value": "", + "Variant SKU": f"{spu}{str(k)}", + "Variant Grams": "0", + "Variant Inventory Tracker": "shopify", + "Variant Inventory Qty": "1000", + "Variant Inventory Policy": "continue", + "Variant Fulfillment Service": "manual", + "Variant Price": expanded_sale_prices[k], + "Variant Compare At Price": expanded_origin_prices[k], + "Variant Requires Shipping": "TRUE", + "Variant Taxable": "TRUE", + "Variant Barcode": "", + "Image Src": expanded_list[k], + "Image Position": (image_counter := image_counter + 1) if expanded_list[k] is not None else "", + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if all_variants[k] else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + else: # 变体少于图片数量 + # 计算扩大长度 + expand_length = len(image_list) - all_variants_count + expanded_all_variants_first_count = all_variants + [None] * expand_length + + # 扩大价格 + expanded_sale_prices = sale_prices + [None] * expand_length + expanded_origin_prices = origin_prices + [None] * expand_length + + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + + for k, variant in enumerate(image_list): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": variant_1 if expanded_all_variants_first_count[k] is not None else "", + "Option1 Value": expanded_all_variants_first_count[k], + "Option2 Name": "", + "Option2 Value": "", + "Option3 Name": "", + "Option3 Value": "", + "Variant SKU": f"{spu}{str(k)}" if expanded_all_variants_first_count[k] is not None else "", + "Variant Grams": "0" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Tracker": "shopify" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Inventory Qty": "1000" if expanded_all_variants_first_count[k] is not None else "", + "Variant Inventory Policy": "continue" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Fulfillment Service": "manual" if expanded_all_variants_first_count[ + k] is not None else "", + "Variant Price": expanded_sale_prices[k], + "Variant Compare At Price": expanded_origin_prices[k], + "Variant Requires Shipping": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Taxable": "TRUE" if expanded_all_variants_first_count[k] is not None else "", + "Variant Barcode": "", + "Image Src": image_list[k], + "Image Position": (image_counter := image_counter + 1) if image_list[k] is not None else "", + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if k == 0 else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + + case 0: + Handle = title[0].replace(' ', '-').replace('/', '-').lower() + image_counter = 0 + for k, variant in enumerate(image_list): + rows = { + "Handle": Handle, + "Title": title[0] if k == 0 else "", + "Body (HTML)": text if k == 0 else "", + "Vendor": "", + "Type": "", + "Tags": "", + "Published": "TRUE" if k == 0 else "", + "Option1 Name": "variant", + "Option1 Value": "default", + "Option2 Name": "", + "Option2 Value": "", + "Option3 Name": "", + "Option3 Value": "", + "Variant SKU": f"{spu}" if k == 0 else "", + "Variant Grams": "0" if k == 0 else "", + "Variant Inventory Tracker": "shopify" if k == 0 else "", + "Variant Inventory Qty": "1000" if k == 0 else "", + "Variant Inventory Policy": "continue" if k == 0 else "", + "Variant Fulfillment Service": "manual" if k == 0 else "", + "Variant Price": sale_prices[0] if k == 0 else "", + "Variant Compare At Price": origin_prices[0] if k == 0 else "", + "Variant Requires Shipping": "TRUE" if k == 0 else "", + "Variant Taxable": "TRUE" if k == 0 else "", + "Variant Barcode": "", + "Image Src": image_list[k], + "Image Position": (image_counter := image_counter + 1) if image_list[k] is not None else "", + "Image Alt Text": "", + "Gift Card": "FALSE" if k == 0 else "", + "SEO Title": title[0] if k == 0 else "", + "SEO Description": title[0] if k == 0 else "", + "Google Shopping / Google Product Category": "", + "Google Shopping / Gender": "", + "Google Shopping / Age Group": "", + "Google Shopping / MPN": "", + "Google Shopping / AdWords Grouping": "", + "Google Shopping / AdWords Labels": "", + "Google Shopping / Condition": "", + "Google Shopping / Custom Product": "", + "Google Shopping / Custom Label 0": "", + "Google Shopping / Custom Label 测试.txt": "", + "Google Shopping / Custom Label 2": "", + "Google Shopping / Custom Label 3": "", + "Google Shopping / Custom Label 4": "", + "Variant Image": "", + "IsDraft": "", + "Variant Weight Unit": "Kg" if k == 0 else "", + "Variant Tax Code": "", + "Cost per item": "", + "Status": "activate" if k == 0 else "", + "Collection": "", + "url": url + } + data_rows.append(rows) + data_rows = [] + df = pd.read_excel(self.__input_excel__) + + # 以相同商品spu为一组 + grouped = df.groupby('商品spu') + + data_rows = [] + + for spu, group_df in grouped: + print(spu) + group_df = group_df.fillna('') + # 获取商品属性为M或S的商品标题 + title = group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['商品标题*'].tolist() + description = group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['商品描述'].tolist() + description.append('') + text = BeautifulSoup(description[0], 'html.parser').get_text() + + variant_name_one = group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['款式1'].tolist()[ + 0] + variant_name_two = group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['款式2'].tolist()[ + 0] + variant_name_three = \ + group_df[(group_df['商品属性*'] == 'M') | (group_df['商品属性*'] == 'S')]['款式3'].tolist()[0] + variant_count = 0 + + if variant_name_one: + variant_count += 1 + if variant_name_two: + variant_count += 1 + if variant_name_three: + variant_count += 1 + + # 统计个数 + match_variant_count(variant_count, group_df, variant_name_one, title, text, spu, data_rows, variant_name_two,variant_name_three) + new_df = pd.DataFrame(data_rows) + new_df.to_csv(self.__output_csv__, index=False, encoding="utf-8") + + return self + + # 数据分割 + def split_csv(self, csv_path: str = "", goods_url_path: str = ""): + if csv_path == "": + csv_path = self.__output_csv__ + if goods_url_path == "": + goods_url_path = self.__goods_url_path__ + + with open(goods_url_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + dict_all = {} + for i in lines: + url = i.strip() + if url == '': + continue + album = url.rsplit('#', 1)[1] + third_album = album.split('/')[-1].lstrip() # 最后一个专辑 + # 全部存进字典 + if third_album not in dict_all: + dict_all[third_album] = [] + + dict_all[third_album].append(url.rsplit('#',1)[0]) + # print(dict_all) + + # 测试.txt. 读取 CSV 文件 + df = pd.read_csv(csv_path) # 替换为你的 CSV 文件路径 + + output_dir = f"{self.__data_path__}/已分类数据" + os.makedirs(output_dir, exist_ok=True) + # 2. 遍历字典的每个键(分类) + for category, urls in dict_all.items(): + # 3. 筛选出 url 列中包含在当前分类的 URL 的行 + filtered_df = df[df['url'].isin(urls)] + + # 4. 如果筛选后的 DataFrame 不为空,则删除 'url' 列并保存到 CSV + if not filtered_df.empty: + filtered_df = filtered_df.drop(columns=['url']) # 删除 url 列 + output_filename = fr"{output_dir}/{category}.csv" + filtered_df.to_csv(output_filename, index=False) + print(f"已保存 {len(filtered_df)} 条数据到 {output_filename}(已删除 url 列)") + else: + print(f"分类 {category} 无匹配数据,跳过保存") + return self + + # 获取原始图片 + def get_excel_images_to_txt(self): + def split_image_urls(cell: str): + """按 ',http' / ',https' 分割字符串,自动补上 'http'。不校验后缀。""" + if not isinstance(cell, str) or not cell.strip(): + return [] + + s = re.sub(r',\s*(https?://)', r'||SEP||\1', cell.strip(), flags=re.IGNORECASE) + parts = s.split('||SEP||') + + urls = [] + for part in parts: + part = part.strip().lstrip(',').strip() + if not part: + continue + m = re.search(r'https?://', part, re.IGNORECASE) + if m: + part = part[m.start():] + urls.append(part) + return urls + + """从 Excel 中读取指定列,返回每行的图片 URL 列表。""" + df = pd.read_excel(self.__input_excel__) + urls_list = [] + for val in df[self.__column_name__].fillna(''): + urls_list.append(split_image_urls(val)) + + """展开 + 去重 + 保存到 txt 文件""" + all_urls = set() # 用 set 去重 + for row_urls in urls_list: + for url in row_urls: + all_urls.add(url.strip()) + + # 写入文件 + with open(self.__images_txt__, "w", encoding="utf-8") as f: + for url in sorted(all_urls): + f.write(url + "\n") + + print(f"✅ 共提取 {len(all_urls)} 张图片,已保存到 {self.__images_txt__}") + return self + + # 上传原始图片 + def update_images(self): + def upload_image(img_url: str): + """下载并上传单张图片""" + HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Referer": "https://www.google.com/" + } + try: + # 通过 requests 下载图片(带代理和请求头) + resp_img = requests.get(img_url, timeout=15, headers=HEADERS) + resp_img.raise_for_status() + img_data = BytesIO(resp_img.content) + + # 生成哈希文件名 + hash_name = hashlib.sha1(img_url.encode("utf-8")).hexdigest() + + # 获取扩展名(安全处理) + ext = img_url.split("?")[0].split(".")[-1].lower() + if len(ext) > 5 or "/" in ext: + ext = "jpg" + + # 上传 + files = {"file": (f"{hash_name}.{ext}", img_data, f"image/{ext}")} + data = {"source_url": img_url} + resp = requests.post(self.__upload_image_domain__, files=files, data=data, timeout=30) + + # 打印结果 + try: + js = resp.json() + print(f"✅ {img_url} -> {js.get('url') or js}") + except Exception: + print(f"❌ {img_url} -> 上传失败: {resp.text[:100]}") + + except Exception as e: + print(f"⚠️ 下载或上传异常: {img_url} -> {e}") + # 读取本地 txt 文件 + with open(self.__images_txt__, "r", encoding="utf-8") as f: + urls = [x.strip() for x in f if x.strip()] + + print(f"共读取到 {len(urls)} 张图片。开始上传...") + + start = time.time() + # 使用线程池并发上传 + with concurrent.futures.ThreadPoolExecutor(max_workers=self.__upload_workers__) as executor: + list(executor.map(upload_image, urls)) + + print(f"\n全部上传完成,用时 {time.time() - start:.2f} 秒。") + + return self + + # 替换原始图片 + def replace_excel_image(self): + """ + 读取 Excel 和 txt,将原图片 URL 替换为新图片 URL,保存到新 Excel + """ + def get_uploaded_image_url_php_style(img_url: str) -> str: + """ + 模拟 PHP 上传脚本的路径生成逻辑: + - hash = sha1(source_url) + - 一级目录 = hash 前 1 字符 + - 二级目录 = hash 第 2 字符 + - 文件名 = hash + 扩展名 + """ + # 计算 sha1 + hash_name = hashlib.sha1(img_url.encode('utf-8')).hexdigest() + + # 取扩展名(与 PHP 一致:从文件名里提取,默认 jpg) + ext = os.path.splitext(img_url.split('?')[0])[1].lstrip('.').lower() + if not ext or len(ext) > 5 or "/" in ext: + ext = 'jpg' + + # 两层目录 + dir1 = hash_name[0] + dir2 = hash_name[1] + + # 拼接最终 URL + new_url = f"{self.__upload_image_domain__}/{dir1}/{dir2}/{hash_name}.{ext}" + + return new_url + + def split_image_urls(cell: str): + """按 ',http' / ',https' 分割字符串,自动补上 'http'。不校验后缀。""" + if not isinstance(cell, str) or not cell.strip(): + return [] + + s = re.sub(r',\s*(https?://)', r'||SEP||\1', cell.strip(), flags=re.IGNORECASE) + parts = s.split('||SEP||') + + urls = [] + for part in parts: + part = part.strip().lstrip(',').strip() + if not part: + continue + m = re.search(r'https?://', part, re.IGNORECASE) + if m: + part = part[m.start():] + urls.append(part) + # cell = ',' + cell + # cell = cell.split(',https://') + # urls = [] + # for image_url in cell: + # if '.svg' in image_url: + # continue + # if image_url == "": + # continue + # urls.append(('https://'+image_url).strip()) + return urls + + # 读取 txt 中的原图片链接 + with open(self.__images_txt__, "r", encoding="utf-8") as f: + original_urls = [line.strip() for line in f if line.strip()] + + # 生成原图片到新图片的映射 + url_mapping = {} + for old_url in original_urls: + new_url = get_uploaded_image_url_php_style(old_url) + url_mapping[old_url] = new_url + + print(f"📋 共加载 {len(url_mapping)} 个图片映射关系") + + # 读取 Excel + df = pd.read_excel(self.__input_excel__) + + # 替换图片列 + replaced_count = 0 + for idx, cell_value in enumerate(df[self.__column_name__].fillna('')): + if not cell_value: + continue + + # 分割出该单元格的所有图片 URL + old_urls = split_image_urls(cell_value) + + # 替换为新 URL + new_urls = [] + for old_url in old_urls: + if old_url in url_mapping: + new_urls.append(url_mapping[old_url]) + replaced_count += 1 + else: + new_urls.append(old_url) # 如果找不到映射,保持原样 + + # 拼接回去(用逗号分隔) + df.at[idx, self.__column_name__] = ','.join(new_urls) + + output_excel = f"{self.__input_excel__}(new_image).xlsx" + # 保存到新 Excel + df.to_excel(output_excel, index=False) + print(f"✅ 替换完成!共替换 {replaced_count} 个图片链接") + print(f"✅ 已保存到 {output_excel}") + self.__input_excel__ = output_excel + + # 一键运行 + def run(self, replace_image: bool = False): + if replace_image: + self.get_excel_images_to_txt().update_images().replace_excel_image() + self.transform_excel_to_csv().split_csv() + +if __name__ == "__main__": + wpdata = Wpdata( + data_path="data", # 已分类数据文件夹保存路径 + input_excel=r"data\jamesavery\info\goods(sort)_2025-12-24.xlsx", # 原始excel路径 + goods_url_path=r"data\jamesavery\urls\goods_2025-12-19.txt_old.txt" # 商品txt路径 + ) + wpdata.run( + # replace_image=True # 是否上传图片 + )