鱼C论坛

 找回密码
 立即注册
查看: 1056|回复: 12

[已解决]关于多线程批量下载的问题

[复制链接]
发表于 2023-5-10 21:39:40 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x


萌新 各种查写了一个批量下载文件的
但是目前有几个问题,
一  线程数量超过200之后 ,下载就会各种失败,下的文件是0KB  
已经添加了锁 但是还是不好使。
二  创建的目录有问题,不能自动按下载地址创建,
比如目前的目录这样创建



需要创建6290\webres\anim\player   6290\webres\anim\monster这样的  

3 程序假死  点击开始下载的时候 ,程序无响应 实际还在下的的

4  json保存的是下载地址,

如何选择josn后 对每行数据前面加上下载地址头,






import json
import os
import requests
import threading
import tkinter as tk
from tkinter import filedialog
 
# 定义下载函数
def download_file(download_url, directory, lock):
    # 获取下载文件的文件名
    filename = os.path.basename(download_url)
    print('正在下载文件:', filename)
     
    # 创建目录
    os.makedirs(directory, exist_ok=True)
    # 下载文件
    response = requests.get(download_url)
    with open(os.path.join(directory, filename), 'wb') as f:
        f.write(response.content)
 
# 定义选择文件函数
def choose_file():
    file_path = filedialog.askopenfilename()
    file_path_var.set(file_path)
    # 读取本地json文件
    with open(file_path, 'r') as f:
        data = json.load(f)
    # 打印下载地址数量
    print('下载地址数量:', len(data))
 
# 定义下载函数
def download(max_threads):
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 创建线程池
    threads = []
    # 创建锁对象
    lock = threading.Lock()
    # 遍历下载地址并下载文件
    count = 0
    for download_url in data:
        # 获取下载文件的目录
        directory = os.path.dirname(download_url)
        # 去除非法字符
        directory = directory.replace(':', '').replace('/', '')
        # 创建线程并启动
        t = threading.Thread(target=download_file, args=(download_url, directory, lock))
        t.start()
        threads.append(t)
        # 添加延迟
        count += 1
        if count >= max_threads:
            # 等待所有线程结束
            for t in threads:
                t.join()
            # 清空线程池和计数器
            threads = []
            count = 0
    # 等待剩余线程结束
    for t in threads:
        t.join()
 
 
# 创建窗口
window = tk.Tk()
window.title('批量下载')
window.geometry('400x250')
 
# 创建选择文件按钮
file_path_var = tk.StringVar()
file_path_label = tk.Label(window, textvariable=file_path_var)
file_path_label.pack()
choose_file_button = tk.Button(window, text='选择文件', command=choose_file)
choose_file_button.pack()
 
# 创建线程数量标签和文本框
thread_label = tk.Label(window, text='线程数量:')
thread_label.pack()
thread_entry = tk.Entry(window)
thread_entry.pack()
 
# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: download(int(thread_entry.get())))
download_button.pack()
 
# 运行窗口
window.mainloop()
最佳答案
2023-5-10 23:07:46
koma610630 发表于 2023-5-10 23:00
如何面板增加个输入框 输入地址
在下载地址前面拼接

你可以通过添加一个新的标签和文本框来实现这个功能。首先,你需要在窗口中添加一个新的标签和文本框。然后,你可以在下载函数中获取文本框中的值,并将其添加到下载地址的前面。

下面是如何修改代码来实现这个功能:
# 定义下载函数
def download(max_threads):
    # 获取输入框中的地址
    base_url = url_entry.get()
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 创建锁对象
    lock = threading.Lock()
    # 创建线程池
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # 遍历下载地址并下载文件
        for download_url in data:
            # 在下载地址前面添加输入框中的地址
            download_url = base_url + download_url
            # 获取下载文件的目录
            directory = os.path.join(BASE_DIR, *download_url.split('/')[1:-1])
            # 提交任务到线程池
            executor.submit(download_file, download_url, directory, lock)

# 创建窗口
window = tk.Tk()
window.title('批量下载')
window.geometry('400x250')

