299 lines
12 KiB
Python
299 lines
12 KiB
Python
#!/usr/bin/python
|
|
import sys
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
import json
|
|
import urllib.parse
|
|
import re
|
|
|
|
class Spider(Spider):
|
|
|
|
def getName(self):
|
|
return "Avple.tv"
|
|
|
|
def init(self, extend=""):
|
|
self.host = "https://avple.tv"
|
|
self.headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
'Connection': 'keep-alive',
|
|
'Referer': self.host
|
|
}
|
|
self.log(f"{self.getName()} 爬虫初始化完成,主站: {self.host}")
|
|
|
|
def isVideoFormat(self, url):
|
|
return False
|
|
|
|
def manualVideoCheck(self):
|
|
return True
|
|
|
|
def homeContent(self, filter):
|
|
"""获取首页内容和分类"""
|
|
result = {}
|
|
classes = self._getCategories()
|
|
result['class'] = classes
|
|
try:
|
|
rsp = self.fetch(self.host, headers=self.headers)
|
|
html = rsp.text
|
|
videos = self._getVideos(html)
|
|
result['list'] = videos
|
|
except Exception as e:
|
|
self.log(f"首页获取出错: {str(e)}")
|
|
result['list'] = []
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
"""首页视频内容(可留空)"""
|
|
return {'list': []}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
"""分类内容"""
|
|
try:
|
|
pg_int = int(pg)
|
|
if tid == 'trending':
|
|
url = f"{self.host}/trending/daily/{pg_int}"
|
|
elif tid == 'latest':
|
|
url = f"{self.host}/latest/{pg_int}"
|
|
else:
|
|
url = f"{self.host}/tags/{str(tid)}/{pg_int}/date"
|
|
|
|
self.log(f"访问分类URL: {url}")
|
|
rsp = self.fetch(url, headers=self.headers)
|
|
html = rsp.text
|
|
|
|
videos = self._getVideos(html)
|
|
|
|
pagecount = 999
|
|
limit = 24
|
|
total = 999999
|
|
|
|
if not videos and pg_int == 1:
|
|
self.log(f"警告: 分类ID {tid}, 页码 {pg} 未找到任何视频。URL: {url}")
|
|
elif not videos:
|
|
self.log(f"信息: 分类ID {tid}, 页码 {pg} 没有更多视频。URL: {url}")
|
|
|
|
return {
|
|
'list': videos,
|
|
'page': pg_int,
|
|
'pagecount': pagecount,
|
|
'limit': limit,
|
|
'total': total
|
|
}
|
|
except Exception as e:
|
|
self.log(f"分类内容获取出错 (tid={tid}, pg={pg}): {str(e)}")
|
|
return {'list': []}
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
"""搜索功能"""
|
|
try:
|
|
search_url = f"{self.host}/search/{urllib.parse.quote(key)}/{pg}"
|
|
self.log(f"搜索URL: {search_url}")
|
|
|
|
rsp = self.fetch(search_url, headers=self.headers)
|
|
html = rsp.text
|
|
videos = self._getVideos(html)
|
|
|
|
return {'list': videos}
|
|
except Exception as e:
|
|
self.log(f"搜索出错: {str(e)}")
|
|
return {'list': []}
|
|
|
|
def detailContent(self, ids):
|
|
"""详情页面"""
|
|
try:
|
|
vid = ids[0]
|
|
detail_url = f"{self.host}/video/{vid}"
|
|
self.log(f"详情URL: {detail_url}")
|
|
rsp = self.fetch(detail_url, headers=self.headers)
|
|
html = rsp.text
|
|
video_info = self._getDetail(html, vid)
|
|
return {'list': [video_info]} if video_info else {'list': []}
|
|
except Exception as e:
|
|
self.log(f"详情获取出错 (vid: {ids[0]}): {str(e)}")
|
|
return {'list': []}
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
"""播放链接解析"""
|
|
try:
|
|
video_url = f"https://s4.avple.tv/video/{id}"
|
|
self.log(f"✅ 找到视频直链: {video_url}")
|
|
return {
|
|
'parse': 0,
|
|
'playUrl': '',
|
|
'url': video_url,
|
|
'header': json.dumps(self.headers)
|
|
}
|
|
except Exception as e:
|
|
self.log(f"播放链接获取出错 (id: {id}): {str(e)}")
|
|
return {'parse': 1, 'playUrl': '', 'url': f"{self.host}/video/{id}"}
|
|
|
|
def _getCategories(self):
|
|
"""从首页提取分类"""
|
|
categories = []
|
|
try:
|
|
categories.append({'type_id': 'trending', 'type_name': '近期热门'})
|
|
categories.append({'type_id': 'latest', 'type_name': '最新影片'})
|
|
|
|
rsp = self.fetch(self.host, headers=self.headers)
|
|
html = rsp.text
|
|
|
|
category_pattern = r'<a\s+[^>]*?href="(/tags/(\d+)/1/date)"[^>]*>([^<]+?)</a>'
|
|
matches = re.findall(category_pattern, html, re.IGNORECASE)
|
|
|
|
for _, cat_id, cat_name in matches:
|
|
if not any(c['type_id'] == cat_id for c in categories):
|
|
categories.append({'type_id': cat_id, 'type_name': cat_name.strip()})
|
|
|
|
if len(categories) < 5:
|
|
self.log("动态获取分类失败或分类过少,使用硬编码分类。")
|
|
hardcoded_categories = [
|
|
{'type_id': '121', 'type_name': '麻豆傳媒'},
|
|
{'type_id': '123', 'type_name': '果凍傳媒'},
|
|
{'type_id': '124', 'type_name': '皇家華人'},
|
|
{'type_id': '125', 'type_name': '精東影業'},
|
|
{'type_id': '126', 'type_name': '天美傳媒'},
|
|
{'type_id': '127', 'type_name': '星空無限傳媒'},
|
|
{'type_id': '128', 'type_name': '樂播傳媒'},
|
|
{'type_id': '129', 'type_name': '蜜桃傳媒'},
|
|
{'type_id': '130', 'type_name': '烏鴉傳媒'},
|
|
{'type_id': '131', 'type_name': '國產自拍'},
|
|
{'type_id': '122', 'type_name': 'SWAG'},
|
|
{'type_id': '135', 'type_name': 'FC2PPV'},
|
|
{'type_id': '15', 'type_name': '黑絲'},
|
|
{'type_id': '113', 'type_name': '旗袍'},
|
|
{'type_id': '49', 'type_name': '校服'},
|
|
{'type_id': '13', 'type_name': '絲襪'},
|
|
{'type_id': '97', 'type_name': '女僕'},
|
|
{'type_id': '76', 'type_name': '吊帶襪'},
|
|
{'type_id': '87', 'type_name': '兔女郎'},
|
|
{'type_id': '1', 'type_name': '巨乳'},
|
|
{'type_id': '63', 'type_name': '貧乳'},
|
|
{'type_id': '79', 'type_name': '露出'},
|
|
{'type_id': '2', 'type_name': '中出'},
|
|
{'type_id': '32', 'type_name': '顏射'},
|
|
{'type_id': '18', 'type_name': '潮吹'},
|
|
{'type_id': '53', 'type_name': '綑綁'},
|
|
{'type_id': '33', 'type_name': '多P'}
|
|
]
|
|
for hc in hardcoded_categories:
|
|
if not any(c['type_id'] == hc['type_id'] for c in categories):
|
|
categories.append(hc)
|
|
|
|
return categories
|
|
|
|
except Exception as e:
|
|
self.log(f"获取分类出错: {str(e)}")
|
|
return [{'type_id': 'trending', 'type_name': '近期热门'}, {'type_id': 'latest', 'type_name': '最新影片'}]
|
|
|
|
def _getVideos(self, html):
|
|
"""从HTML中提取视频列表"""
|
|
videos = []
|
|
try:
|
|
# 尝试匹配首页的JSON-LD结构(最稳定)
|
|
json_ld_match = re.search(r'<script type="application/ld\+json" data-next-head="">(.*?)<\/script>', html, re.DOTALL)
|
|
if json_ld_match:
|
|
json_content = json_ld_match.group(1).strip()
|
|
data = json.loads(json_content)
|
|
if '@graph' in data:
|
|
for item in data['@graph']:
|
|
if item.get('@type') == 'ListItem':
|
|
video_id = item.get('item', '').split('/')[-1]
|
|
video_name = item.get('name', '未知视频')
|
|
thumb_url = f"https://avple-poster.b-cdn.net/video/{video_id}.png"
|
|
video = {
|
|
'vod_id': video_id,
|
|
'vod_name': video_name.strip(),
|
|
'vod_pic': thumb_url,
|
|
'vod_remarks': video_name.split('] ')[0].replace('[', '') if ']' in video_name else ''
|
|
}
|
|
videos.append(video)
|
|
|
|
# 如果JSON-LD解析失败或没有找到数据,则尝试匹配分类页的HTML结构
|
|
if not videos:
|
|
self.log("未从JSON-LD中解析到视频,尝试备用HTML匹配...")
|
|
# 匹配新的HTML结构:每个视频项在一个<a>标签里
|
|
item_pattern = r'<a class="group[^>]*href="/video/(\d+)"[^>]*>.*?data-src="([^"]+)".*?<h2[^>]*?>(.*?)</h2>.*?<p[^>]*?class="text-xs[^"]*?">(.*?)</p>'
|
|
items = re.findall(item_pattern, html, re.DOTALL | re.IGNORECASE)
|
|
|
|
for vid, img_url, title, remark in items:
|
|
# 清理标题和备注中的HTML标签
|
|
title = re.sub(r'<[^>]+>', '', title).strip()
|
|
remark = re.sub(r'<[^>]+>', '', remark).strip()
|
|
img_url = img_url.strip()
|
|
|
|
# 确保图片URL是完整的
|
|
if not img_url.startswith('http'):
|
|
img_url = self.host + img_url
|
|
|
|
video = {
|
|
'vod_id': vid,
|
|
'vod_name': title if title else f"视频 {vid}",
|
|
'vod_pic': img_url,
|
|
'vod_remarks': remark
|
|
}
|
|
videos.append(video)
|
|
|
|
except Exception as e:
|
|
self.log(f"解析视频列表出错: {str(e)}")
|
|
|
|
return videos
|
|
|
|
def _getDetail(self, html, vid):
|
|
"""获取详情信息"""
|
|
try:
|
|
title = self.regStr(r'<h1 class="text-white text-lg font-bold">([^<]+)</h1>', html)
|
|
pic = self.regStr(r'data-src="(https://avple-poster\.b-cdn\.net/video/[^"]+\.png)"', html)
|
|
desc_element = re.search(r'<div id="detail-desc" class="break-words[^>]*">([\s\S]*?)</div>', html)
|
|
desc = ""
|
|
if desc_element:
|
|
desc = desc_element.group(1).strip().replace('<br>', '\n').replace('</br>', '')
|
|
else:
|
|
desc = title if title else "暂无简介"
|
|
actor = self.regStr(r'<span>演員:</span>\s*([^<]+)', html)
|
|
director = ""
|
|
play_from = ["默认源"]
|
|
play_url_list = [f"第1集${vid}"]
|
|
type_name_match = re.search(r'<div class="text-white">[^>]*<a href="/tags/(\d+)/\d+/date">([^<]+)</a>', html)
|
|
type_name = type_name_match.group(2).strip() if type_name_match else "未知"
|
|
return {
|
|
'vod_id': vid,
|
|
'vod_name': title if title else f"视频ID: {vid}",
|
|
'vod_pic': pic if pic else "",
|
|
'type_name': type_name,
|
|
'vod_year': "未知",
|
|
'vod_area': "未知",
|
|
'vod_remarks': "高清",
|
|
'vod_actor': actor if actor else "未知",
|
|
'vod_director': director,
|
|
'vod_content': desc,
|
|
'vod_play_from': '$$$'.join(play_from),
|
|
'vod_play_url': '$$$'.join(play_url_list)
|
|
}
|
|
except Exception as e:
|
|
self.log(f"获取详情失败 (vid={vid}): {str(e)}")
|
|
return {
|
|
'vod_id': vid,
|
|
'vod_name': "加载失败",
|
|
'vod_pic': "",
|
|
'type_name': "未知",
|
|
'vod_year': "",
|
|
'vod_area': "",
|
|
'vod_remarks': "",
|
|
'vod_actor': "",
|
|
'vod_director': "",
|
|
'vod_content': "详情加载失败",
|
|
'vod_play_from': "默认源",
|
|
'vod_play_url': f"第1集${vid}"
|
|
}
|
|
def regStr(self, pattern, string):
|
|
"""正则提取第一个匹配组,并处理空值"""
|
|
try:
|
|
match = re.search(pattern, string, re.DOTALL)
|
|
return match.group(1).strip() if match and match.group(1) else ""
|
|
except Exception as e:
|
|
self.log(f"正则提取出错 (pattern: {pattern}): {str(e)}")
|
|
return ""
|
|
|