Sync all projects
This commit is contained in:
@@ -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,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import requests, re, json, base64
|
||||
from urllib.parse import urljoin, quote, unquote
|
||||
|
||||
HOST = "https://www.moxy.top"
|
||||
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
|
||||
class Spider:
|
||||
def init(self, extend=""):
|
||||
global HOST
|
||||
try:
|
||||
resp = requests.get(HOST, headers={"User-Agent": UA}, timeout=15, allow_redirects=True)
|
||||
if resp.url and "moxy" not in resp.url:
|
||||
HOST = resp.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 = requests.get(HOST, headers={"User-Agent": UA}, timeout=30)
|
||||
resp.encoding = "utf-8"
|
||||
r["list"] = self._items(resp.text)[:60]
|
||||
except:
|
||||
pass
|
||||
return r
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {"list": self.homeContent().get("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:
|
||||
# 网站分页URL格式: /vodshow/{cid}--------{page}---.html
|
||||
if pn > 1:
|
||||
url = f"{HOST}/vodshow/{cid}--------{pn}---.html"
|
||||
else:
|
||||
url = f"{HOST}/vodshow/{cid}-----------.html"
|
||||
resp = requests.get(url, headers={"User-Agent": UA}, timeout=30)
|
||||
resp.encoding = "utf-8"
|
||||
items = self._items(resp.text)
|
||||
# 提取总页数
|
||||
pagecount = self._pagecount(resp.text)
|
||||
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):
|
||||
ids = str(ids) if ids else ""
|
||||
m = re.search(r'(\d+)', ids)
|
||||
vid = m.group(1) if m else ""
|
||||
if not vid: return {"list": []}
|
||||
try:
|
||||
resp = requests.get(f"{HOST}/voddetail{vid}.html", headers={"User-Agent": UA}, timeout=30)
|
||||
resp.encoding = "utf-8"
|
||||
except:
|
||||
return {"list": []}
|
||||
h = resp.text
|
||||
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)
|
||||
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:
|
||||
resp = requests.get(f"{HOST}/vodsearch/{quote(str(key))}-------------.html", headers={"User-Agent": UA}, timeout=15)
|
||||
resp.encoding = "utf-8"
|
||||
if len(resp.text) > 200:
|
||||
return {"list": self._items(resp.text)[:30]}
|
||||
except:
|
||||
pass
|
||||
return {"list": []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags=None):
|
||||
"""
|
||||
兼容两种壳子参数顺序:
|
||||
- FongMi/TV: playerContent(flag, id, vipFlags)
|
||||
- PeekPro/CatVod: playerContent(id, flag, vipFlags)
|
||||
自动检测哪个是播放页URL,哪个是flag
|
||||
"""
|
||||
a, b = str(flag), str(id) if id else ""
|
||||
# 自动判断:包含http或/vodplay的是URL,另一个是flag
|
||||
if a.startswith("http") or "/vodplay/" in a:
|
||||
url, flag_val = a, b
|
||||
elif b.startswith("http") or "/vodplay/" in b:
|
||||
url, flag_val = b, a
|
||||
elif a.startswith("/"):
|
||||
url, flag_val = urljoin(HOST, a), b
|
||||
elif b.startswith("/"):
|
||||
url, flag_val = urljoin(HOST, b), a
|
||||
else:
|
||||
url, flag_val = a, b
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers={"User-Agent": UA}, timeout=30)
|
||||
resp.encoding = "utf-8"
|
||||
except:
|
||||
return {"url": ""}
|
||||
pd = re.search(r'player_data\s*=\s*(\{.*?\})', resp.text, 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):
|
||||
return []
|
||||
|
||||
def _pagecount(self, html):
|
||||
"""从页面提取总页数"""
|
||||
pc = 1
|
||||
# 方法1: 尾页链接
|
||||
last = re.search(r'<a[^>]*href="[^"]*vodshow/\d+[^"]*(\d+)---\.html"[^>]*>尾页', html, re.S)
|
||||
if last:
|
||||
pc = max(pc, int(last.group(1)))
|
||||
# 方法2: page-link page-next 链接中的最大页码
|
||||
page_links = re.findall(r'<a[^>]*href="[^"]*vodshow/\d+-(\d+)', html)
|
||||
for p in page_links:
|
||||
try:
|
||||
n = int(p)
|
||||
if n > 100: # 年份过滤掉
|
||||
continue
|
||||
pc = max(pc, n)
|
||||
except:
|
||||
pass
|
||||
# 方法3: page-current 附近的页码
|
||||
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[^"]*"[^>]*>.*?</a>',
|
||||
html, re.S
|
||||
):
|
||||
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[^"]*"[^>]*>.*?</a>',
|
||||
html, re.S
|
||||
):
|
||||
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