# 创建选择文件按钮
file_path_var = tk.StringVar()
file_path_label = tk.Label(window, textvariable=file_path_var)
file_path_label.pack()
choose_file_button = tk.Button(window, text='选择文件', command=choose_file)
choose_file_button.pack()

# 创建线程数量标签和文本框
thread_label = tk.Label(window, text='线程数量:')
thread_label.pack()
thread_entry = tk.Entry(window)
thread_entry.pack()

# 创建下载地址标签和文本框
url_label = tk.Label(window, text='下载地址:')
url_label.pack()
url_entry = tk.Entry(window)
url_entry.pack()

# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: threading.Thread(target=download, args=(int(thread_entry.get()),)).start())
download_button.pack()

# 运行窗口
window.mainloop()

这样,你就可以在新的文本框中输入下载地址,点击“下载”按钮时,程序会获取文本框中的地址,并将其添加到下载地址的前面。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2023-5-10 21:40:10 | 显示全部楼层
1. 关于下载失败问题,可能是因为线程数过多导致网络连接被占满。可以尝试减少线程数量或者增加下载超时时间来解决。另外,建议使用线程池来管理线程,可以避免线程数量过多导致的问题。

2. 关于创建目录的问题,可以使用os.path模块的join方法来自动创建目录。例如,可以使用os.path.join(directory, 'webres', 'anim', 'player')来创建目录。

3. 关于程序假死的问题,可能是因为下载线程没有正确结束导致。可以在下载线程中加入try...finally块来确保线程结束时正确释放资源。

4. 关于在json每行数据前面加上下载地址头的问题,可以使用列表推导式来实现。例如,可以使用['http://example.com/' + url for url in data]来在每行数据前面加上'http://example.com/'。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-5-10 21:41:17 | 显示全部楼层
isdkz 发表于 2023-5-10 21:40
1. 关于下载失败问题,可能是因为线程数过多导致网络连接被占满。可以尝试减少线程数量或者增加下载超时时 ...

好的,除了第4个能办到, 其他有些麻烦, 能帮忙完善吗
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-10 21:42:46 | 显示全部楼层
1.关于线程数量超过200之后下载失败,可以尝试在for循环内添加一些延迟时间,例如使用time.sleep()函数来放慢线程的速度。
2.关于创建目录的问题,可以使用os.path.join()函数来拼接下载目录和文件名,例如:directory = os.path.join('6290', 'webres', 'anim', 'player')。
3.关于程序假死的问题,可能是由于网络连接等原因导致请求响应缓慢或失效。可以尝试设置一个较长的超时时间,例如response = requests.get(download_url, timeout=60)。
4.关于在每行数据前面加上下载地址头,可以在遍历下载地址并下载文件之前先处理数据。例如:data = [f'http://www.example.com/{url}' for url in data]。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-10 21:53:15 | 显示全部楼层
koma610630 发表于 2023-5-10 21:41
好的,除了第4个能办到, 其他有些麻烦, 能帮忙完善吗

当然可以,这里是一种可能的改进方式:

1. 使用  concurrent.futures.ThreadPoolExecutor  来管理线程,可以更好地控制并发线程的数量,避免线程过多导致的问题。

2. 使用  os.makedirs  函数来创建下载地址中存在的所有目录。

3. 使用  try...except...finally  来确保无论发生什么错误,线程都可以正确结束。

4. 将下载地址头部加入到 json 中的每个下载地址。

这是对应的代码:
import json
import os
import requests
import threading
import tkinter as tk
from tkinter import filedialog
from concurrent.futures import ThreadPoolExecutor

BASE_DIR = 'BASE_DIR'  # 下载文件的基本目录,你需要根据需要进行修改

# 定义下载函数
def download_file(download_url, directory, lock):
    try:
        # 获取下载文件的文件名
        filename = os.path.basename(download_url)
        print('正在下载文件:', filename)

        # 创建目录
        os.makedirs(directory, exist_ok=True)
        # 下载文件
        response = requests.get(download_url, timeout=10)
        with lock:
            with open(os.path.join(directory, filename), 'wb') as f:
                f.write(response.content)
    except Exception as e:
        print(f'下载文件 {download_url} 出错: {e}')
    finally:
        print(f'文件 {download_url} 下载结束')

