初始化

This commit is contained in:
2026-04-10 10:57:44 +08:00
parent ec6e7573de
commit a19a468298
70 changed files with 5676 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/data/*
/database/*
/release/
/test/
/.vscode/
/main.build/
/main.dist/

1
build Normal file
View File

@@ -0,0 +1 @@
nuitka --mingw64 --standalone main.py

3
core/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from core import types
Config = types.Config()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

25
core/browsers.py Normal file
View File

@@ -0,0 +1,25 @@
import socket
import random
from DrissionPage import Chromium, ChromiumOptions
def getBrowsers(chrome_zoom = 1.0, save_path: str = "") -> 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

84
core/excels.py Normal file
View File

@@ -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()

31
core/spiders.py Normal file
View File

@@ -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)

263
core/types.py Normal file
View File

@@ -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

4
lib/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from core import browsers
from lib.module import example
Server: example.SpiderModule

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

131
lib/gether.py Normal file
View File

@@ -0,0 +1,131 @@
import os
import traceback
import concurrent.futures
from utils import files, messages, clash, formats
import lib
from core import Config, types
from lib.model import goods
def get_category_urls():
category_urls_path = f"{lib.Server.__urls_folder__}/categorys.txt"
if os.path.exists(category_urls_path):
return
category_urls = lib.Server.get_category_urls()
for urls in category_urls:
if 'http' not in urls:
continue
files.save_line(category_urls_path, urls)
def get_goods_urls(categorys_len: int = 3):
goods_urls_path = f"{lib.Server.__urls_folder__}/goods.txt"
category_urls_path = f"{lib.Server.__urls_folder__}/categorys.txt"
if Config.home_mode:
goods_urls_path = goods_urls_path.replace('.txt', '(home).txt')
category_urls_path = category_urls_path.replace('.txt', '(home).txt')
category_index = {}
category_urls = files.load_lines(category_urls_path)
if os.path.exists(goods_urls_path):
old_goods_urls = files.load_lines(goods_urls_path)
for old_goods_url in old_goods_urls:
category = old_goods_url.split('#')[-1]
category_index[category] = None
index = 0
max_len = len(category_urls)
for category_url in category_urls:
category = category_url.split('#')[-1]
category_url = '#'.join(category_url.split('#')[:-1])
index += 1
if 'https' not in category_url:
continue
if Config.skip_category and Config.home_mode == False and len(category.split('/')) < categorys_len:
continue
if category in category_index:
continue
try:
goods_urls = lib.Server.get_goods_urls(category_url)
goods_category_urls = []
for goods_url in goods_urls:
goods_category_urls.append(f"{goods_url}#{category}")
files.add_list(goods_urls_path, goods_category_urls)
except Exception as e:
files.add_text(f"{lib.Server.__errors_folder__}/category_errors.log", f"{category_url}:\n[{e}] {traceback.format_exc()}")
messages.sendError(str(e))
messages.sendInfo(f"{index}/{max_len} {len(goods_urls)}")
if Config.home_mode == False:
formats.de_repeat_urls(goods_urls_path)
def get_goods_info():
p_urls = {}
def workers(goods_urls, index):
with concurrent.futures.ThreadPoolExecutor(max_workers=lib.Server.worker_num) as executor:
futures = []
for goods_url in goods_urls:
if goods_url in p_urls:
continue
future = executor.submit(lib.Server.get_goods_info, goods_url)
futures.append(future)
results: list[types.GoodsInfo] = []
for future_index, future in enumerate(futures):
index += 1
try:
result = future.result() # 阻塞直到该任务完成
except Exception as e:
try:
files.add_text(f"{lib.Server.__errors_folder__}/goods_errors.log", f"{goods_urls[future_index]}:\n[{e}] {traceback.format_exc()}")
except:
pass
messages.sendWarn(f"{goods_urls[future_index]} 出错")
continue
results.append(result)
messages.sendInfo(f"{index}/{max_len}")
for goods_info in results:
try:
GoodsModel.add_goods(*goods_info.to_db_data())
for p_url in goods_info.p_urls:
p_urls[p_url] = None
except Exception as e:
messages.sendWarn(f"{e}")
return index
goods_urls = files.load_lines(f"{lib.Server.__urls_folder__}/goods.txt")
messages.sendInfo('读取url完成')
messages.sendInfo('读取上次存档')
GoodsModel = goods.GoodsModel(lib.Server.__database_path__)
excel_index = GoodsModel.select_goods_to_dict()
messages.sendInfo('读取完成')
de_save_url = []
for goods_url in goods_urls:
if goods_url in excel_index:
continue
if 'http' not in goods_url:
continue
de_save_url.append(goods_url)
goods_urls = de_save_url
del excel_index
max_len = len(goods_urls)
index = 1
for i in range(0, len(goods_urls), lib.Server.bitch):
index = workers(goods_urls=goods_urls[i:i+lib.Server.bitch], index=index)
if lib.Server.switch_clash:
try:
clash.change_proxy()
except:
pass
if lib.Server.reflush_browsers:
if lib.Server.worker_num > 1:
lib.Server.reflush_browser()
messages.sendInfo("已保存")

Binary file not shown.

417
lib/model/goods.py Normal file
View File

@@ -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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

176
lib/module/agatameble.py Normal file
View File

@@ -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

View File

@@ -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

201
lib/module/bricomarche.py Normal file
View File

@@ -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('', '')}")
# 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

136
lib/module/condom69.py Normal file
View File

@@ -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

210
lib/module/dk_one.py Normal file
View File

@@ -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

86
lib/module/example.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

371
lib/module/ssfshop.py Normal file
View File

@@ -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}&currentPage={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}&currentPage={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)

131
lib/module/taketoys.py Normal file
View File

@@ -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

140
lib/module/tfashion.py Normal file
View File

@@ -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

371
lib/shopyy.py Normal file
View File

@@ -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)} 张图片")

49
main.py Normal file
View File

@@ -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()

205
server.py Normal file
View File

@@ -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)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
Type,SKU,Name,Published,Description,In Stock?,Stock,Sale Price,Regular Price,Categories,Images,Parent,Attribute 1 Name,Attribute 1 Value(s),Attribute 1 visible,Attribute 2 Name,Attribute 2 Value(s),Attribute 2 visible,Attribute 3 Name,Attribute 3 Value(s),Attribute 3 visible
1 Type SKU Name Published Description In Stock? Stock Sale Price Regular Price Categories Images Parent Attribute 1 Name Attribute 1 Value(s) Attribute 1 visible Attribute 2 Name Attribute 2 Value(s) Attribute 2 visible Attribute 3 Name Attribute 3 Value(s) Attribute 3 visible

BIN
sources/专辑模板.xlsx Normal file

Binary file not shown.

BIN
sources/导航模板.xlsx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

64
utils/clash.py Normal file
View File

@@ -0,0 +1,64 @@
import requests
import random
def change_proxy():
port = '17650'
secret = '7ce2f241-d6a1-4b69-a742-96201072187a'
selector = '🚀 节点选择'
port = port
secret = secret
headers_secret = {
'Authorization': 'Bearer {}'.format(secret)
}
url_all_proxies_info = 'http://127.0.0.1:{}/proxies'.format(port)
url_all_proxies_info = requests.get(url_all_proxies_info,headers=headers_secret).json()
url_all_proxies = 'http://127.0.0.1:{}/proxies/{}'.format(port,selector)
res_proxies = requests.get(url_all_proxies,headers=headers_secret).json()
# res_proxies = res_proxies['proxies'][selector]
proxy_name_list = res_proxies['all'][3:-3]
now_proxy = res_proxies['now']
print('现在的代理是{}'.format(now_proxy))
# if now_proxy not in ['DIRECT','REJECT','账号邮箱看最新的地址','NETV2','自动选择','故障转移']:
# proxy_name_list.remove(now_proxy)
shift = []
continue_proxys = ['♻️ 自动选择', '仅海外用户']
for proxy_name in proxy_name_list:
is_continue = False
for continue_proxy in continue_proxys:
if continue_proxy in proxy_name:
is_continue = True
break
if is_continue:
continue
# if '香港' not in proxy_name or '日本' not in proxy_name or '俄罗斯' not in proxy_name:
# continue
shift.append(proxy_name)
proxy_name_list = shift
while True:
random_proxy = random.choice(proxy_name_list)
delay = url_all_proxies_info['proxies'][random_proxy]['history'][-1]['delay']
if delay!=0:
break
print('随机选择的代理为{}'.format(random_proxy))
data = {
"name": random_proxy
}
headers = {
"content-type": "application/json",
'Authorization': 'Bearer {}'.format(secret)
}
res = requests.put(url=url_all_proxies,json=data,headers=headers)
print('切换代理请求的状态码为{}'.format(res.status_code))
if res.status_code == 204:
print('切换代理成功!现在的代理为{}'.format(random_proxy))
if __name__ == '__main__':
change_proxy()

672
utils/db.py Normal file
View File

@@ -0,0 +1,672 @@
#############################################################################
# Author: Cerys
# Update: 2026-03-06
#############################################################################
import sqlite3
import pymysql
import copy
import functools
import re
from pymysql.constants import CLIENT
from pymysql.cursors import DictCursor
from typing import List, Dict, Any, Optional, Union
from contextlib import contextmanager
# ================= 统一执行装饰器 =================
def db_execute_wrapper(func):
"""
统一执行装饰器:
1. 负责自动获取/检查连接
2. 自动处理非事务状态下的 commit 和 rollback
3. 针对 MySQL 处理 Gone Away 断连重连重试机制
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
self.__connect__()
try:
res = func(self, *args, **kwargs)
return res
except Exception as e:
# 针对 MySQL 的长连接断开 (OperationalError) 尝试进行一次重试
if getattr(self, '_is_mysql', False) and not self._in_transaction:
if isinstance(e, pymysql.err.OperationalError):
try:
# 【修复】废弃 reconnect=True直接销毁并重新初始化连接
self.conn = None
self.__connect__()
res = func(self, *args, **kwargs)
return res
except Exception:
if self.conn:
self.conn.rollback()
raise # 【修复】直接 raise保留完整的错误堆栈 Traceback
# 常规错误回滚
if getattr(self, 'conn', None) and not getattr(self, '_in_transaction', False):
self.conn.rollback()
raise # 【修复】直接 raise保留完整的错误堆栈 Traceback
return wrapper
# =================================================
class BaseDriver:
"""
基类:负责定义接口类型,让 VS Code 能识别返回值
"""
def __init__(self):
self.conn = None
# 子类需设置这两个属性
self.param_mark = '?'
self._is_mysql = False
self._transaction_depth = 0
# -------------- 核心执行方法 (清晰直白) --------------
@db_execute_wrapper
def fetch_all(self, sql: str, params: tuple = ()) -> 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()

24
utils/files.py Normal file
View File

@@ -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')

104
utils/formats.py Normal file
View File

@@ -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 ""

18
utils/messages.py Normal file
View File

@@ -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)

27
utils/uploads.py Normal file
View File

@@ -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'))

828
utils/wpdata.py Normal file
View File

@@ -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 # 是否上传图片
)