快手作品批量去水印:单视频+批量下载(附源码)

Python小工具|快手作品批量去水印:单视频+批量下载(附源码)

以前发过快手的解析源码,年代久远已经时效了决定更新下,于是就抓包分析了一下快手的用户作品接口。发现快手的作品接口并没有加密参数,直接就可以获取到作品的详细信息。

于是顺便写了一个Python小工具,除了可以通过作品的视频分享链接下载视频外,还可以通过分享的用户主页链接批量下载视频作品

一、抓包分析:接口无加密

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

快手作品批量去水印:单视频+批量下载(附源码)
图1:快手作品接口抓包分析

二、跳转流程:从分享链接到播放页

直接访问App分享的作品链接,会进行302跳转。跳转到作品的一个播放页,分享链接相当于一个短链接,只做中转。

抓包后发现,访问分享链接时,响应头里面的Location参数值就是即将跳转的链接。整个过程跳转了2次才到视频播放页。

快手作品批量去水印:单视频+批量下载(附源码)
图2:从分享链接到播放页的跳转流程

第3次请求返回的是一个HTML页面,点击查看预览,就是一个播放页面。我们需要的视频、标题、封面、点赞、观看等数据都在里面。

快手作品批量去水印:单视频+批量下载(附源码)
图3:播放页中的视频数据

三、数据提取:解析 window.__APOLLO_STATE__

需要提取这些数据。把预览选项卡切换到“响应”,这个页面展示的就是播放页的HTML代码。

在代码里面搜索作品的标题,可以看到搜索结果中有类似这样的数据:

"VisionVideoDetailPhoto:3xrpm44xn3x28c2": {
                        "id": "3xrpmxxx3x28c2",
                        "duration": 213133,
                        "caption": "6月12号,周五最新前瞻 #股票知识 #财经形势解说 #散户必看 #股民交流 ",
                        "likeCount": "0",
                        "realLikeCount": 0,
                        "coverUrl": "https:u002Fu002Fp4.a.ae6a.jpg?",
                        "photoUrl": "https:u002Fu002Fk0u76y66.mp4",
                        "liked": false,
                        "timestamp": 1781186105445,
                        "expTag": "1_uu002F2009766578849529937_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字典,再一个一个提取。

快手作品批量去水印:单视频+批量下载(附源码)
图4:window.__APOLLO_STATE__中的视频数据

四、整体思路

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

五、代码实现

1. 解析ID函数

get_video_id_from_url函数会把分享的作品链接和用户主页链接,都解析成视频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
快手作品批量去水印:单视频+批量下载(附源码)
图5:从Location中提取视频ID

2. 获取视频信息函数

把作品的ID传给get_video_info函数,就可以提取出视频链接、标题、封面、点赞、观看等数据。

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))
    }
快手作品批量去水印:单视频+批量下载(附源码)
图6:提取视频详细信息

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

六、使用示例

    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 True

    def 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)
Python小工具运行效果
图7:Python运行效果


接口返回的 photoUrl 是平台存储的原始视频地址,下载下来的视频就是无水印版本。整个过程中没有对视频做任何二次处理,直接拿到的是上传时的原片。如果你批量下载的视频还有水印,那大概率是发布者上传时本身就带了水印。

下载权限
查看
  • 免费下载
    评论并刷新后下载
    登录后下载
  • {{attr.name}}:
您当前的等级为
登录后免费下载登录 小黑屋反思中,不准下载! 评论后刷新页面下载评论 支付以后下载 请先登录 您今天的下载次数(次)用完了,请明天再来 支付积分以后下载立即支付 支付以后下载立即支付 您当前的用户组不允许下载升级会员
您已获得下载权限 您可以每天下载资源次,今日剩余

给TA打赏
共{{data.count}}人
人已打赏
源码分享

「JS压缩加密逆向」实战案例 – 彩票自动投注中的GZIP压缩与Base64编码分析

2026-6-24 2:09:42

源码分享

最实用AI短剧开源软件,让小白的你都能做导演!

2026-6-26 0:53:28

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
有新私信 私信列表
搜索