# 定义选择文件函数
def choose_file():
    file_path = filedialog.askopenfilename()
    file_path_var.set(file_path)
    # 读取本地json文件
    with open(file_path, 'r') as f:
        data = json.load(f)
    # 打印下载地址数量
    print('下载地址数量:', len(data))

# 定义下载函数
def download(max_threads):
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 创建锁对象
    lock = threading.Lock()
    # 创建线程池
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # 遍历下载地址并下载文件
        for download_url in data:
            # 获取下载文件的目录
            directory = os.path.join(BASE_DIR, *download_url.split('/')[1:-1])
            # 提交任务到线程池
            executor.submit(download_file, download_url, directory, lock)

# 创建窗口
window = tk.Tk()
window.title('批量下载')
window.geometry('400x250')

# 创建选择文件按钮
file_path_var = tk.StringVar()
file_path_label = tk.Label(window, textvariable=file_path_var)
file_path_label.pack()
choose_file_button = tk.Button(window, text='选择文件', command=choose_file)
choose_file_button.pack()

# 创建线程数量标签和文本框
thread_label = tk.Label(window, text='线程数量:')
thread_label.pack()
thread_entry = tk.Entry(window)
thread_entry.pack()

# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: download(int(thread_entry.get())))
download_button.pack()

# 运行窗口
window.mainloop()

请注意,这个代码只是一个基础模板,你可能需要根据你的具体需求进行修改。例如,你可能需要根据你的具体需求来调整下载文件的基本目录  BASE_DIR ,或者更改下载地址头。

另外, requests.get  函数的  timeout  参数设置为 10 秒,如果你的网络环境不好或者服务器响应较慢,你可能需要调整这个值。

此外,由于下载文件时可能会出现各种错误(如网络错误、服务器错误等),所以在下载函数  download_file  中加入了错误处理,如果下载文件时出错,会打印出错误信息,并且不会影响其他文件的下载。在实际使用中,你可能需要根据错误类型来决定是否需要重试下载或者其他操作。

还有一点需要注意的是,由于 Python 的 GIL(Global Interpreter Lock)问题,Python 的多线程并不一定能有效利用多核 CPU。如果你需要大量下载文件,可能需要考虑使用多进程或者异步 IO 来提高效率。

最后,这个代码使用了 Python 的  concurrent.futures  模块来创建和管理线程池,这个模块在 Python 3.2 以后的版本中可用。如果你使用的 Python 版本较旧,可能需要更新 Python 或者使用其他方式来创建和管理线程池。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-5-10 22:03:48 | 显示全部楼层
isdkz 发表于 2023-5-10 21:53
当然可以,这里是一种可能的改进方式:

1. 使用  concurrent.futures.ThreadPoolExecutor  来管理线程 ...

还是会出现 程序无响应啊
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-10 22:19:40 | 显示全部楼层
koma610630 发表于 2023-5-10 22:03
还是会出现 程序无响应啊

这是因为在点击“下载”按钮时,你的程序会立即开始下载文件并在主线程中等待下载完成,而在此期间,程序无法响应用户的其他操作,从而导致“程序无响应”。

要解决这个问题,可以在另一个线程中运行下载函数,以使主线程可以继续处理用户的其他操作。下面是如何修改代码来实现这个功能:
# 定义下载函数
def download(max_threads):
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 创建锁对象
    lock = threading.Lock()
    # 创建线程池
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # 遍历下载地址并下载文件
        for download_url in data:
            # 获取下载文件的目录
            directory = os.path.join(BASE_DIR, *download_url.split('/')[1:-1])
            # 提交任务到线程池
            executor.submit(download_file, download_url, directory, lock)

# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: threading.Thread(target=download, args=(int(thread_entry.get()),)).start())
download_button.pack()

这样,当你点击“下载”按钮时,程序会在一个新的线程中运行下载函数,主线程则可以继续处理用户的其他操作,从而避免“程序无响应”。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-5-10 22:25:41 | 显示全部楼层
isdkz 发表于 2023-5-10 22:19
这是因为在点击“下载”按钮时,你的程序会立即开始下载文件并在主线程中等待下载完成,而在此期间,程序 ...


