当前位置:首页>python>抓包揭秘某手:视频接口无加密,Python工具轻松下载

抓包揭秘某手:视频接口无加密,Python工具轻松下载

  • 2026-10-11 07:15:01
抓包揭秘某手:视频接口无加密,Python工具轻松下载
最近在准备开发一个去水印小程序,于是抓包分析了一下快手的用户作品接口。发现快手的作品接口并没有加密参数,直接就能获取到作品的详细信息。
于是顺手写了一个Python 小工具,除了可以通过作品的视频分享链接下载视频外,还可以通过分享的用户主页链接批量下载视频作品。
👉 完整源码获取方式见文末。

01

抓包分析:接口无加密
准备开发去水印小程序时,抓包分析了快手的用户作品接口。发现一个好消息:快手的作品接口并没有加密参数,直接请求就能拿到作品信息。

02

跳转流程:从分享链接到播放页
直接访问 App 分享的作品链接,会进行 302 跳转。跳转到作品的播放页,分享链接相当于一个短链接,只做中转。
抓包后发现,访问分享链接时,响应头里面的 `Location` 参数值就是即将跳转的链接。整个过程跳转了 2 次才到视频播放页。
第 3 次请求返回的是一个 HTML 页面,点击查看预览,就是一个播放页面。我们需要的视频、标题、封面、点赞、观看等数据都在里面。

03

数据提取:解析 `window.APOLLO_STATE`
需要提取这些数据。把预览选项卡切换到“响应”,这个页面展示的就是播放页的 HTML 代码。
在代码里面搜索作品的标题,可以看到搜索结果中类似这样的数据:
"VisionVideoDetailPhoto:3xrpm44xn3x28c2": {    "id": "3xrpmxxx3x28c2",    "duration": 213133,    "caption": "6月12号,周五最新前瞻 #股票知识#财经形势解说#散户必看#股民交流 ",    "likeCount": "0",    "realLikeCount": 0,    "coverUrl": "https://p4.a.ae6a.jpg?",    "photoUrl": "https://k0u76y66.mp4",    "liked": false,    "timestamp": 1781186105445,    "expTag": "1_u/2009766578849529937_xpcwebdetailxxnull0",    "llsid": null,    "viewCount": "527",    "videoRatio": 0.5625,    "stereoType": 0,    "musicBlocked": null,    "riskTagContent": null,    "riskTagUrl": null,    "manifest": {        "type": "id",        "generated": true,        "id": "$VisionVideoDetailPhoto:3xrpm44xn3x28c2.manifest",        "typename": "VisionVideoManifest"    }}
这些数据是 JS 代码,把这个视频信息的对象赋值给了 `window.APOLLO_STATE`。
我们需要用正则先匹配下来,然后把匹配到的字符串转成 Python 里面的 JSON 字典,再一个一个提取。

04

整体思路
  • 访问作品分享链接,从响应头的 `Location` 中提取视频 ID
  • 将视频 ID 拼接到 `/short-video/` 页面
  • 访问播放页,用正则提取 `window.APOLLO_STATE`
  • 解析 JSON,获取视频链接、标题、封面、点赞、观看数据
  • 下载视频

05

代码实现
1. 解析 ID 函数
`getvideoidfromurl` 函数会把分享的作品链接和用户主页链接,都解析成视频 ID 或用户 ID。
def get_video_id_from_url(self, url):    # 如果不包含http,直接当作video_id返回    if not url.startswith(('http://', 'https://')):        return url.strip()    url = url.strip().split()[0]    headers = {        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",    }    try:        response = requests.get(url, headers=headers, allow_redirects=False)        if response.status_code == 302:            location = response.headers.get("Location")            match = re.search(r'/short-video/([a-zA-Z0-9]+)', location)            if match:                return match.group(1)            match = re.search(r'photoId=([a-zA-Z0-9]+)', location)            if match:                return match.group(1)            match = re.search(r'/user/([a-zA-Z0-9]+)', location)            if match:                return match.group(1)    except:        pass    return None
2. 获取视频信息函数
把作品的 ID 传给 `getvideoinfo` 函数,就可以提取出视频链接、标题、封面、点赞、观看等数据。
def get_video_info(self, video_id: str) -> Optional[Dict[str, Any]]:    url = f"https://www.xxxxx.com/short-video/{video_id}"    try:        response = requests.get(url, headers=self.headers, cookies=self.cookies, timeout=30)        response.raise_for_status()    except Exception as e:        print(f"请求失败: {e}")        return None    match = re.search(r'window\.__APOLLO_STATE__\s*=\s*({.*?});', response.text, re.DOTALL)    if not match:        print("解析失败")        return None    try:        apollo_state = json.loads(match.group(1))    except:        print("JSON解析失败")        return None    photo_key = f"VisionVideoDetailPhoto:{video_id}"    photo_data = apollo_state.get("defaultClient", {}).get(photo_key)    if not photo_data:        print("未找到视频数据")        return None    title = sanitize_filename(photo_data.get("caption", ""))    return {        "video_id": video_id,        "title": title,        "cover": photo_data.get("coverUrl", ""),        "video_url": photo_data.get("photoUrl", ""),        "duration": round(photo_data.get("duration", 0) / 1000, 1),        "like_count": int(photo_data.get("likeCount", 0)),        "view_count": int(photo_data.get("viewCount", 0))    }
3. 下载文件函数
通过 `download_file` 函数,把视频和封面下载到本地。
def download_file(self, url: str, save_path: str) -> bool:    """下载文件"""    try:        headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}        response = requests.get(url, headers=headers, stream=True, timeout=60)        response.raise_for_status()        with open(save_path, 'wb') as f:            for chunk in response.iter_content(chunk_size=8192):                if chunk:                    f.write(chunk)        return True    except Exception as e:        print(f"下载失败: {e}")        return False

