#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ BUFF饰品属性信息获取脚本(使用连接池版本) 特点: 1. 使用DBUtils连接池管理数据库连接 2. 自动处理连接失效和重连 3. 更稳定的长连接管理 4. 避免频繁创建和关闭连接的开销 5. 直接请求接口,无代理 快速补齐缺失属性:可设置环境变量调大每轮处理量,例如: BATCH_SIZE=200 python sync_goods_attr.py (当前为串行模式:每次 1 个请求,每 3 秒一次) 安装依赖: pip install DBUtils==3.0.2 """ import os import sys import time import logging import threading import json import requests import pymysql from dbutils.pooled_db import PooledDB import concurrent.futures from datetime import datetime from typing import List, Dict, Any, Optional, Tuple # ============================================================ # 配置信息 # ============================================================ # BUFF API配置 BUFF_API_BASE_URL = "https://buff.163.com/api/market/goods/sell_order" GAME = "csgo" # 数据库配置 DB_HOST = os.getenv("DB_HOST", "125.122.32.114") DB_PORT = int(os.getenv("DB_PORT", "3306")) DB_USER = os.getenv("DB_USER", "root") DB_PASSWORD = os.getenv("DB_PASSWORD", "Buff123456") DB_NAME = os.getenv("DB_NAME", "buff") # 连接池配置 DB_POOL_CONFIG = { 'creator': pymysql, 'host': DB_HOST, 'port': DB_PORT, 'user': DB_USER, 'password': DB_PASSWORD, 'database': DB_NAME, 'charset': 'utf8mb4', 'cursorclass': pymysql.cursors.DictCursor, 'autocommit': True, # 自动提交事务 'maxconnections': 10, # 最大连接数 'mincached': 3, # 初始空闲连接数 'maxcached': 5, # 最大空闲连接数 'maxusage': 1000, # 单个连接最大使用次数 'ping': 7, # 使用ping检查连接:0=从不, 1=创建时, 2=获取时, 4=归还时, 7=总是 'blocking': True, # 连接池满时是否等待 'setsession': [], # 可选的会话命令列表 } # 请求配置(可通过环境变量 BATCH_SIZE 调大每轮处理量) REQUEST_TIMEOUT = 30 REQUEST_INTERVAL = 3 # 每次请求间隔秒数(每3秒请求一次) CONCURRENT_WORKERS = int(os.getenv("CONCURRENT_WORKERS", "1")) # 并发请求数(1=串行,每3秒一次) MAX_RETRIES = 3 RETRY_DELAY = 2 BATCH_SIZE = int(os.getenv("BATCH_SIZE", "100")) # 每轮拉取的饰品数量,调大可加快整体补齐 # ============================================================ # 配置日志 # ============================================================ def setup_logging(): """配置日志系统""" log_dir = os.path.dirname(os.path.abspath(__file__)) log_file = os.path.join(log_dir, "logs", f"sync_goods_attr_{datetime.now().strftime('%Y%m%d')}.log") os.makedirs(os.path.dirname(log_file), exist_ok=True) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler(log_file, encoding='utf-8') ] ) return logging.getLogger(__name__) logger = setup_logging() # 请求频率限制:每 REQUEST_INTERVAL 秒最多一次请求(多线程安全) _request_lock = threading.Lock() _last_request_time = [0.0] # 使用列表以便在闭包中修改 # ============================================================ # 数据库连接池操作类 # ============================================================ class DatabaseConnectionPool: """数据库连接池管理类""" _instance = None _pool = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._init_pool() return cls._instance def _init_pool(self): """初始化连接池""" try: self._pool = PooledDB(**DB_POOL_CONFIG) logger.info(f"数据库连接池初始化成功: {DB_HOST}:{DB_PORT}/{DB_NAME}") logger.info(f"连接池配置: maxconnections={DB_POOL_CONFIG['maxconnections']}, mincached={DB_POOL_CONFIG['mincached']}") except Exception as e: logger.error(f"数据库连接池初始化失败: {e}") raise def get_connection(self): """从连接池获取连接""" try: conn = self._pool.connection() return conn except Exception as e: logger.error(f"从连接池获取连接失败: {e}") # 尝试重新初始化连接池 time.sleep(2) self._init_pool() return self._pool.connection() def close_pool(self): """关闭连接池""" if self._pool: self._pool.close() logger.info("数据库连接池已关闭") class GoodsAttrDatabase: """饰品属性数据库操作类(使用连接池)""" def __init__(self): self.pool = DatabaseConnectionPool() def execute_query(self, sql: str, params=None, fetch_all=True): """执行查询""" conn = None cursor = None try: conn = self.pool.get_connection() cursor = conn.cursor() if params: cursor.execute(sql, params) else: cursor.execute(sql) if fetch_all: return cursor.fetchall() else: return cursor.fetchone() except pymysql.Error as e: logger.error(f"数据库查询失败: {e}, SQL: {sql}") raise finally: if cursor: cursor.close() # 注意:这里不需要关闭conn,连接池会自动管理 def execute_update(self, sql: str, params=None, many=False): """执行更新操作""" conn = None cursor = None try: conn = self.pool.get_connection() cursor = conn.cursor() if many and params: cursor.executemany(sql, params) elif params: cursor.execute(sql, params) else: cursor.execute(sql) affected_rows = cursor.rowcount return affected_rows except pymysql.Error as e: logger.error(f"数据库更新失败: {e}, SQL: {sql}") raise finally: if cursor: cursor.close() def execute_transaction(self, operations: List[Tuple[str, tuple]]): """执行事务(一组操作要么全部成功,要么全部回滚)""" conn = None cursor = None try: conn = self.pool.get_connection() cursor = conn.cursor() # 开始事务 conn.begin() for sql, params in operations: if params: cursor.execute(sql, params) else: cursor.execute(sql) # 提交事务 conn.commit() return True except Exception as e: logger.error(f"事务执行失败: {e}") if conn: try: conn.rollback() except: pass return False finally: if cursor: cursor.close() def get_goods_without_attr(self, limit: int = BATCH_SIZE) -> List[Dict[str, Any]]: """ 获取没有属性信息的饰品列表 从buff_goods_info表中找出在buff_goods_attr表中不存在的饰品 """ try: sql = """ SELECT g.goods_id, g.name, g.buff_url, g.stat_time FROM buff_goods_info g LEFT JOIN buff_goods_attr a ON g.goods_id = a.goods_id WHERE a.goods_id IS NULL ORDER BY g.create_time ASC LIMIT %s """ results = self.execute_query(sql, (limit,)) goods_list = [] for row in results: goods_list.append({ 'goods_id': row['goods_id'], 'name': row['name'], 'buff_url': row['buff_url'], 'stat_time': row['stat_time'] }) logger.info(f"找到 {len(goods_list)} 个没有属性信息的饰品") return goods_list except Exception as e: logger.error(f"查询没有属性信息的饰品失败: {e}") return [] def batch_insert_goods_attrs(self, attr_data_list: List[Dict[str, Any]]) -> int: """批量插入饰品属性信息""" if not attr_data_list: return 0 # 准备批量插入操作 operations = [] for attr_data in attr_data_list: # 删除已存在的记录 delete_sql = "DELETE FROM buff_goods_attr WHERE goods_id = %s" operations.append((delete_sql, (attr_data['goods_id'],))) # 插入新记录 insert_sql = """ INSERT INTO buff_goods_attr (goods_id, name, icon_url, type, type_name, category, category_name, category_group, category_group_name, rarity, rarity_name, quality, quality_name, exterior, exterior_name, weapon, weapon_name, series, series_name, custom, custom_name, itemset, itemset_name, extra_tag_ids, extra_tag_ids_name, custom_patch, custom_patch_name, custom_charm, custom_charm_name, stat_time, create_time, update_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) """ values = ( attr_data.get('goods_id'), attr_data.get('name', ''), attr_data.get('icon_url'), attr_data.get('type'), attr_data.get('type_name'), attr_data.get('category'), attr_data.get('category_name'), attr_data.get('category_group'), attr_data.get('category_group_name'), attr_data.get('rarity'), attr_data.get('rarity_name'), attr_data.get('quality'), attr_data.get('quality_name'), attr_data.get('exterior'), attr_data.get('exterior_name'), attr_data.get('weapon'), attr_data.get('weapon_name'), attr_data.get('series'), attr_data.get('series_name'), attr_data.get('custom'), attr_data.get('custom_name'), attr_data.get('itemset'), attr_data.get('itemset_name'), attr_data.get('extra_tag_ids'), attr_data.get('extra_tag_ids_name'), attr_data.get('custom_patch'), attr_data.get('custom_patch_name'), attr_data.get('custom_charm'), attr_data.get('custom_charm_name'), attr_data.get('stat_time', datetime.now()) ) operations.append((insert_sql, values)) try: # 执行事务 if self.execute_transaction(operations): logger.info(f"成功批量插入 {len(attr_data_list)} 个饰品的属性信息") return len(attr_data_list) else: logger.error("批量插入饰品属性信息失败") return 0 except Exception as e: logger.error(f"批量插入饰品属性信息异常: {e}") return 0 def insert_goods_attr(self, attr_data: Dict[str, Any]) -> bool: """插入单个饰品属性信息""" try: # 使用事务确保删除和插入的原子性 delete_sql = "DELETE FROM buff_goods_attr WHERE goods_id = %s" insert_sql = """ INSERT INTO buff_goods_attr (goods_id, name, icon_url, type, type_name, category, category_name, category_group, category_group_name, rarity, rarity_name, quality, quality_name, exterior, exterior_name, weapon, weapon_name, series, series_name, custom, custom_name, itemset, itemset_name, extra_tag_ids, extra_tag_ids_name, custom_patch, custom_patch_name, custom_charm, custom_charm_name, stat_time, create_time, update_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) """ values = ( attr_data.get('goods_id'), attr_data.get('name', ''), attr_data.get('icon_url'), attr_data.get('type'), attr_data.get('type_name'), attr_data.get('category'), attr_data.get('category_name'), attr_data.get('category_group'), attr_data.get('category_group_name'), attr_data.get('rarity'), attr_data.get('rarity_name'), attr_data.get('quality'), attr_data.get('quality_name'), attr_data.get('exterior'), attr_data.get('exterior_name'), attr_data.get('weapon'), attr_data.get('weapon_name'), attr_data.get('series'), attr_data.get('series_name'), attr_data.get('custom'), attr_data.get('custom_name'), attr_data.get('itemset'), attr_data.get('itemset_name'), attr_data.get('extra_tag_ids'), attr_data.get('extra_tag_ids_name'), attr_data.get('custom_patch'), attr_data.get('custom_patch_name'), attr_data.get('custom_charm'), attr_data.get('custom_charm_name'), attr_data.get('stat_time', datetime.now()) ) # 执行事务 operations = [ (delete_sql, (attr_data['goods_id'],)), (insert_sql, values) ] success = self.execute_transaction(operations) if success: logger.debug(f"成功插入饰品 {attr_data['goods_id']} 的属性信息") else: logger.error(f"插入饰品 {attr_data['goods_id']} 属性信息失败") return success except Exception as e: logger.error(f"插入饰品 {attr_data.get('goods_id', '未知')} 属性信息异常: {e}") return False def check_connection(self): """检查连接是否正常""" try: self.execute_query("SELECT 1") return True except Exception as e: logger.error(f"数据库连接检查失败: {e}") return False # ============================================================ # BUFF API 操作类 # ============================================================ class BuffGoodsAPI: """BUFF饰品API操作类(直连请求)""" def __init__(self): self.session = requests.Session() self.session.headers.update({ "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": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://buff.163.com/", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin" }) def fetch_goods_info(self, goods_id: str) -> Optional[Dict[str, Any]]: """获取单个饰品的详细信息""" url = f"{BUFF_API_BASE_URL}?game={GAME}&goods_id={goods_id}" for retry in range(MAX_RETRIES): try: logger.debug(f"请求饰品 {goods_id} (第{retry + 1}次尝试)...") # 每 REQUEST_INTERVAL 秒请求一次,多线程下串行控制 with _request_lock: elapsed = time.time() - _last_request_time[0] if elapsed < REQUEST_INTERVAL: time.sleep(REQUEST_INTERVAL - elapsed) _last_request_time[0] = time.time() # 使用 requests.get 避免多线程共享 Session,保证并发安全,直连请求 response = requests.get( url, headers=self.session.headers, timeout=REQUEST_TIMEOUT, ) response.raise_for_status() data = response.json() # 检查API响应状态 if data.get("code") != "OK": logger.warning(f"饰品 {goods_id} API返回错误: {data.get('msg', '未知错误')}") return None # 提取饰品信息 goods_info = self._extract_goods_info(data, goods_id) if goods_info: logger.info(f"成功获取饰品 {goods_id} 的属性信息") return goods_info else: logger.warning(f"饰品 {goods_id} 未找到属性信息") return None except requests.exceptions.RequestException as e: logger.error(f"请求饰品 {goods_id} 失败 (尝试 {retry + 1}/{MAX_RETRIES}): {e}") if retry < MAX_RETRIES - 1: time.sleep(RETRY_DELAY * (retry + 1)) else: logger.error(f"饰品 {goods_id} 请求失败,已重试{MAX_RETRIES}次") return None except json.JSONDecodeError as e: logger.error(f"饰品 {goods_id} 响应JSON解析失败: {e}") return None except Exception as e: logger.error(f"处理饰品 {goods_id} 时发生未知错误: {e}") return None return None def _extract_goods_info(self, api_data: Dict[str, Any], goods_id: str) -> Optional[Dict[str, Any]]: """从API响应中提取饰品属性信息""" try: data = api_data.get("data", {}) goods_infos = data.get("goods_infos", {}) # 获取指定goods_id的饰品信息 goods_data = goods_infos.get(str(goods_id)) if not goods_data: logger.warning(f"饰品 {goods_id} 未在响应中找到") return None # 提取tags信息 tags = goods_data.get("tags", {}) # 构建属性字典 attr_data = { 'goods_id': goods_id, 'name': goods_data.get("name", ""), 'icon_url': goods_data.get("icon_url"), 'stat_time': datetime.now(), 'create_time': datetime.now() } # 映射tags到对应字段 tag_mapping = { 'type': ('type', 'type_name'), 'rarity': ('rarity', 'rarity_name'), 'category': ('category', 'category_name'), 'category_group': ('category_group', 'category_group_name'), 'quality': ('quality', 'quality_name'), 'exterior': ('exterior', 'exterior_name'), 'weapon': ('weapon', 'weapon_name'), 'series': ('series', 'series_name'), 'custom': ('custom', 'custom_name'), 'itemset': ('itemset', 'itemset_name') } # 处理每个tag for tag_key, (field_key, field_name_key) in tag_mapping.items(): if tag_key in tags: tag_data = tags[tag_key] attr_data[field_key] = tag_data.get("internal_name") attr_data[field_name_key] = tag_data.get("localized_name") logger.debug(f"饰品 {goods_id} 属性提取成功") return attr_data except Exception as e: logger.error(f"提取饰品 {goods_id} 属性时出错: {e}") return None # ============================================================ # 主逻辑 # ============================================================ class GoodsAttrSyncTask: """饰品属性同步任务""" def __init__(self): self.db = GoodsAttrDatabase() self.api = BuffGoodsAPI() def fetch_and_save_goods_attrs(self, goods_ids: List[str]) -> Dict[str, int]: """获取并保存饰品属性信息(每次最多 CONCURRENT_WORKERS 个并发请求)""" results = {'success': 0, 'failed': 0, 'total': len(goods_ids)} batch_attr_data: List[Dict[str, Any]] = [] def fetch_one(goods_id: str) -> Optional[Dict[str, Any]]: try: return self.api.fetch_goods_info(goods_id) except Exception as e: logger.error(f"获取饰品 {goods_id} 属性异常: {e}") return None with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENT_WORKERS) as executor: future_to_id = {executor.submit(fetch_one, gid): gid for gid in goods_ids} for future in concurrent.futures.as_completed(future_to_id): goods_id = future_to_id[future] try: attr_data = future.result() if attr_data: batch_attr_data.append(attr_data) logger.info(f"获取饰品 {goods_id} 属性成功") else: results['failed'] += 1 logger.warning(f"获取饰品 {goods_id} 属性失败") except Exception as e: results['failed'] += 1 logger.error(f"获取饰品 {goods_id} 异常: {e}") if batch_attr_data: saved_count = self.db.batch_insert_goods_attrs(batch_attr_data) results['success'] += saved_count if saved_count < len(batch_attr_data): results['failed'] += len(batch_attr_data) - saved_count return results def run_single_batch(self) -> bool: """执行单次批次同步任务""" start_time = datetime.now() logger.info("=" * 60) logger.info(f"开始执行饰品属性同步任务: {start_time}") logger.info("=" * 60) try: # 检查数据库连接 if not self.db.check_connection(): logger.error("数据库连接不可用,任务中止") return False # Step 1: 获取需要同步的饰品列表 logger.info("[Step 1/3] 查询需要同步的饰品...") goods_list = self.db.get_goods_without_attr(BATCH_SIZE) if not goods_list: logger.info("没有需要同步的饰品,任务结束") return True logger.info(f"找到 {len(goods_list)} 个需要同步的饰品") # 提取goods_id列表 goods_ids = [goods['goods_id'] for goods in goods_list] # Step 2: 获取并保存饰品属性信息 logger.info("[Step 2/3] 获取并保存饰品属性信息...") results = self.fetch_and_save_goods_attrs(goods_ids) # Step 3: 任务完成统计 end_time = datetime.now() duration = (end_time - start_time).total_seconds() logger.info("=" * 60) logger.info(f"饰品属性同步任务完成!") logger.info(f"处理饰品总数: {results['total']}") logger.info(f"成功获取并保存: {results['success']}") logger.info(f"失败: {results['failed']}") logger.info(f"耗时: {duration:.2f}秒") logger.info("=" * 60) return results['success'] > 0 except Exception as e: logger.exception(f"同步任务失败: {e}") return False def run_continuous(self): """连续运行模式,每次执行后随机暂停""" execution_count = 0 while True: execution_count += 1 logger.info(f"\n{'='*60}") logger.info(f"开始第 {execution_count} 次执行") logger.info(f"{'='*60}") # 执行同步任务 success = self.run_single_batch() if success: logger.info(f"第 {execution_count} 次执行成功") else: logger.warning(f"第 {execution_count} 次执行失败") # 直接进入下一轮,不再等待 def cleanup(self): """清理资源""" try: # 关闭连接池 DatabaseConnectionPool().close_pool() logger.info("资源清理完成") except: pass # ============================================================ # 主函数 # ============================================================ def main(): """主函数""" task = None try: logger.info("启动饰品属性同步脚本(连接池版本)...") logger.info(f"配置信息: 每次处理 {BATCH_SIZE} 个饰品, 串行请求每 {REQUEST_INTERVAL} 秒一次, 直连请求") # 创建任务实例 task = GoodsAttrSyncTask() # 运行连续模式 task.run_continuous() except KeyboardInterrupt: logger.info("\n收到中断信号,正在退出...") if task: task.cleanup() sys.exit(0) except Exception as e: logger.exception(f"程序运行异常: {e}") if task: task.cleanup() sys.exit(1) if __name__ == "__main__": # 安装依赖提示 try: import dbutils except ImportError: print("请先安装依赖: pip install DBUtils==3.0.2") sys.exit(1) main()