好的 多谢  这个问题解决了, 但是我看了下日志 有不少下载失败的,
这个出现的原因是啥, 我线程开100的
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-10 22:29:34 | 显示全部楼层
koma610630 发表于 2023-5-10 22:25
好的 多谢  这个问题解决了, 但是我看了下日志 有不少下载失败的,
这个出现的原因是啥, 我线程开10 ...

下载失败可能有很多原因,可能是由于网络问题、服务器问题、文件问题等。在你的代码中,下载失败的具体原因应该已经被打印出来了。你可以查看打印的错误信息来确定具体原因。

1. 网络问题:如果是网络问题,可能的原因包括网络连接不稳定、网络速度太慢、DNS 解析失败等。对于这种问题,你可以尝试增加下载超时时间,或者在下载失败后尝试重新下载。

2. 服务器问题:如果是服务器问题,可能的原因包括服务器崩溃、服务器过载、服务器阻止了你的请求等。对于这种问题,你可能需要联系服务器的管理员,或者尝试在不同的时间再次下载。

3. 文件问题:如果是文件问题,可能的原因包括文件不存在、文件被锁定、文件权限问题等。对于这种问题,你需要检查下载链接是否正确,或者检查你的程序是否有权限下载和保存这些文件。

此外,你的线程数量是100,这可能会对你的系统或者网络环境产生很大的压力。你可以尝试减少线程数量,看看是否能够改善下载失败的问题。

如果上述方法还不能解决问题,你可能需要更详细地调查和测试你的代码,以确定问题的具体原因。你也可以考虑使用其他的下载工具或者库,比如  wget 、 curl  或者  aiohttp  等,看看是否能够改善下载失败的问题。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-5-10 23:00:27 | 显示全部楼层
isdkz 发表于 2023-5-10 22:29
下载失败可能有很多原因,可能是由于网络问题、服务器问题、文件问题等。在你的代码中,下载失败的具体原 ...

如何面板增加个输入框 输入地址
在下载地址前面拼接
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-10 23:07:46 | 显示全部楼层    本楼为最佳答案   
koma610630 发表于 2023-5-10 23:00
如何面板增加个输入框 输入地址
在下载地址前面拼接

你可以通过添加一个新的标签和文本框来实现这个功能。首先,你需要在窗口中添加一个新的标签和文本框。然后,你可以在下载函数中获取文本框中的值,并将其添加到下载地址的前面。

下面是如何修改代码来实现这个功能:
# 定义下载函数
def download(max_threads):
    # 获取输入框中的地址
    base_url = url_entry.get()
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 创建锁对象
    lock = threading.Lock()
    # 创建线程池
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # 遍历下载地址并下载文件
        for download_url in data:
            # 在下载地址前面添加输入框中的地址
            download_url = base_url + download_url
            # 获取下载文件的目录
            directory = os.path.join(BASE_DIR, *download_url.split('/')[1:-1])
            # 提交任务到线程池
            executor.submit(download_file, download_url, directory, lock)

# 创建窗口
window = tk.Tk()
window.title('批量下载')
window.geometry('400x250')

# 创建选择文件按钮
file_path_var = tk.StringVar()
file_path_label = tk.Label(window, textvariable=file_path_var)
file_path_label.pack()
choose_file_button = tk.Button(window, text='选择文件', command=choose_file)
choose_file_button.pack()

# 创建线程数量标签和文本框
thread_label = tk.Label(window, text='线程数量:')
thread_label.pack()
thread_entry = tk.Entry(window)
thread_entry.pack()

# 创建下载地址标签和文本框
url_label = tk.Label(window, text='下载地址:')
url_label.pack()
url_entry = tk.Entry(window)
url_entry.pack()

# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: threading.Thread(target=download, args=(int(thread_entry.get()),)).start())
download_button.pack()

# 运行窗口
window.mainloop()

这样,你就可以在新的文本框中输入下载地址,点击“下载”按钮时,程序会获取文本框中的地址,并将其添加到下载地址的前面。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2023-5-11 12:57:05 | 显示全部楼层
isdkz 发表于 2023-5-10 23:07
你可以通过添加一个新的标签和文本框来实现这个功能。首先,你需要在窗口中添加一个新的标签和文本框。然 ...