06

使用示例
def download_single_video(self, video_id: str, base_folder: str = "downloads") -> bool:    """下载单个视频和封面到 video_id 文件夹"""    info = self.get_video_info(video_id)    if not info:        return False    # 显示视频信息    print("\n" + "=" * 60)    print(f"📹 视频信息 - {info['video_id']}")    print("=" * 60)    print(f"📝 标题: {info['title']}")    print(f"⏱️  时长: {info['duration']}秒")    print(f"❤️  点赞: {info['like_count']}")    print(f"👀 播放: {info['view_count']}")    print("=" * 60)    # 询问是否下载    download_choice = input("\n是否下载该视频?(y/n): ").strip().lower()    if download_choice != 'y':        print("⏭️ 已跳过下载")        return False    # 创建文件夹    folder = os.path.join(base_folder, f"{info['title'][:10]}")    os.makedirs(folder, exist_ok=True)    # 下载视频    video_path = os.path.join(folder, "视频.mp4")    print(f"\n📥 下载视频中...")    if not self.download_file(info['video_url'], video_path):        return False    print(f"✅ 视频下载完成")    # 下载封面    cover_path = os.path.join(folder, "封面.jpg")    print(f"📥 下载封面中...")    self.download_file(info['cover'], cover_path)    print(f"✅ 封面下载完成")    return Truedef download_user_videos(self, user_id: str, base_folder: str = "downloads") -> None:    """下载用户所有视频和封面到 user_id/作品_id 文件夹"""    result = self.get_user_video_list(user_id)    if not result or result['total'] == 0:        print("❌ 未获取到视频列表")        return    # 显示用户视频列表    print("\n" + "=" * 60)    print(f"👤 用户 {user_id} 的作品列表")    print("=" * 60)    for idx, video in enumerate(result['video_list'], 1):        print(f"{idx:2d}. {video['title'][:50]}...")        print(f"     ⏱️  {video['duration']}秒 | ❤️ {video['like_count']} | 👀 {video['view_count']}")    print("=" * 60)    # 询问是否批量下载    batch_choice = input("\n是否批量下载所有作品?(y/n): ").strip().lower()    if batch_choice != 'y':        print("⏭️ 已跳过批量下载")        return    user_folder = os.path.join(base_folder, f"用户_{result['user']}")    os.makedirs(user_folder, exist_ok=True)    success_count = 0    for idx, video in enumerate(result['video_list'], 1):        print(f"\n[{idx}/{result['total']}] 📹 处理作品: {video['video_id']}")        print(f"    📝 标题: {video['title'][:50]}...")        print(f"    ⏱️  {video['duration']}秒 | ❤️ {video['like_count']} | 👀 {video['view_count']}")        # 检查视频URL是否有效        if not video['video_url']:            print(f"    ❌ 视频URL为空,跳过")            continue        # 创建视频专属文件夹        video_folder = os.path.join(user_folder, f"{idx}_{video['title'][:10]}")        os.makedirs(video_folder, exist_ok=True)        # 下载视频        video_path = os.path.join(video_folder, "视频.mp4")        print(f"    📥 下载视频中...")        if self.download_file(video['video_url'], video_path):            print(f"    ✅ 视频下载完成")            # 下载封面(如果有)            if video['cover']:                cover_path = os.path.join(video_folder, "封面.jpg")                print(f"    📥 下载封面中...")                self.download_file(video['cover'], cover_path)                print(f"    ✅ 封面下载完成")            success_count += 1            print(f"    ✅ 作品处理完成")        else:            print(f"    ❌ 视频下载失败")    print("\n" + "=" * 60)    print(f"📊 下载统计")    print("=" * 60)    print(f"✅ 成功: {success_count} 个")    print(f"❌ 失败: {result['total'] - success_count} 个")    print(f"📁 保存路径: {user_folder}")    print("=" * 60)
接口返回的 `photoUrl` 是平台存储的原始视频地址,下载下来的视频就是无水印版本。整个过程中没有对视频做任何二次处理,直接拿到的是上传时的原片。如果你批量下载的视频还有水印,那大概率是发布者上传时本身就带了水印。

07

获取源码
👉想要获取无删减完整版源码、适配全平台的优化方案、更多实战避坑细节,可以点击文章底部【查看原文】即可查阅全部精彩内容,无压缩、无删减,可直接复制运行
🔔 更多逆向工具、零基础干货教程持续更新,欢迎关注公众号【孤狼网络科技】
温馨提示本文技术仅供学习交流,请勿用于非法用途。

最新文章

随机文章