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中的zip函数使用示例
Jan 29 Python
利用Python的Flask框架来构建一个简单的数字商品支付解决方案
Mar 31 Python
给Python初学者的一些编程技巧
Apr 03 Python
python实现的简单文本类游戏实例
Apr 28 Python
在Mac OS上使用mod_wsgi连接Python与Apache服务器
Dec 24 Python
python回调函数中使用多线程的方法
Dec 25 Python
Python面向对象之继承代码详解
Jan 29 Python
Python中pandas模块DataFrame创建方法示例
Jun 20 Python
Django ValuesQuerySet转json方式
Mar 16 Python
解决Python Matplotlib绘图数据点位置错乱问题
May 16 Python
pandas抽取行列数据的几种方法
Dec 13 Python
python爬虫scrapy基本使用超详细教程
Feb 20 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
使用JSON实现数据的跨域传输的php代码
2011/12/20 PHP
Mysql的Root密码忘记,查看或修改的解决方法(图文介绍)
2013/06/14 PHP
php中让上传的文件大小在上传前就受限制的两种解决方法
2013/06/24 PHP
PHP实现的购物车类实例
2015/06/17 PHP
浅谈PHP中的数据传输CURL
2016/09/06 PHP
php curl上传、下载、https登陆实现代码
2017/07/23 PHP
PHP性能测试工具xhprof安装与使用方法详解
2018/04/29 PHP
json简单介绍
2008/06/10 Javascript
JS随机洗牌算法之数组随机排序
2016/03/23 Javascript
JS 调用微信扫一扫功能
2016/12/22 Javascript
浅析JS中的 map, filter, some, every, forEach, for in, for of 用法总结
2017/03/29 Javascript
es6学习笔记之Async函数的使用示例
2017/05/11 Javascript
vue-router2.0 组件之间传参及获取动态参数的方法
2017/11/10 Javascript
使用json-server简单完成CRUD模拟后台数据的方法
2018/07/12 Javascript
js中innerText/textContent和innerHTML与target和currentTarget的区别
2019/01/21 Javascript
express.js中间件说明详解
2019/03/19 Javascript
在layer弹层layer.prompt中,修改placeholder的实现方法
2019/09/27 Javascript
Python的词法分析与语法分析
2013/05/18 Python
Python实现的石头剪子布代码分享
2014/08/22 Python
浅析Python中将单词首字母大写的capitalize()方法
2015/05/18 Python
Python数据结构之翻转链表
2017/02/25 Python
OPENCV去除小连通区域,去除孔洞的实例讲解
2018/06/21 Python
python 读取目录下csv文件并绘制曲线v111的方法
2018/07/06 Python
Python元组常见操作示例
2019/02/19 Python
Django 项目布局方法(值得推荐)
2020/03/22 Python
公共汽车、火车和飞机票的通用在线预订和销售平台:INFOBUS
2019/11/30 全球购物
大学活动邀请函
2014/01/28 职场文书
生物制药专业自我鉴定
2014/02/19 职场文书
酒店开业庆典主持词
2014/03/21 职场文书
2014卖家双十一活动策划书
2014/09/29 职场文书
2015年民主生活会发言材料
2014/12/15 职场文书
师德师风个人总结
2015/02/06 职场文书
毕业论文致谢格式模板
2015/05/14 职场文书
工资证明范本
2015/06/12 职场文书
暑期社会实践新闻稿
2015/07/17 职场文书
合作意向书怎么写
2019/06/24 职场文书