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


Posted in Python onMay 06, 2021

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

项目地址是: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   : chenshiyang@blued.com
# @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   : chenshiyang@blued.com
# @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 %}
  <div class="jumbotron custom-jum no-mrg">
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="center">
            <div class="home-search">
              <h1>抖音无水印视频下载器</h1>
              <h2>将抖音无水印视频下载到Mp4和Mp3</h2>
            </div>
            <div class="form-home-search">
              <form id="form_download" action='https://www.chenshiyang.com/dytk/downloader' method='POST'>
                <div class="input-group col-lg-10 col-md-10 col-sm-10">
                  <input name="url" class="form-control input-md ht58" placeholder="输入抖音视频 URL ..." type="text"
                    required="" value="">
                  <span class="input-group-btn"><button class="btn btn-primary input-md btn-download ht58" type="submit"
                      id="btn_submit">下载</button></span>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  </div>

  {% endblock %}

# templates/download.html

{% extends "base.html" %}

{% block content %}
  <div class="page-content">
  <div class="container">
    <div class="row">
      <div class="col-lg-12 col-centered">
        <div class="ads mrg-bt20 text-center">
          <ins class="adsbygoogle" style="display:inline-block;width:728px;height:90px"
            data-ad-client="ca-pub-2984659695526033" data-ad-slot="5734284394"></ins>

        </div>
        <div class="card">
          <div class="row">
            <div class="col-md-4 col-sm-4">
              <a href="{{mp4_path}}" rel="external nofollow"  rel="external nofollow"  data-toggle="modal" class="card-aside-column img-video"
                style="height: 252px; background: url(&quot;{{data.cover}}&quot;) 0% 0% / cover;" title="">
                <span class="btn-play-video"><i class="glyphicon glyphicon-play"></i></span>
                <p class="time-video" id="time">{{data.duration}}</p>
              </a>
              <h5>作者: {{data.nickname}}</h5>
              <h5><a href="#" rel="external nofollow" >{{topic}} <i class="open-new-window"></i></a></h5>
              <p class="card-text">{{data.desc}}</p>
            </div>
            <div class="col-md-8 col-sm-8 col-table">
              <table class="table">
                <thead>
                  <tr>
                    <th>format</th>
                    <th>size</th>
                    <th>Downloads</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>

                    <td>mp4</td>
                    <td>{{mp4_content_length}}</td>
                    <td>
                      <a href="{{mp4_path}}" rel="external nofollow"  rel="external nofollow"  class="btn btn-download"  download="">下载</a>
                    </td>
                  </tr>
                  <tr>

                    <td>mp3</td>
                    <td>{{mp3_content_length}}</td>
                    <td>
                      <a href="{{mp3_path}}" rel="external nofollow"  class="btn btn-download"  download="">下载</a>
                    </td>
                  </tr>

                </tbody>

              </table>
            </div>
          </div>
        </div>

        <div class="card card-qrcode">
          <div class="row">
            <div class="col-md-12 qrcode">
              <div class="text-center">
                <p class="qrcode-p">扫描下面的二维码直接下载到您的智能手机或平板电脑!</p>
              </div>
            </div>
            <div class="col-md-4 col-centered qrcode">
              <div id="qrcode" title="{{realpath}}">
                <script src="/static/js/qrcode.min.js"></script>
                <script type="text/javascript">
                  new QRCode(document.getElementById("qrcode"), {
                    text: "{{realpath}}",
                    width: 120,
                    height: 120,
                    correctLevel: QRCode.CorrectLevel.L
                  });
</script>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

{% endblock %}

完整源码地址:

https://github.com/tinysheepyang/python_api

以上就是Django实现在线无水印抖音视频下载(附源码及地址)的详细内容,更多关于Django 无水印抖音视频下载的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python爬虫框架Scrapy常用命令总结
Jul 26 Python
django 外键model的互相读取方法
Dec 15 Python
python使用for循环计算0-100的整数的和方法
Feb 01 Python
对python 自定义协议的方法详解
Feb 13 Python
Python实现最大子序和的方法示例
Jul 05 Python
Django框架组成结构、基本概念与文件功能分析
Jul 30 Python
python实现将range()函数生成的数字存储在一个列表中
Apr 02 Python
Python如何在windows环境安装pip及rarfile
Jun 15 Python
一文解决django 2.2与mysql兼容性问题
Jul 15 Python
python属于哪种语言
Aug 16 Python
Python通过字典映射函数实现switch
Nov 06 Python
Django中session进行权限管理的使用
Jul 09 Python
Django给表单添加honeypot验证增加安全性
Django利用AJAX技术实现博文实时搜索
May 06 #Python
python 如何获取页面所有a标签下href的值
May 06 #Python
Python中常见的导入方式总结
May 06 #Python
Python基础之hashlib模块详解
May 06 #Python
用Python爬虫破解滑动验证码的案例解析
python本地文件服务器实例教程
You might like
php数组函数序列之array_keys() - 获取数组键名
2011/10/30 PHP
Yii把CGridView文本框换成下拉框的方法
2014/12/03 PHP
详解thinkphp实现excel数据的导入导出(附完整案例)
2016/12/29 PHP
PHP排序算法之简单选择排序(Simple Selection Sort)实例分析
2018/04/20 PHP
关于JavaScript中var声明变量作用域的推断
2010/12/16 Javascript
浅析Cookie中的Path与domain
2013/12/18 Javascript
值得学习的bootstrap fileinput文件上传工具
2016/11/08 Javascript
Node.js批量给图片加水印的方法
2016/11/15 Javascript
js 点击a标签 获取a的自定义属性方法
2016/11/21 Javascript
Webpack实现按需打包Lodash的几种方法详解
2017/05/08 Javascript
JS解析url查询参数的简单代码
2017/08/06 Javascript
详解swiper在vue中的应用(以3.0为例)
2018/09/20 Javascript
vue项目动态设置页面title及是否缓存页面的问题
2018/11/08 Javascript
vue实现数字动态翻牌的效果(开箱即用)
2019/12/08 Javascript
javascript 设计模式之组合模式原理与应用详解
2020/04/08 Javascript
微信小程序点击滚动到指定位置的实现
2020/05/22 Javascript
ant-design表单处理和常用方法及自定义验证操作
2020/10/27 Javascript
关于angular 8.1使用过程中的一些记录
2020/11/25 Javascript
js canvas实现五子棋小游戏
2021/01/22 Javascript
[02:24]DOTA2亚洲邀请赛 NAVI战队出场宣传片
2015/02/07 DOTA
python中global与nonlocal比较
2014/11/21 Python
Python自定义函数计算给定日期是该年第几天的方法示例
2019/05/30 Python
树莓派使用USB摄像头和motion实现监控
2019/06/22 Python
python递归法实现简易连连看小游戏
2020/03/25 Python
pytorch实现特殊的Module--Sqeuential三种写法
2020/01/15 Python
Selenium+BeautifulSoup+json获取Script标签内的json数据
2020/12/07 Python
澳大利亚宠物商店:Petbarn
2017/11/18 全球购物
安全教育心得体会
2013/12/29 职场文书
学校庆元旦歌咏比赛主持词
2014/03/18 职场文书
男女朋友协议书
2014/04/23 职场文书
环境保护建议书
2014/08/26 职场文书
安全员岗位职责
2015/02/10 职场文书
计划生育工作总结2015
2015/04/03 职场文书
mysql 如何获取两个集合的交集/差集/并集
2021/06/08 MySQL
如何开启Apache,Nginx和IIS服务器的GZIP压缩功能
2022/04/29 Servers
Python序列化模块JSON与Pickle
2022/06/05 Python