Django实战: 在线无水印下载抖音视频(附源码分析及地址)

分享个由读者陈仕洋同学利用Django开发的在线无水印下载抖音视频项目。该项目功能简单,完全复制SaveTweetVedio的项目。用户观看抖音视频时选择复制视频链接,输入到下载输入栏,即可下载无水印视频,还可扫描二维码手机上预览。亲测成功。

Django实战: 在线无水印下载抖音视频(附源码分析及地址)_第1张图片

项目地址是:https://www.chenshiyang.com/dytk,点击文末原文链接即可跳转。

接下来我们分析下源码简要看下实现原理。

实现原理

该项目不需要使用模型(models), 最核心的只有两个页面:一个主页面(home)展示包含下载url地址的表单,一个下载页面(download)处理表单请求,并展示去水印后的视频文件地址及文件大小,以及用于手机预览的二维码。

对应两个核心页面的路由如下所示,每个url对应一个视图函数。

# urls.py

from django.urls import path


from web.views import home, download


urlpatterns = [
    path('home', home),
    path('downloader', download),
]

#web/urls.py

from django.http import HttpResponse
from django.shortcuts import render, redirect


# Create your views here.
from common.utils import format_duration, load_media
from common.DouYin import DY


def home(request):
    """首页"""
    return render(request, 'home.html')


def download(request):
    """下载"""
    url = request.POST.get('url', None)
    assert url != None


    dy = DY()
    data = dy.parse(url)


    mp4_path, mp4_content_length = load_media(data['mp4'], 'mp4')
    mp3_path, mp3_content_length = load_media(data['mp3'], 'mp3')


    realpath = ''.join(['https://www.chenshiyang.com', mp4_path])


    print('realpath---------------------', realpath)


    if len(data['desc'].split('#')) > 2:
        topic = data['desc'].split('#')[2].rstrip('#')


    return render(request, 'download.html', locals())

可以看出通过home页面表单提交过来的下载url会交由download函数处理。common模块的DouYin.py中定义的DY类负责对url继续解析,爬取相关视频地址,通过自定义utils.py中的load_media方法下载文件,并返回文件路径以及文件大小。

由于解析下载url,从抖音爬取数据的代码都封装到DY类里了,所以我们有必要贴下这个类的代码。另外,我们还需要贴下load_media这个方法的代码。

# common/DouYin.py

# -*- coding: utf-8 -*-
# @Time    : 2020-07-03 13:10
# @Author  : chenshiyang
# @Email   : [email protected]
# @File    : DouYin.py
# @Software: PyCharm




import re
from urllib.parse import urlparse
import requests
from common.utils import format_duration




class DY(object):


    def __init__(self, app=None):
        self.app = app
        if app is not None:
            self.init_app(app)


        self.headers = {
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            # 'accept-encoding': 'gzip, deflate, br',
            'accept-language': 'zh-CN,zh;q=0.9',
            'cache-control': 'no-cache',
            'cookie': 'sid_guard=2e624045d2da7f502b37ecf72974d311%7C1591170698%7C5184000%7CSun%2C+02-Aug-2020+07%3A51%3A38+GMT; uid_tt=0033579d9229eec4a4d09871dfc11271; sid_tt=2e624045d2da7f502b37ecf72974d311; sessionid=2e624045d2da7f502b37ecf72974d311',
            'pragma': 'no-cache',
            'sec-fetch-dest': 'document',
            'sec-fetch-mode': 'navigate',
            'sec-fetch-site': 'none',
            'sec-fetch-user': '?1',
            'upgrade-insecure-requests': '1',
            'user-agent': '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'
        }


        self.domain = ['www.douyin.com',
                       'v.douyin.com',
                       'www.snssdk.com',
                       'www.amemv.com',
                       'www.iesdouyin.com',
                       'aweme.snssdk.com']


    def init_app(self, app):
        self.app = app


    def parse(self, url):
        share_url = self.get_share_url(url)
        share_url_parse = urlparse(share_url)


        if share_url_parse.netloc not in self.domain:
            raise Exception("无效的链接")
        dytk = None
        vid = re.findall(r'\/share\/video\/(\d*)', share_url_parse.path)[0]
        match = re.search(r'\/share\/video\/(\d*)', share_url_parse.path)
        if match:
            vid = match.group(1)


        response = requests.get(
            share_url,
            headers=self.headers,
            allow_redirects=False)


        match = re.search('dytk: "(.*?)"', response.text)


        if match:
            dytk = match.group(1)


        if vid:
            return self.get_data(vid, dytk)
        else:
            raise Exception("解析失败")


    def get_share_url(self, url):
        response = requests.get(url,
                                headers=self.headers,
                                allow_redirects=False)


        if 'location' in response.headers.keys():
            return response.headers['location']
        elif '/share/video/' in url:
            return url
        else:
            raise Exception("解析失败")


    def get_data(self, vid, dytk):
        url = f"https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}&dytk={dytk}"
        response = requests.get(url, headers=self.headers, )
        result = response.json()
        if not response.status_code == 200:
            raise Exception("解析失败")
        item = result.get("item_list")[0]
        author = item.get("author").get("nickname")
        mp4 = item.get("video").get("play_addr").get("url_list")[0]
        cover = item.get("video").get("cover").get("url_list")[0]
        mp4 = mp4.replace("playwm", "play")
        res = requests.get(mp4, headers=self.headers, allow_redirects=True)
        mp4 = res.url
        desc = item.get("desc")
        mp3 = item.get("music").get("play_url").get("url_list")[0]


        data = dict()
        data['mp3'] = mp3
        data['mp4'] = mp4
        data['cover'] = cover
        data['nickname'] = author
        data['desc'] = desc
        data['duration'] = format_duration(item.get("duration"))
        return data

