抖音 APP 中保存到本地就是无水印版本的,所以头条的服务器肯定是保存有无水印版本的抖音视频的,所以只要找到接口地址就可以搞定。先在网上搜罗了一圈,确实有人已经做了解析,还提供了收费解析服务

搜索之后发现又发现了同类型的其他

  • http://douyin.iiilab.com/
  • http://www.dyapp.cc/
  • https://app886.cn/douyin_video

分析接口

分析这几个页面就会发现他们通过分享的链接就能够拿到视频的直接链接肯定是通过接口获取的,所以通过 Android 抓包能找到类似下面的接口

https://aweme.snssdk.com/aweme/v1/play/?
video_id=v0200fd80000bejku38697aj8tin1brg
&line=0
&ratio=720p
&watermark=1
&media_type=4
&vr_type=0
&test_cdn=None
&improve_bitrate=0
&version_code=270

这个请求返回状态码是 302,返回的 response header 中 Location 标示了该视频的真实地址。

后来分析抖音的 Feed 流设计,又发现了另外一个接口

https://api.amemv.com/aweme/v1/play/?video_id=v0200fd80000bejhsoir863vmi5add60&line=0&ratio=720p&media_type=4&vr_type=0&test_cdn=None&improve_bitrate=0

也类似于上面的接口不过域名不同,参数也类似。

入手

入手的几个页面,分享页

https://www.iesdouyin.com/share/video/6604280853952466190/?region=CN&mid=6601014204340275975&u_code=df31b2ff&titleType=title
http://v.douyin.com/dqv3dv/

第二条是短链接,展开就是上面的长链接了。

在分享页面获取视频的 id,然后使用上面的链接获取视频的播放地址。

大致的思路如下

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
from urllib.request import urlopen

import requests
from tqdm import tqdm


class DouyinVideoInfo:
    def __init__(self, vid, title='', nickname=''):
        self.video_id = vid
        self.title = title
        self.nickname = nickname


def download(url, filename, name):
    """
    :param url: 视频直接链接
    :param filename: 保存文件名
    :param name: 进度条内容
    :return:
    """
    file_size = int(urlopen(url).info().get('Content-Length', -1))

    if os.path.exists(filename):
        first_byte = os.path.getsize(filename)
    else:
        first_byte = 0
    if first_byte >= file_size:
        return file_size
    header = {"Range": "bytes=%s-%s" % (first_byte, file_size),
              'User-Agent': 'okhttp/3.10.0.1'}
    pbar = tqdm(total=file_size, initial=first_byte,
                unit='B', unit_scale=True,
                desc=name)
    req = requests.get(url, headers=header, stream=True)
    with(open(filename, 'wb')) as f:
        for chunk in req.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)
                pbar.update(1024)
    pbar.close()
    return file_size


def down_by_vid(info: DouyinVideoInfo):
    r = requests.get('https://api.amemv.com/aweme/v1/play/', params={
        'video_id': info.video_id,
        'line': 0,
        'ratio': '720p',
        'watermark': 0,
        'media_type': 4,
        'vr_type': 0,
        'test_cdn': 'None',
        'improve_bitrate': 0,
        'logo_name': 'aweme'
    }, headers={
        'Host': 'api.amemv.com',
        'sdk-version': '1',
        'X-SS-TC': '0'
    }, allow_redirects=False)
    print(r.headers['Location'])
    download(r.headers['Location'], '{}.mp4'.format(info.video_id), info.title)


def get_video_info(long_url) -> DouyinVideoInfo:
    Headers = {
        'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'max-age=0',
        'upgrade-insecure-requests': '1',
        'user-agent': 'Mozilla/5.0 (Linux; U; Android 5.1.1; zh-cn; MI 4S Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.146 Mobile Safari/537.36 XiaoMi/MiuiBrowser/9.1.3',
    }
    r = requests.get(long_url, headers=Headers)
    video_id = re.findall("(?<=video_id=).+?(?=&amp)", r.text)[0]
    title_name = re.findall("(?<=desc\">).+?(?=</p>)", r.text)[0]
    nick_name = re.findall("(?<=bottom-user\">).+?(?=</p>)", r.text)[0]
    info = DouyinVideoInfo(video_id, title_name, nick_name)
    return info


if __name__ == '__main__':
    d1 = 'https://www.iesdouyin.com/share/video/6604280853952466190/?region=CN&mid=6601014204340275975&u_code=df31b2ff&titleType=title'
    d2 = 'https://www.douyin.com/share/video/6536906252729978119/?region=CN&mid=6441083920867035918&titleType=title&timestamp=1522052360&utm_campaign=client_share&app=aweme&utm_medium=ios&iid=27151889375&utm_source=copy'
    info = get_video_info(d2)
    down_by_vid(info)