Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-14 16:51:08 +00:00
parent 2775420f7c
commit fd64acd0eb
13 changed files with 2617 additions and 827 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+451
View File
@@ -0,0 +1,451 @@
# coding=utf-8
import re, json, requests
from urllib.parse import quote
from lxml import etree
from base.spider import Spider
class Spider(Spider):
def __init__(self):
self.name = "webstar"
self.host = "https://www.webstar.cn"
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)
r.encoding = 'utf-8'
return r.text
def _post(self, url, data=None):
r = requests.post(url, headers=self.header, data=data, timeout=15)
r.encoding = 'utf-8'
return r.text
def _fix_url(self, url):
if not url:
return ''
if url.startswith('//'):
return 'https:' + url
if url.startswith('/'):
return self.host + url
return url
def _parse_text(self, elem):
if elem is None:
return ''
return ''.join(elem.itertext()).strip()
def _parse_list_item(self, item):
a = item.xpath('.//a[contains(@class, "myui-vodlist__thumb")]')
if not a:
return None
a = a[0]
href = a.get('href', '')
m = re.search(r'/voddetail/(\d+)\.html', href)
if not m:
return None
vod_id = m.group(1)
vod_name = a.get('title', '').strip()
if not vod_name:
t = item.xpath('.//h4[contains(@class, "title")]//a/text()')
vod_name = t[0].strip() if t else ''
vod_pic = a.get('data-original', '')
if not vod_pic:
vod_pic = a.get('data-src', '')
if not vod_pic:
img = a.xpath('.//img')
if img:
vod_pic = img[0].get('data-original', '') or img[0].get('data-src', '') or img[0].get('src', '')
vod_pic = self._fix_url(vod_pic)
if vod_pic and vod_pic.startswith('data:image'):
vod_pic = ''
remark = item.xpath('.//span[contains(@class, "pic-text")]/text()')
vod_remarks = remark[0].strip() if remark else ''
if not vod_remarks:
score = item.xpath('.//span[contains(@class, "pic-tag-top")]/text()')
vod_remarks = score[0].strip() if score else ''
return {
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
}
def homeContent(self, filter):
result = {"class": []}
classes = [
{"type_name": "电影", "type_id": "1"},
{"type_name": "电视剧", "type_id": "2"},
{"type_name": "综艺", "type_id": "3"},
{"type_name": "动漫", "type_id": "4"},
{"type_name": "短剧", "type_id": "20"},
{"type_name": "动画", "type_id": "35"}
]
result["class"] = classes
filters = {}
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": "儿童"},
{"n": "网络电影", "v": "网络电影"}
]
year_vals = [
{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"},
{"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"},
{"n": "2021", "v": "2021"}, {"n": "2020", "v": "2020"},
{"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"},
{"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"},
{"n": "2015", "v": "2015"}, {"n": "2014", "v": "2014"},
{"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"},
{"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"},
{"n": "2009", "v": "2009"}, {"n": "2008", "v": "2008"},
{"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"},
{"n": "2005", "v": "2005"}, {"n": "2004", "v": "2004"},
{"n": "2003", "v": "2003"}, {"n": "2002", "v": "2002"},
{"n": "2001", "v": "2001"}, {"n": "2000", "v": "2000"}
]
lang_vals = [
{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"},
{"n": "英语", "v": "英语"}, {"n": "粤语", "v": "粤语"},
{"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"},
{"n": "德语", "v": "德语"}, {"n": "其它", "v": "其它"}
]
order_vals = [
{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"},
{"n": "评分", "v": "score"}
]
for c in classes:
filters[c['type_id']] = [
{"key": "area", "name": "地区", "value": area_vals},
{"key": "class", "name": "类型", "value": class_vals},
{"key": "year", "name": "年份", "value": year_vals},
{"key": "lang", "name": "语言", "value": lang_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)
items = root.xpath('//div[contains(@class, "myui-vodlist__box")]')
for item in items:
try:
video = self._parse_list_item(item)
if video:
videos.append(video)
except Exception:
pass
except Exception:
pass
return {"list": 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', '')
order = extend.get('order', 'time')
area = quote(area) if area else ''
cls = quote(cls) if cls else ''
year = quote(year) if year else ''
lang = quote(lang) if lang else ''
order = quote(order) if order else 'time'
# 456电影网 vodshow URL 12 段:0类型 1地区 2排序 3类型 4语言 5字母 6-7空 8页码 9-10空 11年份
segments = [str(tid), area, order, cls, lang, '', '', '', str(pg), '', '', year]
url = f"{self.host}/vodshow/{'-'.join(segments)}.html"
html = self._get(url)
root = etree.HTML(html)
items = root.xpath('//div[contains(@class, "myui-vodlist__box")]')
for item in items:
try:
video = self._parse_list_item(item)
if video:
videos.append(video)
except Exception:
pass
total_pages = 1
page_links = root.xpath('//ul[contains(@class,"myui-page")]//a/@href')
for href in page_links:
m = re.search(r'/(?:vodshow|vodsearch)/[^-]*(?:-[^-]*)*-(\d+)---\.html', href)
if m:
total_pages = max(total_pages, int(m.group(1)))
if not page_links:
# 无分页时默认只一页
total_pages = 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}/voddetail/{vod_id}.html"
html = self._get(detail_url)
root = etree.HTML(html)
vod_name = ''
title_elem = root.xpath('//div[contains(@class,"myui-content__detail")]//h1[@class="title"]')
if title_elem:
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_pic = ''
thumb = root.xpath('//div[contains(@class,"myui-content__thumb")]//img')
if thumb:
vod_pic = thumb[0].get('data-original', '') or thumb[0].get('data-src', '') or thumb[0].get('src', '')
vod_pic = self._fix_url(vod_pic)
vod_year = ''
vod_area = ''
data_ps = root.xpath('//div[contains(@class,"myui-content__detail")]//p[contains(@class,"data")]')
for p in data_ps:
txt = self._parse_text(p)
if '年份:' in txt and not vod_year:
year_links = p.xpath('.//a/text()')
for y in year_links:
y = y.strip()
if re.match(r'^\d{4}$', y):
vod_year = y
break
if '地区:' in txt and not vod_area:
area_links = p.xpath('.//a/text()')
for a in area_links:
a = a.strip()
if a:
vod_area = a
break
vod_actor = ''
actor_elem = root.xpath('//div[contains(@class,"myui-content__detail")]//p[contains(@class,"data") and contains(.,"主演:")]')
if actor_elem:
actors = actor_elem[0].xpath('.//a/text()')
vod_actor = ' '.join([a.strip() for a in actors if a.strip()])
vod_director = ''
director_elem = root.xpath('//div[contains(@class,"myui-content__detail")]//p[contains(@class,"data") and contains(.,"导演:")]')
if director_elem:
directors = director_elem[0].xpath('.//a/text()')
vod_director = ' '.join([d.strip() for d in directors if d.strip()])
vod_content = ''
desc_elem = root.xpath('//div[contains(@class,"text-collapse")]//span[contains(@class,"data")]')
if desc_elem:
vod_content = self._parse_text(desc_elem[0])
if not vod_content:
desc_elem = root.xpath('//div[contains(@class,"text-collapse")]')
if desc_elem:
vod_content = self._parse_text(desc_elem[0])
vod_content = re.sub(r'想要看更多的.*$', '', vod_content).strip()
vod_play_from = []
vod_play_url = []
# 先尝试带 tab 的播放列表结构
source_tabs = root.xpath('//div[contains(@class,"myui-panel__head")]//ul[contains(@class,"nav-tabs")]//a')
tab_panes = root.xpath('//div[contains(@class,"tab-content")]//div[contains(@class,"tab-pane")]')
if source_tabs and tab_panes:
for idx, tab in enumerate(source_tabs):
source_name = self._parse_text(tab)
source_name = re.sub(r'\s+', ' ', source_name).strip()
if not source_name:
source_name = f"线路{idx + 1}"
if idx < len(tab_panes):
pane = tab_panes[idx]
else:
continue
links = pane.xpath('.//ul[contains(@class,"myui-content__list")]//a')
play_list = []
for a in links:
ep_name = a.text or a.get('title', '')
ep_name = ep_name.strip()
href = a.get('href', '')
if not ep_name or not href:
continue
play_url = self._fix_url(href)
play_list.append(f"{ep_name}${play_url}")
if play_list:
vod_play_from.append(source_name)
vod_play_url.append("#".join(play_list))
else:
# 兜底:直接取播放地址列表
links = root.xpath('//ul[contains(@class,"myui-content__list")]//a')
play_list = []
for a in links:
ep_name = a.text or a.get('title', '')
ep_name = ep_name.strip()
href = a.get('href', '')
if not ep_name or not href:
continue
play_url = self._fix_url(href)
play_list.append(f"{ep_name}${play_url}")
if play_list:
vod_play_from.append("默认")
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_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):
try:
m = re.search(r'var\s+player_aaaa\s*=\s*\{', html)
if not m:
return None
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
player_data = json.loads(html[start:i])
return player_data.get('url', '')
except Exception:
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 = real_url.replace('\\/', '/')
if real_url.startswith('//'):
real_url = 'https:' + 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)}
iframe_match = re.search(r'<iframe[^>]+src\s*=\s*"([^"]+)"', html)
if iframe_match:
real_url = iframe_match.group(1)
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)}
m3u8_match = re.search(r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']', html)
if m3u8_match:
real_url = m3u8_match.group(1)
return {"parse": 0, "playUrl": "", "url": real_url, "header": json.dumps(self.header)}
mp4_match = re.search(r'["\'](https?://[^"\']+\.(?:mp4|flv|ts))["\']', html)
if mp4_match:
real_url = mp4_match.group(1)
return {"parse": 0, "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:
qkey = quote(key)
if str(pg) == '1':
url = f"{self.host}/vodsearch/{qkey}-------------.html"
else:
url = f"{self.host}/vodsearch/{qkey}--------{pg}---.html"
html = self._get(url)
root = etree.HTML(html)
items = root.xpath('//div[contains(@class, "myui-vodlist__box")]')
for item in items:
try:
video = self._parse_list_item(item)
if video:
videos.append(video)
except Exception:
pass
total_pages = 1
page_links = root.xpath('//ul[contains(@class,"myui-page")]//a/@href')
for href in page_links:
m = re.search(r'/vodsearch/[^/]+-(\d+)---\.html', href)
if m:
total_pages = max(total_pages, int(m.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 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
+207
View File
@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
import sys
import re
import requests
from urllib.parse import quote
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = "https://www.ddys24.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36",
"Referer": self.host + "/",
"Origin": self.host
}
def getName(self):
return "低端影视"
def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|flv|avi|mkv|mov|ts)(\?|$)', url or "", re.I))
def manualVideoCheck(self):
return False
def homeContent(self, filter):
return {
"class": [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "3", "type_name": "综艺"},
{"type_id": "4", "type_name": "动漫"},
{"type_id": "42", "type_name": "豆瓣Top250"}
]
}
def homeVideoContent(self):
return {"list": self.parseList(self.get(self.host + "/"))}
def categoryContent(self, tid, pg, filter, extend):
url = self.host + "/ddvodtype/" + str(tid) + ".html" if str(pg) == "1" else self.host + "/ddvodtype/" + str(tid) + "-" + str(pg) + ".html"
html = self.get(url)
if not html and str(pg) != "1":
html = self.get(self.host + "/ddvodshow/" + str(tid) + "--------" + str(pg) + "---.html")
return {
"page": int(pg),
"pagecount": 999,
"limit": 24,
"total": 999999,
"list": self.parseList(html)
}
def detailContent(self, ids):
vid = ids[0]
url = vid if str(vid).startswith("http") else self.host + "/ddvoddetail/" + str(vid) + ".html"
html = self.get(url)
name = self.clean(self.match(html, r'<h1[^>]*class=["\']title["\'][^>]*>(.*?)</h1>') or self.match(html, r'<title>《?([^《》_]+?)》?'))
pic = self.fix(self.match(html, r'<img[^>]+data-original=["\']([^"\']+)') or self.match(html, r'<a[^>]+class=["\'][^"\']*v-thumb[^"\']*["\'][^>]+data-original=["\']([^"\']+)'))
remarks = self.clean(self.match(html, r'<span class=["\']pic-text[^"\']*["\'][^>]*>(.*?)</span>'))
desc = self.clean(self.match(html, r'<span class=["\']detail-content["\'][^>]*>(.*?)</span>') or self.match(html, r'<span class=["\']detail-sketch["\'][^>]*>(.*?)</span>') or self.match(html, r'<meta name=["\']description["\'] content=["\']([^"\']+)'))
actor = self.clean(self.match(html, r'主演:</span>([\s\S]*?)</p>'))
director = self.clean(self.match(html, r'导演:</span>([\s\S]*?)</p>'))
area = self.clean(self.match(html, r'地区:</span>([\s\S]*?)<span'))
year = self.clean(self.match(html, r'年份:</span>([\s\S]*?)</p>'))
play_from = []
play_url = []
blocks = re.findall(r'<div class="stui-pannel-ddy1102-cbox b playlist mb">([\s\S]*?)</div>\s*</div>\s*</div>', html)
if not blocks:
blocks = re.findall(r'<ul class="stui-content_ddy1102-cplaylist clearfix">([\s\S]*?)</ul>', html)
for i, block in enumerate(blocks):
eps = []
for m in re.finditer(r'<a[^>]+href=["\']([^"\']*?/ddvodplay/[^"\']+)["\'][^>]*>(.*?)</a>', block):
u = self.fix(m.group(1))
t = self.clean(m.group(2)) or "播放"
if u:
eps.append(t + "$" + u)
if eps:
line = self.clean(self.match(block, r'<h3 class=["\']title["\'][^>]*>(.*?)</h3>')) or "线路" + str(i + 1)
play_from.append(line)
play_url.append("#".join(eps))
if not play_url:
eps = []
for m in re.finditer(r'<a[^>]+href=["\']([^"\']*?/ddvodplay/' + str(vid) + r'-[^"\']+)["\'][^>]*>(.*?)</a>', html):
u = self.fix(m.group(1))
t = self.clean(m.group(2)) or "播放"
if u:
eps.append(t + "$" + u)
if eps:
play_from.append("线路W")
play_url.append("#".join(eps))
if not play_url:
u = self.fix(self.match(html, r'href=["\']([^"\']*?/ddvodplay/' + str(vid) + r'-1-1\.html)["\']'))
if u:
play_from.append("线路W")
play_url.append("播放$" + u)
return {
"list": [{
"vod_id": vid,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks,
"vod_year": year,
"vod_area": area,
"vod_actor": actor,
"vod_director": director,
"vod_content": desc,
"vod_play_from": "$$$".join(play_from),
"vod_play_url": "$$$".join(play_url)
}]
}
def searchContent(self, key, quick, pg="1"):
k = quote(key)
urls = [
self.host + "/ddvodsearch/" + k + "-------------.html",
self.host + "/ddvodsearch/-------------.html?wd=" + k,
self.host + "/index.php/vod/search.html?wd=" + k,
self.host + "/vodsearch/" + k + "-------------.html"
]
html = ""
for u in urls:
html = self.get(u)
if "ddvoddetail" in html:
break
return {"list": self.parseList(html), "page": int(pg)}
def playerContent(self, flag, id, vipFlags):
return {"parse": 1, "url": id, "header": self.headers}
def localProxy(self, param):
return [404, "text/plain", "", ""]
def destroy(self):
return "正在Destroy"
def get(self, url):
try:
r = requests.get(url, headers=self.headers, timeout=15)
r.encoding = r.apparent_encoding or "utf-8"
return r.text
except Exception:
return ""
def match(self, text, rule):
m = re.search(rule, text or "", re.S)
return m.group(1) if m else ""
def clean(self, text):
return re.sub(r"\s+", " ", re.sub(r"<.*?>", "", text or "")).replace("&nbsp;", " ").replace("&amp;", "&").strip()
def fix(self, url):
if not url:
return ""
if url.startswith("//"):
return "https:" + url
if url.startswith("/"):
return self.host + url
return url
def parseList(self, html):
res = []
seen = set()
for m in re.finditer(r'<a[^>]+class=["\'][^"\']*stui-vodlist_ddy1102-cthumb[^"\']*["\'][^>]+href=["\']([^"\']*?/ddvoddetail/(\d+)\.html)["\'][^>]*title=["\']([^"\']+)["\'][^>]*(?:data-original=["\']([^"\']+)["\'])?[\s\S]*?</a>', html or "", re.S):
vid = m.group(2)
if vid in seen:
continue
seen.add(vid)
block = m.group(0)
pic = m.group(4) or self.match(block, r'data-original=["\']([^"\']+)') or self.match(block, r'src=["\']([^"\']+\.(?:jpg|jpeg|png|webp|gif)[^"\']*)')
remarks = self.clean(self.match(block, r'<span class=["\']pic-text[^"\']*["\'][^>]*>(.*?)</span>'))
name = self.clean(m.group(3))
if name:
res.append({
"vod_id": vid,
"vod_name": name,
"vod_pic": self.fix(pic),
"vod_remarks": remarks
})
if not res:
for m in re.finditer(r'href=["\']([^"\']*?/ddvoddetail/(\d+)\.html)["\'][^>]*title=["\']([^"\']+)["\'][\s\S]{0,500}?(?:data-original|src)=["\']([^"\']+)["\']', html or "", re.S):
vid = m.group(2)
if vid in seen:
continue
seen.add(vid)
res.append({
"vod_id": vid,
"vod_name": self.clean(m.group(3)),
"vod_pic": self.fix(m.group(4)),
"vod_remarks": ""
})
if not res:
for m in re.finditer(r'href=["\']([^"\']*?/ddvoddetail/(\d+)\.html)["\'][^>]*>([\s\S]{0,120}?)</a>', html or "", re.S):
vid = m.group(2)
if vid in seen:
continue
name = self.clean(m.group(3))
if not name:
continue
seen.add(vid)
res.append({
"vod_id": vid,
"vod_name": name,
"vod_pic": "",
"vod_remarks": ""
})
return res
+474
View File
@@ -0,0 +1,474 @@
# coding=utf-8
import re
import json
import requests
from urllib.parse import quote
from lxml import etree
from base.spider import Spider
class Spider(Spider):
def __init__(self):
self.name = "btdy"
self.host = "https://dy.8ttv.cn"
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)
r.encoding = 'utf-8'
return r.text
def _post(self, url, data=None):
r = requests.post(url, headers=self.header, data=data, timeout=15)
r.encoding = 'utf-8'
return r.text
def _fix_url(self, url):
if not url:
return ''
if url.startswith('//'):
return 'https:' + url
if url.startswith('/'):
return self.host + url
return url
@staticmethod
def _parse_text(elem):
if elem is None:
return ''
return ''.join(elem.itertext()).strip()
def _parse_pic(self, elem):
pic = ''
if elem is None:
return pic
if elem.tag == 'img':
pic = elem.get('data-original') or elem.get('data-src') or elem.get('src', '')
else:
imgs = elem.xpath('.//img')
if imgs:
pic = imgs[0].get('data-original') or imgs[0].get('data-src') or imgs[0].get('src', '')
if not pic:
return ''
if pic.startswith('data:image') or 'alicdn.com' in pic:
return ''
return self._fix_url(pic)
def _parse_poster_item(self, a):
href = a.get('href', '')
m = re.search(r'/detail/id/(\d+)\.html', href)
if not m:
return None
vod_id = m.group(1)
vod_name = a.get('title', '').strip()
if not vod_name:
t = a.xpath('.//div[contains(@class,"module-poster-item-title")]/text()')
vod_name = t[0].strip() if t else ''
vod_pic = self._parse_pic(a)
remark = a.xpath('.//div[contains(@class,"module-item-note")]/text()')
vod_remarks = remark[0].strip() if remark else ''
return {
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
}
def homeContent(self, filter):
result = {"class": []}
classes = [
{"type_name": "电影", "type_id": "1"},
{"type_name": "电视剧", "type_id": "2"},
{"type_name": "综艺", "type_id": "3"},
{"type_name": "动漫", "type_id": "4"},
{"type_name": "短剧", "type_id": "5"},
{"type_name": "纪录片", "type_id": "20"}
]
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": "儿童"},
{"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": "其他", "v": "其他"})
by_vals = [
{"n": "最新", "v": "time"},
{"n": "最热", "v": "hits"},
{"n": "高分", "v": "score"}
]
filters = {}
for c in classes:
filters[c['type_id']] = [
{"key": "class", "name": "类型", "value": class_vals},
{"key": "area", "name": "地区", "value": area_vals},
{"key": "year", "name": "年份", "value": year_vals},
{"key": "by", "name": "排序", "value": by_vals}
]
result["filters"] = filters
return result
def homeVideoContent(self):
videos = []
try:
html = self._get(self.host)
root = etree.HTML(html)
items = root.xpath('//a[contains(@class,"module-poster-item") and contains(@class,"module-item")]')
seen = set()
for a in items:
try:
video = self._parse_poster_item(a)
if video and video['vod_id'] not in seen:
videos.append(video)
seen.add(video['vod_id'])
except Exception:
pass
except Exception:
pass
return {"list": 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 = {}
cls = extend.get('class', '')
area = extend.get('area', '')
year = extend.get('year', '')
by = extend.get('by', 'time')
segments = []
if cls:
segments.append(f"class/{quote(cls)}")
if area:
segments.append(f"area/{quote(area)}")
if year:
segments.append(f"year/{year}")
if by:
segments.append(f"by/{quote(by)}")
url = f"{self.host}/index.php/vod/show/"
if segments:
url += '/'.join(segments) + '/'
url += f"id/{tid}"
if int(pg) > 1:
url += f"/page/{pg}"
url += ".html"
html = self._get(url)
root = etree.HTML(html)
items = root.xpath('//a[contains(@class,"module-poster-item") and contains(@class,"module-item")]')
for a in items:
try:
video = self._parse_poster_item(a)
if video:
videos.append(video)
except Exception:
pass
total_pages = 1
page_hrefs = root.xpath('//a[contains(@class,"page-link")]/@href')
for href in page_hrefs:
m = re.search(r'/page/(\d+)\.html', href)
if m:
total_pages = max(total_pages, int(m.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}/index.php/vod/detail/id/{vod_id}.html"
html = self._get(detail_url)
root = etree.HTML(html)
vod_name = ''
h1 = root.xpath('//div[contains(@class,"module-info-heading")]//h1/text()')
if h1:
vod_name = h1[0].strip()
if not vod_name:
title = root.xpath('//title/text()')
if title:
vod_name = title[0].split('-')[0].strip()
vod_pic = ''
poster = root.xpath('//div[contains(@class,"module-info-poster")]')
if poster:
vod_pic = self._parse_pic(poster[0])
vod_year = ''
vod_area = ''
area_set = {'中国大陆', '中国香港', '中国台湾', '美国', '日本', '韩国', '泰国',
'英国', '法国', '德国', '意大利', '西班牙', '加拿大', '印度', '其他'}
for a in root.xpath('//div[contains(@class,"module-info-tag")]//a'):
txt = self._parse_text(a)
title = a.get('title', '')
if not vod_year and (re.match(r'^\d{4}$', txt) or re.match(r'^\d{4}$', title)):
vod_year = txt or title
if not vod_area and txt in area_set:
vod_area = txt
vod_actor = ''
vod_director = ''
info_items = root.xpath('//div[contains(@class,"module-info-item")]')
for item in info_items:
label = item.xpath('.//span[contains(@class,"module-info-item-title")]/text()')
if not label:
continue
label_text = label[0].strip()
content_elems = item.xpath('.//div[contains(@class,"module-info-item-content")]')
if not content_elems:
continue
content = content_elems[0]
if '导演' in label_text:
directors = content.xpath('.//a/text()')
vod_director = ' '.join([d.strip() for d in directors if d.strip()])
elif '主演' in label_text:
actors = content.xpath('.//a/text()')
vod_actor = ' '.join([a.strip() for a in actors if a.strip()])
vod_content = ''
intro = root.xpath('//div[contains(@class,"module-info-introduction-content")]//p')
if intro:
vod_content = self._parse_text(intro[0])
vod_content = re.sub(r'\[.*?\]', '', vod_content).strip()
vod_play_from = []
vod_play_url = []
source_tabs = root.xpath('//div[@id="y-playList"]//div[contains(@class,"module-tab-item")]')
list_boxes = root.xpath('//div[contains(@class,"module-list") and contains(@class,"tab-list")]')
for idx, tab in enumerate(source_tabs):
source_name = tab.get('data-dropdown-value', '').strip()
if not source_name:
spans = tab.xpath('.//span')
source_name = self._parse_text(spans[0]) if spans else ''
source_name = re.sub(r'\s+', ' ', source_name).strip()
if not source_name:
continue
if idx >= len(list_boxes):
continue
box = list_boxes[idx]
links = box.xpath('.//a[contains(@class,"module-play-list-link")]')
play_list = []
for a in links:
ep_name = ''
spans = a.xpath('.//span/text()')
if spans:
ep_name = spans[0].strip()
if not ep_name:
ep_name = a.get('title', '').strip()
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 not vod_play_from:
links = root.xpath('//div[contains(@class,"module-play-list")]//a[contains(@class,"module-play-list-link")]')
play_list = []
for a in links:
ep_name = ''
spans = a.xpath('.//span/text()')
if spans:
ep_name = spans[0].strip()
if not ep_name:
ep_name = a.get('title', '').strip()
href = a.get('href', '')
if ep_name and href:
play_list.append(f"{ep_name}${self._fix_url(href)}")
if play_list:
vod_play_from.append("默认")
vod_play_url.append('#'.join(play_list))
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_play_from": "$$$".join(vod_play_from),
"vod_play_url": "$$$".join(vod_play_url)
}
return {'list': [detail]}
except Exception:
return {'list': []}
def _extract_player_data(self, html):
try:
m = re.search(r'var\s+player_aaaa\s*=\s*\{', html)
if not m:
return None
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
data = json.loads(html[start:i])
return data
except Exception:
return None
def playerContent(self, flag, id, vipFlags):
try:
html = self._get(id)
data = self._extract_player_data(html)
if data and data.get('url'):
real_url = data['url']
if real_url.startswith('//'):
real_url = 'https:' + 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)}
iframe_match = re.search(r'<iframe[^>]+src\s*=\s*"([^"]+)"', html)
if iframe_match:
real_url = iframe_match.group(1)
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)}
m3u8_match = re.search(r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']', html)
if m3u8_match:
return {"parse": 0, "playUrl": "", "url": m3u8_match.group(1), "header": json.dumps(self.header)}
mp4_match = re.search(r'["\'](https?://[^"\']+\.(?:mp4|flv|ts))["\']', html)
if mp4_match:
return {"parse": 0, "playUrl": "", "url": mp4_match.group(1), "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:
url = f"{self.host}/index.php/vod/search/wd/{quote(key)}.html"
html = self._get(url)
root = etree.HTML(html)
items = root.xpath('//div[contains(@class,"module-card-item")]')
for item in items:
try:
a = item.xpath('.//div[contains(@class,"module-card-item-title")]//a')[0]
href = a.get('href', '')
m = re.search(r'/detail/id/(\d+)\.html', href)
if not m:
continue
vod_id = m.group(1)
vod_name = self._parse_text(a)
poster_a = item.xpath('.//a[contains(@class,"module-card-item-poster")]')
vod_pic = self._parse_pic(poster_a[0]) if poster_a else ''
remark = item.xpath('.//div[contains(@class,"module-item-note")]/text()')
vod_remarks = remark[0].strip() if remark else ''
videos.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
except Exception:
pass
return {
'list': videos,
'page': int(pg),
'pagecount': 1,
'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
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
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
logger = logging.getLogger(__name__)
class Spider(BaseSpider):
"""映像星球爬虫 - 适配 MxPro CMS"""
BASE_URL = "https://www.yxxq41.cc"
HEADERS = {
"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,en;q=0.8",
}
CATEGORY_MAP = {
"1": "电影",
"2": "电视剧",
"3": "综艺",
"4": "动漫",
"7": "纪录片",
"39": "短剧",
"53": "体育",
}
def __init__(self):
try:
super().__init__()
except Exception:
pass
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
def init(self, extend):
pass
def getName(self):
return "映像星球"
def homeContent(self, filter=False):
"""首页内容"""
try:
url = f"{self.BASE_URL}/"
resp = self.session.get(url, timeout=30)
resp.raise_for_status()
resp.encoding = 'utf-8'
html = resp.text
classes = []
for cate_id, cate_name in self.CATEGORY_MAP.items():
classes.append({
"type_id": cate_id,
"type_name": cate_name,
})
soup = BeautifulSoup(html, 'html.parser')
videos = []
seen_ids = set()
for item in soup.select('a.module-poster-item'):
if item.get('data-ad-slot') or 'mac-ad-card' in item.get('class', []):
continue
href = item.get('href', '')
m = re.search(r'/html/(\d+)\.html', href)
if not m:
continue
vid = m.group(1)
if vid in seen_ids:
continue
seen_ids.add(vid)
title_el = item.select_one('.module-poster-item-title')
title = title_el.get_text(strip=True) if title_el else ''
pic_el = item.select_one('.module-item-pic img')
pic = ''
if pic_el:
pic = pic_el.get('data-original', '') or pic_el.get('src', '')
note_el = item.select_one('.module-item-note')
remark = note_el.get_text(strip=True) if note_el else ''
videos.append({
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_remarks": remark,
})
if len(videos) >= 36:
break
return {
"class": classes,
"list": videos,
}
except Exception as e:
logger.error(f"获取首页失败: {e}")
return {}
def homeVideoContent(self):
home = self.homeContent()
return {"list": home.get("list", [])}
def categoryContent(self, tid, pg, filter, ext):
"""分类内容"""
try:
page = int(pg) if pg else 1
if page == 1:
url = f"{self.BASE_URL}/list/{tid}.html"
else:
url = f"{self.BASE_URL}/list/{tid}-{page}.html"
resp = self.session.get(url, timeout=30)
resp.raise_for_status()
resp.encoding = 'utf-8'
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
total = 0
total_page = 1
vod_list = []
for item in soup.select('a.module-poster-item'):
if item.get('data-ad-slot') or 'mac-ad-card' in item.get('class', []):
continue
href = item.get('href', '')
m = re.search(r'/html/(\d+)\.html', href)
if not m:
continue
vid = m.group(1)
title_el = item.select_one('.module-poster-item-title')
title = title_el.get_text(strip=True) if title_el else ''
pic_el = item.select_one('.module-item-pic img')
pic = ''
if pic_el:
pic = pic_el.get('data-original', '') or pic_el.get('src', '')
note_el = item.select_one('.module-item-note')
remark = note_el.get_text(strip=True) if note_el else ''
vod_list.append({
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_remarks": remark,
})
m_total = re.search(r'共(\d+)条', html)
if m_total:
total = int(m_total.group(1))
for a in soup.select('.module-page a'):
txt = a.get_text(strip=True)
if txt.isdigit():
num = int(txt)
if num > total_page:
total_page = num
return {
"list": vod_list,
"page": page,
"pagecount": total_page,
"limit": 20,
"total": total,
}
except Exception as e:
logger.error(f"获取分类内容失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
def detailContent(self, ids):
"""详情内容 - 精确提取播放列表"""
try:
vod_id = ids[0] if isinstance(ids, list) else str(ids)
url = f"{self.BASE_URL}/html/{vod_id}.html"
resp = self.session.get(url, timeout=30)
resp.raise_for_status()
resp.encoding = 'utf-8'
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
# 基本信息
name = ''
name_el = soup.select_one('.module-info-heading h1')
if name_el:
name = name_el.get_text(strip=True)
pic = ''
pic_el = soup.select_one('.module-info-poster .module-item-pic img')
if pic_el:
pic = pic_el.get('data-original', '') or pic_el.get('src', '')
year = ''
area = ''
type_name = ''
for tl in soup.select('.module-info-tag-link a'):
txt = tl.get_text(strip=True)
if re.search(r'20\d{2}', txt) and not year:
year = txt
if re.search(r'(大陆|香港|台湾|韩国|日本|美国|欧美|海外|国产|泰国|印度)', txt) and not area:
area = txt
if not type_name:
type_name = txt
remark = ''
content = ''
for item in soup.select('.module-info-item'):
title_el = item.select_one('.module-info-item-title')
if title_el:
t = title_el.get_text(strip=True)
content_el = item.select_one('.module-info-item-content')
if content_el and '备注' in t:
remark = content_el.get_text(strip=True)
desc_div = soup.select_one('.module-info-introduction-content')
if desc_div:
content = desc_div.get_text(strip=True)
# ===== 精确提取播放列表 =====
play_sources = []
# 获取播放源名称
source_names = []
for tab in soup.select('#y-playList .tab-item'):
name_tmp = tab.get_text(strip=True)
if name_tmp:
source_names.append(name_tmp)
# 获取 play-list 分组 (class="tab-list his-tab-list")
play_panels = soup.select('.tab-list.his-tab-list')
if source_names and play_panels:
for i, src_name in enumerate(source_names):
episodes = []
if i < len(play_panels):
for link in play_panels[i].select('a.module-play-list-link'):
href = link.get('href', '')
ep_name_el = link.select_one('span')
ep_name = ep_name_el.get_text(strip=True) if ep_name_el else ''
if href and ep_name:
episodes.append((ep_name, href))
if episodes:
play_sources.append((src_name, episodes))
# 备用:正则提取
if not play_sources:
all_links = re.findall(
r'<a class="module-play-list-link"[^>]*href="(/play/\d+-\d+-\d+\.html)"[^>]*>.*?<span>(.*?)</span>',
html, re.DOTALL
)
if all_links:
src_names = source_names or ['线路①']
for sn in src_names:
episodes = [(n.strip(), h) for h, n in all_links]
if episodes:
play_sources.append((sn, episodes))
break
play_from_list = []
play_url_list = []
for src_name, episodes in play_sources:
play_from_list.append(src_name)
ep_list = []
for ep_name, href in episodes:
full_url = href if href.startswith('http') else f"{self.BASE_URL}{href}"
ep_list.append(f"{ep_name}${full_url}")
play_url_list.append('#'.join(ep_list))
vod_item = {
"vod_id": vod_id,
"vod_name": name,
"vod_pic": pic,
"type_name": type_name,
"vod_year": year,
"vod_area": area,
"vod_remarks": remark,
"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 playerContent(self, flag, id, vipFlags):
"""播放内容 - 解析真实m3u8地址"""
try:
play_url = urllib.parse.unquote(id) if id else ''
if not play_url:
return {"parse": 0, "url": ""}
if not play_url.startswith('http'):
play_url = f"{self.BASE_URL}{play_url}" if play_url.startswith('/') else f"{self.BASE_URL}/{play_url}"
resp = self.session.get(play_url, timeout=30)
resp.raise_for_status()
resp.encoding = 'utf-8'
html = resp.text
m = re.search(r'player_aaaa\s*=\s*({.*?});', html, re.DOTALL)
if m:
try:
data = json.loads(m.group(1))
real_url = data.get('url', '')
if real_url:
return {
"parse": 0,
"url": real_url,
"header": json.dumps(self.HEADERS),
}
except Exception:
pass
return {
"parse": 1,
"url": play_url,
}
except Exception as e:
logger.error(f"解析播放失败: {e}")
return {"parse": 0, "url": ""}
def searchContent(self, key, quick, pg):
"""搜索内容"""
try:
page = int(pg) if pg else 1
encoded_key = urllib.parse.quote(key)
url = f"{self.BASE_URL}/search/{encoded_key}-------------{page}.html"
resp = self.session.get(url, timeout=30)
resp.raise_for_status()
resp.encoding = 'utf-8'
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
vod_list = []
seen_ids = set()
for item in soup.select('.module-item'):
detail_link = item.select_one('a[href*="/html/"]')
if not detail_link:
continue
href = detail_link.get('href', '')
m = re.search(r'/html/(\d+)\.html', href)
if not m:
continue
vid = m.group(1)
if vid in seen_ids:
continue
seen_ids.add(vid)
title = detail_link.get('title', '') or detail_link.get_text(strip=True)
pic = ''
img = item.select_one('img')
if img:
pic = img.get('data-original', '') or img.get('src', '')
remark = ''
remark_el = item.select_one('.module-item-note, .video-note, .note')
if remark_el:
remark = remark_el.get_text(strip=True)
vod_list.append({
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_remarks": remark,
})
if not vod_list:
for link in soup.select('a[href*="/html/"]'):
href = link.get('href', '')
m = re.search(r'/html/(\d+)\.html', href)
if not m:
continue
vid = m.group(1)
if vid in seen_ids:
continue
seen_ids.add(vid)
title = link.get_text(strip=True)
if not title:
continue
vod_list.append({
"vod_id": vid,
"vod_name": title,
"vod_pic": '',
"vod_remarks": '',
})
total = 0
m_total = re.search(r'找到(\d+)部影片', html)
if m_total:
total = int(m_total.group(1))
else:
total = len(vod_list)
return {
"list": vod_list,
"page": page,
"pagecount": 1,
"limit": 20,
"total": total,
}
except Exception as e:
logger.error(f"搜索失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
def localProxy(self, param):
return []
def main():
spider = Spider()
print("=" * 60)
print("【1】测试首页")
home = spider.homeContent()
print(f"分类: {len(home.get('class', []))}, 推荐: {len(home.get('list', []))}")
for v in home.get('list', [])[:5]:
print(f" - {v['vod_name']} [{v['vod_remarks']}]")
print("\n" + "=" * 60)
print("【2】测试分类 (电影)")
cat = spider.categoryContent("1", "1", False, {})
print(f"总数: {cat.get('total')}, 本页: {len(cat.get('list', []))}")
for v in cat.get('list', [])[:5]:
print(f" - {v['vod_name']} [{v['vod_remarks']}]")
print("\n" + "=" * 60)
print("【3】测试详情 (23026 给阿嬷的情书)")
detail = spider.detailContent(["23026"])
if detail.get('list'):
d = detail['list'][0]
print(f"标题: {d['vod_name']}")
play_from = d.get('vod_play_from', '')
play_url = d.get('vod_play_url', '')
sources = play_from.split('$$$')
urls = play_url.split('$$$') if play_url else []
print(f"播放源: {len(sources)}")
for i, src in enumerate(sources):
ep_count = len(urls[i].split('#')) if i < len(urls) else 0
print(f" - {src}: {ep_count}")
print("\n" + "=" * 60)
print("【4】测试播放解析")
if detail.get('list') and detail['list'][0].get('vod_play_url'):
first = detail['list'][0]['vod_play_url'].split('$$$')[0].split('#')[0]
if '$' in first:
ep_url = first.split('$')[1]
play = spider.playerContent('', ep_url, [])
print(f"解析结果: {play.get('url', '')[:80]}...")
print("\n" + "=" * 60)
print("【5】测试搜索")
search = spider.searchContent("战狼", False, "1")
print(f"结果: {search.get('total')}")
for v in search.get('list', [])[:5]:
print(f" - {v['vod_name']}")
print("\n完成!")
if __name__ == "__main__":
main()
+173
View File
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
import sys
import re
import requests
from urllib.parse import quote
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = "https://citapa.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36",
"Referer": self.host + "/",
"Origin": self.host
}
def getName(self):
return "茶杯狐"
def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|flv|avi|mkv|mov|ts)(\?|$)', url or "", re.I))
def manualVideoCheck(self):
return False
def homeContent(self, filter):
return {
"class": [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "3", "type_name": "综艺"},
{"type_id": "4", "type_name": "动漫"}
]
}
def homeVideoContent(self):
return {"list": self.parseList(self.get(self.host + "/"))}
def categoryContent(self, tid, pg, filter, extend):
html = self.get(self.host + "/search.php?searchtype=5&tid=" + str(tid) + "&page=" + str(pg))
return {
"page": int(pg),
"pagecount": 999,
"limit": 24,
"total": 999999,
"list": self.parseList(html)
}
def detailContent(self, ids):
vid = ids[0]
html = self.get(self.host + "/movie/index" + vid + ".html")
name = self.clean(self.match(html, r'<h1[^>]*>(.*?)</h1>') or self.match(html, r'<meta property="og:title" content="(.*?)"'))
pic = self.fix(self.match(html, r'<meta property="og:image" content="(.*?)"') or self.match(html, r'<img[^>]+(?:data-src|data-original|src)=["\']([^"\']+)'))
desc = self.clean(self.match(html, r'<meta property="og:description" content="(.*?)"') or self.match(html, r'剧情:([\s\S]*?)在线观看'))
tabs = re.findall(r'data-dropdown-value=["\']([^"\']+)["\']', html)
panels = re.findall(r'<div class="module-list module-player-list[\s\S]*?</div>\s*</div>\s*</div>', html)
play_from = []
play_url = []
for i, p in enumerate(panels):
eps = []
for m in re.finditer(r'<a[^>]+title=["\']([^"\']+)["\'][^>]+href=["\']([^"\']*?/play/[^"\']+)["\']', p):
t = self.clean(m.group(1))
u = self.fix(m.group(2))
if t and u:
eps.append(t + "$" + u)
if not eps:
for m in re.finditer(r'<a[^>]+href=["\']([^"\']*?/play/[^"\']+)["\'][^>]*>(.*?)</a>', p):
t = self.clean(m.group(2))
u = self.fix(m.group(1))
if t and u:
eps.append(t + "$" + u)
if eps:
key = tabs[i] if i < len(tabs) else "线路" + str(i + 1)
if key not in play_from:
play_from.append(key)
play_url.append("#".join(eps))
if not play_url:
eps = []
for m in re.finditer(r'<a[^>]+href=["\']([^"\']*?/play/' + vid + r'-[^"\']+)["\'][^>]*>(.*?)</a>', html):
t = self.clean(m.group(2)) or "播放"
u = self.fix(m.group(1))
if t and u:
eps.append(t + "$" + u)
if eps:
play_from.append("默认")
play_url.append("#".join(eps))
return {
"list": [{
"vod_id": vid,
"vod_name": name,
"vod_pic": pic,
"vod_content": desc,
"vod_play_from": "$$$".join(play_from),
"vod_play_url": "$$$".join(play_url)
}]
}
def searchContent(self, key, quick, pg="1"):
html = ""
try:
html = requests.post(self.host + "/search.php", headers=self.headers, data={"searchword": key}, timeout=15).text
except Exception:
html = ""
if not html or "module-item" not in html:
html = self.get(self.host + "/search.php?searchword=" + quote(key) + "&page=" + str(pg))
return {"list": self.parseList(html), "page": int(pg)}
def playerContent(self, flag, id, vipFlags):
return {"parse": 1, "url": id, "header": self.headers}
def localProxy(self, param):
return [404, "text/plain", "", ""]
def destroy(self):
return "正在Destroy"
def get(self, url):
try:
r = requests.get(url, headers=self.headers, timeout=15)
r.encoding = r.apparent_encoding or "utf-8"
return r.text
except Exception:
return ""
def match(self, text, rule):
m = re.search(rule, text or "", re.S)
return m.group(1) if m else ""
def clean(self, text):
return re.sub(r"\s+", " ", re.sub(r"<.*?>", "", text or "")).strip()
def fix(self, url):
if not url:
return ""
if url.startswith("//"):
return "https:" + url
if url.startswith("/"):
return self.host + url
return url
def parseList(self, html):
res = []
seen = set()
for m in re.finditer(r'<div class="module-item">([\s\S]*?)</div>\s*</div>', html or "", re.S):
item = m.group(1)
href = self.match(item, r'href=["\']/movie/index(\d+)\.html["\']')
if not href or href in seen:
continue
seen.add(href)
name = self.clean(self.match(item, r'alt=["\']([^"\']+)') or self.match(item, r'title=["\']([^"\']+)') or self.match(item, r'class="module-item-title"[^>]*>(.*?)</a>'))
pic = self.fix(self.match(item, r'(?:data-src|data-original|src)=["\']([^"\']+\.(?:jpg|jpeg|png|webp|gif)[^"\']*)'))
remarks = self.clean(self.match(item, r'class="module-item-text"[^>]*>(.*?)</div>'))
if name:
res.append({
"vod_id": href,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
})
if not res:
for m in re.finditer(r'<a[^>]+href=["\']/movie/index(\d+)\.html["\'][^>]*title=["\']([^"\']+)["\'][\s\S]*?<img[^>]+(?:data-src|data-original|src)=["\']([^"\']+)["\'][\s\S]*?(?:class="module-item-text"[^>]*>(.*?)</div>)?', html or "", re.S):
vid = m.group(1)
if vid in seen:
continue
seen.add(vid)
res.append({
"vod_id": vid,
"vod_name": self.clean(m.group(2)),
"vod_pic": self.fix(m.group(3)),
"vod_remarks": self.clean(m.group(4))
})
return res