初始化

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

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