使用python实现抖音视频去水印功能

抖音上下载的视频通常都会加上作者账号水印,或者有些视频直接不开放下载。如果需要下载无水印的视频该怎么如何操作呢?
其实思路很简单,我们只需要找到原始视频地址就可以了。
通常我们在我们抖音视频中选择复制链接,会得到一个类似下图所示的链接。

访问这个地址会发现被302重定向到一个类似这样的url

https://www.iesdouyin.com/share/video/6901990942908124431/?region=CN&mid=6798390707091573516&u_code=i3g58h16&titleType=title&did=MS4wLjABAAAAuaorE6aYRkmwcoa3QreMSLRIA9sWQDpEQPRByLXSoyA&iid=MS4wLjABAAAADuPZL4CgoFrcLpcrtq1sf89qhSbZG7CCSGyKGbePPPWTIdVx6dS252KnzTNkQP_Q&with_sec_did=1&utm_source=copy_link&utm_campaign=client_share&utm_medium=android&app=aweme&scheme_type=1

打开chrome控制台,发现页面发起了这样一个请求

https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=6901990942908124431

这个请求返回了一个json串,里面包含了我们需要的原始视频地址,音频地址等等各种信息

// 20210905191510
// https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=6901990942908124431

{
  "item_list": [
    {
      "share_url": "https://www.iesdouyin.com/share/video/6901990942908124431/?region=&mid=6798390707091573516&u_code=48&titleType=title&did=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1",
      "text_extra": [
        {
          "end": 15,
          "type": 1,
          "hashtag_name": "旅行大玩家",
          "hashtag_id": 1608054515739651,
          "start": 9
        }
      ],
      "image_infos": null,
      "is_preview": 0,
      "group_id": 6901990942908124000,
      "is_live_replay": false,
      "desc": "我要路过你了。  #旅行大玩家",
      "author": {
        "avatar_larger": {
          "uri": "1080x1080/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786",
          "url_list": [
            "https://p6.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_1080x1080.jpeg?from=116350172",
            "https://p11.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_1080x1080.jpeg?from=116350172",
            "https://p9.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_1080x1080.jpeg?from=116350172"
          ]
        },
        "avatar_thumb": {
          "uri": "100x100/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786",
          "url_list": [
            "https://p11.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_100x100.jpeg?from=116350172",
            "https://p29.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_100x100.jpeg?from=116350172",
            "https://p3.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_100x100.jpeg?from=116350172"
          ]
        },
        "avatar_medium": {
          "uri": "720x720/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786",
          "url_list": [
            "https://p26.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_720x720.jpeg?from=116350172",
            "https://p9.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_720x720.jpeg?from=116350172",
            "https://p11.douyinpic.com/img/tos-cn-avt-0015/01e9fdc06ca5b20e0083ea392f6fe786~c5_720x720.jpeg?from=116350172"
          ]
        },
        "unique_id": "",
        "platform_sync_info": null,
        "short_id": "2219249505",
        "nickname": "一朵小森林",
        "signature": "摄影师,治愈系旅行博主。\n接写真,情侣,旅拍跟拍。短视频出售,定制。\n📩yanglinpt",
        "geofencing": null,
        "policy_version": null,
        "type_label": null,
        "uid": "4419667439400716",
        "followers_detail": null
      },
      "forward_id": "0",
      "promotions": null,
      "long_video": null,
      "duration": 10332,
      "geofencing": null,
      "statistics": {
        "comment_count": 131140,
        "digg_count": 2393061,
        "play_count": 0,
        "share_count": 135917,
        "aweme_id": "6901990942908124431"
      },
      "share_info": {
        "share_weibo_desc": "#在抖音,记录美好生活#我要路过你了。  #旅行大玩家",
        "share_desc": "在抖音,记录美好生活",
        "share_title": "我要路过你了。  #旅行大玩家"
      },
      "video_text": null,
      "aweme_id": "6901990942908124431",
      "cha_list": [
        {
          "view_count": 0,
          "cid": "1608054515739651",
          "cha_name": "旅行大玩家",
          "desc": "带活动话题投稿,价值万元dou+、上亿流量等你瓜分,快和抖音旅行一起暑期神游吧!活动规则点击:https://aweme.snssdk.com/magic/eco/runtime/release/60fe2fc950440c032a3bbd7e?appType=douyin&magic_source=search",
          "type": 2,
          "is_commerce": false,
          "user_count": 0,
          "connect_music": null,
          "cover_item": {
            "uri": "douyin-admin-obj/180c18f991d04a59c7b7baafcc6e8c57",
            "url_list": [
              "https://p3-dy.bytecdn.cn/live/100x100/douyin-admin-obj/180c18f991d04a59c7b7baafcc6e8c57.jpg",
              "https://p9-dy.bytecdn.cn/live/100x100/douyin-admin-obj/180c18f991d04a59c7b7baafcc6e8c57.jpg",
              "https://p9-dy.bytecdn.cn/live/100x100/douyin-admin-obj/180c18f991d04a59c7b7baafcc6e8c57.jpg"
            ]
          },
          "hash_tag_profile": "douyin-admin-obj/180c18f991d04a59c7b7baafcc6e8c57"
        }
      ],
      "author_user_id": 4419667439400716,
      "label_top_text": null,
      "images": null,
      "music": {
        "id": 6798390707091574000,
        "mid": "6798390707091573516",
        "author": "温柔的岁月",
        "cover_large": {
          "uri": "1080x1080/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6",
          "url_list": [
            "https://p26.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172",
            "https://p29.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172",
            "https://p6.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172"
          ]
        },
        "cover_medium": {
          "uri": "720x720/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6",
          "url_list": [
            "https://p3.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_720x720.jpeg?from=116350172",
            "https://p11.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_720x720.jpeg?from=116350172",
            "https://p26.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_720x720.jpeg?from=116350172"
          ]
        },
        "play_url": {
          "uri": "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/1659763355067443.mp3",
          "url_list": [
            "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/1659763355067443.mp3",
            "https://sf6-cdn-tos.douyinstatic.com/obj/ies-music/1659763355067443.mp3"
          ]
        },
        "duration": 10,
        "title": "@温柔的岁月创作的原声",
        "cover_hd": {
          "uri": "1080x1080/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6",
          "url_list": [
            "https://p26.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172",
            "https://p29.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172",
            "https://p6.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_1080x1080.jpeg?from=116350172"
          ]
        },
        "cover_thumb": {
          "uri": "168x168/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6",
          "url_list": [
            "https://p3.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_168x168.jpeg?from=116350172",
            "https://p26.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_168x168.jpeg?from=116350172",
            "https://p11.douyinpic.com/img/tos-cn-avt-0015/5c58a7121b884d996c5b677c82766dc6~c5_168x168.jpeg?from=116350172"
          ]
        },
        "position": null,
        "status": 1
      },
      "comment_list": null,
      "video_labels": null,
      "create_time": 1606994997,
      "video": {
        "width": 1280,
        "dynamic_cover": {
          "uri": "tos-cn-p-0015/ea1d7868cf7a496ebbbc3f67a9acb400_1606994998",
          "url_list": [
            "https://p3.douyinpic.com/obj/tos-cn-p-0015/ea1d7868cf7a496ebbbc3f67a9acb400_1606994998?from=4257465056_large",
            "https://p29.douyinpic.com/obj/tos-cn-p-0015/ea1d7868cf7a496ebbbc3f67a9acb400_1606994998?from=4257465056_large",
            "https://p26.douyinpic.com/obj/tos-cn-p-0015/ea1d7868cf7a496ebbbc3f67a9acb400_1606994998?from=4257465056_large"
          ]
        },
        "origin_cover": {
          "uri": "tos-cn-p-0015/06e9dcdb0ede4a19aaddabd983df72f7_1606994998",
          "url_list": [
            "https://p9.douyinpic.com/tos-cn-p-0015/06e9dcdb0ede4a19aaddabd983df72f7_1606994998~tplv-dy-360p.jpeg?from=4257465056&s=&se=false&sh=&sc=&l=202109051915100102121481502C609810&biz_tag=feed_cover",
            "https://p29.douyinpic.com/tos-cn-p-0015/06e9dcdb0ede4a19aaddabd983df72f7_1606994998~tplv-dy-360p.jpeg?from=4257465056&s=&se=false&sh=&sc=&l=202109051915100102121481502C609810&biz_tag=feed_cover",
            "https://p3.douyinpic.com/tos-cn-p-0015/06e9dcdb0ede4a19aaddabd983df72f7_1606994998~tplv-dy-360p.jpeg?from=4257465056&s=&se=false&sh=&sc=&l=202109051915100102121481502C609810&biz_tag=feed_cover"
          ]
        },
        "has_watermark": true,
        "bit_rate": null,
        "duration": 10332,
        "play_addr": {
          "uri": "v0d00f730000bv4coanfom5kn499oqbg",
          "url_list": [
            "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00f730000bv4coanfom5kn499oqbg&ratio=720p&line=0"
          ]
        },
        "cover": {
          "uri": "tos-cn-p-0015/cd2c543889fe456ca390864df50234bf",
          "url_list": [
            "https://p26.douyinpic.com/img/tos-cn-p-0015/cd2c543889fe456ca390864df50234bf~c5_300x400.jpeg?from=4257465056_large",
            "https://p9.douyinpic.com/img/tos-cn-p-0015/cd2c543889fe456ca390864df50234bf~c5_300x400.jpeg?from=4257465056_large",
            "https://p3.douyinpic.com/img/tos-cn-p-0015/cd2c543889fe456ca390864df50234bf~c5_300x400.jpeg?from=4257465056_large"
          ]
        },
        "height": 720,
        "ratio": "720p",
        "vid": "v0d00f730000bv4coanfom5kn499oqbg"
      },
      "risk_infos": {
        "warn": false,
        "type": 0,
        "content": ""
      },
      "aweme_type": 4
    }
  ],
  "extra": {
    "now": 1630840510000,
    "logid": "202109051915100102121481502C609810"
  },
  "status_code": 0
}


