我被百度网盘限速后,一怒之下编了一个“摆渡下载器”来讽刺某些软件!

演示视频:UP被百度网盘限速后一怒之下编写了一个“摆渡下载器”来讽刺某些软件!_哔哩哔哩_bilibili

源代码(注释还不够吗?):

import sys
import requests
import time
import tkinter as tk
import tkinter.ttk as ttk
from threading import Lock
import tkinter.filedialog as tf
import tkinter.messagebox as tm
import threading
from PIL import Image, ImageTk
import webbrowser

url = ''
filename = ''
root = tk.Tk()
root.title('摆渡下载器--畅享高速下载')
filename_var = tk.StringVar()
present = 0.0
vip = False


def get_file_size(download_url):
    # 初始化一个计数器 con_count 并设置为0。这个计数器用于跟踪尝试获取文件大小的次数。
    con_count = 0
    # 开始一个无限循环。这个循环将一直执行,直到成功获取文件大小或达到最大尝试次数。
    while True:
        if con_count > 50:
            # 如果 con_count 大于50,则显示一个错误消息,告诉用户文件获取失败。在显示错误消息后,函数返回 False,表示未能成功获取文件大小。
            tm.showerror("错误", "文件获取失败")
            return False
        try:
            # 使用 requests 库发送一个GET请求到给定的 url。
            # stream=True 意味着响应内容将不会被立即下载。
            # 从响应头中获取 'content-length' 字段,这通常表示文件的大小(以字节为单位)。
            # 将这个值转换为整数并存储在 file_size 变量中。
            file_size = int(requests.get(url=download_url, stream=True).headers['content-length'])
        except (Exception, ValueError):  # 之所以加一个ValueError是因为pycharm的“弱警告”的那条黄色波浪线很烦人……
            # 如果发生异常,增加 con_count 的值。
            con_count += 1
            # 更新 root(防止死机)
            root.update()
            # 暂停执行0.1秒。这可以帮助减少连续尝试之间的频率,并可能避免某些服务器因频繁请求而封锁。
            time.sleep(0.1)
            # 再次更新 root
            root.update()
            continue
        # 如果成功获取了文件大小,函数返回这个值。
        return file_size


