上传文件至「py」

This commit is contained in:
2026-08-14 11:24:54 +02:00
parent 23a6e84111
commit 9e90ad0c97
5 changed files with 1774 additions and 0 deletions
+470
View File
@@ -0,0 +1,470 @@
# -*- coding: utf-8 -*-
import re
import sys
import json
import time
import random
import string
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.extend = extend
self.cookie_cache = ""
def getName(self):
return "抖音直播"
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def destroy(self):
pass
def localProxy(self, param):
return None
host = "https://live.douyin.com"
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
headers = {
"User-Agent": ua,
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
}
classes_config = [
{"type_id": "10000$3", "type_name": "娱乐天地"},
{"type_id": "10001$3", "type_name": "科技文化"},
{"type_id": "102$4", "type_name": "音乐"},
{"type_id": "103$4", "type_name": "游戏"},
{"type_id": "105$4", "type_name": "舞蹈"},
{"type_id": "101$4", "type_name": "聊天"},
{"type_id": "108$4", "type_name": "运动"},
{"type_id": "107$4", "type_name": "生活"},
{"type_id": "106$4", "type_name": "文化"},
{"type_id": "104$4", "type_name": "二次元"},
]
# ==================== 工具函数 ====================
def _generate_device_id(self):
timestamp = self._base36_encode(int(time.time() * 1000))
random_part = ''.join(random.choices(string.ascii_lowercase + string.digits, k=13))
return f"{timestamp}{random_part}"
@staticmethod
def _base36_encode(num):
alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'
if num == 0:
return '0'
res = []
while num > 0:
num, rem = divmod(num, 36)
res.append(alphabet[rem])
return ''.join(reversed(res))
def _get_cookie(self):
if self.cookie_cache:
return self.cookie_cache
try:
resp = self.fetch(self.host, headers=self.headers, verify=False)
cookies = resp.headers.get('set-cookie', '')
if cookies:
match = re.search(r'ttwid=([^;]+)', cookies)
if match:
self.cookie_cache = f"ttwid={match.group(1)}"
except Exception:
pass
return self.cookie_cache
def _get_headers(self):
cookie = self._get_cookie()
hd = self.headers.copy()
hd["Referer"] = self.host
if cookie:
hd["Cookie"] = cookie
return hd
def _first_non_empty(self, *values):
for v in values:
if v is not None and v != '':
return v
return None
def _parse_raw_live_data(self, item):
if not isinstance(item, dict):
return None
candidates = [
item.get('lives', {}).get('rawdata'),
item.get('lives', {}).get('raw_data'),
item.get('live', {}).get('rawdata'),
item.get('live_info', {}).get('rawdata'),
item.get('aweme_info', {}).get('live_info', {}).get('rawdata'),
item.get('data', {}).get('rawdata'),
item.get('rawdata'),
item.get('lives'),
item.get('live'),
item.get('live_info'),
item.get('aweme_info', {}).get('live_info'),
item.get('aweme_info'),
item.get('data'),
item
]
for c in candidates:
if isinstance(c, str):
try:
parsed = json.loads(c)
if isinstance(parsed, dict):
return parsed
except Exception:
continue
elif isinstance(c, dict):
return c
return None
def _normalize_search_item(self, raw, fallback=None):
if not isinstance(raw, dict):
return None
fallback = fallback or {}
room_id = self._first_non_empty(
raw.get('id_str'),
raw.get('room_id_str'),
raw.get('room', {}).get('id_str'),
raw.get('room', {}).get('id'),
raw.get('room_id'),
raw.get('roomId')
)
if not room_id:
return None
web_rid = self._first_non_empty(
raw.get('owner', {}).get('web_rid'),
raw.get('web_rid'),
raw.get('room', {}).get('owner', {}).get('web_rid')
) or self._generate_device_id()
nickname = self._first_non_empty(
raw.get('owner', {}).get('nickname'),
raw.get('nickname'),
raw.get('room', {}).get('owner', {}).get('nickname'),
fallback.get('nickname')
) or '抖音直播'
title = self._first_non_empty(
raw.get('title'),
raw.get('room', {}).get('title'),
fallback.get('title'),
nickname
)
pic = self._first_non_empty(
raw.get('owner', {}).get('avatar_large', {}).get('url_list', [None])[0],
raw.get('room', {}).get('cover', {}).get('url_list', [None])[0],
raw.get('cover', {}).get('url_list', [None])[0],
raw.get('cover_url')
) or ''
online_text = self._first_non_empty(
raw.get('room', {}).get('stats', {}).get('user_count_str'),
raw.get('user_count_str'),
raw.get('room', {}).get('user_count_str'),
raw.get('user_count')
)
tag_text = self._first_non_empty(
raw.get('video_feed_tag'),
raw.get('room', {}).get('partition_road_map', [{}])[0].get('title'),
raw.get('partition', {}).get('title'),
fallback.get('tag')
)
remark = ' '.join(filter(None, [tag_text, online_text]))
return {
"vod_id": f"{web_rid}@@{room_id}",
"vod_name": nickname,
"vod_pic": pic,
"vod_remarks": remark,
"vod_content": title
}
def _extract_search_videos(self, payload):
if not isinstance(payload, list):
return []
results = []
seen = set()
for item in payload:
raw = self._parse_raw_live_data(item)
norm = self._normalize_search_item(raw, {
"nickname": item.get('nickname'),
"title": item.get('title') or item.get('desc'),
"tag": item.get('search_keyword')
})
if not norm:
continue
if norm['vod_id'] in seen:
continue
seen.add(norm['vod_id'])
results.append(norm)
return results
# ==================== 框架标准方法 ====================
def homeContent(self, filter):
classes = self.classes_config
return {
"class": classes,
"list": []
}
def homeVideoContent(self):
return {}
def categoryContent(self, tid, pg, filter, extend):
category_id = str(tid)
page = int(pg or 1)
offset = 15 * (page - 1)
parts = category_id.split('$')
if len(parts) < 2:
return {"list": [], "page": page, "pagecount": 0, "limit": 15, "total": 9999}
partition, ptype = parts[0], parts[1]
params = {
"aid": "6383",
"app_name": "douyin_web",
"live_id": "1",
"device_platform": "web",
"language": "zh-CN",
"browser_language": "zh-CN",
"browser_platform": "Win32",
"browser_name": "Chrome",
"browser_version": "120.0.0.0",
"partition": partition,
"partition_type": ptype,
"count": "15",
"offset": str(offset),
"web_rid": self._generate_device_id(),
"cookie_enabled": "true",
"screen_width": "1920",
"screen_height": "1080"
}
headers = self._get_headers()
urls = [
"https://live.douyin.com/webcast/web/partition/detail/room/v2/",
"https://webcast.amemv.com/webcast/web/partition/detail/room/v2/",
]
list_ = []
for url in urls:
try:
resp = self.fetch(url, headers=headers, params=params, verify=False)
data = resp.json()
if data.get('status_code') != 0:
continue
if not data.get('data', {}).get('data'):
break
items = data['data']['data']
for it in items:
web_rid = it.get('web_rid') or self._generate_device_id()
room = it['room']
list_.append({
"vod_id": f"{web_rid}@@{room['id_str']}",
"vod_name": room['title'],
"vod_pic": room['cover']['url_list'][0],
"vod_remarks": f"{room['owner']['nickname']} (🔥{room['stats']['user_count_str']})"
})
break
except Exception:
continue
return {
"list": list_,
"page": page,
"pagecount": 9999,
"limit": 15,
"total": 999999
}
def searchContent(self, key, quick, pg="1"):
kw = key.strip()
if not kw:
return {"list": [], "page": 1}
page = 1
offset = 0
headers = self._get_headers()
# 策略1 专用直播搜索
try:
params1 = {
"device_platform": "webapp",
"aid": "6383",
"channel": "channel_pc_web",
"search_channel": "aweme_live",
"search_source": "switch_tab",
"query_correct_type": "1",
"need_filter_settings": "1",
"list_type": "single",
"keyword": kw,
"offset": str(offset),
"count": "20",
"os_version": "10"
}
r = self.fetch("https://www.douyin.com/aweme/v1/web/live/search/",
params=params1, headers=headers, verify=False)
data = r.json()
list_ = self._extract_search_videos(data.get('data'))
if list_:
return {"list": list_, "page": 1}
except Exception:
pass
# 策略2 通用搜索
try:
params2 = {
"device_platform": "webapp",
"aid": "6383",
"channel": "channel_pc_web",
"search_channel": "aweme_live",
"keyword": kw,
"offset": str(offset),
"count": "20",
"os_version": "10"
}
r = self.fetch("https://www.douyin.com/aweme/v1/web/general/search/stream/",
params=params2, headers=headers, verify=False)
data = r.json()
list_ = self._extract_search_videos(data.get('data'))
if list_:
return {"list": list_, "page": 1}
except Exception:
pass
# 降级分区搜索
try:
part_url = f"https://live.douyin.com/webcast/web/partition/search/?keyword={kw}&aid=6383"
r = self.fetch(part_url, headers=self._get_headers(), verify=False)
data = r.json()
partitions = data.get('data', {}).get('SearchResult', [])
if not partitions:
return {"list": [], "page": 1}
merged = []
seen = set()
for i in range(min(3, len(partitions))):
part = partitions[i].get('partition', {})
p_id = part.get('id_str')
p_type = part.get('type')
if not p_id or p_type is None:
continue
cate_ret = self.categoryContent(f"{p_id}${p_type}", 1, None, None)
for item in cate_ret.get('list', []):
if item['vod_id'] in seen:
continue
seen.add(item['vod_id'])
item['vod_remarks'] = item['vod_remarks'] or part.get('title', kw)
merged.append(item)
if len(merged) >= 20:
break
if len(merged) >= 20:
break
return {"list": merged, "page": 1}
except Exception:
pass
return {"list": [], "page": 1}
def detailContent(self, ids):
if not ids:
return {"list": []}
raw_id = ids[0]
parts = raw_id.split('@@')
if len(parts) != 2:
return {"list": []}
web_rid, room_id = parts[0], parts[1]
url = "https://live.douyin.com/webcast/room/web/enter/"
params = {
"aid": "6383",
"app_name": "douyin_web",
"live_id": "1",
"device_platform": "web",
"enter_from": "web_live",
"browser_language": "zh-CN",
"browser_platform": "Win32",
"browser_name": "Chrome",
"browser_version": "120.0.0.0",
"web_rid": web_rid,
"room_id_str": room_id,
"enter_source": "",
"is_need_double_stream": "false"
}
headers = self._get_headers()
try:
r = self.fetch(url, params=params, headers=headers, verify=False)
data = r.json()
if not data.get('data', {}).get('data'):
return {"list": []}
info = data['data']['data'][0]
resolution_map = {
"FULL_HD1": "蓝光",
"HD1": "超清",
"ORIGION": "原画",
"SD1": "标清",
"SD2": "高清"
}
flv_pull = info.get('stream_url', {}).get('flv_pull_url', {})
flv_episodes = []
for k, v in flv_pull.items():
name = resolution_map.get(k, k)
flv_episodes.append(f"{name}${v}")
hls_pull = info.get('stream_url', {}).get('hls_pull_url_map', {})
hls_episodes = []
for k, v in hls_pull.items():
name = resolution_map.get(k, k)
hls_episodes.append(f"{name}${v}")
vod_play_from = ""
vod_play_url = ""
if flv_episodes:
vod_play_from += "FLV$$$"
vod_play_url += "#".join(flv_episodes) + "$$$"
if hls_episodes:
vod_play_from += "HLS"
vod_play_url += "#".join(hls_episodes)
vod_play_from = vod_play_from.rstrip("$$$")
vod_play_url = vod_play_url.rstrip("$$$")
vod = {
"vod_id": raw_id,
"vod_name": info['title'],
"vod_pic": info['cover']['url_list'][0],
"vod_actor": info['owner']['nickname'],
"vod_content": "拾光请你看:"+info['title'],
"vod_play_from": vod_play_from,
"vod_play_url": vod_play_url
}
return {"list": [vod]}
except Exception:
return {"list": []}
def playerContent(self, flag, id, vipFlags):
if not id:
return {"parse": 0, "url": "", "header": self.headers}
return {
"parse": 0,
"url": id,
"header": {
"User-Agent": self.ua,
"Referer": self.host
}
}
+392
View File
@@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
import re, urllib.parse
import json
from bs4 import BeautifulSoup
import requests
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def init(self, extend=""):
self.host = "https://maihaolian.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}
def getName(self):
return '枫叶影院'
def homeContent(self, filter):
return {"class": [
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
{"type_id": "5", "type_name": "红果短剧"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "1", "type_name": "电影"},
{"type_id": "4", "type_name": "动漫"},
{"type_id": "3", "type_name": "综艺"},
], "filters": self._build_filters()}
def _build_filters(self):
area = [{"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 = [{"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"}]
lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
{"n": "其它", "v": "其它"}]
sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}]
letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"},
{"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"},
{"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"},
{"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"},
{"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"},
{"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"},
{"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}]
return {
"2": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"},
{"n": "海外剧", "v": "16"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("古装", "古装"), ("战争", "战争"),
("青春偶像", "青春偶像"), ("喜剧", "喜剧"),
("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"),
("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"),
("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"),
("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"1": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
{"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"},
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"),
("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"),
("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"),
("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"),
("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"),
("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"),
("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"),
("古装", "古装"), ("历史", "历史"), ("运动", "运动"),
("农村", "农村"), ("儿童", "儿童"),
("网络电影", "网络电影")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"4": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("情感", "情感"), ("科幻", "科幻"),
("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"),
("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"),
("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"),
("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"),
("励志", "励志"), ("其他", "其他")]]},
{"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
{"n": "其它", "v": "其它"}]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"3": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("选秀", "选秀"), ("情感", "情感"),
("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"),
("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"),
("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]},
{"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
}
def homeVideoContent(self):
html = self._fetch('/')
return {"list": self._parse_video_list(html)}
def categoryContent(self, tid, pg, filter, extend):
# 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter
if tid.startswith('/label'):
url = f'{tid}/page/{pg}.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
page_count = page if len(items) < 24 else page + 2
return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24}
args = {}
if extend and isinstance(extend, dict):
for k, v in extend.items():
if v:
args[k] = str(v)
if isinstance(filter, dict):
for k, v in filter.items():
if v and k not in args:
args[k] = str(v)
route_tid = args.get('class', args.get('tid', str(tid)))
area = args.get('area', '')
genre = args.get('genre', '')
year = args.get('year', '')
lang = args.get('lang', '')
letter = args.get('letter', '')
sort = args.get('sort', '')
# 无筛选走正常分页
if not area and not genre and not year and not lang and not letter and not sort:
url = f'/cupfox-list/{route_tid}--------{pg}---.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
soup = BeautifulSoup(html, 'html.parser')
pagecount = page
for a in soup.select('a.page-link'):
if a.text == '尾页':
m = re.search(r'---(\d+)---', a.get('href', ''))
if m:
pagecount = int(m.group(1))
break
if not items:
pagecount = 0
return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999}
# 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html
segs = [route_tid, area, sort, genre, lang, letter, '', '', year]
url = '/cupfox-list/' + '-'.join(segs) + '.html'
html = self._fetch(url)
items = self._parse_video_list(html)
return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999}
def detailContent(self, ids):
result = {"list": []}
vid = ids[0].split(',')[0].strip()
try:
html = self._fetch(f'/detail/{vid}.html')
if not html: return result
soup = BeautifulSoup(html, 'html.parser')
vod_name = soup.select_one('h3.slide-info-title')
vod_name = vod_name.text.strip() if vod_name else ''
vod_pic = soup.select_one('img.lazy')
vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else ''
vod_director = ''
vod_actor = ''
for el in soup.select('.slide-info'):
text = el.get_text(' ').strip()
if text.startswith('导演:'):
vod_director = text.replace('导演:', '').strip()
elif text.startswith('演员:'):
vod_actor = text.replace('演员:', '').strip()
vod_content = soup.select_one('#height_limit')
vod_content = vod_content.get_text(' ', strip=True) if vod_content else ''
play_from, play_url = [], []
for tab in soup.select('.anthology-tab a.swiper-slide'):
src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip()
if src_name:
play_from.append(src_name)
tab_blocks = soup.select('.anthology-list-box')
for i, block in enumerate(tab_blocks):
ep_list = []
for a in block.select('li a'):
href = a.get('href', '')
m = re.search(r'/play/(.*?)\.html', href)
if m:
ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}')
ep_list.reverse()
if ep_list and i < len(play_from):
play_url.append('#'.join(ep_list))
valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)]
result["list"].append({
"vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic,
"vod_director": vod_director, "vod_actor": vod_actor,
"vod_content": vod_content,
"vod_play_from": "$$$".join(valid_from),
"vod_play_url": "$$$".join(play_url),
})
except:
pass
return result
def searchContent(self, key, quick, pg="1"):
try:
decoded = urllib.parse.unquote(key)
except:
decoded = key
html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html')
items = self._parse_search_list(html)
return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)}
def playerContent(self, flag, id, vipFlags):
url = ''
try:
url = id if id.startswith('http') else f'{self.host}/play/{id}.html'
html = self._fetch(url)
if html:
m = re.search(r'player_aaaa=(.*?)</script>', html, re.S)
if m:
try:
pd = json.loads(m.group(1))
except Exception as e:
print(e)
pd = {}
# print('pd:', pd)
play_url = pd.get('url')
play_id = pd.get('from')
api_map = {
'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php',
'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php',
}
if not play_url:
return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8',
"header": {'User-Agent': 'Mozilla/5.0'}}
if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')):
return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}}
else:
headers = {
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
'accept-language': "zh-CN,zh;q=0.9",
'cache-control': "no-cache",
'pragma': "no-cache",
'priority': "u=0, i",
'referer': "https://www.ht10010.com/",
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers)
token = re.search(r'data-te="(.*?)"', response.text)
if token:
token = token.group(1)
payload = {
'url': play_url,
'token': token
}
# print('payload', payload)
try:
response = self.post(api_map[play_id], data=payload, headers=headers)
response.raise_for_status()
result = response.json()
# print('result:', result)
if result['code'] == 200 and 'url' in result:
play_url = result['url']
return {"parse": 0, "url": play_url, "header": {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'}}
except Exception as e:
print(e)
except Exception as e:
print(e)
return {"parse": 1, "url": url}
def localProxy(self, param=''):
return {}
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def _fetch(self, url):
try:
if not url.startswith('http'):
url = self.host + url
rsp = self.fetch(url, headers=self.headers)
return rsp.text if rsp else ''
except:
return ''
def _fix_pic(self, u):
if not u: return ''
if u.startswith('//'): return 'https:' + u
return u.replace('&amp;', '&')
def _parse_video_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
span = ','.join([span.text for span in a.select('span.public-prt')])
# print('span', span)
vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or ''
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span})
return videos
def _parse_search_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]')
if title_el:
vod_name = title_el.text.strip()
else:
vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or ''
remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
return videos
if __name__ == '__main__':
sp = Spider()
sp.init()
# 20067-5-189
print(sp.categoryContent('/label/qq','1',True, {}))
# print(sp.playerContent('', '20067-6-189', []))
# print(sp.playerContent('', '20067-5-189', []))
pass
+392
View File
@@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
import re, urllib.parse
import json
from bs4 import BeautifulSoup
import requests
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def init(self, extend=""):
self.host = "https://www.shzchc.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}
def getName(self):
return '枫叶影院'
def homeContent(self, filter):
return {"class": [
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "1", "type_name": "电影"},
{"type_id": "4", "type_name": "动漫"},
{"type_id": "3", "type_name": "综艺"},
{"type_id": "5", "type_name": "热门短剧"},
], "filters": self._build_filters()}
def _build_filters(self):
area = [{"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 = [{"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"}]
lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
{"n": "其它", "v": "其它"}]
sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}]
letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"},
{"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"},
{"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"},
{"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"},
{"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"},
{"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"},
{"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}]
return {
"2": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"},
{"n": "海外剧", "v": "16"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("古装", "古装"), ("战争", "战争"),
("青春偶像", "青春偶像"), ("喜剧", "喜剧"),
("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"),
("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"),
("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"),
("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"1": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
{"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"},
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"),
("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"),
("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"),
("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"),
("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"),
("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"),
("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"),
("古装", "古装"), ("历史", "历史"), ("运动", "运动"),
("农村", "农村"), ("儿童", "儿童"),
("网络电影", "网络电影")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"4": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("情感", "情感"), ("科幻", "科幻"),
("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"),
("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"),
("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"),
("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"),
("励志", "励志"), ("其他", "其他")]]},
{"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
{"n": "其它", "v": "其它"}]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"3": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("选秀", "选秀"), ("情感", "情感"),
("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"),
("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"),
("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]},
{"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
}
def homeVideoContent(self):
html = self._fetch('/')
return {"list": self._parse_video_list(html)}
def categoryContent(self, tid, pg, filter, extend):
# 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter
if tid.startswith('/label'):
url = f'{tid}/page/{pg}.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
page_count = page if len(items) < 24 else page + 2
return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24}
args = {}
if extend and isinstance(extend, dict):
for k, v in extend.items():
if v:
args[k] = str(v)
if isinstance(filter, dict):
for k, v in filter.items():
if v and k not in args:
args[k] = str(v)
route_tid = args.get('class', args.get('tid', str(tid)))
area = args.get('area', '')
genre = args.get('genre', '')
year = args.get('year', '')
lang = args.get('lang', '')
letter = args.get('letter', '')
sort = args.get('sort', '')
# 无筛选走正常分页
if not area and not genre and not year and not lang and not letter and not sort:
url = f'/cupfox-list/{route_tid}--------{pg}---.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
soup = BeautifulSoup(html, 'html.parser')
pagecount = page
for a in soup.select('a.page-link'):
if a.text == '尾页':
m = re.search(r'---(\d+)---', a.get('href', ''))
if m:
pagecount = int(m.group(1))
break
if not items:
pagecount = 0
return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999}
# 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html
segs = [route_tid, area, sort, genre, lang, letter, '', '', year]
url = '/cupfox-list/' + '-'.join(segs) + '.html'
html = self._fetch(url)
items = self._parse_video_list(html)
return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999}
def detailContent(self, ids):
result = {"list": []}
vid = ids[0].split(',')[0].strip()
try:
html = self._fetch(f'/detail/{vid}.html')
if not html: return result
soup = BeautifulSoup(html, 'html.parser')
vod_name = soup.select_one('h3.slide-info-title')
vod_name = vod_name.text.strip() if vod_name else ''
vod_pic = soup.select_one('img.lazy')
vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else ''
vod_director = ''
vod_actor = ''
for el in soup.select('.slide-info'):
text = el.get_text(' ').strip()
if text.startswith('导演:'):
vod_director = text.replace('导演:', '').strip()
elif text.startswith('演员:'):
vod_actor = text.replace('演员:', '').strip()
vod_content = soup.select_one('#height_limit')
vod_content = vod_content.get_text(' ', strip=True) if vod_content else ''
play_from, play_url = [], []
for tab in soup.select('.anthology-tab a.swiper-slide'):
src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip()
if src_name:
play_from.append(src_name)
tab_blocks = soup.select('.anthology-list-box')
for i, block in enumerate(tab_blocks):
ep_list = []
for a in block.select('li a'):
href = a.get('href', '')
m = re.search(r'/play/(.*?)\.html', href)
if m:
ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}')
ep_list.reverse()
if ep_list and i < len(play_from):
play_url.append('#'.join(ep_list))
valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)]
result["list"].append({
"vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic,
"vod_director": vod_director, "vod_actor": vod_actor,
"vod_content": vod_content,
"vod_play_from": "$$$".join(valid_from),
"vod_play_url": "$$$".join(play_url),
})
except:
pass
return result
def searchContent(self, key, quick, pg="1"):
try:
decoded = urllib.parse.unquote(key)
except:
decoded = key
html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html')
items = self._parse_search_list(html)
return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)}
def playerContent(self, flag, id, vipFlags):
url = ''
try:
url = id if id.startswith('http') else f'{self.host}/play/{id}.html'
html = self._fetch(url)
if html:
m = re.search(r'player_aaaa=(.*?)</script>', html, re.S)
if m:
try:
pd = json.loads(m.group(1))
except Exception as e:
print(e)
pd = {}
# print('pd:', pd)
play_url = pd.get('url')
play_id = pd.get('from')
api_map = {
'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php',
'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php',
}
if not play_url:
return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8',
"header": {'User-Agent': 'Mozilla/5.0'}}
if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')):
return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}}
else:
headers = {
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
'accept-language': "zh-CN,zh;q=0.9",
'cache-control': "no-cache",
'pragma': "no-cache",
'priority': "u=0, i",
'referer': "https://www.ht10010.com/",
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers)
token = re.search(r'data-te="(.*?)"', response.text)
if token:
token = token.group(1)
payload = {
'url': play_url,
'token': token
}
# print('payload', payload)
try:
response = self.post(api_map[play_id], data=payload, headers=headers)
response.raise_for_status()
result = response.json()
# print('result:', result)
if result['code'] == 200 and 'url' in result:
play_url = result['url']
return {"parse": 0, "url": play_url, "header": {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'}}
except Exception as e:
print(e)
except Exception as e:
print(e)
return {"parse": 1, "url": url}
def localProxy(self, param=''):
return {}
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def _fetch(self, url):
try:
if not url.startswith('http'):
url = self.host + url
rsp = self.fetch(url, headers=self.headers)
return rsp.text if rsp else ''
except:
return ''
def _fix_pic(self, u):
if not u: return ''
if u.startswith('//'): return 'https:' + u
return u.replace('&amp;', '&')
def _parse_video_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
span = ','.join([span.text for span in a.select('span.public-prt')])
# print('span', span)
vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or ''
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span})
return videos
def _parse_search_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]')
if title_el:
vod_name = title_el.text.strip()
else:
vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or ''
remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
return videos
if __name__ == '__main__':
sp = Spider()
sp.init()
# 20067-5-189
print(sp.categoryContent('/label/qq','1',True, {}))
# print(sp.playerContent('', '20067-6-189', []))
# print(sp.playerContent('', '20067-5-189', []))
pass
+250
View File
@@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
# by @嗷呜
import re
import sys
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.extend = extend
def getName(self):
return "毒舌影视"
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def destroy(self):
pass
host = 'https://www.xnhrsb.com/'
headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Mobile) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/114.0.0.0 Mobile Safari/537.36',
'Referer': host,
}
# 对应 js 里的 class_name / class_url
classes_config = [
("电影", "1"),
("电视剧", "2"),
("综艺", "3"),
("动漫", "4"),
("短剧", "5"),
("豆瓣", "duoban"),
]
def homeContent(self, filter):
result = {}
classes = []
vlist = []
# 静态分类
for name, tid in self.classes_config:
classes.append({
"type_name": name,
"type_id": tid
})
# 首页推荐,沿用一级规则里的 .mrb ul li
rsp = self.fetch(self.host, headers=self.headers)
data = pq(rsp.text)
# 一级: '.mrb&&ul li;.dytit&&Text;.lazy&&data-original;.hdinfo&&Text;a&&href'
vlist.extend(self.getlist(data('.mrb ul li')))
result['class'] = classes
result['list'] = vlist
return result
def homeVideoContent(self):
return {}
def categoryContent(self, tid, pg, filter, extend):
# js: url: '/dsshiyisw/fyclass--------fypage---.html'
url = f'{self.host}dsshiyisw/{tid}--------{pg}---.html'
rsp = self.fetch(url, headers=self.headers)
data = pq(rsp.text)
videos = self.getlist(data('.mrb ul li'))
result = {
'list': videos,
'page': pg,
'pagecount': 9999,
'limit': 90,
'total': 999999
}
return result
def detailContent(self, ids):
vid = ids[0]
if not vid.startswith('http'):
if vid.startswith('/'):
url = self.host.rstrip('/') + vid
else:
url = self.host.rstrip('/') + '/' + vid
else:
url = vid
rsp = self.fetch(url, headers=self.headers)
data = pq(rsp.text)
# 二级: title/img/desc/content/tabs/lists
# title: 'h1&&Text;.moviedteail_list li&&a&&Text'
name = data('h1').text()
info_list = data('.moviedteail_list li')
def li_text(idx):
return info_list.eq(idx).text() if idx < len(info_list) else ''
# 参考 desc 顺序,大致映射
# desc: '.moviedteail_list li:eq(3)&&Text;
# .moviedteail_list li:eq(2)&&Text;
# .moviedteail_list li:eq(1)&&Text;
# .moviedteail_list li:eq(6)&&Text;
# .moviedteail_list li:eq(4)&&Text'
type_name = li_text(3)
director = li_text(2)
actor = li_text(1)
remarks = li_text(6)
year_or_area = li_text(4)
pic = data('div.dyimg img').attr('src') or ''
if pic and not pic.startswith('http'):
if pic.startswith('/'):
pic = self.host.rstrip('/') + pic
else:
pic = self.host.rstrip('/') + '/' + pic
content = data('.yp_context').text()
vod = {
'vod_id': vid,
'vod_name': name,
'vod_pic': pic,
'type_name': type_name,
'vod_year': year_or_area,
'vod_area': '',
'vod_remarks': remarks,
'vod_actor': actor,
'vod_director': director,
'vod_content': content,
'vod_play_from': '',
'vod_play_url': ''
}
# tabs: '.mi_paly_box .ypxingq_t'
tabs = [i.text() for i in data('.mi_paly_box .ypxingq_t').items()]
# lists: '.paly_list_btn:eq(#id) a:gt(0)'
# 对应每个 tab 一组播放列表
play_lists = []
play_uls = list(data('.paly_list_btn').items())
for idx, ul in enumerate(play_uls):
items = []
for i, a in enumerate(ul('a').items()):
# a:gt(0) => 跳过第一个
if i == 0:
continue
title = a.text()
href = a.attr('href') or ''
if href and not href.startswith('http'):
if href.startswith('/'):
href = self.host.rstrip('/') + href
else:
href = self.host.rstrip('/') + '/' + href
items.append(f'{title}${href}')
play_lists.append('#'.join(items))
vod['vod_play_from'] = '$$$'.join(tabs)
vod['vod_play_url'] = '$$$'.join(play_lists)
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
# js: searchUrl: '/dsshiyisc/**----------fypage---.html'
url = f'{self.host}dsshiyisc/{key}----------{pg}---.html'
rsp = self.fetch(url, headers=self.headers)
data = pq(rsp.text)
videos = self.getlist(data('.mrb ul li'))
return {
'list': videos,
'page': pg
}
def playerContent(self, flag, id, vipFlags):
"""
对应 js 里的 lazy 解析:
- 如果页面中存在 player_aaaa 且脚本里有 "url":"xxx.m3u8" 就直接取 m3u8
- 否则返回原始地址
"""
if id.startswith('http'):
play_url = id
else:
if id.startswith('/'):
play_url = self.host.rstrip('/') + id
else:
play_url = self.host.rstrip('/') + '/' + id
p = 0 # 直接返回真实地址,不再二级解析
# 如果本身就是 m3u8
if '.m3u8' in play_url:
return {'parse': p, 'url': play_url, 'header': self.headers}
rsp = self.fetch(play_url, headers=self.headers)
html = rsp.text
# 查找包含 player_aaaa 的脚本并提取 m3u8
# js 里正则: /\"url\"\\s*:\\s*\"([^\"]+\\.m3u8[^\"]*)\"/
m = re.search(r'"url"\s*:\s*"([^"]+\.m3u8[^"]*)"', html)
if m:
real = m.group(1).replace('\\/', '/')
return {'parse': p, 'url': real, 'header': self.headers}
# 兜底,返回原地址
return {'parse': p, 'url': play_url, 'header': self.headers}
def localProxy(self, param):
return None
def getlist(self, data):
"""
对应 js 一级/搜索 规则:
'.mrb&&ul li;.dytit&&Text;.lazy&&data-original;.hdinfo&&Text;a&&href'
"""
vlist = []
for j in data.items():
name = j('.dytit').text()
pic = j('.lazy').attr('data-original') or ''
remark = j('.hdinfo').text()
href = j('a').attr('href') or ''
if pic and not pic.startswith('http'):
if pic.startswith('/'):
pic = self.host.rstrip('/') + pic
else:
pic = self.host.rstrip('/') + '/' + pic
vod_id = href
vlist.append({
'vod_id': vod_id,
'vod_name': name,
'vod_pic': pic,
'vod_remarks': remark
})
return vlist
+270
View File
@@ -0,0 +1,270 @@
"""
@header({
searchable: 1,
filterable: 1,
quickSearch: 1,
title: '独播库',
lang: 'hipy'
})
"""
# -*- coding: utf-8 -*-
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
from base.spider import Spider
import sys,time,json,base64,random,urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
sys.path.append('..')
class Spider(Spider):
headers,host = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'Referer': 'https://www.dbku.tv/'
}, 'https://api.dbokutv.com'
def init(self, extend=""):
pass
filters = {
1: [
{'key': 'area', 'name': '地区', 'value': [
{'n': '全部', 'v': ''}, {'n': '大陆', 'v': '大陆'}, {'n': '香港', 'v': '香港'},
{'n': '台湾', 'v': '台湾'}, {'n': '韩国', 'v': '韩国'}, {'n': '日本', 'v': '日本'},
{'n': '欧美', 'v': '欧美'}, {'n': '泰国', 'v': '泰国'}]},
{'key': 'order', 'name': '排序', 'value': [
{'n': '时间', 'v': ''}, {'n': '人气', 'v': '人气'}, {'n': '评分', 'v': '评分'}]},
{'key': 'class', 'name': '剧情', 'value': [
{'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': '短片'}]},
{'key': 'lang', 'name': '语言', 'value': [
{'n': '全部', 'v': ''}, {'n': '国语', 'v': '国语'}, {'n': '粤语', 'v': '粤语'},
{'n': '英语', 'v': '英语'}, {'n': '韩语', 'v': '韩语'}, {'n': '日语', 'v': '日语'},
{'n': '泰语', 'v': '泰语'}]},
{'key': 'year', 'name': '年份', 'value': [
{'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': '更早', 'v': '更早'}]},
],
2: [
{'key': 'area', 'name': '地区', 'value': [
{'n': '全部', 'v': ''}, {'n': '大陆', 'v': '大陆'}, {'n': '香港', 'v': '香港'},
{'n': '台湾', 'v': '台湾'}, {'n': '韩国', 'v': '韩国'}, {'n': '日本', 'v': '日本'},
{'n': '新加坡', 'v': '新加坡'}, {'n': '泰国', 'v': '泰国'}]},
{'key': 'order', 'name': '排序', 'value': [
{'n': '时间', 'v': ''}, {'n': '人气', 'v': '人气'}, {'n': '评分', 'v': '评分'}]},
{'key': 'class', 'name': '剧情', 'value': [
{'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': '同性'}]},
{'key': 'lang', 'name': '语言', 'value': [
{'n': '全部', 'v': ''}, {'n': '国语', 'v': '国语'}, {'n': '粤语', 'v': '粤语'},
{'n': '韩语', 'v': '韩语'}, {'n': '泰语', 'v': '泰语'}, {'n': '日语', 'v': '日语'}]},
{'key': 'year', 'name': '年份', 'value': [
{'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': '更早', 'v': '更早'}]},
],
3: [
{'key': 'area', 'name': '地区', 'value': [
{'n': '全部', 'v': ''}, {'n': '大陆', 'v': '大陆'}, {'n': '香港', 'v': '香港'},
{'n': '台湾', 'v': '台湾'}, {'n': '韩国', 'v': '韩国'}, {'n': '日本', 'v': '日本'},
{'n': '欧美', 'v': '欧美'}]},
{'key': 'order', 'name': '排序', 'value': [
{'n': '时间', 'v': ''}, {'n': '人气', 'v': '人气'}, {'n': '评分', 'v': '评分'}]},
{'key': 'year', 'name': '年份', 'value': [
{'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': '更早', 'v': '更早'}]},
],
4: [
{'key': 'area', 'name': '地区', 'value': [
{'n': '全部', 'v': ''}, {'n': '大陆', 'v': '大陆'}, {'n': '日本', 'v': '日本'},
{'n': '欧美', 'v': '欧美'}, {'n': '韩国', 'v': '韩国'}]},
{'key': 'order', 'name': '排序', 'value': [
{'n': '时间', 'v': ''}, {'n': '人气', 'v': '人气'}, {'n': '评分', 'v': '评分'}]},
{'key': 'year', 'name': '年份', 'value': [
{'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': '更早', 'v': '更早'}]},
],
}
def homeContent(self, filter):
if not self.host: return None
result = {'class': [
{'type_id': 1, 'type_name': '电影'},
{'type_id': 2, 'type_name': '连续剧'},
{'type_id': 3, 'type_name': '综艺'},
{'type_id': 4, 'type_name': '动漫'}]}
if filter:
result['filters'] = self.filters
return result
def homeVideoContent(self):
response = self.fetch(self.sign(f'{self.host}/home'), headers=self.headers, verify=False).json()
videos = []
for i in response:
for j in i.get('VodList',[]):
videos.append({
'vod_id': self.decode(j.get('DId', j.get('DuId'))),
'vod_name': j['Name'],
'vod_pic': self.decode(j['TnId']),
'vod_remarks': j['Tag']
})
return {'list': videos}
def categoryContent(self, tid, pg, filter, extend):
area = extend.get('area', '')
order = extend.get('order', '')
cls = extend.get('class', '')
lang = extend.get('lang', '')
year = extend.get('year', '')
pg_str = '' if str(pg) == '1' else str(pg)
# URL格式: /vodshow/{tid}-{area}-{order}-{class}-{lang}-{letter}-{?}-{?}-{pg}-{?}-{year}
path = f"{self.host}/vodshow/{tid}-{area}-{order}-{cls}-{lang}----{pg_str}---{year}"
response = self.fetch(self.sign(path), headers=self.headers, verify=False).json()
videos,pagecount = [], 0
for i in response['VodList']:
videos.append({
'vod_id': self.decode(i.get('DId',i.get('DuId'))),
'vod_name': i['Name'],
'vod_pic': self.decode(i['TnId']),
'vod_remarks': i['Tag']
})
try:
for j in response['PaginationList']:
if j['Type'] == 'StartEnd':
pgcount = int(self.decode(j.get('PId',j.get('PuId'))).split('-')[8])
if pgcount: pagecount = pgcount
except Exception:
pass
if pagecount == 0:
try:
for j in response['PaginationList']:
if j['Type'] == 'ShortPage':
pgcount = int(j['Name'].split('/')[1])
if pgcount: pagecount = pgcount
except Exception:
pagecount = 1
return {'list': videos,'pagecount': pagecount, 'page': pg}
def searchContent(self, key, quick, pg='1'):
response = self.fetch(f"{self.sign(f'{self.host}/vodsearch')}&wd={key}", headers=self.headers, verify=False).json()
videos = []
for i in response:
videos.append({
'vod_id': self.decode(i.get('DId', i.get('DuId'))),
'vod_name': i['Name'],
'vod_pic': self.decode(i['TnId']),
'vod_remarks': i['Tag'],
'vod_actor': i['Actor'],
'vod_score': i['Rating']
})
return {'list': videos, 'page': pg}
def detailContent(self, ids):
data = self.fetch(self.sign(f"{self.host}{ids[0]}"), headers=self.headers, verify=False).json()
play_urls = [f"{i['EpisodeName']}${self.decode(i['VId'])}" for i in data['Playlist']]
video = {
'vod_id': ids[0],
'vod_name': data['Name'],
'vod_pic': self.decode(data['TnId']),
'vod_remarks': f"评分:{data['Rating']}",
'vod_year': data['ReleaseYear'],
'vod_area': data['Region'],
'vod_actor': ','.join(data['Actor']),
'vod_director': data['Director'],
'vod_content': data['Description'],
'vod_play_from': '独播库',
'vod_play_url': '#'.join(play_urls),
'type_name': f"{data['Genre']},{data['Scenario']},{data['Language']}"
}
return {'list': [video]}
def playerContent(self, flag, video_id, vip_flags):
res = self.fetch(self.sign(f"{self.host}{video_id}"), headers=self.headers, verify=False).json()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate',
'origin': 'https://w.duboku.io',
'referer': 'https://w.duboku.io/',
'priority': 'u=1, i'
}
return { 'jx': 0, 'parse': 0, 'url': self.decode(res['HId']), 'header': headers}
def decode(self, data):
if not data or not isinstance(data, str): return ''
stripped_str = data.strip('\'"')
if not stripped_str: return ''
segment_length = 10
try:
reversed_segments = (stripped_str[i:i + segment_length][::-1] for i in range(0, len(stripped_str), segment_length))
processed_base64 = ''.join(reversed_segments).replace('.', '=')
except Exception:
return ''
try:
padding_needed = 4 - len(processed_base64) % 4
if padding_needed != 4:
processed_base64 += '=' * padding_needed
decoded_bytes = base64.b64decode(processed_base64, validate=True)
return decoded_bytes.decode('utf-8')
except Exception:
return ''
def sign(self,raw):
random.seed(int(time.time()))
random_number = random.randint(0, 800000000)
value_a = random_number + 100000000
value_b = 900000000 - random_number
current_unix_timestamp = int(time.time())
unix_timestamp_str = str(current_unix_timestamp)
interleaved_str = self.interleave_strings(f'{value_a}{value_b}', unix_timestamp_str)
ssid_base64_encoded = base64.b64encode(interleaved_str.encode()).decode().replace('=', '.')
random_sign = self.random_string(60)
random_token = self.random_string(38)
return f"{raw}?sign={random_sign}&token={random_token}&ssid={ssid_base64_encoded}"
def interleave_strings(self,first_str, second_str):
interleaved_chars = []
min_length = min(len(first_str), len(second_str))
for i in range(min_length):
interleaved_chars.append(first_str[i])
interleaved_chars.append(second_str[i])
interleaved_chars.append(first_str[min_length:])
interleaved_chars.append(second_str[min_length:])
return ''.join(interleaved_chars)
def random_string(self, length):
character = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
random.seed(int(time.time()) + len(character))
return ''.join(random.choice(character) for _ in range(length))
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def localProxy(self, param):
pass