在响应结果中找到video字段,有一个play_addr,看名字应该就是视频地址了,把这个url拷贝到浏览器中试一下,发现可以正常播放,但是仍然有水印

https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00f730000bv4coanfom5kn499oqbg&ratio=720p&line=0

目前看来这条路好像被堵死了,但是我们仔细分析一下这个url,会发现其中playwm后面的wm像是一个单词的缩写,没错就是水印watermark的缩写。我们修改一下这个url,将playwm改为play,尝试一下能否播放,结果神奇的事情发生了,视频播放成功,而且视频中没有水印。
所以我们往回推,只要调用iteminfo这个接口,就能获得我们想要的原始视频地址,这个接口只有一个参数item_ids。这个参数的值该如何获取呢?还记得最前面吗,我们访问抖音的分享链接时,302重定向到了一个url,这个url中就包含了这个id的值。
至此,我们完整的思路有了,第一步访问分享的原始链接,取得302重定向后的url,从url中截取id,将id传入刚才的iteminfo接口,从接口的响应结果中解析出视频地址。将视频地址中的playwm替换成play就大功告成了。
完整代码的后端代码如下

import json
import requests
from flask import Flask,render_template,jsonify,request
from uuid import uuid4
from flask_cors import CORS

app= Flask(__name__)
CORS(app)
@app.route("/",methods=["GET","POST"])
def homepage():
    return render_template("dyqt.html")
    #定义前端页面