def download(entry1, entry2, label, pro, btn: ttk.Button):
    btn["state"] = "disabled"  # 防止重复点按
    global url, filename
    # 检查URL和文件名是否有效
    if entry1.get() and entry2.get():
        url = entry1.get()
        filename = entry2.get()
    else:
        # 如果entry1或entry2输入框的值为空,则显示错误提示并退出函数
        tm.showerror("错误!", "请输入完整信息")
        btn["state"] = "normal"
        return
    _all = get_file_size(url)  # 使用前面定义的 get_file_size 函数获取文件大小
    if not _all:  # 如果 get_file_size 函数返回 False,则表示未能成功获取文件大小
        # 显示错误提示并退出函数
        tm.showerror("错误!", "下载文件失效,请检察链接地址和网络连接")
        btn["state"] = "normal"
        return
    # 使用 requests 库发送GET请求,并初始化一个二进制写文件用于保存下载的数据。stream=True 意味着响应内容将不会被立即下载(流式下载)。
    r = requests.get(url, stream=True)
    file = open(filename, 'wb')
    # 分多次下载数据
    _len = 0
    # 如果用户是VIP(由变量 vip 判断),则执行多线程下载;否则,限速下载。
    if vip:
        chunk_size = 1024 * 4096  # 每次下载的数据量为4MB
        file = open(filename, 'wb')
        _bytes = 0  # 已经下载完成的字节数

        def downloads(start, end):  # 使用多线程来并行下载文件的不同部分,以加速下载过程
            # nonlocal 关键字允许内部函数修改外部作用域的变量
            nonlocal _bytes
            # 这里定义了一个 HTTP 请求头,用于告诉服务器要下载文件的哪一部分。Range 头的值是一个字节范围,从 start 开始到 end 结束
            headers = {
                'Range': f'bytes={start}-{end}',
            }
            # 使用 requests 库发送一个带有 Range 头的 GET 请求,以获取文件的指定部分。
            req = requests.get(url, stream=True, headers=headers)
            pos = start  # 文件指针,用于跟踪当前写入的文件位置。这个值初始化为分块开始的字节位置,防止多线程导致的数据混乱
            # 使用 iter_content 方法迭代响应内容,每次迭代返回一个大小为 chunk_size 的字节块。这样可以避免一次性加载整个响应内容到内存中。
            for loop_count in req.iter_content(chunk_size=chunk_size):
                # 检查字节块是否非空。如果服务器返回了空的字节块(可能是因为文件结束或网络问题),则跳过此次循环。
                if loop_count:
                    # lock.acquire() 和 lock.release(): 这两个调用用于获取和释放线程锁 lock。线程锁用于确保同一时间只有一个线程可以写入文件,防止多个线程同时写入导致的数据混乱。
                    lock.acquire()
                    file.seek(pos)  # 将文件指针移动到 pos 指定的位置,以便开始写入新的字节块。
                    file.write(loop_count)  # 将字节块写入文件。
                    lock.release()  # 见上
                    # 更新文件指针的位置,以便下一次迭代写入下一个字节块。
                    pos += chunk_size
                    # bytes += chunk_size: 更新已下载字节的总数。
                    _bytes += chunk_size
                    # 计算已下载的百分比。_all 是文件的总字节数,通过 get_file_size(url) 获取。
                    present_now = round(_bytes / _all, 6)
                    # 确保百分比不会超过 100%(文件大小可能不是4MB的倍数)
                    if present_now > 1:
                        present_now = 1
                    # 更新进度标签的文本,显示已下载的百分比
                    label.config(text="已下载:{}%".format(format(round(present_now * 100, 5), '.3f')))
                    # 更新进度条的值。这里将百分比乘以 1000 是因为进度条使用的是 0-1000 的范围来表示 0%-100%。
                    pro['value'] = int(round(present_now * 1000))
                    # 更新 GUI(图形用户界面),使进度标签和进度条的更新对用户可见,并防止死机
                    root.update()

        # stream=True确保响应内容以流的形式返回,这是必要的,因为我们并不下载整个文件内容。
        r2 = requests.get(url, stream=True)
        # 创建一个线程锁对象。这个锁将被用来确保同时只有一个线程在更新共享资源(文件和GUI元素)时不会发生冲突
        lock = Lock()
        # 设置线程数为6线程下载
        thread_num = 6
        # 从HTTP响应头中提取Content-Length字段,得到文件的总字节大小,并将其转换为整数。(同函数get_file_size)
        size = int(r2.headers['Content-Length'])
        for i in range(thread_num):
            # 查当前是否是最后一个线程。如果是,则需要对最后一个线程进行特殊处理,因为它需要下载文件的剩余部分,这部分可能不是size // thread_num的整数倍。
            if i == thread_num - 1:
                # 创建一个新线程,其目标是downloads函数。这个线程将下载文件的最后一个部分,从i * (size // thread_num)开始到文件末尾
                t1 = threading.Thread(target=downloads, args=(i * (size // thread_num), size))
                # 启动这个线程
                t1.start()
            else:
                # 创建一个新线程,其目标是downloads函数。这个线程将下载文件的一个固定大小的部分,从i * (size // thread_num)开始到(i + 1) * (size //
                # thread_num)结束
                t1 = threading.Thread(target=downloads, args=(i * (size // thread_num), (i + 1) * (size // thread_num)))
                t1.start()
    else:
        # 每次从服务器读取并写入文件的数据块大小为256字节,就算每秒可以循环100次,也只能下载50kb
        chunk_size = 512
        for i in r.iter_content(chunk_size=chunk_size):  # 限速下载
            if i:  # 判断是否为空数据
                # 将从服务器读取的字节串写入到本地文件中
                file.write(i)
                # 更新计数器_len,表示已经写入的次数
                _len += 1
                # 计算已下载的百分比。
                now_present = round(_len * chunk_size / _all, 6)
                if now_present > 1:
                    # 确保百分比不会超过100%。因为文件大小不一定是256字节的整倍数。
                    now_present = 1
                # 更新GUI控件(同上)
                label.config(text="已下载:{}%".format(format(round(now_present * 100, 5), '.3f')))
                pro['value'] = int(round(now_present * 1000))
                root.update()
                # 限速,保证每秒循环次数不超过10次(只能下载不到5kb)
                time.sleep(0.1)
        file.close()
    btn["state"] = "normal"


def dile_upload():
    # 弹出文件保存对话框,标题为'选择文件',文件类型过滤器设置为'所有文件'
    name = tf.asksaveasfilename(title='选择文件', filetypes=[('所有文件', '*.*')])
    # 设置全局变量 filename_var 的值为用户选择的文件路径,它与entry控件的 text variable 属性相关联
    filename_var.set(name)


def get_screen_size(win):
    # 使用 win.winfo_screenwidth() 和 win.winfo_screenheight() 方法来获取屏幕的宽度和高度(以像素为单位)。
    screen_width = win.winfo_screenwidth()
    screen_height = win.winfo_screenheight()
    return int(screen_width), int(screen_height)


def get_win_pos(win):
    # 首先,它调用 get_screen_size(win) 来获取屏幕的宽度和高度。
    # 然后,它计算窗口的初始位置,将窗口放置在屏幕的左上角(即屏幕的 1/8 宽度和 1/8 高度处)。
    # 返回计算得到的 x 和 y 坐标作为一个元组 (pos_x, pos_y)
    w, h = get_screen_size(win)
    return int(w / 8), int(h / 8)


def center(win):
    # 首先,它调用 get_win_pos(win) 来获取窗口应该放置的 x 和 y 坐标。 然后,它使用 win.geometry("+{}+{}".format(pos_x, pos_y)) 来设置窗口的位置。这里使用的
    # geometry 方法设置窗口的位置和大小,但由于只提供了位置信息而没有提供大小信息,窗口的大小不会改变。 最后,win.update() 被调用以确保窗口的位置更新被立即应用。
    pos_x, pos_y = get_win_pos(win)
    win.geometry("+{}+{}".format(pos_x, pos_y))
    win.update()


def init():
    global vip
    # 检查 VIP 状态
    try:
        # 尝试读取 vip.baidu。这个文件用来存储用户是否为 VIP 的状态。
        with open("./vip.baidu", 'r') as f_obj:
            if f_obj.read() == "False":
                vip = True
    except (FileNotFoundError, Exception):
        # 如果文件不存在或读取时发生任何异常,尝试创建这个文件并写入 "True",表示用户不是 VIP。
        try:
            with open("./vip.baidu", 'w') as f_obj:
                f_obj.write("True")
            vip = False
        except (PermissionError, Exception):
            # 如果在创建或写入文件时遇到权限错误或其他异常,程序会显示一个错误对话框,并退出。
            tm.showerror("错误", "软件出现问题,它必须退出。")
            sys.exit()
    try:
        # 接下来,函数尝试设置应用程序窗口的图标。它尝试加载一个名为 icon.png 的图片作为窗口图标。
        root.iconphoto(True, tk.PhotoImage(file="./icon.png"))
    except (tk.TclError, Exception):
        # 如果加载图标时发生错误(如文件不存在),则捕获异常并忽略它,程序继续执行。
        pass
    # 使用 root.attributes("-topmost", 1) 将窗口设置为始终置顶。这意味着无论用户打开或切换到其他窗口,这个窗口都会保持在最前面。
    root.attributes("-topmost", 1)
    # 绑定两个鼠标事件到 root 窗口
    root.bind("<Enter>", lambda event: mouse_enter())
    root.bind("<Leave>", lambda event: mouse_leave())


def startup():
    try:
        # 使用 ImageTk.PhotoImage 和 Image.open 方法从文件 "./BAIDU.png" 中加载一个图像
        image = ImageTk.PhotoImage(Image.open("./BAIDU.png"))
        # 创建一个 tkinter 的 Label 控件,并将其图像属性设置为加载的图像
        img_label = tk.Label(root, image=image)
        img_label.pack()
        # 调用 center 函数来将窗口放置在屏幕的中央位置。
        center(root)
        # 调用 root.update() 来更新窗口并显示加载的图像。
        root.update()
        # 调用 init 函数来执行一些初始化设置,比如设置VIP状态、窗口图标等。
        init()
        # 使用 time.sleep(1) 暂停程序执行一秒。这是为了让用户有时间看到启动画面
        time.sleep(1)
        # 一秒过后,调用 img_label.destroy() 来销毁图像标签,并从窗口中移除它。
        img_label.destroy()
        del img_label
    except (FileNotFoundError, tk.TclError, Exception):
        # 如果在加载图像或执行上述任何操作时发生任何异常,则只执行最关键的 init 等函数
        init()
        center(root)
        root.update()


def tops(btn: ttk.Button):
    # 当用户点击 VIP 办理窗口中的图片时,这个函数会被调用。
    def click():
        global vip
        # 销毁 VIP 办理窗口。
        top.destroy()
        # 使用默认浏览器打开指定的 URL(这里是本天才的空间)。
        webbrowser.open("https://space.bilibili.com/3493125991434836?spm_id_from=333.788.0.0")
        try:
            # 尝试写入文件 vip.baidu:更新VIP状态,并标记用户现在是VIP。
            with open("./vip.baidu", 'w') as f_obj:
                f_obj.write("False")
            vip = True
        except (PermissionError, Exception):
            # 如果写入文件时发生权限错误或其他异常,将按钮状态设置为正常,销毁窗口,并显示一个错误对话框
            btn["state"] = "normal"
            top.destroy()
            tm.showerror("错误", "VIP办理失败,请稍后再试!")
            return
        # 如果一切正常,更新按钮文本和显示一个信息对话框,告知用户他们现在是 VIP。
        btn.config(text="您好,尊贵的VIP!摆渡衷心祝您下载愉快!")
        tm.showinfo("恭喜!", "您已经是尊贵的VIP会员了!开始畅快下载吧~")

    # 当用户尝试关闭 VIP 办理窗口时,这个函数会被调用
    def when_exit():
        # 使用 tm.askyesno 弹出一个对话框,询问用户是否真的要取消办理 VIP。
        is_exit = tm.askyesno("问题", "在摆渡,下载的每一秒都是生命!确认取消办理VIP吗?")
        if is_exit:
            # 如果用户选择“是”,则更新按钮状态为正常,销毁窗口
            btn["state"] = "normal"
            top.destroy()
        else:
            # 如果用户选择“否”,则什么也不做
            return "break"

    top = tk.Toplevel(root)
    top.title("办理VIP会员--享受飞起的速度!")
    image = ImageTk.PhotoImage(Image.open("./vip.png"))
    img_label = tk.Label(top, image=image, cursor="heart")  # 把鼠标形状设为一个心形
    img_label.pack()
    # 为图片绑定鼠标左键点击事件,当点击时调用 click 函数
    img_label.bind("<Button-1>", lambda event: click())
    text_label = tk.Label(top, text="办理方法:点击图片、关注UP主并三连就可以了~", font=("微软雅黑", 15))
    text_label.pack()
    # 设置窗口大小不可调整
    top.resizable(False, False)
    # 禁用传入的按钮,防止用户在办理过程中重复点击。
    btn["state"] = "disabled"
    # 为窗口设置关闭时的回调函数为 when_exit
    top.protocol("WM_DELETE_WINDOW", when_exit)
    top.mainloop()


def mouse_enter():
    # 鼠标进入,设置窗口透明度为不透明
    root.attributes("-alpha", 1)


def mouse_leave():
    # 鼠标离开,设置窗口透明度为半透明
    root.attributes("-alpha", 0.5)


def guis():
    # 使用布局管理器管理GUI布局,懒得说了
    frame = tk.Frame(root)
    frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
    label = tk.Label(frame, text="请输入下载链接:")
    label.grid(row=0, column=0, pady=5)
    entry = ttk.Entry(frame)
    entry.grid(row=0, column=1, pady=5)
    download_btn = ttk.Button(frame, text="下载", command=lambda: download(entry, entry2, label3, pro, download_btn))
    download_btn.grid(row=0, column=2, pady=5, padx=10)
    label2 = tk.Label(frame, text="请输入保存路径:")
    entry2 = ttk.Entry(frame, textvariable=filename_var)
    btn = ttk.Button(frame, text="选择文件", command=dile_upload)
    label2.grid(row=1, column=0, pady=5)
    entry2.grid(row=1, column=1, pady=5)
    btn.grid(row=1, column=2, pady=5, padx=10)
    label3 = tk.Label(frame, text="已下载:0.000%")
    label3.grid(row=2, column=0, pady=5)
    pro = ttk.Progressbar(frame)
    pro.grid(row=2, column=1, columnspan=2, pady=5, sticky=tk.W + tk.E)
    pro["maximum"] = 1000
    # 为ttk.Button添加样式
    style0 = ttk.Style()
    style0.configure("C.TButton", foreground="gold")
    if not vip:
        btn2 = ttk.Button(root, text="下载太慢?办理VIP可提速4915200%!!!", style="C.TButton", command=lambda: tops(btn2))
    else:
        btn2 = ttk.Button(root, text="您好,尊贵的VIP!摆渡衷心祝您下载愉快!", style="C.TButton",
                          command=lambda: tops(btn2))
        btn["state"] = "disabled"
    btn2.pack(fill=tk.BOTH, expand=True, pady=5, padx=10)
    root.update()


if __name__ == "__main__":
    startup()
    guis()
    root.resizable(False, False)
    root.update()
    root.mainloop()

谢谢支持!

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值