Files
tvbox_collect/jaychouqq/yingshi/py9/1.py
T
2026-07-14 00:02:39 +00:00

442 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding=utf-8
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
# 严禁将其用于任何商业用途,下载后请于24小时内删除,搜索结果均来自源站,本人不承担任何责任。
import base64
import requests
import json
import re
import sys
from urllib.parse import quote, unquote
from base.spider import Spider as BaseSpider
sys.path.append("..")
class Spider(BaseSpider):
def __init__(self):
self.name = "Gimy剧迷"
self.host = "https://gimyai.tw"
self.headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/146.0.0.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://gimyai.tw/",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
}
self.categories = [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "4", "type_name": "动漫"},
{"type_id": "29", "type_name": "综艺"},
{"type_id": "34", "type_name": "短剧"},
{"type_id": "13", "type_name": "陆剧"},
]
filter_value = [
{"n": "最新", "v": "time"},
{"n": "人气", "v": "hits"},
{"n": "评分", "v": "score"},
]
self.filters = {
key: [{"key": "by", "name": "排序", "init": "time", "value": filter_value}]
for key in ["1", "2", "4", "29", "34", "13"]
}
self.filter_def = {key: {"by": "time"} for key in self.filters}
self.t2s_map = {
"鬥": "斗",
"羅": "罗",
"陸": "陆",
"劇": "剧",
"藝": "艺",
"綜": "综",
"動": "动",
"畫": "画",
"機": "机",
"戰": "战",
"鋼": "钢",
"彈": "弹",
"雲": "云",
"狀": "状",
"態": "态",
"類": "类",
"國": "国",
"導": "导",
"簡": "简",
"線": "线",
"賊": "贼",
}
def init(self, extend=""):
return None
def getName(self):
return self.name
def homeContent(self, filter):
return {"class": self.categories, "filters": self.filters}
def _build_url(self, href):
raw = str(href or "").strip()
if not raw:
return ""
if raw.startswith(("http://", "https://")):
return raw
if raw.startswith("//"):
return "https:" + raw
return self.host + "/" + raw.lstrip("/")
def _normalize_text(self, text):
value = re.sub(r"<[^>]+>", " ", str(text or ""))
return re.sub(r"\s+", " ", value.replace("&nbsp;", " ")).strip()
def _to_display_text(self, text):
clean = self._normalize_text(text)
for trad, simp in self.t2s_map.items():
clean = clean.replace(trad, simp)
return clean
def _extract_cards(self, html):
root = self.html(html)
if root is None:
return []
items = []
seen = set()
for anchor in root.xpath("//a[contains(@href,'/detail/')]"):
href = (anchor.xpath("./@href") or [""])[0].strip()
matched = re.search(r"/detail/(\d+)\.html", href)
if not matched:
continue
vod_id = matched.group(1)
if vod_id in seen:
continue
title = (
(anchor.xpath("./@title") or [""])[0].strip()
or (anchor.xpath(".//img/@alt") or [""])[0].strip()
or "".join(anchor.xpath(".//text()")).strip()
)
pic = (
(anchor.xpath("./@data-original") or [""])[0].strip()
or (anchor.xpath("./@data-src") or [""])[0].strip()
or (anchor.xpath(".//img/@src") or [""])[0].strip()
)
parent = anchor.getparent()
remarks = ""
if parent is not None:
remarks = "".join(
parent.xpath(".//*[contains(@class,'note') or contains(@class,'text')]//text()")
).strip()
seen.add(vod_id)
items.append(
{
"vod_id": vod_id,
"vod_name": self._to_display_text(title),
"vod_pic": self._build_url(pic),
"vod_remarks": self._to_display_text(remarks),
}
)
return items
def _build_category_url(self, tid, pg, extend):
page = int(pg)
values = dict(self.filter_def.get(str(tid), {"by": "time"}))
if isinstance(extend, dict):
values.update(extend)
by = str(values.get("by", "time"))
url = f"{self.host}/genre/{tid}.html"
if page > 1 or by != "time":
url += f"?page={page}&by={by}"
return url
def _request_html(self, path_or_url, referer=None):
target = path_or_url if str(path_or_url).startswith("http") else self._build_url(path_or_url)
headers = dict(self.headers)
headers["Referer"] = referer or self.headers["Referer"]
response = self.fetch(target, headers=headers, timeout=10)
if response.status_code != 200:
return ""
return response.text or ""
def homeVideoContent(self):
html = self._request_html(self.host)
return {"list": self._extract_cards(html)[:24]}
def categoryContent(self, tid, pg, filter, extend):
url = self._build_category_url(str(tid), str(pg), extend if isinstance(extend, dict) else {})
items = self._extract_cards(self._request_html(url))
page = int(pg)
pagecount = page + 1 if len(items) >= 20 else page
return {
"list": items,
"page": page,
"pagecount": pagecount,
"limit": 20,
"total": page * 20 + len(items),
}
def _pick_info(self, html, label):
matched = re.search(rf"<span[^>]*>\s*{re.escape(label)}[:]\s*</span>\s*(.*?)(?:</div>|</li>)", str(html or ""), re.S)
return self._to_display_text(matched.group(1) if matched else "")
def _parse_play_sources(self, html):
root = self.html(html)
if root is None:
return []
groups = []
sources = root.xpath("//span[contains(@class,'source') and @data-route-switch]")
if sources:
for src in sources:
sid = (src.xpath("./@data-route-switch") or [""])[0].strip()
name = self._to_display_text("".join(src.xpath(".//text()")))
if not sid:
continue
episodes = []
seen_eps = set()
for ep_anchor in root.xpath(f"//div[contains(@class,'episodes-route') and @data-route-sid='{sid}']//a[contains(@href,'/play/')]"):
href = (ep_anchor.xpath("./@href") or [""])[0].strip()
matched = re.search(r"/play/(\d+-\d+-\d+)\.html", href)
if not matched:
continue
ep_id = matched.group(1)
if ep_id in seen_eps:
continue
seen_eps.add(ep_id)
title = self._to_display_text("".join(ep_anchor.xpath(".//text()"))) or "正片"
episodes.append(f"{title}${ep_id}")
if episodes:
groups.append({"from": name or f"线路{sid}", "urls": "#".join(episodes)})
if not groups:
tabs = []
for tab in root.xpath("//*[@id='playTab']//a[starts-with(@href,'#con_playlist_')]"):
tabs.append(
{
"id": (tab.xpath("./@href") or [""])[0].replace("#", ""),
"name": self._to_display_text("".join(tab.xpath(".//text()"))),
}
)
for tab in tabs:
episodes = []
seen_eps = set()
for anchor in root.xpath(f"//*[@id='{tab['id']}']//a[contains(@href,'/play/')]"):
href = (anchor.xpath("./@href") or [""])[0].strip()
matched = re.search(r"/play/(\d+-\d+-\d+)\.html", href)
if not matched:
continue
ep_id = matched.group(1)
if ep_id in seen_eps:
continue
seen_eps.add(ep_id)
title = self._to_display_text("".join(anchor.xpath(".//text()"))) or "正片"
episodes.append(f"{title}${ep_id}")
if episodes:
groups.append({"from": tab["name"], "urls": "#".join(episodes)})
if not groups:
episodes = []
seen_urls = set()
for anchor in root.xpath("//a[contains(@href,'/play/')]"):
href = (anchor.xpath("./@href") or [""])[0].strip()
matched = re.search(r"/play/(\d+-\d+-\d+)\.html", href)
if not matched:
continue
vid = matched.group(1)
if vid in seen_urls:
continue
seen_urls.add(vid)
title = self._to_display_text("".join(anchor.xpath(".//text()"))) or "正片"
episodes.append(f"{title}${vid}")
if episodes:
groups.append({"from": "高清线路", "urls": "#".join(episodes)})
return groups
def _extract_year_from_category(self, html):
matched = re.search(r'<span[^>]*>\s*類別[:]\s*</span>\s*<b>[^·]*·\s*(\d{4})\s*</b>', str(html or ""), re.S)
return matched.group(1) if matched else ""
def _parse_detail_page(self, html, vod_id):
root = self.html(html)
if root is None:
return {"list": []}
title = "".join(
root.xpath("//h1[contains(@class,'text-overflow')][1]//text()") or root.xpath("//h1[1]//text()")
).strip()
poster = ((root.xpath("//meta[@property='og:image']/@content") or [""])[0]).strip()
content = "".join(
root.xpath("//div[contains(@class,'desc')]//p//text()")
).strip()
actors = []
for div in root.xpath("//div[contains(@class,'detail__meta')]//div[contains(.,'主演')]//a"):
text = "".join(div.xpath(".//text()")).strip()
if text:
actors.append(self._to_display_text(text))
directors = []
for div in root.xpath("//div[contains(@class,'detail__meta')]//div[contains(.,'導演') or contains(.,'导演')]"):
for a in div.xpath(".//a"):
text = "".join(a.xpath(".//text()")).strip()
if text:
directors.append(self._to_display_text(text))
break
play_groups = self._parse_play_sources(html)
return {
"list": [
{
"vod_id": str(vod_id),
"vod_name": self._to_display_text(title),
"vod_pic": self._build_url(poster),
"vod_content": self._to_display_text(content),
"vod_remarks": self._pick_info(html, "狀態") or self._pick_info(html, "状态"),
"type_name": self._pick_info(html, "類別") or self._pick_info(html, "类别"),
"vod_year": self._pick_info(html, "年代") or self._pick_info(html, "年份") or self._extract_year_from_category(html),
"vod_area": self._pick_info(html, "國家/地區") or self._pick_info(html, "国家/地区"),
"vod_actor": ",".join([item for item in actors if item]),
"vod_director": ",".join([item for item in directors if item]),
"vod_play_from": "$$$".join([item["from"] for item in play_groups]),
"vod_play_url": "$$$".join([item["urls"] for item in play_groups]),
}
]
}
def detailContent(self, ids):
raw = ids[0]
url = raw if str(raw).startswith("http") else f"{self.host}/detail/{raw}.html"
html = self._request_html(url)
matched = re.search(r"/detail/(\d+)\.html", url)
vod_id = matched.group(1) if matched else str(raw)
return self._parse_detail_page(html, vod_id)
def _normalize_search_keyword(self, text):
value = self._to_display_text(text).lower()
return re.sub(r"[\s\-_—–·•:,.。!?!?'""“”‘’()()\[\]【】{}]", "", value)
def _build_search_tokens(self, keyword):
base = self._normalize_search_keyword(keyword)
if not base:
return []
tokens = [base]
simplified = re.sub(
r"线上看|線上看|全集|連續劇|连续剧|電視劇|电视剧|動漫|动漫|電影|电影|綜藝|综艺",
"",
base,
).strip()
if simplified and simplified not in tokens:
tokens.append(simplified)
stripped = re.sub(r"第\d+[集季部篇]?$", "", simplified)
stripped = re.sub(r"\d+$", "", stripped)
if stripped and stripped not in tokens:
tokens.append(stripped)
return sorted([item for item in tokens if item], key=len, reverse=True)
def _score_search_result(self, vod_name, keyword):
name = self._normalize_search_keyword(vod_name)
best = 0
for token in self._build_search_tokens(keyword):
if name == token:
best = max(best, 1000 + len(token))
elif name.startswith(token):
best = max(best, 800 + len(token))
elif token in name:
best = max(best, 600 + len(token))
elif len(name) >= 2 and name in token:
best = max(best, 450 + len(name))
return best
def _refine_search_results(self, items, keyword):
scored = []
for item in items:
score = self._score_search_result(item.get("vod_name", ""), keyword)
if score > 0:
scored.append((score, item))
if scored:
scored.sort(key=lambda pair: (-pair[0], pair[1].get("vod_name", "")))
return [item for _, item in scored]
loose_key = self._normalize_search_keyword(keyword)
return [
item
for item in items
if loose_key and loose_key in self._normalize_search_keyword(item.get("vod_name", ""))
]
def searchContent(self, key, quick, pg="1"):
url = f"{self.host}/find/-------------.html?wd={quote(str(key))}&page={pg}"
html = self._request_html(url)
raw_items = self._extract_cards(html)
items = self._refine_search_results(raw_items, key)
page = int(pg)
pagecount = page + 1 if len(raw_items) >= 20 else page
return {
"list": items,
"page": page,
"pagecount": pagecount,
"limit": len(items),
"total": len(items),
}
def _extract_player_data(self, html):
matched = re.search(r"var\s+player_data\s*=\s*(\{[\s\S]*?\})\s*;?\s*</script>", str(html or ""), re.S)
if not matched:
return {}
try:
return json.loads(matched.group(1))
except Exception:
return {}
def _decode_play_url(self, player_data):
raw = str(player_data.get("url", "")).strip()
encrypt = str(player_data.get("encrypt", "0")).strip()
if not raw:
return ""
if encrypt == "1":
return unquote(raw)
if encrypt == "2":
try:
decoded = base64.b64decode(raw).decode("utf-8")
return unquote(decoded)
except Exception:
return ""
return raw
def _is_direct_media_url(self, url):
return bool(re.match(r"^https?://.*\.(m3u8|mp4|flv|m4s)(\?.*)?$", str(url or ""), re.I))
def _build_parse_context(self, raw_url, play_from, play_page_url):
base = "https://play.gimyai.tw/v/"
referer = f"https://play.gimyai.tw/v/?url={quote(raw_url)}"
if play_from in ["JD4K", "JD2K", "JDHG", "JDQM"]:
base = "https://play.gimyai.tw/d/"
referer = (
f"https://play.gimyai.tw/d/?url={quote(raw_url)}"
f"&jctype={play_from}&next={quote(play_page_url)}"
)
elif play_from == "NSYS":
base = "https://play.gimyai.tw/n/"
referer = (
f"https://play.gimyai.tw/n/?url={quote(raw_url)}"
f"&jctype={play_from}&next={quote(play_page_url)}"
)
return {
"parse_url": f"{base}parse.php?url={quote(raw_url)}&_t=1",
"referer": referer,
"origin": "https://play.gimyai.tw",
}
def playerContent(self, flag, id, vipFlags):
play_id = str(id)
play_page_url = play_id if play_id.startswith("http") else f"{self.host}/play/{play_id}.html"
page_headers = {
"User-Agent": self.headers["User-Agent"],
"Referer": play_page_url,
"Origin": self.host,
}
html = self._request_html(play_page_url, referer=play_page_url)
player_data = self._extract_player_data(html)
raw_url = self._decode_play_url(player_data)
if self._is_direct_media_url(raw_url):
return {
"parse": 0,
"jx": 0,
"url": raw_url,
"header": {"User-Agent": self.headers["User-Agent"], "Referer": play_page_url},
}
if raw_url:
context = self._build_parse_context(raw_url, str(player_data.get("from", "")).strip(), play_page_url)
parse_text = self._request_html(context["parse_url"], referer=context["referer"])
try:
parse_json = json.loads(parse_text or "{}")
except Exception:
parse_json = {}
media_url = str(
parse_json.get("url") or parse_json.get("video") or parse_json.get("playurl") or ""
).strip()
if media_url:
return {
"parse": 0,
"jx": 0,
"url": media_url,
"header": {
"User-Agent": self.headers["User-Agent"],
"Referer": context["referer"],
"Origin": context["origin"],
},
}
if raw_url.startswith("http"):
return {
"parse": 1,
"jx": 1,
"url": raw_url,
"header": {
"User-Agent": self.headers["User-Agent"],
"Referer": context["referer"],
},
}
return {"parse": 1, "jx": 1, "url": play_page_url, "header": page_headers}