链路拆解
好看视频这条链路比较适合写成逆向笔记,因为它没有一上来就进入复杂浏览器环境,而是可以拆成几步:
- 搜索接口:
https://haokan.baidu.com/haokan/ui-search/pc/search/video - 详情页:
https://haokan.baidu.com/v?vid=<vid> - 页面里提取
encrptedVideoMeta - Base64 解码后用
guanghui456做 XOR 解密 - JSON 里读取
clarityUrl,按rank选择最高画质
这里最关键的是第四步。它不是标准 AES 这类加密,而是简单的 Base64 + XOR。代码一旦拆开,其实很直观。
搜索接口签名
搜索接口的 sign 由页码、关键词、分页数量、毫秒时间戳和固定版本位拼出来,再做 MD5:
from hashlib import md5
import time
import urllib.parse
def build_search_params(keyword: str, page: int = 1, page_size: int = 20) -> dict:
timestamp = int(time.time() * 1000)
quoted = urllib.parse.quote(keyword)
sign = md5(f"{page}_{quoted}_{page_size}_{timestamp}_1".encode()).hexdigest()
return {
"pn": page,
"rn": str(page_size),
"type": "video",
"query": keyword,
"sign": sign,
"version": "1",
"timestamp": str(timestamp),
}
这类签名不是为了安全通信,更像是接口请求完整性校验。逆向时重点是找到拼接顺序、编码方式和时间戳粒度。
可运行解析代码
下面这份是整理后的可运行版本。它保留真实接口、真实字段和真实解密 key。个人 cookie 不写死,确实需要时通过环境变量 HAOKAN_COOKIE 注入。
import base64
import json
import os
import re
import urllib.parse
from dataclasses import dataclass
from hashlib import md5
import time
import requests
DOMAIN_URL = "https://haokan.baidu.com"
SEARCH_URL = "https://haokan.baidu.com/haokan/ui-search/pc/search/video"
DECRYPT_KEY = "guanghui456"
HEADERS = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"referer": "https://haokan.baidu.com/",
"user-agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
),
}
if os.getenv("HAOKAN_COOKIE"):
HEADERS["cookie"] = os.environ["HAOKAN_COOKIE"]
@dataclass
class HaokanVideo:
vid: str
title: str
cover: str
author: str
duration: str
video_url: str
quality: str
def build_search_params(keyword: str, page: int = 1, page_size: int = 20) -> dict:
timestamp = int(time.time() * 1000)
sign = md5(f"{page}_{urllib.parse.quote(keyword)}_{page_size}_{timestamp}_1".encode()).hexdigest()
return {
"pn": page,
"rn": str(page_size),
"type": "video",
"query": keyword,
"sign": sign,
"version": "1",
"timestamp": str(timestamp),
}
def search_haokan(keyword: str, page: int = 1, page_size: int = 20) -> list[dict]:
response = requests.get(
SEARCH_URL,
params=build_search_params(keyword, page, page_size),
headers=HEADERS,
timeout=15,
)
response.raise_for_status()
data = response.json()
return data.get("data", {}).get("list", [])
def extract_haokan_vid(url_or_vid: str) -> str:
value = (url_or_vid or "").strip()
if re.fullmatch(r"\d{6,}", value):
return value
parsed = urllib.parse.urlparse(value)
query = urllib.parse.parse_qs(parsed.query)
for key in ("vid", "pd", "id"):
item = query.get(key)
if item and re.fullmatch(r"\d{6,}", item[0]):
return item[0]
match = re.search(r"(?:vid=|/v/|video/|haokan_)(\d{6,})", value)
if match:
return match.group(1)
raise ValueError("未能从链接中提取好看视频 vid")
def decrypt_video_meta(encrypted_meta: str) -> dict:
decoded = base64.b64decode(encrypted_meta).decode("utf-8")
chars = []
for index, char in enumerate(decoded):
chars.append(chr(ord(char) ^ ord(DECRYPT_KEY[index % len(DECRYPT_KEY)])))
return json.loads("".join(chars))
def fetch_detail(vid: str) -> dict:
response = requests.get(
f"{DOMAIN_URL}/v",
params={"vid": vid},
headers=HEADERS,
timeout=20,
)
response.raise_for_status()
response.encoding = "utf-8"
found = re.findall(r'"encrptedVideoMeta":"(.*?)",', response.text)
if not found:
raise RuntimeError("详情页未找到 encrptedVideoMeta")
return decrypt_video_meta(found[0])
def pick_best_video_url(detail: dict) -> dict:
candidates = [
item for item in detail.get("clarityUrl", [])
if isinstance(item, dict) and item.get("url")
]
if not candidates:
raise RuntimeError("clarityUrl 里没有可用视频地址")
return sorted(candidates, key=lambda item: item.get("rank", 0), reverse=True)[0]
def parse_haokan(url_or_vid: str) -> HaokanVideo:
vid = extract_haokan_vid(url_or_vid)
detail = fetch_detail(vid)
best = pick_best_video_url(detail)
author = detail.get("author") if isinstance(detail.get("author"), dict) else {}
return HaokanVideo(
vid=vid,
title=detail.get("title", ""),
cover=detail.get("cover", ""),
author=author.get("name", "好看视频用户"),
duration=str(detail.get("duration", "")),
video_url=best["url"],
quality=best.get("key") or best.get("name") or best.get("definition") or "最高画质",
)
if __name__ == "__main__":
# 搜索示例
for item in search_haokan("麦小兜", page=1, page_size=5):
print(item.get("title"), item.get("vid"))
# 详情解析示例:传 vid 或包含 vid 的 URL 都可以
result = parse_haokan("123456789")
print(result)
复盘
好看视频这条链路的亮点不在请求头伪装,而在页面内加密字段的处理。encrptedVideoMeta 解开之后,数据结构很完整,包含标题、封面、作者、时长和多清晰度地址。
我会把这类解析器做成两层:搜索只返回列表和 vid,详情解析再按需触发。这样界面第一次加载更快,也能避免每一页搜索都去解析所有视频直链。