@app.route('/douyin',methods=['GET','POST'])
def douyin():
    url =request.form.get("oldurl")
    ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
    headers = {"User-Agent":ua}
    r = requests.get(url,headers=headers).url
    # 获取重定向后的url
    midurl ="https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids="+r[38:57]
    #使用切片截取id后传递iteminfo接口
    nr = requests.get(midurl,headers=headers)
    res = nr.json()
    #获取接口返回的内容,接口返回的是json格式的数据,先将其反序列化为python字典
    urllist =res["item_list"][0]["video"]["play_addr"]["url_list"][0]
    #获取视频地址
    desc = res["item_list"][0]["desc"]
    musicUrl = res["item_list"][0]["music"]["play_url"]["uri"]
    newurl = urllist.replace("wm","")
    #替换url中"playwm"为"play"
    reurl = requests.get(newurl,headers=headers).url
    #发起请求得到跳转后的视频地址
    return jsonify({"videoUrl":reurl,"desc":desc,"musicUrl":musicUrl})
    #将结果封装成json格式返回


if __name__=='__main__':
    app.run(host='0.0.0.0',port = 5000,debug=True)

简单解释一下这段代码,我们用到了requests模块,使用它来模拟发送请求,需要注意的是,需要加上手机UA,因为PC端的请求逻辑稍有区别,我们前面这段分析都是建立在手机访问的情况下。
然后用到了flask来做server,因为我们需要把原始的分享链接提交过去(注意 ,原始链接是类似这段 https://v.douyin.com/djnUPYe/) 。
前端我不太会弄,就简单做了一个输入框和一个提交按钮,通过ajax将url传递给服务端
qyqt.html关键代码如下:

<html>
    <head>
        <meta name="referrer" content="no-referrer"/>
    <script src="{{url_for('static',filename='jquery.min.js')}}"></script>  
    </head>
<body>
<form  method="POST">
    <p>
        <h1>输入分享链接:</h1>
    </p>
    <p><input type="text" name="oldurl" id="oldurl"/></p>
    <p><input type="button" value="提交" name="Submit" id="Submit"/></p>
    <p id="newurl" name="newurl"></p>
</form>

<script>    
    $(function(){
$("#Submit").click(function()
{
    $.ajax({
    url:'/douyin',
    async:false,
    type:'POST',
    data:{oldurl:$("#oldurl").val()},
    dataType:'json',
    success: function(data,status){
       // alert(1);
       var nurl =data.videoUrl;
        $("#newurl").text(nurl)                   
        },
    error: function(e){
    console.log("ajax请求失败");
    }
})



})
    })

</script>
</body>
</html>

顺便提一下另一种思路,使用selenium无头模式请求url,通过xpath获取视频的播放地址。
目前的xpath路径为

//*[@id="root"]/div/div[2]/div[1]/div[1]/div[1]/div[1]/div/div[1]/div[2]/div/video

不过xpath存在不稳定的情况,如果页面布局有变动就会失效,所以还是推荐使用第一种方式,使用接口来获取.