Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-04 19:15:18 +00:00
parent 99182bf2dd
commit 7cd5fd27a3
15 changed files with 3467 additions and 2096 deletions
+300
View File
@@ -0,0 +1,300 @@
#coding=utf-8
import sys
import re
import json
import html as html_module
import requests
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.site = 'https://www.qmao.net'
self.session = requests.Session()
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
self.session.headers.update({'User-Agent': self.ua})
self.cateManual = {
'电影': '1',
'电视剧': '2',
'动漫': '3',
'短剧': '4',
}
self._m = chr(0x661f) + chr(0x6cb3)
def _clean(self, text):
if not text:
return ''
text = re.sub(r'<[^>]+>', '', text)
text = html_module.unescape(text)
text = text.replace('\xa0', ' ').replace('&nbsp;', ' ')
text = ' '.join(text.split())
return text.strip()
def _get(self, url):
try:
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
r.encoding = 'utf-8'
return r.text
except:
return ''
def init(self, extend=''):
pass
def getName(self):
return '七猫短剧'
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
for k, v in self.cateManual.items():
result['class'].append({'type_id': str(v), 'type_name': k})
return result
def _extract_list(self, html):
videos = []
seen = set()
for m in re.finditer(r'href="/voddetail/(\d+)\.html"', html):
vid = m.group(1)
if vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+800]
# 标题:分类页 BrowseList,首页 FeaturedList,搜索页 MTagBookList
title = ''
tm = re.search(r'(BrowseList|FeaturedList|MTagBookList)_bookName[^>]*>(.*?)</a>', snippet, re.DOTALL)
if tm:
title = self._clean(tm.group(2))
if not title:
tm = re.search(r'title="([^"]*)"', snippet)
if tm:
title = self._clean(tm.group(1))
# 封面
pic = ''
pm = re.search(r'src="([^"]*\.(?:jpg|webp)[^"]*)"', snippet)
if pm:
pic = pm.group(1).strip()
if not pic.startswith('http'):
pic = self.site + pic
# 备注
note = ''
nm = re.search(r'(BrowseList|FeaturedList)_(?:lastChapter|tagsBox)[^>]*>(.*?)</(?:a|div)', snippet, re.DOTALL)
if nm:
note = self._clean(nm.group(2))
if not note:
nm = re.search(r'(BrowseList|FeaturedList)_bookViewCount[^>]*>([^<]+)<', snippet)
if nm:
note = self._clean(nm.group(2))
# 搜索页:从 img alt 获取集数
if not note:
am = re.search(r'alt="([^"]*(?:集|完结)[^"]*)"', snippet)
if am:
note = self._clean(am.group(1))
if title:
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
return videos
def homeVideoContent(self):
result = {'list': [], 'parse': 0, 'jx': 0}
html = self._get(self.site)
if html:
result['list'] = self._extract_list(html)
return result
def categoryContent(self, tid, pg, filter, extend):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
url = f'{self.site}/vodtype/{tid}.html'
html = self._get(url)
if html:
result['list'] = self._extract_list(html)
result['page'] = page
result['pagecount'] = page + 1 if result['list'] else page
result['limit'] = len(result['list'])
result['total'] = len(result['list'])
return result
def detailContent(self, ids):
result = {'list': [], 'parse': 0, 'jx': 0}
vid = ''
if isinstance(ids, list):
vid = ids[0] if ids else ''
elif ids:
vid = str(ids)
if not vid:
return result
# 播放页获取 player_aaaa
play_html = self._get(f'{self.site}/vodplay/{vid}-1-1.html')
pd = {}
if play_html:
m = re.search(r'var player_aaaa=(\{[^<]+\})', play_html)
if m:
try:
pd = json.loads(m.group(1))
except:
pass
# 详情页
detail_html = self._get(f'{self.site}/voddetail/{vid}.html')
# 标题
title = pd.get('vod_data', {}).get('vod_name', '')
if not title:
m2 = re.search(r'dramaDetail_bookName[^>]*>([^<]+)<', detail_html)
if m2:
title = self._clean(m2.group(1))
if not title:
m2 = re.search(r'<title>([^<]+)', detail_html)
if m2:
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
# 封面
pic = ''
m2 = re.search(r'dramaDetail_bookCover[^>]*>\s*<img[^>]*src="([^"]*)"', detail_html)
if m2:
pic = m2.group(1).strip()
if not pic.startswith('http'):
pic = self.site + pic
# 标签/类型
vod_class = ''
m2 = re.search(r'dramaDetail_tagsBox[^>]*>(.*?)</div>', detail_html, re.DOTALL)
if m2:
vod_class = self._clean(m2.group(1))
if not vod_class:
vod_class = pd.get('vod_data', {}).get('vod_class', '')
# 演员
actor = pd.get('vod_data', {}).get('vod_actor', '')
# 导演
director = pd.get('vod_data', {}).get('vod_director', '')
if director:
director = self._m + '' + director
else:
director = self._m
# 播放列表 - 从播放页提取集数
play_from = []
play_url_list = []
if play_html:
# 提取所有线路
tabs = re.findall(r'episode_tabBtn[^>]*data-sid="(\d+)"[^>]*data-from="([^"]*)"[^>]*>([^<]*)<', play_html)
if not tabs:
tabs = re.findall(r'episode_tabBtn[^>]*>([^<]*)<', play_html)
if tabs:
tabs = [(str(i+1), '', t) for i, t in enumerate(tabs)]
# 提取集数链接
episodes = []
for em in re.finditer(r'<a[^>]*class="CatalogList_linkBox"[^>]*href="(/vodplay/[^"]+)"', play_html):
ep_href = em.group(1)
# 在 </a> 前提取集数编号
snippet = play_html[em.start():em.start()+600]
num = re.search(r'>\s*(?:<[^>]*>\s*)*(\d+)\s*</a>', snippet)
if num:
ep_num = num.group(1).strip()
episodes.append(f'{ep_num}集${ep_href}')
else:
episodes.append(f'播放${ep_href}')
# 备用:data-part
if not episodes:
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*data-part="([^"]*)"', play_html):
episodes.append(f'{em.group(2)}${em.group(1)}')
if not episodes:
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*>([^<]*)<', play_html):
if em.group(2).strip():
episodes.append(f'{em.group(2).strip()}${em.group(1)}')
if episodes:
line_name = tabs[0][2] if tabs else '默认'
if not line_name:
line_name = tabs[0][1] or '默认'
play_from.append(self._clean(line_name))
play_url_list.append('#'.join(episodes))
# 如果没有从播放页拿到集数,用 player_aaaa 的 URL 直接播放
if not play_from and pd:
url = pd.get('url', '')
from_flag = pd.get('from', '')
if url:
play_from.append(from_flag or '默认')
play_url_list.append(f'播放${url}')
vod = {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': vod_class,
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director,
'vod_content': '',
'vod_play_from': '$$$'.join(play_from),
'vod_play_url': '$$$'.join(play_url_list)
}
result['list'].append(vod)
return result
def playerContent(self, flag, id, vipFlags):
result = {}
try:
play_url = id
if not play_url.startswith('http'):
play_url = self.site + play_url
html = self._get(play_url)
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
if m:
pd = json.loads(m.group(1))
url = pd.get('url', '')
if url:
result['parse'] = 0
result['url'] = url
result['jx'] = 0
result['header'] = {
'User-Agent': self.ua,
'Referer': self.site + '/'
}
return result
result['parse'] = 1
result['url'] = play_url
result['jx'] = 0
result['header'] = {
'User-Agent': self.ua,
'Referer': self.site + '/'
}
except Exception as e:
print(f'playerContent error: {e}')
if not result:
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
return result
def searchContent(self, key, quick, pg='1'):
result = {'list': [], 'parse': 0, 'jx': 0}
wd = requests.utils.quote(key)
url = f'{self.site}/vodsearch/-------------.html?wd={wd}'
html = self._get(url)
if html:
result['list'] = self._extract_list(html)
return result
def localProxy(self, params):
return [200, "video/MP2T", {}, ""]
+347
View File
@@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
import sys
import json
import time
import base64
import hashlib
import urllib3
import concurrent.futures
from urllib.parse import quote
from base.spider import Spider
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
sys.path.append('..')
class Spider(Spider):
host, userid, episode_list = '', '', []
# ---------- 加密与签名相关常量 ----------
PUB_KEY_B64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB"
APP_SIGN_SHA1 = "09a8dc51639a31801af5f6418caebfabc695eb24"
DEVICE_ID = "2d590b9842d064a1"
# RSA 私钥(用于解密响应)
PRIV_KEY_B64 = """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg=="""
headers = {
'User-Agent': "okhttp/4.12.0",
'Connection': "Keep-Alive",
'Accept-Encoding': "gzip",
'Content-Type': "application/json;charset=UTF-8",
'Cache-Control': "no-cache",
'token': "",
'deviceId': DEVICE_ID,
'client': "app",
'deviceType': "Android"
}
# ---------- RSA 加密 ----------
def rsa_encrypt(self, data: str) -> str:
key = RSA.import_key(base64.b64decode(self.PUB_KEY_B64))
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(data.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
# ---------- RSA 解密(支持分块) ----------
def rsa_decrypt(self, encrypted_b64: str) -> str:
key = RSA.import_key(base64.b64decode(self.PRIV_KEY_B64))
cipher = PKCS1_v1_5.new(key)
encrypted_bytes = base64.b64decode(encrypted_b64)
block_size = 256
decrypted_parts = []
for i in range(0, len(encrypted_bytes), block_size):
block = encrypted_bytes[i:i+block_size]
decrypted_parts.append(cipher.decrypt(block, None))
return b''.join(decrypted_parts).decode('utf-8')
# ---------- 构建签名参数 ----------
def build_params_string(self, episode_id="", episode_index="", vid="", player_id="", type_id="", user_id=""):
return (f"episodeId{episode_id}"
f"episodeIndex{episode_index}"
f"id{vid}"
f"playerId{player_id}"
f"source0"
f"typeId{type_id}"
f"userId{user_id}")
def generate_sign(self, timestamp: str, params_str: str, device_id: str) -> str:
raw = f"SaltLSFBTimestamp{timestamp}Params{params_str}ClientappDeviceId{device_id}"
b64 = base64.b64encode(raw.encode('utf-8')).decode('utf-8')
md5 = hashlib.md5(b64.encode('utf-8')).hexdigest().upper()
return md5
def build_encrypted_headers(self, body_json: str, params_str: str) -> dict:
timestamp = str(int(time.time()))
encrypted_key = self.rsa_encrypt(body_json)
snjm = self.rsa_encrypt("113")
appsign = self.rsa_encrypt(self.APP_SIGN_SHA1)
sign = self.generate_sign(timestamp, params_str, self.DEVICE_ID)
headers = {
"snjm": snjm,
"appsign": appsign,
"timestamp": timestamp,
"sign": sign,
"deviceId": self.DEVICE_ID,
"token": self.headers.get('token', ''),
"client": "app",
"deviceType": "Android",
"Content-Type": "application/json;charset=UTF-8",
"Cache-Control": "no-cache",
"User-Agent": "okhttp/4.12.0"
}
return headers, {"key": encrypted_key}
# ---------- 原有接口(保持不变) ----------
def init(self, extend=''):
self.headers['deviceId'] = self.DEVICE_ID
self.host = 'http://qkys.qukanwh.com'
response = self.fetch(f'{self.host}/api/v1/app/user/visitorInfo', headers=self.headers).json()
self.userid = response['data']['id']
token = response['data']['token']
self.headers['token'] = token
def homeContent(self, filter):
response = self.post(f'{self.host}/api/v1/app/screen/screenType', headers=self.headers).json()
data = response['data']
classes = []
for i in data:
classes.append({'type_id': i['id'], 'type_name': i['name']})
return {'class': classes}
def homeVideoContent(self):
response = self.post(f'{self.host}/api/v1/app/recommend/recommendList', headers=self.headers).json()
data = response['data']
videos = []
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_id = {
executor.submit(
self.post,
f'{self.host}/api/v1/app/recommend/recommendSubList',
data=json.dumps({
"condition": item['id'],
"pageNum": 1,
"pageSize": 6
}),
headers=self.headers
): item['id'] for item in data
}
for future in concurrent.futures.as_completed(future_to_id):
try:
response = future.result().json()
for video in response['data']['records']:
videos.append({
"vod_id": video['id'],
"vod_name": video['name'],
"vod_pic": video['cover']
})
except Exception as e:
print(f"Request failed for item {future_to_id[future]}: {str(e)}")
return {'list': videos}
def categoryContent(self, tid, pg, filter, extend):
payload = {
"condition": {
"classify": "",
"region": "",
"sreecnTypeEnum": "NEWEST",
"typeId": tid,
"year": ""
},
"pageNum": pg,
"pageSize": 40
}
response = self.post(f'{self.host}/api/v1/app/screen/screenMovie', data=json.dumps(payload), headers=self.headers).json()
videos = []
for i in response['data']['records']:
videos.append({
"vod_id": i['id'],
"vod_name": i['name'],
"vod_pic": i['cover'],
"vod_remarks": i['area'],
"vod_year": i['year']
})
return {'list': videos, 'page': pg}
def searchContent(self, key, quick, pg='1'):
payload = {
"condition": {
"value": key
},
"pageNum": pg,
"pageSize": 40
}
response = self.post(f'{self.host}/api/v1/app/search/searchMovie', data=json.dumps(payload), headers=self.headers).json()
videos = []
for i in response['data']['records']:
videos.append({
'vod_id': i['id'],
'vod_name': i['name'],
'vod_pic': i['cover'],
'vod_remarks': i['area'],
'vod_year': i['year'],
'vod_area': i['area'],
'vod_content': i['desc']
})
return {'list': videos, 'page': pg}
# ---------- 详情页(已集成解密) ----------
def detailContent(self, ids):
type_id = "M15" # 注意:原脚本写死为 M17,可根据需要修改
vid = ids[0]
body = {
"id": vid,
"source": 0,
"typeId": type_id,
"userId": self.userid,
"episodeId": "",
"episodeIndex": "",
"playerId": ""
}
body_json = json.dumps(body, separators=(',', ':'))
params_str = self.build_params_string(
episode_id="",
episode_index="",
vid=str(vid),
player_id="",
type_id=type_id,
user_id=str(self.userid)
)
headers, payload = self.build_encrypted_headers(body_json, params_str)
# 发送加密请求
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(payload), headers=headers).json()
encrypted_data = resp_raw.get('data')
if not encrypted_data:
raise Exception("响应中 data 为空")
# 解密 data 字段
decrypted_json_str = self.rsa_decrypt(encrypted_data)
data = json.loads(decrypted_json_str)
# 后续处理与原脚本相同
currentplayerid = data['playerId']
play_urls = []
play_url = []
show = []
for i in data['episodeList']:
play_url.append(f"{i['episode']}${ids[0]}@{currentplayerid}@{i['id']}@episode")
play_urls.append('#'.join(play_url))
moviePlayerList = data['moviePlayerList']
for i2 in moviePlayerList:
if i2['id'] == currentplayerid:
show.append(i2['moviePlayerName'])
for j in moviePlayerList:
playerid = j['id']
episodeTotal = j.get('episodeTotal')
if playerid == currentplayerid or episodeTotal is None:
continue
play_url = []
for k in range(1, episodeTotal + 1):
play_url.append(f"{k}集${k}@{playerid}@{ids[0]}@virtual")
play_urls.append('#'.join(play_url))
if j['moviePlayerName'] not in show:
show.append(j['moviePlayerName'])
# 获取简介(此接口可能无需加密,保持原样)
payload_desc = {
"id": ids[0],
"typeId": type_id
}
response_desc = self.post(f'{self.host}/api/v1/app/play/movieDesc', data=json.dumps(payload_desc), headers=self.headers).json()
data2 = response_desc['data']
video = {
'vod_id': data2['id'],
'vod_name': data2['name'],
'vod_pic': data2['cover'],
'vod_content': data2['introduce'],
'vod_year': data2['year'],
'vod_area': data2['area'],
'vod_remarks': '',
'vod_score': data2['score'],
'type_name': data2['classify'],
'vod_director': data2['director'],
'vod_actor': data2['star'],
'vod_play_from': '$$$'.join(show),
'vod_play_url': '$$$'.join(play_urls)
}
return {'list': [video]}
# ---------- 播放页(已集成解密) ----------
def playerContent(self, flag, id, vipflags):
param, playerid, param2, param3 = id.split('@')
if param3 == 'virtual':
payload = {
"episodeIndex": str(int(param) - 1),
"id": int(param2),
"playerId": playerid,
"source": 0,
"typeId": "M15",
"userId": self.userid,
"episodeId": ""
}
else:
payload = {
"episodeId": param2,
"id": int(param),
"playerId": playerid,
"source": 0,
"typeId": "M15",
"userId": self.userid,
"episodeIndex": ""
}
body_json = json.dumps(payload, separators=(',', ':'))
print(body_json)
params_str = self.build_params_string(
episode_id=payload.get("episodeId", ""),
episode_index=payload.get("episodeIndex", ""),
vid=str(payload["id"]),
player_id=payload["playerId"],
type_id=payload["typeId"],
user_id=str(payload["userId"])
)
print(params_str)
headers, encrypted_payload = self.build_encrypted_headers(body_json, params_str)
print(headers)
print(encrypted_payload)
# 获取播放信息(加密响应)
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(encrypted_payload), headers=headers).json()
encrypted_data = resp_raw.get('data')
if not encrypted_data:
raise Exception("响应中 data 为空")
decrypted_json_str = self.rsa_decrypt(encrypted_data)
data = json.loads(decrypted_json_str)
print(data)
parse_url = data['url']
playerid = data['playerId']
# 调用分析接口(注:analysisMovieUrl 的响应可能也是加密的,但原脚本直接取 data,这里暂不做额外解密)
analysis_body = {
"playerUrl": parse_url,
"playerId": playerid
}
analysis_json = json.dumps(analysis_body, separators=(',', ':'))
# analysisMovieUrl 接口的参数拼接?理论上也需要签名,但原脚本是 GET 方式,为了兼容,我们沿用原脚本的 GET 方式
# 原脚本使用 fetch GET 带参数,并未加密。这里也采用 GET 方式,不使用加密 headers
resp_analysis = self.fetch(f"{self.host}/api/v1/app/play/analysisMovieUrl?playerUrl={quote(parse_url,safe='')}&playerId={playerid}", headers=self.headers).json()
url = resp_analysis.get('data')
return {'jx': '0', 'parse': '0', 'url': url, 'header': {'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'}}
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def localProxy(self, param):
pass
+343
View File
@@ -0,0 +1,343 @@
#coding=utf-8
import sys
import re
import json
import requests
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.site = 'https://www.cd-zj.com'
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://www.cd-zj.com/'
})
self.cateManual = {
'\u7535\u5f71': '1',
'\u7535\u89c6\u5267': '2',
'\u7efc\u827a': '3',
'\u52a8\u6f2b': '4',
'\u70ed\u95e8\u77ed\u5267': '5',
'\u817e\u8bafSVIP': 'label/qq',
'\u4f18\u9177SVIP': 'label/youku',
'B\u7ad9SVIP': 'label/bli',
}
def init(self, extend=""):
pass
def getName(self):
return "\u67ab\u53f64K\u5907\u7528"
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def _clean(self, text):
if not text:
return ''
text = re.sub(r'<[^>]+>', '', text)
text = text.replace('&nbsp;', ' ').replace('&amp;', '&').replace('\u3000', ' ')
text = ' '.join(text.split())
return text.strip()
def _get(self, url):
try:
r = self.session.get(url, timeout=15)
r.encoding = 'utf-8'
return r.text
except:
return ''
def getVid(self, url):
if not url:
return ''
m = re.search(r'/detail/(\d+)\.html', url)
if m:
return m.group(1)
m = re.search(r'/play/(\d+)-', url)
if m:
return m.group(1)
return ''
def homeContent(self, filter):
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
for k, v in self.cateManual.items():
result['class'].append({'type_id': str(v), 'type_name': k})
return result
def homeVideoContent(self):
videos = []
try:
html = self._get(self.site)
seen = set()
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
if title:
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
except Exception as e:
print(f'homeVideoContent error: {e}')
return {'list': videos, 'parse': 0, 'jx': 0}
def categoryContent(self, tid, pg, filter, extend):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
try:
if str(tid).startswith('label/'):
if page == 1:
url = f'{self.site}/{tid}.html'
else:
url = f'{self.site}/{tid}-{page}.html'
else:
if page == 1:
url = f'{self.site}/type/{tid}.html'
else:
url = f'{self.site}/type/{tid}-{page}.html'
html = self._get(url)
seen = set()
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
if title:
result['list'].append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
except Exception as e:
print(f'categoryContent error: {e}')
result['page'] = page
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
result['limit'] = len(result['list'])
result['total'] = len(result['list'])
return result
def detailContent(self, ids):
result = {'list': [], 'parse': 0, 'jx': 0}
vid = ids[0] if ids else ''
if not vid:
return result
try:
html = self._get(f'{self.site}/detail/{vid}.html')
title = ''
tm = re.search(r'<title>\u300a(.+?)\u300b', html)
if tm:
title = tm.group(1)
if not title:
tm = re.search(r'<title>([^<]+)', html)
if tm:
title = self._clean(tm.group(1))
pic = ''
pm = re.search(r'lazy1[^>]*data-src="([^"]+)"', html)
if pm:
pic = pm.group(1).replace('&amp;', '&')
desc = ''
dm = re.search(r'<meta name="description" content="(.+?)"', html)
if dm:
desc = dm.group(1).replace('\u5267\u60c5\u4ecb\u7ecd\uff1a', '').strip()
actor = ''
director = ''
info = re.search(r'slide-info(.*?)(?:anthology|swiper)', html, re.DOTALL)
if info:
block = info.group(1)
am = re.search(r'\u4e3b\u6f14[:\uff1a]\s*([^\n<]+)', block)
if am:
actor = am.group(1).strip()
dm2 = re.search(r'\u5bfc\u6f14[:\uff1a]\s*([^\n<]+)', block)
if dm2:
director = dm2.group(1).strip()
play_from = []
play_url = []
# \u627e anthology-tab \u533a\u5757\u5185\u7684\u6240\u6709 <a class="swiper-slide">
tab_block = re.search(r'class="anthology-tab[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
if tab_block:
tabs = re.findall(r'<a[^>]*class="swiper-slide"[^>]*>(.*?)</a>', tab_block.group(1), re.DOTALL)
else:
tabs = []
# \u627e\u6240\u6709 anthology-list-box \u533a\u5757
panels = re.findall(r'class="anthology-list-box[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
for i, tab in enumerate(tabs):
tab_name = self._clean(tab) or f'\u7ebf\u8def{i+1}'
play_from.append(tab_name)
episodes = []
if i < len(panels):
for em in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>([^<]+)<', panels[i]):
ep_href = em.group(1)
ep_name = em.group(2).strip()
if ep_name and ep_href:
episodes.append(f'{ep_name}${ep_href}')
play_url.append('#'.join(episodes))
vod = {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director if director else bytes.fromhex('e6989fe6b2b3').decode('utf-8'),
'vod_content': desc,
'vod_play_from': '$$$'.join(play_from) if play_from else '',
'vod_play_url': '$$$'.join(play_url) if play_url else ''
}
result['list'].append(vod)
except Exception as e:
print(f'detailContent error: {e}')
return result
def playerContent(self, flag, id, vipFlags):
result = {}
try:
play_url = id
if id and not id.startswith('http'):
play_url = self.site + id
html = self._get(play_url)
# \u4f18\u5148\u4ece player_aaaa JSON \u63d0\u53d6 m3u8 \u76f4\u94fe
m = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*<', html)
if m:
try:
data = json.loads(m.group(1))
m3u8 = data.get('url', '')
if m3u8 and '.m3u8' in m3u8:
result['parse'] = 0
result['url'] = m3u8
result['jx'] = 0
result['header'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
return result
except:
pass
# \u5907\u7528: \u4ece\u9875\u9762\u4e2d\u627e m3u8 \u94fe\u63a5
m = re.search(r'url":\s*"(https?://[^"]*\.m3u8[^"]*)"', html)
if m:
result['parse'] = 0
result['url'] = m.group(1).replace('\\/', '/')
result['jx'] = 0
result['header'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
return result
# \u5907\u7528: iframe
m = re.search(r'<iframe[^>]+src="([^"]+)"', html)
if m:
result['parse'] = 1
result['url'] = m.group(1)
result['jx'] = 0
result['header'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
else:
result['parse'] = 1
result['url'] = play_url
result['jx'] = 0
result['header'] = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
except Exception as e:
print(f'playerContent error: {e}')
result['parse'] = 1
result['url'] = id
result['jx'] = 0
result['header'] = {}
return result
def searchContent(self, key, quick, pg='1'):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
try:
url = f'{self.site}/cupfox-search/-------------.html'
params = {'wd': key}
if page > 1:
params['page'] = page
html = self._get(url)
seen = set()
for m in re.finditer(r'href="(/detail/\d+\.html)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
if title:
result['list'].append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
except Exception as e:
print(f'searchContent error: {e}')
return result
def localProxy(self, params):
return [200, "video/MP2T", {}, ""]
+283
View File
@@ -0,0 +1,283 @@
#coding=utf-8
import sys
import re
import json
import html as html_module
import requests
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.site = 'https://www.jxuma.com'
self.session = requests.Session()
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
self.session.headers.update({'User-Agent': self.ua})
self.cateManual = {
'电影': '1',
'电视剧': '2',
'综艺': '3',
'动漫': '4',
'短剧': '36',
}
self._m = chr(0x661f) + chr(0x6cb3)
def _clean(self, text):
if not text:
return ''
text = re.sub(r'<[^>]+>', '', text)
text = html_module.unescape(text)
text = text.replace('\xa0', ' ')
text = ' '.join(text.split())
return text.strip()
def _get(self, url):
try:
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
r.encoding = 'utf-8'
return r.text
except:
return ''
def init(self, extend=''):
pass
def getName(self):
return '麻花影视'
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
for k, v in self.cateManual.items():
result['class'].append({'type_id': str(v), 'type_name': k})
return result
def _extract_list(self, html):
videos = []
seen = set()
for m in re.finditer(r'href="/umo/(\d+)\.html"[^>]*?title="([^"]*)"', html):
vid = m.group(1)
title = m.group(2).strip()
if vid in seen or not title:
continue
snippet = html[m.start():m.start()+400]
pm = re.search(r'data-original="([^"]*)"', snippet)
pic = pm.group(1).strip() if pm else ''
if vid in seen:
continue
seen.add(vid)
note = ''
nm = re.search(r'pic-text text-right">([^<]*)', snippet)
if nm:
note = nm.group(1).strip()
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': note
})
return videos
def homeVideoContent(self):
result = {'list': [], 'parse': 0, 'jx': 0}
html = self._get(self.site)
if html:
result['list'] = self._extract_list(html)
return result
def categoryContent(self, tid, pg, filter, extend):
result = {'list': [], 'parse': 0, 'jx': 0}
page = int(pg) if pg else 1
url = f'{self.site}/jxk/{tid}.html'
html = self._get(url)
if html:
result['list'] = self._extract_list(html)
result['page'] = page
result['pagecount'] = page + 1 if result['list'] else page
result['limit'] = len(result['list'])
result['total'] = len(result['list'])
return result
def detailContent(self, ids):
result = {'list': [], 'parse': 0, 'jx': 0}
vid = ''
if isinstance(ids, list):
vid = ids[0] if ids else ''
elif ids:
vid = str(ids)
if not vid:
return result
# 用播放页第一集来获取 player_aaaa 数据
html = self._get(f'{self.site}/aey/{vid}/1-1.html')
if not html:
return result
# 提取 player_aaaa
pd = {}
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
if m:
try:
pd = json.loads(m.group(1))
except:
pass
# 从详情页获取更多信息
detail_html = self._get(f'{self.site}/umo/{vid}.html')
# 标题
title = pd.get('vod_data', {}).get('vod_name', '')
if not title:
m2 = re.search(r'<h1[^>]*class="title"[^>]*>([^<]*)', detail_html)
if m2:
title = self._clean(m2.group(1))
if not title:
m2 = re.search(r'<title>([^<]+)', detail_html)
if m2:
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
# 封面
pic = ''
m2 = re.search(r'data-original="([^"]+)"[^>]*class="[^"]*cover[^"]*"', detail_html)
if not m2:
m2 = re.search(r'data-original="([^"]+)"[^>]*rel="nofollow"', detail_html)
if m2:
pic = m2.group(1).strip()
# 类型、地区、年份、语言、主演、导演、简介
def extract_info(pattern, text):
m3 = re.search(pattern, text)
if m3:
return self._clean(m3.group(1))
return ''
vod_class = extract_info(r'类型:</span>(.*?)(?:</a>|<span)', detail_html)
area = extract_info(r'地区:</span>(.*?)(?:</a>|<span)', detail_html)
year = extract_info(r'年份:</span>(.*?)(?:</a>|<span)', detail_html)
lang = extract_info(r'语言:</span>(.*?)(?:</a>|<span)', detail_html)
actor = extract_info(r'主演:</span>(.*?)(?:</p>|</div>)', detail_html)
if not actor:
actor = pd.get('vod_data', {}).get('vod_actor', '')
director = extract_info(r'导演:</span>(.*?)(?:</p>|</div>)', detail_html)
if not director:
director = pd.get('vod_data', {}).get('vod_director', '')
if director:
director = self._m + '' + director
else:
director = self._m
desc = extract_info(r'detail-sketch">(.*?)</span>', detail_html)
# 播放列表 - 从详情页提取所有线路和集数
play_from = []
play_url_list = []
# 提取线路名(在 playlist data-toggle="tab" 里)
line_names = re.findall(r'playlist\d+" data-toggle="tab"[^>]*rel="nofollow">([^<]+)<', detail_html)
# 如果没找到,尝试提取 pannel__head 里的文字
if not line_names:
line_names = re.findall(r'pannel__head[^>]*>([^<]*)<', detail_html)
# 提取每个播放面板的链接
link_groups = re.findall(r'tab-pane fade[^>]*>(.*?)</ul>', detail_html, re.DOTALL)
for i, group_html in enumerate(link_groups):
line_name = line_names[i] if i < len(line_names) else f'线路{i+1}'
line_name = self._clean(line_name)
episodes = []
for em in re.finditer(r'href="(/aey/\d+/(\d+-\d+)\.html)"[^>]*>([^<]*)<', group_html):
ep_href = em.group(1)
ep_label = em.group(3).strip()
if ep_label and ep_href:
episodes.append(f'{ep_label}${ep_href}')
if episodes:
play_from.append(line_name)
play_url_list.append('#'.join(episodes))
# 把华为云排到第一个(1080p
for i, name in enumerate(play_from):
if '华为' in name and i > 0:
play_from.insert(0, play_from.pop(i))
play_url_list.insert(0, play_url_list.pop(i))
break
# 备用:如果详情页没有找到播放列表,直接用播放页的 URL
if not play_from and pd:
url = pd.get('url', '')
from_flag = pd.get('from', '')
if url:
play_from.append(from_flag or '线路①')
play_url_list.append(f'播放${url}')
vod = {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': vod_class,
'vod_year': year,
'vod_area': area,
'vod_lang': lang,
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director,
'vod_content': desc,
'vod_play_from': '$$$'.join(play_from),
'vod_play_url': '$$$'.join(play_url_list)
}
result['list'].append(vod)
return result
def playerContent(self, flag, id, vipFlags):
result = {}
try:
# id 格式: /aey/119317/1-1.html
play_url = id
if not play_url.startswith('http'):
play_url = self.site + play_url
html = self._get(play_url)
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
if m:
pd = json.loads(m.group(1))
url = pd.get('url', '')
if url:
result['parse'] = 0
result['url'] = url
result['jx'] = 0
result['header'] = {
'User-Agent': self.ua,
'Referer': self.site + '/'
}
return result
# 备用:嗅探
result['parse'] = 1
result['url'] = play_url
result['jx'] = 0
result['header'] = {
'User-Agent': self.ua,
'Referer': self.site + '/'
}
except Exception as e:
print(f'playerContent error: {e}')
if not result:
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
return result
def searchContent(self, key, quick, pg='1'):
result = {'list': [], 'parse': 0, 'jx': 0}
wd = requests.utils.quote(key)
url = f'{self.site}/search/-------------.html?wd={wd}'
html = self._get(url)
if html:
result['list'] = self._extract_list(html)
return result
def localProxy(self, params):
return [200, "video/MP2T", {}, ""]