ファイル
sg/py/稀饭动漫.py
T
2026-08-14 11:26:45 +02:00

296 行
12 KiB
Python
Raw Blame 履歴

このファイルには曖昧(ambiguous)なUnicode文字が含まれています
このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
# junyouyun
import re
import json
from urllib.parse import quote, urljoin, parse_qs
import requests
from bs4 import BeautifulSoup
from base.spider import Spider
BASE = "https://anime.xifanacg.com"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Referer": BASE,
}
CLASS_OPTIONS = {
"1": ["搞笑", "原创", "轻小说改", "恋爱", "百合", "漫改", "校园", "战斗", "治愈", "奇幻",
"日常", "青春", "乙女向", "悬疑", "后宫", "科幻", "冒险", "热血", "异世界", "游戏改",
"音乐", "偶像", "美食", "耽美"],
"2": ["搞笑", "原创", "轻小说改", "恋爱", "百合", "漫改", "校园", "战斗", "治愈", "奇幻",
"日常", "青春", "乙女向", "悬疑", "后宫", "科幻", "冒险", "热血", "异世界", "游戏改",
"音乐", "偶像", "美食", "耽美", "2026年1月", "2025年10月"],
"3": [],
"21": [],
}
AREA_OPTIONS = {
"1": ["日本"],
"2": [],
"3": ["日本"],
"21": [],
}
YEAR_RANGE = [str(y) for y in range(2026, 2004, -1)]
ORDER_OPTIONS = [
{"n": "按最新", "v": "time"},
{"n": "按最热", "v": "hits"},
{"n": "按评分", "v": "score"},
]
class Spider(Spider):
def init(self, extend=""):
self.session = requests.Session()
self.session.headers.update(HEADERS)
def _get_decoded_html(self, url):
r = self.session.get(url, timeout=15)
r.encoding = r.apparent_encoding if r.apparent_encoding else 'utf-8'
return r.text
def _parse_vod_cards(self, html):
soup = BeautifulSoup(html, 'html.parser')
vod_list = []
for box in soup.select('.public-list-box.public-pic-b'):
link = box.select_one('a.public-list-exp')
if not link:
continue
href = link.get('href', '')
vid = href.split('/')[-1].replace('.html', '')
title = link.get('title', '')
img = box.select_one('img.gen-movie-img')
pic = img.get('data-src', '') if img else ''
prb = box.select_one('.public-list-prb')
remarks = prb.get_text(strip=True) if prb else ''
vod_list.append({
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_remarks": remarks
})
return vod_list
def homeContent(self, filter):
classes = [
{"type_name": "连载新番", "type_id": "1"},
{"type_name": "完结旧番", "type_id": "2"},
{"type_name": "剧场版", "type_id": "3"},
{"type_name": "美漫", "type_id": "21"},
]
filters = {}
for tid in ["1", "2", "3", "21"]:
f_list = []
if CLASS_OPTIONS[tid]:
f_list.append({
"key": "class", "name": "类型",
"value": [{"n": "全部", "v": ""}] + [{"n": c, "v": c} for c in CLASS_OPTIONS[tid]]
})
if AREA_OPTIONS[tid]:
f_list.append({
"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}] + [{"n": a, "v": a} for a in AREA_OPTIONS[tid]]
})
if tid != "21":
f_list.append({
"key": "year", "name": "年份",
"value": [{"n": "全部", "v": ""}] + [{"n": y, "v": y} for y in YEAR_RANGE]
})
f_list.append({"key": "by", "name": "排序", "value": ORDER_OPTIONS})
filters[tid] = f_list
return {"class": classes, "filters": filters}
def homeVideoContent(self):
try:
html = self._get_decoded_html(BASE)
return {"list": self._parse_vod_cards(html)}
except Exception:
data = self.categoryContent("1", "1", False, {})
return {"list": data.get("list", [])}
def categoryContent(self, tid, pg, filter, extend):
"""
分类列表:POST 请求 /index.php/ds_api/vod
传递完整表单参数(包含空字段 lang/version/state/letter/time/level/weekday)
"""
# 2. 构建完整表单数据(必须包含所有字段,空值用空字符串)
form_data = {
'type': tid,
'class': extend.get("class", ""),
'area': extend.get("area", ""),
'year': extend.get("year", ""),
'lang': extend.get("lang", ""),
'version': extend.get("version", ""),
'state': extend.get("state", ""),
'letter': extend.get("letter", ""),
'time': extend.get("time", ""),
'level': extend.get("level", "0"),
'weekday': extend.get("weekday", ""),
'by': extend.get("by", "time"),
'page': pg,
}
url = f"{BASE}/index.php/ds_api/vod"
try:
r = self.session.post(url, data=form_data, timeout=15)
data = r.json()
except Exception:
return {"list": [], "page": int(pg), "pagecount": 1, "limit": 40, "total": 0}
vod_list = []
for item in data.get("list", []):
vod_id = str(item.get("vod_id", ""))
actor_raw = item.get("vod_actor", "")
actor = ','.join([a for a in actor_raw.split(',') if a.strip()]) if actor_raw else ''
vod_list.append({
"vod_id": vod_id,
"vod_name": item.get("vod_name", ""),
"vod_pic": item.get("vod_pic", ""),
"vod_remarks": item.get("vod_remarks", ""),
"vod_actor": actor,
"vod_douban_score": str(item.get("vod_douban_score", "")) if item.get("vod_douban_score") else "",
})
return {
"list": vod_list,
"page": int(data.get("page", pg)),
"pagecount": int(data.get("pagecount", 1)),
"limit": int(data.get("limit", 40)),
"total": int(data.get("total", 0)),
}
def detailContent(self, ids):
vid = ids[0] if isinstance(ids, list) else ids
url = f"{BASE}/bangumi/{vid}.html"
try:
html = self._get_decoded_html(url)
except Exception:
return {"list": [{"vod_id": vid, "vod_name": "获取失败"}]}
soup = BeautifulSoup(html, 'html.parser')
name = soup.select_one('h3.slide-info-title')
name = name.text.strip() if name else ''
pic_tag = soup.select_one('.detail-pic img.lazy')
pic = pic_tag.get('data-src', '') if pic_tag else ''
score_tag = soup.select_one('.fraction')
score = score_tag.text.strip() if score_tag else ''
remarks_tag = soup.select_one('.slide-info-remarks')
remarks = remarks_tag.text.strip() if remarks_tag else ''
director = actor = area = year = desc = ''
param_box = soup.select_one('.info-parameter')
if param_box:
for li in param_box.find_all('li'):
em = li.find('em')
if not em:
continue
key = em.get_text(strip=True).rstrip('').rstrip(':')
li_clone = BeautifulSoup(str(li), 'html.parser')
for e in li_clone.find_all('em'):
e.decompose()
value = li_clone.get_text(strip=True)
if '导演' in key:
director = value
elif '主演' in key:
actor = value
elif '地区' in key:
area = value
elif '年份' in key:
year = value
elif '简介' in key:
desc = value
if not director:
d_tag = soup.find('strong', string=re.compile('导演'))
if d_tag and d_tag.find_next('a'):
director = d_tag.find_next('a').text.strip()
if not actor:
a_tag = soup.find('strong', string=re.compile('演员'))
if a_tag:
actor_links = a_tag.find_next_siblings('a')
actor = ','.join([a.text.strip() for a in actor_links]) if actor_links else ''
if not desc:
desc_div = soup.select_one('#height_limit')
desc = desc_div.text.strip() if desc_div else ''
play_from, play_url = [], []
tab_links = soup.select('.anthology-tab .swiper-slide')
list_boxes = soup.select('.anthology-list-box')
for idx, box in enumerate(list_boxes):
line_name = f"线路{idx+1}"
if idx < len(tab_links):
raw = tab_links[idx].get_text(strip=True)
badge = tab_links[idx].find('span', class_='badge')
if badge:
raw = raw.replace(badge.text, '').strip()
if raw:
line_name = raw
play_from.append(line_name)
eps = [f"{a.text.strip()}${urljoin(BASE, a['href'])}" for a in box.select('li a.this-link')]
play_url.append("#".join(eps))
vod = {
"vod_id": str(vid), "vod_name": name, "vod_pic": pic,
"vod_score": score, "vod_remarks": remarks,
"vod_year": year, "vod_area": area, "vod_actor": actor,
"vod_director": director, "vod_content": desc,
"vod_play_from": "$$$".join(play_from) if play_from else "",
"vod_play_url": "$$$".join(play_url) if play_url else "",
}
return {"list": [vod]}
def searchContent(self, key, quick, pg="1"):
# 优先使用 AJAX suggest 接口
try:
url = f"{BASE}/index.php/ajax/suggest?mid=1&wd={quote(key)}"
r = self.session.get(url, timeout=10)
data = r.json()
vod_list = []
for item in data.get("list", []):
vod_list.append({
"vod_id": str(item.get("id", "")),
"vod_name": item.get("name", ""),
"vod_pic": item.get("pic", ""),
})
if vod_list:
return {"list": vod_list, "page": int(pg)}
except Exception:
pass # 接口失败则回退到页面解析
# 回退:静态搜索页面
try:
search_url = f"{BASE}/search.html?wd={quote(key)}"
html = self._get_decoded_html(search_url)
vod_list = self._parse_vod_cards(html)
return {"list": vod_list, "page": int(pg)}
except Exception:
return {"list": [], "page": int(pg)}
def playerContent(self, flag, video_id, vipFlags):
try:
html = self._get_decoded_html(video_id)
except Exception:
return {"parse": 0, "url": ""}
m = re.search(r'var\s+player_\w+\s*=\s*({[^;]+})', html, re.DOTALL)
if not m:
direct = re.search(r'"url":"(https?://[^"]+\.(?:mp4|m3u8)[^"]*)"', html)
return {"parse": 0, "url": direct.group(1)} if direct else {"parse": 0, "url": ""}
try:
data = json.loads(m.group(1))
except json.JSONDecodeError:
raw = m.group(1)
fallback = re.search(r'"url":"(https?://[^"]+)"', raw)
return {"parse": 0, "url": fallback.group(1)} if fallback else {"parse": 0, "url": ""}
mp4_url = data.get("url", "")
if data.get("encrypt") == 1 and data.get("from") in ("xfy2", "CS"):
mp4_url = f"https://player.moedot.net/player/index.php?code=xfdm1&from=cf&url={mp4_url}"
return {"parse": 1, "url": mp4_url}
return {"parse": 0, "url": mp4_url}
def getName(self):
return "xifanacg"
def destroy(self):
if hasattr(self, "session"):
self.session.close()