# coding=utf-8
# hanime1.py —— TVBox / FongMi Python 爬虫
import re
import sys
import urllib.parse
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
host = 'https://hanime1.me'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Referer': 'https://hanime1.me/',
'Accept-Language': 'zh-TW,zh;q=0.9,zh-CN;q=0.8',
}
cates = [
('最新里番', 'genre_sort:裏番|最新上市'),
('最新上市', 'sort:最新上市'),
('最新上传', 'sort:最新上傳'),
('里番', '裏番'),
('泡面番', '泡麵番'),
('Motion Anime', 'Motion Anime'),
('3DCG', '3DCG'),
('2.5D', '2.5D'),
('2D动画', '2D動畫'),
('AI生成', 'AI生成'),
('MMD', 'MMD'),
('Cosplay', 'Cosplay'),
('新番预告', '新番預告'),
]
sorts = [
{'n': '最新上市', 'v': '最新上市'},
{'n': '最新上传', 'v': '最新上傳'},
{'n': '本日排行', 'v': '本日排行'},
{'n': '本周排行', 'v': '本週排行'},
{'n': '本月排行', 'v': '本月排行'},
]
# ---------- 基础 ----------
def getName(self):
return 'hanime1'
def init(self, extend=''):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def localProxy(self, param):
return None
def _get(self, url):
try:
return self.fetch(url, headers=self.headers, timeout=15).text
except Exception:
import requests
return requests.get(url, headers=self.headers, timeout=15).text
def _meta(self, html, prop):
m = re.search(r']+property="%s"[^>]+content="([^"]*)"' % prop, html)
if not m:
m = re.search(r']+content="([^"]*)"[^>]+property="%s"' % prop, html)
return m.group(1).strip() if m else ''
# ---------- 列表解析 ----------
def parse_list(self, html):
videos = []
seen = set()
card_pattern = re.compile(
r']+href=["\'](?:https?://hanime1\.me)?/watch\?v=(\d+)["\'][^>]*>(.*?)',
re.S
)
for m in card_pattern.finditer(html):
vid = m.group(1)
if vid in seen:
continue
inner = m.group(2)
title = ''
t = re.search(
r'class="[^"]*(?:title|name)[^"]*"[^>]*>\s*([^<]+?)\s*<',
inner, re.S
)
if t:
title = t.group(1).strip()
if not title:
t = re.search(r']+alt="([^"]+)"', inner)
if t:
title = t.group(1).strip()
if not title:
t = re.search(r'title="([^"]+)"', m.group(0))
if t:
title = t.group(1).strip()
if not title:
continue
pic = ''
for img in re.findall(r'
]+(?:data-src|src)="(http[^"]+)"', inner):
if '.gif' in img or 'icon' in img or 'avatar' in img:
continue
pic = img
break
seen.add(vid)
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': ''
})
if not videos:
videos = self._parse_list_fallback(html)
return videos
def _parse_list_fallback(self, html):
videos = []
seen = set()
for m in re.finditer(
r']+href=["\'][^"\']*watch\?v=(\d+)["\']([^>]*)>(.*?)',
html, re.S
):
vid = m.group(1)
if vid in seen:
continue
attrs = m.group(2)
inner = m.group(3)
t = re.search(r'title="([^"]+)"', attrs)
title = t.group(1).strip() if t else ''
if not title:
title = re.sub(r'<[^>]+>', '', inner).strip()
if not title:
continue
seen.add(vid)
videos.append({'vod_id': vid, 'vod_name': title, 'vod_pic': '', 'vod_remarks': ''})
return videos
# ---------- 首页 ----------
def homeContent(self, filter):
classes = [{'type_name': n, 'type_id': v} for n, v in self.cates]
filters = {}
for _, v in self.cates:
if not v.startswith('sort:') and not v.startswith('genre_sort:'):
filters[v] = [{'key': 'sort', 'name': '排序', 'value': self.sorts}]
return {'class': classes, 'filters': filters}
def homeVideoContent(self):
html = self._get(self.host)
return {'list': self.parse_list(html)[:30]}
# ---------- 分类 ----------
def categoryContent(self, tid, pg, filter, extend):
if tid.startswith('genre_sort:'):
parts = tid[len('genre_sort:'):].split('|', 1)
genre = parts[0]
sort = parts[1] if len(parts) > 1 else ''
url = '{}/search?genre={}&page={}'.format(
self.host, urllib.parse.quote(genre), pg)
if sort:
url += '&sort=' + urllib.parse.quote(sort)
elif tid.startswith('sort:'):
sort_val = tid[len('sort:'):]
url = '{}/search?sort={}&page={}'.format(
self.host, urllib.parse.quote(sort_val), pg)
else:
url = '{}/search?genre={}&page={}'.format(
self.host, urllib.parse.quote(tid), pg)
if extend and extend.get('sort'):
url += '&sort=' + urllib.parse.quote(extend['sort'])
html = self._get(url)
videos = self.parse_list(html)
return {
'list': videos,
'page': int(pg),
'pagecount': int(pg) + (1 if len(videos) >= 20 else 0),
'limit': 30,
'total': 999999
}
# ---------- 详情(核心修复:把当前请求的这一集放到播放列表首位) ----------
def detailContent(self, ids):
vid = ids[0]
html = self._get('{}/watch?v={}'.format(self.host, vid))
title = self._meta(html, 'og:title')
pic = self._meta(html, 'og:image')
desc = self._meta(html, 'og:description')
episodes = self._extract_playlist(html, vid, title)
play_url = '#'.join(
'{}${}'.format(ep['name'], ep['vid'])
for ep in episodes
)
vod = {
'vod_id' : vid,
'vod_name' : title,
'vod_pic' : pic,
'vod_content' : desc,
'vod_play_from': 'Hanime1',
'vod_play_url' : play_url,
}
return {'list': [vod]}
def _extract_playlist(self, html, current_vid, current_title):
"""
提取播放列表(选集),并把当前请求的这一集放到列表最前面,
这样无论用户从外层列表点的是第几集,进详情页后默认播放的
都是用户实际点击的那一集,而不是固定播放第一集。
"""
episodes = []
seen = set()
ep_pattern = re.compile(
r'\s*'
r'