Sync all projects
This commit is contained in:
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
@@ -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}
|
||||
@@ -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* .*?(?:<a[^>]*>[\s\S]*?</a>)\s*([^<]+)', h)
|
||||
if not desc_m:
|
||||
desc_m = re.search(r'class="fed-part-esan[^"]*"[\s\S]*?<span[^>]*>简介[::]?</span>\s* \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
|
||||
@@ -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 ''
|
||||
@@ -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(' ', ' ').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(' ', ' ').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(' ', ' ').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(' ', ' ').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(' ', ' ').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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user