Files
2026-08-14 11:24:20 +02:00

388 lines
16 KiB
Python

# coding=utf-8
"""
目标站: 4kvm 首页: https://www.4kvm.net
动态筛选、精准分集、去重列表
"""
import re
import sys
import json
import urllib.parse
from bs4 import BeautifulSoup
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.site_url = "https://www.4kvm.top"
self.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',
'Referer': self.site_url,
'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'
}
self.categories = [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "3", "type_name": "动漫"}
]
self._filters_cache = None
# ================= 动态筛选解析 =================
def _fetch_filters_for_classify(self, tid):
"""请求 /filter?classify=tid,解析页面筛选区域,返回该分类的筛选列表"""
url = f"{self.site_url}/filter?classify={tid}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return []
soup = BeautifulSoup(resp.text, 'html.parser')
filter_groups = []
containers = soup.select('main div.flex.flex-wrap.items-center.gap-3')
for container in containers:
links = container.select('a[href]')
if len(links) < 2:
continue
first_text = links[0].get_text(strip=True)
if not first_text.startswith('全部'):
continue
group_name = first_text.replace('全部', '', 1).strip()
# 从非全部的链接中提取参数键
param_key = None
for a in links[1:]:
href = a.get('href', '')
parsed = urllib.parse.urlparse(href)
qs = urllib.parse.parse_qs(parsed.query)
for k in qs:
if k not in ('classify', 'page'):
param_key = k
break
if param_key:
break
if not param_key:
continue
if param_key in ('sort_by', 'order'):
continue
options = []
for a in links:
text = a.get_text(strip=True)
href = a.get('href', '')
parsed = urllib.parse.urlparse(href)
qs = urllib.parse.parse_qs(parsed.query)
val = ''
if param_key in qs:
val = qs[param_key][0] if qs[param_key] else ''
if text.startswith('全部'):
val = ''
options.append({"n": text, "v": val})
if options:
filter_groups.append({
"key": param_key,
"name": group_name,
"value": options
})
return filter_groups
def _get_all_filters(self):
if self._filters_cache is not None:
return self._filters_cache
filters = {}
for cat in self.categories:
tid = cat["type_id"]
groups = self._fetch_filters_for_classify(tid)
if groups:
filters[tid] = groups
# 为没有筛选的分类复用电影分类的筛选
if "1" in filters:
if "3" not in filters:
filters["3"] = filters["1"]
if "4" not in filters:
filters["4"] = filters["1"]
self._filters_cache = filters
return filters
# ================= 核心业务方法 =================
def homeContent(self, filter):
url = self.site_url + "/"
resp = self.fetch(url, headers=self.headers)
video_list = []
if resp:
soup = BeautifulSoup(resp.text, 'html.parser')
# 使用唯一卡片容器
cards = soup.select('div[data-vod-id]')
for card in cards[:20]:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
return {"class": self.categories, "list": video_list, "filters": self._get_all_filters()}
def homeVideoContent(self):
return self.homeContent(False)
def categoryContent(self, tid, pg, filter, extend):
page = int(pg) if pg else 1
params = {"classify": tid}
if extend:
for k, v in extend.items():
if v and k != 'classify':
params[k] = v
if page > 1:
params['page'] = page
query = urllib.parse.urlencode(params)
url = f"{self.site_url}/filter?{query}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 24, "total": 0}
soup = BeautifulSoup(resp.text, 'html.parser')
video_list = []
cards = soup.select('div[data-vod-id]')
for card in cards:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
# 分页处理
pagecount = page
page_text = soup.find(string=re.compile(r'共\s*\d+\s*页'))
if page_text:
nums = re.findall(r'\d+', page_text)
if nums:
pagecount = int(nums[-1])
else:
page_block = soup.select_one('.flex.justify-center')
if page_block:
page_links = page_block.select('a[href*="page="]')
for a in page_links:
text = a.get_text(strip=True)
if text.isdigit():
pagecount = max(pagecount, int(text))
return {
"list": video_list,
"page": page,
"pagecount": pagecount,
"limit": 24,
"total": len(video_list) * pagecount
}
def detailContent(self, ids):
if not ids:
return {"list": []}
vod_id = ids[0]
url = f"{self.site_url}/play/{vod_id}"
resp = self.fetch(url, headers=self.headers)
if not resp or resp.status_code != 200:
return {"list": []}
soup = BeautifulSoup(resp.text, 'html.parser')
# 标题
title_elem = soup.select_one('h1.text-xl') or soup.select_one('h1') or soup.select_one('h2')
vod_name = title_elem.get_text(strip=True) if title_elem else vod_id
# 图片
vod_pic = ''
img_elem = soup.select_one('img.w-full') or soup.select_one('img[src]')
if img_elem:
src = img_elem.get('src', '') or img_elem.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
# 导演、主演、简介
vod_director = ''
vod_actor = ''
vod_content = ''
info_block = soup.select_one('.rounded-lg div.grid') or soup.select_one('div.grid')
if info_block:
text = info_block.get_text(' ', strip=True)
dir_match = re.search(r'导演\s*([^主\n]+)', text)
if dir_match:
vod_director = dir_match.group(1).strip()
act_match = re.search(r'主演\s*([^剧\n]+)', text)
if act_match:
vod_actor = act_match.group(1).strip()
desc_match = re.search(r'剧情简介\s*(.+)', text, re.DOTALL)
if desc_match:
vod_content = desc_match.group(1).strip()
elif re.search(r'简介\s*(.+)', text, re.DOTALL):
vod_content = re.search(r'简介\s*(.+)', text, re.DOTALL).group(1).strip()
# ================= 分集解析 (基于 episodeManager) =================
play_from_list = []
play_url_list = []
episode_manager = soup.select_one('[x-data*="episodeManager"]')
if episode_manager:
xdata = episode_manager.get('x-data', '')
lines_raw = re.findall(r'\{[^}]*lineName\s*:\s*\'([^\']+)\'[^}]*episodeCount\s*:\s*(\d+)[^}]*\}', xdata)
lines_info = [{'lineName': name, 'episodeCount': int(count)} for name, count in lines_raw]
episode_links = episode_manager.select('a[data-episode]')
lines_eps = {}
for a in episode_links:
line = a.get('data-line', '1')
ep = a.get('data-episode', '')
href = a.get('href', '')
if not href or not ep:
continue
full_url = href if href.startswith('http') else self.site_url + href
lines_eps.setdefault(line, []).append((int(ep), full_url))
for line_key in sorted(lines_eps.keys()):
eps = sorted(lines_eps[line_key], key=lambda x: x[0])
line_name = f'线路{line_key}'
for info in lines_info:
line_name = info['lineName']
break # 目前只用第一个线路名
if not eps:
continue
episode_strs = [f"第{ep[0]}集${ep[1]}" for ep in eps]
play_from_list.append(line_name)
play_url_list.append('#'.join(episode_strs))
# 回退:无分集则直接播放当前页
if not play_url_list:
play_from_list.append('播放')
play_url_list.append(f"播放${vod_id}")
vod_play_from = '$$$'.join(play_from_list)
vod_play_url = '$$$'.join(play_url_list)
result = [{
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_content": vod_content,
"vod_actor": vod_actor,
"vod_director": vod_director,
"vod_area": "",
"vod_year": "",
"vod_play_from": vod_play_from,
"vod_play_url": vod_play_url
}]
return {"list": result}
def searchContent(self, key, quick, pg="1"):
page = int(pg) if pg else 1
params = {"q": key}
if page > 1:
params['page'] = page
query = urllib.parse.urlencode(params)
url = f"{self.site_url}/search?{query}"
resp = self.fetch(url, headers=self.headers)
if not resp:
return {"list": [], "page": page, "pagecount": 1}
soup = BeautifulSoup(resp.text, 'html.parser')
video_list = []
cards = soup.select('div[data-vod-id]')
if not cards:
# 搜索页可能没有 data-vod-id,降级处理
for a in soup.select('a.block[href^="/play/"]'):
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
h3 = a.select_one('h3')
vod_name = h3.get_text(strip=True) if h3 else href
if not vod_name:
continue
img = a.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": ''
})
else:
for card in cards[:30]:
a = card.select_one('a.block[href^="/play/"]')
if not a:
continue
vod_id = card.get('data-vod-id', '').strip()
if not vod_id:
href = a.get('href', '')
vod_id = href.replace('/play/', '').strip()
if not vod_id:
continue
title_tag = card.select_one('h3.text-white') or card.select_one('h3')
vod_name = title_tag.get_text(strip=True) if title_tag else ''
if not vod_name:
continue
img = card.select_one('img[data-src]')
vod_pic = ''
if img:
src = img.get('data-src', '')
if src and not src.startswith('data:'):
vod_pic = src if src.startswith('http') else 'https:' + src
remark_tag = card.select_one('.text-green-500, .text-yellow-400, span[class*="px-1.5"]')
vod_remarks = remark_tag.get_text(strip=True) if remark_tag else ''
video_list.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks
})
return {"list": video_list, "page": page, "pagecount": 1}
def playerContent(self, flag, id, vipFlags):
if not id.startswith('http'):
url = f"{self.site_url}/play/{id}"
else:
url = id
return {"parse": 1, "url": url, "header": self.headers}