# -*- coding: utf-8 -*- # @Author : Adapted for 華視頻 # @Time : 2025/04/05 import sys import requests from lxml import etree import re import json from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry sys.path.append('..') from base.spider import Spider class Spider(Spider): def __init__(self): self.home_url = 'https://hlove.tv' self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "Referer": "https://hlove.tv/", "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" } self.default_pic = 'https://hlove.tv/api/images/default' # 配置帶重試的會話 self.session = requests.Session() retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504]) self.session.mount('https://', HTTPAdapter(max_retries=retries)) def init(self, extend): pass def getName(self): return "華視界" def getDependence(self): return [] def isVideoFormat(self, url): pass def manualVideoCheck(self): pass def homeContent(self, filter): categories = "电影$movie#电视剧$drama#动漫$animation#综艺$variety#儿童$children" class_list = [{'type_id': v.split('$')[1], 'type_name': v.split('$')[0]} for v in categories.split('#')] # 電影篩選條件 movie_classes = "全部$all#剧情$juqing#喜剧$xiju#动作$dongzuo#惊悚$jingsong#爱情$aiqing#恐怖$kongbu#犯罪$fanzui#冒险$maoxian#奇幻$qihuan#悬疑$xuanyi#科幻$kehuan#家庭$jiating#动画$donghua#历史$lishi#战争$zhanzheng#音乐$yinle#动漫$dongman#电视电影$dianshidianying#西部$xibu#网络电影$wangluodianying#纪录$jilu#同性$tongxing#歌舞$gewu#灾难$zainan#动作冒险$dongzuomaoxian#战争政治$zhanzhengzhengzhi" movie_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others" movie_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970" # 電視劇篩選條件 drama_classes = "全部$all#国产剧$guocanju#韩剧$hanju#欧美剧$oumeiju#港台剧$gangtaiju#英剧$yingju#新马泰$xinmata#剧情$juqing#喜剧$xiju#悬疑$xuanyi#犯罪$fanzui#科幻&奇幻$kehuanqihuan#动作冒险$dongzuomaoxian#动作&冒险$dongzuojiemaoxian#家庭$jiating#战争&政治$zhanzhengzhengzhi#爱情$aiqing#肥皂剧$feizaoju#短剧$duanju#同性$tongxing#西部$xibu#儿童$ertong#真人秀$zhenrenxiu#动画$donghua#惊悚$jingsong#脱口秀$tuokouxiu#动作$dongzuo#罪案$zuian#古装$guzhuang#都市$dushi#奇幻$qihuan#科幻$kehuan#历史$lishi#青春$qinchun#新闻$xinwen#穿越$chuanyue#军旅$junlv#歌舞$gewu#玄幻$xuanhuan#纪录$jilu#言情$yanqing#警匪$jingfei#音乐剧$yinleju#商战$shangzhan#武侠$wuxia#电视电影$dianshidianying" drama_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#其他地区$others" drama_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970" # 綜藝篩選條件 variety_classes = "全部$all#真人秀$zhenrenxiu#喜剧$xiju#脱口秀$tuokouxiu#家庭$jiating#剧情$juqing#动作冒险$dongzuomaoxian#悬疑$xuanyi#动作&冒险$dongzuojiemaoxian#犯罪$fanzui#儿童$ertong#晚会$wanhui#音乐$yinle#动画$donghua#纪录$jilu#纪录片$jilupian" variety_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others" variety_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970" # 動漫篩選條件 animation_classes = "全部$all#动画$donghua#喜剧$xiju#科幻&奇幻$kehuanqihuan#动作冒险$dongzuomaoxian#动作&冒险$dongzuojiemaoxian#剧情$juqing#悬疑$xuanyi#家庭$jiating#魔幻$mohuan#热血$rexue#犯罪$fanzui#战争&政治$zhanzhengzhengzhi#冒险$maoxian#剧场版$juchangban#其它$qita#恋爱$lianai#科幻$kehuan#爆笑$baoxiao#儿童$ertong#校园$xiaoyuan#竞技$jingji#少女$shaonv#爱情$aiqing#泡面$paomian#西部$xibu#穿越$chuanyue#格斗$gedou#治愈$zhiyu#机战$jizhan#推理$tuili#耽美$danmei#肥皂剧$feizaoju" animation_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others" animation_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970" # 兒童篩選條件 children_classes = "全部$all#儿童$ertong#动画$donghua#喜剧$xiju#动作冒险$dongzuomaoxian#科幻&奇幻$kehuanqihuan#家庭$jiating#动作&冒险$dongzuojiemaoxian#剧情$juqing#悬疑$xuanyi#犯罪$fanzui#冒险$maoxian#科幻$kehuan#动作$dongzuo#动漫$dongman#历史$lishi#奇幻$qihuan" children_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others" children_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970" filters = { 'movie': [ {'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_classes.split('#')]}, {'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_areas.split('#')]}, {'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_years.split('#')]} ], 'drama': [ {'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_classes.split('#')]}, {'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_areas.split('#')]}, {'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_years.split('#')]} ], 'animation': [ {'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_classes.split('#')]}, {'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_areas.split('#')]}, {'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_years.split('#')]} ], 'variety': [ {'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_classes.split('#')]}, {'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_areas.split('#')]}, {'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_years.split('#')]} ], 'children': [ {'name': '类型', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_classes.split('#')]}, {'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_areas.split('#')]}, {'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_years.split('#')]} ] } return {'class': class_list, 'filters': filters} def homeVideoContent(self): d = [] try: # 增加超時時間並使用帶重試的會話 res = self.session.get(self.home_url, headers=self.headers, timeout=20) res.encoding = 'utf-8' html_text = res.text next_data = re.search(r'', html_text) if not next_data: print("未找到 __NEXT_DATA__") return {'list': [], 'parse': 0, 'jx': 0} next_json = json.loads(next_data.group(1)) cards = next_json['props']['pageProps'].get('cards', []) print(f"找到 {len(cards)} 個分類區塊") for section in cards: section_title = section.get('name', '未知分類') section_cards = section.get('cards', []) for card in section_cards: vod_id = card.get('id', '') vod_name = card.get('name', '') vod_pic = card.get('img', '') vod_remarks = card.get('countStr', section_title) if not vod_id or not vod_name: continue vod_path = f"/vod/detail/{vod_id}" if not vod_pic or vod_pic == '/api/images/init': vod_pic = self.default_pic d.append({ 'vod_id': vod_path, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks }) # 簡化去重,直接返回列表 print(f"最終返回 {len(d)} 個影片") return {'list': d, 'parse': 0, 'jx': 0} except Exception as e: print(f"Error in homeVideoContent: {e}") return {'list': [], 'parse': 0, 'jx': 0} def infer_category(self, section_title): category_mapping = { '電影': 'movie', '电视剧': 'drama', '动漫': 'animation', '综艺': 'variety', '儿童': 'children' } for key, value in category_mapping.items(): if key in section_title: return value return 'movie' def categoryContent(self, cid, page, filter, ext): _year = ext.get('year', 'all') _class = ext.get('class', 'all') _area = ext.get('area', 'all') url = f"{self.home_url}/{cid}/{_year}/{_class}/{_area}" if page != '1': url += f"?page={page}" d = [] try: res = self.session.get(url, headers=self.headers, timeout=20) res.encoding = 'utf-8' root = etree.HTML(res.text) data_list = root.xpath('//div[contains(@class, "h-film-listall_cardList___IXsY")]/a') next_data = re.search(r'', res.text) total = 0 init_cards = [] if next_data: next_json = json.loads(next_data.group(1)) init_cards = next_json['props']['pageProps'].get('initCard', []) total = next_json['props']['pageProps'].get('total', len(data_list)) for i, card in enumerate(data_list): vod_name = card.xpath('.//div[contains(@class, "h-film-listall_name__Gyb9x")]/text()')[0].strip() vod_id = card.get('href', '') vod_pic_list = card.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src') vod_pic = vod_pic_list[0] if vod_pic_list else None if not vod_pic or vod_pic == '/api/images/init': vod_pic = init_cards[i]['img'] if i < len(init_cards) and 'img' in init_cards[i] else self.default_pic vod_remarks = init_cards[i]['countStr'] if i < len(init_cards) and 'countStr' in init_cards[i] else '' d.append({ 'vod_id': vod_id, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks }) pagecount = (total + 23) // 24 if total > 0 else 999 return {'list': d, 'page': int(page), 'pagecount': pagecount, 'limit': 24, 'total': total} except Exception as e: print(f"Error in categoryContent: {e}") return {'list': d, 'page': int(page), 'pagecount': 999, 'limit': 24, 'total': 0} def detailContent(self, did): ids = did[0] video_list = [] if not ids.startswith('/vod/detail/'): ids = f"/vod/detail/{ids.lstrip('/')}" detail_url = f"{self.home_url}{ids}" print(f"請求的 detail_url: {detail_url}") try: res = self.session.get(detail_url, headers=self.headers, timeout=20) print(f"HTTP 狀態碼: {res.status_code}") if res.status_code != 200: print(f"頁面不存在,URL: {detail_url}") return {'list': [], 'msg': f'頁面不存在 (狀態碼: {res.status_code})'} res.encoding = 'utf-8' next_data = re.search(r'', res.text) if not next_data: print(f"未找到 __NEXT_DATA__,URL: {detail_url}, 響應片段: {res.text[:200]}") return {'list': [], 'msg': '未找到影片數據'} next_json = json.loads(next_data.group(1)) page_props = next_json.get('props', {}).get('pageProps', {}) if 'collectionInfo' not in page_props: print(f"collectionInfo 未找到,URL: {detail_url}, pageProps: {json.dumps(page_props, ensure_ascii=False)}") return {'list': [], 'msg': '影片數據缺少 collectionInfo'} collection_info = page_props['collectionInfo'] vod_name = collection_info.get('name', '') vod_year = collection_info.get('time', '') vod_area = collection_info.get('country', '') vod_content = collection_info.get('desc', '') vod_remarks = collection_info.get('countStr', '') vod_actor = ', '.join([actor['name'] for actor in collection_info.get('actor', [])]) vod_director = ', '.join([director['name'] for director in collection_info.get('director', [])]) vod_pic = collection_info.get('imgUrl', self.default_pic) is_movie = collection_info.get('isMovie', False) play_from = [] play_url = [] for group in collection_info.get('videosGroup', []): if not group.get('videos'): continue line_name = group.get('name', '线路1') if is_movie: video = group['videos'][0] play_from.append(line_name) play_url.append(f"{vod_name}${video['purl']}") else: episodes = [] for video in group['videos']: ep_name = f"第{video['eporder']}集" ep_url = video['purl'] episodes.append(f"{ep_name}${ep_url}") play_from.append(line_name) play_url.append('#'.join(episodes)) video_list.append({ 'vod_id': ids, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks, 'vod_year': vod_year, 'vod_area': vod_area, 'vod_actor': vod_actor, 'vod_director': vod_director, 'vod_content': vod_content, 'vod_play_from': '$$$'.join(play_from), 'vod_play_url': '$$$'.join(play_url) }) print(f"成功解析影片: {vod_name}, URL: {detail_url}") return {"list": video_list} except Exception as e: print(f"Error in detailContent: {str(e)}, URL: {detail_url}") return {'list': [], 'msg': f'解析錯誤: {str(e)}'} def searchContent(self, key, quick): try: search_url = f"{self.home_url}/search?q={key}" res = self.session.get(search_url, headers=self.headers, timeout=20) res.encoding = 'utf-8' root = etree.HTML(res.text) data_list = root.xpath('//div[contains(@class, "h-film-listall_cardList___IXsY")]/a') result = [] for item in data_list: vod_name = item.xpath('.//div[contains(@class, "h-film-listall_name__Gyb9x")]/text()')[0].strip() vod_id = item.get('href', '') vod_pic = item.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src')[0] if item.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src') else self.default_pic if vod_pic == '/api/images/init': vod_pic = self.default_pic result.append({ 'vod_id': vod_id, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': '' }) return {'list': result} except Exception as e: print(f"Error in searchContent: {e}") return {'list': []} def playerContent(self, flag, pid, vipFlags): try: play_url = pid headers = self.headers.copy() headers['Referer'] = 'https://hlove.tv/' # 添加 Referer 以確保播放鏈接有效 return { 'url': play_url, 'header': json.dumps(headers), 'parse': 0, 'jx': 0 } except Exception as e: print(f"Error in playerContent: {e}") return {'url': '', 'parse': 0, 'jx': 0} def generate_children_html(self, vod_id): detail = self.detailContent([vod_id]) if not detail['list']: return "

無法加載內容

" vod = detail['list'][0] vod_name = vod['vod_name'] play_from = vod['vod_play_from'].split('$$$') play_url = vod['vod_play_url'].split('$$$') lines = list(zip(play_from, play_url)) sorted_lines = sorted(lines, key=lambda x: x[0] != 'heimuer') selected_play_url = sorted_lines[0][1].split('#')[0].split('$')[1] if sorted_lines else '' html = f""" {vod_name} - 兒童播放

{vod_name}

""" return html def localProxy(self, params): pass def destroy(self): return '正在Destroy' if __name__ == "__main__": spider = Spider() # 測試主頁 result = spider.homeVideoContent() print(json.dumps(result, ensure_ascii=False, indent=2)) # 測試詳情頁 result = spider.detailContent(["/vod/detail/se4pnjL1IF6D"]) print(json.dumps(result, ensure_ascii=False, indent=2))