如何在窗口上显示 当前正在下载的文件名  跟显示总任务下载进度呢
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-5-11 16:12:15 | 显示全部楼层
koma610630 发表于 2023-5-11 12:57
如何在窗口上显示 当前正在下载的文件名  跟显示总任务下载进度呢

要在窗口上显示当前正在下载的文件名,你可以创建一个新的标签,并在下载文件时更新标签的文本。要显示总任务下载进度,你可以创建一个新的进度条,并在每次下载文件后更新进度条的值。

请注意,由于下载文件的操作是在子线程中进行的,而更新 GUI 的操作必须在主线程中进行,所以你需要使用 tkinter 的  StringVar  和  IntVar  类型来在子线程中更新 GUI。

下面是如何修改代码来实现这个功能:
import tkinter.ttk as ttk

# 定义下载函数
def download_file(download_url, directory, filename_var, progress_var, lock):
    # 获取下载文件的文件名
    filename = os.path.basename(download_url)
    print('正在下载文件:', filename)
    # 更新正在下载的文件名
    filename_var.set('正在下载文件:' + filename)
    # 创建目录
    os.makedirs(directory, exist_ok=True)
    # 下载文件
    response = requests.get(download_url, timeout=10)
    with open(os.path.join(directory, filename), 'wb') as f:
        f.write(response.content)
    # 更新下载进度
    with lock:
        progress_var.set(progress_var.get() + 1)

# 定义下载函数
def download(max_threads):
    # 获取输入框中的地址
    base_url = url_entry.get()
    # 读取本地json文件
    with open(file_path_var.get(), 'r') as f:
        data = json.load(f)
    # 设置下载进度的最大值
    progress_var.set(0)
    progress_bar['maximum'] = len(data)
    # 创建锁对象
    lock = threading.Lock()
    # 创建线程池
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # 遍历下载地址并下载文件
        for download_url in data:
            # 在下载地址前面添加输入框中的地址
            download_url = base_url + download_url
            # 获取下载文件的目录
            directory = os.path.join(BASE_DIR, *download_url.split('/')[1:-1])
            # 提交任务到线程池
            executor.submit(download_file, download_url, directory, filename_var, progress_var, lock)

# 创建窗口
window = tk.Tk()
window.title('批量下载')
window.geometry('400x250')

# 创建选择文件按钮
file_path_var = tk.StringVar()
file_path_label = tk.Label(window, textvariable=file_path_var)
file_path_label.pack()
choose_file_button = tk.Button(window, text='选择文件', command=choose_file)
choose_file_button.pack()

# 创建线程数量标签和文本框
thread_label = tk.Label(window, text='线程数量:')
thread_label.pack()
thread_entry = tk.Entry(window)
thread_entry.pack()

# 创建下载地址标签和文本框
url_label = tk.Label(window, text='下载地址:')
url_label.pack()
url_entry = tk.Entry(window)
url_entry.pack()

# 创建显示当前正在下载的文件名标签
filename_var = tk.StringVar()
filename_label = tk.Label(window, textvariable=filename_var)
filename_label.pack()

# 创建显示下载进度的进度条
progress_var = tk.IntVar()
progress_bar = ttk.Progressbar(window, length=200, mode='determinate', variable=progress_var)
progress_bar.pack()

# 创建下载按钮
download_button = tk.Button(window, text='下载', command=lambda: threading.Thread(target=download, args=(int(thread_entry.get()),)).start())
download_button.pack()

# 运行窗口
window.mainloop()

这样,你就可以在新的标签上看到当前正在下载的文件名,而新的进度条会显示总任务下载进度。

请注意,在这个例子中,我们假设  download_file  函数在文件下载完毕后立即返回。如果你的  download_file  函数有其他的阻塞操作,那么进度条和正在下载的文件名的更新可能会延迟。如果你希望更精确地控制进度条的更新,你可能需要对  download_file  函数做更多的修改。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-9-23 07:25

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表