Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-20 06:50:50 +00:00
parent c349d619a1
commit a206ad21a8
68 changed files with 9257 additions and 6201 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+591
View File
@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ztzssz.com 爬虫 - TVBox/影视仓 Spider 插件
支持分类浏览、筛选(类型/地区/语言/年份/字母)、搜索、详情获取、播放链接解析
选集正序排列,海报封面补充
"""
import re
import json
import logging
import urllib.parse
import os
import sys
import requests
from bs4 import BeautifulSoup
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
from base.spider import Spider as BaseSpider
except ImportError:
BaseSpider = object
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Spider(BaseSpider):
"""ztzssz.com 爬虫"""
BASE_URL = "https://www.ztzssz.com"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-S908U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://www.ztzssz.com/",
}
# 分类:键为网站分类ID,与 /vodtype/{id}.html 对应
CATEGORY_MAP = {
"1": {"name": "电影", "url": "/vodtype/1.html"},
"2": {"name": "电视剧", "url": "/vodtype/2.html"},
"3": {"name": "综艺", "url": "/vodtype/3.html"},
"4": {"name": "动漫", "url": "/vodtype/4.html"},
"20": {"name": "短剧", "url": "/vodtype/20.html"},
"35": {"name": "动画片", "url": "/vodtype/35.html"},
"36": {"name": "4K电影", "url": "/vodtype/36.html"},
"37": {"name": "Netflix作品", "url": "/vodtype/37.html"},
}
def __init__(self):
try:
super().__init__()
except Exception:
pass
self.session = requests.Session()
self.session.verify = False
self.session.headers.update(self.HEADERS)
def init(self, extend):
pass
def getName(self):
return "ztzssz影视"
def _parse_ext(self, ext):
"""解析ext参数,兼容dict和JSON字符串"""
if not ext:
return {}
if isinstance(ext, dict):
return ext
if isinstance(ext, str):
try:
return json.loads(ext)
except Exception:
return {}
return {}
def _get(self, url):
try:
resp = self.session.get(url, timeout=30)
resp.encoding = "utf-8"
return resp
except Exception as e:
logger.error(f"请求失败 {url}: {e}")
return None
# ==================== 首页 ====================
def homeContent(self, filter=False):
try:
url = f"{self.BASE_URL}/"
resp = self._get(url)
if not resp:
return {}
classes = [{"type_id": cid, "type_name": info["name"]}
for cid, info in self.CATEGORY_MAP.items()]
home_list = self._parse_video_list(resp.text)
return {
"class": classes,
"filters": self._get_filters(),
"list": home_list,
}
except Exception as e:
logger.error(f"获取首页失败: {e}")
return {}
def homeVideoContent(self):
home = self.homeContent()
return {"list": home.get("list", [])}
# ==================== 筛选 ====================
def _get_filters(self):
"""筛选配置:类型/地区/语言/年份/字母
网站筛选URL为中文值(如 喜剧/大陆/国语),故筛选value直接用中文。
"""
filters = {}
# 类型:按分类区分(电影/电视剧/动漫等类型不同),这里取较通用的集合
type_values_common = [
{"n": "全部", "v": ""},
{"n": "动作", "v": "动作"}, {"n": "喜剧", "v": "喜剧"},
{"n": "爱情", "v": "爱情"}, {"n": "科幻", "v": "科幻"},
{"n": "恐怖", "v": "恐怖"}, {"n": "剧情", "v": "剧情"},
{"n": "战争", "v": "战争"}, {"n": "警匪", "v": "警匪"},
{"n": "犯罪", "v": "犯罪"}, {"n": "动画", "v": "动画"},
{"n": "奇幻", "v": "奇幻"}, {"n": "武侠", "v": "武侠"},
{"n": "冒险", "v": "冒险"}, {"n": "枪战", "v": "枪战"},
{"n": "悬疑", "v": "悬疑"}, {"n": "惊悚", "v": "惊悚"},
{"n": "经典", "v": "经典"}, {"n": "青春", "v": "青春"},
{"n": "文艺", "v": "文艺"}, {"n": "古装", "v": "古装"},
{"n": "历史", "v": "历史"}, {"n": "运动", "v": "运动"},
{"n": "农村", "v": "农村"}, {"n": "儿童", "v": "儿童"},
{"n": "网络电影", "v": "网络电影"},
]
# 电视剧/综艺/动漫常用类型
type_values_series = [
{"n": "全部", "v": ""},
{"n": "古装", "v": "古装"}, {"n": "战争", "v": "战争"},
{"n": "青春偶像", "v": "青春偶像"}, {"n": "喜剧", "v": "喜剧"},
{"n": "家庭", "v": "家庭"}, {"n": "犯罪", "v": "犯罪"},
{"n": "动作", "v": "动作"}, {"n": "奇幻", "v": "奇幻"},
{"n": "剧情", "v": "剧情"}, {"n": "历史", "v": "历史"},
{"n": "经典", "v": "经典"}, {"n": "乡村", "v": "乡村"},
{"n": "情景", "v": "情景"}, {"n": "商战", "v": "商战"},
{"n": "网剧", "v": "网剧"}, {"n": "其他", "v": "其他"},
]
area_values = [
{"n": "全部", "v": ""},
{"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"},
{"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
{"n": "日本", "v": "日本"}, {"n": "韩国", "v": "韩国"},
{"n": "德国", "v": "德国"}, {"n": "泰国", "v": "泰国"},
{"n": "印度", "v": "印度"}, {"n": "意大利", "v": "意大利"},
{"n": "西班牙", "v": "西班牙"}, {"n": "加拿大", "v": "加拿大"},
{"n": "其他", "v": "其他"},
]
lang_values = [
{"n": "全部", "v": ""},
{"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"},
{"n": "韩语", "v": "韩语"}, {"n": "日语", "v": "日语"},
{"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
{"n": "其它", "v": "其它"},
]
year_values = [{"n": "全部", "v": ""}]
for y in range(2026, 1999, -1):
year_values.append({"n": str(y), "v": str(y)})
letter_values = [{"n": "全部", "v": ""}]
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
letter_values.append({"n": letter, "v": letter})
letter_values.append({"n": "其他", "v": "0"})
for cate_id in self.CATEGORY_MAP:
if cate_id in ("2", "3", "4", "20"):
tv = type_values_series
else:
tv = type_values_common
filters[cate_id] = [
{"key": "type", "name": "类型", "value": tv},
{"key": "area", "name": "地区", "value": area_values},
{"key": "lang", "name": "语言", "value": lang_values},
{"key": "year", "name": "年份", "value": year_values},
{"key": "letter", "name": "字母", "value": letter_values},
]
return filters
# ==================== 分类 ====================
def categoryContent(self, tid, pg, filter, ext):
try:
page = int(pg) if pg else 1
type_id = str(tid)
cate_info = self.CATEGORY_MAP.get(type_id)
if not cate_info:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
ext_dict = self._parse_ext(ext)
type_filter = ext_dict.get('type', '')
lang_filter = ext_dict.get('lang', '')
year_filter = ext_dict.get('year', '')
letter_filter = ext_dict.get('letter', '')
area_filter = ext_dict.get('area', '')
has_filter = any([type_filter, lang_filter, year_filter, letter_filter, area_filter])
if has_filter:
# 筛选URL: /vodshow/{cate_id}-{area}--{type}-{lang}-{letter}---{page}---{year}.html
# 共12段: [cate_id, area, '', type, lang, letter, '', '', page, '', '', year]
seg_page = str(page) if page > 1 else ''
segs = [
type_id, area_filter, '', type_filter, lang_filter,
letter_filter, '', '', seg_page, '', '', year_filter
]
url = f"{self.BASE_URL}/vodshow/{'-'.join(segs)}.html"
else:
# 无筛选: /vodtype/{id}.html,分页 /vodtype/{id}-{page}.html
url = self.BASE_URL + cate_info["url"]
if page > 1:
url = url.replace('.html', f'-{page}.html')
resp = self._get(url)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
videos = self._parse_video_list(resp.text)
pagecount = self._parse_total_pages(resp.text)
return {
"list": videos,
"page": page,
"pagecount": pagecount,
"limit": 20,
"total": len(videos) * pagecount if pagecount else len(videos),
}
except Exception as e:
logger.error(f"获取分类内容失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
def _parse_total_pages(self, html):
"""解析总页数,格式: <span class="num">1/1213</span>"""
patterns = [
r'class="num"[^>]*>\s*(\d+)\s*/\s*(\d+)',
r'(\d+)\s*/\s*(\d+)\s*</span>',
r'/vodtype/\d+-(\d+)\.html["\'][^>]*>尾页',
r'/vodshow/[^"\'-]+-(\d+)---\.html["\'][^>]*>尾页',
]
for pattern in patterns:
match = re.search(pattern, html)
if match:
return int(match.group(2) if match.lastindex and match.lastindex >= 2 else match.group(1))
return 1
def _parse_video_list(self, html):
"""解析视频列表(首页/分类/筛选/搜索通用)
卡片: a.ewave-vodlist__thumb (含 data-original, title, href) 或 div.ewave-vodlist__thumb > a.thumb-link
"""
videos = []
soup = BeautifulSoup(html, 'html.parser')
# 兼容两种结构:a.vodlist__thumb 自带链接,或 div.vodlist__thumb 内含 a.thumb-link
items = soup.find_all('a', class_=re.compile(r'vodlist__thumb'))
if not items:
items = soup.find_all('div', class_=re.compile(r'vodlist__thumb'))
seen_ids = set()
for item in items:
href = item.get('href', '')
if not href:
a_inner = item.find('a', href=True)
href = a_inner.get('href', '') if a_inner else ''
vid_match = re.search(r'/voddetail/(\d+)\.html', href)
if not vid_match:
continue
vid_id = vid_match.group(1)
if vid_id in seen_ids:
continue
seen_ids.add(vid_id)
title = item.get('title', '')
poster = item.get('data-original', '')
if not poster:
img = item.find('img')
if img:
poster = img.get('data-original', '') or img.get('src', '')
if not title:
img = item.find('img')
if img:
title = img.get('alt', '') or item.get('title', '')
remark_tag = item.find(class_=re.compile(r'pic-text|pic_tag'))
remarks = remark_tag.get_text(strip=True) if remark_tag else ''
if title:
videos.append({
"vod_id": vid_id,
"vod_name": title,
"vod_pic": poster,
"vod_remarks": remarks,
})
return videos
# ==================== 详情 ====================
def detailContent(self, ids):
try:
vod_id = ids[0] if isinstance(ids, list) else str(ids)
url = f"{self.BASE_URL}/voddetail/{vod_id}.html"
resp = self._get(url)
if not resp:
return {"list": []}
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
# 标题: h1.title 内第一个 span(去除评分)
title = ''
h1 = soup.find('h1', class_=re.compile(r'title'))
if h1:
span = h1.find('span')
title = span.get_text(strip=True) if span else h1.get_text(strip=True)
if not title:
h1 = soup.find('h1')
if h1:
title = h1.get_text(strip=True)
# 海报: div.ewave-content__thumb 内 img[data-original]
poster = ''
thumb_box = soup.find(class_=re.compile(r'content__thumb|vodlist__thumb'))
if thumb_box:
img = thumb_box.find('img')
if img:
poster = img.get('data-original', '') or img.get('src', '')
# 信息: p.data 列表,一个p内可能含多个 label(类型/地区/年份用 span.text-muted 分隔)
year = area = type_name = actor = director = content = remarks = ''
data_ps = soup.find_all('p', class_=re.compile(r'data'))
for p in data_ps:
# 收集该p下所有 label span 及其后续文本,按 label 分组
labels = p.find_all('span', class_=re.compile(r'text-muted|left'))
for idx, label_tag in enumerate(labels):
label = label_tag.get_text(strip=True)
# 该label之后、下一个label之前的所有文本
nxt = labels[idx + 1] if idx + 1 < len(labels) else None
val = ''
for sib in label_tag.next_siblings:
if sib is nxt:
break
if hasattr(sib, 'get_text'):
t = sib.get_text(strip=True)
else:
t = str(sib).strip()
if t:
val += t
val = val.strip()
if label.startswith('类型'):
type_name = val
elif label.startswith('地区'):
area = val
elif label.startswith('年份'):
year_match = re.search(r'(\d{4})', val)
year = year_match.group(1) if year_match else val
elif label.startswith('主演'):
actor = val
elif label.startswith('导演'):
director = val
elif label.startswith('更新'):
remarks = val
# 简介: p.desc
desc_tag = soup.find('p', class_=re.compile(r'desc|content__desc'))
if desc_tag:
content = desc_tag.get_text(strip=True)
content = re.sub(r'详情\s*$', '', content).strip()
# 播放源和集数(正序)
play_from_list, play_url_list = self._parse_play_sources(html, vod_id)
vod_item = {
"vod_id": vod_id,
"vod_name": title,
"vod_pic": poster,
"type_name": type_name,
"vod_year": year,
"vod_area": area,
"vod_remarks": remarks,
"vod_actor": actor,
"vod_director": director,
"vod_content": content,
"vod_play_from": '$$$'.join(play_from_list),
"vod_play_url": '$$$'.join(play_url_list),
}
return {"list": [vod_item]}
except Exception as e:
logger.error(f"获取详情失败: {e}")
return {"list": []}
def _parse_play_sources(self, html, vod_id):
"""解析播放源和集数 - 正序排列
结构: ul.nav-tabs > li > a[href="#playlist{sid}"] (源名)
div.tab-pane#playlist{sid} > ul > a[href="/vodplay/{vid}-{sid}-{nid}.html"] (集数)
"""
play_from_list = []
play_url_list = []
soup = BeautifulSoup(html, 'html.parser')
# 源名映射: {sid: 源名}
source_names = {}
nav = soup.find('ul', class_=re.compile(r'nav-tabs'))
if nav:
for a in nav.find_all('a', href=True):
m = re.search(r'#playlist(\w+)', a.get('href', ''))
if m:
source_names[m.group(1)] = a.get_text(strip=True)
# 每个 tab-pane 为一个源
panes = soup.find_all('div', class_=re.compile(r'tab-pane'))
if not panes:
# 兜底:直接按 play 链接分组
return self._parse_play_sources_fallback(html, vod_id)
for pane in panes:
pane_id = pane.get('id', '')
sid_match = re.search(r'playlist(\w+)', pane_id)
if not sid_match:
continue
sid = sid_match.group(1)
from_name = source_names.get(sid, f"线路{sid}")
ul = pane.find('ul')
if not ul:
continue
episodes = []
seen_nid = set()
for a in ul.find_all('a', href=re.compile(r'/vodplay/')):
href = a.get('href', '')
m = re.search(r'/vodplay/(\d+)-(\w+)-(\d+)\.html', href)
if not m:
continue
nid = int(m.group(3))
if nid in seen_nid:
continue
seen_nid.add(nid)
ep_name = a.get_text(strip=True) or f"{nid}"
full_url = self.BASE_URL + href
episodes.append((nid, ep_name, full_url))
if not episodes:
continue
# 正序排列
episodes = sorted(episodes, key=lambda x: x[0])
play_from_list.append(from_name)
urls = [f"{name}${u}" for _, name, u in episodes]
play_url_list.append('#'.join(urls))
if not play_from_list:
return self._parse_play_sources_fallback(html, vod_id)
return play_from_list, play_url_list
def _parse_play_sources_fallback(self, html, vod_id):
"""兜底:从所有 play 链接按 sid 分组"""
play_from_list = []
play_url_list = []
links = re.findall(r'/vodplay/\d+-\w+-\d+\.html', html)
sources = {}
for link in links:
m = re.match(r'/vodplay/(\d+)-(\w+)-(\d+)\.html', link)
if not m:
continue
sid = m.group(2)
nid = int(m.group(3))
sources.setdefault(sid, {})[nid] = link
for sid in sorted(sources.keys()):
eps = sources[sid]
episodes = sorted(eps.items())
play_from_list.append(f"线路{sid}")
urls = [f"{nid}集${self.BASE_URL}{link}" for nid, link in episodes]
play_url_list.append('#'.join(urls))
return play_from_list, play_url_list
# ==================== 播放 ====================
def playerContent(self, flag, id, vipFlags):
"""解析播放页 m3u8 链接"""
try:
url = id
# 兼容相对路径
if url.startswith('/'):
url = self.BASE_URL + url
elif not url.startswith('http'):
url = self.BASE_URL + '/vodplay/' + url
resp = self._get(url)
if not resp:
return {}
m3u8_url = self._extract_m3u8(resp.text)
if not m3u8_url:
return {}
return {
"parse": 0,
"playUrl": "",
"url": m3u8_url,
"header": "",
}
except Exception as e:
logger.error(f"解析播放失败: {e}")
return {}
def _extract_m3u8(self, html):
"""从播放页提取 m3u8 链接"""
match = re.search(r'player_aaaa\s*=\s*({[^<]+})', html)
if match:
try:
data = json.loads(match.group(1))
m3u8_url = data.get('url', '')
if m3u8_url:
return m3u8_url
except Exception:
pass
# 兜底:直接匹配 m3u8
m = re.search(r'(https?://[^"\'\\s]+\.m3u8[^"\'\\s]*)', html)
return m.group(1) if m else ''
# ==================== 搜索 ====================
def searchContent(self, key, quick, pg="1"):
"""搜索内容 - TVBox标准接口(key, quick, pg)
优先使用AJAX接口(JSON格式,速度快),失败则回退到HTML搜索页
"""
try:
page = int(pg) if pg else 1
encoded_key = urllib.parse.quote(key)
# 优先尝试 AJAX 建议接口(返回JSON,数据干净)
ajax_url = f"{self.BASE_URL}/index.php/ajax/suggest?mid=1&wd={encoded_key}"
resp = self._get(ajax_url)
if resp and resp.headers.get('content-type', '').find('json') >= 0:
try:
data = resp.json()
if data.get('code') == 1 and data.get('list'):
videos = []
for item in data['list']:
videos.append({
"vod_id": str(item.get('id', '')),
"vod_name": item.get('name', ''),
"vod_pic": item.get('pic', ''),
"vod_remarks": item.get('note', ''),
})
return {
"list": videos,
"page": data.get('page', page),
"pagecount": data.get('pagecount', 1),
"limit": data.get('limit', 20),
"total": data.get('total', len(videos)),
}
except Exception:
pass
# 回退到HTML搜索页
url = f"{self.BASE_URL}/vodsearch/-------------.html?wd={encoded_key}"
if page > 1:
url += f"&page={page}"
resp = self._get(url)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
if any(k in resp.text for k in ['验证码', '人机验证', '安全验证', 'just_a_test']):
logger.warning("搜索被安全验证拦截")
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
videos = self._parse_video_list(resp.text)
pagecount = self._parse_total_pages(resp.text)
return {
"list": videos,
"page": page,
"pagecount": pagecount if pagecount > 1 else 1,
"limit": 20,
"total": len(videos) * pagecount if pagecount > 1 else len(videos),
}
except Exception as e:
logger.error(f"搜索失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
+259
View File
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
from urllib.parse import urljoin, quote
sys.path.append('..')
try:
from base.spider import Spider
except ImportError:
class Spider:
def fetch(self, url, headers=None, **kw):
import requests as rq
kw.pop('timeout', None)
r = rq.get(url, headers=headers, timeout=15, **kw)
r.encoding = 'utf-8'
return r
HOST = "https://gqc.ink"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
CATEGORIES = {
"dianying": "电影", "lianxuju": "连续剧", "zongyi": "综艺",
"dongman": "动漫", "duanju": "短剧",
}
class Spider(Spider):
def init(self, extend=""):
global HOST
try:
r = self.fetch(HOST, headers={"User-Agent": UA}, timeout=15000)
if hasattr(r, 'url') and r.url and r.url != HOST.rstrip("/"):
HOST = r.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": []}
for k, v in CATEGORIES.items():
r["class"].append({"type_id": k, "type_name": v})
return r
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cat = str(tid)
if cat not in CATEGORIES:
cat = "dianying"
slug = cat
try:
if pn > 1:
url = f"{HOST}/vodshow/{slug}--------{pn}---.html"
else:
url = f"{HOST}/vodshow/{slug}--------1---.html"
r = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
html = r.text if hasattr(r, 'text') else str(r)
items = self._items(html)
return {"page": pn, "pagecount": self._pagecount(html), "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
if isinstance(ids, list):
vid = ids[0] if ids else ""
else:
vid = str(ids) if ids else ""
m = re.search(r'(\d+)', str(vid))
vid = m.group(1) if m else ""
if not vid:
return {"list": []}
try:
r = self.fetch(f"{HOST}/neirong/{vid}.html", headers={"User-Agent": UA}, timeout=30000)
h = r.text if hasattr(r, 'text') else str(r)
except:
return {"list": []}
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h, re.S)
if t1:
clean = re.sub(r'<[^>]+>', ' ', t1.group(1)).strip()
d["vod_name"] = clean.split('\n')[0].strip()
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if not p:
p = re.search(r'data-background="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if not p:
p = re.search(r'<img[^>]*src="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if p: d["vod_pic"] = p.group(1)
desc_m = re.search(r'class="fed-part-esan[^"]*"[\s\S]*?<span[^>]*>简介[:]?</span>\s*&nbsp;.*?(?:<a[^>]*>[\s\S]*?</a>)\s*([^<]+)', h)
if not desc_m:
desc_m = re.search(r'class="fed-part-esan[^"]*"[\s\S]*?<span[^>]*>简介[:]?</span>\s*&nbsp;\s*([^<]+)', h)
if desc_m:
desc = re.sub(r'\s+', ' ', desc_m.group(1)).strip()[:500]
d["vod_content"] = desc
for mi in re.finditer(r'class="fed-deta-info[^"]*"[\s\S]*?>(.*?)</li>', h, re.S):
t = re.sub(r'<[^>]+>', '', mi.group(1)).strip()
if "导演" in t: d["vod_director"] = t.split("")[-1].split(":")[-1].strip()
elif "主演" in t: d["vod_actor"] = t.split("")[-1].split(":")[-1].strip()
elif "年份" in t or "上映" in t:
ym = re.search(r'(\d{4})', t)
if ym: d["vod_year"] = ym.group(1)
elif "备注" in t or "更新" in t: d["vod_remarks"] = t
try:
pf, pu = [], []
tops = re.search(r'fed-drop-tops(.*?)fed-drop-btms', h, re.S)
route_buttons = []
if tops:
for bm in re.finditer(r'href="/bofang/\d+-(\d+)-1\.html"[\s\S]*?>(.*?)<span[^>]*>(\d+)<', tops.group(1), re.S):
route = bm.group(1)
name = re.sub(r'<[^>]+>', '', bm.group(2)).strip()
count = int(bm.group(3))
if name and count > 0:
route_buttons.append((route, name, count))
vis = re.search(r'fed-play-item fed-drop-item fed-visible(.*?)</ul>\s*</div>', h, re.S)
default_eps = []
default_route = None
if vis:
for em in re.finditer(r'href="/bofang/\d+-(\d+)-(\d+)\.html"[\s\S]*?>([^<]+)<', vis.group(1)):
r2, nid, name = em.group(1), em.group(2), em.group(3).strip()
if not default_route:
default_route = r2
default_eps.append((int(nid), name))
if not route_buttons:
items = re.findall(r'href="(/bofang/\d+-\d+-\d+\.html)"[\s\S]*?>([^<]+)<', h)
routes = {}
for href, name in items:
name = name.strip()
if not name or "报错" in name or "剧情" in name or "简介" in name or "立即播放" in name:
continue
rm = re.search(r'/bofang/\d+-(\d+)-(\d+)\.html', href)
if rm:
rt, nid = rm.group(1), rm.group(2)
if rt not in routes:
routes[rt] = []
routes[rt].append(f"{name}${urljoin(HOST, href)}")
for rt in sorted(routes):
pf.append(f"{len(pf)+1}")
pu.append("#".join(routes[rt]))
else:
for route, name, count in route_buttons:
eps_list = []
if default_route == route and default_eps:
for nid, ename in default_eps:
eps_list.append(f"{ename}${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
else:
if default_eps:
for nid, ename in default_eps:
if nid <= count:
eps_list.append(f"{ename}${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
else:
for nid in range(1, count + 1):
eps_list.append(f"{nid}集${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
if eps_list:
pf.append(name)
pu.append("#".join(eps_list))
if pf:
d["vod_play_from"] = "$$$".join(pf)
d["vod_play_url"] = "$$$".join(pu)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
a, b = str(flag), str(id) if id else ""
if a.startswith("http") or "/bofang/" in a:
url = a
elif b.startswith("http") or "/bofang/" in b:
url = b
elif a.startswith("/"):
url = urljoin(HOST, a)
elif b.startswith("/"):
url = urljoin(HOST, b)
else:
url = a
try:
r = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
h = r.text if hasattr(r, 'text') else str(r)
except:
return {"url": ""}
dm = re.search(r'"dmId"\s*:\s*"([^"]+)"', h)
if dm:
u = dm.group(1)
if u.startswith("http"):
if ".m3u8" in u:
return {"url": u}
return {"url": u}
m3u8 = re.search(r'(https?://[^\s"\'<>]+\.m3u8)', h)
if m3u8:
return {"url": m3u8.group(1)}
return {"url": ""}
def localProxy(self, param):
pass
def _pagecount(self, html):
pc = 1
# 优先用 data-total 属性 (fed模板分页跳转按钮)
dt = re.findall(r'data-total="(\d+)"', html)
for t in dt:
try: pc = max(pc, int(t))
except: pass
# /vodshow/ 格式分页: href="/vodshow/dianying--------2---.html"
pages_vod = re.findall(r'href="/vodshow/[^"]*-(\d+)-*--*\.html"', html)
for p in pages_vod:
try:
n = int(p)
if n > 1 and n < 10000:
pc = max(pc, n)
except: pass
# /fenlei/ 格式分页 (兼容旧格式)
pages_fen = re.findall(r'href="/fenlei/[^"]*-?(\d+)\.html"', html)
for p in pages_fen:
try:
n = int(p)
if n > 1 and n < 10000:
pc = max(pc, n)
except: pass
return pc
def _items(self, html):
items, seen = [], set()
# 只取 data-original 的列表项,不取 data-background 的轮播项
# 轮播项标题和封面不在同一个结构里,混入会导致错位
cover_map = {}
for m in re.finditer(r'data-original="(https?://[^"]+)"', html, re.I):
# 找到 data-original 前面最近的 href (取最后一个,不是第一个)
before = html[max(0, m.start()-500):m.start()]
hms = re.findall(r'href="(/neirong/(\d+)\.html)"', before)
if hms:
hm = hms[-1]
vid = hm[1]
if vid not in cover_map:
cover_map[vid] = (hm[0], m.group(1))
# 标题: fed-list-title
titles = {}
for tm in re.finditer(r'class="[^"]*fed-list-title[^"]*"[\s\S]*?href="/neirong/(\d+)\.html"[\s\S]*?>\s*(.*?)\s*</a>', html, re.S):
name = re.sub(r'<[^>]+>', '', tm.group(2)).strip()
if name:
titles[tm.group(1)] = name[:50]
# 合并: 只用列表项的封面
for vid, (href, pic) in cover_map.items():
if vid in seen:
continue
seen.add(vid)
items.append({
"vod_id": vid, "vod_name": titles.get(vid, ""),
"vod_pic": pic, "vod_remarks": "", "vod_url": urljoin(HOST, href),
})
return items
+408
View File
@@ -0,0 +1,408 @@
#coding=utf-8
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TVBox / 影视仓 Python源脚本
站点: 可可影视 (103.51.147.112:51120)
"""
import sys
import re
import json
import requests
from urllib.parse import quote
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.site = 'https://103.51.147.112:51120'
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',
'Referer': 'https://103.51.147.112:51120/'
})
self.cateManual = {
'电影': '1',
'连续剧': '2',
'动漫': '3',
'综艺纪录': '4',
'短剧': '6'
}
# 隐晦的站点标识
self._mark = chr(26143) + chr(27827)
def init(self, extend=""):
pass
def getName(self):
return "可可影视"
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
for k, v in self.cateManual.items():
result['class'].append({
'type_id': str(v),
'type_name': k
})
return result
def homeVideoContent(self):
videos = []
try:
url = f'{self.site}/channel/1.html'
r = self.session.get(url, timeout=15, verify=False)
r.encoding = 'utf-8'
doc = pq(r.text)
items = doc('.module-item')
seen = set()
for item in items.items():
a = item.find('.v-item')
href = a.attr('href') or ''
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
# 标题
titles = item.find('.v-item-title')
title = ''
for j in range(len(titles)):
t = titles.eq(j).text().strip()
if t and t != '可可影视-kekys.com':
title = t
break
# 图片
pic = ''
imgs = item.find('img')
for j in range(len(imgs)):
img = imgs.eq(j)
src = img.attr('data-original') or ''
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
pic = src
break
if pic and pic.startswith('/'):
pic = 'https://vres.zyxpedu.com' + pic
# 备注
note = ''
bottom = item.find('.v-item-bottom span')
if bottom.length:
note = bottom.text().strip()
if title:
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
except Exception as e:
print(f'homeVideoContent error: {e}')
return {'list': videos[:24], 'parse': 0, 'jx': 0}
def categoryContent(self, tid, pg, filter, extend):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
try:
url = f'{self.site}/channel/{tid}.html?page={page}'
r = self.session.get(url, timeout=15, verify=False)
r.encoding = 'utf-8'
doc = pq(r.text)
items = doc('.module-item')
for item in items.items():
a = item.find('.v-item')
href = a.attr('href') or ''
vid = self.getVid(href)
if not vid:
continue
# 标题
titles = item.find('.v-item-title')
title = ''
for j in range(len(titles)):
t = titles.eq(j).text().strip()
if t and t != '可可影视-kekys.com':
title = t
break
# 图片
pic = ''
imgs = item.find('img')
for j in range(len(imgs)):
img = imgs.eq(j)
src = img.attr('data-original') or ''
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
pic = src
break
if pic and pic.startswith('/'):
pic = 'https://vres.zyxpedu.com' + pic
# 备注
note = ''
bottom = item.find('.v-item-bottom span')
if bottom.length:
note = bottom.text().strip()
if title:
result['list'].append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
except Exception as e:
print(f'categoryContent error: {e}')
result['page'] = page
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
result['limit'] = len(result['list'])
result['total'] = len(result['list'])
return result
def detailContent(self, ids):
result = {'list': [], 'parse': 0, 'jx': 0}
vid = ids[0] if ids else ''
if not vid:
return result
try:
url = f'{self.site}/detail/{vid}.html'
r = self.session.get(url, timeout=15, verify=False)
r.encoding = 'utf-8'
html = r.text
# 标题:从title标签提取,最可靠
title = ''
title_match = re.search(r'<title>(.+?)</title>', html)
if title_match:
title = title_match.group(1).split('-')[0].strip()
# 去掉特殊字符水印
title = re.sub(r'[𝕜𝕜𝕪𝕤𝟘𝟙𝕔𝕠𝕞.\s]+', ' ', title).strip()
title = re.sub(r'\s+', ' ', title).strip()
# 图片:从og:image提取
pic = ''
og_img = re.search(r'<meta\s+property="og:image"\s+content="([^"]+)"', html)
if og_img:
pic = og_img.group(1)
if pic.startswith('/'):
pic = 'https://vres.zyxpedu.com' + pic
# 简介:从meta description提取
desc = ''
desc_match = re.search(r'<meta\s+name="description"\s+content="([^"]+)"', html)
if desc_match:
desc = desc_match.group(1).strip()
# 播放线路和集数
play_from = []
play_url = []
episodes_by_sid = {}
sids_in_order = []
seen_sids = set()
# 纯正则提取所有播放链接
all_play = re.findall(r'<a[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]+class="episode-item"[^>]*>(.*?)</a>', html, re.DOTALL)
# 如果没匹配到,试试class在href前面的情况
if not all_play:
all_play = re.findall(r'<a[^>]+class="episode-item"[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]*>(.*?)</a>', html, re.DOTALL)
for href, sid, nid, link_html in all_play:
text = re.sub(r'<[^>]+>', '', link_html).strip()
if not text:
continue
if sid not in episodes_by_sid:
episodes_by_sid[sid] = []
episodes_by_sid[sid].append(f'{text}${href}')
if sid not in seen_sids:
seen_sids.add(sid)
sids_in_order.append(sid)
# 线路名称
source_labels = []
all_labels = re.findall(r'class="source-item-label"[^>]*>([^<]+)</', html)
for label in all_labels:
label = label.strip()
if label:
source_labels.append(label)
for i, sid in enumerate(sids_in_order):
if sid in episodes_by_sid and episodes_by_sid[sid]:
if i < len(source_labels):
line_name = source_labels[i]
else:
line_name = f'线路{sid}'
# 跳过4K线路(只有APP端能用)
if line_name == '4K':
continue
play_from.append(line_name)
play_url.append('#'.join(episodes_by_sid[sid]))
vod = {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': '',
'vod_director': self._mark,
'vod_content': desc,
'vod_play_from': '$$$'.join(play_from) if play_from else '',
'vod_play_url': '$$$'.join(play_url) if play_url else ''
}
result['list'].append(vod)
except Exception as e:
print(f'detailContent error: {e}')
return result
def playerContent(self, flag, id, vipFlags):
result = {}
try:
play_url = id
if id and not id.startswith('http'):
play_url = self.site + id
r = self.session.get(play_url, timeout=15, verify=False)
r.encoding = 'utf-8'
video_url = ''
patterns = [
r'src:\s*["\']([^"\']+\.(m3u8|mp4)[^"\']*)["\']',
r'"url"\s*:\s*"([^"]+\.(m3u8|mp4)[^"]*)"',
r"url\s*:\s*'([^']+\.(m3u8|mp4)[^']*)'",
]
for pat in patterns:
m = re.search(pat, r.text, re.DOTALL)
if m:
video_url = m.group(1)
break
if not video_url:
all_urls = re.findall(r'https?://[^\s"\'<>]+\.(m3u8|mp4)[^\s"\'<>]*', r.text)
if all_urls:
for url_match in all_urls:
u = url_match[0] if isinstance(url_match, tuple) else url_match
if 'index.m3u8' in u or 'video.m3u8' in u or '.mp4' in u:
video_url = u
break
if not video_url:
video_url = all_urls[0][0] if isinstance(all_urls[0], tuple) else all_urls[0]
if video_url:
result['parse'] = 0
result['url'] = video_url
result['jx'] = 0
result['header'] = {
'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',
'Referer': self.site + '/'
}
else:
result['parse'] = 1
result['url'] = play_url
result['jx'] = 0
result['header'] = {
'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',
'Referer': self.site + '/'
}
except Exception as e:
print(f'playerContent error: {e}')
result['parse'] = 1
result['url'] = id if id.startswith('http') else self.site + id
result['jx'] = 0
result['header'] = {
'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',
'Referer': self.site + '/'
}
return result
def searchContent(self, key, quick, pg='1'):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
try:
# 先访问搜索页获取token
search_url = f'{self.site}/search?k={quote(key)}'
r = self.session.get(search_url, timeout=15, verify=False)
r.encoding = 'utf-8'
t_match = re.search(r'name="t" value="([^"]+)"', r.text)
t = t_match.group(1) if t_match else ''
if t:
url = f'{self.site}/search?k={quote(key)}&t={quote(t)}'
if page > 1:
url += f'&page={page}'
r = self.session.get(url, timeout=15, verify=False)
r.encoding = 'utf-8'
doc = pq(r.text)
items = doc('.search-result-item')
for item in items.items():
href = item.attr('href') or ''
vid = self.getVid(href)
if not vid:
continue
# 标题
title = ''
title_elem = item.find('.title')
if title_elem.length:
title = title_elem.text().strip()
if not title:
img = item.find('img')
if img.length:
title = img.attr('alt') or img.attr('title') or ''
title = title.strip()
# 图片
pic = ''
imgs = item.find('img')
for j in range(len(imgs)):
img = imgs.eq(j)
src = img.attr('data-original') or img.attr('src') or ''
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
pic = src
break
if pic and pic.startswith('/'):
pic = 'https://vres.zyxpedu.com' + pic
if title:
result['list'].append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': ''
})
except Exception as e:
print(f'searchContent error: {e}')
result['page'] = page
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
result['limit'] = len(result['list'])
result['total'] = len(result['list'])
return result
def localProxy(self, params):
return [200, "video/MP2T", {}, ""]
def getVid(self, url):
if not url:
return ''
m = re.search(r'/detail/(\d+)\.html', url)
if m:
return m.group(1)
m = re.search(r'/play/(\d+)-', url)
if m:
return m.group(1)
return ''
+545
View File
@@ -0,0 +1,545 @@
# -*- coding: utf-8 -*-
"""
悟空影视爬虫
站点: https://www.yikucun.com
基于 MacCMS (苹果CMS) 程序,HTML 解析方式
OK影视 / 海阔视界 兼容版
"""
import re
import time
import json
import urllib.parse
import requests
try:
from base.spider import Spider as BaseSpider
except ImportError:
class BaseSpider:
pass
class Spider(BaseSpider):
BASE_URL = "https://www.yikucun.com"
# 分类映射
TYPE_MAP = {
"1": "电影",
"2": "电视剧",
"3": "综艺",
"4": "动漫",
"5": "短剧",
}
# 筛选条件 - 各分类的类型和地区
FILTERS = {
"1": { # 电影
"类型": ["全部", "动作", "喜剧", "爱情", "科幻", "恐怖", "剧情", "战争", "犯罪", "奇幻", "悬疑", "动画", "恐怖", "纪录片", "其他"],
"地区": ["全部", "大陆", "香港", "台湾", "日本", "韩国", "美国", "法国", "英国", "德国", "泰国", "印度", "其他"],
"年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015", "2014", "2013", "2012", "2011", "2010"],
},
"2": { # 电视剧
"类型": ["全部", "古装", "战争", "青春偶像", "喜剧", "家庭", "犯罪", "动作", "奇幻", "剧情", "历史", "经典", "乡村", "情景", "商战", "网剧", "其他"],
"地区": ["全部", "内地", "韩国", "香港", "台湾", "日本", "美国", "泰国", "英国", "新加坡", "其他"],
"年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015", "2014", "2013", "2012", "2011", "2010"],
},
"3": { # 综艺
"类型": ["全部", "真人秀", "脱口秀", "访谈", "美食", "旅游", "选秀", "情感", "音乐", "舞蹈", "其他"],
"地区": ["全部", "大陆", "香港", "台湾", "日本", "韩国", "欧美", "其他"],
"年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015"],
},
"4": { # 动漫
"类型": ["全部", "日本动漫", "国产动漫", "欧美动漫", "其他"],
"地区": ["全部", "日本", "大陆", "美国", "其他"],
"年份": ["全部", "2026", "2025", "2024", "2023", "2022", "2021", "2020", "2019", "2018", "2017", "2016", "2015"],
},
"5": { # 短剧
"类型": ["全部", "其他"],
"地区": ["全部", "其他"],
"年份": ["全部", "2026", "2025", "2024", "2023"],
},
}
def __init__(self):
super().__init__()
self.name = ""
self.error_play_url = "https://kjjsaas-sh.oss-cn-shanghai.aliyuncs.com/u/3401405881/20240818-936952-fc31b16575e80a7562cdb1f81a39c6b0.mp4"
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": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://www.yikucun.com/",
"Connection": "keep-alive",
})
self._init_cookies()
def getName(self):
return self.name
def init(self, extend="{}"):
try:
self.extend = json.loads(extend)
self.name = self.extend.get("name", "")
except Exception as e:
print(e)
self.extend = {}
def _init_cookies(self):
"""初始化 cookies(处理 508 反爬)"""
try:
r = self.session.get(self.BASE_URL, timeout=10, verify=False)
if r.status_code == 508:
time.sleep(1)
self.session.get(self.BASE_URL, timeout=10, verify=False)
except Exception:
pass
def _get(self, url, **kwargs):
"""GET 请求,自动处理 508"""
try:
r = self.session.get(url, timeout=15, verify=False, **kwargs)
if r.status_code == 508:
time.sleep(0.5)
r = self.session.get(url, timeout=15, verify=False, **kwargs)
r.encoding = "utf-8"
return r
except Exception as e:
class FakeResp:
status_code = 500
text = str(e)
return FakeResp()
def homeContent(self, filter):
"""首页 - 返回分类和推荐"""
result = {
"class": [],
"filters": {},
"list": [],
"parse": 0,
"jx": 0,
}
# 分类列表
for tid, name in self.TYPE_MAP.items():
result["class"].append({
"type_id": tid,
"type_name": name,
})
# 筛选条件
for tid, flist in self.FILTERS.items():
result["filters"][tid] = []
for fname, fvalues in flist.items():
result["filters"][tid].append({
"key": fname,
"name": fname,
"value": [{"n": v, "v": v} for v in fvalues],
})
# 首页推荐
try:
r = self._get(f"{self.BASE_URL}/")
if r.status_code == 200:
result["list"] = self._parse_list(r.text)
except Exception:
pass
return result
def categoryContent(self, tid, pg, filter, extend):
"""分类页"""
result = {
"list": [],
"parse": 0,
"jx": 0,
}
# URL 格式: 12个位置,用 - 分隔
# 位置: 1=tid, 2=地区, 3=空, 4=类型, 5=空, 6=空, 7=空, 8=空, 9=页码, 10=空, 11=空, 12=年份
area = extend.get("地区", "") if isinstance(extend, dict) else ""
type_val = extend.get("类型", "") if isinstance(extend, dict) else ""
year = extend.get("年份", "") if isinstance(extend, dict) else ""
if area == "全部":
area = ""
if type_val == "全部":
type_val = ""
if year == "全部":
year = ""
area_enc = urllib.parse.quote(area) if area else ""
type_enc = urllib.parse.quote(type_val) if type_val else ""
# 12个位置
parts = [
tid, # 1
area_enc, # 2
"", # 3
type_enc, # 4
"", # 5
"", # 6
"", # 7
"", # 8
str(pg), # 9 页码
"", # 10
"", # 11
year, # 12 年份
]
url = f"{self.BASE_URL}/ucusw/{'-'.join(parts)}.html"
try:
r = self._get(url)
if r.status_code == 200:
result["list"] = self._parse_list(r.text)
except Exception:
pass
return result
def detailContent(self, ids):
"""详情页"""
result = {
"list": [],
"parse": 0,
"jx": 0,
}
vid = ids[0]
url = f"{self.BASE_URL}/ucudt/{vid}.html"
try:
r = self._get(url)
if r.status_code == 200:
detail = self._parse_detail(r.text, vid)
if detail:
result["list"].append(detail)
except Exception:
pass
return result
def searchContent(self, key, quick, pg="1"):
"""搜索"""
result = {
"list": [],
"parse": 0,
"jx": 0,
}
try:
# 搜索 URL: /ucusc/-------------.html?wd=关键词
# 分页: /ucusc/-------------(页码).html?wd=关键词
if int(pg) > 1:
search_url = f"{self.BASE_URL}/ucusc/-------------{pg}.html?wd={urllib.parse.quote(key)}"
else:
search_url = f"{self.BASE_URL}/ucusc/-------------.html?wd={urllib.parse.quote(key)}"
r = self._get(search_url)
if r.status_code == 200:
result["list"] = self._parse_list(r.text)
except Exception:
pass
return result
def playerContent(self, flag, id, vipFlags):
"""播放页 - 获取播放地址"""
result = {
"url": self.error_play_url,
"parse": 0,
"jx": 0,
"header": {},
}
try:
# id 格式: 视频ID-线路ID-集数ID
parts = id.split("-")
if len(parts) >= 3:
vid, sid, nid = parts[0], parts[1], parts[2]
play_url = f"{self.BASE_URL}/ucupy/{vid}-{sid}-{nid}.html"
r = self._get(play_url)
if r.status_code == 200:
url = self._parse_play_url(r.text)
if url:
result["url"] = url
result["parse"] = 0
except Exception:
pass
return result
def localProxy(self, params):
return 0
def _parse_list(self, html):
"""解析列表页"""
items = []
# 找到所有 class="name" 的标题,然后往前找最近的图片
name_pattern = r'class="name"[^>]*>\s*<a[^>]*href="/ucudt/(\d+)\.html"[^>]*>([^<]+)</a>'
# 先找出所有 name 的位置
name_matches = list(re.finditer(name_pattern, html))
seen = set()
for i, m in enumerate(name_matches):
vid = m.group(1)
name = m.group(2).strip()
if vid in seen:
continue
seen.add(vid)
# 往前找最近的图片(在这个 name 之前)
pos = m.start()
start = max(0, pos - 2000)
segment = html[start:pos]
# 找所有图片
img_matches = re.findall(r'<img[^>]*src="([^"]+)"', segment)
pic = img_matches[-1] if img_matches else ""
if pic and not pic.startswith("http"):
pic = "https:" + pic if pic.startswith("//") else pic
# 找备注
remarks = ""
rgba_match = re.search(r'class="rgba[^"]*"[^>]*>([^<]+)</span>', segment)
if rgba_match:
remarks = rgba_match.group(1).strip()
items.append({
"vod_id": vid,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks,
})
return items[:30]
def _parse_detail(self, html, vid):
"""解析详情页"""
detail = {
"vod_id": vid,
"vod_name": "",
"vod_pic": "",
"type_name": "",
"vod_year": "",
"vod_area": "",
"vod_remarks": "",
"vod_actor": "",
"vod_director": "",
"vod_content": "",
"vod_play_from": "",
"vod_play_url": "",
}
# 标题
title_match = re.search(r'<title>《([^》]+)》', html)
if title_match:
detail["vod_name"] = title_match.group(1).strip()
# 图片
pic_match = re.search(r'og:image"[^>]*content="([^"]+)"', html)
if pic_match:
pic = pic_match.group(1)
if not pic.startswith("http"):
pic = "https:" + pic if pic.startswith("//") else pic
detail["vod_pic"] = pic
# 主演
actor_match = re.search(r'<dt>主演[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if actor_match:
actors = re.sub(r'<[^>]+>', '', actor_match.group(1)).strip()
actors = actors.replace('&nbsp;', ' ').replace('\xa0', ' ')
detail["vod_actor"] = actors
# 导演
dir_match = re.search(r'<dt>导演[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if dir_match:
directors = re.sub(r'<[^>]+>', '', dir_match.group(1)).strip()
directors = directors.replace('&nbsp;', ' ').replace('\xa0', ' ').strip()
# 隐晦水印
import base64
wm = base64.b64decode(b'5pif5rKz').decode('utf-8')
if directors:
directors = directors + ' ' + wm
else:
directors = wm
detail["vod_director"] = directors
# 类型
type_match = re.search(r'<dt>类型[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if type_match:
type_name = re.sub(r'<[^>]+>', '', type_match.group(1)).strip()
type_name = type_name.replace('&nbsp;', ' ').replace('\xa0', ' ').strip()
detail["type_name"] = type_name
# 地区
area_match = re.search(r'<dt>地区[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if area_match:
area = re.sub(r'<[^>]+>', '', area_match.group(1)).strip()
area = area.replace('&nbsp;', ' ').replace('\xa0', ' ').strip()
detail["vod_area"] = area
# 年代
year_match = re.search(r'<dt>年代[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if year_match:
year = re.sub(r'<[^>]+>', '', year_match.group(1)).strip()
year = year.replace('&nbsp;', ' ').replace('\xa0', ' ').strip()
detail["vod_year"] = year
# 备注/状态
remark_match = re.search(r'<dt>备注[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if remark_match:
detail["vod_remarks"] = re.sub(r'<[^>]+>', '', remark_match.group(1)).strip()
# 简介
desc_match = re.search(r'<dt>剧情[:]</dt>\s*<dd>(.*?)</dd>', html, re.DOTALL)
if desc_match:
content = re.sub(r'<[^>]+>', '', desc_match.group(1)).strip()
content = content.replace("详细", "").strip()
detail["vod_content"] = content
# 播放源和播放地址
# 1. 从 tab2 提取线路 id -> 名称 映射 (按顺序)
source_order = []
tab_match = re.search(r'class="tab2">(.*?)</dt>', html, re.DOTALL)
if tab_match:
tab_html = tab_match.group(1)
source_pattern = r'<span[^>]*id="([^"]+)"[^>]*>([^<]+)</span>'
source_matches = re.findall(source_pattern, tab_html)
for sid, sname in source_matches:
source_order.append((sid, sname.strip()))
# 2. 定位到 content 区域
source_eps = {}
content_match = re.search(r'<div[^>]*id="content"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
if content_match:
content = content_match.group(1)
# 用 <dd 分割,逐个处理
dd_parts = re.split(r'<dd\s+', content)
for part in dd_parts[1:]:
class_match = re.search(r'class="([^"]+)"', part)
if not class_match:
continue
dd_class = class_match.group(1)
if 'mod' not in dd_class:
continue
source_key = dd_class.replace('mod', '').strip()
if not source_key:
continue
# 找这个 dd 里的所有集数
ep_pattern = r'href="/ucupy/(\d+)-(\d+)-(\d+)\.html"[^>]*>([^<]+)<'
ep_matches = re.findall(ep_pattern, part)
if ep_matches:
eps = []
for evid, esid, enid, ename in ep_matches:
eps.append(f"{ename.strip()}${evid}-{esid}-{enid}")
source_eps[source_key] = "#".join(eps)
# 3. 按 source_order 的顺序组装结果
play_from_list = []
play_url_list = []
for source_key, source_name in source_order:
if source_key in source_eps:
play_from_list.append(source_name)
play_url_list.append(source_eps[source_key])
if play_from_list:
detail["vod_play_from"] = "$$$".join(play_from_list)
detail["vod_play_url"] = "$$$".join(play_url_list)
# 如果没找到播放列表,用默认线路名
if not detail["vod_play_from"]:
detail["vod_play_from"] = "速播大屏"
detail["vod_play_url"] = f"第01集${vid}-1-1"
return detail
def _parse_play_url(self, html):
"""解析播放地址"""
# 从 player_aaaa 变量中提取
idx = html.find('player_aaaa=')
if idx >= 0:
eq_idx = html.find('=', idx)
if eq_idx >= 0:
end_idx = html.find('</script>', eq_idx)
if end_idx > 0:
json_str = html[eq_idx+1:end_idx].strip()
if json_str.endswith(';'):
json_str = json_str[:-1].strip()
try:
data = json.loads(json_str)
url = data.get("url", "")
if url:
return url
except Exception:
pass
# 备用:直接找 m3u8
url_match = re.search(r'"url"\s*:\s*"(https?://[^"]+\.m3u8[^"]*)"', html)
if url_match:
url = url_match.group(1).replace("\\/", "/").replace("\\", "")
return url
return ""
def main():
"""测试用"""
spider = Spider()
spider.init('{}')
print("=== homeContent 测试 ===")
result = spider.homeContent({})
print(f"分类数: {len(result['class'])}")
print(f"首页推荐: {len(result['list'])}")
for item in result['list'][:3]:
print(f" {item['vod_id']}: {item['vod_name']}")
print()
print("=== categoryContent 测试 (电视剧) ===")
result = spider.categoryContent("2", "1", "", {})
print(f"结果数: {len(result['list'])}")
for item in result['list'][:3]:
print(f" {item['vod_id']}: {item['vod_name']}")
print()
print("=== searchContent 测试 (千香) ===")
result = spider.searchContent("千香", False, "1")
print(f"结果数: {len(result['list'])}")
for item in result['list'][:3]:
print(f" {item['vod_id']}: {item['vod_name']}")
if result["list"]:
vid = result["list"][0]["vod_id"]
print(f"\n=== detailContent 测试 ({vid}) ===")
detail_result = spider.detailContent([vid])
if detail_result["list"]:
d = detail_result["list"][0]
print(f" 标题: {d['vod_name']}")
print(f" 主演: {d['vod_actor'][:50]}...")
print(f" 线路: {d['vod_play_from']}")
sources = d['vod_play_from'].split('$$$')
print(f" 线路数: {len(sources)}")
# 播放测试
urls = d['vod_play_url'].split('$$$')
first_ep = urls[0].split('#')[0]
ep_id = first_ep.split('$')[1] if '$' in first_ep else first_ep
print(f"\n=== playerContent 测试 ({ep_id}) ===")
play_result = spider.playerContent(sources[0], ep_id, [])
print(f" parse: {play_result['parse']}")
print(f" url: {play_result['url'][:80] if play_result['url'] else ''}...")
if __name__ == "__main__":
main()
+512
View File
@@ -0,0 +1,512 @@
# coding=utf-8
import re, json, requests, urllib3
from urllib.parse import quote
from lxml import etree
from base.spider import Spider
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class Spider(Spider):
def __init__(self):
self.name = "smys"
self.host = "https://www.china-eae.com"
self.header = {
'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': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Referer': self.host
}
def getName(self):
return self.name
def init(self, extend=''):
pass
def _get(self, url, params=None):
r = requests.get(url, headers=self.header, params=params, timeout=15, verify=False)
r.encoding = 'utf-8'
return r.text
def _post(self, url, data=None):
r = requests.post(url, headers=self.header, data=data, timeout=15, verify=False)
r.encoding = 'utf-8'
return r.text
def _fix_url(self, url):
if not url:
return ''
url = url.strip().strip('`')
if url.startswith('/img.php?url='):
url = url[len('/img.php?url='):]
url = url.strip().strip('`')
if url.startswith('//'):
return 'https:' + url
if url.startswith('/'):
return self.host + url
return url
def _parse_pic(self, elem):
pic = ''
if elem is None:
return pic
# 元素自身可能带 data-original(列表页的 a 标签)
pic = elem.get('data-original') or elem.get('data-src') or ''
if not pic and elem.tag == 'img':
pic = elem.get('src', '')
if not pic:
imgs = elem.xpath('.//img')
if imgs:
pic = imgs[0].get('data-original') or imgs[0].get('data-src') or imgs[0].get('src', '')
if pic and pic.startswith('data:image'):
pic = ''
return self._fix_url(pic)
def _parse_text(self, elem):
if elem is None:
return ''
return ''.join(elem.itertext()).strip()
def _extract_id(self, href):
m = re.search(r'/about/(\d+)\.html', href)
return m.group(1) if m else ''
def _parse_list_item(self, item):
thumb = item.xpath('.//a[contains(@class, "stui-vodlist__thumb")]')
if not thumb:
return None
thumb = thumb[0]
href = thumb.get('href', '')
vod_id = self._extract_id(href)
if not vod_id:
return None
vod_name = thumb.get('title', '').strip()
if not vod_name:
title_a = item.xpath('.//h4[contains(@class, "title")]/a')
if title_a:
vod_name = self._parse_text(title_a[0])
vod_pic = self._parse_pic(thumb)
remark = ''
pic_text = item.xpath('.//span[contains(@class, "pic-text")]/text()')
if pic_text:
remark = pic_text[0].strip()
return {
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": remark
}
def homeContent(self, filter):
result = {"class": []}
classes = [
{"type_name": "电影", "type_id": "dianying"},
{"type_name": "电视剧", "type_id": "dianshiju"},
{"type_name": "综艺片", "type_id": "zongyipian"},
{"type_name": "动漫片", "type_id": "dongmanpian"},
{"type_name": "预告片", "type_id": "yugaopian"},
{"type_name": "体育赛事", "type_id": "tiyusaishi"},
{"type_name": "短剧大全", "type_id": "duanjudaquan"},
{"type_name": "电影解说", "type_id": "dianyingjieshuo"}
]
result["class"] = classes
area_vals = [
{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"},
{"n": "香港", "v": "香港"}, {"n": "台湾", "v": "台湾"},
{"n": "美国", "v": "美国"}, {"n": "日本", "v": "日本"},
{"n": "韩国", "v": "韩国"}, {"n": "泰国", "v": "泰国"},
{"n": "英国", "v": "英国"}, {"n": "法国", "v": "法国"},
{"n": "德国", "v": "德国"}, {"n": "意大利", "v": "意大利"},
{"n": "西班牙", "v": "西班牙"}, {"n": "加拿大", "v": "加拿大"},
{"n": "印度", "v": "印度"}, {"n": "其他", "v": "其他"}
]
class_vals = [
{"n": "全部", "v": ""}, {"n": "喜剧", "v": "喜剧"},
{"n": "爱情", "v": "爱情"}, {"n": "恐怖", "v": "恐怖"},
{"n": "动作", "v": "动作"}, {"n": "科幻", "v": "科幻"},
{"n": "剧情", "v": "剧情"}, {"n": "战争", "v": "战争"},
{"n": "警匪", "v": "警匪"}, {"n": "犯罪", "v": "犯罪"},
{"n": "动画", "v": "动画"}, {"n": "奇幻", "v": "奇幻"},
{"n": "武侠", "v": "武侠"}, {"n": "冒险", "v": "冒险"},
{"n": "枪战", "v": "枪战"}, {"n": "悬疑", "v": "悬疑"},
{"n": "惊悚", "v": "惊悚"}, {"n": "经典", "v": "经典"},
{"n": "青春", "v": "青春"}, {"n": "文艺", "v": "文艺"},
{"n": "古装", "v": "古装"}, {"n": "历史", "v": "历史"},
{"n": "运动", "v": "运动"}, {"n": "儿童", "v": "儿童"},
{"n": "网络电影", "v": "网络电影"}, {"n": "纪录片", "v": "纪录片"}
]
year_vals = [{"n": "全部", "v": ""}]
for y in range(2026, 1989, -1):
year_vals.append({"n": str(y), "v": str(y)})
year_vals.append({"n": "80年代", "v": "80年代"})
year_vals.append({"n": "更早", "v": "更早"})
lang_vals = [
{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"},
{"n": "英语", "v": "英语"}, {"n": "日语", "v": "日语"},
{"n": "韩语", "v": "韩语"}, {"n": "法语", "v": "法语"},
{"n": "德语", "v": "德语"}, {"n": "其它", "v": "其它"}
]
letter_vals = [{"n": "全部", "v": ""}]
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
letter_vals.append({"n": c, "v": c})
letter_vals.append({"n": "0-9", "v": "0-9"})
order_vals = [
{"n": "最新", "v": "time"},
{"n": "最热", "v": "hits"},
{"n": "评分", "v": "score"}
]
filters = {}
for c in classes:
filters[c['type_id']] = [
{"key": "area", "name": "地区", "value": area_vals},
{"key": "class", "name": "类型", "value": class_vals},
{"key": "lang", "name": "语言", "value": lang_vals},
{"key": "letter", "name": "字母", "value": letter_vals},
{"key": "year", "name": "年份", "value": year_vals},
{"key": "order", "name": "排序", "value": order_vals}
]
result["filters"] = filters
return result
def homeVideoContent(self):
videos = []
try:
html = self._get(self.host)
root = etree.HTML(html)
videos = self._parse_video_list(root)
except Exception:
pass
return {"list": videos}
def _parse_video_list(self, root):
videos = []
if root is None:
return videos
items = root.xpath('//div[contains(@class, "stui-vodlist__box")]')
seen = set()
for item in items:
try:
video = self._parse_list_item(item)
if video and video.get('vod_id') and video['vod_id'] not in seen:
seen.add(video['vod_id'])
videos.append(video)
except Exception:
pass
return videos
def categoryContent(self, tid, pg, filter, extend):
videos = []
try:
if isinstance(extend, str) and extend:
try:
extend = json.loads(extend)
except Exception:
extend = {}
elif not extend:
extend = {}
area = extend.get('area', '')
cls = extend.get('class', '')
year = extend.get('year', '')
lang = extend.get('lang', '')
letter = extend.get('letter', '')
order = extend.get('order', 'time')
# URL 规则:/lstd/{tid}/area/{area}/by/{order}/class/{class}/lang/{lang}/letter/{letter}/year/{year}/page/{pg}.html
segments = [f"/lstd/{tid}"]
if area:
segments.append(f"area/{quote(area)}")
if order:
segments.append(f"by/{quote(order)}")
if cls:
segments.append(f"class/{quote(cls)}")
if lang:
segments.append(f"lang/{quote(lang)}")
if letter:
segments.append(f"letter/{quote(letter)}")
if year:
segments.append(f"year/{quote(year)}")
if int(pg) > 1:
segments.append(f"page/{pg}")
url = self.host + '/'.join(segments) + ".html"
html = self._get(url)
root = etree.HTML(html)
videos = self._parse_video_list(root)
total_pages = 1
# 解析分页,页码文本形如 "1/1665"
page_texts = root.xpath('//text()[contains(., "/")]')
for t in page_texts:
tm = re.search(r'(\d+)\s*/\s*(\d+)', t)
if tm:
total_pages = max(total_pages, int(tm.group(2)))
break
# 兜底:取最大页码链接
links = root.xpath('//a[contains(@href, "/page/")]')
for link in links:
tm = re.search(r'/page/(\d+)\.html', link.get('href', ''))
if tm:
total_pages = max(total_pages, int(tm.group(1)))
return {
'list': videos,
'page': int(pg),
'pagecount': total_pages,
'limit': len(videos),
'total': total_pages * len(videos) if videos else total_pages * 36
}
except Exception:
return {'list': [], 'page': 1, 'pagecount': 0, 'limit': 0, 'total': 0}
def detailContent(self, ids):
try:
vod_id = ids[0]
detail_url = f"{self.host}/about/{vod_id}.html"
html = self._get(detail_url)
root = etree.HTML(html)
# 标题
vod_name = ''
title_elem = root.xpath('//div[contains(@class, "stui-content__detail")]/h1[contains(@class, "title")]')
if title_elem:
# 去掉评分 span
for span in title_elem[0].xpath('.//span'):
span.getparent().remove(span)
vod_name = self._parse_text(title_elem[0])
if not vod_name:
title = root.xpath('//title/text()')
if title:
vod_name = title[0].split('-')[0].strip()
vod_name = vod_name.strip()
# 封面
vod_pic = ''
thumb_img = root.xpath('//div[contains(@class, "stui-content__thumb")]//img')
if thumb_img:
vod_pic = self._parse_pic(thumb_img[0])
# 信息行
info_lines = root.xpath('//div[contains(@class, "stui-content__detail")]/p[contains(@class, "data")]')
def _line_text_containing(keyword):
for line in info_lines:
txt = self._parse_text(line)
if keyword in txt:
return txt
return ''
# 类型/地区/年份(在同一行)
first_line = _line_text_containing('类型')
vod_type = ''
vod_area = ''
vod_year = ''
if first_line:
m = re.search(r'类型[:]\s*([^\s&]+)', first_line)
if m:
vod_type = m.group(1).strip()
m = re.search(r'地区[:]\s*([^\s&]+)', first_line)
if m:
vod_area = m.group(1).strip()
m = re.search(r'年份[:]\s*(\d{4})', first_line)
if m:
vod_year = m.group(1)
# 主演
vod_actor = ''
actor_line = None
for line in info_lines:
txt = self._parse_text(line)
if '主演' in txt:
actor_line = line
break
if actor_line is not None:
actors = actor_line.xpath('.//a/text()')
vod_actor = ' '.join([a.strip() for a in actors if a.strip()])
# 导演
vod_director = ''
director_line = None
for line in info_lines:
txt = self._parse_text(line)
if '导演' in txt:
director_line = line
break
if director_line is not None:
directors = director_line.xpath('.//a/text()')
vod_director = ' '.join([d.strip() for d in directors if d.strip()])
# 更新/状态
vod_remarks = ''
for line in info_lines:
txt = self._parse_text(line)
if '更新' in txt:
m = re.search(r'更新[:]\s*([^\n]+)', txt)
if m:
vod_remarks = m.group(1).strip()
break
# 简介
vod_content = ''
desc_elem = root.xpath('//div[contains(@class, "stui-content__detail")]/p[contains(@class, "desc")]')
if desc_elem:
# 去掉“详情”链接
for a in desc_elem[0].xpath('.//a'):
a.getparent().remove(a)
vod_content = self._parse_text(desc_elem[0])
vod_content = re.sub(r'^[\s\n]*简介[:]\s*', '', vod_content)
if not vod_content:
desc_p = root.xpath('//div[@id="desc"]//p[contains(@class, "col-pd")]')
if desc_p:
vod_content = self._parse_text(desc_p[0]).strip()
# 播放源
vod_play_from = []
vod_play_url = []
source_boxes = root.xpath('//div[contains(@class, "stui-pannel-box") and contains(@class, "playlist")]')
for box in source_boxes:
head = box.xpath('.//div[contains(@class, "stui-pannel__head")]//h3[contains(@class, "title")]')
if not head:
continue
source_name = self._parse_text(head[0]).strip()
source_name = re.sub(r'\s+', ' ', source_name)
if not source_name:
continue
links = box.xpath('.//ul[contains(@class, "stui-content__playlist")]//a')
play_list = []
for a in links:
ep_name = self._parse_text(a)
href = a.get('href', '')
if not ep_name or not href:
continue
play_list.append(f"{ep_name}${self._fix_url(href)}")
if play_list:
vod_play_from.append(source_name)
vod_play_url.append("#".join(play_list))
if vod_play_from:
vod_play_from_str = "$$$".join(vod_play_from)
vod_play_url_str = "$$$".join(vod_play_url)
else:
vod_play_from_str = "默认"
vod_play_url_str = ""
detail = {
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_year": vod_year,
"vod_area": vod_area,
"vod_actor": vod_actor,
"vod_director": vod_director,
"vod_content": vod_content,
"vod_remarks": vod_remarks,
"vod_play_from": vod_play_from_str,
"vod_play_url": vod_play_url_str
}
return {'list': [detail]}
except Exception:
return {'list': []}
def _extract_player_url(self, html):
# 常见 AppleCMS 播放器变量
for pattern in [
r'var\s+player_aaaa\s*=\s*\{',
r'var\s+player_data\s*=\s*\{',
r'var\s+player\s*=\s*\{',
r'var\s+config\s*=\s*\{',
]:
m = re.search(pattern, html)
if m:
start = m.end() - 1
depth = 1
i = start + 1
while i < len(html) and depth > 0:
if html[i] == '{':
depth += 1
elif html[i] == '}':
depth -= 1
i += 1
try:
data = json.loads(html[start:i])
url = data.get('url') or data.get('video', {}).get('url', '')
if url:
return url
except Exception:
pass
# 直接提取 m3u8/mp4
m = re.search(r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']', html)
if m:
return m.group(1)
m = re.search(r'["\'](https?://[^"\']+\.(?:mp4|flv|ts))["\']', html)
if m:
return m.group(1)
# iframe
m = re.search(r'<iframe[^>]+src\s*=\s*["\']([^"\']+)["\']', html)
if m:
return m.group(1)
return None
def playerContent(self, flag, id, vipFlags):
try:
html = self._get(id)
real_url = self._extract_player_url(html)
if real_url:
real_url = self._fix_url(real_url)
parse_flag = 0 if self.isVideoFormat(real_url) else 1
return {"parse": parse_flag, "playUrl": "", "url": real_url, "header": json.dumps(self.header)}
return {"parse": 1, "playUrl": "", "url": id, "header": json.dumps(self.header)}
except Exception:
return {"parse": 0, "playUrl": "", "url": ""}
def searchContent(self, key, quick, pg='1'):
videos = []
try:
params = {"wd": key}
if int(pg) > 1:
params["page"] = pg
url = f"{self.host}/msfw/search/-------------.html"
html = self._get(url, params=params)
root = etree.HTML(html)
videos = self._parse_video_list(root)
total_pages = 1
page_texts = root.xpath('//text()[contains(., "/")]')
for t in page_texts:
tm = re.search(r'(\d+)\s*/\s*(\d+)', t)
if tm:
total_pages = max(total_pages, int(tm.group(2)))
break
links = root.xpath('//a[contains(@href, "/page/")]')
for link in links:
tm = re.search(r'/page/(\d+)\.html', link.get('href', ''))
if tm:
total_pages = max(total_pages, int(tm.group(1)))
return {
'list': videos,
'page': int(pg),
'pagecount': total_pages,
'limit': len(videos),
'total': len(videos)
}
except Exception:
return {'list': [], 'page': 1, 'pagecount': 0, 'limit': 0, 'total': 0}
def isVideoFormat(self, url):
return any(url.lower().endswith(fmt) for fmt in ['.m3u8', '.mp4', '.flv', '.ts'])
def manualVideoCheck(self):
pass
def localProxy(self, params):
return None
def destroy(self):
pass
+215
View File
@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
import base64
from urllib.parse import urljoin, quote, unquote
sys.path.append('..')
try:
from base.spider import Spider
except ImportError:
class Spider:
def fetch(self, url, headers=None, **kw):
import requests as rq
kw.pop('timeout', None)
r = rq.get(url, headers=headers, timeout=15, **kw)
r.encoding = 'utf-8'
return r
HOST = "https://www.moxy.top"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
class Spider(Spider):
def init(self, extend=""):
global HOST
try:
r = self.fetch(HOST, headers={"User-Agent": UA}, timeout=15000)
if hasattr(r, 'url') and r.url and "moxy" not in r.url:
HOST = r.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": [], "filter": {}}
for k, v in {"1":"电影","2":"连续剧","3":"综艺","4":"动漫","5":"短剧"}.items():
r["class"].append({"type_id": k, "type_name": v})
try:
resp = self.fetch(HOST, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
r["list"] = self._items(html)[:60]
except:
pass
return r
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cid = str(tid) if str(tid) in "12345" else "1"
try:
if pn > 1:
url = f"{HOST}/vodshow/{cid}--------{pn}---.html"
else:
url = f"{HOST}/vodshow/{cid}-----------.html"
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
items = self._items(html)
return {"page": pn, "pagecount": self._pagecount(html), "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
if isinstance(ids, list):
vid = ids[0] if ids else ""
else:
vid = str(ids) if ids else ""
m = re.search(r'(\d+)', str(vid))
vid = m.group(1) if m else ""
if not vid: return {"list": []}
try:
resp = self.fetch(f"{HOST}/voddetail{vid}.html", headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"list": []}
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h, re.S)
if t1: d["vod_name"] = t1.group(1).strip()
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="([^"]+)"', h)
if p: d["vod_pic"] = p.group(1)
for t in re.findall(r'<a[^>]*title="(\d{4})"', h):
d["vod_year"] = t
for t in re.findall(r'<a[^>]*title="([^"]*)"', h):
if t in ("中国大陆","中国","香港","台湾","美国","日本","韩国","英国","法国","泰国","印度"):
d["vod_area"] = t
desc = re.search(r'<div[^>]*class="[^"]*module-info-introduction-content[^"]*"[^>]*>\s*<p>(.*?)</p>', h, re.S)
if desc: d["vod_content"] = re.sub(r'<[^>]+>', '', desc.group(1)).strip()[:500]
for m in re.finditer(r'<div[^>]*class="[^"]*module-info-item[^"]*"[^>]*>(.*?)</div>', h, re.S):
t = re.sub(r'<[^>]+>', '', m.group(1)).strip()
if "导演" in t: d["vod_director"] = t.replace("导演:","").replace("导演:","").strip()
elif "主演" in t: d["vod_actor"] = t.replace("主演:","").replace("主演:","").strip()
elif "备注" in t: d["vod_remarks"] = t.replace("备注:","").replace("备注:","").strip()
try:
sources = re.findall(r'data-dropdown-value="([^"]+)"', h) or ["默认"]
blocks = re.findall(r'<div[^>]*class="[^"]*module-play-list[^"]*"[^>]*>(.*?)</div>\s*</div>\s*</div>', h, re.S)
if not blocks:
blocks = re.findall(r'<div[^>]*class="[^"]*module-play-list-content[^"]*"[^>]*>(.*?)</div>', h, re.S)
pf, pu = [], []
for i, blk in enumerate(blocks):
eps = re.findall(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>(?:<[^>]+>)*([^<]{1,20})(?:</[^>]+>)*</a>', blk, re.S)
if not eps:
eps = re.findall(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>.*?<span>(.*?)</span>', blk, re.S)
if eps:
src = sources[i] if i < len(sources) else f"{i+1}"
el = [f"{n.strip()}${urljoin(HOST, u)}" for u, n in eps if n.strip()]
if el:
pf.append(src)
pu.append("#".join(el))
if pf:
d["vod_play_from"] = "$$$".join(pf)
d["vod_play_url"] = "$$$".join(pu)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
try:
url = f"{HOST}/vodsearch/{quote(str(key))}-------------.html"
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=15000)
html = resp.text if hasattr(resp, 'text') else str(resp)
if len(html) > 200:
return {"list": self._items(html)[:30]}
except:
pass
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
a, b = str(flag), str(id) if id else ""
if a.startswith("http") or "/vodplay/" in a:
url = a
elif b.startswith("http") or "/vodplay/" in b:
url = b
elif a.startswith("/"):
url = urljoin(HOST, a)
elif b.startswith("/"):
url = urljoin(HOST, b)
else:
url = a
try:
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"url": ""}
pd = re.search(r'player_data\s*=\s*(\{.*?\})', h, re.S)
if pd:
try:
data = json.loads(pd.group(1))
u = data.get("url", "")
if u:
try:
real_url = unquote(base64.b64decode(u).decode("utf-8"))
except:
real_url = u
if real_url.startswith("http"):
return {"url": real_url}
except:
pass
return {"url": ""}
def localProxy(self, param):
pass
def _pagecount(self, html):
pc = 1
last = re.search(r'<a[^>]*href="[^"]*vodshow/\d+[^"]*(\d+)---\.html"[^>]*>尾页', html, re.S)
if last:
pc = max(pc, int(last.group(1)))
page_links = re.findall(r'<a[^>]*href="[^"]*vodshow/\d+-(\d+)', html)
for p in page_links:
try:
n = int(p)
if n <= 100:
pc = max(pc, n)
except:
pass
all_nums = re.findall(r'class="[^"]*page-number[^"]*"[^>]*>\s*(\d+)\s*<', html)
for n in all_nums:
try: pc = max(pc, int(n))
except: pass
return pc
def _items(self, html):
items, seen = [], set()
def ext(href, block):
v = re.search(r'/voddetail(\d+)\.html', href)
if not v or v.group(1) in seen: return None
seen.add(v.group(1))
t = (re.search(r'title="([^"]*)"', block) or re.search(r'alt="([^"]*)"', block))
if not t: return None
p = re.search(r'data-original="([^"]+)"', block)
n = re.search(r'<div[^>]*class="[^"]*module-item-note[^"]*"[^>]*>([^<]+)</div>', block)
return {"vod_id": v.group(1), "vod_name": t.group(1),
"vod_pic": p.group(1) if p else "",
"vod_remarks": n.group(1).strip() if n else "",
"vod_url": urljoin(HOST, href)}
for m in re.finditer(
r'<a[^>]*href="(/voddetail\d+\.html)"[^>]*title="([^"]*)"[^>]*class="[^"]*module-poster-item[^"]*"[\s\S]*?</a>',
html
):
item = ext(m.group(1), m.group(0))
if item: items.append(item)
for m in re.finditer(
r'<a[^>]*href="(/voddetail\d+\.html)"[^>]*class="[^"]*module-card-item-poster[^"]*"[\s\S]*?</a>',
html
):
item = ext(m.group(1), m.group(0))
if item: items.append(item)
return items
+244
View File
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
import base64
from urllib.parse import urljoin, quote, unquote
sys.path.append('..')
try:
from base.spider import Spider
except ImportError:
class Spider:
def fetch(self, url, headers=None, **kw):
import requests as rq
kw.pop('timeout', None)
r = rq.get(url, headers=headers, timeout=15, **kw)
r.encoding = 'utf-8'
return r
HOST = "https://www.qlys.cc"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
CATEGORIES = {
"1": "电影", "2": "电视剧", "3": "短剧", "4": "动漫", "5": "综艺",
}
class Spider(Spider):
def init(self, extend=""):
global HOST
try:
resp = self.fetch(HOST, headers={"User-Agent": UA}, timeout=15000)
if hasattr(resp, 'url') and resp.url and resp.url != HOST.rstrip("/"):
HOST = resp.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": [], "filter": {}}
for k, v in CATEGORIES.items():
r["class"].append({"type_id": k, "type_name": v})
try:
resp = self.fetch(HOST, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
items = self._items(html)
r["list"] = [it for it in items if it["vod_pic"]][:30]
except:
pass
return r
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cid = str(tid)
try:
if pn > 1:
url = f"{HOST}/index.php/vod/type/id/{cid}/page/{pn}.html"
else:
url = f"{HOST}/index.php/vod/type/id/{cid}.html"
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
items = self._items(html)
pagecount = self._pagecount(html)
return {"page": pn, "pagecount": pagecount, "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
if isinstance(ids, list):
vid = ids[0] if ids else ""
else:
vid = str(ids) if ids else ""
m = re.search(r'(\d+)', str(vid))
vid = m.group(1) if m else ""
if not vid: return {"list": []}
try:
resp = self.fetch(f"{HOST}/index.php/vod/detail/id/{vid}.html", headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"list": []}
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h, re.S)
if t1:
clean = re.sub(r'<[^>]+>', ' ', t1.group(1)).strip()
parts = re.split(r'\s{2,}|\n', clean)
for part in parts:
p = part.strip()
if p and len(p) > 1:
d["vod_name"] = p
break
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="([^"]+)"', h)
if not p:
p = re.search(r'data-background="([^"]+)"', h)
if not p:
p = re.search(r'<img[^>]*src="([^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if p: d["vod_pic"] = p.group(1)
for t in re.findall(r'<a[^>]*title="(\d{4})"', h):
d["vod_year"] = t
desc = re.search(r'<div\s+id="content"[^>]*>(.*?)</div>\s*</div>', h, re.S)
if desc: d["vod_content"] = re.sub(r'<[^>]+>', '', desc.group(1)).strip()[:500]
if not d["vod_content"]:
desc2 = re.search(r'class="vod_content[^"]*"[^>]*>(.*?)</div>', h, re.S)
if desc2: d["vod_content"] = re.sub(r'<[^>]+>', '', desc2.group(1)).strip()[:500]
for mi in re.finditer(r'class="slide_info[^"]*"[^>]*>(.*?)</div>', h, re.S):
t = re.sub(r'<[^>]+>', '', mi.group(1)).strip()
if "导演" in t: d["vod_director"] = t.split("")[-1].split(":")[-1].strip()
elif "主演" in t: d["vod_actor"] = t.split("")[-1].split(":")[-1].strip()
elif "备注" in t or "更新" in t or "集数" in t: d["vod_remarks"] = t
try:
default_block = re.search(r'id="tab_con_playlist_(\d+)"[^>]*(?:style="display:block"|style="[^"]*"[^>]*>)(.*?)(?=<div\s+class="tab-content"|<div\s+id="tab_con_playlist_)', h, re.S)
if not default_block:
default_block = re.search(r'id="tab_con_playlist_(\d+)"[^>]*>(.*?)(?=<div\s+class="tab-content"|<div\s+id="tab_con_playlist_)', h, re.S)
default_pid = default_block.group(1) if default_block else "1"
default_content = default_block.group(2) if default_block else ""
tab_names = re.findall(r'<li[^>]*class="tab-switch[^"]*"[^>]*switch="tab_con_playlist_(\d+)"[^>]*>.*?<a[^>]*>\s*(.*?)\s*</a>', h, re.S)
default_name = "默认"
for pid, name in tab_names:
if pid == default_pid:
default_name = name.strip()
break
eps = re.findall(r'href="(/index\.php/vod/play/id/[^"]+/sid/\d+/nid/\d+\.html)"[^>]*>([^<]*)<', default_content)
el = []
for u, n in eps:
n = n.strip()
if n and "报错" not in n:
el.append(f"{n}${urljoin(HOST, u)}")
if not el:
play_eps = re.findall(r'href="(/index\.php/vod/play/id/\d+/sid/(\d+)/nid/\d+\.html)"[^>]*>([^<]*)<', h)
sources = {}
for href, sid, name in play_eps:
name = name.strip()
if not name or "报错" in name: continue
if sid not in sources:
sources[sid] = []
sources[sid].append(f"{name}${urljoin(HOST, href)}")
if default_pid in sources:
el = sources[default_pid]
if el:
d["vod_play_from"] = default_name
d["vod_play_url"] = "#".join(el)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
try:
resp = self.fetch(f"{HOST}/index.php/vod/search.html", params={"wd": str(key)}, headers={"User-Agent": UA}, timeout=15000)
html = resp.text if hasattr(resp, 'text') else str(resp)
if len(html) > 200:
return {"list": self._items(html)[:30]}
except:
pass
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
a, b = str(flag), str(id) if id else ""
if a.startswith("http") or "/vod/play/" in a or "/index.php/vod/play/" in a:
url = a
elif b.startswith("http") or "/vod/play/" in b or "/index.php/vod/play/" in b:
url = b
elif a.startswith("/"):
url = urljoin(HOST, a)
elif b.startswith("/"):
url = urljoin(HOST, b)
else:
url = a
try:
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"url": ""}
data = None
for tag in ['player_aaaa', 'player_data']:
idx = h.find(tag)
if idx < 0: continue
brace = h.find('{', idx)
if brace < 0: continue
depth, end = 0, brace
for i in range(brace, min(brace + 10000, len(h))):
if h[i] == '{': depth += 1
elif h[i] == '}':
depth -= 1
if depth == 0:
end = i + 1
break
try:
data = json.loads(h[brace:end])
except:
data = None
if data:
break
if data:
u = data.get("url", "")
if u:
if data.get("encrypt", 0) == 1:
try: u = unquote(base64.b64decode(u).decode("utf-8"))
except: pass
if u.startswith("http"):
return {"url": u}
return {"url": ""}
def localProxy(self, param):
pass
def _pagecount(self, html):
pc = 1
last = re.search(r'<a[^>]*href="[^"]*page/(\d+)\.html"[^>]*>尾页', html)
if last:
pc = max(pc, int(last.group(1)))
pages = re.findall(r'href="[^"]*page/(\d+)\.html"', html)
for p in pages:
try: pc = max(pc, int(p))
except: pass
return pc
def _items(self, html):
items, seen = [], set()
for m in re.finditer(r'<a[^>]*href="(/index\.php/vod/detail/id/(\d+)\.html)"[^>]*title="([^"]*)"', html):
vid, title = m.group(2), m.group(3)
if vid in seen: continue
seen.add(vid)
a_end = html.find('</a>', m.start())
if a_end < 0: continue
a_block = html[m.start():min(a_end + 4, m.start() + 2000)]
pic = re.search(r'data-original="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
if not pic:
pic = re.search(r'data-background="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
if not pic:
pic = re.search(r'<img[^>]*src="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
items.append({
"vod_id": vid, "vod_name": title,
"vod_pic": pic.group(1) if pic else "",
"vod_remarks": "",
"vod_url": urljoin(HOST, m.group(1)),
})
return items