Files
tvbox_collect/recha18/py/html/pansou.py
T
2026-07-11 04:45:19 +00:00

372 lines
13 KiB
Python

import sys
import json
import requests
import random
from datetime import datetime
from typing import List
from urllib.parse import urlparse, urlencode
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
sys.path.append('..')
from base.spider import Spider
class CloudSearchSpider(Spider):
"""网盘资源搜索爬虫"""
DEFAULT_BASE_URL = "https://so.252035.xyz"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Content-Type": "application/json"
}
SEARCH_PAGE_SIZE = 300
REQUEST_TIMEOUT = 30
MAX_RETRIES = 3
BACKOFF_FACTOR = 0.5
PAN_CONFIG = {
'ali': {
'api_type': 'aliyun',
'name': '阿里',
'keywords': ['alipan.com', 'aliyundrive.com'],
'icon': 'ali.png'
},
'quark': {
'api_type': 'quark',
'name': '夸克',
'keywords': ['pan.quark.cn'],
'icon': 'quark.png'
},
'uc': {
'api_type': 'uc',
'name': 'UC',
'keywords': ['drive.uc.cn'],
'icon': 'uc.png'
},
'xunlei': {
'api_type': 'xunlei',
'name': '迅雷',
'keywords': ['xunlei', 'thunder'],
'icon': 'xunlei.png'
},
'a123': {
'api_type': '123',
'name': '123',
'keywords': ['123684.com', '123685.com', '123912.com', '123pan.com', '123pan.cn', '123592.com'],
'icon': '123.png'
},
'a189': {
'api_type': 'tianyi',
'name': '天翼',
'keywords': ['cloud.189.cn'],
'icon': '189.png'
},
'a139': {
'api_type': 'mobile',
'name': '移动',
'keywords': ['caiyun.139.com'],
'icon': '139.png'
},
'a115': {
'api_type': '115',
'name': '115',
'keywords': ['115cdn.com', '115.com', 'anxia.com'],
'icon': '115.png'
},
'baidu': {
'api_type': 'baidu',
'name': '百度',
'keywords': ['baidu'],
'icon': 'baidu.png'
},
'pikpak': {
'api_type': 'pikpak',
'name': 'PikPak',
'keywords': ['pikpak'],
'icon': 'pikpak.png'
},
'magnet': {
'api_type': 'magnet',
'name': '磁力',
'keywords': ['magnet'],
'icon': 'cili.png'
},
'ed2k': {
'api_type': 'ed2k',
'name': '电驴',
'keywords': ['ed2k'],
'icon': ''
}
}
REVERSE_PAN_MAP = {v['api_type']: k for k, v in PAN_CONFIG.items()}
# 优化:从 PAN_CONFIG 自动派生网盘简称,消除数据冗余
PAN_SHORT_NAMES = {k: v['name'] for k, v in PAN_CONFIG.items()}
def __init__(self):
super().__init__()
self.base_url = self.DEFAULT_BASE_URL
self.proxy = ''
self.pan_priority = ''
self.pan_order = ''
self.channels = ''
self.plugins = ''
self.cloud_types = ''
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
# 增强重试策略
retries = Retry(
total=self.MAX_RETRIES,
backoff_factor=self.BACKOFF_FACTOR,
status_forcelist=[429, 500, 502, 503, 504],
raise_on_status=False
)
self.session.mount('http://', HTTPAdapter(max_retries=retries))
self.session.mount('https://', HTTPAdapter(max_retries=retries))
def init(self, extend):
try:
extend_dict = json.loads(extend) if extend else {}
self.base_url = extend_dict.get('server', self.DEFAULT_BASE_URL).rstrip('/')
self.proxy = extend_dict.get('proxy', '')
self.pan_priority = extend_dict.get('pan_priority', '')
self.pan_order = extend_dict.get('pan_order', '')
# 新增三个搜索过滤参数
self.channels = extend_dict.get('channels', '')
self.plugins = extend_dict.get('plugins', '')
self.cloud_types = extend_dict.get('cloud_types', '')
except json.JSONDecodeError:
self._reset_to_defaults()
# 代理协议分离
if self.proxy:
self.session.proxies = {
"http": self.proxy,
"https": self.proxy
}
def _reset_to_defaults(self):
self.base_url = self.DEFAULT_BASE_URL
self.proxy = ''
self.pan_priority = ''
self.pan_order = ''
self.channels = ''
self.plugins = ''
self.cloud_types = ''
def getName(self):
return "盘搜"
def homeContent(self, filter):
return {'class': [{"type_id": "1", "type_name": "盘搜|聚合搜索"}], 'list': []}
def homeVideoContent(self):
return {}
def categoryContent(self, cid, page, filter, ext):
return {
'list': [{"vod_id": "1", "vod_name": "请在搜索框中输入关键词搜索", "vod_pic": "", "vod_remarks": "盘搜"}],
'page': 1, 'pagecount': 1, 'limit': 1, 'total': 1
}
def detailContent(self, did):
result = {'list': []}
if not did or not did[0]:
return result
resource_url = did[0]
pan_type = self._extract_pan_type_from_url(resource_url)
pan_config = self.PAN_CONFIG.get(pan_type, {})
pan_name = pan_config.get('name', '网盘资源')
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
result['list'].append({
"vod_id": resource_url,
"vod_name": f"{pan_name}资源",
"vod_pic": pan_icon,
"vod_play_from": "盘搜",
"vod_play_url": f"盘搜${resource_url}",
"vod_content": f"网盘类型: {pan_name}\n资源链接: {resource_url}"
})
return result
def searchContent(self, key, quick, pg="1"):
return self._perform_search(key, pg)
def searchContentPage(self, key, quick, page):
return self._perform_search(key, page)
def _perform_search(self, keywords, page_str):
try:
page = int(page_str)
except (ValueError, TypeError):
page = 1
result = {'list': [], 'page': page, 'pagecount': 1, 'limit': self.SEARCH_PAGE_SIZE, 'total': 0}
if not keywords:
return result
try:
# 构建查询参数
params = {'kw': keywords}
if self.channels:
params['channels'] = self.channels
if self.plugins:
params['plugins'] = self.plugins
if self.cloud_types:
params['cloud_types'] = self.cloud_types
url = f"{self.base_url}/api/search"
search_response = self.session.get(
url,
params=params,
timeout=self.REQUEST_TIMEOUT
)
search_response.raise_for_status()
search_data = search_response.json()
if search_data.get('code') != 0:
return result
all_results = self._parse_and_sort_results(search_data, keywords)
total_count = len(all_results)
start_index = (page - 1) * self.SEARCH_PAGE_SIZE
paged_results = all_results[start_index:start_index + self.SEARCH_PAGE_SIZE]
result.update({
'list': paged_results,
'total': total_count,
'pagecount': max(1, (total_count + self.SEARCH_PAGE_SIZE - 1) // self.SEARCH_PAGE_SIZE)
})
except requests.RequestException:
pass
except Exception:
pass
return result
def _parse_and_sort_results(self, data, keywords):
# 优化:移除此处的硬编码字典,它已在类级别定义
if self.pan_order:
enabled_pan_types = [pan.strip() for pan in self.pan_order.split(',') if pan.strip()]
use_pan_order = True
elif self.pan_priority:
enabled_pan_types = [pan.strip() for pan in self.pan_priority.split(',') if pan.strip()]
use_pan_order = False
else:
enabled_pan_types = []
use_pan_order = False
all_items = []
all_images = []
# 收集所有图片用于兜底随机
for items in data.get('data', {}).get('merged_by_type', {}).values():
for item in items:
if item.get('images'):
all_images.extend(item['images'])
# 设置随机种子,确保同关键词结果一致
random.seed(hash(keywords))
for cloud_type, items in data.get('data', {}).get('merged_by_type', {}).items():
pan_type = self.REVERSE_PAN_MAP.get(cloud_type, cloud_type)
if enabled_pan_types and pan_type not in enabled_pan_types:
continue
for item in items:
url = item.get('url')
if not url:
continue
pan_config = self.PAN_CONFIG.get(pan_type, {})
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
# 优化:使用 self.PAN_SHORT_NAMES 替代本地字典
pan_short = self.PAN_SHORT_NAMES.get(pan_type, '网盘')
# 简化图片逻辑:优先自带 → 全局随机 → 图标
vod_pic = self._get_best_image(item, all_images, pan_icon)
dt_obj = self._to_datetime(item.get('datetime'))
time_str = dt_obj.strftime("%m-%d") if dt_obj else ""
source = item.get('source', '盘搜')
remarks = f"{pan_short}:{time_str}|{source}" if time_str else f"{pan_short}|{source}"
all_items.append({
"vod_id": url,
"vod_name": item.get('note', ''),
"vod_pic": vod_pic,
"vod_remarks": remarks,
"_timestamp": dt_obj.timestamp() if dt_obj else 0,
"_pan_type": pan_type
})
# 排序逻辑
if use_pan_order:
all_items.sort(key=lambda x: -x['_timestamp'])
elif enabled_pan_types:
pan_priority_order = {pan: idx for idx, pan in enumerate(enabled_pan_types)}
all_items.sort(key=lambda x: (pan_priority_order.get(x['_pan_type'], len(enabled_pan_types)), -x['_timestamp']))
else:
all_items.sort(key=lambda x: -x['_timestamp'])
# 清理临时字段
for item in all_items:
item.pop('_timestamp', None)
item.pop('_pan_type', None)
return all_items
def _get_best_image(self, item, all_images, pan_icon):
if item.get('images') and item['images']:
return item['images'][0]
if all_images:
return random.choice(all_images)
return pan_icon
def playerContent(self, flag, pid, vipFlags):
result = {"parse": 0, "header": self.HEADERS, "url": ""}
if not pid:
return result
url = pid.strip()
if not url.startswith('push:'):
if not url.startswith(('http://', 'https://')):
url = f'https://{url}'
result['url'] = f"push:{url}"
else:
result['url'] = url
return result
def _to_datetime(self, time_str):
if not time_str or time_str == "0001-01-01T00:00:00Z":
return None
try:
time_str_clean = time_str.replace('Z', '+00:00')
return datetime.fromisoformat(time_str_clean)
except (ValueError, TypeError):
try:
return datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError):
return None
def _extract_pan_type_from_url(self, url):
if not url:
return "unknown"
url_lower = urlparse(url).netloc.lower() or url.lower()
for pan_type, config in self.PAN_CONFIG.items():
if any(keyword in url_lower for keyword in config['keywords']):
return pan_type
return "unknown"
def _get_icon_url(self, icon_name):
if not icon_name:
return ""
return f"http://127.0.0.1:9978/file/Download/lib/icon/{icon_name}"
Spider = CloudSearchSpider