从代码你可以看到返回的data字典里包括了mp3和mp4源文件地址,以及视频的封面,作者昵称及描述等等。

接下来你可以看到load_media方法爬取了视频到本地,并提供了新的path和大小。

#common/utils.py

# -*- coding: utf-8 -*-
# @Time    : 2020-06-29 17:26
# @Author  : chenshiyang
# @Email   : [email protected]
# @File    : utils.py
# @Software: PyCharm
import os
import time


import requests




def format_duration(duration):
    """
    格式化时长
    :param duration 毫秒
    """


    total_seconds = int(duration / 1000)
    minute = total_seconds // 60
    seconds = total_seconds % 60
    return f'{minute:02}:{seconds:02}'


SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
    1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}




def approximate_size(size, a_kilobyte_is_1024_bytes=True):


    '''Convert a file size to human-readable form.
    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000
    Returns: string
    '''


    if size < 0:
        raise ValueError('number must be non-negative')


    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)


    raise ValueError('number too large')




def do_load_media(url, path):
    """
    对媒体下载
    :param url:         多媒体地址
    :param path:        文件保存路径
    :return:            None
    """
    try:
        headers = {
            "User-Agent": "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"}
        pre_content_length = 0


        # 循环接收视频数据
        while True:
            # 若文件已经存在,则断点续传,设置接收来需接收数据的位置
            if os.path.exists(path):
                headers['Range'] = 'bytes=%d-' % os.path.getsize(path)
            res = requests.get(url, stream=True, headers=headers)


            content_length = int(res.headers['content-length'])
            # 若当前报文长度小于前次报文长度,或者已接收文件等于当前报文长度,则可以认为视频接收完成
            if content_length < pre_content_length or (
                    os.path.exists(path) and os.path.getsize(path) == content_length):
                break
            pre_content_length = content_length


            # 写入收到的视频数据
            with open(path, 'ab') as file:
                file.write(res.content)
                file.flush()
                print('receive data,file size : %d   total size:%d' % (os.path.getsize(path), content_length))
                return approximate_size(content_length, a_kilobyte_is_1024_bytes=False)


    except Exception as e:
        print('视频下载异常:{}'.format(e))




def load_media(url, path):
    basepath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))


    # 生成13位时间戳
    suffixes = str(int(round(time.time() * 1000)))
    path = ''.join(['/media/', path, '/', '.'.join([suffixes, path])])
    targetpath = ''.join([basepath, path])
    content_length = do_load_media(url, targetpath)
    return path, content_length




def main(url, suffixes, path):
    load_media(url, suffixes, path)




if __name__ == "__main__":
    # url = 'https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fe70000br155v26tgq06h08e0lg&ratio=720p&line=0'
    # suffixes = 'test'
    # main(url, suffixes, 'mp4',)


    print(approximate_size(3726257, a_kilobyte_is_1024_bytes=False))

接下来我们看下模板, 这个没什么好说的。

# templates/home.html

{% extends "base.html" %}


{% block content %}
  
{% endblock %}

# templates/download.html

{% extends "base.html" %}


{% block content %}
  

{ {data.duration}}

作者: { {data.nickname}}
{ {topic}}

{ {data.desc}}

format size Downloads
mp4 { {mp4_content_length}} 下载
mp3 { {mp3_content_length}} 下载

扫描下面的二维码直接下载到您的智能手机或平板电脑!

{% endblock %}

完整源码地址:

https://github.com/tinysheepyang/python_api。

如果你有什么好的Django项目或文章需要分享,欢迎留言联系啊。

Django实战: 在线无水印下载抖音视频(附源码分析及地址)_第2张图片

大江狗

2020.9.7

你可能感兴趣的:(python,django,web,html,移动开发)